From d1f59592b36052b3cc48d58c36536b3931a26206 Mon Sep 17 00:00:00 2001 From: harsh0641 Date: Fri, 13 Nov 2026 13:22:34 -0500 Subject: [PATCH] feat: versioned model artifact with train script --- models/gbm.joblib | Bin 0 -> 114115 bytes models/gbm.json | 27 +++++++++++++++++++++++++++ src/lysis/app.py | 13 +++++++------ src/lysis/cli.py | 5 ++--- src/lysis/models/artifact.py | 32 ++++++++++++++++++++++++++++++++ src/lysis/models/gbm.py | 15 ++++++++++----- src/lysis/models/train.py | 19 +++++++++++++++++++ tests/test_artifact.py | 13 +++++++++++++ 8 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 models/gbm.joblib create mode 100644 models/gbm.json create mode 100644 src/lysis/models/artifact.py create mode 100644 src/lysis/models/train.py create mode 100644 tests/test_artifact.py diff --git a/models/gbm.joblib b/models/gbm.joblib new file mode 100644 index 0000000000000000000000000000000000000000..3f2100a4f9cceaa4c7116cff9c1e4e903b6843ab GIT binary patch literal 114115 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 literal 0 HcmV?d00001 diff --git a/models/gbm.json b/models/gbm.json new file mode 100644 index 0000000..ef10444 --- /dev/null +++ b/models/gbm.json @@ -0,0 +1,27 @@ +{ + "n_train": 7769, + "base_rate": 0.098, + "cv": { + "ap": 0.278, + "auc": 0.753 + }, + "trained_at": "2026-11-13T18:19:35.186273+00:00", + "sklearn": "1.9.1", + "features": [ + "gfi", + "help_wanted", + "bug", + "docs", + "enhancement", + "n_labels", + "body_len_log", + "title_len", + "has_code", + "is_maintainer", + "stars_log", + "forks_log", + "repo_size_log", + "has_license", + "repo_age_days" + ] +} \ No newline at end of file diff --git a/src/lysis/app.py b/src/lysis/app.py index d2c724e..09cc93b 100644 --- a/src/lysis/app.py +++ b/src/lysis/app.py @@ -6,8 +6,7 @@ from lysis.cli import rank_repo from lysis.ingestion.github_client import GitHubClient -from lysis.models.evaluate import load_dataset -from lysis.models.gbm import make_gbm_ranker +from lysis.models.artifact import load_meta, load_model load_dotenv() st.set_page_config(page_title="Lysis", page_icon="🧭") @@ -15,14 +14,16 @@ @st.cache_resource def get_model(): - df = load_dataset() - return make_gbm_ranker(df), len(df), df["first_timer"].mean() + return load_model(), load_meta() -ranker, n, base = get_model() +ranker, meta = get_model() st.title("Lysis") st.caption("Beginner-friendly GitHub issue ranker") -st.sidebar.write(f"Model trained on {n:,} closed issues. Base rate: {base:.2f}") +st.sidebar.write( + f"Model trained {meta['trained_at'][:10]} on {meta['n_train']:,} issues. " + f"CV AP {meta['cv']['ap']}, AUC {meta['cv']['auc']}" +) repo = st.text_input("Repository", "psf/requests", placeholder="owner/repo") top = st.slider("Top N", 5, 30, 10) diff --git a/src/lysis/cli.py b/src/lysis/cli.py index 16b679f..4efd8e8 100644 --- a/src/lysis/cli.py +++ b/src/lysis/cli.py @@ -7,8 +7,7 @@ from lysis.features.rows import issue_row from lysis.ingestion.github_client import GitHubClient -from lysis.models.evaluate import load_dataset -from lysis.models.gbm import make_gbm_ranker +from lysis.models.artifact import load_model def rank_repo(gh, full_name, ranker, top=10, half_life_days=365): @@ -33,7 +32,7 @@ def main(): if len(sys.argv) < 2: sys.exit("usage: python src\\lysis\\cli.py owner/repo [top]") top = int(sys.argv[2]) if len(sys.argv) > 2 else 10 - ranker = make_gbm_ranker(load_dataset()) + ranker = load_model() print(rank_repo(GitHubClient(), sys.argv[1], ranker, top).to_string(index=False)) diff --git a/src/lysis/models/artifact.py b/src/lysis/models/artifact.py new file mode 100644 index 0000000..144283a --- /dev/null +++ b/src/lysis/models/artifact.py @@ -0,0 +1,32 @@ +"""Save and load the trained ranker as a versioned artifact.""" + +import json +from datetime import UTC, datetime +from pathlib import Path + +import joblib +import sklearn + +MODEL_PATH = Path("models/gbm.joblib") + + +def save_model(ranker, meta, path=MODEL_PATH): + path.parent.mkdir(parents=True, exist_ok=True) + joblib.dump(ranker, path) + meta = { + **meta, + "trained_at": datetime.now(UTC).isoformat(), + "sklearn": sklearn.__version__, + "features": ranker.cols, + } + path.with_suffix(".json").write_text(json.dumps(meta, indent=2)) + + +def load_model(path=MODEL_PATH): + if not path.exists(): + raise FileNotFoundError("No model found. Run: python src\\lysis\\models\\train.py") + return joblib.load(path) + + +def load_meta(path=MODEL_PATH): + return json.loads(path.with_suffix(".json").read_text()) \ No newline at end of file diff --git a/src/lysis/models/gbm.py b/src/lysis/models/gbm.py index bca2669..49cde45 100644 --- a/src/lysis/models/gbm.py +++ b/src/lysis/models/gbm.py @@ -5,12 +5,17 @@ from lysis.features.issue_features import FEATURE_COLUMNS, build_features +class GBMRanker: + def __init__(self, model, cols): + self.model = model + self.cols = cols + + def __call__(self, df): + return self.model.predict_proba(build_features(df)[self.cols])[:, 1] + + def make_gbm_ranker(train, seed=42, drop=()): cols = [c for c in FEATURE_COLUMNS if c not in drop] model = HistGradientBoostingClassifier(max_depth=3, learning_rate=0.1, random_state=seed) model.fit(build_features(train)[cols], train["first_timer"]) - - def ranker(df): - return model.predict_proba(build_features(df)[cols])[:, 1] - - return ranker \ No newline at end of file + return GBMRanker(model, cols) \ No newline at end of file diff --git a/src/lysis/models/train.py b/src/lysis/models/train.py new file mode 100644 index 0000000..b8114c6 --- /dev/null +++ b/src/lysis/models/train.py @@ -0,0 +1,19 @@ +"""Train on all data, report CV, save the artifact.""" + +from lysis.models.artifact import save_model +from lysis.models.evaluate import cross_validate, load_dataset +from lysis.models.gbm import make_gbm_ranker + + +def main(): + df = load_dataset() + cv = cross_validate(df, {"gbm": make_gbm_ranker})["gbm"] + save_model( + make_gbm_ranker(df), + {"n_train": len(df), "base_rate": round(float(df["first_timer"].mean()), 3), "cv": cv}, + ) + print("saved models/gbm.joblib", cv) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_artifact.py b/tests/test_artifact.py new file mode 100644 index 0000000..32b117c --- /dev/null +++ b/tests/test_artifact.py @@ -0,0 +1,13 @@ +import pandas as pd + +from lysis.models.artifact import load_meta, load_model, save_model +from lysis.models.gbm import make_gbm_ranker + + +def test_save_load_roundtrip(issues_df, tmp_path): + big = pd.concat([issues_df] * 10, ignore_index=True) + ranker = make_gbm_ranker(big) + path = tmp_path / "m.joblib" + save_model(ranker, {"n_train": len(big)}, path) + assert list(load_model(path)(big)) == list(ranker(big)) + assert load_meta(path)["n_train"] == len(big) \ No newline at end of file