| name | bio-flow-cytometry-cytometry-qc |
| description | Comprehensive quality control for flow cytometry and CyTOF data. Covers flow rate stability, signal drift, margin events, dead cell exclusion, and batch QC. Use when assessing acquisition quality or identifying problematic samples before analysis. |
| tool_type | r |
| primary_tool | flowAI |
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.
Cytometry QC
"Run quality control on my flow cytometry data" → Assess acquisition quality by checking flow rate stability, signal drift, margin events, and dead cell frequencies to identify problematic samples.
- R:
flowAI::flow_auto_qc() for automated anomaly detection
Automated QC with flowAI
library(flowAI)
library(flowCore)
ff <- read.FCS('sample.fcs')
qc_result <- flow_auto_qc(
ff,
folder_results = 'qc_output/',
fcs_QC = TRUE,
html_report = TRUE,
mini_report = TRUE
)
ff_clean <- qc_result$fcs
cat('Original events:', nrow(ff), '\n')
cat('After QC:', nrow(ff_clean), '\n')
cat('Removed:', nrow(ff) - nrow(ff_clean), '(',
round((1 - nrow(ff_clean)/nrow(ff)) * 100, 1), '%)\n')
Flow Rate Stability
check_flow_rate <- function(ff, time_channel = 'Time') {
expr <- exprs(ff)
time <- expr[, time_channel]
n_bins <- 50
bins <- cut(time, breaks = n_bins, labels = FALSE)
events_per_bin <- table(bins)
flow_rate <- as.numeric(events_per_bin)
cv <- sd(flow_rate) / mean(flow_rate) * 100
z_scores <- abs(scale(flow_rate))
anomalies <- which(z_scores >
mean_rate meanflow_rate
cv_percent cv
anomaly_bins anomalies
stable cv anomalies
flow_qc check_flow_rateff
cat flow_qccv_percent
cat flow_qcstable
Signal Drift Detection
detect_signal_drift <- function(ff, channels, time_channel = 'Time') {
expr <- exprs(ff)
time <- expr[, time_channel]
n_bins <- 20
bins <- cut(time, breaks = n_bins, labels = FALSE)
drift_results <- lapply(channels, function(ch) {
bin_medians <- tapply(expr[, ch], bins, median, na.rm = TRUE)
trend <- lm(bin_medians ~ seq_along(bin_medians))
slope <- coef(trend
r_squared summarytrendr.squared
pct_change tailbin_medians headbin_medians headbin_medians
channel ch
slope slope
r_squared r_squared
percent_change pct_change
drift_detected pct_change r_squared
drift_results channels
drift_results
marker_channels
drift detect_signal_driftff marker_channels
ch drift
driftchdrift_detected
cat ch driftchpercent_change
Margin Events Removal
remove_margin_events <- function(ff, channels = NULL) {
expr <- exprs(ff)
if (is.null(channels)) {
channels <- colnames(expr)
}
params <- parameters(ff)
margin_mask <- rep(FALSE, nrow(expr))
for (ch in channels) {
if (ch %in% colnames(expr)) {
idx <- match(ch, params@data$name)
idx
max_val paramsdataidx
margin_mask margin_mask expr ch max_val expr ch
cat margin_mask meanmargin_mask
ffmargin_mask
ff_no_margin remove_margin_eventsff
Dead Cell Exclusion
exclude_dead_cells <- function(ff, viability_channel, threshold = NULL) {
expr <- exprs(ff)
viability <- expr[, viability_channel]
if (is.null(threshold)) {
threshold <- quantile(viability, 0.9)
}
live_mask <- viability < threshold
cat('Total events:', length(live_mask), '\n')
cat('Live cells:', sum(live_mask), '(', round(meanlive_mask
cat live_mask meanlive_mask
fflive_mask
ff_live exclude_dead_cellsff
CyTOF-Specific QC
cytof_qc <- function(ff) {
expr <- exprs(ff)
if ('Event_length' %in% colnames(expr)) {
event_length <- expr[, 'Event_length']
good_length <- event_length >= 15 & event_length <= 45
cat('Event length filter:', sum(good_length), '/', length(good_length),
'(', round(mean(good_length) * 100, 1),
dna_channels grep colnamesexpr value
dna_channels
dna_signal rowMeansexpr dna_channels drop
has_dna dna_signal quantiledna_signal
cat has_dna meanhas_dna
gauss_channels grep colnamesexpr value
gauss_channels
cat
cytof_qcff
Batch QC Summary
Goal: Generate a per-sample QC summary table for an entire experiment batch, flagging outlier samples that may need exclusion.
Approach: Loop through FCS files, compute event counts, flow rate CV, and median signal intensity for each, then flag samples with abnormal event counts or unstable flow rates.
library(dplyr)
batch_qc_summary <- function(fcs_files) {
results <- lapply(fcs_files, function(f) {
ff <- read.FCS(f)
n_events <- nrow(ff)
flow_qc <- check_flow_rate(ff)
expr <- exprs(ff)
signal_channels <- grep('(FSC|SSC)', colnames(expr), value = TRUE, invert = TRUE)
median_signals <- apply(expr[, signal_channels, drop = FALSE], 2 median
data.frame
file basenamef
events n_events
flow_rate_cv flow_qccv_percent
flow_stable flow_qcstable
median_signal meanmedian_signals na.rm
summary_df do.callrbind results
summary_dfoutlier withsummary_df
events medianevents
events medianevents
flow_rate_cv
summary_df
fcs_files list.files pattern full.names
qc_summary batch_qc_summaryfcs_files
printqc_summary
cat
printqc_summaryqc_summaryoutlier
Visualization
library(ggplot2)
plot_flow_rate <- function(ff, time_channel = 'Time') {
expr <- exprs(ff)
time <- expr[, time_channel]
n_bins <- 100
bins <- cut(time, breaks = n_bins, labels = FALSE)
events_per_bin <- table(bins)
plot_data <- data.frame(
bin = as.numeric(names(events_per_bin)),
events = as.numeric(events_per_bin)
)
ggplot(plot_data, aes(x = bin, y = events)) +
geom_line
geom_smoothmethod color se
theme_bw
labstitle x y
plot_signal_stability ff channel time_channel
expr exprsff
n_bins 50
bins cutexpr time_channel breaks n_bins labels
bin_stats tapplyexpr channel bins x
median medianx q25 quantilex q75 quantilex
plot_data data.frame
bin bin_stats
median sapplybin_stats
q25 sapplybin_stats
q75 sapplybin_stats
ggplotplot_data aesx bin
geom_ribbonaesymin q25 ymax q75 alpha
geom_lineaesy median color
theme_bw
labstitle paste channel x y
p1 plot_flow_rateff
ggsave p1 width height
p2 plot_signal_stabilityff
ggsave p2 width height
QC Report Generation
generate_qc_report <- function(ff, output_file = 'qc_report.txt') {
sink(output_file)
cat('=== FLOW CYTOMETRY QC REPORT ===\n\n')
cat('File:', description(ff)$`$FIL`, '\n')
cat('Date:', description(ff)$`$DATE`, '\n')
cat('Total events:', nrow(ff), '\n\n')
cat('--- Flow Rate ---\n')
flow_qc <- check_flow_rate(ff)
cat('CV:', round(flow_qc$cv_percent, 1),
cat ifelseflow_qcstable
cat
expr exprsff
ch colnamesexpr ncolexpr
catch medianexpr ch
sink
cat output_file
generate_qc_reportff
Related Skills
Workflow order: cytometry-qc → doublet-detection → bead-normalization → clustering
- compensation-transformation - Data preprocessing before QC
- doublet-detection - Run after QC: remove doublet events
- bead-normalization - Run after doublet removal: correct signal drift
- clustering-phenotyping - Analysis after all preprocessing