31 * 78[1] 2418
697 / 41[1] 17
x <- 39
x[1] 39
y <- 22
y[1] 22
Introduction to R and Basic Programming Concepts
Before You Start : Some Raw Material
Please copy the below code:
Assume you observed the expression of six genes under two conditions:
C1 <- c(g1=0, g2=1, g3=10, g4=6, g5=4, g6=23)
C2 <- c(g1=20, g2=11, g3=1, g4=7, g5=3, g6=5)In addition you have a grouping variable for the genes:
G <- c(g1="A",g2="A",g3="B", g4="C",g5="C",g6="B")Use the cbind() function (“column-wise bind”) to bind C1 and C2 into a matrix (if you are unsure, check ?cbind).
Assign the result to a variable named “M” and print it.
Can you get the number of rows and columns of M using R code?
What is the data type of M? Do you have an idea why actually two types are listed?
Extract the first column by index and by name. What is its data type?
Use the is.vector() function to confirm that it is a vector.
Extract the second row by index and by name. What is its data type?
Use the is.vector() function to confirm that it is a vector, too.
The operators “+” and “/” are vectorizing, that is, they can operate on entire vectors.
Get the mean expression of the 6 genes in the two conditions by adding C1 and C2, and then dividing the result by 2.
Assign the result to a variable named Cmean.
Hint: Put parentheses where needed, to ensure the correct order of computations!
R has a function rowMeans(), which does exactly the computation of Exercise 4a. Try it and compare to C1C2_mean of Exercise 4a, using function identical().
What do you think the pendant colMeans() does? Try it.
Try to cbind() all 3 vectors C1, C2, and G.
It will work, but something goes wrong. Do you observe something unexpected?
The list() function combines heterogeneous R objects into a common list object.
The names of the resulting list elements can be specified like so: list(my_name=my_object).
Use the function to create a list of C1, C2, and G,
with names “condition1”, “condition2”, and “group”.`
Assign the list object to a variable named “L” and print it.
A list is convenient because of its permissiveness: you can store (nearly) anything there. But it can also be cumbersome to handle.
List L has a special property: all of its elements are vectors of a fixed length (although of different data types).
Such lists can be transformed such that they look very similar to matrices, yet internally stay lists with all their benefits.
This matrix-like data structure is the dataframe (data type data.frame).
Convert L to a dataframe using as.data.frame(), assign the result to a variable named df, and print it.
A dataframe can be indexed like a matrix.
Extract the first column by index or by name, and use class() and is.vector() on it.
Then do the same with the second row. Do both dimensions behave in the same way?
Vector G (which is also the “group” column of df) is a grouping variable for the ratio of the count vectors C1 and C2:
C2 is higher than C1 in group “A” of G, lower than C1 in group “B”, and about the same in group “C”.
We can make it explicit that it is a grouping variable by coding it as a factor.
In a much larger, real dataset, such a grouping factor for genes could be used to statistically relate gene induction or repression to some other attribute of the genes, for example to pathway membership.
Here we simply practice to set up a factor variable:
Create an unordered factor with base level “C” from vector G and assign it to variable f1.
Print f1.
Create an ordered factor with levels c(“C”, “B”, “A”) from G and assign it to variable f2.
Print f2.
Create an unordered factor with base level “C” from vector df$group, and assign it back to df$group.
Column “group” should now be a factor. Check it using class().
if, if...else)You are analyzing the expression level of a target cancer gene ((p53)). You need to write a script that checks a scalar expression value and prints a diagnostic classification.
Task: Create a variable named p53_expr. Write an if...else if...else structure that:
Bonus Challenge: Test your code by converting the structure into a vectorized function using ifelse() to handle an entire vector of expressions: c(12, 5, 1, 8, 15).
for, next, break)You have a list of DNA primer sequences. You need to screen them sequentially. However, your laboratory pipeline must skip short sequences and completely halt if an invalid or contaminated sequence is detected.
Task: Write a for loop to iterate through primers and evaluate the text length using nchar():
primers <- c("ATGGC", "AT", "GTCGATC", "UNKNOWN", "CTGA","AYTGC")
"Skipping short primer: [sequence_here]" and continue to the next iteration."Processing primer: [sequence_here]".while or repeat)A bacterial culture starts with a population of 150 cells and doubles every hour. You need to determine exactly how many hours it takes for the colony to breach a critical biohazard threshold of 20,000 cells.
Task: Write a for loop to iterate through primers and evaluate the text length using nchar():
Bonus Challenge: Modify your loop to include a safety check that halts the process if it exceeds 24 hours, printing “Process halted: Time limit exceeded.” and breaking the loop. Plot the population growth over time using a simple line graph.
apply() family functionYou receive a 2D dataset tracking the concentrations of 4 different metabolites (columns) across 6 laboratory tissue samples (rows). You must extract the baseline variability for each metabolite.
Task: Copy this code to generate your matrix:
set.seed(42)
metabolite_matrix <- matrix(rnorm(24, mean = 5, sd = 2), nrow = 6)
colnames(metabolite_matrix) <- c("Glucose", "Lactate", "Pyruvate", "Alanine")apply() function call to compute the median concentration for each row (per tissue sample).apply() function call to compute the standard deviation (sd) for each column (per metabolite type).Estimated time: 20 minutes
A collaborator sends you a small gene expression table. The decimals are written with a comma and columns are separated by semicolons. You want to round-trip the file (write it, read it back) and make sure nothing changes silently — in particular, that character columns do not get converted to factors.
Task:
expr <- data.frame(
gene = c("TP53", "BRCA1", "MYC", "EGFR", "KRAS"),
condition = c("ctrl", "ctrl", "treat", "treat", "ctrl"),
log2FC = c(-0.42, 1.87, 2.31, -1.12, 0.05)
)expr to a file expr_de.csv using write.csv2() (German-style: ; separator, , decimal). Inspect the raw file content with readLines("expr_de.csv").read.csv2() into an object expr_back. Check class(expr_back$gene) — is it character or factor?stringsAsFactors = FALSE (or convert with as.character()) so that gene and condition stay as character vectors.identical(expr, expr_back) and all.equal(expr, expr_back). If they differ, explain why.Bonus Challenge: Write the same data with the default write.csv() and read it back with read.csv(). Which separator and decimal symbol does each pair use? When would the German variant matter in practice?
saveRDS vs saveEstimated time: 15 minutes
After a long overnight analysis you want to checkpoint your results so a colleague can reload them tomorrow without rerunning anything. R offers two binary formats: saveRDS() / readRDS() (one object, assigned on read) and save() / load() (multiple objects, restored under their original names).
Task:
deg_results <- data.frame(
gene = paste0("g", 1:6),
log2FC = c(1.2, -0.8, 2.5, -1.7, 0.3, 0.0),
padj = c(0.001, 0.04, 0.0001, 0.02, 0.6, 0.9)
)
sample_info <- data.frame(
sample_id = paste0("s", 1:4),
group = c("ctrl", "ctrl", "treat", "treat")
)deg_results to deg.rds with saveRDS(). Reload it under a different name, deg2 <- readRDS("deg.rds"). Verify with identical(deg_results, deg2).deg_results and sample_info together to session.RData with save(). Remove them from your workspace with rm(deg_results, sample_info) and confirm with ls().load("session.RData"). Notice they reappear under their original names — you do not assign the result of load()..rds over .RData?Estimated time: 25 minutes
You have measured the expression of three genes at four time points. Your raw table is in wide format (one column per time point), but the plotting function you want to use later expects long format (one row per measurement).
Task:
wide <- data.frame(
gene = c("TP53", "BRCA1", "MYC"),
t0 = c(1.0, 0.8, 1.2),
t1 = c(1.4, 0.9, 2.1),
t2 = c(2.1, 1.1, 3.5),
t3 = c(3.0, 1.3, 5.0)
)wide to long format with reshape() using gene as idvar, the four time columns as varying, v.names = "expression", timevar = "time", and direction = "long". Store it in long.long — how many rows does it have? Why?long back to wide format and store it in wide_back.all.equal(wide, wide_back). If attributes differ, try all.equal(wide, wide_back, check.attributes = FALSE).Bonus Challenge: Use stack() on a subset of the wide table (e.g. wide[, -1]) and compare the result with what reshape() produced. When is stack() simpler? When does it fall short?
merge, cbind, rbindEstimated time: 25 minutes
You have two tables from the same experiment: gene-level counts and sample metadata. They share a sample_id column. You want to attach metadata to counts so that each row of counts knows which group it belongs to.
Task:
counts <- data.frame(
sample_id = c("s1", "s2", "s3", "s4"),
TP53 = c(120, 95, 300, 280),
MYC = c(45, 60, 410, 390)
)
meta <- data.frame(
sample_id = c("s1", "s2", "s3", "s4"),
group = c("ctrl", "ctrl", "treat", "treat"),
batch = c(1, 1, 2, 2)
)counts and meta by sample_id using merge(). Store the result in full. How many rows does it have?meta (meta_bad <- rbind(meta, meta[1, ])) and merge again. How many rows do you get now? Explain the Cartesian product.cbind() to add a new column total <- counts$TP53 + counts$MYC to full. Why does cbind() work safely here but would fail if you tried to bind counts and meta directly?s5, s6) with the same columns as counts, then rbind() it onto counts. Verify the result has 6 rows.Before You Start
Please re-create species_tables, species_means, species_colors, and trait_colors as given at the beginning of Session5.R. Try to understand each step of the computations!
We want to study the distances between members of the versicolor and the setosa population, and compare it to the distances between members of the versicolor and the virginia population. Given the assumed genotypes (virginia 4n, setosa 2n, and the hybrid versicolor 4n+2n = 6n), versicolor and virginica should be closer to each other than versicolor and setosa, because they share more alleles. Here we do it for one flower trait at a time (assigned to variable “feature”).
Take the difference between the vectors species_tables$versicolor[[feature]] species_tables$setosa[[feature]] and assign it to variable “d_vs”.
Take the difference between the vectors species_tables$versicolor[[feature]] species_tables$virginica[[feature]] and assign it to variable “d_vv”.
We want to produce a plot which shows histograms of both difference vectors on the same canvas. See lines 347-388 in Session5.R for hints.
Copy the code of Slide 17/Session5.html / Session5.R/lines 216-234.
Try the following: (1) Identify the 3 highest y values. (2) find all points which reach these values (these can be more than the 3 unique highest values, if multiple points have the same value). (3) mark all points found with a fat red point.
Hints: (1) We need function sort() for sorting all values. The sort should be decreasing, such that the highest values are on top. Check ?sort to learn how a decreasing sort is selected. Then run unique on the sorted values, to remove repeated values. Finally assign the top 3 values of the sorted list to variable “top3”. (2) We saw the operator %in%, which is used as "x %in% y" and finds those points in vector x which are in vector y. Can you transfer this pattern to the task at hand? (3) Use the points() function to color the points red. Check ?points to learn about parameters for choosing symbol, color and expansion factor. Try different values of the cex parameter.
mean, median, summary)You are measuring the concentration of a signaling protein (pg/mL) in 10 wild-type mice. However, due to a pipetting error or an extreme biological anomaly, one sample has an massive outlier value.
Task
protein_levels <- c(22, 24, 21, 25, 23, 22, 26, 24, 22, 450)sd, var, range, IQR)An ecologist measures the root depth of a desert plant species in meters. For a publication layout, they need to convert all measurements to centimeters (multiply by 100).
Task
root_m <- c(1.2, 1.5, 0.8, 2.1, 1.4, 1.7)root_m.root_m by 100.root_cm.table, prop.table, addmargins)You are breeding fruit flies (Drosophila) to study classical genetics. You cross two flies and count the offspring phenotypes, categorizing them by eye color (Red vs. White) and wing type (Normal vs. Vestigial).
Task
flies <- data.frame(
eyes = c("Red", "Red", "White", "Red", "Red", "White", "Red", "White"),
wings = c("Normal", "Normal", "Vestigial", "Vestigial", "Normal", "Normal", "Normal", "Vestigial")
)fly_table <- table(flies$eyes, flies$wings). Print it out.prop.table()). What percentage of Red-eyed flies have Normal wings?addmargins(fly_table) to quickly add row and column sums to your original count table.cor, cor.test)A limnologist tracks water dissolved oxygen levels (mg/L) and water temperature (°C) in an alpine lake over several weeks to find if there is a stable relationship.
Task
temp <- c(4.2, 5.5, 7.1, 9.3, 12.1, 14.5, 16.2)
oxygen <- c(12.5, 11.8, 11.2, 10.1, 9.4, 8.7, 8.1)cor(temp, oxygen) to calculate the basic Pearson correlation coefficient. Is the linear relationship positive or negative?cor.test(temp, oxygen) to extract deeper inferential parameters. 3 Read the console output from step 2: What are the exact lower and upper bounds of the 95 percent confidence interval for this correlation coefficient?You are an environmental data scientist processing seasonal weather anomalies. You need to read raw files, reshape and clean data, apply programmatic thresholds, isolate metrics using the apply family, generate analytical base R plots, and automatically compile everything into a professional HTML report.
For this project, you will use the airquality dataset, which is built directly into R. It tracks daily air quality measurements in New York, including solar radiation, wind, temperature, and ozone levels—factors that heavily influence biological ecosystems.
Step 1: Data IO & Reshaping
write.csv().read.csv().summer_env.Step 2: Loop Execution & Custom Control Flow
for loop that iterates sequentially through every row of your summer_env data frame to check the Temp (Temperature) value for that day using an if...else if...else structure:
next statement without printing anything.Step 3: High-Throughput Metrics via the apply Family
apply family function to find the min, maximum and standard deviation for Temp from the summer_envStep 4: Multi-Panel Visualizations with Base R Plots
par(mfrow = c(1, 2)) to arrange your upcoming plots side-by-side in a 1-row, 2-column grid.hist() histogram of summer_env$Temp. Color it "coral", add clear axis text, and label the title "Summer Temperature Profile".plot() scatter plot showing Ozone on the y-axis and Solar.R (Solar Radiation) on the x-axis. Use solid circular indicators (pch = 16), color the dots "darkblue", and add a line of best fit through the plot utilizing abline(lm(Ozone ~ Solar.R, data = summer_env), col = "red", lwd = 2).par(mfrow = c(1, 1)).Step 5: Wrap it into an Automated HTML Report
To practice modern literate programming as covered in our very first discussion, compile your steps into an automatically generated standalone HTML report document using Quarto (or R Markdown).
Estimated time: 75 minutes (Phases A–C: ~45 min · Phase D + Bonus: ~30 min)
This exercise ties together everything from Sessions 1–6. You will save and reload a small gene-expression dataset, classify genes by their fold change (control flow), summarise the groups two different ways (loop vs functional style), reshape the per-sample measurements, merge in sample metadata, order by biological importance, round-trip the result through CSV and RDS to see how the two formats differ, and finally peek ahead at visualisation.
Setup — generate the two data frames in memory:
set.seed(2026)
genes <- paste0("g", 1:12)
# Per-gene baseline expression and treatment effect.
# Effects are a mix of up-regulation (+), down-regulation (-) and flat (~0)
# so the classification step later produces all three categories.
baseline <- round(rnorm(12, mean = 5, sd = 1), 2)
effect <- c( 3.0, 2.5, -2.2, -1.6, 0.1, -0.2,
2.8, -1.9, 0.3, 3.5, -2.0, 0.0)
noise <- function() round(rnorm(12, 0, 0.3), 2)
expr_wide <- data.frame(
gene = genes,
s1 = baseline + noise(), # ctrl
s2 = baseline + noise(), # ctrl
s3 = baseline + effect + noise(), # treat
s4 = baseline + effect + noise() # treat
)
meta <- data.frame(
sample_id = c("s1", "s2", "s3", "s4"),
group = c("ctrl", "ctrl", "treat", "treat")
)The two objects expr_wide and meta now live in your R session. From here on, you are responsible for getting them onto disk and back.
expr_wide and meta to disk as expr_wide.csv and meta.csv. Read them back into new objects expr and meta_in. (Since R 4.0, read.csv() keeps strings as character by default — no extra argument needed, but you may pass stringsAsFactors = FALSE for clarity.)treat samples (s3, s4) and the ctrl samples (s1, s2). Hint: log2(mean_treat / mean_ctrl). Store it as a new column log2FC in expr. Then run hist(expr$log2FC) — does the distribution suggest where the ±0.5 threshold should sit?ifelse() into one of three categories, stored in expr$direction:
"up" if log2FC > 0.5"down" if log2FC < -0.5"flat" otherwisebarplot(table(expr$direction)) to verify you have genes in all three groups.for loop that, for each direction in c("up", "down", "flat"), prints the number of genes and the mean log2FC.tapply() (or sapply() + subsetting). Confirm the numbers match.expr from wide to long with reshape(), producing a data frame long with columns gene, sample_id, expression.long with meta_in by sample_id so each row also knows its group. Then merge in log2FC and direction from expr so every row carries its gene-level annotation. Run boxplot(expression ~ group, data = long) as a sanity check — can you already see the treatment effect?abs(log2FC) descending (most strongly affected genes first), then by group. This is the canonical ordering in differential-expression output.results.csv (text) and results.rds (binary). Read both back and compare each reload to the original using identical() and all.equal(). Which format round-trips exactly? Where does the other one differ (row order? row names? numeric precision? column types?).stripchart(expression ~ factor(direction, levels = c("down", "flat", "up")), data = long[long$group == "treat", ], vertical = TRUE, method = "jitter", pch = 19, col = c("tomato", "grey60", "steelblue"), main = "Treated samples: expression by direction") to see the individual data points for each gene category. Do the up and down genes separate cleanly from the flat ones?Bonus Challenge: Use aggregate() to compute the standard deviation of expression per (gene, group) pair on the long table. Which gene is the most variable inside the treat group? Does its direction (up/down/flat) explain that variability, or is something else going on?