SIDISH — Cell-Death / High-Risk Cell Identification
Overview
What it does. SIDISH (Semi-supervised Iterative Deep Learning for Identifying Single-cell High-Risk Populations) integrates three data types — scRNA-seq, bulk RNA-seq and patient survival — into one iterative deep-learning loop:
- Phase 1 trains a VAE on the single-cell expression matrix.
- Phase 2 trains a Deep Cox survival model on bulk RNA-seq, using the VAE latent space to score cells with the patient-level hazard model.
- Between iterations, cell weights and patient weights are updated (SHAP-based feature selection + a Weibull/fitted hazard distribution), and cells above the chosen hazard percentile are relabelled High-Risk (
h) vs Background (b).
Its downstream module performs in-silico perturbation: knock a gene (or a gene pair) out, re-run the hazard model, and count how many High-Risk cells flip back to Background. That gives a ranked list of candidate targets.
When to use it.
- You have scRNA-seq (or spatial) plus a bulk RNA-seq cohort with survival data (duration + event), and you want cell populations whose transcriptional state tracks the worst clinical outcome.
- You want a therapeutic-target shortlist from perturbation scoring, or a risk-associated cell-state marker-gene list to test on the bulk cohort (Kaplan-Meier).
When NOT to use it.
- No survival data / no bulk cohort → the whole Phase 2 signal is missing; use a death-signature or apoptosis-scoring approach instead.
- You literally need dead/dying cells (Annexin V, Caspase, cell-cycle arrest, pct MT): SIDISH has no cell-death score in its objective. It labels cells that are survival-linked high-risk; interpreting them as "dying/damaged cells" is a biological interpretation that must be validated (see Pitfalls).
Upstream.
- Repo: https://github.com/mcgilldinglab/SIDISH (verified HTTP 200)
- Docs/API: https://sidish.readthedocs.io/en/latest/api.html (verified HTTP 200)
- PyPI: SIDISH==1.0.0 (https://pypi.org/pypi/SIDISH/json verified 200, only release 1.0.0)
- Paper-style description and tutorials (4 notebooks) live in the repo tutorials/.
This skill's script. scripts/sidish_cell_death.py is a single command-line wrapper that reproduces the reference notebook chain (script/SIDISH.ipynb in the user's copy, equivalent to upstream tutorials/tutorial_0..3) with every path as a CLI flag and no hardcoded directories.
Inputs
Verified against the reference dataset shipped with the local copy of the tool (LUAD / TCGA-LUAD):
| File | Shape / size (observed) | Required content |
|---|---|---|
adata.h5ad (raw) |
4102 cells × 1208 genes, X = dense float32 ndarray |
expression matrix; var_names must be HGNC gene symbols |
bulk.csv |
55402 genes × 572 TCGA samples, 282 MB | genes are rows, samples are columns → the notebook reads it with pd.read_csv(...).T |
sample_info.csv |
572 samples | index = sample IDs (TCGA.97.7938.01A); columns Overall_survival_days, Sample_Status (1=dead, 0=alive) |
inter.csv |
2 columns | list of genes shared by bulk and scRNA-seq (advisory) |
PPI/9606.protein.info.v11.5.txt |
6.0 MB | STRING v11.5 protein → gene-symbol map. Fetched separately — the GitHub repo does not ship it. |
PPI/9606.protein.links.v11.5.txt |
549 MB | STRING v11.5 interactions |
PPI/hippie_current.txt |
9.5 KB in the local copy | HIPPIE interactions (the local copy is an HTML error page, not data — see Pitfalls) |
| trained model dir | vae_transfer (8.0 MB), deepCox (2.8 MB), W_matrix_{0..4}.csv (~26 MB each), adata_SIDISH.h5ad (2.1 MB), adata_SIDISH_embedding.h5ad (4.3 MB) |
written by training; consumed by reload/perturbation |
Single-cell .obs columns the pipeline reads or writes: cells, n_genes, scissors, scAB, degas, SIDISH_value, risk_value, SIDISH (labels h/b). preprocess(processed=True) optionally renames a cell-type column to celltype_major; the reference data has no cell-type column, so celltype_name is a no-op there. var columns: gene_ids, n_cells — preprocess() prefers var['gene_ids'] over var_names when intersecting with bulk genes.
Data you must bring for a new project: (1) an .h5ad whose var_names (or var['gene_ids']) are gene symbols, (2) a bulk matrix of the same gene space, (3) sample-level survival with 1/0 status whose sample IDs match the bulk columns (the notebook normalises TCGA.97.7938.01A → TCGA-97-7938-01A by replacing . with -).
Steps
Script: scripts/sidish_cell_death.py (all flags, no hardcoded paths). Run with the SIDISH environment's python on a GPU machine.
- Check the data contract first (cheap, CPU, no torch needed) — this is the only stage that works in a CPU-only container:
python3 scripts/sidish_cell_death.py --stage dry-run \
--input-h5ad data/adata.h5ad --bulk-csv data/bulk.csv \
--survival-csv data/sample_info.csv --ppi-dir data/PPI \
--output-dir out
Exit 0 = compatible; exit 2 = contract violation (it reports gene/sample overlap, event-value validity, PPI completeness). Writes out/Stat/dry_run_report.json.
- Prepare (notebook Step 0) —
SIDISH.SIDISH.preprocess(processed=True)intersects sc/bulk genes, merges survival (duration,event), writes the two files the trainer reads:
python3 scripts/sidish_cell_death.py --stage prepare --input-h5ad data/adata.h5ad \
--bulk-csv data/bulk.csv --survival-csv data/sample_info.csv --output-dir out
# -> out/Rdata/processed_adata.h5ad (4102 x 1208)
# -> out/Stat/processed_bulk.csv (572 x [duration,event,1208 genes])
Use --no-processed to make SIDISH do its own QC/normalisation/HVG selection, and --batch-correction + --batch-key <obs col> for Harmony (sc) / ComBat (bulk).
- Train (notebook Step 1) — VAE (Phase 1) + Deep Cox (Phase 2),
iterationsouter rounds, checkpoints into--model-dir:
python3 scripts/sidish_cell_death.py --stage train --device cuda \
--processed-h5ad out/Rdata/processed_adata.h5ad \
--processed-bulk-csv out/Stat/processed_bulk.csv \
--output-dir out --model-dir out/Rdata/model
# library API used: sidish(adata, bulk, "cuda", seed=0)
# .init_Phase1(225, 20, 32, [512,128], 256, "Adam", 1e-4, 1e-4, 0)
# .init_Phase2(500, 128, 1e-4, 0, 0.2, 256)
# .train(5, 0.95, 30, model_dir, 0, True, "fitted")
# -> out/Rdata/model/{vae_transfer, deepCox, W_matrix_{i}.csv, adata_SIDISH.h5ad}
- Reload + visualise + markers + KM (notebook Step 2):
python3 scripts/sidish_cell_death.py --stage infer --device cuda \
--processed-h5ad out/Rdata/model/adata_SIDISH.h5ad \
--processed-bulk-csv out/Stat/processed_bulk.csv \
--output-dir out --model-dir out/Rdata/model --distribution-fit fitted
# reload(model_dir); get_percentille(0.95)
# plot_CellType_UMAP(resolution=0.8, celltype=False); plot_HighRisk_UMAP(resolution=0.08, celltype=False)
# get_MarkerGenes(logfc_threshold=1); plot_KM(penalizer=10)
The UMAP calls write adata_SIDISH_embedding.h5ad with obsm['latent'], obsm['X_umap'], obs['leiden'], obs['SIDISH_'], obs['SIDISH__'].
- In-silico perturbation (notebook Step 3) — needs the PPI network;
--ppi-diris staged via a symlink so the library's'../PPI/...'lookups resolve (see Pitfalls):
python3 scripts/sidish_cell_death.py --stage perturb --device cuda \
--processed-bulk-csv out/Stat/processed_bulk.csv --ppi-dir data/PPI \
--output-dir out --model-dir out/Rdata/model \
--perturb-genes AGRN CEBPB RHOB CDK2
# get_percentille(0.95); run_Perturbation(n_jobs=4)
# plot_top_perturbed_genes(percent_change/delta_change, top_n=30)
# run_double_Perturbation(top 20 genes, threshold=0.8)
--stage allchains prepare → train → infer → perturb in one process (same SIDISH object, sop_flip/percent_changeare available to the double-perturbation step).--smokeshrinks every numeric setting (epochs=1, iterations=1, small layers, ≤64-cell batches) for a best-effort CPU sanity run — never verified to converge (see Pitfalls).
Parameters
Notebook values are what script/SIDISH.ipynb actually passes; the script's defaults equal them.
| Script flag | Meaning | Notebook value | Script default |
|---|---|---|---|
--stage |
dry-run / prepare / train / infer / perturb / all |
manual cell order | all |
--input-h5ad |
raw scRNA-seq .h5ad |
data/adata.h5ad |
none (required for prepare/dry-run) |
--bulk-csv |
bulk matrix, genes × samples | data/bulk.csv |
none |
--survival-csv |
survival table | data/sample_info.csv |
none |
--output-dir |
output root; creates Img/, Stat/, Rdata/ |
(hardcoded data/, data/LUNG/) |
./sidish_out |
--model-dir |
checkpoint dir, lives under Rdata/ (trailing / added for you) |
data/LUNG/ |
<output-dir>/Rdata/model |
--processed-h5ad, --processed-bulk-csv |
processed inputs | processed_adata.h5ad, processed_bulk.csv |
<output-dir>/Rdata/processed_adata.h5ad, <output-dir>/Stat/processed_bulk.csv |
--ppi-dir |
dir with the 3 PPI txt files | data/PPI |
auto-probe ./PPI, <input dir>/PPI, … |
--label-key |
obs column holding the h/b labels to export |
SIDISH |
SIDISH |
--batch-key |
obs column for Harmony/ComBat |
Sample (as patient_id) |
None → falls back to --patient-key |
--patient-key |
patient_id for batch correction |
Sample |
Sample |
--celltype-key |
celltype_name (renamed to celltype_major) |
celltype_major |
celltype_major |
--survival-col, --status-col |
survival_, status args of preprocess |
Overall_survival_days, Sample_Status |
same |
--processed / --no-processed |
skip / run QC+normalise+HVG | True |
True |
--batch-correction |
Harmony + ComBat | off | off |
--no-fix-sample-names |
stop replacing .→- in survival index |
replacement is applied | applied |
--epochs / --i-epochs |
VAE epochs (iter. 1 / later iters) | 225 / 20 |
225 / 20 |
--latent-size, --layer-dims |
VAE latent dim, hidden dims | 32, [512,128] |
same |
--batch-size |
VAE batch size | 256 (c30 uses 512) |
256 |
--optimizer, --lr, --lr3, --dropout |
Phase-1 optimiser settings | Adam, 1e-4, 1e-4, 0 |
same |
--phase2-epochs, --phase2-hidden |
Deep Cox epochs / hidden | 500, 128 |
same |
--phase2-lr, --phase2-dropout, --test-size, --batch-size-bulk |
Phase-2 settings | 1e-4, 0, 0.2, 256 |
same |
--iterations, --percentile, --steepness |
train() args |
5, 0.95, 30 |
same |
--distribution-fit |
hazard distribution: fixed Weibull vs AIC/BIC/KS/R² model selection | fitted |
fitted |
--seed |
seed/ite |
0 |
0 |
--device |
cpu or cuda |
cuda |
cpu (validated against torch.cuda.is_available()) |
--leiden-resolution, --highrisk-resolution |
Leiden res for the two UMAPs | 0.8, 0.08 |
same |
--logfc-threshold, --pval-threshold |
get_MarkerGenes cutoffs |
1 (logfc), 0.05 |
same |
--km-penalizer |
plot_KM CoxPH penaliser |
10 |
10 |
--top-n, --double-top-n |
barplot depth / gene-pair depth | 30, 20 |
same |
--ppi-threshold |
load_network(threshold) |
0.8 (0.7 inside run_Perturbation) |
0.8 |
--n-jobs |
parallelism for perturbation | 4 |
4 (see Pitfalls — currently ignored upstream) |
--perturb-genes |
genes drawn in the perturbation UMAP panels | AGRN CEBPB RHOB CDK2 |
none (skip) |
--perturbation-differential, --no-double-perturbation, --skip-umap, --smoke |
optional extras | — | off |
Outputs
--output-dir is a root, not a flat folder: the script creates three sub-directories on start-up (idempotently) and writes every artifact into the one that matches its type — <out>/Img/ figures, <out>/Stat/ tables & text, <out>/Rdata/ objects & binaries. A run ends by printing the contents of all three.
| File | Content |
|---|---|
<out>/Stat/dry_run_report.json |
stage dry-run: cell/gene counts, existing h/b counts, sample & gene overlap, PPI completeness |
<out>/Stat/run_config.json |
every resolved argument of the run (including the resolved --model-dir / processed-input paths) |
<out>/Rdata/processed_adata.h5ad, <out>/Stat/processed_bulk.csv |
stage prepare: HVG-aligned AnnData + bulk with duration, event first |
<out>/Rdata/model/vae_transfer, <out>/Rdata/model/deepCox, <out>/Rdata/model/W_matrix_{i}.csv, <out>/Rdata/model/adata_SIDISH.h5ad |
stage train (written by the library itself) |
<out>/Rdata/model/adata_SIDISH_embedding.h5ad |
stage infer: + obsm['latent'], obsm['X_umap'], obs['leiden'], obs['SIDISH_'], obs['SIDISH__'] |
<out>/Stat/results_cells.csv |
one row per cell: SIDISH (h/b), SIDISH_value (0/1), risk_value (continuous hazard), leiden, n_genes, (celltype_*) |
<out>/Stat/results_upregulated_genes.csv, <out>/Stat/results_downregulated_genes.csv |
Wilcoxon markers of High-Risk vs Background with logfc > threshold, pval < threshold |
<out>/Stat/results_perturbation_{percent_change,delta_change,p_flip,p_score}.csv |
per-gene perturbation scores, sorted descending |
<out>/Stat/results_double_perturbation_percent_change.csv, <out>/Stat/results_double_perturbation_pvalue.csv |
gene pairs keyed GENE1+GENE2 |
<out>/Img/umap_leiden.{pdf,png}, <out>/Img/umap_high_risk.{pdf,png}, <out>/Img/km_high_risk_vs_background.{pdf,png}, <out>/Img/top_perturbed_genes_binary.{pdf,png}, <out>/Img/top_perturbed_genes_continuous.{pdf,png}, <out>/Img/perturbation_umap_*.{pdf,png}, <out>/Img/double_perturbation_heatmap.{pdf,png} |
figures (SIDISH calls plt.show(); the script runs the Agg backend and saves the open figure) |
<out>/.sidish_work/ |
scratch dir (PPI symlink + CWD for the perturbation calls) — internal, not a deliverable |
--model-dir (default <out>/Rdata/model) and --processed-h5ad / --processed-bulk-csv (defaults <out>/Rdata/processed_adata.h5ad, <out>/Stat/processed_bulk.csv) are the only knobs that move files out of the layout; an explicit value always wins, and a --model-dir outside the root keeps its Rdata/-style checkpoint files wherever you point it.
Reading the results: risk_value is the Deep Cox output per cell (higher = worse predicted survival); SIDISH == 'h' ⇔ risk_value >= percentile_cells where percentile_cells is the --percentile quantile of the fitted hazard distribution (default 95th → ~5 % High-Risk cells; the local tutorial model gives 228/4102 = 5.6 % h). In perturbation tables, positive percent_change = knocking that gene pushes High-Risk cells back to Background (a candidate vulnerability); negative = the knockout increases risk. p_flip is a one-sided sign test on the flips, p_score tests the continuous shift.
Pitfalls
- GPU is effectively mandatory. Upstream README (local copy) states
环境:(需要GPU). Phase 1 runs 225 epochs and the full loop repeats the VAE + 500-epoch Deep Cox model--iterationstimes, plus one forward pass per cell per gene in the perturbation step. Treat "hours on a GPU box" as the realistic budget; there is no published CPU timing. - This container cannot run SIDISH. No
torch, noSIDISHpackage, no GPU here (verified:import torch→ModuleNotFoundError,import SIDISH→ModuleNotFoundError,/opt/conda/bin/python33.12.14). Only--stage dry-runis runnable locally; every other stage exits 3 with an install hint. Do not promise training results from this container.--smokeexists for a GPU-less sanity run but its convergence is UNVERIFIED. --model-dirmust end with/. The library builds checkpoint paths by string formatting ("{}vae_transfer".format(self.path)with noos.path.join); the script appends the separator for you, but hand-written calls must do the same.- SIDISH reads
'../PPI/*.txt'relative to the process CWD.ppi_network_handler.pydoesos.path.dirname('../data/')→'..', so the network is looked up in the parent of the working directory. The notebook comment ("脚本和data一定要放在平行文件夹") refers to this. The script copies the pattern by symlinking<output-dir>/.sidish_work/PPI -> --ppi-dirandchdir-ing into.sidish_work/run/for perturbation calls. - PPI files are not in the repo. The GitHub tree (checked) has no
PPI/directory and PyPI only ships the package.9606.protein.links.v11.5.txtmust come from STRING (https://stringdb-downloads.org/download/protein.links.v11.5/9606.protein.links.v11.5.txt.gz, verified 200) and the info file from the matchingprotein.info.v11.5URL (verified 200). HIPPIE:http://cbdm-01.zdv.uni-mainz.de/~mschaefer/hippie/hippie_current.txt(301 → 200, 138 MB). - The local
hippie_current.txtis a broken download. It is 9,557 bytes and starts with<!DOCTYPE html …— an HTML error page, not the 138 MB HIPPIE table.load_network()splits on tabs and silently skips lines with <5 fields, so nothing crashes: you just lose the HIPPIE contribution and keep STRING. Verify the file's first bytes before trusting a perturbation run. load_network()loads the whole 549 MB STRING file into RAM (f.readlines()[1:]) and builds a Python dict of interactions; andplot_perturbation_UMAP_default/differentialcallload_network()once per gene inside the loop (line ~982 / ~1069). On a 4-gene panel that is 4× the I/O. Budget tens of GB of RAM for the STRING step, and prefer running it once per process.- Gene symbols are required.
load_network()maps STRING proteins to gene symbols and keeps only pairs whose symbol is inadata.var.index. An.h5adindexed by Ensembl IDs gives an empty network → every "knockout" silently degrades to a plain single-gene zeroing, and the perturbation scores become meaningless. Checkvar_names[:5]first. get_percentille()always fits a Weibull, regardless of--distribution-fit:Utils.get_thresholdcallsweibull_min.fit/ppfdirectly, whiletrain()may have selected Weibull/Gamma/Exponential by AIC/BIC/KS/R². So the reload-side threshold can differ in family from the training-side labelling — keep--distribution-fitconsistent between train and infer, and expect theh/bcounts to shift slightly.get_MarkerGenes()mutates the AnnData in place (normalize_total+log1ponself.adata). Call it after the embeddings are saved (the script does), and don't reuse the object expecting raw counts — in--stage allthe script deliberately re-readsadata_SIDISH.h5adbefore the perturbation step so the hazard percentile is computed on the un-normalised matrix.run_Perturbation()re-reads<model-dir>/adata_SIDISH.h5adand overwritesself.adata, so it ignores anything you did in memory; the script re-reads the file and recomputesget_percentille()before it (as the notebook does).plot_perturbation_UMAP_*andrun_double_Perturbationalso re-read that file internally.--n-jobsis currently ignored:run_Perturbation(n_jobs=…)then hardcodesrun_parallel_processing(self.adata, n_jobs=4)upstream; the PPI threshold inside that call is likewise hardcoded to0.7while the UMAP panels use0.8. Reported here because it silently changes what you think you configured.plot_double_Perturbation_Heatmap()is broken upstream — it evaluatesself.percent_change[gene][id](builtinid) →TypeError. The reference notebook replaces it with plain matplotlib, and the script reproduces that fallback (Img/double_perturbation_heatmap.*). Note the double-perturbation keys are strings"GENE1+GENE2".plot_CellType_UMAP/plot_HighRisk_UMAPraiseValueErrorifcelltype=Trueand aresolutionis passed, and each call recomputes latent → neighbours → UMAP → Leiden (two calls = twice the work).plot_CellType_UMAPcolours by theleidencolumn, so it only works after clustering;--skip-umapavoids the cost.- Notebook inconsistency:
init_Phase1usesbatch_size=256in the training cell but512in the reload cells. Only architecture-defining arguments (--latent-size,--layer-dims) must match across train/reload; a mismatch there makesload_state_dictfail. - The tutorial
.h5adalready carriesSIDISH,SIDISH_value,risk_valuecolumns (168h/ 3934bindata/adata.h5ad) — leftovers from an earlier run. Training overwrites them; don't read them as ground truth.--label-keyonly selects whichobscolumn the script exports/compares. - Event status is validated strictly:
map_event_columnaccepts onlydead/alive/1/0and raisesValueErrorotherwise (e.g.censored,2,NaN). The--stage dry-runcheck flags this before you allocate a GPU. bulk.csvorientation: genes are rows and samples are columns (55,402 × 572 here); the notebook's.Tis what makespreprocess()'s "columns are genes" assumption true. A transposed input file silently yields 0 overlapping genes.- Memory model:
adata.Xis a densefloat32array andinit_Phase1allocatesW_matrix = np.ones(adata.X.shape). Large cohorts (10⁵ cells × 2 000 genes) mean ~800 MB per dense copy — subset cells rather than hoping.
Environment
This container (verified):
| Item | Value |
|---|---|
| Python | /opt/conda/bin/python3 → 3.12.14 (stdlib + /opt/user_python_base/lib/python3.12/site-packages) |
| Installed & usable | scanpy 1.12.1, anndata 0.12.16, pandas, numpy — enough for --stage dry-run |
| Not installed | SIDISH (ModuleNotFoundError), torch (ModuleNotFoundError), lifelines, shap, pyro-ppl |
| GPU | none — --device cuda will fail the preflight check |
| Verified working here | python3 -m py_compile scripts/sidish_cell_death.py (exit 0); --help (exit 0); --stage dry-run on the tutorial data (exit 0: 4102×1208 cells/genes, 1208/1208 gene overlap, 572/572 sample overlap, PPI complete) with the three output sub-directories Img/, Stat/, Rdata/ created and Stat/dry_run_report.json written |
| Not verified here | prepare / train / infer / perturb (need torch + SIDISH + GPU) — treat their library calls as read-from-source, not executed |
Upstream installation (per README, verified URLs):
conda create --name sidish_env python=3.12 && conda activate sidish_env
# install the PyTorch build matching your GPU first (https://pytorch.org/get-started/locally/)
git clone https://github.com/mcgilldinglab/SIDISH.git && cd SIDISH
pip install -e . # or: pip install . / pip install SIDISH==1.0.0
pip install -r requirements.txt
requirements.txt (upstream) pins: anndata 0.11.1, scanpy 1.10.4, numpy 1.26.4, pandas 2.2.3, scipy 1.14.1, scikit-learn 1.5.2, matplotlib 3.9.2, seaborn 0.13.2, shap 0.46.0, statsmodels 0.14.4, lifelines 0.30.0, pyro-ppl 1.9.1, leidenalg 0.10.2, python-igraph 0.11.8, imbalanced-learn 0.12.4, h5py 3.12.1, numba 0.60.0, torchvision 0.21.0, tqdm 4.67.1, umap-learn 0.5.7, bioinfokit 2.1.3.
Environment the reference notebook was produced in (a dedicated conda env, per the project README, which states 环境:(需要GPU)): python 3.12 with torch 2.6.0, scanpy 1.10.4, anndata 0.11.1, lifelines 0.30.0, shap 0.46.0, pyro-ppl 1.9.1, torch-geometric 2.7.0, matplotlib 3.9.2 (versions read from that environment's site-packages). Run the script with that interpreter (or an equivalent GPU env) for the training stages.
UNVERIFIED: no end-to-end SIDISH training was executed while writing this skill, so absolute runtimes, memory peaks and whether --smoke converges are unmeasured.