Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Analyze Perturb-seq and CROP-seq CRISPR screening data integrated with scRNA-seq. Use when identifying gene function through pooled genetic perturbations in single cells.
.claude/skills/bio-single-cell-perturb-seq/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | — | — |
| case-15 | ✗→✓ | ▲ Improved | — | — |
| case-14 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✗→✓ | ▲ Improved | — | — |
Reference examples tested with: MAGeCK 0.5+, pandas 2.2+, pertpy 0.7+, scanpy 1.10+
Before using code patterns, verify installed versions match. If versions differ:
pip show <package> then help(module.function) to check signaturespackageVersion('<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.
"Analyze my Perturb-seq CRISPR screen" → Link guide RNA assignments to transcriptional phenotypes in pooled CRISPR screens with single-cell readout to identify gene function.
pertpy.tl.Mixscape(adata) for perturbation classification, pertpy.tl.Augur for prioritizationpythonimport scanpy as sc import pertpy as pt adata = sc.read_h5ad('perturb_seq.h5ad') # Guide assignments typically stored in obs # Format: cell barcode -> guide identity -> target gene adata.obs['guide_id'] = guide_assignments['guide_id'] adata.obs['target_gene'] = guide_assignments['target_gene'] # Mark non-targeting controls adata.obs['is_control'] = adata.obs['target_gene'] == 'non-targeting'
python# Initialize perturbation analysis ps = pt.tl.PerturbationSpace(adata) # Differential expression per perturbation vs control de = pt.tl.PseudobulkDE(adata) de.fit( groupby='target_gene', control='non-targeting', n_threads=8 ) results = de.results() # Filter significant genes sig_results = results[results['pval_adj'] < 0.05] # Perturbation signatures (effect sizes) ps = pt.tl.PerturbationSignature(adata) ps.compute(groupby='target_gene', control='non-targeting') # Get signature matrix signatures = ps.get_signature_matrix()
python# Compute perturbation-level embeddings pt.tl.perturbation_embedding(adata, groupby='target_gene', method='mean') # Cluster perturbations by phenotype pt.tl.cluster_perturbations(adata, resolution=0.5) # Find functionally related perturbations pt.pl.perturbation_heatmap(adata, groupby='perturbation_cluster')
Goal: Classify cells in a CRISPR screen as successfully perturbed or escaped based on their transcriptional response relative to non-targeting controls.
Approach: Compute per-cell perturbation signatures against non-targeting controls using PCA-projected differences, then run Mixscape mixture model classification to separate knockout-responsive cells from escapees.
rlibrary(Seurat) library(SeuratObject) # Load Perturb-seq data seurat <- Read10X('filtered_feature_bc_matrix/') seurat <- CreateSeuratObject(seurat) # Add perturbation metadata seurat <- AddMetaData(seurat, metadata = perturbation_calls) # Standard preprocessing seurat <- NormalizeData(seurat) seurat <- FindVariableFeatures(seurat) seurat <- ScaleData(seurat) seurat <- RunPCA(seurat) seurat <- RunUMAP(seurat, dims = 1:30) # Mixscape: Classify perturbed vs non-perturbed cells seurat <- CalcPerturbSig( seurat, assay = 'RNA', slot = 'data', new.assay.name = 'PRTB', gd.class = 'gene', nt.cell.class = 'NT', num.neighbors = 20, reduction = 'pca', ndims = 15 ) # Run Mixscape classification seurat <- RunMixscape( seurat, assay = 'PRTB', slot = 'scale.data', labels = 'gene', nt.class.name = 'NT', min.de.genes = 5, iter.num = 10, de.assay = 'RNA', prtb.type = 'KO' ) # View classification results table(seurat$mixscape_class.global)
r# UMAP colored by perturbation DimPlot(seurat, reduction = 'umap', group.by = 'mixscape_class', label = TRUE) # Perturbation score distribution VlnPlot(seurat, features = 'mixscape_class_p_ko', group.by = 'gene') # DE genes for each perturbation MixscapeHeatmap(seurat, ident.1 = 'TP53', ident.2 = 'NT', balanced = TRUE) # LDA projection seurat <- MixscapeLDA(seurat, labels = 'gene', nt.class.name = 'NT') LDAPlot(seurat)
pythonimport pandas as pd # From Cell Ranger output (CRISPR Guide Capture) guides = pd.read_csv('crispr_analysis/protospacer_calls_per_cell.csv') # Clean up guide calls guides['cell_barcode'] = guides['cell_barcode'].str.replace('-1', '') guides = guides[guides['num_features'] == 1] # Single guide per cell # Merge with expression data adata.obs = adata.obs.merge( guides[['cell_barcode', 'feature_call', 'target_gene']], left_index=True, right_on='cell_barcode', how='left' )
python# Check guide representation guide_counts = adata.obs['target_gene'].value_counts() print(f'Guides per target: {guide_counts.mean():.1f}') print(f'Cells per guide: {adata.obs.groupby("guide_id").size().mean():.1f}') # Filter low-representation guides # Standard: keep guides with >= 100 cells min_cells = 100 valid_guides = guide_counts[guide_counts >= min_cells].index adata = adata[adata.obs['target_gene'].isin(valid_guides)] # Check for guide bias sc.pl.violin(adata, keys='n_genes_by_counts', groupby='target_gene', rotation=90)
python# Cells with multiple guides (MOI > 1) multi_guide = adata.obs[adata.obs['num_guides'] > 1] print(f'Multi-guide cells: {len(multi_guide) / len(adata):.1%}') # Options: # 1. Remove multi-guide cells adata = adata[adata.obs['num_guides'] == 1] # 2. Keep only cells where guides target same gene # 3. Analyze combinatorial effects
python# Aggregate to pseudobulk for robust DE from pertpy.tools import PseudobulkDE pb = PseudobulkDE(adata) pb.fit( groupby='target_gene', control='non-targeting', method='deseq2', # or 'edger', 'wilcoxon' min_cells=50 ) # Get results for specific perturbation tp53_de = pb.results('TP53') sig_genes = tp53_de[tp53_de['padj'] < 0.05].sort_values('log2FoldChange')
pythonimport decoupler as dc # Get DE genes per perturbation de_results = pb.results() # Run pathway enrichment dc.run_ora( mat=de_results, net=dc.get_resource('MSigDB'), source='geneset', target='gene' ) # Visualize top pathways dc.plot_barplot(de_results, 'TP53', top_n=20)
| Metric | Good | Acceptable | Poor | |--------|------|------------|------| | Cells per guide | >200 | 100-200 | <100 | | Guide detection rate | >90% | 80-90% | <80% | | Non-targeting cells | 5-15% | 15-25% | >25% | | Mixscape KO fraction | >50% | 30-50% | <30% |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | 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, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +59 percentage points is the difference between those two pass rates over the 21 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.