scperturb

repository·master·Indexed 41 days ago

https://github.com/sanderlab/scperturb

A Python package for single-cell perturbation analysis. It provides tools to compute E-distances via edist() and perform E-tests via etest() to quantify and statistically validate differences between perturbed cells and control groups. The package includes utilities for drug name normalization to ChEMBL IDs and provides Snakemake workflows for automating distance computations (such as EMD and E-distance) across standard, downsampled, and split-population analysis configurations.

Tokens
21.1K
Snippets
61
Records
73
Agent score
52%

What's inside scperturb

  1. Harmonize AnnData observations for scperturb

    master

    To ensure compatibility with scperturb workflows, AnnData objects should be harmonized. This involves standardizing column names in adata.obs and ensuring specific metadata fields are present.

    Key transformations include:

    • Renaming columns like Sample to sample, Phase to cellcycle_phase, Clusters to celltype, Guide to guide_id, and Timepoint to time.
    • Mapping Guide IDs to a perturbation column (e.g., identifying 'NTC' as 'control').
    • Setting standard metadata such as organism, disease, cancer, perturbation_type, tissue_type, and tissue.
    • Ensuring var.index is named gene_symbol and obs.index is named cell_barcode.
    def harmonize_data(adata, key):
        adata.var.index.name = 'gene_symbol'
        adata.obs.index.name = 'cell_barcode'
        adata.obs['organism'] = 'Mus musculus'
        adata.obs['disease'] = 'leukemia' if key=='leukemia' else 'healthy'
        adata.obs['cancer'] = key=='leukemia'
        adata.obs['perturbation_type'] = 'CRISPR-cas9'
        adata.obs['tissue_type'] = 'primary'
        adata.obs['tissue'] = 'bone marrow transplant'
    
        # Handle UMAP coordinates if present
        if 'UMAP1' in adata.obs.columns:
            adata.obsm['X_umap'] = adata.obs[['UMAP1', 'UMAP2']].values
    
        # Remove duplicates and map perturbations
        adata.obs = adata.obs.loc[:, ~adata.obs.columns.duplicated(keep='first')]
        adata.obs['perturbation'] = np.array([None if pd.isna(x) else 'control' if x[:3]=='NTC' else x.split('_')[0] for x in adata.obs.Guide])
        
        # Rename standard columns
        adata.obs.rename({
            'Sample': 'sample',
            'Phase': 'cellcycle_phase',
            'Clusters': 'celltype',
            'Mixscape': 'Mixscape_classification',
            'mixscape': 'Mixscape_classification',
            'Guide': 'guide_id',
            'Timepoint': 'time'
        }, axis=1, inplace=True)
    
        # Reorder columns for consistency
        order = ['perturbation', 'guide_id', 'sample', 'cellcycle_phase', 'Mixscape_classification', 
                 'celltype', 'organism', 'disease', 'cancer', 'perturbation_type', 'tissue_type', 'tissue']
        if 'time' in adata.obs.columns: 
            order = ['time'] + order 
        adata.obs = adata.obs[order]
        return adata
  2. Identify the correct normalized drug name file by match type

    master

    Depending on the confidence of the normalization software, results are divided into different files. Use these to determine how much manual verification is required:

    • normalized_drug_names_EXACT.csv: High confidence. You can assign the ChEMBL ID directly.
    • normalized_drug_names_SUBSTRING.csv: Moderate confidence. These entries follow the format NAME (CODE_NAME_1, CODE_NAME_2, ...). They may require minor processing to assign a ChEMBL ID.
    • manual_normalized_drug_names_PARTIAL.csv: Low confidence. Matches were partial (e.g., due to typos). These must be manually verified by searching the amended name on the ChEMBL website.
    • manual_normalized_drug_names_NONE.csv: No match found. These entries are missing from the gold file and must be manually resolved via the ChEMBL website.
  3. Use Snakemake to compute cell group distances

    master

    The scperturb repository provides Snakemake files to automate the computation of distances between groups of cells (such as EMD and E-distance). Depending on your analysis needs, you can use different Snakemake configurations:

    • Standard analysis: Use the default Snakefile to compute all distances between groups of cells.
    • Downsampled analysis: Use Snakefile_ndeep to downsample cell populations to the specific number of cells used by Tessa's Autoencoders.
    • Split population testing: Use Snakefile_split to validate distance metrics. This workflow artificially splits populations by assigning a label (e.g., {perturbation}_X) to 50% of the cells in each perturbation, creating two groups that should theoretically be nearly identical, and then runs distance computations between them.
  4. Import scperturb from local source

    master

    If you are working within the repository structure, you can import scperturb by adding the source directory to your sys.path.

    sys.path.insert(1, '../../package/src/')
    from scperturb import *
  5. Harmonize Metadata for scperturb workflows

    master

    When preparing single-cell AnnData objects for scperturb, you often need to harmonize metadata to ensure compatibility with downstream analysis. Key steps include:

    1. Standardizing Index Names: Ensure obs_names and var_names are correctly formatted (e.g., stripping suffixes from barcodes).
    2. Renaming Columns: Map dataset-specific metadata keys to standard names:
      • barcode $\rightarrow$ guide_id
      • sgrna $\rightarrow$ guide_sequence
      • gene $\rightarrow$ perturbation
      • type $\rightarrow$ medium
    3. Defining Control Groups: Explicitly label non-targeting controls (e.g., setting 'Non-Targeting' to 'control').
    4. Adding Contextual Metadata: Populate fields like perturbation_type, organism, tissue_type, and celltype to enable group-based comparisons.
    # Example harmonization pattern
    adata.obs.rename({
        'barcode': 'guide_id',
        'sgrna': 'guide_sequence',
        'gene': 'perturbation',
        'type': 'medium'
    }, axis=1, inplace=True)
    
    # Set control labels
    adata.obs.perturbation[adata.obs.perturbation=='Non-Targeting'] = 'control'
    
    # Add required context
    adata.obs['perturbation_type'] = 'CRISPR-cas9'
    adata.obs['organism'] = 'mouse'
  6. Use Snakemake to compute distances between cell groups

    master

    The repository provides Snakemake files to automate the computation of distances (such as EMD and E-distance) between different groups of cells. Depending on your analysis needs, you can use different Snakefile configurations:

    • Standard analysis: Use the default Snakefile to compute all distances between groups of cells.
    • Downsampled analysis: Use Snakefile_ndeep to downsample cell populations to the specific number of cells used by Tessa's Autoencoders.
    • Split-population validation: Use Snakefile_split to perform a validation test. This workflow artificially splits populations by assigning a label (e.g., {perturbation}_X) to 50% of the cells in each perturbation. This creates two groups that should theoretically be nearly identical, allowing you to test the sensitivity and accuracy of the distance metrics.
  7. Install dependencies for scperturb

    master

    To use the notebook and the scperturb package, you need to install several bioinformatics and data science libraries. This includes scanpy, scvelo, anndata, and the scperturb package itself (which may require specific installation steps depending on your environment).

    !pip install mygene statannotations scrublet scanpy scvelo decoupler goatools gseapy scperturb chembl_webresource_client biomart PyComplexHeatmap statsmodels omnipath git+https://github.com/saezlab/pypath.git --quiet
  8. Import scperturb and utility modules

    master

    When working within the repository structure, you can import the scperturb package and local utilities by adding the relevant source directories to your sys.path.

    import sys
    
    # Import local utilities
    sys.path.insert(1, '../../')
    from utils import *
    
    # Import scperturb package
    sys.path.insert(1, '../../package/src/')
    from scperturb import *
  9. Harmonize metadata for scperturb analysis

    master

    To ensure compatibility with scperturb workflows, you should harmonize your AnnData.obs metadata. Key steps include:

    • Normalizing perturbation names (e.g., converting 'non-targeting' to 'control').
    • Renaming columns like guide_identity to guide_id and number_of_guides to nperts.
    • Adding required biological annotations such as perturbation_type, organism, celltype, and tissue_type.
    • Running annotate_qc(adata) and assert_annotations(adata) to validate the metadata structure.
    # Harmonize perturbation names and rename columns
    adata.obs['perturbation'] = ['_'.join(np.unique([y.split('_')[0] for y in x.split(';')])).replace('non-targeting', 'control').replace('control_','').replace('_control','') if type(x)==str else None for x in adata.obs.guide_identity]
    adata.obs = adata.obs.rename({'guide_identity': 'guide_id', 'number_of_guides': 'nperts'}, axis=1)
    
    # Add biological context
    adata.obs['perturbation_type'] = 'CRISPR-cas9'
    adata.obs['organism'] = 'human'
    # ... add other metadata as needed
    
    # Validate
    annotate_qc(adata)
    assert_annotations(adata)
  10. Prepare AnnData by moving non-gene features to obsm

    master

    In single-cell datasets containing non-gene features (such as SCV_ velocity features or lenti_ lentivirus capture features), it is common practice to move these from the main expression matrix (adata.X) into the .obsm slot. This keeps the primary gene expression matrix clean while preserving the additional data.

    # Example: Moving SCV_ features to obsm['SCOV_expression']
    group1 = adata.var.index.str.startswith('SCV_')
    non_genes = list(adata.var_names[group1])
    adata.obsm['SCOV_expression'] = pd.DataFrame(adata[:, non_genes].X.A, index=adata.obs_names, columns=non_genes)
    adata = adata[:, ~group1].copy()
    
    # Example: Moving lenti_ features to obsm['lentivirus_capture']
    group2 = adata.var.index.str.startswith('lenti_')
    non_genes = list(adata.var_names[group2])
    adata.obsm['lentivirus_capture'] = pd.DataFrame(adata[:, non_genes].X.A, index=adata.obs_names, columns=non_genes)
    adata = adata[:, ~adata.var.index.str.startswith('lenti_')].copy()