ringguard / scoring /train_model.py
Rohit Patil
RingGuard Phase 3 - initial HF Spaces deploy
753201e
Raw History Blame Contribute Delete
5.03 kB
"""
train_model.py
--------------
Train a LightGBM binary risk classifier on the training dataset.
Outputs:
scoring/model.lgb — trained LightGBM model
scoring/holdout_split.json — held-out test rows for evaluation (Section 6)
Usage:
python scoring/train_model.py
"""
import json
import os
import pickle
import sys
import lightgbm as lgb
import numpy as np
from sklearn.model_selection import train_test_split
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TRAIN_DIR = os.path.join(BASE_DIR, "data", "synthetic_train")
SCORING_DIR = os.path.join(BASE_DIR, "scoring")
MODEL_PATH = os.path.join(SCORING_DIR, "model.lgb")
HOLDOUT_PATH = os.path.join(SCORING_DIR, "holdout_split.json")
# ---------------------------------------------------------------------------
# Feature columns (must match scoring/features.py)
# ---------------------------------------------------------------------------
FEATURE_COLUMNS = [
"size",
"strong_link_count",
"weak_link_count",
"strong_link_density",
"has_same_contact_edge",
"order_count",
"total_amount",
"avg_order_amount",
"return_rate",
"chargeback_rate",
"shared_coupon_count",
"time_span_days",
"order_velocity",
"formation_type_strong",
]
RANDOM_STATE = 42
def load_data():
"""Load clusters, orders, ground truth, and graph from the training directory."""
sys.path.insert(0, BASE_DIR)
from scoring.features import extract_all, _load_json, _load_graph
clusters_path = os.path.join(TRAIN_DIR, "candidate_clusters.json")
orders_path = os.path.join(TRAIN_DIR, "orders.json")
gt_path = os.path.join(TRAIN_DIR, "ground_truth.json")
graph_path = os.path.join(TRAIN_DIR, "graph.gpickle")
clusters = _load_json(clusters_path)
orders = _load_json(orders_path)
ground_truth = _load_json(gt_path)
graph = _load_graph(graph_path) if os.path.exists(graph_path) else None
rows = extract_all(clusters, orders, ground_truth=ground_truth, graph=graph)
return rows
def train():
print("Loading training data...")
rows = load_data()
print(f" {len(rows)} candidate clusters loaded")
X = np.array([[r[f] for f in FEATURE_COLUMNS] for r in rows])
y = np.array([r["label"] for r in rows])
cluster_ids = [r["cluster_id"] for r in rows]
feature_rows = [{f: r[f] for f in FEATURE_COLUMNS} for r in rows]
pos = y.sum()
neg = len(y) - pos
print(f" Labels — positive: {int(pos)}, negative: {int(neg)}")
# 5.2 — stratified 80/20 split
(X_train, X_test, y_train, y_test,
ids_train, ids_test,
feat_train, feat_test) = train_test_split(
X, y, cluster_ids, feature_rows,
test_size=0.20,
stratify=y,
random_state=RANDOM_STATE,
)
print(f" Train: {len(X_train)} Test (holdout): {len(X_test)}")
print(f" Holdout positives: {y_test.sum():.0f} negatives: {(len(y_test)-y_test.sum()):.0f}")
# 5.3 — simple LightGBM model (small dataset, keep it simple)
model = lgb.LGBMClassifier(
num_leaves=7,
max_depth=4,
min_child_samples=5,
n_estimators=100,
learning_rate=0.05,
objective="binary",
random_state=RANDOM_STATE,
verbose=-1,
)
print("Training LightGBM classifier...")
model.fit(X_train, y_train)
print(" Training complete.")
# 5.4 — save model
model.booster_.save_model(MODEL_PATH)
print(f" Model saved to {MODEL_PATH}")
# Verify reload
loaded = lgb.Booster(model_file=MODEL_PATH)
test_pred = loaded.predict(X_test)
print(f" Model reload verified (test pred shape: {test_pred.shape})")
# 5.5 — feature importances
importances = model.feature_importances_
importance_pairs = sorted(
zip(FEATURE_COLUMNS, importances),
key=lambda x: x[1], reverse=True
)
print("\nFeature importances (gain):")
for feat, imp in importance_pairs:
print(f" {feat:30s}: {imp}")
if importance_pairs[0][0] == "size" and importance_pairs[0][1] > sum(importances) * 0.5:
print(" [NOTE] 'size' dominates feature importance — worth flagging in summary")
# Save holdout split (cluster IDs + features + labels + probabilities)
holdout = []
probs = loaded.predict(X_test)
for i in range(len(ids_test)):
holdout.append({
"cluster_id": ids_test[i],
"features": feat_test[i],
"label": int(y_test[i]),
"risk_probability": float(probs[i]),
})
with open(HOLDOUT_PATH, "w", encoding="utf-8") as f:
json.dump(holdout, f, indent=2, ensure_ascii=False)
print(f" Holdout split saved to {HOLDOUT_PATH} ({len(holdout)} rows)")
return model, holdout, importance_pairs
if __name__ == "__main__":
train()