| name | bio-flow-cytometry-doublet-detection |
| description | Detect and remove doublets from flow and mass cytometry data. Covers FSC/SSC gating and computational doublet detection methods. Use when filtering out cell aggregates before clustering or quantitative analysis. |
| tool_type | r |
| primary_tool | flowCore |
Version Compatibility
Reference examples tested with: flowCore 2.14+, ggplot2 3.5+
Before using code patterns, verify installed versions match. If versions differ:
- R:
packageVersion('<pkg>') then ?function_name to verify parameters
If code throws ImportError, AttributeError, or TypeError, introspect the installed
package and adapt the example to match the actual API rather than retrying.
Doublet Detection
"Remove doublets from my flow cytometry data" → Detect and filter out cell aggregates using FSC-A/FSC-H gating or computational methods before clustering or quantitative analysis.
- R:
flowCore rectangular gates on FSC-A vs FSC-H
FSC-A vs FSC-H Gating (Standard Method)
library(flowCore)
library(ggcyto)
fs <- read.flowSet(list.files('data/', pattern = '\\.fcs$', full.names = TRUE))
singlet_gate <- rectangleGate(
filterId = 'singlets',
'FSC-A' = c(50000, 250000),
'FSC-H' = c(50000, 250000)
)
singlet_polygon <- polygonGate(
filterId = 'singlets',
.gate = data.frame(
'FSC-A' = c(50000, 250000, 250000, 50000),
'FSC-H' = c(40000, 200000, 260000, 60000)
)
)
singlets <- Subset(fs, singlet_gate)
autoplot(fs[[1]], 'FSC-A', 'FSC-H') + geom_gate(singlet_gate)
Automated Singlet Gating with flowDensity
library(flowDensity)
singlet_result <- flowDensity(
fs[[1]],
channels = c('FSC-A', 'FSC-H'),
position = c(TRUE, TRUE),
gates = c(NA, NA)
)
singlets <- getflowFrame(singlet_result)
pct_singlets <- nrow(singlets) / nrow(fs[[1]]) * 100
cat('Singlets:', round(pct_singlets, 1), '%\n')
flowAI Quality Control
library(flowAI)
fs_qc <- flow_auto_qc(
fs,
folder_results = 'flowAI_results',
fcs_QC = TRUE,
fcs_highQ = TRUE
)
FSC-A/FSC-W Method (Width Parameter)
if ('FSC-W' %in% colnames(fs[[1]])) {
singlet_gate_w <- rectangleGate(
filterId = 'singlets',
'FSC-A' = c(50000, 250000),
'FSC-W' = c(50000, 100000)
)
singlets <- Subset(fs, singlet_gate_w)
}
Ratio-Based Doublet Detection
calculate_fsc_ratio <- function(ff) {
fsc_a <- exprs(ff)[, 'FSC-A']
fsc_h <- exprs(ff)[, 'FSC-H']
ratio <- fsc_a / (fsc_h + 1)
return(ratio)
}
for (i in 1:length(fs)) {
ratio <- calculate_fsc_ratio(fs[[i]])
fs[[i]] <- cbind2(fs[[i]], ratio)
colnames(fs[[i]])[ncol(fs[[i]])] <- 'FSC_ratio'
}
ratio_cutoff <- quantile(exprs(fs[[1]])[, 'FSC_ratio'], 0.95)
singlet_gate_ratio <- rectangleGate(filterId = 'singlets', 'FSC_ratio' = c(0, ratio_cutoff))
SSC-Based Doublet Detection
ssc_singlet_gate <- rectangleGate(
filterId = 'ssc_singlets',
'SSC-A' = c(10000, 200000),
'SSC-H' = c(10000, 200000)
)
combined_gate <- singlet_gate & ssc_singlet_gate
singlets <- Subset(fs, combined_gate)
CyTOF Doublet Detection
library(CATALYST)
sce <- prepData(fs, panel, md)
if ('Event_length' %in% rownames(sce)) {
event_length <- assay(sce)['Event_length', ]
singlet_idx <- event_length < quantile(event_length, 0.95)
sce_singlets <- sce[, singlet_idx]
cat('Removed', sum(!singlet_idx), 'doublets based on event length\n')
}
if (all(c('DNA1', 'DNA2') %in% rownames(sce))) {
dna_total <- assay(sce)['DNA1', ] + assay(sce)['DNA2', ]
dna_cutoff <- quantile(dna_total, 0.95)
singlet_idx <- dna_total < dna_cutoff
sce_singlets <- sce[, singlet_idx]
}
CATALYST Workflow with Doublet Removal
Goal: Detect and remove cell doublets from a CyTOF/flow dataset using a regression-based approach on scatter parameters.
Approach: Model the expected FSC-A vs FSC-H relationship for singlets with linear regression, classify events with large residuals (above the 95th percentile) as doublets, and filter them out.
library(CATALYST)
sce <- prepData(fs, panel, md, transform = TRUE, cofactor = 5)
sce <- filterSCE(sce, !is_doublet(sce))
fsc_a <- colData(sce)$FSC_A
fsc_h <- colData(sce)$FSC_H
fit <- lm(fsc_a ~ fsc_h)
residuals <- abs(fsc_a - predict(fit))
threshold <- quantile(residuals, 0.95)
colData(sce)$doublet <- residuals > threshold
sce_singlets <- sce[, !colData(sce)$doublet]
cat('Doublet rate:', round(mean(colData(sce)$doublet) * 100, 1), '%\n')
Batch Processing
detect_doublets <- function(ff, method = 'fsc') {
if (method == 'fsc') {
fsc_a <- exprs(ff)[, 'FSC-A']
fsc_h <- exprs(ff)[, 'FSC-H']
fit <- lm(fsc_a ~ fsc_h)
residuals <- abs(fsc_a - predict(fit))
threshold <- quantile(residuals, 0.95)
singlet_idx <- residuals <= threshold
} else if (method == 'ratio') {
ratio <- exprs(ff)[, 'FSC-A'] / (exprs(ff)[, 'FSC-H'] + 1)
singlet_idx <- ratio < quantile(ratio, 0.95)
}
return(ff[singlet_idx, ])
}
fs_singlets <- fsApply(fs, detect_doublets, method = 'fsc')
doublet_rates <- sapply(1:length(fs), function(i) {
1 - nrow(fs_singlets[[i]]) / nrow(fs[[i]])
})
cat('Mean doublet rate:', round(mean(doublet_rates) * 100, 1), '%\n')
Visualization
library(ggplot2)
plot_data <- data.frame(
FSC_A = exprs(fs[[1]])[, 'FSC-A'],
FSC_H = exprs(fs[[1]])[, 'FSC-H']
)
fit <- lm(FSC_A ~ FSC_H, data = plot_data)
plot_data$residual <- abs(plot_data$FSC_A - predict(fit))
plot_data$doublet <- plot_data$residual > quantile(plot_data$residual, 0.95)
ggplot(plot_data, aes(x = FSC_H, y = FSC_A, color = doublet)) +
geom_point(alpha = 0.3, size = 0.5) +
scale_color_manual(values = c('gray', 'red')) +
theme_bw() +
labs(title = 'Doublet Detection', x = 'FSC-H', y = 'FSC-A')
ggsave('doublet_detection.png', width = 8, height = 6)
Related Skills
Workflow order: cytometry-qc → doublet-detection → bead-normalization → clustering
- cytometry-qc - Run first: identify flow rate and signal issues
- bead-normalization - Run after: correct remaining instrument drift
- fcs-handling - Load FCS files
- gating-analysis - Manual gating workflows
- clustering-phenotyping - Downstream analysis after doublet removal