Lecture 6: Data Structures, Strings, Missing Data & Functions
2026-10-14
Before new structures, ten minutes on last week’s ground: syntax, variables, types, math, strings, operators, control flow.
⏱️ No Notes, No Peeking
Three rapid-fire rounds, then one warm-up in live R. Write your answers first — reading the solution twice teaches you nothing.
🔁 Review: Lecture 5
No peeking!
print("hello) and press Enter. What happens?150 in a variable called x?1genecell countgene_AGene_A🔁 Review: Lecture 5
+ prompt, waiting for the rest of the string — it stays stuck until every quote and bracket matchesx <- 1501gene and cell count → syntax errors, R stops. Gene_A → legal but dangerous: a different variable from gene_A. gene_A → safe🔁 Review: Lecture 5
No peeking!
150? What type is "EGFR"?10 %% 3 versus 10 %/% 3?read.csv("data\visium\counts.csv") crashes. Why, and what is the rule?🔁 Review: Lecture 5
150 is numeric. "EGFR" is character — the quotes make it text, so it can never be counted or averaged1 and 3: %% returns the remainder, %/% floor-divides\ is the escape character, so \v and \c are invalid codes. The rule: always use forward slashes / in R paths🔁 Review: Lecture 5
No peeking!
if tested one condition and it came back FALSE. How do you check a second condition?500 <= x <= 2500.for loop, how do next and break differ?🔁 Review: Lecture 5
else if — it runs only when the condition above it was FALSE, and the first TRUE branch wins(x >= 500) & (x <= 2500). R has no chained comparisonsnext skips the rest of the current iteration and keeps going; break aborts the loop entirely🔁 Review: Lecture 5
<-, namingNA / NaN / Inf%%, %/%, log2, CPMcat(), escapes==, &, |, !, no chainingif / else, for, next, break, whileEverything from here builds on top of this.
Song, Y.-A., Ma, T., Zhang, X.-Y., Cheng, X.-S., Olajuyin, A.-M., Sun, Z.-F., & Zhang, X.-J. (2019). Apatinib preferentially inhibits PC9 gefitinib-resistant cancer cells by inducing cell cycle arrest and inhibiting VEGFR signaling pathway. Cancer Cell International, 19(1), 117. https://doi.org/10.1186/s12935-019-0836-8
2 cell lines \(\times\) 4 groups \(\times\) 3 replicates each = 12 samples.
Your Turn!
Which comparison answers the paper’s question, and which is the easy control?
Which genes change when apatinib hits resistant cells — and do the same genes change in the sensitive ones?
🎯 Today Is Plumbing
The whole lecture is about getting this one object into R correctly — structures, strings, NA, apply, functions, merging.
Let’s make a folder for the data.
💡 Always -o, Always Quote
GEO’s content-disposition header makes -O unreliable here, so name the file yourself.
That was two lines — and one was a 136-character URL.
By hand
By script
./download.sh♻️ Six Months From Now
A script is a text file anyone can read. A shell history is not.
#!/usr/bin/env bash
set -euo pipefail
OUT_DIR="./counts"
mkdir -p "$OUT_DIR"
curl -o "$OUT_DIR/counts.tsv.gz" \
"https://www.ncbi.nlm.nih.gov/geo/download/?type=rnaseq_counts&acc=GSE129221&format=file&file=GSE129221_raw_counts_GRCh38.p13_NCBI.tsv.gz"
gunzip -f "$OUT_DIR/counts.tsv.gz"
echo "File downloaded at $OUT_DIR/counts.tsv:"
ls -lh "$OUT_DIR"
echo "Head of the file:"
head -n 5 "$OUT_DIR/counts.tsv"🧊 set -euo pipefail in One Line
Stop on the first error · fail on unset variables · fail on any failed pipe. Without it, a broken download scrolls past and you analyse an HTML error page as counts.
# counts/
Raw RNA-seq data for **[GSE129221](https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=GSE129221)** (Song et al. 2019, Cancer Cell Int 19:117).
| File | Source | Notes |
|---|---|---|
| `counts.tsv` | GEO (via download.sh) | 39,376 genes x 12 samples, raw counts |
Run `./download.sh` to fetch it📝 Data Provenance
A README beside your data answers what is this, where did it come from, and is it complete without opening a browser tab.
You have the count matrix. It is not yet useful information:
100287102 tells you nothing1. R Data Structures
2. Reading, Writing & Merging Files
3. Vectorization & the apply Family
4. Strings & Regular Expressions
5. Cleaning Missing Data (NA)
6. Custom Functions
7. Practice Exercises
1. R Data Structures
Source: geeksforgeeks.org
🎯 What You Will Actually Use
Vectors and data frames every single day. Matrices for count matrices and images. Lists hide behind read.csv(), model fits and packages. Factors for every categorical annotation.
1. R Data Structures
A vector is a 1D chain of values that all share the same type.
1. R Data Structures
A plain sequence:
and a custom one
Or, repetition:
Note
Once a vector exists, every function in R expects one. mean(), sum(), length() all take vectors.
1. R Data Structures
Counting Starts at 1
🧬 Bio Example
gene_reads[gene_reads > 500] keeps only the highly expressed genes — the R version of filtering a column in Excel. Index 0 does not exist, so the first element is [1], never [0].
1. R Data Structures
A matrix is a 2D grid where every cell holds the same type. Spot metadata matrices are exactly this shape.
⚠️ Fills by Column
Values fill down columns by default. Add byrow = TRUE to fill row-by-row.
1. R Data Structures
Tip
Labelled matrices print like a real annotation table — always name them before you show anyone your numbers.
1. R Data Structures
🖼️ Bio Example
Arrays are 3D+ data. Histology H&E images (height × width × RGB channels) are stored this way.
1. R Data Structures
Elements can have completely different types and lengths
💡 [[ ]] vs [ ]
[[ ]] extracts the actual value. [ ] extracts a sublist holding the value. Always use [[ ]] (or $) to compute with the data.
1. R Data Structures
A data frame is a 2D table where each column keeps its own type. This is your spot-metadata table.
💡 Columns are Vectors
Each column is a vector wearing a name. That is why you can do spots$umi_count > 10 and get a logical vector back.
1. R Data Structures
🧭 The Five-Line Tour
Run head(), dim(), str() on every dataset you meet. Ninety seconds here saves an hour of confused errors later.
1. R Data Structures
📐 The Comma Means “and”
df[rows, columns] — think of it as df[what rows I want, what columns I want]. Leave the row part empty (df[, 1]) for “all rows”; leave the column part empty (df[1, ]) for “all columns”.
1. R Data Structures · Extra
A factor stores category labels plus the list of allowed levels — the R way to hold annotation columns.
💡 Levels Order
Levels are alphabetical by default. Specify order with factor(x, levels = c(...)) before plotting or modelling.
1. R Data Structures · Extra
A tibble (tbl_df) is an enhanced modern data frame from the tidyverse.
[ , 1].📦 Tibble vs Data Frame
Base R functions always accept data frames; tibble objects are 100% compatible and inherit from data.frame.
1. R Data Structures · Extra
1. R Data Structures · Exercise
Four genes, three samples:
| gene_id | S1 | S2 | S3 |
|---|---|---|---|
| gene_A | 2 | 3 | 1 |
| gene_B | 0 | 45 | 12 |
| gene_C | 310 | 85 | 0 |
| gene_D | 89 | 12 | 640 |
Two data types live here: gene IDs (text) and read counts (numbers).
1. R Data Structures · Exercise
e) Data frame — the only structure that is both rectangular and lets each column keep its own type.
| Structure | Why it fails |
|---|---|
| a) Vector | One long line. No rows, no columns, no way to ask for “S1”. |
| b) Matrix | One type for everything. The gene_id column is text. |
| c) Array | Same one-type rule, plus a third dimension this table does not have. |
| d) List | Holds anything, but has no row/column order — tbl[, 1] means nothing. |
1. R Data Structures · Exercise
counts?What does dim(counts) give you?
3 44 31 34 41. R Data Structures · Exercise
counts?b) 4 3
dim() is always rows, then columns.
🧬 Rows Are the Genes
The gene IDs became row labels, not a column — that is what row.names = 1 did. Without it you would have 4 columns and 4 4.
1. R Data Structures · Exercise
You want the read counts for S1 only, as a vector.
counts[1, ]counts$S1counts[S1]counts["S1"]1. R Data Structures · Exercise
b) counts$S1 — $ pulls the column out by name and hands you the bare values.
| Option | Why not |
|---|---|
a) counts[1, ] |
one gene (gene_A), all 3 samples — the wrong way round |
c) counts[S1] |
Error: object 'S1' not found |
d) counts["S1"] |
a 4×1 data frame — one column, wrapped in a table, not a vector |
🧬 Want the Vector? Use [[ ]]
counts[[1]] and counts[["S1"]] both unwrap to the same thing: 2 0 310 89.
1. R Data Structures
2. Reading, Writing & Merging Files
3. Vectorization & the apply Family
4. Strings & Regular Expressions
5. Cleaning Missing Data (NA)
6. Custom Functions
7. Practice Exercises
2. Reading, Writing & Merging Files
🧬 Never Hard-Code Absolute Paths
/Users/phongdinh/... works on your laptop and on nobody else’s — every hard-coded path is a reproducibility bug waiting for a co-author or reviewer.
2. Reading, Writing & Merging Files
One path, two folders. Walk into a subdirectory and the same line flips its answer:
From the repo root:
Now step one level deeper — nothing about f changes:
🧬 The File Did Not Move
counts/counts.tsv is right where it was. Only getwd() changed — and that is enough to make R look somewhere else entirely.
2. Reading, Writing & Merging Files
here() builds the path from the project root, so the answer stops depending on where you stand:
📦 Install with install.packages("here")
Inside a script, here::i_am("analysis/code/demo.R") pins the root explicitly.
2. Reading, Writing & Merging Files
The file you downloaded in the first half of this lecture. It is tab-separated, so read.delim() is the right call:
🔑 Why row.names = 1 on the Way In
The first column holds Entrez gene IDs. With row.names = 1 that column becomes the labels you address genes by. Without it you get 13 columns, and the IDs sit there as ordinary data.
2. Reading, Writing & Merging Files · Extra
All read.* functions wrap read.table(), but differ in default parameters:
| Function | sep |
header |
dec |
Use Case |
|---|---|---|---|---|
read.delim() |
\t (Tab) |
TRUE |
. |
TSV count matrices & annotations |
read.csv() |
, (Comma) |
TRUE |
. |
Standard CSV files (US/UK) |
read.table() |
"" (Space) |
FALSE |
. |
Space-separated raw tables |
read.delim2() / csv2() |
\t / ; |
TRUE |
, |
European decimal comma format |
⚡ Large Datasets (>100 MB)
Base R functions load into memory as standard data frames. For huge single-cell matrices, data.table::fread() or readr::read_tsv() are significantly faster.
2. Reading, Writing & Merging Files
Export a subset to disk with write.csv():
⚠️ Keep row.names = TRUE (Default)
If set to FALSE, row names (gene IDs) are dropped, leaving numbers without gene identifiers!
2. Reading, Writing & Merging Files
Look at the column headers of your count matrix:
GSM...) tell you nothing about the biology.2. Reading, Writing & Merging Files
Build the lookup mapping and save it to disk with write.csv():
sample_info <- data.frame(
sample_id = colnames(counts),
cell_line = rep(c("PC9", "PC9GR"), each = 6),
treatment = rep(c("Control", "Apatinib", "Control", "Apatinib"), each = 3)
)
# Save sample metadata table for your project:
write.csv(sample_info, "sample_metadata.csv", row.names = FALSE)
head(sample_info, 4)2. Reading, Writing & Merging Files
Calculate total reads per sample, then link them with merge():
🔑 merge(x, y, by = "key")
Base R’s merge() aligns rows automatically using a shared ID key — no manual index matching needed!
2. Reading, Writing & Merging Files
⚡ Rule of Thumb
Under ~100 MB: read.csv() is fine. Above that: data.table::fread(). You will meet the threshold the day you load your first gene-by-spot matrix.
1. R Data Structures
2. Reading, Writing & Merging Files
3. Vectorization & the apply Family
4. Strings & Regular Expressions
5. Cleaning Missing Data (NA)
6. Custom Functions
7. Practice Exercises
3. Vectorization & the apply Family
How many genes read zero in every sample?
3. Vectorization & the apply Family
The same 39,376 numbers, in one call:
Don’t take my word for the speedup — run it. This uses the same shape as the real matrix (39,376 genes × 12 samples):
Your absolute numbers will be larger than mine — this is R compiled to WebAssembly, not native R. The ratio is the point: one trip, versus 39,376.
The rest of the family was already there:
🎯 The Rule
If a function already comes in families — mean(), sum(), rowMeans(), colSums() — reach for those first. apply() is for when no built-in exists.
3. Vectorization & the apply Family
📐 Margin = The Axis You Collapse
MARGIN = 1 → R goes down the columns, returning one value per rowMARGIN = 2 → R goes across the rows, returning one value per columnSay “down” for 1, “across” for 2, and you will never mix them up again.
3. Vectorization & the apply Family
apply() Is a Loop Wearing a Maskapply(X, 1, mean) slices out each row, calls mean() once per row, and pastes the answers back together. It is the for loop from two slides ago — with better manners.
At three rows you cannot see the cost. Scale up:
🕵️ When apply() Earns Its Place
Identical answers, wildly different cost. rowMeans() hands the whole grid to compiled C in one call; apply() walks it in 20,000.
So apply() is the tool for when no dedicated function exists — your own function, an unusual margin-dependent statistic. The moment a row*() / col*() twin exists, take it.
3. Vectorization & the apply Family
Note
lapply() never surprises you — the shape of the answer always matches the input shape. sapply() tries to be prettier and occasionally returns a matrix instead of a vector. When the shape matters, use lapply().
3. Vectorization & the apply Family
Tip
This single pattern replaces most of your pivot tables: group a numeric vector by a factor, summarise it. Differential expression at its core is tapply with a test function instead of mean.
3. Vectorization & the apply Family
| Function | You give it | You get back |
|---|---|---|
apply |
matrix / data frame + margin | vector or matrix |
lapply |
list / vector | list (always) |
sapply |
list / vector | simplified vector (usually) |
tapply |
vector + grouping factor | vector per group |
colMeans |
matrix | one number per column |
Tip
Work top to bottom: check the dedicated function, then apply, then lapply/sapply, then tapply.
1. R Data Structures
2. Reading, Writing & Merging Files
3. Vectorization & the apply Family
4. Strings & Regular Expressions
5. Cleaning Missing Data (NA)
6. Custom Functions
7. Practice Exercises
4. Strings & Regular Expressions
Your gene list looks like this:
You need the suffix stripped and the cases normalised — across 20,000 rows.
🛑 Why Not Find-and-Replace in Excel?
Because the labels are not identical: "ACTB-mouse", "Actb-mouse" and "ACTB" are three different strings. A pattern catches all of them; a literal search does not.
4. Strings & Regular Expressions
Tip
grep() gives you indices, so it plugs straight into subsetting: genes[grep("mouse", genes)]. Give it value = TRUE when you just want to eyeball the hits.
4. Strings & Regular Expressions
Note
sub() is the surgical tool, gsub() is the hammer. gsub("-", "", x) will strip every hyphen in a string — which is exactly why you should test on genes first, not on a production column.
4. Strings & Regular Expressions
Tip
Whitespace is invisible and deadly. A label with one trailing space will never match a database key. Run trimws() on every text column you import.
4. Strings & Regular Expressions
| Pattern | Matches | In gene labels |
|---|---|---|
^ |
start of string | ^mouse |
$ |
end of string | mouse$ |
. |
any single character | CD.8A |
[A-Z] |
one character from a set | [A-Z]+ |
+ |
one or more | mouse+ |
\\. |
a literal dot | \\.1 |
🧬 Naming Changes Are Real Biology
HGNC renamed MARCH1 and SEPT1 to MARCHF1 and SEPTIN1 precisely because Excel kept mangling the old names. Never assume a symbol’s current name — look it up.
1. R Data Structures
2. Reading, Writing & Merging Files
3. Vectorization & the apply Family
4. Strings & Regular Expressions
5. Cleaning Missing Data (NA)
6. Custom Functions
7. Practice Exercises
5. Cleaning Missing Data (NA)
Tip
Always count the missing values per column before deciding what to do with them. sum(is.na(x)) is the one line that tells you whether a dataset is trustworthy.
5. Cleaning Missing Data (NA)
🧬 Transcriptomic Connection
R propagates NA on purpose: one unknown value makes the result unknown. A single unmeasured spot must not silently become a zero. na.rm = TRUE is you explicitly accepting “drop the unknowns and compute on the rest”.
5. Cleaning Missing Data (NA)
Note
For a vector, 1 and 2 are identical. For a data frame, na.omit(df) deletes any row with an NA anywhere — which may delete far more than you intended.
5. Cleaning Missing Data (NA)
⚠️ Choosing Wisely
In sequencing, “not detected” and “not present” are different claims. Say which one you mean.
5. Cleaning Missing Data (NA)
Your Turn!
pass column that is TRUE when umi > 500.mito values with 0 in a copy of the table.5. Cleaning Missing Data (NA)
1. R Data Structures
2. Reading, Writing & Merging Files
3. Vectorization & the apply Family
4. Strings & Regular Expressions
5. Cleaning Missing Data (NA)
6. Custom Functions
7. Practice Exercises
6. Custom Functions
A function is a named recipe: inputs, a body, and a returned value.
Note
function(...) defines the recipe, calc_percentage(...) runs it. Inputs you name in the definition are the arguments; the values you pass are the arguments at the call.
6. Custom Functions
Write return() when the function has more than a couple of steps. Your future self — and your reviewers — will thank you.
6. Custom Functions
💡 stop() Is a Feature
A cryptic error ten lines later is expensive. A clear stop() at the door tells the analyst exactly what they passed in. This is defensive programming — write it today, thank yourself during review.
6. Custom Functions
Your Turn!
calc_cpm(reads, total) that returns counts per million.mito_pct column for spot_id, mito_reads = c(12, 4).sapply() to turn the CPM column into a numeric vector of values.6. Custom Functions
1. R Data Structures
2. Reading, Writing & Merging Files
3. Vectorization & the apply Family
4. Strings & Regular Expressions
5. Cleaning Missing Data (NA)
6. Custom Functions
7. Practice Exercises
7. Practice Exercises
A QC report came back from the pipeline as a list:
Your Turn!
umi_counts as a vector (not wrapped in a list).100 reads?7. Practice Exercises
Note
qc_report$umi_counts would have worked too — $ is shorthand for [[ when the name is known. Use [[ ]] when the name comes from a variable.
7. Practice Exercises
A gene expression matrix arrived with holes in it:
Your Turn!
NA with 0.apply().7. Practice Exercises
Tip
which.max() and which.min() return positions, not values — the same idea as grep(). They are the standard way to find the top gene or the worst spot.
7. Practice Exercises
Your Turn!
-mouse suffix.7. Practice Exercises
Note
Clean the case first, then the pattern — otherwise you need one regex per capitalisation. Order of operations matters in every cleaning pipeline.
[[ ]] unwraps a list element, [ ] wraps it againgrep() finds, sub()/gsub() replace, toupper()/trimws() normaliseNA propagates on purpose — count it, then remove it or na.rm = TRUEcolMeans(), apply(margin), lapply/sapply, tapplystop()melt() / dcast() to move between wide and long tablesplot() for a first look, ggplot2 for publication figures📓 Keep Practising
Finish 01b_R_Data_Manipulation.ipynb before next week — the exercises assume you have run every cell in this deck at least once.
“A data frame is just a list of vectors that all have the same length — once that clicks, the rest of R stops being mysterious.”
Fighting Cancer with Transcriptomics - Lecture 6