Practice Session & Exercise

Introduction to R and Basic Programming Concepts

Author
Affiliation

Bioinformatics Core Facility CECAD

Published

May 21, 2026

1 Practice by Sessions

1.1 Session 1

1.1.1 Exercise 1:

  • Use R to calculate the following:
    • multiply 31 and 78
    • divide 697 with 41
  • Assign the value of 39 to x
  • Assign the value of 22 to y

1.2 Session 2

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")

1.2.1 Exercise 1: Create a Matrix

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?  

1.2.2 Exercise 2: Data Type of a Matrix

What is the data type of M? Do you have an idea why actually two types are listed?  

1.2.3 Exercise 3: Inspect the Matrix

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. 

1.2.4 Exercise 4a: Row Means by Hand

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!  

1.2.5 Exercise 4b: Row (and Column) Means Computationally

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.

1.2.6 Exercise 5: Type Conversion (Not Always Intended)

Try to cbind() all 3 vectors C1, C2, and G. 
It will work, but something goes wrong. Do you observe something unexpected?  

1.2.7 Exercise 6: Make a List

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.

1.2.8 Exercise 7: Make a Dataframe

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.  

1.2.9 Exercise 8a: Inspect the Dataframe

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?

1.2.10 Exercise/ Question 8b: Why Can Dataframe Rows Not Be Vectors?

1.2.11 Exercise 9: Make a Factor

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().

1.3 Session 3

1.3.1 Exercise 1: R Control Flow (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:

  1. Prints “Critical Overexpression” if p53_expr is strictly greater than 10.
  2. Prints “Normal Expression” if p53_expr is between 3 and 10 (inclusive).
  3. Prints “Underexpressed” if p53_expr is less than 3.

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).

1.3.2 Exercise 2: R Loops (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")

  1. If a sequence matches unknown character, print “Contamination detected! Halting pipeline.” and exit the loop immediately.
  2. If a sequence has fewer than 4 characters, skip it , print the message: "Skipping short primer: [sequence_here]" and continue to the next iteration.
  3. For all valid sequences, print the message: "Processing primer: [sequence_here]".

1.3.3 Exercise 3: R Loops (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():

  1. Initialize two variables: population <- 150 and hours <- 0.
  2. Write a while loop that continues running as long as population is less than 20000.
  3. Inside the loop, increment the hours counter by 1 and double the population.
  4. After the loop terminates, print a final summary sentence showing the total hours and ending population.

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.

1.3.4 Exercise 4: apply() family function

You 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")
  1. Use a single apply() function call to compute the median concentration for each row (per tissue sample).
  2. Use a second apply() function call to compute the standard deviation (sd) for each column (per metabolite type).

1.4 Session 4

1.4.1 Exercise 1: CSV I/O & the factor gotcha

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)
)
  1. Write expr to a file expr_de.csv using write.csv2() (German-style: ; separator, , decimal). Inspect the raw file content with readLines("expr_de.csv").
  2. Read the file back with read.csv2() into an object expr_back. Check class(expr_back$gene) — is it character or factor?
  3. Re-read the file passing stringsAsFactors = FALSE (or convert with as.character()) so that gene and condition stay as character vectors.
  4. Compare the two data frames with 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?

1.4.2 Exercise 2: Binary formats — saveRDS vs save

Estimated 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")
)
  1. Save deg_results to deg.rds with saveRDS(). Reload it under a different name, deg2 <- readRDS("deg.rds"). Verify with identical(deg_results, deg2).
  2. Save both 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().
  3. Reload them with load("session.RData"). Notice they reappear under their original names — you do not assign the result of load().
  4. Explain in one sentence: when would you prefer .rds over .RData?

1.4.3 Exercise 3: Reshaping a time-course experiment

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)
)
  1. Reshape 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.
  2. Inspect long — how many rows does it have? Why?
  3. Reshape long back to wide format and store it in wide_back.
  4. Compare with 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?

1.4.4 Exercise 4: Combining tables — merge, cbind, rbind

Estimated 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)
)
  1. Merge counts and meta by sample_id using merge(). Store the result in full. How many rows does it have?
  2. Now duplicate the first row of meta (meta_bad <- rbind(meta, meta[1, ])) and merge again. How many rows do you get now? Explain the Cartesian product.
  3. Use 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?
  4. Build a second batch of two samples (s5, s6) with the same columns as counts, then rbind() it onto counts. Verify the result has 6 rows.

1.5 Session 5

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!

1.5.1 Exercise 1:

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.

1.5.2 Exercise 2: Label Extreme Points

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.

1.6 Session 6

1.6.1 Exercise 1: Measures of Central Tendency (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)
  1. Calculate the arithmetic mean and the median of the raw data.
  2. Recalculate the mean and median excluding the outlier value of 450 (Hint: you can index it out or use protein_levels[protein_levels < 100]).
  3. Look at how much each metric shifted. Which measure of central tendency proved robust against the extreme outlier

1.6.2 Exercise 2: Measures of Variability (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)
  1. Calculate the standard deviation (sd()) and the variance (var()) of root_m.
  2. Create a new vector called root_cm by multiplying root_m by 100.
  3. Calculate the standard deviation and variance of root_cm.
  4. Observe the mathematical changes: By what scaling factor did the standard deviation increase? By what factor did the variance increase?

1.6.3 Exercise 3: Contingency Tables (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")
)
  1. Generate a 2D contingency table of counts using fly_table <- table(flies$eyes, flies$wings). Print it out.
  2. Convert this count table into conditional relative frequencies based on the row totals (Hint: look at the margin argument inside prop.table()). What percentage of Red-eyed flies have Normal wings?
  3. Use addmargins(fly_table) to quickly add row and column sums to your original count table.

1.6.4 Exercise 4: Correlation Coefficients (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)
  1. Use cor(temp, oxygen) to calculate the basic Pearson correlation coefficient. Is the linear relationship positive or negative?
  2. Use 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?

2 Practice across Sessions

2.1 Exercise 1: Environmental Monitoring Pipeline

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

  • Save the built-in R dataset to a temporary local CSV file on your hard drive using write.csv().
  • Read that exact file back into a new R variable named raw_env using read.csv().
  • Extract only rows matching the summer months of July and August into a filtered data frame called summer_env.

Step 2: Loop Execution & Custom Control Flow

  • Write a 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:
    • If Temp is strictly greater than 90°F, print: “Extreme Heat Detected ([Temp] degrees)”.
    • If Temp is between 80°F and 90°F (inclusive), print: “Moderate Warning”.
    • For all other temperatures, skip processing entirely using the next statement without printing anything.

Step 3: High-Throughput Metrics via the apply Family

  • Use apply family function to find the min, maximum and standard deviation for Temp from the summer_env

Step 4: Multi-Panel Visualizations with Base R Plots

  • Configure your R graphics engine using par(mfrow = c(1, 2)) to arrange your upcoming plots side-by-side in a 1-row, 2-column grid.
  • Plot 1: Draw a shaded hist() histogram of summer_env$Temp. Color it "coral", add clear axis text, and label the title "Summer Temperature Profile".
  • Plot 2: Draw a 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).
  • Reset your plotting layout back to standard single-panel parameters using 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).

  1. In RStudio, create a new document: File > New File > Quarto Document… (Select HTML output).
  2. ipe out the default placeholder text and paste the complete solution script into clean execution code chunks.
  3. Add professional Markdown headers (## Step 1…) between chunks to describe your analysis steps.
  4. Click the Render (or Knit) button at the top of your screen to run the entire pipeline and generate your final report.html file automatically.

2.2 Exercise 2: Mini differential-expression pipeline

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.

2.2.1 Phase A — Read & Write

  1. Save 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.)
  2. Compute a per-gene log2 fold change between the 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?

2.2.2 Phase B — Classify & Summarise

  1. Classify each gene using ifelse() into one of three categories, stored in expr$direction:
    • "up" if log2FC > 0.5
    • "down" if log2FC < -0.5
    • "flat" otherwise
    Then run barplot(table(expr$direction)) to verify you have genes in all three groups.
  2. Summarise the groups two different ways so you can compare imperative vs functional R:
    1. Write a for loop that, for each direction in c("up", "down", "flat"), prints the number of genes and the mean log2FC.
    2. Reproduce the same summary in a single call using tapply() (or sapply() + subsetting). Confirm the numbers match.

2.2.3 Phase C — Reshape, Merge, Order

  1. Reshape the four sample columns of expr from wide to long with reshape(), producing a data frame long with columns gene, sample_id, expression.
  2. Merge 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?
  3. Order the merged table by abs(log2FC) descending (most strongly affected genes first), then by group. This is the canonical ordering in differential-expression output.

2.2.4 Phase D — Save, Round-trip, Visualise

  1. Save the final ordered table both as 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?).
  2. Peek at Session 5: use 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?