Spaces:
Sleeping
Sleeping
Commit Β·
b1410eb
1
Parent(s): 60c3ccb
Refactor explainer methods and enhance evaluation template with detailed CF approach explanations
Browse files- app.py +0 -5
- recommender/__pycache__/explainer.cpython-310.pyc +0 -0
- recommender/explainer.py +49 -16
- templates/evaluation.html +28 -2
app.py
CHANGED
|
@@ -41,11 +41,6 @@ for _, u in users.iterrows():
|
|
| 41 |
CATEGORIES = sorted(products["category"].unique().tolist())
|
| 42 |
BRANDS = sorted(products["brand"].unique().tolist())
|
| 43 |
|
| 44 |
-
for _ in cf_train.train_svd_generator():
|
| 45 |
-
pass
|
| 46 |
-
for _ in cf_train.compute_slope_one_dev_generator():
|
| 47 |
-
pass
|
| 48 |
-
|
| 49 |
APPROACHES = {
|
| 50 |
"cf": {
|
| 51 |
"label": "Collaborative Filtering",
|
|
|
|
| 41 |
CATEGORIES = sorted(products["category"].unique().tolist())
|
| 42 |
BRANDS = sorted(products["brand"].unique().tolist())
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
APPROACHES = {
|
| 45 |
"cf": {
|
| 46 |
"label": "Collaborative Filtering",
|
recommender/__pycache__/explainer.cpython-310.pyc
CHANGED
|
Binary files a/recommender/__pycache__/explainer.cpython-310.pyc and b/recommender/__pycache__/explainer.cpython-310.pyc differ
|
|
|
recommender/explainer.py
CHANGED
|
@@ -15,45 +15,78 @@ class Explainer:
|
|
| 15 |
def _get_user(self, user_id):
|
| 16 |
return self._user_cache.get(user_id, {})
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
def explain_cf(self, method, user_id, product_id, details=None):
|
| 19 |
product = self._get_product(product_id)
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
details = details or {}
|
| 22 |
-
|
| 23 |
sim = min(details.get('sim_score', 0), 1.0)
|
|
|
|
|
|
|
| 24 |
templates = {
|
| 25 |
-
"user_based": f"
|
| 26 |
-
"item_based": f"
|
| 27 |
-
"svd": f"
|
| 28 |
-
"knn": f"
|
| 29 |
-
"slope_one": f"
|
| 30 |
}
|
| 31 |
return templates.get(method, f"Recommended based on collaborative filtering")
|
| 32 |
|
| 33 |
def explain_content(self, method, user_id, product_id, details=None):
|
| 34 |
product = self._get_product(product_id)
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
| 37 |
details = details or {}
|
| 38 |
-
|
| 39 |
score = min(details.get('score', 0), 1.0)
|
|
|
|
|
|
|
| 40 |
templates = {
|
| 41 |
-
"tfidf": f"
|
| 42 |
-
"feature_match": f"
|
| 43 |
}
|
| 44 |
return templates.get(method, f"Recommended based on item features")
|
| 45 |
|
| 46 |
def explain_knowledge(self, method, user_id, product_id, details=None):
|
| 47 |
product = self._get_product(product_id)
|
| 48 |
-
|
|
|
|
|
|
|
| 49 |
brand = product.get("brand", "")
|
| 50 |
price = product.get("price", 0)
|
|
|
|
| 51 |
details = details or {}
|
|
|
|
| 52 |
|
| 53 |
templates = {
|
| 54 |
-
"constraint": f"
|
| 55 |
-
"rule": f"
|
| 56 |
-
"utility": f"
|
| 57 |
}
|
| 58 |
return templates.get(method, f"Recommended based on your requirements")
|
| 59 |
|
|
|
|
| 15 |
def _get_user(self, user_id):
|
| 16 |
return self._user_cache.get(user_id, {})
|
| 17 |
|
| 18 |
+
def _fmt_score(self, score):
|
| 19 |
+
if score >= 0.9:
|
| 20 |
+
return "Excellent match"
|
| 21 |
+
elif score >= 0.7:
|
| 22 |
+
return "Strong match"
|
| 23 |
+
elif score >= 0.5:
|
| 24 |
+
return "Good match"
|
| 25 |
+
elif score >= 0.3:
|
| 26 |
+
return "Moderate match"
|
| 27 |
+
else:
|
| 28 |
+
return "Partial match"
|
| 29 |
+
|
| 30 |
+
def _pref_list(self, user, key):
|
| 31 |
+
raw = user.get(key, "")
|
| 32 |
+
if raw is None or isinstance(raw, float):
|
| 33 |
+
return []
|
| 34 |
+
if isinstance(raw, str):
|
| 35 |
+
return [p.strip() for p in raw.split(",") if p.strip()][:2]
|
| 36 |
+
return list(raw)[:2] if raw else []
|
| 37 |
+
|
| 38 |
def explain_cf(self, method, user_id, product_id, details=None):
|
| 39 |
product = self._get_product(product_id)
|
| 40 |
+
user = self._get_user(user_id)
|
| 41 |
+
pname = product.get("name", f"Item #{product_id}")
|
| 42 |
+
cat = product.get("category", "")
|
| 43 |
+
brand = product.get("brand", "")
|
| 44 |
+
price = product.get("price", 0)
|
| 45 |
details = details or {}
|
|
|
|
| 46 |
sim = min(details.get('sim_score', 0), 1.0)
|
| 47 |
+
pref_cats = self._pref_list(user, "preferred_categories")
|
| 48 |
+
|
| 49 |
templates = {
|
| 50 |
+
"user_based": f"Users with {cat} taste also liked {pname}",
|
| 51 |
+
"item_based": f"Matches your {cat} preferences β similar to items you've rated highly",
|
| 52 |
+
"svd": f"Fits your profile: {brand}'s {cat} β top latent factor match" if brand and cat else f"Fits your profile: {cat} β top latent factor match",
|
| 53 |
+
"knn": f"Popular among peers who also like {cat}" if cat else f"Popular among users with similar taste",
|
| 54 |
+
"slope_one": f"Frequently chosen by users who liked the same {cat} products" if cat else f"Frequently chosen by users with your taste",
|
| 55 |
}
|
| 56 |
return templates.get(method, f"Recommended based on collaborative filtering")
|
| 57 |
|
| 58 |
def explain_content(self, method, user_id, product_id, details=None):
|
| 59 |
product = self._get_product(product_id)
|
| 60 |
+
user = self._get_user(user_id)
|
| 61 |
+
pname = product.get("name", f"Item #{product_id}")
|
| 62 |
+
cat = product.get("category", "")
|
| 63 |
+
brand = product.get("brand", "")
|
| 64 |
+
sub = product.get("subcategory", "")
|
| 65 |
details = details or {}
|
|
|
|
| 66 |
score = min(details.get('score', 0), 1.0)
|
| 67 |
+
pref_cats = self._pref_list(user, "preferred_categories")
|
| 68 |
+
|
| 69 |
templates = {
|
| 70 |
+
"tfidf": f"Content matches: {cat} / {sub} β strongly aligns with your past likes" if sub else f"Content matches your preferred {cat} items",
|
| 71 |
+
"feature_match": f"Matches your preferences: {cat}, {brand}" if brand else f"Matches your preferred category: {cat}",
|
| 72 |
}
|
| 73 |
return templates.get(method, f"Recommended based on item features")
|
| 74 |
|
| 75 |
def explain_knowledge(self, method, user_id, product_id, details=None):
|
| 76 |
product = self._get_product(product_id)
|
| 77 |
+
user = self._get_user(user_id)
|
| 78 |
+
pname = product.get("name", f"Item #{product_id}")
|
| 79 |
+
cat = product.get("category", "")
|
| 80 |
brand = product.get("brand", "")
|
| 81 |
price = product.get("price", 0)
|
| 82 |
+
budget_max = details.get("budget_max", 0)
|
| 83 |
details = details or {}
|
| 84 |
+
pref_brands = self._pref_list(user, "favorite_brands")
|
| 85 |
|
| 86 |
templates = {
|
| 87 |
+
"constraint": f"Fits your criteria: ${price:.0f} within budget Β· {cat} Β· {brand}" if brand else f"Fits your criteria: ${price:.0f} within budget Β· {cat}",
|
| 88 |
+
"rule": f"{cat} buyers commonly add {pname} β cross-sell match" if cat else f"Customers also purchased {pname}",
|
| 89 |
+
"utility": f"Top utility score: {cat} Γ {brand} matches your preference weights" if brand else f"Top utility score: {cat} matches your preference weights",
|
| 90 |
}
|
| 91 |
return templates.get(method, f"Recommended based on your requirements")
|
| 92 |
|
templates/evaluation.html
CHANGED
|
@@ -405,14 +405,40 @@
|
|
| 405 |
buildBestMethodAnalysis() {
|
| 406 |
const best = this.cfData.find(r => r.method === this.bestCfMethod);
|
| 407 |
if (!best) return 'Run evaluation to find out.';
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
},
|
| 410 |
|
| 411 |
buildBestApproachAnalysis(data) {
|
| 412 |
if (!data.best_approach) return 'Run evaluation to find out.';
|
| 413 |
const bestApp = data.approaches?.find(a => a.approach === data.best_approach);
|
| 414 |
const precVal = bestApp ? bestApp['Precision@5'].toFixed(4) : 'β';
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
},
|
| 417 |
|
| 418 |
animateCounter(obj, key, target, decimals = 4, duration = 800) {
|
|
|
|
| 405 |
buildBestMethodAnalysis() {
|
| 406 |
const best = this.cfData.find(r => r.method === this.bestCfMethod);
|
| 407 |
if (!best) return 'Run evaluation to find out.';
|
| 408 |
+
|
| 409 |
+
const methodExplanations = {
|
| 410 |
+
user_based: 'simplicity and direct peer comparison β works well when the target user has enough similar neighbors.',
|
| 411 |
+
item_based: 'direct item-item similarity β effective on dense data where co-rated item pairs are plentiful.',
|
| 412 |
+
svd: 'latent factor modeling β captures hidden patterns in the user-item matrix, excelling on sparse, large-scale data.',
|
| 413 |
+
knn: 'nearest-neighbor aggregation β robust to noise, but performance depends on the quality of the neighbor set.',
|
| 414 |
+
slope_one: 'simplicity and speed of prediction β performs well when rating deviations are consistent across users.',
|
| 415 |
+
};
|
| 416 |
+
const why = methodExplanations[this.bestCfMethod] || 'strong performance on this dataset.';
|
| 417 |
+
|
| 418 |
+
return `<strong>${methodLabels[this.bestCfMethod] || this.bestCfMethod}</strong> achieves the lowest RMSE (${best.RMSE.toFixed(4)}) among all CF methods. ` +
|
| 419 |
+
`${methodLabels[this.bestCfMethod] || this.bestCfMethod} excels because of ${why}`;
|
| 420 |
},
|
| 421 |
|
| 422 |
buildBestApproachAnalysis(data) {
|
| 423 |
if (!data.best_approach) return 'Run evaluation to find out.';
|
| 424 |
const bestApp = data.approaches?.find(a => a.approach === data.best_approach);
|
| 425 |
const precVal = bestApp ? bestApp['Precision@5'].toFixed(4) : 'β';
|
| 426 |
+
const second = [...(data.approaches || [])].sort((a, b) => (b['Precision@5'] || 0) - (a['Precision@5'] || 0));
|
| 427 |
+
const runnerUp = second.length > 1 ? second[1].approach : null;
|
| 428 |
+
|
| 429 |
+
const explanations = {
|
| 430 |
+
'Collaborative Filtering': `strongest when rating data is dense (${this.cfData.length} methods averaged). Works by leveraging peer behavior patterns.`,
|
| 431 |
+
'Content-Based': `best when item features are informative (${data.approaches?.length || 0} approaches compared). Shines for cold-start items and niche categories where collaborative signals are weak.`,
|
| 432 |
+
'Knowledge-Based': `ideal for cold-start users with explicit constraints. Deterministic and transparent, but limited by the quality of hand-crafted rules.`,
|
| 433 |
+
};
|
| 434 |
+
const why = explanations[data.best_approach] || 'strong performance on this dataset.';
|
| 435 |
+
|
| 436 |
+
let text = `<strong>${data.best_approach}</strong> achieves the highest Precision@5 (${precVal}) on this dataset. ` +
|
| 437 |
+
`${data.best_approach} performs ${why}`;
|
| 438 |
+
if (runnerUp) {
|
| 439 |
+
text += ` <strong>${runnerUp}</strong> follows as the runner-up.`;
|
| 440 |
+
}
|
| 441 |
+
return text;
|
| 442 |
},
|
| 443 |
|
| 444 |
animateCounter(obj, key, target, decimals = 4, duration = 800) {
|