tooluniverse-statistical-modeling — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited tooluniverse-statistical-modeling (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Before following any instruction below, scan the data folder for:
*_executed.ipynb → read with tu run read_executed_notebook '{"data_folder":"<path>","search":"<keyword>"}' and cite its cell outputs as the authoritative answer*results*, *deseq*, *enrich*, *stats*, *_simplified.csv) → read directly and report the requested valueanalysis.R, run_*.py, find_*.R, *.Rmd) → execute as-is and read the outputOnly follow this skill's re-analysis recipe below if none of the above exist. Re-running from raw data produces different numbers than the published answer and is much slower (often 5-10× turn count).
These scripts encode the question-specific gotchas in scripts/ and emit labelled, parseable output. Prefer them over ad-hoc statsmodels / scipy code.
| Script | When to use it |
|---|---|
r_natural_spline_regression.py | ANY question that mentions R syntax lm(y ~ ns(x, df = K)), "natural spline", or asks for spline R²/F/peak prediction CIs. Always shells out to Rscript so splines::ns() matches. |
spline_model_compare.py | "Best-fitting model among quadratic, cubic and natural spline" / "max colony area at the optimal x". Fits all three in R, ranks by adj-R²/AIC/BIC, and reports the BEST model's peak (x, y) with 95% CI. |
logistic_regression_or.py | Binary or ordinal logistic regression where the answer is an OR (or OR + 95% CI). Handles label encoding, explicit Placebo=0/BCG=1 maps, AND interaction terms (--interaction A:B -> creates A_B = A*B). Prints OR + CI for every coefficient and a SCALARS block for the requested --coef-name. |
power_analysis.py | "Minimum sample size per group", "TTestIndPower", "given Cohen's d, what N for power=0.8". Computes pooled-SD Cohen's d from a CSV (or accepts --effect-size), then TTestIndPower.solve_power. |
expression_anova.py | Per-gene ANOVA / median LFC across cell types or sample groups (NOT pooled across genes — see warnings below). |
prepare_ae_cohort.py | Clinical-trial AE severity tests (chi-square / ordinal) on SDTM DM/AE files (encoding='latin1', max(AESEV) per subject across ALL AEs — no AEPT filter). |
stat_tests.py | Stdlib-only chi-square goodness-of-fit, Fisher's exact, simple OLS. Use when scipy/statsmodels aren't available. |
Natural-spline regression (R^2, overall F-test p, peak Y + 95% CI):
python skills/tooluniverse-statistical-modeling/scripts/r_natural_spline_regression.py \
--csv data.csv --y-col Area \
--ratio-col Ratio --new-x-col Frequency_strain \
--filter "StrainNumber not in ['1', '98']" \
--df 4 --workdir /tmp/spline_runQuadratic vs cubic vs natural-spline comparison + best-model peak:
python skills/tooluniverse-statistical-modeling/scripts/spline_model_compare.py \
--csv data.csv --y-col Area \
--ratio-col Ratio --new-x-col Frequency_strain \
--filter "StrainNumber not in ['1', '98']" \
--ns-df 4 --workdir /tmp/spline_cmp*Report the peak location (`x) in the units of the fitted x-variable, not a derived label.** When the model is fit on a frequency/proportion column (e.g. Frequency_strain, a 0–1 value), the answer to "at what ratio/frequency is the maximum" is that fraction (e.g. 0.909), NOT the colon-ratio it was derived from (e.g. 10:1). Convert a colon ratio a:b to the fraction a/(a+b)` when the question expects a 0–1 value or the fitted x-column is a fraction.
Ordinal logistic regression with interaction term (e.g. trial AE severity):
python skills/tooluniverse-statistical-modeling/scripts/logistic_regression_or.py \
--csv merged.csv --outcome AESEV --outcome-type ordinal --outcome-order "1,2,3,4" \
--predictors TRTGRP,expect_interact,patients_seen,MHONGO \
--encode TRTGRP,expect_interact,patients_seen \
--encode-map "TRTGRP:Placebo=0,BCG=1" \
--interaction MHONGO:TRTGRP_cat \
--coef-name TRTGRP_catTwo-sample power analysis from a pilot CSV:
python skills/tooluniverse-statistical-modeling/scripts/power_analysis.py \
--csv pilot.csv --value-col MeasuredValue --group-col Group \
--group-a Treatment --group-b Control \
--power 0.8 --alpha 0.05The input data folder for any analysis must remain untouched so re-runs are reproducible. Scripts that write intermediate files (R drivers, prepared CSVs, comparison tables) must write to /tmp/ or to a --workdir you pass in. Both R-based scripts in this skill refuse to run if --workdir resolves to the input CSV's parent directory (or any ancestor of it).
# OK
--workdir /tmp/spline_run
# Refused:
--workdir <path-equal-to-or-containing-the-input-csv>/...clinical_trial_ae_severity_test ToolUniverse tool which wraps it): tu run clinical_trial_ae_severity_test '{"dm_file":"DM.csv","ae_file":"AE.csv","test":"chi-square","group_col":"TRTGRP"}'
# Or directly:
python skills/tooluniverse-statistical-modeling/scripts/prepare_ae_cohort.py \
--dm DM.csv --ae AE.csv --test chi-square --group TRTGRP \
--subgroup "expect_interact=Yes" # optionalThe script/tool handles: encoding='latin1' for SDTM CSVs, max(AESEV) per subject across ALL AEs (no AEPT filtering), inner join with DM, optional subgroup filter, optional ordinal-logistic with covariates.
Why no AEPT filter — AESEV is a protocol-defined severity scale on the AE table. Filtering AE by AEPT (e.g. keeping only AEPT == "COVID-19") drops subjects whose worst severity was recorded under a different AEPT label, drastically changes the contingency table, and can flip the test result. The phrase "COVID-19 severity" describes the OUTCOME, NOT a filter criterion.
ae[ae['AEPT'].str.contains('COVID-19')].groupby('USUBJID')['AESEV'].max() — filters to COVID-19 eventsae.groupby('USUBJID')['AESEV'].max() — uses ALL AE recordsFor "the F-statistic" or "a fold change" as a single value, run per-gene then summarize — NEVER pool expr.values.ravel() across all genes.
design=~<group>, extract per-gene log2FoldChange (with shrinkage if the pipeline uses it), then take median/mean across genes.❌ WRONG (aggregate): log2(sum_counts_groupA / sum_counts_groupB) per sample then summarize — gives ratio of totals, dominated by high-expression genes. ✅ RIGHT (per-gene): DESeq2 → results_df['log2FoldChange'].median().
Sanity heuristics: F > 50 for biological ANOVA across a few groups means you aggregated (typical biological F is 0.5–10). |median LFC| > 2 between similar groups means you aggregated (typical |median| < 1).
Use the bundled script: python skills/tooluniverse-statistical-modeling/scripts/expression_anova.py (or the expression_anova_per_gene ToolUniverse tool).
splines::ns(x, df=K) ≠ Python patsy.dmatrix("cr(x, df=K)"). They produce different design matrices because of internal-knot placement, boundary-knot placement, and basis orthogonalization. For ANY question that references R syntax like lm(y ~ ns(x, df = 4)), run R via Rscript. Use the bundled wrapper: python skills/tooluniverse-statistical-modeling/scripts/r_natural_spline_regression.py \
--csv data.csv --y-col Y --x-col X --df 4 --workdir /tmp/spline_runFor "frequency of strain X" co-culture models, include pure focal strain (freq=1) but exclude non-focal pure strain (freq=0).
encoding='latin1'.spans orders of magnitude (raw read counts, TPM, gene length, transcript abundance), raw Pearson r is often near 0 even when log-transformed r is moderate. ALWAYS compute and explicitly report ALL FOUR variants in your final answer body: r(x, y), r(log10(x+1), y), r(x, log10(y+1)), r(log10(x+1), log10(y+1)). List as a table; mark one as your primary pick. The published answer can be ANY of the four, and the question text rarely disambiguates which transform combination was used.
## Primary answer: r = X.XXX (transform: <name>)
## Sensitivity (all 4 transform combinations):
- r(x, y) = ...
- r(log10(x+1), y) = ...
- r(x, log10(y+1)) = ...
- r(log10(x+1), log10(y+1)) = ...Background — for any single transform variant:
import numpy as np
from scipy.stats import pearsonr
r_raw, _ = pearsonr(x, y)
r_log, _ = pearsonr(np.log10(x + 1), y)
print(f"r_raw={r_raw:.4f} r_log10={r_log:.4f}")Defaults:
|r_raw| < 0.1 AND |r_log10| > 0.2, prefer r_log10❌ WRONG: report only r_raw ≈ 0.05 when log is 0.35 ✅ RIGHT: "r_raw = 0.05; r_log10 = 0.35 (canonical for log-distributed expression)"
Write and run Python code (via Bash) for every statistical analysis. Never describe what you "would do" — do it. Use pandas for data wrangling, statsmodels for regression, scipy for tests, and matplotlib for plots. Execute the code and report actual numbers (β, p-value, CI, N).
When uncertain about any scientific fact, SEARCH databases first rather than reasoning from memory.
Apply this skill when user asks:
START: What type of outcome variable?
|
+-- CONTINUOUS (height, blood pressure, score)
| +-- Independent observations -> Linear Regression (OLS)
| +-- Repeated measures -> Mixed-Effects Model (LMM)
| +-- Count data -> Poisson/Negative Binomial
|
+-- BINARY (yes/no, disease/healthy)
| +-- Independent observations -> Logistic Regression
| +-- Repeated measures -> Logistic Mixed-Effects (GLMM/GEE)
| +-- Rare events -> Firth logistic regression
|
+-- ORDINAL (mild/moderate/severe, stages I/II/III/IV)
| +-- Ordinal Logistic Regression (Proportional Odds)
|
+-- MULTINOMIAL (>2 unordered categories)
| +-- Multinomial Logistic Regression
|
+-- TIME-TO-EVENT (survival time + censoring)
+-- Regression -> Cox Proportional Hazards
+-- Survival curves -> Kaplan-MeierGoal: Load data, identify variable types, check for missing values.
CRITICAL: Identify the Outcome Variable First
Before any analysis, verify what you're actually predicting:
Common mistake: Question mentions "obesity" -> Assumed outcome = BMI >= 30 (circular logic with BMI predictor). Always check data columns first: print(df.columns.tolist())
import pandas as pd
import numpy as np
df = pd.read_csv('data.csv')
print(f"Observations: {len(df)}, Variables: {len(df.columns)}, Missing: {df.isnull().sum().sum()}")
for col in df.columns:
n_unique = df[col].nunique()
if n_unique == 2:
print(f"{col}: binary")
elif n_unique <= 10 and df[col].dtype == 'object':
print(f"{col}: categorical ({n_unique} levels)")
elif df[col].dtype in ['float64', 'int64']:
print(f"{col}: continuous (mean={df[col].mean():.2f})")Goal: Fit appropriate model based on outcome type.
Use the decision tree above to select model type, then refer to the appropriate reference file for detailed code:
references/linear_models.mdreferences/logistic_regression.mdreferences/ordinal_logistic.mdreferences/cox_regression.mdanova_and_tests.mdQuick reference for key models:
import statsmodels.formula.api as smf
import numpy as np
# Linear regression
model = smf.ols('outcome ~ predictor1 + predictor2', data=df).fit()
# Logistic regression (odds ratios)
model = smf.logit('disease ~ exposure + age + sex', data=df).fit(disp=0)
ors = np.exp(model.params)
ci = np.exp(model.conf_int())
# Cox proportional hazards
from lifelines import CoxPHFitter
cph = CoxPHFitter()
cph.fit(df[['time', 'event', 'treatment', 'age']], duration_col='time', event_col='event')
hr = cph.hazard_ratios_['treatment']When data has multiple features (genes, miRNAs, metabolites), use per-feature ANOVA (not aggregate). This is the most common pattern in genomics.
See anova_and_tests.md for the full decision tree, both methods, and worked examples.
Default for gene expression data: Per-feature ANOVA (Method B).
Goal: Check model assumptions and fit quality.
Key diagnostics by model type:
cph.check_assumptions()See references/troubleshooting.md for diagnostic code and common issues.
Goal: Generate publication-quality summary.
For every result, report: effect size (OR/HR/coefficient), 95% CI, p-value, and model fit statistic. See common_patterns_summary.md for common question-answer patterns.
| Pattern | Question Type | Key Steps |
|---|---|---|
| 1 | Odds ratio from ordinal regression | Fit OrderedModel, exp(coef) |
| 2 | Percentage reduction in OR | Compare crude vs adjusted model |
| 3 | Interaction effects | Fit A * B, extract A:B coef |
| 4 | Hazard ratio | Cox PH model, exp(coef) |
| 5 | Multi-feature ANOVA | Per-feature F-stats (not aggregate) |
See common_patterns_summary.md for solution code for each pattern. See references/common_patterns.md for 15+ detailed question patterns.
| Use Case | Library | Reason |
|---|---|---|
| Inference (p-values, CIs, ORs) | statsmodels | Full statistical output |
| Prediction (accuracy, AUC) | scikit-learn | Better prediction tools |
| Mixed-effects models | statsmodels | Only option |
| Regularization (LASSO, Ridge) | scikit-learn | Better optimization |
| Survival analysis | lifelines | Specialized library |
General rule: Use statsmodels for statistical inference questions (p-values, ORs, HRs).
statsmodels>=0.14.0
scikit-learn>=1.3.0
lifelines>=0.27.0
pandas>=2.0.0
numpy>=1.24.0
scipy>=1.10.0| Grade | Criteria | Example |
|---|---|---|
| Strong | p < 0.001, effect size clinically meaningful, model assumptions met | OR = 3.5 (95% CI: 2.1-5.8), p < 0.001, Hosmer-Lemeshow p > 0.05 |
| Moderate | p < 0.05, reasonable effect size, minor assumption concerns | HR = 1.8 (95% CI: 1.1-2.9), p = 0.02, borderline PH test |
| Weak | p < 0.05 but wide CI, small effect, or assumption violations | OR = 1.2 (95% CI: 1.01-1.43), p = 0.04, VIF > 5 for a covariate |
| Insufficient | p >= 0.05, or model fails convergence/diagnostics | Non-significant coefficient with model separation warning |
Before finalizing any statistical analysis:
These ready-to-run scripts live in skills/tooluniverse-statistical-modeling/scripts/. Use them via the Bash tool — they are the deterministic answer for the recurring question patterns documented above.
r_natural_spline_regression.py — Natural spline regression in RShells out to Rscript to fit lm(y ~ ns(x, df=K)) with splines::ns(). Emits R², adj R², F-stat with df1/df2, overall F-test p-value, residual SE, coefficient table (estimate, SE, t, p), and the prediction-grid peak with 95% CI from predict.lm(..., interval='confidence'). Supports a --ratio-col shortcut to convert "a:b" string ratios into a frequency fraction a/(a+b). Refuses to write into the input CSV's parent directory.
spline_model_compare.py — Quadratic vs cubic vs natural splineFits all three models on the same x,y in R, ranks by adjusted R², AIC, and BIC, and emits the best model's peak (x, y) with 95% CI. Use for "best-fitting model" questions and "maximum predicted y at optimal x".
logistic_regression_or.py — Binary or ordinal logistic regression with ORsFits sm.Logit (binary) or OrderedModel (ordinal proportional-odds) and emits ORs (exp(coef)) plus 95% CIs and p-values for every coefficient. Handles label encoding (--encode A,B,C), explicit value maps (--encode-map TRTGRP:Placebo=0,BCG=1), and interaction columns (--interaction A:B -> creates A_B = A*B). With --coef-name <NAME> also prints a SCALARS block tagged for the requested coefficient.
power_analysis.py — Two-sample required-N for a t-testComputes Cohen's d (pooled SD) from a CSV given --value-col, --group-col, --group-a, --group-b, then TTestIndPower.solve_power with --alpha, --power, --alternative. Use for "minimum sample size per group" power questions. Returns both the raw and the ceil-ed N.
stat_tests.py — Basic statistical tests (pure stdlib, no scipy)Implements chi-square goodness-of-fit, Fisher's exact test, and simple linear regression without any external dependencies. All p-values are computed from first principles using the gamma function (chi-square) or hypergeometric enumeration (Fisher's).
# Chi-square goodness-of-fit
python stat_tests.py --type chi_square --observed 100,50,25 --expected 87.5,50,37.5
# Fisher's exact test (2×2 table)
python stat_tests.py --type fisher_exact --a 10 --b 5 --c 3 --d 20
python stat_tests.py --type fisher_exact --a 10 --b 5 --c 3 --d 20 --alternative greater
# Simple linear regression (OLS)
python stat_tests.py --type regression --x "1,2,3,4,5" --y "2.1,4.0,5.9,8.1,10.0"Key formulas:
chi_square: χ² = Σ (O−E)²/E; p-value via upper regularized incomplete gamma Q(df/2, χ²/2)fisher_exact: hypergeometric PMF; p-value = sum of probabilities ≤ P(observed)regression: b1 = Sxy/Sxx; b0 = ȳ − b1x̄; R² = 1 − SSR/SST; SE and t-statistics includedOutput includes: full contingency/data table, step-by-step arithmetic, significance statement, and a round-trip verification for each test.
When to use stat_tests.py vs statsmodels:
stat_tests.py when you need a quick sanity check with no imports, or when theenvironment lacks scipy/statsmodels.
format_statistical_output.py — Format results for reportingUtility functions to format fitted statsmodels results as publication-ready tables. Import and call from analysis scripts; not a standalone CLI tool.
model_diagnostics.py — Automated model diagnosticsRuns assumption checks (normality, heteroscedasticity, multicollinearity) on fitted models. Import and call from analysis scripts; not a standalone CLI tool.
tooluniverse-statistical-modeling/
+-- SKILL.md # This file (workflow guide)
+-- QUICK_START.md # 8 quick examples
+-- EXAMPLES.md # Legacy examples
+-- TOOLS_REFERENCE.md # ToolUniverse tool catalog
+-- anova_and_tests.md # ANOVA decision tree and code
+-- common_patterns_summary.md # Common solution patterns
+-- test_skill.py # Test suite
+-- references/
| +-- logistic_regression.md # Detailed logistic examples
| +-- ordinal_logistic.md # Ordinal logit guide
| +-- cox_regression.md # Survival analysis guide
| +-- linear_models.md # OLS and mixed-effects
| +-- common_patterns.md # 15+ question patterns
| +-- troubleshooting.md # Diagnostic issues
+-- scripts/
+-- r_natural_spline_regression.py # lm(y ~ ns(x, df=K)) via Rscript
+-- spline_model_compare.py # quadratic vs cubic vs natural-spline (Rscript)
+-- logistic_regression_or.py # binary / ordinal logistic + ORs + interactions
+-- power_analysis.py # TTestIndPower required-N from CSV
+-- expression_anova.py # per-gene ANOVA / log2FC summary
+-- prepare_ae_cohort.py # SDTM AE/DM cohort prep
+-- stat_tests.py # Chi-square, Fisher's exact, OLS (stdlib)
+-- format_statistical_output.py # Format results for reporting
+-- model_diagnostics.py # Automated diagnosticsWhile this skill is primarily computational, ToolUniverse tools can provide data:
| Use Case | Tools |
|---|---|
| Clinical trial data | search_clinical_trials |
| Drug safety outcomes | FAERS_calculate_disproportionality |
| Gene-disease associations | OpenTargets_target_disease_evidence |
| Biomarker data | fda_pharmacogenomic_biomarkers |
See TOOLS_REFERENCE.md for complete tool catalog.
These conventions are validated best practices. Apply when the dataset/question matches.
For per-gene ANOVA or median log2FC questions, use the bundled script:
python skills/tooluniverse-statistical-modeling/scripts/expression_anova.py \
--counts counts.csv --meta meta.csv --group-col cell_type \
--exclude-groups PBMC --mode anova
python skills/tooluniverse-statistical-modeling/scripts/expression_anova.py \
--counts counts.csv --meta meta.csv --group-col cell_type \
--group-a CD14 --group-b CD19 --mode fold_changeDo NOT write your own pandas ANOVA — the aggregation level (per-gene, not per-sample) is critical and easy to get wrong.
Clinical-trial exports (SDTM) are often latin1. If pd.read_csv() fails with UnicodeDecodeError, retry with encoding='latin1'.
For any statistical test of a clinical-trial AE severity outcome (chi-square, ordinal/logistic regression, Mann-Whitney, etc.) on trial covariates, fit/test on the cohort that reported any AE (inner-joined to demographics). Do NOT pre-filter AE records by AEPT — NOT even when the question says "COVID-19 severity" or names any specific condition. Use max(AESEV) across all AE records per subject, regardless of AEPT.
Why: AESEV on the AE table reflects the study's protocol-defined severity scale. Pre-filtering to specific AEPT values (e.g., keeping only certain condition labels) drops subjects whose worst severity was recorded under a different AEPT label, which drastically changes the contingency table and can flip results from significant to non-significant.
Do NOT pad subjects with no AEs as AESEV=0 — that dilutes the signal.
dm = pd.read_csv("DM.csv", encoding='latin1')
ae = pd.read_csv("AE.csv", encoding='latin1')
sev = ae.groupby('USUBJID')['AESEV'].max().reset_index()
df = dm.merge(sev, on='USUBJID', how='inner').dropna(subset=['AESEV'])
df['AESEV'] = df['AESEV'].astype(int)
# Now ordinal regression / chi-square on df"Percentage reduction in odds ratio" means (1 − OR) × 100% — deviation from OR=1 (no effect). OR=0.75 → 25% reduction vs reference. Do NOT interpret as (unadjusted_OR − adjusted_OR) / unadjusted_OR; that's almost always ≈0% because adjustment barely moves a well-specified OR.
scipy.stats.f_oneway(g1, g2, ...) returns (F, p). If GT looks like (0.76, 0.78) and you computed F=91.6, you returned the F-statistic when the question asked for the p-value (or vice-versa). Always re-read the question for which the answer expects.
When asked for "F-statistic comparing expression levels across cell types/groups":
f_oneway(group1_values, group2_values, ...) where each group has N gene-level valuesWhen asked for "median log2 fold change between group A and group B":
log2(mean_expr_A / mean_expr_B)Bundled script for both ANOVA and fold change:
# Per-gene ANOVA across cell types
python skills/tooluniverse-statistical-modeling/scripts/expression_anova.py \
--counts counts.csv --meta meta.csv --group-col cell_type \
--exclude-groups PBMC --mode anova
# Per-gene log2FC between two groups
python skills/tooluniverse-statistical-modeling/scripts/expression_anova.py \
--counts counts.csv --meta meta.csv --group-col cell_type \
--group-a CD14 --group-b CD19 --mode fold_changeWhen fitting models on strain co-culture frequency data:
"10:1", convert to a frequency fraction: first / (first + second) = 0.909. Report as a fraction in [0, 1], not as ratio notation.ns() (from the splines package) and Python's patsy.cr() or scipy BSpline produce DIFFERENT knot placements and boundary conditions. If the question references R's lm(y ~ ns(x, df=4)), use the bundled wrapper which runs R via Rscript: python skills/tooluniverse-statistical-modeling/scripts/r_natural_spline_regression.py \
--csv data.csv --y-col Area --x-col Frequency \
--df 4 --workdir /tmp/spline_runThat emits R², adjusted R², overall F-test p-value, the coefficient table, and the prediction grid with peak (x, y) and 95% confidence interval at the peak.
Do NOT substitute Python's patsy.dmatrix("cr(x, df=4)") — it will give different R², F-statistics, and predictions.
which.max(pred[,"fit"]) plus its CI from interval="confidence". If you must do this by hand, follow the same recipe; do not use a coarse grid. python skills/tooluniverse-statistical-modeling/scripts/spline_model_compare.py \
--csv data.csv --y-col Area --x-col Frequency \
--ns-df 4 --workdir /tmp/spline_cmpThe output's BEST_BY_ADJ_R2 row tells you which model wins, and BEST_PEAK_Y is the peak y to report.
For detailed examples and troubleshooting:
references/logistic_regression.mdreferences/ordinal_logistic.mdreferences/cox_regression.mdreferences/linear_models.mdreferences/common_patterns.mdanova_and_tests.mdreferences/troubleshooting.md~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.