Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Unsupervised clustering and cell type identification for flow/mass cytometry. Covers FlowSOM, Phenograph, and CATALYST workflows. Use when discovering cell populations in high-dimensional cytometry data without predefined gates.
.claude/skills/bio-flow-cytometry-clustering-phenotyping/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-10 | ✗→✓ | ▲ Improved | — | — |
| case-15 | ✗→✓ | ▲ Improved | — | — |
| case-17 | ✗→✓ | ▲ Improved | — | — |
| case-13 | ✗→✓ | ▲ Improved | — | — |
Reference examples tested with: FlowSOM 2.10+, scanpy 1.10+
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.
"Cluster my cytometry data to find cell types" → Discover cell populations in high-dimensional flow/mass cytometry data using unsupervised clustering without predefined gates.
FlowSOM::FlowSOM() for self-organizing map clusteringCATALYST::cluster() with Phenograph or FlowSOMGoal: Cluster cytometry events into cell populations using self-organizing maps.
Approach: Build a FlowSOM grid on marker channels, then extract metacluster assignments per cell.
rlibrary(FlowSOM) # Prepare data expr <- exprs(fcs) marker_cols <- grep('CD|HLA', colnames(fcs), value = TRUE) # Build SOM fsom <- FlowSOM(fcs, colsToUse = marker_cols, xdim = 10, ydim = 10, nClus = 20, seed = 42) # Get cluster assignments clusters <- GetMetaclusters(fsom) # Add to flowFrame exprs(fcs) <- cbind(exprs(fcs), cluster = clusters)
Goal: Run the complete CATALYST clustering pipeline from flowSet to annotated cell populations.
Approach: Convert flowSet to SingleCellExperiment with prepData, then cluster on type markers with FlowSOM via CATALYST.
rlibrary(CATALYST) library(SingleCellExperiment) # Create SCE from flowSet sce <- prepData(fs, panel, md, transform = TRUE, cofactor = 5) # Clustering sce <- cluster(sce, features = 'type', # Use 'type' markers from panel xdim = 10, ydim = 10, maxK = 20, seed = 42) # View cluster assignments table(cluster_ids(sce, 'meta20'))
Goal: Identify cell populations using graph-based community detection on marker expression.
Approach: Build a k-nearest-neighbor graph on type markers, then partition with Louvain community detection via Rphenograph.
rlibrary(Rphenograph) # Extract expression matrix expr <- assay(sce, 'exprs') # Run Phenograph pheno_result <- Rphenograph(t(expr[rowData(sce)$marker_class == 'type', ]), k = 30) # Get clusters sce$phenograph <- factor(membership(pheno_result[[2]]))
Goal: Project high-dimensional cytometry data into 2D for visualization of cell populations.
Approach: Run UMAP or tSNE on type marker channels using CATALYST's runDR wrapper, then plot colored by cluster.
r# UMAP sce <- runDR(sce, dr = 'UMAP', features = 'type') # tSNE sce <- runDR(sce, dr = 'TSNE', features = 'type') # Plot plotDR(sce, 'UMAP', color_by = 'meta20')
Goal: Assign cell type labels to clusters based on marker expression profiles.
Approach: Visualize median marker expression per cluster with a heatmap, then map cluster IDs to cell type names.
r# Heatmap of marker expression by cluster plotExprHeatmap(sce, features = 'type', by = 'cluster_id', k = 'meta20', scale = 'first', row_anno = FALSE) # Manual annotation cluster_annotation <- c( '1' = 'CD4 T cells', '2' = 'CD8 T cells', '3' = 'B cells', '4' = 'NK cells', '5' = 'Monocytes' ) sce$cell_type <- cluster_annotation[as.character(cluster_ids(sce, 'meta20'))]
Goal: Reduce overclustering by merging similar clusters into biologically meaningful groups.
Approach: Define a mapping table from original to merged cluster IDs, then apply with CATALYST's mergeClusters.
r# Merge similar clusters merging_table <- data.frame( original = 1:20, merged = c(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10) ) sce <- mergeClusters(sce, k = 'meta20', table = merging_table, id = 'merged')
Goal: Quantify the relative frequency of each cell population across samples and conditions.
Approach: Cross-tabulate cluster assignments by sample ID, convert to proportions, and plot grouped by condition.
r# Cluster frequencies per sample abundances <- table(cluster_ids(sce, 'meta20'), sce$sample_id) freq <- prop.table(abundances, margin = 2) # Plot plotAbundances(sce, k = 'meta20', by = 'cluster_id', group_by = 'condition')
Goal: Summarize and compare marker expression levels across clusters and conditions.
Approach: Plot per-cluster median expression with CATALYST's plotClusterExprs and pseudo-bulk expression faceted by cluster.
r# Median expression per cluster plotClusterExprs(sce, k = 'meta20', features = 'type') # Expression by cluster and condition plotPbExprs(sce, k = 'meta20', features = 'type', facet_by = 'cluster_id')
Goal: Save clustering results and annotated SCE object for downstream analysis or sharing.
Approach: Extract cluster assignments into colData, export as CSV, and serialize the full SCE as RDS.
r# Add cluster info to metadata colData(sce)$cluster <- cluster_ids(sce, 'meta20') # Export to CSV results <- as.data.frame(colData(sce)) write.csv(results, 'clustering_results.csv', row.names = FALSE) # Save SCE saveRDS(sce, 'sce_clustered.rds')
Goal: Determine the optimal number of metaclusters for the dataset.
Approach: Compare normalized reduction stability (NRS) plots and heatmaps at different K values to find where clusters remain distinct.
r# Delta area plot plotNRS(sce, features = 'type') # Or visual inspection of heatmap at different K plotExprHeatmap(sce, features = 'type', by = 'cluster_id', k = 'meta10') plotExprHeatmap(sce, features = 'type', by = 'cluster_id', k = 'meta20')
Goal: Remove batch effects from cytometry data before or after clustering.
Approach: Detect batch effects by coloring UMAP by batch variable, then apply MNN correction with batchelor if needed.
r# If batch effects present library(batchelor) sce <- runDR(sce, dr = 'UMAP', features = 'type') # Check for batch effects plotDR(sce, 'UMAP', color_by = 'batch') # MNN correction if needed sce_corrected <- fastMNN(sce, batch = sce$batch)
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | 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 +23 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.