Initial release: rarelens platform skeleton (AGPL-3.0)
ci / api (push) Failing after 10s
ci / terraform (push) Failing after 11s
ci / web (push) Failing after 35s
ci / pipeline (push) Failing after 2m29s
ci / images (api) (push) Skipped
ci / images (ml) (push) Skipped
ci / images (pipeline) (push) Skipped
ci / images (web) (push) Skipped

End-to-end variant interpretation platform for rare genetic disease research:
SvelteKit UI, FastAPI + PostgreSQL API, Nextflow/Ensembl VEP pipeline,
LightGBM pathogenicity scoring with MLflow, K8s/ArgoCD/GCP infrastructure.
Public test data only; no clinical claims.
This commit is contained in:
2026-09-11 16:55:35 +01:00
commit 5463f489a3
74 changed files with 4597 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
FROM python:3.12-slim
WORKDIR /ml
RUN pip install --no-cache-dir uv
COPY pyproject.toml .
RUN uv pip install --system -e .
COPY rarelens_ml ./rarelens_ml
ENTRYPOINT ["python", "-m", "rarelens_ml.train"]
+8
View File
@@ -0,0 +1,8 @@
[project]
name = "rarelens-ml"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["lightgbm>=4.5", "mlflow>=2.16", "pandas", "scikit-learn", "sqlalchemy", "psycopg[binary]"]
[project.optional-dependencies]
gpu = ["torch"] # for the optional deep-learning baseline on GPU
View File
+16
View File
@@ -0,0 +1,16 @@
"""Feature engineering shared by training and serving. Keep this identical to api/app/services/scoring.py."""
import pandas as pd
IMPACT_ORDER = {"MODIFIER": 0, "LOW": 1, "MODERATE": 2, "HIGH": 3}
CATEGORICAL = ["consequence"]
NUMERIC = ["impact_rank", "gnomad_af", "cadd_phred", "am_pathogenicity"]
def build(df: pd.DataFrame) -> pd.DataFrame:
out = pd.DataFrame()
out["impact_rank"] = df["impact"].map(IMPACT_ORDER).fillna(0)
out["gnomad_af"] = pd.to_numeric(df["gnomad_af"], errors="coerce").fillna(0.0)
out["cadd_phred"] = pd.to_numeric(df["cadd_phred"], errors="coerce")
out["am_pathogenicity"] = pd.to_numeric(df["am_pathogenicity"], errors="coerce")
out["consequence"] = df["consequence"].astype("category")
return out
+55
View File
@@ -0,0 +1,55 @@
"""Train a pathogenicity classifier on ClinVar labels (Pathogenic/Likely pathogenic vs Benign/Likely benign).
Label leakage warning: CLIN_SIG must never be a feature. This is a learning exercise, not a clinical model.
Usage: python -m rarelens_ml.train --tsv results/clinvar.vep.tsv
"""
import argparse
import lightgbm as lgb
import mlflow
import mlflow.lightgbm
import pandas as pd
from sklearn.metrics import average_precision_score, roc_auc_score
from sklearn.model_selection import train_test_split
from rarelens_ml.features import build
POS = {"Pathogenic", "Likely_pathogenic", "Pathogenic/Likely_pathogenic"}
NEG = {"Benign", "Likely_benign", "Benign/Likely_benign"}
def load(tsv: str) -> tuple[pd.DataFrame, pd.Series]:
df = pd.read_csv(tsv, sep="\t", comment="#", header=None, dtype=str)
with open(tsv) as fh:
df.columns = next(l for l in fh if l.startswith("#Uploaded")).lstrip("#").rstrip().split("\t")
df = df.rename(columns={"IMPACT": "impact", "Consequence": "consequence", "gnomADe_AF": "gnomad_af",
"CADD_PHRED": "cadd_phred"})
y = df["CLIN_SIG"].map(lambda s: 1 if s in POS else 0 if s in NEG else None)
keep = y.notna()
return build(df[keep]), y[keep].astype(int)
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--tsv", required=True)
p.add_argument("--register", action="store_true")
a = p.parse_args()
X, y = load(a.tsv)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
mlflow.set_experiment("rarelens-pathogenicity")
with mlflow.start_run():
params = {"n_estimators": 400, "learning_rate": 0.05, "num_leaves": 31, "class_weight": "balanced"}
mlflow.log_params(params)
model = lgb.LGBMClassifier(**params).fit(Xtr, ytr)
proba = model.predict_proba(Xte)[:, 1]
mlflow.log_metrics({"auroc": roc_auc_score(yte, proba), "auprc": average_precision_score(yte, proba)})
mlflow.lightgbm.log_model(
model, "model",
registered_model_name="rarelens-pathogenicity" if a.register else None,
)
if __name__ == "__main__":
main()