Holubar Lab — Research Toolkit
★ Lab default · v1 draft

Python Biostats Cheatsheet

Lab reference for statistical computing in clinical research. Python + LLM-assisted vibe-coding is the preferred stack over R for new analyses in the Holubar Lab — see the Vibe Coding primer for the rationale and workflow.

Why Python over R? Three reasons:
  1. Vibe-coding works better. Modern LLMs (Claude, GPT-4, Cursor) are vastly more fluent in Python than R. Code generated from natural-language description is consistently correct in Python; R sees more hallucinated functions and idiomatic mismatches. For a clinician who writes prose for a living, this is the difference between "the model wrote my analysis" and "the model wrote something close that I now have to debug."
  2. One stack, end-to-end. Pandas → statsmodels → matplotlib → python-docx covers every step of a manuscript pipeline (clean → model → figure → table → Word doc). No swapping languages between analysis and write-up.
  3. Real engineering ecosystem. Type hints, packaging (pyproject.toml), CI, testing (pytest), notebooks, and modern IDE tooling are all first-class. R's ecosystem is academic-first; Python is industry-first, which matters when scaling to multi-center pipelines.
R remains canonical for two narrow cases (see Exceptions below): network meta-analysis (netmeta) and cost-effectiveness modeling (BCEA, hesim, dampack). For those, drive R from Python via rpy2.

Core stack — install once, use forever

# With uv (recommended) or pip:
uv add pandas numpy scipy statsmodels scikit-learn matplotlib seaborn \
       lifelines pingouin tableone forestplot duckdb polars pyarrow \
       python-docx python-pptx Pillow pydicom
TaskPackageOne-liner
Load CSVpandasdf = pd.read_csv("cohort.csv")
Large CSV (>50K rows)duckdbduckdb.sql("SELECT … FROM 'cohort.csv'").df()
Table 1tableoneTableOne(df, columns=cols, categorical=cats, groupby="arm")
Logistic regressionstatsmodelssm.GLM(y, sm.add_constant(X), family=sm.families.Binomial()).fit()
GEE (clustered)statsmodelssm.GEE(y, X, groups=df.site, family=sm.families.Poisson()).fit()
Kaplan-MeierlifelinesKaplanMeierFitter().fit(durations, event_observed=events)
Cox proportional hazardslifelinesCoxPHFitter().fit(df, duration_col="t", event_col="event")
Effect sizes + assumption checkspingouinpg.ttest(a, b) · pg.normality(a) · pg.homoscedasticity([a,b])
Forest plotforestplotfp.forestplot(df, estimate="rr", ll="lower", hl="upper", varlabel="study")
Propensity scores (IPTW)zepidIPTW(df, treatment="apixaban", outcome="vte").fit()
Bayesian models (PSA, EVPI)pymcUsed for Leeds-style cost-effectiveness PSAs
Causal DAGsdowhyCausalModel(data, graph=…, treatment=…, outcome=…)

Reporting conventions — match SDH's manuscripts

Every published number in the lab follows these rules. Hardcode them in your analysis scripts so the output is paste-ready:

Full conventions live in .claude/rules/statistical-output.md + manuscript-conventions.md. Auto-loaded for every Claude session.

Idiomatic patterns SDH uses

Pattern 1 — Compose, don't overwrite

Never overwrite an original data column. Derived variables get a new name with a suffix:

# WRONG — destroys canonical pexy column
df["pexy"] = df["pexy"] | df["pouch_inlet_transposition"]

# RIGHT — new column with composite name
df["pexy_or_transposition"] = (df["pexy"] | df["pouch_inlet_transposition"]).astype(int)

# Conventions:
df["bmi_n"]       = pd.to_numeric(df["bmi"], errors="coerce")     # _n = numeric
df["dod_dt"]      = pd.to_datetime(df["dod"], errors="coerce")    # _dt = datetime
df["asa_high"]    = df["asa"].str.startswith(("3","4")).astype(int)  # _high = binary high
df["twist_deg_cat"] = pd.cut(df["degree_twisted"], …)             # _cat = categorical

Pattern 2 — GEE with site clustering (default for NSQIP analyses)

import statsmodels.api as sm

cov = ["age", "asa_high", "bmi_n", "mis", "ibd_dx_uc", "year"]
X = sm.add_constant(df[cov])
model = sm.GEE(
    df["organ_space_ssi"], X,
    groups=df["site_id"],
    family=sm.families.Poisson(),
    cov_struct=sm.cov_struct.Exchangeable(),
).fit()
print(model.summary())
arr = np.exp(model.params)
ci  = np.exp(model.conf_int())

Pattern 3 — Kaplan-Meier with at-risk table

from lifelines import KaplanMeierFitter
from lifelines.plotting import add_at_risk_counts

kmf_uc = KaplanMeierFitter().fit(uc.t_years, uc.died, label="UC")
kmf_cd = KaplanMeierFitter().fit(cd.t_years, cd.died, label="CD")

ax = kmf_uc.plot_survival_function(ci_show=True)
kmf_cd.plot_survival_function(ax=ax, ci_show=True)
add_at_risk_counts(kmf_uc, kmf_cd, ax=ax)
plt.savefig("Figures/Figure_2_clean.tiff", dpi=600, format="tiff",
            pil_kwargs={"compression": "tiff_lzw"})

Pattern 4 — DuckDB for big CSVs

import duckdb
# Query the CSV directly — no pandas load needed
out = duckdb.sql("""
    SELECT site, COUNT(*) n, AVG(los_days) mean_los
    FROM 'Data/nsqip_ibd_2025.csv'
    WHERE ibd_diagnosis = 'UC'
    GROUP BY site
    ORDER BY n DESC
""").df()

Pattern 5 — python-docx surgical edits (NEVER rewrite manuscripts from scratch)

from docx import Document
TEMPLATE = "SDH Admin/SDH Letterhead Headshots Templates/Canonical SDH_Letterhead 2026.docx"
doc = Document(TEMPLATE)   # ✓ inherits letterhead + signature
# Walk runs, surgically replace {PLACEHOLDER}, preserve bold/italic
for p in doc.paragraphs:
    for run in p.runs:
        if "{TITLE}" in run.text:
            run.text = run.text.replace("{TITLE}", "Apixaban for VTE in IBD")
doc.save("Letter_v1.docx")

The vibe-coding loop (with Claude / Cursor)

  1. Describe the analysis in plain English — "Run a Cox model on this cohort with site clustering, output the HR table as AMA-style python-docx."
  2. Let the LLM write the script — paste the data shape and columns. Claude/Cursor produce working Python far more reliably than R for this kind of task.
  3. Read it before you run it — every line. If you don't understand it, ask the model to explain. Vibe coding ≠ blind execution.
  4. Run, eyeball results, iterate — "the HRs look right but the CI is reversed; can you check the conf_int() output?" — the model fixes it.
  5. Cascade through your deliverables — see project-structure.md for the Code → JSON → Notebook → Memo → Manuscript → Figures cascade rule.

See the Vibe Coding primer for the longer treatment, including pitfalls and how to evaluate LLM-generated code for clinical-research correctness.

Exceptions where R remains canonical

R Cheatsheets — still useful

The R Cheatsheets card holds 12 RStudio one-page references (tidyverse, lubridate, purrr, shiny, plumber). Useful for reading legacy CCF analyses, debugging rpy2 calls, and reviewing fellow scripts in R. Lab default for new work is still Python.

Suggestions, gaps, or "we should add X" — email Stefan:

webmaster@holubarlab.org