Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Differential abundance and state analysis for cytometry data. Compare cell populations between conditions using statistical methods. Use when testing for significant changes in cell frequencies or marker expression between groups.
.claude/skills/bio-flow-cytometry-differential-analysis/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-15 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-20 | ✗→✓ | ▲ Improved | — | — |
| case-22 | ✗→✓ | ▲ Improved | — | — |
Reference examples tested with: R stats (base), edgeR 4.0+, ggplot2 3.5+, limma 3.58+
Before using code patterns, verify installed versions match. If versions differ:
packageVersion('<pkg>') then ?function_name to verify parametersIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
"Compare cell populations between my conditions" → Test for significant changes in cell type frequencies (differential abundance) or marker expression levels (differential state) between experimental groups.
CATALYST::testDA_edgeR() or diffcyt::testDA_GLMM()Goal: Test which cell population clusters differ in frequency between experimental conditions.
Approach: Create a design matrix and contrast from sample metadata, then run edgeR-based differential abundance testing on cluster counts per sample using testDA_edgeR from the diffcyt framework.
rlibrary(CATALYST) library(diffcyt) # Load clustered data sce <- readRDS('sce_clustered.rds') # Create design matrix design <- createDesignMatrix(ei(sce), cols_design = 'condition') # Create contrast contrast <- createContrast(c(0, 1)) # Treatment vs Control # Differential abundance test res_DA <- testDA_edgeR(sce, design, contrast, cluster_id = 'meta20') # View results rowData(res_DA)$cluster_id rowData(res_DA)$p_adj # Significant clusters sig_DA <- rowData(res_DA)$p_adj < 0.05 table(sig_DA)
r# Test for marker expression differences within clusters res_DS <- testDS_limma(sce, design, contrast, cluster_id = 'meta20', markers_include = rownames(sce)[rowData(sce)$marker_class == 'state']) # Results per marker per cluster ds_results <- rowData(res_DS)
r# DA results heatmap plotDiffHeatmap(sce, res_DA, all = TRUE, fdr = 0.05) # DS results heatmap plotDiffHeatmap(sce, res_DS, all = TRUE, fdr = 0.05) # Abundance by condition plotAbundances(sce, k = 'meta20', by = 'cluster_id', group_by = 'condition')
rlibrary(tidyverse) # Get cluster frequencies per sample freqs <- colData(sce) %>% as.data.frame() %>% group_by(sample_id, condition, cluster_id = cluster_ids(sce, 'meta20')) %>% summarise(n = n(), .groups = 'drop') %>% group_by(sample_id) %>% mutate(freq = n / sum(n) * 100) # Test each cluster test_abundance <- function(df, cluster) { cluster_data <- filter(df, cluster_id == cluster) ctrl <- filter(cluster_data, condition == 'Control')$freq treat <- filter(cluster_data, condition == 'Treatment')$freq if (length(ctrl) >= 2 && length(treat) >= 2) { test <- t.test(treat, ctrl) return(data.frame( cluster = cluster, fc = mean(treat) / mean(ctrl), pvalue = test$p.value )) } return(NULL) } results <- map_dfr(unique(freqs$cluster_id), ~test_abundance(freqs, .x)) results$padj <- p.adjust(results$pvalue, method = 'BH')
rlibrary(lme4) library(lmerTest) # For paired/repeated measures designs # Random effect for patient/donor fit_mixed <- function(df, cluster) { cluster_data <- filter(df, cluster_id == cluster) model <- lmer(freq ~ condition + (1|patient_id), data = cluster_data) coef <- summary(model)$coefficients return(data.frame( cluster = cluster, estimate = coef[2, 'Estimate'], pvalue = coef[2, 'Pr(>|t|)'] )) }
rlibrary(citrus) # Prepare data fcs_files <- list.files('data', pattern = '\\.fcs$', full.names = TRUE) labels <- c(rep('Control', 2), rep('Treatment', 2)) # Run CITRUS citrus_result <- citrus( fcs_files, labels, fileSampleSize = 1000, featureType = 'abundances', modelType = 'glmnet', family = 'classification' ) # Get significant clusters citrus_plot(citrus_result)
rlibrary(ggplot2) # From DA results da_df <- as.data.frame(rowData(res_DA)) da_df$significant <- da_df$p_adj < 0.05 ggplot(da_df, aes(x = logFC, y = -log10(p_adj), color = significant)) + geom_point() + geom_hline(yintercept = -log10(0.05), linetype = 'dashed') + geom_vline(xintercept = c(-1, 1), linetype = 'dashed') + scale_color_manual(values = c('gray', 'red')) + theme_bw() + labs(title = 'Differential Abundance')
r# Combine DA and DS results da_results <- as.data.frame(rowData(res_DA)) da_results$analysis <- 'DA' ds_results <- as.data.frame(rowData(res_DS)) ds_results$analysis <- 'DS' # Save write.csv(da_results, 'da_results.csv', row.names = FALSE) write.csv(ds_results, 'ds_results.csv', row.names = FALSE)
r# For multiple conditions design_full <- model.matrix(~ 0 + condition, data = ei(sce)) colnames(design_full) <- levels(factor(ei(sce)$condition)) # Multiple contrasts contrasts <- makeContrasts( TreatA_vs_Ctrl = TreatmentA - Control, TreatB_vs_Ctrl = TreatmentB - Control, TreatA_vs_B = TreatmentA - TreatmentB, levels = design_full ) # Test each contrast res_list <- lapply(1:ncol(contrasts), function(i) { testDA_edgeR(sce, design_full, contrasts[, i], cluster_id = 'meta20') })
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +36 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.