Skip to content

Commit ea083fd

Browse files
authored
Merge pull request #158 from torres-alexis/dge_stats
- fixes DGE group stats calculation - Update pipeline docs with DGE group stats calculation fix - fix arrows in workflow images
2 parents 4bc0dc5 + f8dc1b0 commit ea083fd

33 files changed

Lines changed: 1701 additions & 1242 deletions

RNAseq/Pipeline_GL-DPPD-7101_Versions/GL-DPPD-7101-G.md

Lines changed: 140 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
---
66

7-
**Date:** April 10, 2025
7+
**Date:** February 20, 2025
88
**Revision:** G
99
**Document Number:** GL-DPPD-7101-G
1010

@@ -17,7 +17,6 @@ Barbara Novak (GeneLab Data Processing Lead)
1717
Amanda Saravia-Butler (GeneLab Science Lead)
1818
Samrawit Gebre (OSDR Project Manager)
1919
Danielle Lopez (OSDR Deputy Project Manager)
20-
Lauren Sanders (OSDR Project Scientist)
2120

2221
---
2322

@@ -1182,7 +1181,10 @@ library(DESeq2)
11821181
library(BiocParallel)
11831182

11841183
### Define a system-specific BiocParallelParam object to run DGE R script functions in parallel when applicable ###
1185-
BPPARAM <- SerialParam()
1184+
BPPARAM <- SerialParam(RNGseed = 7)
1185+
1186+
### Set random seed for reproducibility ###
1187+
set.seed(7)
11861188

11871189
### Define which organism is used in the study - this should be consistent with the species name in the "species" column of the GL-DPPD-7110-A_annotations.csv file ###
11881190
organism <- "organism_that_samples_were_derived_from"
@@ -1210,7 +1212,7 @@ setwd(file.path(work_dir))
12101212

12111213
* {GLDS-Accession-ID}_bulkRNASeq_v{version}_runsheet.csv (runsheet, output from [Step 9a](#9a-create-sample-runsheet))
12121214
* `organism` (name of organism samples were derived from, found in the species column of [GL-DPPD-7110-A_annotations.csv](../../GeneLab_Reference_Annotations/Pipeline_GL-DPPD-7110_Versions/GL-DPPD-7110-A/GL-DPPD-7110-A_annotations.csv) file)
1213-
- `BPPARAM` (system-specific BiocParallelParam object for parallel processing configuration, by default this is set to `SerialParam()`)
1215+
- `BPPARAM` (system-specific BiocParallelParam object for parallel processing configuration, by default this is set to `SerialParam(RNGseed = 7)`)
12141216

12151217
**Output Data:**
12161218

@@ -1230,18 +1232,31 @@ compare_csv_from_runsheet <- function(runsheet_path) {
12301232
factors <- df %>%
12311233
select(matches("Factor.Value", ignore.case = TRUE)) %>%
12321234
rename_with(~ paste0("factor_", seq_along(.)))
1233-
result <- df %>%
1234-
select(sample_id = Sample.Name) %>%
1235-
bind_cols(factors)
1235+
1236+
# Check if both Source.Name and Has.Tech.Reps columns exist
1237+
if ("Source.Name" %in% colnames(df) && "Has.Tech.Reps" %in% colnames(df)) {
1238+
result <- df %>%
1239+
select(Sample.Name, Source.Name, Has.Tech.Reps) %>%
1240+
bind_cols(factors)
1241+
} else {
1242+
result <- df %>%
1243+
select(Sample.Name) %>%
1244+
bind_cols(factors)
1245+
}
1246+
12361247
return(result)
12371248
}
12381249

12391250
### Load metadata from runsheet csv file ###
12401251
compare_csv <- compare_csv_from_runsheet(runsheet_path)
12411252

12421253
### Create data frame containing all samples and respective factors ###
1243-
study <- compare_csv[, -1, drop=FALSE] # Exclude sample_id
1244-
rownames(study) <- compare_csv$sample_id
1254+
study <- if ("Source.Name" %in% colnames(compare_csv) && "Has.Tech.Reps" %in% colnames(compare_csv)) {
1255+
compare_csv[, -c(1, 2, 3), drop=FALSE] # Exclude Sample.Name, Source.Name, and Has.Tech.Reps
1256+
} else {
1257+
compare_csv[, -1, drop=FALSE] # Exclude only Sample.Name
1258+
}
1259+
rownames(study) <- compare_csv$Sample.Name
12451260

12461261
### Format groups and indicate the group that each sample belongs to ###
12471262
group <- if (ncol(study) >= 2) {
@@ -1257,7 +1272,7 @@ rm(group_names)
12571272
### Format contrasts table, defining pairwise comparisons for all groups ###
12581273
contrast.names <- combn(levels(factor(names(group))),2)
12591274

1260-
## generate matrix of pairwise group combinations for comparison
1275+
### Generate matrix of pairwise group combinations for comparison ###
12611276
contrasts <- apply(contrast.names, MARGIN=2, function(col) sub("^BLOCKER_", "", make.names(paste0("BLOCKER_", stringr::str_sub(col, 2, -2))))) # limited make.names call for each group (also removes leading parentheses)
12621277
contrast.names <- c(paste(contrast.names[1,],contrast.names[2,],sep = "v"),paste(contrast.names[2,],contrast.names[1,],sep = "v")) ## format combinations for output table files names
12631278
contrasts <- cbind(contrasts,contrasts[c(2,1),])
@@ -1271,6 +1286,7 @@ rm(contrast.names)
12711286

12721287
**Output Data:**
12731288

1289+
* `compare_csv` (data frame containing sample names, technical replicate information if provided, and factor levels from the runsheet)
12741290
* `study` (data frame specifying factor levels assigned to each sample)
12751291
* `group` (named vector specifying the group or set of factor levels for each sample)
12761292
* `contrasts` (matrix defining pairwise comparisons between groups)
@@ -1327,73 +1343,134 @@ txi.rsem$length[txi.rsem$length == 0] <- 1
13271343
sampleTable <- data.frame(condition=factor(group))
13281344
rownames(sampleTable) <- colnames(txi.rsem$counts)
13291345

1330-
### Handle technical replicates in sample table ###
1331-
# Replicates are identified by the presence of a "_techrepX" suffix in the sample name.
1332-
# Replicates are removed from the sampleTable such that the minimal number of equal technical replicates are kept across all samples.
1333-
handle_technical_replicates <- function(sampleTable) {
1334-
# Extract base names and tech rep numbers
1346+
### Handle technical replicates in sample table - STEP 1/2: Filter which samples to retain ###
1347+
# Only if both `Source Name` and `Has Tech Reps` columns exist in runsheet
1348+
if ("Source.Name" %in% colnames(compare_csv) && "Has.Tech.Reps" %in% colnames(compare_csv)) {
1349+
all_samples <- rownames(sampleTable)
1350+
1351+
# Get source names and tech rep status for each sample
13351352
sample_info <- data.frame(
1336-
full_name = rownames(sampleTable),
1337-
condition = sampleTable$condition,
1353+
name = all_samples,
1354+
source_name = compare_csv$Source.Name[match(all_samples, compare_csv$Sample.Name)],
1355+
has_tech_reps = compare_csv$Has.Tech.Reps[match(all_samples, compare_csv$Sample.Name)],
13381356
stringsAsFactors = FALSE
13391357
)
13401358

1341-
# Identify technical replicates
1342-
tech_rep_pattern <- "_techrep(\\d+)$"
1343-
has_tech_rep <- grepl(tech_rep_pattern, sample_info$full_name)
1359+
# Count samples per source name (all samples regardless of tech rep status)
1360+
source_counts <- table(sample_info$source_name)
1361+
min_samples_per_source <- min(source_counts)
13441362

1345-
if (!any(has_tech_rep)) {
1346-
return(sampleTable) # No technical replicates found
1347-
}
1348-
1349-
# Extract base names and tech rep numbers
1350-
sample_info$base_name <- sub(tech_rep_pattern, "", sample_info$full_name)
1351-
sample_info$tech_rep <- as.numeric(sub(".*_techrep", "", sample_info$full_name))
1352-
sample_info$tech_rep[!has_tech_rep] <- 1
1353-
1354-
# Group samples by base name and keep track of which ones to keep
1355-
samples_to_keep <- c()
1356-
unique_base_names <- unique(sample_info$base_name)
1357-
1358-
for (base_name in unique_base_names) {
1359-
base_samples <- which(sample_info$base_name == base_name)
1360-
if (length(base_samples) == 1) {
1361-
# Single sample, keep as is
1362-
samples_to_keep <- c(samples_to_keep, base_samples)
1363+
if (min_samples_per_source > 1) {
1364+
# Use collapseReplicates approach
1365+
# Create grouping variable: Source Name for tech reps, Sample Name for non-tech reps
1366+
collapse_groups <- ifelse(
1367+
toupper(sample_info$has_tech_reps) == "TRUE",
1368+
sample_info$source_name,
1369+
sample_info$name
1370+
)
1371+
1372+
# Get unique groups and count for balancing
1373+
unique_groups <- unique(collapse_groups)
1374+
group_counts <- sapply(unique_groups, function(grp) {
1375+
sum(collapse_groups == grp)
1376+
})
1377+
1378+
min_group_size <- min(group_counts)
1379+
1380+
# Keep first min_group_size samples from each group
1381+
samples_to_keep <- character(0)
1382+
for (grp in unique_groups) {
1383+
indices <- which(collapse_groups == grp)
1384+
samples_to_keep <- c(samples_to_keep, sample_info$name[indices[1:min_group_size]])
1385+
}
1386+
1387+
# Update sample table and counts
1388+
sampleTable <- sampleTable[samples_to_keep, , drop=FALSE]
1389+
1390+
if (params$microbes) {
1391+
counts <- counts[, samples_to_keep]
1392+
} else {
1393+
txi.rsem$counts <- txi.rsem$counts[, samples_to_keep]
1394+
txi.rsem$abundance <- txi.rsem$abundance[, samples_to_keep]
1395+
txi.rsem$length <- txi.rsem$length[, samples_to_keep]
1396+
}
1397+
1398+
} else {
1399+
# min_samples_per_source = 1, use manual filtering approach
1400+
# Keep only first sample from each Source Name + Has Tech Reps = TRUE group
1401+
# Keep all samples with Has Tech Reps = FALSE
1402+
1403+
samples_to_keep <- character(0)
1404+
1405+
# Group by source name and tech rep status
1406+
for (src in unique(sample_info$source_name)) {
1407+
src_samples <- sample_info[sample_info$source_name == src, ]
1408+
1409+
# Separate tech reps from non-tech reps
1410+
tech_rep_samples <- src_samples[toupper(src_samples$has_tech_reps) == "TRUE", ]
1411+
non_tech_rep_samples <- src_samples[toupper(src_samples$has_tech_reps) == "FALSE", ]
1412+
1413+
# Keep only first tech rep sample if any exist
1414+
if (nrow(tech_rep_samples) > 0) {
1415+
samples_to_keep <- c(samples_to_keep, tech_rep_samples$name[1])
1416+
}
1417+
1418+
# Keep all non-tech rep samples
1419+
if (nrow(non_tech_rep_samples) > 0) {
1420+
samples_to_keep <- c(samples_to_keep, non_tech_rep_samples$name)
1421+
}
1422+
}
1423+
1424+
# Update sample table and counts
1425+
sampleTable <- sampleTable[samples_to_keep, , drop=FALSE]
1426+
1427+
if (params$microbes) {
1428+
counts <- counts[, samples_to_keep]
13631429
} else {
1364-
# Multiple samples (tech reps), keep only the minimum number
1365-
tech_rep_samples <- base_samples[order(sample_info$tech_rep[base_samples])]
1366-
samples_to_keep <- c(samples_to_keep, tech_rep_samples[1]) # Keep only first tech rep
1430+
txi.rsem$counts <- txi.rsem$counts[, samples_to_keep]
1431+
txi.rsem$abundance <- txi.rsem$abundance[, samples_to_keep]
1432+
txi.rsem$length <- txi.rsem$length[, samples_to_keep]
13671433
}
13681434
}
1369-
1370-
# Create new sample table with only the kept samples
1371-
new_sampleTable <- data.frame(
1372-
condition = sample_info$condition[samples_to_keep],
1373-
row.names = sample_info$base_name[samples_to_keep],
1374-
stringsAsFactors = FALSE
1375-
)
1376-
1377-
return(new_sampleTable)
13781435
}
13791436

1380-
### Apply the technical replicate handling to the sample table ###
1381-
sampleTable <- handle_technical_replicates(sampleTable)
1382-
1383-
# Update the counts matrix to match the new sample table
1384-
txi.rsem$counts <- txi.rsem$counts[, rownames(sampleTable)]
1385-
13861437
### Build dds object ###
13871438
dds <- DESeqDataSetFromTximport(
13881439
txi = txi.rsem,
13891440
colData = sampleTable,
13901441
design = ~condition
13911442
)
13921443

1393-
### Collapse technical replicates if present ###
1394-
if (any(grepl("_techrep\\d+$", rownames(sampleTable)))) {
1395-
tech_rep_groups <- sub("_techrep\\d+$", "", rownames(sampleTable))
1396-
dds <- collapseReplicates(dds, tech_rep_groups)
1444+
### Handle technical replicates - STEP 2/2: Collapse retained tech reps in DESeq2 object ###
1445+
# Only if both `Source Name` and `Has Tech Reps` columns exist in runsheet
1446+
if ("Source.Name" %in% colnames(compare_csv) && "Has.Tech.Reps" %in% colnames(compare_csv)) {
1447+
# Get info for remaining samples after filtering
1448+
remaining_samples <- rownames(sampleTable)
1449+
remaining_info <- data.frame(
1450+
name = remaining_samples,
1451+
source_name = compare_csv$Source.Name[match(remaining_samples, compare_csv$Sample.Name)],
1452+
has_tech_reps = compare_csv$Has.Tech.Reps[match(remaining_samples, compare_csv$Sample.Name)],
1453+
stringsAsFactors = FALSE
1454+
)
1455+
1456+
# Create collapse grouping: Source Name for tech reps, Sample Name for non-tech reps
1457+
collapse_source_names <- ifelse(
1458+
toupper(remaining_info$has_tech_reps) == "TRUE",
1459+
remaining_info$source_name,
1460+
remaining_info$name
1461+
)
1462+
1463+
# Only collapse if there are multiple samples with the same collapse group
1464+
if (length(unique(collapse_source_names)) < length(collapse_source_names)) {
1465+
# Collapse only if >1 replicate per group exists
1466+
dds <- collapseReplicates(dds, groupby = collapse_source_names)
1467+
1468+
collapsed_names <- unique(collapse_source_names)
1469+
# Update sampleTable to match collapsed samples
1470+
# For collapsed tech reps, use the source name; for non-tech reps, use sample name
1471+
sampleTable <- sampleTable[match(collapsed_names, collapse_source_names), , drop = FALSE]
1472+
rownames(sampleTable) <- collapsed_names
1473+
}
13971474
}
13981475

13991476
### Filter low count genes ###
@@ -1425,8 +1502,9 @@ res_lrt <- results(dds_lrt)
14251502
**Input Data:**
14261503

14271504
* `group` (named vector specifying the group or set of factor levels for each sample, output from [Step 9c](#9c-configure-metadata-sample-grouping-and-group-comparisons))
1505+
* `compare_csv` (data frame containing sample names, technical replicates information if provided, and factor levels from the runsheet, output from [Step 9c](#9c-configure-metadata-sample-grouping-and-group-comparisons))
14281506
* `txi.rsem` (imported RSEM data containing counts matrix, output from [Step 9d](#9d-import-rsem-genecounts))
1429-
- `BPPARAM` (system-specific BiocParallelParam object for parallel processing configuration, output from [Step 9b](#9b-environment-set-up))
1507+
* `BPPARAM` (system-specific BiocParallelParam object for parallel processing configuration, output from [Step 9b](#9b-environment-set-up))
14301508

14311509
**Output Data:**
14321510

@@ -1480,7 +1558,7 @@ output_table$LRT.p.value <- res_lrt@listData$padj
14801558

14811559
### Add group-wise statistics ###
14821560
tcounts <- as.data.frame(t(normCounts))
1483-
tcounts$group <- sampleTable$condition[match(rownames(tcounts), rownames(sampleTable))]
1561+
tcounts$group <- colData(dds)$condition[match(rownames(tcounts), rownames(colData(dds)))]
14841562

14851563
### Aggregate group means and standard deviations ###
14861564
agg_means <- aggregate(. ~ group, data = tcounts, FUN = mean, na.rm = TRUE)

0 commit comments

Comments
 (0)