onehaut_backend / scripts /evaluate.py
DTanzillo's picture
:robot: Add model training scripts, evaluation, and image feature extraction
0ba38d0
Raw
History Blame Contribute Delete
3.97 kB
# AI-assisted (Claude Code, claude.ai) -- https://claude.ai
"""Evaluation metrics for recommendation models: HR@K, NDCG@K, Coverage, Novelty."""
import numpy as np
import pandas as pd
from pathlib import Path
def hit_rate_at_k(recommendations: dict, test: pd.DataFrame, k: int = 12):
"""Fraction of users where at least one test item appears in top-K."""
test_items = test.groupby("customer_id")["article_id"].apply(set).to_dict()
hits = 0
total = 0
for uid, rec_list in recommendations.items():
if uid not in test_items:
continue
total += 1
if set(rec_list[:k]) & test_items[uid]:
hits += 1
return hits / total if total > 0 else 0.0
def ndcg_at_k(recommendations: dict, test: pd.DataFrame, k: int = 12):
"""Normalized Discounted Cumulative Gain at K."""
test_items = test.groupby("customer_id")["article_id"].apply(set).to_dict()
ndcgs = []
for uid, rec_list in recommendations.items():
if uid not in test_items:
continue
relevant = test_items[uid]
dcg = sum(
1.0 / np.log2(i + 2)
for i, item in enumerate(rec_list[:k])
if item in relevant
)
# Ideal DCG: all relevant items ranked first
ideal_dcg = sum(
1.0 / np.log2(i + 2)
for i in range(min(len(relevant), k))
)
ndcgs.append(dcg / ideal_dcg if ideal_dcg > 0 else 0.0)
return np.mean(ndcgs) if ndcgs else 0.0
def coverage(recommendations: dict, all_article_ids: list, k: int = 12):
"""Fraction of catalog that appears in any user's top-K."""
recommended = set()
for rec_list in recommendations.values():
recommended.update(rec_list[:k])
return len(recommended) / len(all_article_ids)
def novelty(recommendations: dict, train: pd.DataFrame, k: int = 12):
"""Average self-information of recommended items (higher = less popular = more novel)."""
item_popularity = train.groupby("article_id").size()
total = len(train)
scores = []
for rec_list in recommendations.values():
for item in rec_list[:k]:
pop = item_popularity.get(item, 1) / total
scores.append(-np.log2(pop))
return np.mean(scores) if scores else 0.0
def evaluate_model(model, test: pd.DataFrame, train: pd.DataFrame, all_article_ids: list, k: int = 12, model_name: str = "model"):
"""Run all metrics for a model that has a .recommend(customer_id, k) method."""
test_users = test["customer_id"].unique()
print(f"Generating recommendations for {len(test_users):,} test users...")
recommendations = {}
for uid in test_users:
recs = model.recommend(uid, k=k)
if recs:
recommendations[uid] = recs
hr = hit_rate_at_k(recommendations, test, k)
ndcg = ndcg_at_k(recommendations, test, k)
cov = coverage(recommendations, all_article_ids, k)
nov = novelty(recommendations, train, k)
print(f"\n{'=' * 40}")
print(f" {model_name} @ K={k}")
print(f"{'=' * 40}")
print(f" Hit Rate: {hr:.4f}")
print(f" NDCG: {ndcg:.4f}")
print(f" Coverage: {cov:.4f}")
print(f" Novelty: {nov:.2f} bits")
print(f" Users evaluated: {len(recommendations):,}")
print(f"{'=' * 40}")
return {"model": model_name, "HR@K": hr, "NDCG@K": ndcg, "coverage": cov, "novelty": nov}
if __name__ == "__main__":
# Quick test with baseline
from train_baseline import PopularityRecommender
data_dir = Path("data/processed")
train = pd.read_csv(data_dir / "train.csv", dtype={"article_id": str})
test = pd.read_csv(data_dir / "test.csv", dtype={"article_id": str})
articles = pd.read_csv(data_dir / "articles_subset.csv", dtype={"article_id": str})
model = PopularityRecommender.load("models/baseline/popularity.pkl")
evaluate_model(model, test, train, articles["article_id"].tolist(), model_name="Popularity (global)")