Spaces:
Sleeping
Sleeping
| """ | |
| Component 3: ML Ranking Engine | |
| - Loads features.csv from Component 2 | |
| - Trains a LightGBM model to score each candidate timestamp | |
| - Outputs ranked_candidates.json with placement scores | |
| - Generates SHAP feature importance analysis | |
| """ | |
| import pandas as pd | |
| import numpy as np | |
| import lightgbm as lgb | |
| import shap | |
| import json | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| FEATURES = [ | |
| "content_score", | |
| "retention_at_t", | |
| "retention_drop_rate", | |
| "retention_recovery", | |
| "relative_position", | |
| "time_since_last_candidate", | |
| ] | |
| def load_features(path="features.csv"): | |
| df = pd.read_csv(path) | |
| print(f"[Component 3] Loaded {len(df)} candidates from {path}") | |
| print(df[["timestamp", "type", "retention_at_t", "label"]].to_string(index=False)) | |
| return df | |
| def encode_type(df): | |
| type_map = {"silence": 0, "scene_change": 1, "transcript_boundary": 2} | |
| df = df.copy() | |
| df["type_encoded"] = df["type"].map(type_map).fillna(0).astype(int) | |
| return df | |
| def train_model(df): | |
| df = encode_type(df) | |
| feature_cols = FEATURES + ["type_encoded"] | |
| X = df[feature_cols].fillna(0) | |
| y = df["label"] | |
| params = { | |
| "objective": "binary", | |
| "metric": "auc", | |
| "learning_rate": 0.05, | |
| "num_leaves": 15, | |
| "min_child_samples": 1, | |
| "n_estimators": 100, | |
| "verbose": -1, | |
| "random_state": 42, | |
| } | |
| n_positive = y.sum() | |
| n_samples = len(y) | |
| if n_samples >= 10 and n_positive >= 3: | |
| from sklearn.model_selection import StratifiedKFold | |
| from sklearn.metrics import roc_auc_score | |
| skf = StratifiedKFold(n_splits=3, shuffle=True, random_state=42) | |
| auc_scores = [] | |
| for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)): | |
| X_train, X_val = X.iloc[train_idx], X.iloc[val_idx] | |
| y_train, y_val = y.iloc[train_idx], y.iloc[val_idx] | |
| m = lgb.LGBMClassifier(**params) | |
| m.fit(X_train, y_train, callbacks=[lgb.log_evaluation(period=-1)]) | |
| preds = m.predict_proba(X_val)[:, 1] | |
| auc = roc_auc_score(y_val, preds) | |
| auc_scores.append(auc) | |
| print(f" Fold {fold+1} AUC: {auc:.4f}") | |
| print(f" Mean CV AUC: {np.mean(auc_scores):.4f}") | |
| else: | |
| print(f" [Note] Small dataset ({n_samples} samples, {n_positive} positive)") | |
| print(f" Skipping CV — training directly on all data") | |
| print(f" (CV requires 10+ samples with 3+ positive labels)") | |
| final_model = lgb.LGBMClassifier(**params) | |
| final_model.fit(X, y, callbacks=[lgb.log_evaluation(period=-1)]) | |
| print(" Final model trained ✅") | |
| return final_model, feature_cols | |
| def score_candidates(df, model, feature_cols): | |
| df = encode_type(df) | |
| X = df[feature_cols].fillna(0) | |
| df = df.copy() | |
| df["placement_score"] = model.predict_proba(X)[:, 1] | |
| df_sorted = df.sort_values("placement_score", ascending=False).reset_index(drop=True) | |
| df_sorted["rank"] = df_sorted.index + 1 | |
| mins = (df_sorted["timestamp"] // 60).astype(int) | |
| secs = (df_sorted["timestamp"] % 60).astype(int) | |
| df_sorted["timestamp_formatted"] = mins.astype(str) + "m " + secs.astype(str) + "s" | |
| return df_sorted | |
| def save_shap_plot(model, df, feature_cols, output_path="shap_importance.png"): | |
| df = encode_type(df) | |
| X = df[feature_cols].fillna(0) | |
| explainer = shap.TreeExplainer(model) | |
| shap_values = explainer.shap_values(X) | |
| vals = shap_values[1] if isinstance(shap_values, list) else shap_values | |
| mean_shap = np.abs(vals).mean(axis=0) | |
| feat_imp = pd.Series(mean_shap, index=feature_cols).sort_values(ascending=True) | |
| fig, ax = plt.subplots(figsize=(8, 5)) | |
| feat_imp.plot(kind="barh", ax=ax, color="#6C63FF") | |
| ax.set_title("Feature Importance (SHAP)", fontsize=13, fontweight="bold") | |
| ax.set_xlabel("Mean |SHAP value|") | |
| plt.tight_layout() | |
| plt.savefig(output_path, dpi=150) | |
| plt.close() | |
| print(f" SHAP plot saved → {output_path}") | |
| def save_results(df_ranked, output_path="ranked_candidates.json"): | |
| results = [] | |
| for _, row in df_ranked.iterrows(): | |
| results.append({ | |
| "rank": int(row["rank"]), | |
| "timestamp": float(row["timestamp"]), | |
| "timestamp_formatted": row["timestamp_formatted"], | |
| "type": row["type"], | |
| "placement_score": round(float(row["placement_score"]), 4), | |
| "retention_at_t": round(float(row["retention_at_t"]), 2), | |
| "label": int(row["label"]) | |
| }) | |
| with open(output_path, "w") as f: | |
| json.dump({"total_candidates": len(results), "ranked_placements": results}, f, indent=2) | |
| print(f" Results saved → {output_path}") | |
| def run(features_path="features.csv"): | |
| print("=" * 55) | |
| print(" COMPONENT 3: ML RANKING ENGINE") | |
| print("=" * 55) | |
| df = load_features(features_path) | |
| print("\n[Training LightGBM Ranker...]") | |
| model, feature_cols = train_model(df) | |
| print("\n[Scoring All Candidates...]") | |
| df_ranked = score_candidates(df, model, feature_cols) | |
| print("\n[Ranked Placement Timestamps]") | |
| print(df_ranked[["rank", "timestamp_formatted", "type", | |
| "placement_score", "retention_at_t", "label"]].to_string(index=False)) | |
| print("\n[Generating SHAP Feature Importance...]") | |
| save_shap_plot(model, df, feature_cols) | |
| save_results(df_ranked) | |
| print("\n✅ Component 3 Complete!") | |
| print(" → ranked_candidates.json") | |
| print(" → shap_importance.png") | |
| print(" Next: python component4_recommender.py") | |
| if __name__ == "__main__": | |
| run() |