Bias detection and correction framework for genomic cancer AI
Project description
AncestryAudit
Bias detection and correction framework for genomic cancer AI.
Detects ancestry-linked performance gaps in copy number variation (CNV)-based cancer classifiers, applies McNemar's paired test for correction validation, and generates structured audit reports.
Developed from research on ancestry bias in TCGA-LIHC/STAD/ESCA classification (Yergaliyeva, 2026).
v0.3.0 — Statistical overhaul: Original bootstrap t-test replaced with label-permutation test (FPR: 85% → 0%). Correction module rewritten around McNemar's paired test. Power analysis added. Full methodology in paper.
Installation
pip install ancestryaudit
# or from source:
pip install -e .
Input Format
AncestryAudit works on any CNV feature matrix:
| Format | Shape | Notes |
|---|---|---|
np.ndarray |
(n_samples, n_genes) |
Continuous copy-number values |
pd.DataFrame |
(n_samples, n_genes) |
Column names = gene identifiers |
Column values: continuous copy number (e.g. TCGA ABSOLUTE pipeline output, where 2.0 = normal diploid, >2 = amplification, <2 = deletion).
Labels: binary integer (0 or 1), one per sample.
Models: any scikit-learn compatible estimator with fit / predict interface.
Quick Start
from ancestryaudit import AncestryAuditFramework
from sklearn.linear_model import LogisticRegression
framework = AncestryAuditFramework()
# Step 0: Check if you have enough data (run this first)
power = framework.power_analysis(n_source=451, n_target=242, expected_gap_pp=3.0)
# → UNDERPOWERED: need n_target≈836 for 80% power at 3pp
# Step 1: Detect ancestry-linked performance gap
report = framework.audit(
LogisticRegression(max_iter=1000),
X_western, y_western,
X_asian, y_asian
)
print(f"Gap: {report.gap_pp:.2f}pp, p={report.p_value:.4f}")
print(f"Recommendation: {report.recommendation}")
Output:
Gap: +2.39pp, p=0.0069
Recommendation: correction_required
Full Pipeline
from ancestryaudit import AncestryAuditFramework
from sklearn.linear_model import LogisticRegression
framework = AncestryAuditFramework(
random_state=42,
threshold_pp=2.0,
threshold_p=0.05
)
# ── Step 0: Power analysis (before anything else) ──────────────────────────
power = framework.power_analysis(
n_source=451, n_target=242, expected_gap_pp=3.0
)
# Tells you whether your data is sufficient before you run the audit
# ── Step 1: Filter population-stratification noise (optional) ──────────────
X_western_filtered, kept_genes, filter_log = framework.filter_stratification_noise(
X_western_df, gene_list
)
# ── Step 2: Audit ──────────────────────────────────────────────────────────
audit_report = framework.audit(
LogisticRegression(max_iter=1000),
X_western_filtered, y_western,
X_asian_filtered, y_asian
)
print(audit_report)
# AuditReport(gap=+2.39pp, p=0.0069, d=1.52,
# null_CI=[-2.10, 2.15], recommendation='correction_required')
# ── Step 3: Correct ────────────────────────────────────────────────────────
if audit_report.recommendation == "correction_required":
corrected_model, correction_report = framework.correct(
LogisticRegression(max_iter=1000),
X_western_filtered, y_western,
X_asian_labeled, y_asian_labeled,
n_samples=75
)
print(correction_report)
# CorrectionReport(delta=+1.43pp, McNemar p=0.031,
# direction='fine-tuned better')
# ── Step 4: Validate ───────────────────────────────────────────────────────
validation_report = framework.validate(
corrected_model,
X_asian_holdout, y_asian_holdout
)
# ── Step 5: Report ─────────────────────────────────────────────────────────
framework.generate_report("my_audit_report.json")
framework.summary()
API Reference
AncestryAuditFramework
| Method | Description | Returns |
|---|---|---|
power_analysis(n_source, n_target, expected_gap_pp) |
Check data sufficiency before audit | dict |
audit(model, X_source, y_source, X_target, y_target) |
Detect gap (permutation test) | AuditReport |
filter_stratification_noise(X, gene_list) |
Remove OR/pseudogene columns | (X_filtered, kept_genes, filter_log) |
correct(model, X_source, y_source, X_target_labeled, y_target_labeled, n_samples) |
Fine-tune + McNemar test | (corrected_model, CorrectionReport) |
validate(corrected_model, X_holdout, y_holdout) |
Post-correction audit | ValidationReport |
generate_report(save_path) |
Full JSON report | dict |
summary() |
Print pipeline summary | str |
AuditReport fields
| Field | Type | Description |
|---|---|---|
gap_pp |
float | Accuracy gap in percentage points (positive = source better) |
p_value |
float | Two-sided p-value from label-permutation test |
cohen_d |
float | Between-group effect size (pooled SD of per-sample correctness) |
null_ci |
tuple | 2.5/97.5 percentiles of permutation null — not a CI on the gap |
source_accuracy |
float | Model accuracy on held-out source data |
target_accuracy |
float | Model accuracy on target data |
n_source |
int | Source sample count |
n_target |
int | Target sample count |
recommendation |
str | "correction_required" or "no_action" |
CorrectionReport fields
| Field | Type | Description |
|---|---|---|
delta_pp |
float | Accuracy improvement on holdout (pp) |
p_value |
float | McNemar's test p-value |
n_used |
int | Target samples used for fine-tuning |
n_holdout |
int | Holdout samples used for evaluation |
mcnemar |
dict | b, c, p_value, direction, test_used |
refit_robustness |
dict | 5 refits varying model random_state (split fixed) |
direction_confirmed |
bool | True if McNemar p<0.05 and fine-tuned better |
baseline_accuracy |
float | Source-only accuracy on full target |
corrected_accuracy |
float | Fine-tuned accuracy on full target |
ValidationReport fields
| Field | Type | Description |
|---|---|---|
pre_gap |
float | Performance gap before correction (pp) |
post_gap |
float | Performance gap after correction (pp) |
correction_magnitude |
float | pre_gap - post_gap |
improvement_pp |
float | Accuracy improvement on target (pp) |
Statistical Design
audit() — Permutation test
Why not bootstrap? The original implementation used a bootstrap t-test that produced 85% false positive rate on null data (identical distributions). Root cause: testing whether a point estimate's resampling variance excludes zero is circular — it will always reject H0 on finite data regardless of whether a true effect exists.
Correct approach: Label-permutation test. Shuffle which samples are "source test" vs "target" 1000 times, recompute the gap under each shuffle, locate the observed gap in that null distribution. Verified: 0% FPR across 800 null trials.
correct() — McNemar's paired test
Why not bootstrap on folds? Three attempts at fold-based bootstrap (independent seeds, StratifiedKFold, RepeatedStratifiedKFold) all produced inflated FPR (22–26%) due to holdout overlap between folds at n_target=242.
Correct approach: McNemar's exact/chi-square test on a single fixed holdout. Tests whether the two classifiers disagree asymmetrically — exactly the right question. Verified: 0% FPR across 500 null trials.
Power Analysis
Critical: run power_analysis() before audit(). At TCGA-scale Asian
cohorts (n≈242), the minimum detectable effect is ~8–11pp. Realistic
ancestry-linked gaps are 1–5pp. The test is structurally underpowered
for this problem at current sample sizes.
result = framework.power_analysis(
n_source=451, n_target=242, expected_gap_pp=3.0
)
# → UNDERPOWERED
# → n_target needed: ≈836 (both sides must scale together)
# → Minimum viable study: n_target≥750, n_source≥1,500
Reference: Saldanha et al. (2024, Nature Medicine) documented 3–16pp ancestry-linked gaps in TCGA-trained cancer AI. Our MDE of 8.47pp exceeds the upper end of this range.
Filtering Details
filter_stratification_noise removes three gene categories:
- Olfactory receptor genes (
OR*) — ancestry-linked CNV unrelated to cancer - Pseudogenes (
*P,*P1,*P2, …) — non-functional, high stratification signal - Uncharacterized loci (names containing
.) — no biological interpretation
Methods disclosure: Feature space defined using all samples prior to train/test split (bounded data snooping). No label information used. (Kaufman et al., 2012)
Expected Results
Disclaimer: Results depend on model, train/test split, and preprocessing. Directional consistency (not numerical identity) with paper results is the correct validation criterion.
Null calibration (verified):
audit()permutation test: ~1-2% FPR (independent 800-trial replication)correct()McNemar test: 1.7% FPR (independent 300-trial replication)- Both well below 5% nominal α; McNemar exact is conservative by construction
Paper reproduction:
| Metric | Paper | Library |
|---|---|---|
| Mean PGI (LIHC/STAD) | +2.39pp | +2.393pp |
| Algorithms positive | 7/7 | 7/7 |
| p-value (permutation) | 0.0048 | verified |
Tests
python tests/test_null_calibration.py # FPR ≤ 20% on null data
python tests/test_power_calibration.py # flip_rate calibration regression
Citation
Yergaliyeva, D. (2026). Statistical pitfalls in ancestry-stratified
cancer genomic AI: Power analysis, circular-inference correction,
and structural confounds in TCGA CNV data.
[Manuscript in preparation]
License
MIT License. Copyright (c) 2026 Dana Yergaliyeva.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ancestryaudit-0.3.7.tar.gz.
File metadata
- Download URL: ancestryaudit-0.3.7.tar.gz
- Upload date:
- Size: 23.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d19161b6404cf1d2630652753867983af4c8269e19844c772a18785f4168b33
|
|
| MD5 |
37b810e6e92e8a2bc3d9feffc1bae6fd
|
|
| BLAKE2b-256 |
018b30d338384168ea68442ae982f60229fe06a9f4c0e720824d0977ca3e7ca5
|
File details
Details for the file ancestryaudit-0.3.7-py3-none-any.whl.
File metadata
- Download URL: ancestryaudit-0.3.7-py3-none-any.whl
- Upload date:
- Size: 20.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51e4c2248151929dab4fb9e64ba3b1d3adb4901e864b676bb0d561fd511b823c
|
|
| MD5 |
bfeabd18251c15eb93c71019b2f40efc
|
|
| BLAKE2b-256 |
b5d627530567ed72916f2c0be9eb06ef3f44d371a70d5986cecf7b04f102ac2b
|