Spaces:
Sleeping
Sleeping
Commit ·
f9ade9b
1
Parent(s): 44f6b36
Production readiness: port 7860, Dockerfile, HF Spaces config
Browse files- .env.example +19 -0
- Dockerfile +29 -0
- README.md +24 -87
- app.py +48 -6
- deploy/.dockerignore +8 -0
- deploy/Dockerfile +33 -0
- deploy/nginx.conf +44 -0
- deploy/tasteengine.service +27 -0
- discussion/01-collaborative-filtering.md +172 -0
- discussion/02-content-based.md +102 -0
- discussion/03-knowledge-based.md +161 -0
- discussion/04-evaluation.md +172 -0
- discussion/05-explainer.md +97 -0
- discussion/06-similarity.md +103 -0
- discussion/07-helpers.md +94 -0
- discussion/08-data-generator.md +104 -0
- discussion/09-app.md +171 -0
- discussion/10-mathematical-details.md +723 -0
- requirements.txt +2 -0
.env.example
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TasteEngine — Environment Configuration
|
| 2 |
+
# Copy to .env and adjust values
|
| 3 |
+
|
| 4 |
+
# Flask secret key (generate with: python3 -c "import os; print(os.urandom(24).hex())")
|
| 5 |
+
FLASK_SECRET_KEY=changeme_to_a_random_hex_string
|
| 6 |
+
|
| 7 |
+
# Set to "1" to enable Flask debug mode (dev only!)
|
| 8 |
+
FLASK_DEBUG=0
|
| 9 |
+
|
| 10 |
+
# Port for the Flask dev server (ignored when using Gunicorn)
|
| 11 |
+
PORT=5000
|
| 12 |
+
|
| 13 |
+
# Set to "1" to pre-warm SVD and Slope One caches on startup.
|
| 14 |
+
# First user won't pay the 10s training penalty, but startup takes ~15s longer.
|
| 15 |
+
PREWARM_CACHE=0
|
| 16 |
+
|
| 17 |
+
# OpenBLAS thread control (leave at 1 for Gunicorn multi-worker setups)
|
| 18 |
+
OPENBLAS_NUM_THREADS=1
|
| 19 |
+
OMP_NUM_THREADS=1
|
Dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use multi-stage build for smaller image
|
| 2 |
+
FROM python:3.11-slim AS builder
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
FROM python:3.11-slim
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 12 |
+
build-essential \
|
| 13 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
|
| 16 |
+
COPY . .
|
| 17 |
+
|
| 18 |
+
ENV OPENBLAS_NUM_THREADS=1
|
| 19 |
+
ENV OMP_NUM_THREADS=1
|
| 20 |
+
ENV PYTHONUNBUFFERED=1
|
| 21 |
+
|
| 22 |
+
EXPOSE 7860
|
| 23 |
+
|
| 24 |
+
CMD ["gunicorn", "app:app", \
|
| 25 |
+
"--workers", "2", \
|
| 26 |
+
"--timeout", "120", \
|
| 27 |
+
"--bind", "0.0.0.0:7860", \
|
| 28 |
+
"--access-logfile", "-", \
|
| 29 |
+
"--error-logfile", "-"]
|
README.md
CHANGED
|
@@ -1,93 +1,30 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
- **Collaborative Filtering** — 5 methods: User-Based, Item-Based, SVD (Matrix Factorization), KNN-Based, and Slope One
|
| 8 |
-
- **Content-Based Filtering** — TF-IDF similarity and feature matching based on user preferences
|
| 9 |
-
- **Knowledge-Based Filtering** — Constraint-based, rule-based, and utility-based recommendation strategies
|
| 10 |
-
- **Evaluation Suite** — RMSE, MAE, Precision@k, Recall@k, F1@k, and Coverage metrics
|
| 11 |
-
- **Explanation Engine** — Human-readable explanations for every recommendation
|
| 12 |
-
- **Interactive Web UI** — Built with Flask, allowing users to select approaches, compare results, and explore products
|
| 13 |
-
- **Synthetic Dataset** — Generates realistic e-commerce data (products, users, ratings)
|
| 14 |
-
|
| 15 |
-
## Project Structure
|
| 16 |
-
|
| 17 |
-
```
|
| 18 |
-
TasteEngine/
|
| 19 |
-
├── app.py # Flask web application
|
| 20 |
-
├── requirements.txt # Python dependencies
|
| 21 |
-
├── data/
|
| 22 |
-
│ ├── generate_data.py # Synthetic dataset generator
|
| 23 |
-
│ ├── products.csv # Product catalog
|
| 24 |
-
│ ├── users.csv # User profiles
|
| 25 |
-
│ └── ratings.csv # User-product ratings
|
| 26 |
-
├── recommender/
|
| 27 |
-
│ ├── collaborative.py # Collaborative filtering implementations
|
| 28 |
-
│ ├── content_based.py # Content-based recommendation engine
|
| 29 |
-
│ ├── knowledge_based.py # Knowledge-based recommendation engine
|
| 30 |
-
│ ├── evaluation.py # Evaluation metrics and comparison
|
| 31 |
-
│ └── explainer.py # Recommendation explanation engine
|
| 32 |
-
├── utils/
|
| 33 |
-
│ ├── helpers.py # Data loading and preprocessing utilities
|
| 34 |
-
│ └── similarity.py # Similarity metrics (cosine, pearson, adjusted cosine, jaccard)
|
| 35 |
-
├── templates/
|
| 36 |
-
│ ├── index.html # Landing page
|
| 37 |
-
│ ├── recommend.html # Recommendation interface
|
| 38 |
-
│ └── evaluation.html # Evaluation dashboard
|
| 39 |
-
├── static/
|
| 40 |
-
│ └── css/
|
| 41 |
-
│ └── style.css # Application styles
|
| 42 |
-
└── plan/ # Design documents
|
| 43 |
-
├── 01-overview.md
|
| 44 |
-
├── 02-dataset.md
|
| 45 |
-
├── 03-collaborative-filtering.md
|
| 46 |
-
├── 04-content-based.md
|
| 47 |
-
├── 05-knowledge-based.md
|
| 48 |
-
├── 06-evaluation.md
|
| 49 |
-
├── 07-explanation.md
|
| 50 |
-
├── 08-ui-ux.md
|
| 51 |
-
└── 09-implementation-phases.md
|
| 52 |
-
```
|
| 53 |
-
|
| 54 |
-
## Installation
|
| 55 |
-
|
| 56 |
-
```bash
|
| 57 |
-
git clone https://github.com/Abdallah4Z/TasteEngine.git
|
| 58 |
-
cd TasteEngine
|
| 59 |
-
pip install -r requirements.txt
|
| 60 |
-
```
|
| 61 |
-
|
| 62 |
-
## Usage
|
| 63 |
|
| 64 |
-
|
| 65 |
-
python app.py
|
| 66 |
-
```
|
| 67 |
-
|
| 68 |
-
Then open `http://localhost:5000` in your browser.
|
| 69 |
-
|
| 70 |
-
### Generating Fresh Data
|
| 71 |
-
|
| 72 |
-
```bash
|
| 73 |
-
python data/generate_data.py
|
| 74 |
-
```
|
| 75 |
-
|
| 76 |
-
## Recommendation Approaches
|
| 77 |
-
|
| 78 |
-
| Approach | Methods | Description |
|
| 79 |
-
|---|---|---|
|
| 80 |
-
| Collaborative Filtering | User-Based, Item-Based, SVD, KNN, Slope One | Leverages user-item interaction patterns |
|
| 81 |
-
| Content-Based | TF-IDF, Feature Matching | Uses product attributes and user preferences |
|
| 82 |
-
| Knowledge-Based | Constraint, Rule, Utility | Applies domain rules and constraints |
|
| 83 |
-
|
| 84 |
-
## Dependencies
|
| 85 |
|
| 86 |
-
-
|
| 87 |
-
-
|
| 88 |
-
-
|
| 89 |
-
-
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
##
|
| 92 |
|
| 93 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: TasteEngine
|
| 3 |
+
emoji: 🎯
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
---
|
| 11 |
|
| 12 |
+
# TasteEngine — Multi-Approach Recommender System
|
| 13 |
|
| 14 |
+
An interactive recommender system comparing **Collaborative Filtering**, **Content-Based**, and **Knowledge-Based** approaches side by side.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
## Features
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
- **5 Collaborative Filtering methods**: User-Based, Item-Based, SVD, KNN, Slope One
|
| 19 |
+
- **2 Content-Based methods**: TF-IDF Similarity, Feature Matching
|
| 20 |
+
- **3 Knowledge-Based methods**: Constraint, Rule, Utility
|
| 21 |
+
- **Evaluation Dashboard**: Compare all methods across 6 metrics (RMSE, MAE, Precision@k, Recall@k, F1@k, Coverage)
|
| 22 |
+
- **Interactive UI**: Built with Alpine.js, HTMX, and Chart.js
|
| 23 |
+
- **Incremental Evaluation**: Results appear as each method finishes
|
| 24 |
+
- **Dark/Light Mode**: Toggleable theme
|
| 25 |
|
| 26 |
+
## Tech Stack
|
| 27 |
|
| 28 |
+
- **Backend**: Python, Flask, NumPy, Pandas, scikit-learn
|
| 29 |
+
- **Frontend**: Alpine.js, HTMX, Chart.js, Lucide icons
|
| 30 |
+
- **Deployment**: Docker on Hugging Face Spaces
|
app.py
CHANGED
|
@@ -1,7 +1,13 @@
|
|
| 1 |
import os
|
| 2 |
import sys
|
|
|
|
|
|
|
|
|
|
| 3 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 4 |
|
|
|
|
|
|
|
|
|
|
| 5 |
import json as json_module
|
| 6 |
import numpy as np
|
| 7 |
import pandas as pd
|
|
@@ -13,7 +19,18 @@ from recommender.knowledge_based import KnowledgeBasedRecommender
|
|
| 13 |
from recommender.evaluation import Evaluator
|
| 14 |
from recommender.explainer import Explainer
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
app = Flask(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
products, users, ratings = load_data()
|
| 19 |
cf = CollaborativeFiltering(ratings)
|
|
@@ -73,6 +90,17 @@ APPROACHES = {
|
|
| 73 |
},
|
| 74 |
}
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
def get_product_info(product_id):
|
| 78 |
row = products[products["product_id"] == product_id]
|
|
@@ -91,6 +119,11 @@ def get_product_info(product_id):
|
|
| 91 |
}
|
| 92 |
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
@app.route("/")
|
| 95 |
def index():
|
| 96 |
return render_template("index.html",
|
|
@@ -212,7 +245,8 @@ def api_recommend():
|
|
| 212 |
return jsonify({"recommendations": explanations})
|
| 213 |
|
| 214 |
except Exception as e:
|
| 215 |
-
|
|
|
|
| 216 |
|
| 217 |
|
| 218 |
CF_METHOD_NAMES = ["user_based", "item_based", "svd", "knn", "slope_one"]
|
|
@@ -232,7 +266,8 @@ def api_evaluate_cf(method):
|
|
| 232 |
result = evaluator.evaluate_cf_method(method, cf_train, TEST, k=5)
|
| 233 |
return jsonify(result)
|
| 234 |
except Exception as e:
|
| 235 |
-
|
|
|
|
| 236 |
|
| 237 |
|
| 238 |
@app.route("/api/evaluate/cf/<method>/stream")
|
|
@@ -258,7 +293,8 @@ def api_evaluate_cf_stream(method):
|
|
| 258 |
result = evaluator.evaluate_cf_method(method, cf_train, TEST, k=5)
|
| 259 |
yield json_module.dumps({"type": "result", "data": result}) + "\n"
|
| 260 |
except Exception as e:
|
| 261 |
-
|
|
|
|
| 262 |
|
| 263 |
return Response(stream_with_context(generate()), mimetype="application/x-ndjson")
|
| 264 |
|
|
@@ -329,7 +365,8 @@ def api_evaluate_approaches():
|
|
| 329 |
best = max(results, key=lambda a: a.get("Precision@5", 0))["approach"] if results else None
|
| 330 |
return jsonify({"approaches": results, "best_approach": best})
|
| 331 |
except Exception as e:
|
| 332 |
-
|
|
|
|
| 333 |
|
| 334 |
|
| 335 |
@app.route("/api/products/filter")
|
|
@@ -408,6 +445,7 @@ def api_create_user():
|
|
| 408 |
"budget_min": float(data.get("budget_min", 0)),
|
| 409 |
"budget_max": float(data.get("budget_max", 500)),
|
| 410 |
})
|
|
|
|
| 411 |
return jsonify({"success": True, "user_id": new_id, "name": new_name})
|
| 412 |
|
| 413 |
|
|
@@ -441,7 +479,8 @@ def htmx_recommend():
|
|
| 441 |
else:
|
| 442 |
return f'<div class="empty-state"><div class="empty-icon">❌</div><p>Unknown approach: {approach}</p></div>'
|
| 443 |
except Exception as e:
|
| 444 |
-
|
|
|
|
| 445 |
|
| 446 |
if not recs:
|
| 447 |
return '<div class="empty-state"><div class="empty-icon">📭</div><p>No recommendations found.</p></div>'
|
|
@@ -481,4 +520,7 @@ def stars_html(rating):
|
|
| 481 |
|
| 482 |
|
| 483 |
if __name__ == "__main__":
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import sys
|
| 3 |
+
import logging
|
| 4 |
+
import traceback
|
| 5 |
+
|
| 6 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 7 |
|
| 8 |
+
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
|
| 9 |
+
os.environ.setdefault("OMP_NUM_THREADS", "1")
|
| 10 |
+
|
| 11 |
import json as json_module
|
| 12 |
import numpy as np
|
| 13 |
import pandas as pd
|
|
|
|
| 19 |
from recommender.evaluation import Evaluator
|
| 20 |
from recommender.explainer import Explainer
|
| 21 |
|
| 22 |
+
logging.basicConfig(
|
| 23 |
+
level=logging.INFO,
|
| 24 |
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
| 25 |
+
datefmt="%Y-%m-%d %H:%M:%S",
|
| 26 |
+
)
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
app = Flask(__name__)
|
| 30 |
+
app.secret_key = os.environ.get(
|
| 31 |
+
"FLASK_SECRET_KEY",
|
| 32 |
+
os.urandom(24).hex()
|
| 33 |
+
)
|
| 34 |
|
| 35 |
products, users, ratings = load_data()
|
| 36 |
cf = CollaborativeFiltering(ratings)
|
|
|
|
| 90 |
},
|
| 91 |
}
|
| 92 |
|
| 93 |
+
if os.environ.get("PREWARM_CACHE", "0") == "1":
|
| 94 |
+
import threading
|
| 95 |
+
logger.info("Pre-warming SVD and Slope One caches...")
|
| 96 |
+
threading.Thread(
|
| 97 |
+
target=lambda: (
|
| 98 |
+
list(cf_train.train_svd_generator()),
|
| 99 |
+
list(cf_train.compute_slope_one_dev_generator()),
|
| 100 |
+
),
|
| 101 |
+
daemon=True,
|
| 102 |
+
).start()
|
| 103 |
+
|
| 104 |
|
| 105 |
def get_product_info(product_id):
|
| 106 |
row = products[products["product_id"] == product_id]
|
|
|
|
| 119 |
}
|
| 120 |
|
| 121 |
|
| 122 |
+
@app.route("/health")
|
| 123 |
+
def health():
|
| 124 |
+
return jsonify({"status": "ok", "users": len(USER_OPTIONS), "products": len(products)})
|
| 125 |
+
|
| 126 |
+
|
| 127 |
@app.route("/")
|
| 128 |
def index():
|
| 129 |
return render_template("index.html",
|
|
|
|
| 245 |
return jsonify({"recommendations": explanations})
|
| 246 |
|
| 247 |
except Exception as e:
|
| 248 |
+
logger.error("Recommendation error: %s\n%s", e, traceback.format_exc())
|
| 249 |
+
return jsonify({"error": "An internal error occurred while generating recommendations"}), 500
|
| 250 |
|
| 251 |
|
| 252 |
CF_METHOD_NAMES = ["user_based", "item_based", "svd", "knn", "slope_one"]
|
|
|
|
| 266 |
result = evaluator.evaluate_cf_method(method, cf_train, TEST, k=5)
|
| 267 |
return jsonify(result)
|
| 268 |
except Exception as e:
|
| 269 |
+
logger.error("CF evaluation error [%s]: %s\n%s", method, e, traceback.format_exc())
|
| 270 |
+
return jsonify({"method": method, "error": "Evaluation failed"})
|
| 271 |
|
| 272 |
|
| 273 |
@app.route("/api/evaluate/cf/<method>/stream")
|
|
|
|
| 293 |
result = evaluator.evaluate_cf_method(method, cf_train, TEST, k=5)
|
| 294 |
yield json_module.dumps({"type": "result", "data": result}) + "\n"
|
| 295 |
except Exception as e:
|
| 296 |
+
logger.error("Stream evaluation error [%s]: %s\n%s", method, e, traceback.format_exc())
|
| 297 |
+
yield json_module.dumps({"type": "error", "message": "Evaluation failed"}) + "\n"
|
| 298 |
|
| 299 |
return Response(stream_with_context(generate()), mimetype="application/x-ndjson")
|
| 300 |
|
|
|
|
| 365 |
best = max(results, key=lambda a: a.get("Precision@5", 0))["approach"] if results else None
|
| 366 |
return jsonify({"approaches": results, "best_approach": best})
|
| 367 |
except Exception as e:
|
| 368 |
+
logger.error("Approach evaluation error: %s\n%s", e, traceback.format_exc())
|
| 369 |
+
return jsonify({"error": "Approach comparison failed"}), 500
|
| 370 |
|
| 371 |
|
| 372 |
@app.route("/api/products/filter")
|
|
|
|
| 445 |
"budget_min": float(data.get("budget_min", 0)),
|
| 446 |
"budget_max": float(data.get("budget_max", 500)),
|
| 447 |
})
|
| 448 |
+
logger.info("Created user %s (ID %d)", new_name, new_id)
|
| 449 |
return jsonify({"success": True, "user_id": new_id, "name": new_name})
|
| 450 |
|
| 451 |
|
|
|
|
| 479 |
else:
|
| 480 |
return f'<div class="empty-state"><div class="empty-icon">❌</div><p>Unknown approach: {approach}</p></div>'
|
| 481 |
except Exception as e:
|
| 482 |
+
logger.error("HTMX recommend error: %s\n%s", e, traceback.format_exc())
|
| 483 |
+
return f'<div class="empty-state"><div class="empty-icon">❌</div><p>An error occurred</p></div>'
|
| 484 |
|
| 485 |
if not recs:
|
| 486 |
return '<div class="empty-state"><div class="empty-icon">📭</div><p>No recommendations found.</p></div>'
|
|
|
|
| 520 |
|
| 521 |
|
| 522 |
if __name__ == "__main__":
|
| 523 |
+
debug_mode = os.environ.get("FLASK_DEBUG", "0") == "1"
|
| 524 |
+
port = int(os.environ.get("PORT", 7860))
|
| 525 |
+
logger.info("Starting TasteEngine on port %d (debug=%s)", port, debug_mode)
|
| 526 |
+
app.run(debug=debug_mode, host="0.0.0.0", port=port)
|
deploy/.dockerignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
.env
|
| 4 |
+
.git/
|
| 5 |
+
.gitignore
|
| 6 |
+
discussion/
|
| 7 |
+
plan/
|
| 8 |
+
*.md
|
deploy/Dockerfile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies (for numpy/scikit-learn)
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
build-essential \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Copy and install Python dependencies
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy application code
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
# Environment variables
|
| 18 |
+
ENV OPENBLAS_NUM_THREADS=1
|
| 19 |
+
ENV OMP_NUM_THREADS=1
|
| 20 |
+
ENV PREWARM_CACHE=1
|
| 21 |
+
ENV PYTHONUNBUFFERED=1
|
| 22 |
+
|
| 23 |
+
# Health check
|
| 24 |
+
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
| 25 |
+
CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1
|
| 26 |
+
|
| 27 |
+
# Run with Gunicorn
|
| 28 |
+
CMD ["gunicorn", "app:app", \
|
| 29 |
+
"--workers", "4", \
|
| 30 |
+
"--timeout", "120", \
|
| 31 |
+
"--bind", "0.0.0.0:5000", \
|
| 32 |
+
"--access-logfile", "-", \
|
| 33 |
+
"--error-logfile", "-"]
|
deploy/nginx.conf
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
server {
|
| 2 |
+
listen 80;
|
| 3 |
+
server_name tasteengine.example.com;
|
| 4 |
+
|
| 5 |
+
# Static files — served directly by nginx, Flask never touched
|
| 6 |
+
location /static/ {
|
| 7 |
+
alias /home/abdallah/recommender-project/TasteEngine/static/;
|
| 8 |
+
expires 7d;
|
| 9 |
+
add_header Cache-Control "public, immutable";
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
# Application proxy
|
| 13 |
+
location / {
|
| 14 |
+
proxy_pass http://127.0.0.1:5000;
|
| 15 |
+
proxy_http_version 1.1;
|
| 16 |
+
proxy_set_header Host $host;
|
| 17 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 18 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 19 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 20 |
+
|
| 21 |
+
# CRITICAL: disable buffering for SSE/streaming endpoints (SVD evaluation)
|
| 22 |
+
proxy_buffering off;
|
| 23 |
+
proxy_cache off;
|
| 24 |
+
|
| 25 |
+
# Timeouts
|
| 26 |
+
proxy_connect_timeout 60s;
|
| 27 |
+
proxy_read_timeout 180s;
|
| 28 |
+
proxy_send_timeout 60s;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
# Optional: rate limiting on recommendation API
|
| 32 |
+
location /api/recommend {
|
| 33 |
+
limit_req zone=recommend burst=5 nodelay;
|
| 34 |
+
proxy_pass http://127.0.0.1:5000;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
# SSL (uncomment after running certbot)
|
| 38 |
+
# listen 443 ssl;
|
| 39 |
+
# ssl_certificate /etc/letsencrypt/live/tasteengine.example.com/fullchain.pem;
|
| 40 |
+
# ssl_certificate_key /etc/letsencrypt/live/tasteengine.example.com/privkey.pem;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# Rate limiting zone definition
|
| 44 |
+
limit_req_zone $binary_remote_addr zone=recommend:10m rate=10r/s;
|
deploy/tasteengine.service
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[Unit]
|
| 2 |
+
Description=TasteEngine Recommender System
|
| 3 |
+
After=network.target
|
| 4 |
+
|
| 5 |
+
[Service]
|
| 6 |
+
Type=simple
|
| 7 |
+
User=abdallah
|
| 8 |
+
WorkingDirectory=/home/abdallah/recommender-project/TasteEngine
|
| 9 |
+
|
| 10 |
+
Environment=OPENBLAS_NUM_THREADS=1
|
| 11 |
+
Environment=OMP_NUM_THREADS=1
|
| 12 |
+
Environment=PREWARM_CACHE=1
|
| 13 |
+
EnvironmentFile=/home/abdallah/recommender-project/TasteEngine/.env
|
| 14 |
+
|
| 15 |
+
ExecStart=/home/abdallah/.local/bin/gunicorn app:app \
|
| 16 |
+
--workers 4 \
|
| 17 |
+
--timeout 120 \
|
| 18 |
+
--bind 0.0.0.0:5000 \
|
| 19 |
+
--access-logfile /var/log/tasteengine/access.log \
|
| 20 |
+
--error-logfile /var/log/tasteengine/error.log \
|
| 21 |
+
--log-level info
|
| 22 |
+
|
| 23 |
+
Restart=always
|
| 24 |
+
RestartSec=5
|
| 25 |
+
|
| 26 |
+
[Install]
|
| 27 |
+
WantedBy=multi-user.target
|
discussion/01-collaborative-filtering.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Collaborative Filtering — `recommender/collaborative.py`
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
Collaborative Filtering (CF) is the most widely used recommendation approach. It makes predictions based on the collective behavior of users — "users who liked similar items to you also liked these." This implementation provides **5 distinct methods** in a single `CollaborativeFiltering` class.
|
| 6 |
+
|
| 7 |
+
**Class:** `CollaborativeFiltering`
|
| 8 |
+
**File:** `recommender/collaborative.py` (248 lines)
|
| 9 |
+
**Dependencies:** `numpy`, `pandas`, `utils.similarity`, `utils.helpers`, `sklearn.neighbors`
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Architecture
|
| 14 |
+
|
| 15 |
+
### Initialization (`__init__`)
|
| 16 |
+
|
| 17 |
+
```python
|
| 18 |
+
def __init__(self, ratings_df):
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
Takes a ratings DataFrame with columns `[user_id, product_id, rating]` and:
|
| 22 |
+
|
| 23 |
+
1. **Builds a user-item matrix** via `build_user_item_matrix()` — a pivot table where rows = users, columns = items, values = ratings. Missing ratings are `NaN`.
|
| 24 |
+
2. **Extracts metadata:** `n_users`, `n_items`, `user_ids`, `item_ids`, `user_means`, `global_mean`
|
| 25 |
+
3. **Initializes caches:** `_svd_cache = None`, `_slope_one_dev = None` — lazy-computed on first method call, reused thereafter
|
| 26 |
+
|
| 27 |
+
### Helper Methods
|
| 28 |
+
|
| 29 |
+
| Method | Purpose |
|
| 30 |
+
|---|---|
|
| 31 |
+
| `_get_user_index(user_id)` | Converts user ID to matrix row index |
|
| 32 |
+
| `_get_item_index(item_id)` | Converts item ID to matrix column index |
|
| 33 |
+
|
| 34 |
+
---
|
| 35 |
+
|
| 36 |
+
## The 5 Methods
|
| 37 |
+
|
| 38 |
+
### 1. User-Based CF (`user_based_cf`)
|
| 39 |
+
|
| 40 |
+
**Idea:** Find users similar to the target user, aggregate their ratings for unseen items.
|
| 41 |
+
|
| 42 |
+
**Algorithm:**
|
| 43 |
+
1. Fill missing ratings with the global mean
|
| 44 |
+
2. Compute a full `n_users × n_users` cosine similarity matrix
|
| 45 |
+
3. For each item the target user hasn't rated:
|
| 46 |
+
- Find the top `k=20` most similar users who DID rate that item
|
| 47 |
+
- Predict rating = weighted average of their ratings (weighted by similarity)
|
| 48 |
+
|
| 49 |
+
**Complexity:** O(n_users² × n_items) — scales poorly with users, fast on this dataset (200 users).
|
| 50 |
+
|
| 51 |
+
**Strengths:** Intuitive, works well with dense data, captures nuanced user preferences.
|
| 52 |
+
**Weaknesses:** Expensive with many users, performs poorly on sparse data (cold-start users with few ratings).
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
### 2. Item-Based CF (`item_based_cf`)
|
| 57 |
+
|
| 58 |
+
**Idea:** Find items similar to those the user already liked, recommend them.
|
| 59 |
+
|
| 60 |
+
**Algorithm:**
|
| 61 |
+
1. Compute an `n_items × n_items` **adjusted cosine similarity** matrix — this normalizes by subtracting each user's mean rating before computing cosine, removing user bias (some users rate everything high/low)
|
| 62 |
+
2. For each unseen item, find the top `k=15` most similar items from the user's rated set
|
| 63 |
+
3. Predict rating = weighted average of the user's ratings on those similar items
|
| 64 |
+
|
| 65 |
+
**Complexity:** O(n_items² × n_users) — precomputes item-item similarity once, then per-user prediction is O(n_items × k).
|
| 66 |
+
|
| 67 |
+
**Strengths:** Fast per-user prediction after precomputation, stable results (item similarities change less than user similarities), handles larger user bases better than User-Based.
|
| 68 |
+
**Weaknesses:** Item-item matrix is O(n_items²) in memory, can miss cross-category recommendations.
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
### 3. SVD (Matrix Factorization) — `svd`
|
| 73 |
+
|
| 74 |
+
**Idea:** Factorize the user-item matrix into two low-rank matrices (user factors × item factors) that capture latent features like "action vs drama" or "budget vs premium."
|
| 75 |
+
|
| 76 |
+
**Training (cached via `train_svd_generator`):**
|
| 77 |
+
1. Initialize random factor matrices: `P (n_users × 20)`, `Q (n_items × 20)`, plus bias terms `bu`, `bi`
|
| 78 |
+
2. For each of **100 epochs**, iterate over all observed ratings and update:
|
| 79 |
+
```
|
| 80 |
+
prediction = global_mean + bu[u] + bi[i] + dot(P[u], Q[i])
|
| 81 |
+
error = actual_rating - prediction
|
| 82 |
+
bu[u] += learning_rate * (error - regularization * bu[u])
|
| 83 |
+
bi[i] += learning_rate * (error - regularization * bi[i])
|
| 84 |
+
P[u] += learning_rate * (error * Q[i] - regularization * P[u])
|
| 85 |
+
Q[i] += learning_rate * (error * P[u] - regularization * Q[i])
|
| 86 |
+
```
|
| 87 |
+
3. Cache `(P, Q, bu, bi)` — first call takes ~10s, subsequent calls are instant
|
| 88 |
+
|
| 89 |
+
**Prediction:**
|
| 90 |
+
```python
|
| 91 |
+
pred = global_mean + bu[u_idx] + bi[i_idx] + dot(P[u_idx], Q[i_idx])
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
**Hyperparameters:**
|
| 95 |
+
| Param | Value | Effect |
|
| 96 |
+
|---|---|---|
|
| 97 |
+
| `n_factors` | 20 | More factors = more expressive, risk of overfitting |
|
| 98 |
+
| `n_epochs` | 100 | More epochs = better convergence |
|
| 99 |
+
| `lr` | 0.01 | Learning rate — higher = faster but may overshoot |
|
| 100 |
+
| `reg` | 0.02 | Regularization — higher = prevents overfitting |
|
| 101 |
+
|
| 102 |
+
**Strengths:** Handles sparsity well, discovers latent relationships, state-of-the-art baseline for many recommendation tasks.
|
| 103 |
+
**Weaknesses:** Slow to train (100 epochs), less interpretable than memory-based methods, requires careful hyperparameter tuning.
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
### 4. KNN-Based CF (`knn_cf`)
|
| 108 |
+
|
| 109 |
+
**Idea:** Use sklearn's `NearestNeighbors` to efficiently find the closest users, then aggregate their ratings.
|
| 110 |
+
|
| 111 |
+
**Algorithm:**
|
| 112 |
+
1. Fill missing ratings with global mean
|
| 113 |
+
2. Fit a `NearestNeighbors(k=10, metric="cosine")` model on the filled matrix
|
| 114 |
+
3. For the target user, find `k` nearest neighbors
|
| 115 |
+
4. For each unseen item, predict = weighted average of neighbor ratings (weights = inverse distances)
|
| 116 |
+
|
| 117 |
+
**Complexity:** KNN indexing is O(n_users × log(n_users)), per-user prediction is O(k × n_items).
|
| 118 |
+
|
| 119 |
+
**Strengths:** Very fast nearest-neighbor search via sklearn's optimized KD-tree/ball-tree, robust to outliers.
|
| 120 |
+
**Weaknesses:** Sensitive to `k` choice, requires the full matrix in memory.
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
### 5. Slope One (`slope_one`)
|
| 125 |
+
|
| 126 |
+
**Idea:** Predict ratings based on the average difference ("deviation") between items. "If users rate item A 0.5 higher than item B on average, and you rate item B 4.0, you'll likely rate item A 4.5."
|
| 127 |
+
|
| 128 |
+
**Precomputation (cached via `compute_slope_one_dev_generator`):**
|
| 129 |
+
For every pair of items `(i, j)`:
|
| 130 |
+
```
|
| 131 |
+
dev[i][j] = average(rating_i - rating_j) across all users who rated both
|
| 132 |
+
cnt[i][j] = number of users who rated both
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
**Prediction:**
|
| 136 |
+
For the target user and each unseen item `i_idx`:
|
| 137 |
+
```
|
| 138 |
+
numerator = sum of (user_rating[j_idx] + dev[i_idx][j_idx]) for all rated items j_idx with cnt > 0
|
| 139 |
+
denominator = number of such rated items
|
| 140 |
+
pred = numerator / denominator
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
**Strengths:** Simple, fast prediction (O(n_items × n_rated) per user), no hyperparameters, easy to explain, can handle new ratings incrementally.
|
| 144 |
+
**Weaknesses:** Precomputation is O(n_items² × n_users), memory is O(n_items²), doesn't handle item cold-start.
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
## Routing (`recommend` method)
|
| 149 |
+
|
| 150 |
+
```python
|
| 151 |
+
def recommend(self, method, user_id, n_recommendations=10, **kwargs):
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
Maps method name strings to the corresponding function:
|
| 155 |
+
- `"user_based"` → `user_based_cf`
|
| 156 |
+
- `"item_based"` → `item_based_cf`
|
| 157 |
+
- `"svd"` → `svd`
|
| 158 |
+
- `"knn"` → `knn_cf`
|
| 159 |
+
- `"slope_one"` → `slope_one`
|
| 160 |
+
|
| 161 |
+
All methods return `list[tuple(product_id, predicted_rating)]` sorted by rating descending.
|
| 162 |
+
|
| 163 |
+
---
|
| 164 |
+
|
| 165 |
+
## Caching Strategy
|
| 166 |
+
|
| 167 |
+
| Cache | Trigger | Benefit |
|
| 168 |
+
|---|---|---|
|
| 169 |
+
| `_svd_cache` | First `svd()` call | Avoids re-training 100 epochs per user |
|
| 170 |
+
| `_slope_one_dev` | First `slope_one()` call | Avoids O(n_items² × n_users) recomputation per user |
|
| 171 |
+
|
| 172 |
+
Both caches use **generator functions** (`train_svd_generator`, `compute_slope_one_dev_generator`) that yield intermediate progress — used by the streaming API to show epoch-level progress in the UI.
|
discussion/02-content-based.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Content-Based Filtering — `recommender/content_based.py`
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
Content-Based Filtering recommends items by comparing their **features** (category, brand, name, subcategory) against a user's profile. It doesn't need other users' data — only the target user's history and item attributes.
|
| 6 |
+
|
| 7 |
+
**Class:** `ContentBasedRecommender`
|
| 8 |
+
**File:** `recommender/content_based.py` (76 lines)
|
| 9 |
+
**Dependencies:** `numpy`, `sklearn.feature_extraction.text.TfidfVectorizer`, `sklearn.metrics.pairwise.cosine_similarity`
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Architecture
|
| 14 |
+
|
| 15 |
+
### Initialization (`__init__`)
|
| 16 |
+
|
| 17 |
+
```python
|
| 18 |
+
def __init__(self, products_df):
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
1. Copies the products DataFrame
|
| 22 |
+
2. Builds a **text_features** column by concatenating: `category + subcategory + brand + name`
|
| 23 |
+
3. Fits a **TF-IDF vectorizer** (with English stop words removed) on all product text features
|
| 24 |
+
4. Transforms all products into a **TF-IDF matrix** of shape `(n_products × n_features)`
|
| 25 |
+
5. Stores `product_ids` array for index-to-ID mapping
|
| 26 |
+
|
| 27 |
+
**TF-IDF** (Term Frequency — Inverse Document Frequency) gives higher weight to words that are distinctive to a product ("iPhone" is more important than "device") and lower weight to common words.
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## The 2 Methods
|
| 32 |
+
|
| 33 |
+
### 1. TF-IDF Similarity (`tfidf_recommend`)
|
| 34 |
+
|
| 35 |
+
**Idea:** Build a user profile from items they rated highly, then find similar items by TF-IDF cosine similarity.
|
| 36 |
+
|
| 37 |
+
**Algorithm:**
|
| 38 |
+
1. Take `user_profile_items` — product IDs the user rated ≥ 3.5
|
| 39 |
+
2. Look up each product's index in the products DataFrame
|
| 40 |
+
3. Average their TF-IDF vectors → **user profile vector** (a single vector representing the user's taste)
|
| 41 |
+
4. Compute cosine similarity between the profile vector and ALL product vectors
|
| 42 |
+
5. Exclude already-rated items, sort by similarity, return top N
|
| 43 |
+
|
| 44 |
+
**Equation:**
|
| 45 |
+
```
|
| 46 |
+
user_profile = mean(tfidf[item] for item in user_rated_items)
|
| 47 |
+
score[product] = cosine_similarity(user_profile, tfidf[product])
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
**Complexity:** O(n_products × n_features) — linear in the product catalog size.
|
| 51 |
+
|
| 52 |
+
**Strengths:** No need for other users' data, works for niche/long-tail items, can recommend new items.
|
| 53 |
+
**Weaknesses:** Requires sufficient rating history to build a profile, tends to overspecialize (keeps recommending the same category), can't capture quality or popularity.
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
### 2. Feature Matching (`feature_match_recommend`)
|
| 58 |
+
|
| 59 |
+
**Idea:** Score each product based on how well it matches explicit user preferences (category, brand, price), without needing rating history.
|
| 60 |
+
|
| 61 |
+
**Algorithm:**
|
| 62 |
+
For each product, compute a score from preference matches:
|
| 63 |
+
|
| 64 |
+
| Criterion | Weight | Condition |
|
| 65 |
+
|---|---|---|
|
| 66 |
+
| Category match | +40 | Product category is in user's preferred categories |
|
| 67 |
+
| Brand match | +30 | Product brand is in user's favorite brands |
|
| 68 |
+
| Budget fit | +20 | Product price is within user's budget range |
|
| 69 |
+
| Subcategory match | +10 | Product subcategory is in user's preferred set |
|
| 70 |
+
|
| 71 |
+
```
|
| 72 |
+
total_score = category_score + brand_score + budget_score + subcategory_score
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**Complexity:** O(n_products) — iterates over every product once.
|
| 76 |
+
|
| 77 |
+
**Strengths:** Works with **zero rating history** — needs only preference data (cold-start friendly), transparent and explainable, simple rule-based logic.
|
| 78 |
+
**Weaknesses:** No learning from past behavior, weights are hardcoded (not learned), can't account for items that don't match stated preferences but the user might still like.
|
| 79 |
+
|
| 80 |
+
---
|
| 81 |
+
|
| 82 |
+
## Routing (`recommend` method)
|
| 83 |
+
|
| 84 |
+
```python
|
| 85 |
+
def recommend(self, method, user_profile_items=None, preferences=None, n_recommendations=10):
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
- `"tfidf"` → `tfidf_recommend(user_profile_items, n_recommendations)`
|
| 89 |
+
- `"feature_match"` → `feature_match_recommend(preferences, n_recommendations)`
|
| 90 |
+
|
| 91 |
+
Both return `list[tuple(product_id, score)]` sorted by score descending.
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
## When to Use Each
|
| 96 |
+
|
| 97 |
+
| Method | Best For | Needs |
|
| 98 |
+
|---|---|---|
|
| 99 |
+
| **TF-IDF** | Users with 5+ rated items | `user_profile_items` (rating history) |
|
| 100 |
+
| **Feature Match** | New users, cold-start | `preferences` (categories, brands, budget) |
|
| 101 |
+
|
| 102 |
+
The TF-IDF method captures subtle textual similarities between items (e.g., a "Sneakers" product in Clothing and "Running Shoes" in Footwear), while Feature Matching is explicit and rule-based.
|
discussion/03-knowledge-based.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Knowledge-Based Filtering — `recommender/knowledge_based.py`
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
Knowledge-Based Filtering makes recommendations using **explicit domain rules** and **user constraints** rather than past behavior. It's the only approach that works for cold-start users with **zero rating history** — it only needs preferences (categories, brands, budget).
|
| 6 |
+
|
| 7 |
+
**Class:** `KnowledgeBasedRecommender`
|
| 8 |
+
**File:** `recommender/knowledge_based.py` (117 lines)
|
| 9 |
+
**Dependencies:** `numpy`, `pandas`
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Architecture
|
| 14 |
+
|
| 15 |
+
### Initialization (`__init__`)
|
| 16 |
+
|
| 17 |
+
```python
|
| 18 |
+
def __init__(self, products_df):
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
Stores a copy of the products DataFrame for filtering and scoring.
|
| 22 |
+
|
| 23 |
+
No training, no fitting — this is a **memory-based, rule-driven approach** that operates directly on product attributes.
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## The 3 Methods
|
| 28 |
+
|
| 29 |
+
### 1. Constraint-Based (`constraint_based`)
|
| 30 |
+
|
| 31 |
+
**Idea:** Filter the product catalog by hard constraints, then sort by rating.
|
| 32 |
+
|
| 33 |
+
**Algorithm:**
|
| 34 |
+
1. Start with all products
|
| 35 |
+
2. **Sequentially filter** by (in order):
|
| 36 |
+
- `budget_max` — remove products exceeding max budget
|
| 37 |
+
- `budget_min` — remove products below min budget
|
| 38 |
+
- `category` — keep only preferred categories (if specified)
|
| 39 |
+
- `brand` — if brand preferences exist AND matches found, restrict to those brands only
|
| 40 |
+
- `min_rating` — keep only products above minimum rating
|
| 41 |
+
- `subcategory` — keep matching subcategories (if any found)
|
| 42 |
+
3. Sort remaining products by `avg_rating` descending
|
| 43 |
+
4. Return top N
|
| 44 |
+
|
| 45 |
+
**Key behavior:** Brand and subcategory filters only activate if there's at least one match — otherwise they're skipped (prevents empty results from overly strict constraints).
|
| 46 |
+
|
| 47 |
+
**Complexity:** O(n_products) — each filter is a pandas operation on the DataFrame.
|
| 48 |
+
|
| 49 |
+
**Strengths:** Deterministic, transparent, respects hard constraints perfectly.
|
| 50 |
+
**Weaknesses:** Can return empty results if constraints are too strict, no ranking beyond rating, doesn't consider user preferences for non-filtered attributes.
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
### 2. Rule-Based (`rule_based`)
|
| 55 |
+
|
| 56 |
+
**Idea:** Apply cross-selling rules and preference scoring — "laptop buyers also buy accessories."
|
| 57 |
+
|
| 58 |
+
**Built-in Cross-Sell Rules:**
|
| 59 |
+
|
| 60 |
+
| If user interacted with... | Recommend accessories like... |
|
| 61 |
+
|---|---|
|
| 62 |
+
| `Laptops` | Laptop Bag, Mouse, Keyboard, Cooling Pad |
|
| 63 |
+
| `Smartphones` | Phone Case, Screen Protector, Charger, Power Bank |
|
| 64 |
+
| `Cameras` | Camera Bag, Tripod, Memory Card, Lens Kit |
|
| 65 |
+
| `Headphones` | Headphone Stand, Cable, Carrying Case, Ear Pads |
|
| 66 |
+
|
| 67 |
+
**Scoring Formula:**
|
| 68 |
+
|
| 69 |
+
```
|
| 70 |
+
score = 0
|
| 71 |
+
|
| 72 |
+
if interacted_category matches and product is a related accessory:
|
| 73 |
+
score += 50 (cross-sell bonus)
|
| 74 |
+
|
| 75 |
+
if product category in preferred_categories:
|
| 76 |
+
score += 20 (category preference)
|
| 77 |
+
|
| 78 |
+
if price within budget range:
|
| 79 |
+
score += 15 (budget fit)
|
| 80 |
+
|
| 81 |
+
if brand in favorite_brands:
|
| 82 |
+
score += 10 (brand loyalty)
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
**Complexity:** O(n_products) — scores every product once.
|
| 86 |
+
|
| 87 |
+
**Strengths:** Captures retail domain knowledge (cross-selling), rewards accessory discovery along with category/brand preferences.
|
| 88 |
+
**Weaknesses:** Rules are hardcoded for Electronics only, doesn't learn from data, `interacted_category` must be provided by the application.
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
### 3. Utility-Based (`utility_based`)
|
| 93 |
+
|
| 94 |
+
**Idea:** Compute a weighted multi-attribute utility score for every product, measuring how well it satisfies the user's preferences across multiple dimensions.
|
| 95 |
+
|
| 96 |
+
**Utility Function:**
|
| 97 |
+
|
| 98 |
+
```
|
| 99 |
+
U(product) = w_price × price_score
|
| 100 |
+
+ w_category × category_score
|
| 101 |
+
+ w_brand × brand_score
|
| 102 |
+
+ w_rating × rating_score
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
**Default Weights:**
|
| 106 |
+
|
| 107 |
+
| Attribute | Weight | Rationale |
|
| 108 |
+
|---|---|---|
|
| 109 |
+
| `category` | 0.3 (30%) | Category preference is strongest signal |
|
| 110 |
+
| `rating` | 0.3 (30%) | Product quality equally important |
|
| 111 |
+
| `brand` | 0.2 (20%) | Brand loyalty is meaningful |
|
| 112 |
+
| `price` | 0.2 (20%) | Budget fit is a soft constraint |
|
| 113 |
+
|
| 114 |
+
**Attribute Score Functions:**
|
| 115 |
+
|
| 116 |
+
- **Price score:** Normalized distance from midpoint of user's budget range — penalizes items at the edges, rewards the center
|
| 117 |
+
```
|
| 118 |
+
price_score = 1 - |price - budget_mid| / (budget_range / 2)
|
| 119 |
+
clamped to [0, 1]
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
- **Category score:** 1.0 if in preferred categories, 0.0 otherwise
|
| 123 |
+
|
| 124 |
+
- **Brand score:** 1.0 if in favorite brands, 0.0 otherwise
|
| 125 |
+
|
| 126 |
+
- **Rating score:** Normalized by max rating in catalog
|
| 127 |
+
```
|
| 128 |
+
rating_score = avg_rating / max_rating_in_catalog
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
**Complexity:** O(n_products) — computes utility for every product.
|
| 132 |
+
|
| 133 |
+
**Strengths:** Continuous scoring (not binary filtering), learnable weights (could be optimized with feedback), handles trade-offs between competing preferences.
|
| 134 |
+
**Weaknesses:** Weights are fixed but could be tuned, equal treatment within attributes (all categories weighted equally).
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
## Routing (`recommend` method)
|
| 139 |
+
|
| 140 |
+
```python
|
| 141 |
+
def recommend(self, method, constraints=None, context=None, preferences=None, n_recommendations=10):
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
- `"constraint"` → `constraint_based(constraints, n_recommendations)`
|
| 145 |
+
- `"rule"` → `rule_based(context, n_recommendations)`
|
| 146 |
+
- `"utility"` → `utility_based(preferences, n_recommendations=n_recommendations)`
|
| 147 |
+
|
| 148 |
+
All return `list[tuple(product_id, score)]` sorted by score descending.
|
| 149 |
+
|
| 150 |
+
---
|
| 151 |
+
|
| 152 |
+
## Knowledge-Based vs Other Approaches
|
| 153 |
+
|
| 154 |
+
| Aspect | Knowledge-Based | Collaborative | Content-Based |
|
| 155 |
+
|---|---|---|---|
|
| 156 |
+
| Needs rating history | **No** | Yes | Yes (TF-IDF) |
|
| 157 |
+
| Needs preferences | Yes | No | Yes (Feature Match) |
|
| 158 |
+
| Cold-start capable | **Yes** | No | Partial |
|
| 159 |
+
| Learns from data | No | Yes | Partial |
|
| 160 |
+
| Deterministic | **Yes** | No (SVD stochastic) | Yes |
|
| 161 |
+
| Explainability | High | Medium | High |
|
discussion/04-evaluation.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Evaluation Metrics — `recommender/evaluation.py`
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Evaluator measures recommendation quality using **6 standard metrics** across 4 dimensions: prediction accuracy (RMSE, MAE) and ranking quality (Precision, Recall, F1, Coverage).
|
| 6 |
+
|
| 7 |
+
**Class:** `Evaluator`
|
| 8 |
+
**File:** `recommender/evaluation.py` (161 lines)
|
| 9 |
+
**Dependencies:** `numpy`, `pandas`, `sklearn.metrics`
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Architecture
|
| 14 |
+
|
| 15 |
+
### Initialization (`__init__`)
|
| 16 |
+
|
| 17 |
+
```python
|
| 18 |
+
def __init__(self, ratings_df, predictions_df=None):
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
Stores the full ratings DataFrame and optionally pre-computed predictions. The evaluator needs a **test set** to compare predicted vs actual ratings.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## The 6 Metrics
|
| 26 |
+
|
| 27 |
+
### 1. RMSE — Root Mean Square Error
|
| 28 |
+
|
| 29 |
+
**What it measures:** Average magnitude of prediction errors (larger errors penalized more due to squaring).
|
| 30 |
+
|
| 31 |
+
```python
|
| 32 |
+
def rmse(self, y_true, y_pred):
|
| 33 |
+
return float(np.sqrt(mean_squared_error(y_true, y_pred)))
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
**Range:** `0.0` (perfect) to `5.0` (worst, on a 1-5 scale)
|
| 37 |
+
**Interpretation:** A score of 0.95 means predictions are off by ~0.95 stars on average.
|
| 38 |
+
**Lower is better.**
|
| 39 |
+
|
| 40 |
+
**Why squared?** Errors of 2.0 count 4× more than errors of 1.0 — this penalizes large outliers.
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
### 2. MAE — Mean Absolute Error
|
| 45 |
+
|
| 46 |
+
**What it measures:** Average absolute prediction error (treats all errors equally).
|
| 47 |
+
|
| 48 |
+
```python
|
| 49 |
+
def mae(self, y_true, y_pred):
|
| 50 |
+
return float(mean_absolute_error(y_true, y_pred))
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
**Range:** `0.0` to `5.0`
|
| 54 |
+
**Interpretation:** Same units as ratings (stars). MAE < RMSE means no large outliers.
|
| 55 |
+
**Lower is better.**
|
| 56 |
+
|
| 57 |
+
**RMSE vs MAE:** If RMSE is significantly higher than MAE, the model occasionally makes very bad predictions.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
### 3. Precision@k
|
| 62 |
+
|
| 63 |
+
**What it measures:** Of the top-k recommendations, what fraction are actually relevant (rated ≥ 3.5 by the user)?
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
def precision_at_k(self, recommended_items, relevant_items, k=5):
|
| 67 |
+
top_k = recommended_items[:k]
|
| 68 |
+
hits = len(set(top_k) & set(relevant_items))
|
| 69 |
+
return hits / k if k > 0 else 0
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
**Range:** `0.0` to `1.0`
|
| 73 |
+
**Interpretation:** Precision@5 = 0.05 means 0.25 out of 5 recommendations are relevant (1 item in 20).
|
| 74 |
+
**Higher is better.**
|
| 75 |
+
|
| 76 |
+
**Why @k?** Users only see the top-k recommendations — Precision@k measures real-world utility better than global precision.
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
### 4. Recall@k
|
| 81 |
+
|
| 82 |
+
**What it measures:** Of all the items the user would find relevant, what fraction appears in the top-k?
|
| 83 |
+
|
| 84 |
+
```python
|
| 85 |
+
def recall_at_k(self, recommended_items, relevant_items, k=5):
|
| 86 |
+
top_k = recommended_items[:k]
|
| 87 |
+
hits = len(set(top_k) & set(relevant_items))
|
| 88 |
+
return hits / len(relevant_items) if len(relevant_items) > 0 else 0
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
**Range:** `0.0` to `1.0`
|
| 92 |
+
**Interpretation:** Recall@5 = 0.045 means the top-5 captures 4.5% of all relevant items.
|
| 93 |
+
**Higher is better.**
|
| 94 |
+
|
| 95 |
+
**Precision vs Recall:** Precision asks "are the recommendations good?" Recall asks "are we finding all the good items?" There's typically a trade-off.
|
| 96 |
+
|
| 97 |
+
---
|
| 98 |
+
|
| 99 |
+
### 5. F1@k
|
| 100 |
+
|
| 101 |
+
**What it measures:** Harmonic mean of Precision and Recall — a single score balancing both.
|
| 102 |
+
|
| 103 |
+
```python
|
| 104 |
+
def f1_at_k(self, recommended_items, relevant_items, k=5):
|
| 105 |
+
p = self.precision_at_k(recommended_items, relevant_items, k)
|
| 106 |
+
r = self.recall_at_k(recommended_items, relevant_items, k)
|
| 107 |
+
return 2 * p * r / (p + r) if (p + r) > 0 else 0
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
**Range:** `0.0` to `1.0`
|
| 111 |
+
**Interpretation:** F1@5 = 0.036 means balanced precision-recall performance.
|
| 112 |
+
**Higher is better.** F1 is low if either precision or recall is low.
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
### 6. Coverage
|
| 117 |
+
|
| 118 |
+
**What it measures:** What fraction of the total product catalog does the recommender ever recommend across all users?
|
| 119 |
+
|
| 120 |
+
```python
|
| 121 |
+
def coverage(self, recommended_items_list, total_items):
|
| 122 |
+
recommended_set = set()
|
| 123 |
+
for items in recommended_items_list:
|
| 124 |
+
recommended_set.update(items)
|
| 125 |
+
return len(recommended_set) / total_items if total_items > 0 else 0
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
**Range:** `0.0` to `1.0`
|
| 129 |
+
**Interpretation:** Coverage = 0.86 means 86% of all products appear in at least one user's recommendations.
|
| 130 |
+
**Higher is better.** Low coverage means the system only recommends popular items (the "Harry Potter problem").
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## Evaluation Process
|
| 135 |
+
|
| 136 |
+
### Per-Method Evaluation (`evaluate_cf_method`)
|
| 137 |
+
|
| 138 |
+
For each CF method, the evaluator:
|
| 139 |
+
|
| 140 |
+
1. **Selects up to 20 test users** from the test set
|
| 141 |
+
2. For each user:
|
| 142 |
+
- Generates 20 recommendations using the CF method
|
| 143 |
+
- Collects actual ratings from the test set
|
| 144 |
+
- Compares predicted vs actual for RMSE/MAE
|
| 145 |
+
- Computes Precision/Recall/F1@k for ranking quality
|
| 146 |
+
3. Aggregates all metrics across users
|
| 147 |
+
|
| 148 |
+
**Handling missing predictions:** If a product in the test set wasn't predicted, the evaluator imputes a rating of 2.5 (neutral) — this penalizes methods that can't predict all test items.
|
| 149 |
+
|
| 150 |
+
### Full Comparison (`compare_cf_methods`)
|
| 151 |
+
|
| 152 |
+
Iterates over all 5 CF methods, calling `evaluate_cf_method` for each. Returns a list of result dicts.
|
| 153 |
+
|
| 154 |
+
### Approach Comparison (`compare_approaches`)
|
| 155 |
+
|
| 156 |
+
Compares the 3 approaches (CF, Content, Knowledge) by:
|
| 157 |
+
1. Using the **best** CF method (Item-Based) for the CF column
|
| 158 |
+
2. Using TF-IDF for Content-Based
|
| 159 |
+
3. Using Constraint-Based for Knowledge-Based
|
| 160 |
+
4. Evaluating on the same 20 test users
|
| 161 |
+
|
| 162 |
+
---
|
| 163 |
+
|
| 164 |
+
## How Evaluation is Used
|
| 165 |
+
|
| 166 |
+
The backend exposes two endpoints:
|
| 167 |
+
|
| 168 |
+
- **`GET /api/evaluate/cf/<method>`** — evaluates a single CF method (returns metrics in ~2-15s)
|
| 169 |
+
- **`GET /api/evaluate/cf/<method>/stream`** — streaming version for SVD and Slope One (shows epoch/item progress)
|
| 170 |
+
- **`GET /api/evaluate/approaches`** — compares all 3 approaches
|
| 171 |
+
|
| 172 |
+
The frontend calls these incrementally, showing results as each method finishes.
|
discussion/05-explainer.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Explanation Generator — `recommender/explainer.py`
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Explainer generates **human-readable explanations** for every recommendation. Each of the 10 recommendation methods has its own explanation template, incorporating product and user details for specificity.
|
| 6 |
+
|
| 7 |
+
**Class:** `Explainer`
|
| 8 |
+
**File:** `recommender/explainer.py` (99 lines)
|
| 9 |
+
**Dependencies:** None (pure Python)
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Architecture
|
| 14 |
+
|
| 15 |
+
### Initialization (`__init__`)
|
| 16 |
+
|
| 17 |
+
```python
|
| 18 |
+
def __init__(self, products_df, users_df):
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
Builds two in-memory **caches** (dictionaries) for fast lookup:
|
| 22 |
+
- `_product_cache`: `{product_id: pandas_Series}` — full product row (name, category, brand, price, subcategory)
|
| 23 |
+
- `_user_cache`: `{user_id: pandas_Series}` — full user row (name, age, preferred_categories, favorite_brands, budget)
|
| 24 |
+
|
| 25 |
+
This avoids pandas DataFrame lookups during real-time explanation generation.
|
| 26 |
+
|
| 27 |
+
### Helper Methods
|
| 28 |
+
|
| 29 |
+
| Method | Purpose |
|
| 30 |
+
|---|---|
|
| 31 |
+
| `_get_product(product_id)` | Returns product data dict from cache |
|
| 32 |
+
| `_get_user(user_id)` | Returns user data dict from cache |
|
| 33 |
+
| `_pref_list(user, key)` | Safely extracts comma-separated preferences, handles NaN |
|
| 34 |
+
|
| 35 |
+
The `_pref_list` method guards against `NaN` values from the pandas cache (a common edge case when user fields are empty in the CSV):
|
| 36 |
+
```python
|
| 37 |
+
def _pref_list(self, user, key):
|
| 38 |
+
raw = user.get(key, "")
|
| 39 |
+
if raw is None or isinstance(raw, float):
|
| 40 |
+
return []
|
| 41 |
+
if isinstance(raw, str):
|
| 42 |
+
return [p.strip() for p in raw.split(",") if p.strip()][:2]
|
| 43 |
+
return list(raw)[:2] if raw else []
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## Explanation Templates by Method
|
| 49 |
+
|
| 50 |
+
### Collaborative Filtering (5 methods)
|
| 51 |
+
|
| 52 |
+
| Method | Template | Example Output |
|
| 53 |
+
|---|---|---|
|
| 54 |
+
| `user_based` | `"Users with {category} taste also liked {name}"` | "Users with Electronics taste also liked X-T5" |
|
| 55 |
+
| `item_based` | `"Matches your {category} preferences — similar to items you've rated highly"` | "Matches your Toys preferences — similar to items you've rated highly" |
|
| 56 |
+
| `svd` | `"Fits your profile: {brand}'s {category} — top latent factor match"` | "Fits your profile: Dyson's Home & Kitchen — top latent factor match" |
|
| 57 |
+
| `knn` | `"Popular among peers who also like {category}"` | "Popular among peers who also like Sports" |
|
| 58 |
+
| `slope_one` | `"Frequently chosen by users who liked the same {category} products"` | "Frequently chosen by users who liked the same Toys products" |
|
| 59 |
+
|
| 60 |
+
### Content-Based (2 methods)
|
| 61 |
+
|
| 62 |
+
| Method | Template | Example Output |
|
| 63 |
+
|---|---|---|
|
| 64 |
+
| `tfidf` | `"Content matches: {category} / {subcategory} — strongly aligns with your past likes"` | "Content matches: Home & Kitchen / Storage — strongly aligns with your past likes" |
|
| 65 |
+
| `feature_match` | `"Matches your preferences: {category}, {brand}"` | "Matches your preferences: Home & Kitchen, KitchenAid" |
|
| 66 |
+
|
| 67 |
+
### Knowledge-Based (3 methods)
|
| 68 |
+
|
| 69 |
+
| Method | Template | Example Output |
|
| 70 |
+
|---|---|---|
|
| 71 |
+
| `constraint` | `"Fits your criteria: ${price} within budget · {category} · {brand}"` | "Fits your criteria: $241 within budget · Books · Penguin" |
|
| 72 |
+
| `rule` | `"{category} buyers commonly add {name} — cross-sell match"` | "Home & Kitchen buyers commonly add Sofa Set — cross-sell match" |
|
| 73 |
+
| `utility` | `"Top utility score: {category} × {brand} matches your preference weights"` | "Top utility score: Books × Macmillan matches your preference weights" |
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## Routing (`get_explanation`)
|
| 78 |
+
|
| 79 |
+
```python
|
| 80 |
+
def get_explanation(self, approach, method, user_id, product_id, details=None):
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
Dispatches to the correct explainer based on approach:
|
| 84 |
+
- `"cf"` → `explain_cf(method, user_id, product_id, details)`
|
| 85 |
+
- `"content"` → `explain_content(method, user_id, product_id, details)`
|
| 86 |
+
- `"knowledge"` → `explain_knowledge(method, user_id, product_id, details)`
|
| 87 |
+
|
| 88 |
+
Each sub-explainer receives **details** (a dict from the backend with scores and metadata) and uses `_get_product` / `_get_user` to access full attribute data.
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
## Design Principles
|
| 93 |
+
|
| 94 |
+
1. **Specific over generic** — Every template includes the product's category, brand, or subcategory. No two explanations read the same way.
|
| 95 |
+
2. **No fake precision** — Scores are not shown as "100% similar" in templates. The score bar (frontend) shows relative strength via a progress bar.
|
| 96 |
+
3. **Domain-aware** — Knowledge templates mention budgets, cross-sell rules, and utility scores.
|
| 97 |
+
4. **Safe defaults** — Fallback templates exist for every method branch in case data is missing.
|
discussion/06-similarity.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Similarity Metrics — `utils/similarity.py`
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
Provides 4 similarity/distance metrics used by the Collaborative Filtering module. These measure how "close" two users or two items are based on their rating vectors.
|
| 6 |
+
|
| 7 |
+
**File:** `utils/similarity.py` (30 lines)
|
| 8 |
+
**Dependencies:** `numpy`, `sklearn.metrics.pairwise`
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## The 4 Metrics
|
| 13 |
+
|
| 14 |
+
### 1. Cosine Similarity
|
| 15 |
+
|
| 16 |
+
**Used by:** User-Based CF (full similarity matrix)
|
| 17 |
+
|
| 18 |
+
```python
|
| 19 |
+
def cosine_similarity(matrix):
|
| 20 |
+
return sklearn_cosine(matrix)
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
**What it measures:** The angle between two vectors, ignoring magnitude. Two users who rate items in the same proportion (one rates everything 4s, the other 5s) are considered similar.
|
| 24 |
+
|
| 25 |
+
**Equation:**
|
| 26 |
+
```
|
| 27 |
+
cos(A, B) = (A · B) / (|A| × |B|)
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
**Range:** `-1` to `1` (for rating vectors, typically `0` to `1` since ratings are positive)
|
| 31 |
+
|
| 32 |
+
**Why not Pearson?** Cosine similarity with raw ratings is simpler and performs comparably when data isn't heavily biased. The Item-Based method uses **adjusted cosine** which subtracts user means first (equivalent to Pearson normalization).
|
| 33 |
+
|
| 34 |
+
---
|
| 35 |
+
|
| 36 |
+
### 2. Pearson Similarity
|
| 37 |
+
|
| 38 |
+
**Defined but not currently used** by any active method.
|
| 39 |
+
|
| 40 |
+
```python
|
| 41 |
+
def pearson_similarity(matrix):
|
| 42 |
+
matrix_centered = matrix - np.nanmean(matrix, axis=1, keepdims=True)
|
| 43 |
+
matrix_centered = np.nan_to_num(matrix_centered, nan=0.0)
|
| 44 |
+
mask = ~np.isnan(matrix)
|
| 45 |
+
norm = np.sqrt(np.nansum(matrix_centered ** 2, axis=1, keepdims=True))
|
| 46 |
+
norm[norm == 0] = 1
|
| 47 |
+
matrix_norm = matrix_centered / norm
|
| 48 |
+
return matrix_norm @ matrix_norm.T
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
**What it measures:** Linear correlation between rating patterns. Accounts for user bias — a user who rates everything 4-5 and another who rates 2-3 might agree on relative preferences even though absolute ratings differ.
|
| 52 |
+
|
| 53 |
+
**Why unused:** On this dataset, cosine similarity with mean-imputed missing values gives comparable results with less code complexity.
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
### 3. Adjusted Cosine Similarity
|
| 58 |
+
|
| 59 |
+
**Used by:** Item-Based CF (item-item similarity matrix)
|
| 60 |
+
|
| 61 |
+
```python
|
| 62 |
+
def adjusted_cosine_similarity(matrix):
|
| 63 |
+
user_mean = np.nanmean(matrix, axis=1, keepdims=True)
|
| 64 |
+
matrix_centered = matrix - user_mean
|
| 65 |
+
matrix_centered = np.nan_to_num(matrix_centered, nan=0.0)
|
| 66 |
+
return sklearn_cosine(matrix_centered.T)
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
**What it measures:** Cosine similarity on **mean-centered ratings** (each user's ratings shifted by their average). Then computes similarity between **items** (transposed matrix), not users.
|
| 70 |
+
|
| 71 |
+
**Why adjusted?** Raw cosine similarity between items is misleading because users have different rating scales. Alice might give 5 to everything she likes, while Bob gives 4. Adjusted cosine removes this bias by centering.
|
| 72 |
+
|
| 73 |
+
**Implementation note:** The `.T` transposes the user-item matrix to item-user before computing cosine, giving `n_items × n_items` similarity.
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
### 4. Jaccard Similarity
|
| 78 |
+
|
| 79 |
+
**Defined but not used** by any active method.
|
| 80 |
+
|
| 81 |
+
```python
|
| 82 |
+
def jaccard_similarity(set1, set2):
|
| 83 |
+
intersection = len(set1 & set2)
|
| 84 |
+
union = len(set1 | set2)
|
| 85 |
+
return intersection / union if union > 0 else 0
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
**What it measures:** Overlap between two sets — suitable for binary data (purchased/not purchased) rather than rating data.
|
| 89 |
+
|
| 90 |
+
**Why unused:** Rating data is continuous (1-5), not binary. Jaccard would waste information by discarding rating values.
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
## Metric Selection Guide
|
| 95 |
+
|
| 96 |
+
| Metric | Used By | Data Type | Key Property |
|
| 97 |
+
|---|---|---|---|
|
| 98 |
+
| Cosine Similarity | User-Based CF, KNN | Continuous ratings | Simple, fast |
|
| 99 |
+
| Adjusted Cosine | Item-Based CF | Continuous ratings | Removes user bias |
|
| 100 |
+
| Pearson | (available) | Continuous ratings | Correlation-based |
|
| 101 |
+
| Jaccard | (available) | Binary sets | Set overlap |
|
| 102 |
+
|
| 103 |
+
All metrics are computed using **imputed matrices** — missing values (`NaN`) are filled with the global mean before similarity computation.
|
discussion/07-helpers.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Utilities — `utils/helpers.py`
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
Shared utility functions used by the recommender modules and the Flask application. Provides data loading, matrix construction, and user preference extraction.
|
| 6 |
+
|
| 7 |
+
**File:** `utils/helpers.py` (37 lines)
|
| 8 |
+
**Dependencies:** `pandas`
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## Functions
|
| 13 |
+
|
| 14 |
+
### 1. `load_data()`
|
| 15 |
+
|
| 16 |
+
```python
|
| 17 |
+
def load_data():
|
| 18 |
+
products = pd.read_csv(...)
|
| 19 |
+
users = pd.read_csv(...)
|
| 20 |
+
ratings = pd.read_csv(...)
|
| 21 |
+
return products, users, ratings
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
**Purpose:** Loads the 3 CSV files from the `data/` directory into pandas DataFrames.
|
| 25 |
+
|
| 26 |
+
**Returns:**
|
| 27 |
+
- `products` — 201 products with columns: `product_id, name, category, subcategory, brand, price, avg_rating, num_reviews`
|
| 28 |
+
- `users` — 200 users with columns: `user_id, name, age, preferred_categories, budget_min, budget_max, favorite_brands`
|
| 29 |
+
- `ratings` — 8000 ratings with columns: `user_id, product_id, rating`
|
| 30 |
+
|
| 31 |
+
**Called by:** `app.py` at module load time, data generator.
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
### 2. `build_user_item_matrix()`
|
| 36 |
+
|
| 37 |
+
```python
|
| 38 |
+
def build_user_item_matrix(ratings_df):
|
| 39 |
+
return ratings_df.pivot_table(
|
| 40 |
+
index="user_id", columns="product_id", values="rating"
|
| 41 |
+
)
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
**Purpose:** Pivots the ratings DataFrame into a **sparse matrix** where:
|
| 45 |
+
- Rows = users (by `user_id`)
|
| 46 |
+
- Columns = items (by `product_id`)
|
| 47 |
+
- Values = ratings (`NaN` where no rating exists)
|
| 48 |
+
|
| 49 |
+
**Output shape:** `(200 users, 201 products)` — a 200×201 matrix with ~8000 non-NaN entries (~20% density).
|
| 50 |
+
|
| 51 |
+
**Called by:** `CollaborativeFiltering.__init__()`
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
### 3. `get_user_preferences()`
|
| 56 |
+
|
| 57 |
+
```python
|
| 58 |
+
def get_user_preferences(users_df, user_id):
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
**Purpose:** Extracts a user's preference profile from the users DataFrame into a clean Python dict.
|
| 62 |
+
|
| 63 |
+
**Returns:**
|
| 64 |
+
```python
|
| 65 |
+
{
|
| 66 |
+
"preferred_categories": {"Electronics", "Sports"}, # set
|
| 67 |
+
"budget_min": 10.0, # float
|
| 68 |
+
"budget_max": 500.0, # float
|
| 69 |
+
"favorite_brands": {"Nike", "Samsung"}, # set
|
| 70 |
+
"name": "Alice_1", # str
|
| 71 |
+
"age": 28, # int
|
| 72 |
+
}
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**Key details:**
|
| 76 |
+
- `preferred_categories` and `favorite_brands` are stored as comma-separated strings in CSV → converted to Python `set()` for efficient membership testing
|
| 77 |
+
- All numeric types are converted to native Python types (`int`, `float`) for JSON serialization compatibility
|
| 78 |
+
- Returns empty dict `{}` if user not found
|
| 79 |
+
|
| 80 |
+
**Called by:** `app.py` API endpoints (`/api/recommend`, `/api/user/<id>/preferences`, evaluation).
|
| 81 |
+
|
| 82 |
+
---
|
| 83 |
+
|
| 84 |
+
## Data Flow
|
| 85 |
+
|
| 86 |
+
```
|
| 87 |
+
data/*.csv → load_data() → products, users, ratings (global DataFrames)
|
| 88 |
+
↓
|
| 89 |
+
build_user_item_matrix(ratings) → CF user-item matrix
|
| 90 |
+
↓
|
| 91 |
+
get_user_preferences(users, id) → preference dict
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
The DataFrames are loaded once at module load time and shared across all request handlers.
|
discussion/08-data-generator.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Synthetic Data Generator — `data/generate_data.py`
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
Generates a complete synthetic e-commerce dataset with 201 products, 200 users, 8000 ratings, and 2000 purchase interactions. The data has realistic structure: category hierarchies, brand-category associations, and preference-influenced ratings.
|
| 6 |
+
|
| 7 |
+
**File:** `data/generate_data.py` (240 lines)
|
| 8 |
+
**Dependencies:** `pandas`, `numpy`
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## Generated Files
|
| 13 |
+
|
| 14 |
+
| File | Records | Columns |
|
| 15 |
+
|---|---|---|
|
| 16 |
+
| `products.csv` | 201 | `product_id, name, category, subcategory, brand, price, avg_rating, num_reviews` |
|
| 17 |
+
| `users.csv` | 200 | `user_id, name, age, preferred_categories, budget_min, budget_max, favorite_brands` |
|
| 18 |
+
| `ratings.csv` | 8000 | `user_id, product_id, rating` |
|
| 19 |
+
| `interactions.csv` | 2000 | `user_id, product_id, purchased, quantity` |
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## Product Generation (`generate_products`)
|
| 24 |
+
|
| 25 |
+
### Category Hierarchy
|
| 26 |
+
|
| 27 |
+
8 categories, each with 5 subcategories (40 subcategories total):
|
| 28 |
+
|
| 29 |
+
| Category | Example Subcategories | Brands |
|
| 30 |
+
|---|---|---|
|
| 31 |
+
| Electronics | Smartphones, Laptops, Headphones, Tablets, Cameras | Samsung, Apple, Sony, LG... |
|
| 32 |
+
| Clothing | Men's, Women's, Kids', Accessories, Footwear | Nike, Adidas, Zara, H&M... |
|
| 33 |
+
| Home & Kitchen | Furniture, Appliances, Cookware, Decor, Storage | IKEA, KitchenAid, Ninja... |
|
| 34 |
+
| Books | Fiction, Non-Fiction, Science, Technology, Self-Help | Penguin, HarperCollins... |
|
| 35 |
+
| Sports | Fitness, Outdoor, Team Sports, Cycling, Swimming | Nike, Wilson, Adidas... |
|
| 36 |
+
| Beauty | Skincare, Makeup, Haircare, Fragrance, Tools | L'Oreal, Clinique, Neutrogena... |
|
| 37 |
+
| Toys | Educational, Action Figures, Board Games, Dolls, Puzzles | LEGO, Mattel, Hasbro... |
|
| 38 |
+
| Automotive | Car Care, Interior, Exterior, Tools, Electronics | Meguiar's, Armor All... |
|
| 39 |
+
|
| 40 |
+
### Product Generation Logic
|
| 41 |
+
- 8 products per subcategory → 5 subcategories × 8 = 40 products per category → 8 categories × 40 = 320 total... but limited to available names (varies by subcategory)
|
| 42 |
+
- **Price:** Uniform random between \$9.99 and \$1,499.99
|
| 43 |
+
- **Rating:** Uniform random between 3.0 and 5.0 (rounded to 1 decimal)
|
| 44 |
+
- **Reviews:** Random integer between 10 and 500
|
| 45 |
+
|
| 46 |
+
### Product Name Templates
|
| 47 |
+
Product names are curated per subcategory (e.g., Electronics/Laptops → "ThinkPad X1", "MacBook Pro", "XPS 15"). Each subcategory has 5 unique named products.
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## User Generation (`generate_users`)
|
| 52 |
+
|
| 53 |
+
Each user gets:
|
| 54 |
+
1. **Name** — Random from 30 first names + user ID suffix (e.g., "Alice_1", "Bob_2")
|
| 55 |
+
2. **Age** — Random integer 18-65
|
| 56 |
+
3. **Preferred Categories** — 1-3 categories randomly selected from the 8 available
|
| 57 |
+
4. **Budget** — `budget_min` (\$10-\$200) and `budget_max` (budget_min + \$50-\$1,500)
|
| 58 |
+
5. **Favorite Brands** — 0-2 brands randomly chosen from ALL brands across categories
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## Rating Generation (`generate_ratings`)
|
| 63 |
+
|
| 64 |
+
### Data Volume
|
| 65 |
+
- **8000 ratings** for 200 users × 201 products = 40,200 possible pairs → ~20% density
|
| 66 |
+
- Each user has ~40 ratings on average (range varies by random selection)
|
| 67 |
+
|
| 68 |
+
### Rating Formula
|
| 69 |
+
|
| 70 |
+
```
|
| 71 |
+
base = 3.0 (starting point)
|
| 72 |
+
|
| 73 |
+
if product.category in user.preferred_categories:
|
| 74 |
+
base += random(0.5, 1.5) # category preference bonus
|
| 75 |
+
|
| 76 |
+
if product.brand in user.favorite_brands:
|
| 77 |
+
base += random(0.3, 1.0) # brand loyalty bonus
|
| 78 |
+
|
| 79 |
+
if product.price within user.budget:
|
| 80 |
+
base += random(0.0, 0.5) # budget fit bonus
|
| 81 |
+
else:
|
| 82 |
+
base -= random(0.0, 0.5) # budget mismatch penalty
|
| 83 |
+
|
| 84 |
+
rating = clamp(base + gaussian_noise(0, 0.5), to [1.0, 5.0])
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
This creates realistic patterns: users rate preferred-category items higher, within-budget items higher, and favorite-brand items higher. The Gaussian noise adds realistic variance.
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## Interaction Generation (`generate_interactions`)
|
| 92 |
+
|
| 93 |
+
Samples 2000 rows from the ratings DataFrame and marks them as "purchased" with a random quantity (1-3). This simulates purchase history separate from rating history.
|
| 94 |
+
|
| 95 |
+
---
|
| 96 |
+
|
| 97 |
+
## Running the Generator
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
cd TasteEngine
|
| 101 |
+
python data/generate_data.py
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
This overwrites the 4 CSV files in `data/` with fresh synthetic data. The random seed is fixed (`np.random.seed(42)`) for reproducible output.
|
discussion/09-app.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Flask Web Application — `app.py`
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Flask application ties together all recommender modules, serving both **HTML pages** (server-rendered with Jinja2) and **JSON API endpoints** (consumed by Alpine.js on the frontend).
|
| 6 |
+
|
| 7 |
+
**File:** `app.py` (453 lines)
|
| 8 |
+
**Dependencies:** `Flask`, `pandas`, `numpy`, `recommender/*`, `utils/*`
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## Architecture
|
| 13 |
+
|
| 14 |
+
### Module-Level Initialization
|
| 15 |
+
|
| 16 |
+
```python
|
| 17 |
+
products, users, ratings = load_data() # Load CSVs once at startup
|
| 18 |
+
cf = CollaborativeFiltering(ratings) # CF with 5 methods
|
| 19 |
+
cb = ContentBasedRecommender(products) # Content-Based with 2 methods
|
| 20 |
+
kb = KnowledgeBasedRecommender(products) # Knowledge-Based with 3 methods
|
| 21 |
+
explainer = Explainer(products, users) # Explanation generator
|
| 22 |
+
evaluator = Evaluator(ratings) # Evaluation metrics
|
| 23 |
+
|
| 24 |
+
TRAIN, TEST = train_test_split(ratings) # 80/20 split
|
| 25 |
+
cf_train = CollaborativeFiltering(TRAIN) # CF trained only on training set
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
All recommender instances are **global singletons** — created once when the server starts and shared across all requests. This means caches (SVD factors, Slope One deviation matrix) persist across requests.
|
| 29 |
+
|
| 30 |
+
### Pre-computed Data
|
| 31 |
+
|
| 32 |
+
| Variable | Type | Purpose |
|
| 33 |
+
|---|---|---|
|
| 34 |
+
| `USER_OPTIONS` | `list[dict]` | User dropdown options (name, age, categories, budget, brands) |
|
| 35 |
+
| `CATEGORIES` | `list[str]` | All product categories |
|
| 36 |
+
| `BRANDS` | `list[str]` | All product brands |
|
| 37 |
+
| `APPROACHES` | `dict` | Approach/method definitions for UI buttons |
|
| 38 |
+
|
| 39 |
+
---
|
| 40 |
+
|
| 41 |
+
## Routes
|
| 42 |
+
|
| 43 |
+
### Page Routes (HTML)
|
| 44 |
+
|
| 45 |
+
| Route | Template | Description |
|
| 46 |
+
|---|---|---|
|
| 47 |
+
| `GET /` | `index.html` | Landing page with user selection + profile + preference editor |
|
| 48 |
+
| `GET /recommend` | `recommend.html` | Recommendation interface with approach/method selection |
|
| 49 |
+
| `GET /evaluate` | `evaluation.html` | Evaluation dashboard with metrics, charts, analysis |
|
| 50 |
+
|
| 51 |
+
All page routes inject `active_page`, `users`, `categories`, `brands`, `approaches` as template variables.
|
| 52 |
+
|
| 53 |
+
### API Routes (JSON)
|
| 54 |
+
|
| 55 |
+
#### Users
|
| 56 |
+
|
| 57 |
+
| Method | Route | Purpose |
|
| 58 |
+
|---|---|---|
|
| 59 |
+
| `GET` | `/api/users` | List all users |
|
| 60 |
+
| `GET` | `/api/user/<id>` | Get single user preferences |
|
| 61 |
+
| `POST` | `/api/users` | Create a new user |
|
| 62 |
+
|
| 63 |
+
**Create User** (`POST /api/users`):
|
| 64 |
+
```python
|
| 65 |
+
new_id = max(user_ids) + 1
|
| 66 |
+
# Creates DataFrame row, concats to global users
|
| 67 |
+
# Appends to USER_OPTIONS for immediate dropdown availability
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
#### Products
|
| 71 |
+
|
| 72 |
+
| Method | Route | Purpose |
|
| 73 |
+
|---|---|---|
|
| 74 |
+
| `GET` | `/api/products?category=X` | Get products (filtered by category) |
|
| 75 |
+
| `GET` | `/api/products/filter?category=X&brand=Y&price_max=Z&q=search` | Filter products by multiple criteria |
|
| 76 |
+
|
| 77 |
+
#### Recommendations
|
| 78 |
+
|
| 79 |
+
| Method | Route | Purpose |
|
| 80 |
+
|---|---|---|
|
| 81 |
+
| `POST` | `/api/recommend` | Generate recommendations |
|
| 82 |
+
|
| 83 |
+
**Request body:**
|
| 84 |
+
```json
|
| 85 |
+
{
|
| 86 |
+
"user_id": 1,
|
| 87 |
+
"approach": "cf",
|
| 88 |
+
"method": "user_based",
|
| 89 |
+
"n": 10
|
| 90 |
+
}
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
**Flow:**
|
| 94 |
+
1. Load user's rated items (`rating >= 3.5`) and preferences
|
| 95 |
+
2. Dispatch to correct recommender based on `approach` + `method`
|
| 96 |
+
3. Generate explanations for each recommendation
|
| 97 |
+
4. Return list with product info, score, and explanation
|
| 98 |
+
|
| 99 |
+
**Response:**
|
| 100 |
+
```json
|
| 101 |
+
{
|
| 102 |
+
"recommendations": [
|
| 103 |
+
{
|
| 104 |
+
"id": 58, "name": "X-T5", "category": "Electronics",
|
| 105 |
+
"price": 999.99, "avg_rating": 4.5,
|
| 106 |
+
"score": 0.92, "explanation": "Users with Electronics taste also liked X-T5"
|
| 107 |
+
}
|
| 108 |
+
]
|
| 109 |
+
}
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
#### Evaluation
|
| 113 |
+
|
| 114 |
+
| Method | Route | Purpose |
|
| 115 |
+
|---|---|---|
|
| 116 |
+
| `GET` | `/api/evaluate/cf/<method>` | Evaluate a single CF method |
|
| 117 |
+
| `GET` | `/api/evaluate/cf/<method>/stream` | Stream epoch/item progress during evaluation |
|
| 118 |
+
| `GET` | `/api/evaluate/approaches` | Compare all 3 approaches |
|
| 119 |
+
|
| 120 |
+
**Streaming endpoint** (`/stream`):
|
| 121 |
+
Returns NDJSON (newline-delimited JSON):
|
| 122 |
+
```
|
| 123 |
+
{"type":"progress","current":47,"total":100} ← SVD epoch 47/100
|
| 124 |
+
{"type":"progress","current":48,"total":100}
|
| 125 |
+
...
|
| 126 |
+
{"type":"phase","label":"Evaluating users..."} ← Training done, now evaluating
|
| 127 |
+
{"type":"result","data":{"RMSE":0.9675,...}} ← Final metrics
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
#### Preferences
|
| 131 |
+
|
| 132 |
+
| Method | Route | Purpose |
|
| 133 |
+
|---|---|---|
|
| 134 |
+
| `PUT` | `/api/user/<id>/preferences` | Update user budget preferences |
|
| 135 |
+
|
| 136 |
+
#### HTMX
|
| 137 |
+
|
| 138 |
+
| Method | Route | Purpose |
|
| 139 |
+
|---|---|---|
|
| 140 |
+
| `POST` | `/htmx/recommend` | Return recommendation HTML fragment (for HTMX-powered requests) |
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## UI Integration
|
| 145 |
+
|
| 146 |
+
The Flask app integrates with a **3-tier frontend**:
|
| 147 |
+
|
| 148 |
+
1. **Alpine.js** — manages interactive state (approach selection, results display, compare view, evaluation progress) via `x-data` components
|
| 149 |
+
2. **HTMX** — provides AJAX navigation patterns (loaded but used minimally)
|
| 150 |
+
3. **Lucide** — SVG icon library (loaded but used for brand icon only)
|
| 151 |
+
|
| 152 |
+
### Key Frontend-Backend Interactions
|
| 153 |
+
|
| 154 |
+
| Feature | Frontend | Backend |
|
| 155 |
+
|---|---|---|
|
| 156 |
+
| User selection | Alpine reads `userData` from template variable | Injects `users` as JSON |
|
| 157 |
+
| Recommendations | Alpine `fetch()` → `POST /api/recommend` | Returns JSON with product list |
|
| 158 |
+
| Compare all | Parallel `fetch()` → multiple `POST /api/recommend` | Each returns recommendations |
|
| 159 |
+
| Evaluation | Incremental `fetch()` → per-method endpoints | SVD/Slope One use streaming |
|
| 160 |
+
| Preferences | Alpine `fetch()` → `PUT /api/user/<id>/preferences` | Updates in-memory DataFrame |
|
| 161 |
+
|
| 162 |
+
---
|
| 163 |
+
|
| 164 |
+
## Error Handling
|
| 165 |
+
|
| 166 |
+
All API endpoints return JSON errors with appropriate HTTP status codes:
|
| 167 |
+
- `400` — Missing or invalid parameters
|
| 168 |
+
- `404` — User/product not found
|
| 169 |
+
- `500` — Internal server error with error message
|
| 170 |
+
|
| 171 |
+
Frontend displays errors via the `toast()` notification system.
|
discussion/10-mathematical-details.md
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Mathematical Foundations of Recommender Systems
|
| 2 |
+
|
| 3 |
+
## Table of Contents
|
| 4 |
+
|
| 5 |
+
1. [Collaborative Filtering](#1-collaborative-filtering)
|
| 6 |
+
- [1.1 User-Based CF](#11-user-based-cf)
|
| 7 |
+
- [1.2 Item-Based CF](#12-item-based-cf)
|
| 8 |
+
- [1.3 SVD (Matrix Factorization)](#13-svd-matrix-factorization)
|
| 9 |
+
- [1.4 KNN-Based CF](#14-knn-based-cf)
|
| 10 |
+
- [1.5 Slope One](#15-slope-one)
|
| 11 |
+
2. [Content-Based Filtering](#2-content-based-filtering)
|
| 12 |
+
- [2.1 TF-IDF Similarity](#21-tf-idf-similarity)
|
| 13 |
+
- [2.2 Feature Matching](#22-feature-matching)
|
| 14 |
+
3. [Knowledge-Based Filtering](#3-knowledge-based-filtering)
|
| 15 |
+
- [3.1 Constraint-Based](#31-constraint-based)
|
| 16 |
+
- [3.2 Rule-Based](#32-rule-based)
|
| 17 |
+
- [3.3 Utility-Based](#33-utility-based)
|
| 18 |
+
4. [Similarity Metrics](#4-similarity-metrics)
|
| 19 |
+
- [4.1 Cosine Similarity](#41-cosine-similarity)
|
| 20 |
+
- [4.2 Pearson Correlation](#42-pearson-correlation)
|
| 21 |
+
- [4.3 Adjusted Cosine Similarity](#43-adjusted-cosine-similarity)
|
| 22 |
+
- [4.4 Jaccard Similarity](#44-jaccard-similarity)
|
| 23 |
+
5. [Evaluation Metrics](#5-evaluation-metrics)
|
| 24 |
+
- [5.1 RMSE](#51-rmse)
|
| 25 |
+
- [5.2 MAE](#52-mae)
|
| 26 |
+
- [5.3 Precision@k](#53-precisionk)
|
| 27 |
+
- [5.4 Recall@k](#54-recallk)
|
| 28 |
+
- [5.5 F1@k](#55-f1k)
|
| 29 |
+
- [5.6 Coverage](#56-coverage)
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
# 1. Collaborative Filtering
|
| 34 |
+
|
| 35 |
+
## Notation
|
| 36 |
+
|
| 37 |
+
Let:
|
| 38 |
+
|
| 39 |
+
- $U = \{1, 2, \dots, n_u\}$ be the set of $n_u$ users
|
| 40 |
+
- $I = \{1, 2, \dots, n_i\}$ be the set of $n_i$ items
|
| 41 |
+
- $\mathbf{R} \in \mathbb{R}^{n_u \times n_i}$ be the user-item rating matrix, where $r_{u,i}$ is the rating of user $u$ for item $i$
|
| 42 |
+
- $\mathbf{R}_{u,i} = \text{NaN}$ if user $u$ has not rated item $i$
|
| 43 |
+
- $\mu$ be the global mean rating across all observed ratings
|
| 44 |
+
- $\mu_u$ be the mean rating of user $u$
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## 1.1 User-Based CF
|
| 49 |
+
|
| 50 |
+
### Concept
|
| 51 |
+
|
| 52 |
+
Predict ratings by aggregating ratings from the **most similar users** (neighbors) to the target user.
|
| 53 |
+
|
| 54 |
+
### Step 1: Compute User Similarity Matrix
|
| 55 |
+
|
| 56 |
+
Let $\mathbf{R}^\text{imp}$ be the imputed rating matrix where all NaN values are replaced by the global mean $\mu$:
|
| 57 |
+
|
| 58 |
+
$$\hat{r}_{u,i} = \begin{cases} r_{u,i} & \text{if } r_{u,i} \text{ is observed} \\ \mu & \text{otherwise} \end{cases}$$
|
| 59 |
+
|
| 60 |
+
Compute the pair-wise cosine similarity matrix $\mathbf{S} \in \mathbb{R}^{n_u \times n_u}$:
|
| 61 |
+
|
| 62 |
+
$$s_{u,v} = \cos(\mathbf{R}^\text{imp}_{u,:}, \mathbf{R}^\text{imp}_{v,:}) = \frac{\mathbf{R}^\text{imp}_{u,:} \cdot \mathbf{R}^\text{imp}_{v,:}}{\|\mathbf{R}^\text{imp}_{u,:}\| \cdot \|\mathbf{R}^\text{imp}_{v,:}\|}$$
|
| 63 |
+
|
| 64 |
+
Where $\mathbf{R}^\text{imp}_{u,:}$ is the row vector of user $u$'s imputed ratings.
|
| 65 |
+
|
| 66 |
+
### Step 2: Predict Rating for Unseen Item
|
| 67 |
+
|
| 68 |
+
For each unseen item $i$ (where $\mathbf{R}_{u,i} = \text{NaN}$):
|
| 69 |
+
|
| 70 |
+
1. Find the $k$ most similar users who have rated item $i$ (similarity $s_{u,v} > 0$)
|
| 71 |
+
2. Let $N_{u,i} = \{v_1, v_2, \dots, v_k\}$ be the set of valid neighbors
|
| 72 |
+
|
| 73 |
+
The predicted rating is the **weighted average**:
|
| 74 |
+
|
| 75 |
+
$$\hat{r}_{u,i} = \frac{\sum_{v \in N_{u,i}} s_{u,v} \cdot r_{v,i}}{\sum_{v \in N_{u,i}} s_{u,v}}$$
|
| 76 |
+
|
| 77 |
+
### Step 3: Generate Top-N Recommendations
|
| 78 |
+
|
| 79 |
+
$$\text{Rec}(u) = \text{top-}n \text{ items with highest } \hat{r}_{u,i} \text{ among unseen items}$$
|
| 80 |
+
|
| 81 |
+
### Complexity
|
| 82 |
+
|
| 83 |
+
- Similarity matrix: $O(n_u^2 \cdot n_i)$
|
| 84 |
+
- Per-user prediction: $O(k \cdot n_i)$
|
| 85 |
+
|
| 86 |
+
### Free parameters
|
| 87 |
+
|
| 88 |
+
- $k = 20$ (number of neighbors)
|
| 89 |
+
- Imputation strategy: global mean
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## 1.2 Item-Based CF
|
| 94 |
+
|
| 95 |
+
### Concept
|
| 96 |
+
|
| 97 |
+
Predict ratings by finding items **similar to items the user already rated highly**. Item similarities are computed using **adjusted cosine similarity** to remove user bias.
|
| 98 |
+
|
| 99 |
+
### Step 1: Compute Item-Adjusted Cosine Similarity
|
| 100 |
+
|
| 101 |
+
For each pair of items $(i, j)$, the adjusted cosine similarity is:
|
| 102 |
+
|
| 103 |
+
$$s_{i,j}^\text{adj} = \frac{\sum_{u \in U_{ij}} (r_{u,i} - \mu_u)(r_{u,j} - \mu_u)}{\sqrt{\sum_{u \in U_{ij}} (r_{u,i} - \mu_u)^2} \sqrt{\sum_{u \in U_{ij}} (r_{u,j} - \mu_u)^2}}$$
|
| 104 |
+
|
| 105 |
+
Where:
|
| 106 |
+
- $U_{ij}$ is the set of users who rated both items $i$ and $j$
|
| 107 |
+
- $\mu_u$ is user $u$'s mean rating
|
| 108 |
+
|
| 109 |
+
This normalizes each user's ratings by their mean, removing the bias of users who rate everything high or low.
|
| 110 |
+
|
| 111 |
+
### Step 2: Predict Rating
|
| 112 |
+
|
| 113 |
+
Let $I_u^+ = \{i \in I : r_{u,i} \text{ is observed}\}$ be the set of items user $u$ has rated.
|
| 114 |
+
|
| 115 |
+
For each unseen item $i$, find the top $k$ most similar items among $I_u^+$:
|
| 116 |
+
|
| 117 |
+
$$N_{i,u} = \text{top-}k \left\{ j \in I_u^+ : s_{i,j}^\text{adj} > 0 \right\}$$
|
| 118 |
+
|
| 119 |
+
$$\hat{r}_{u,i} = \frac{\sum_{j \in N_{i,u}} s_{i,j}^\text{adj} \cdot r_{u,j}}{\sum_{j \in N_{i,u}} s_{i,j}^\text{adj}}$$
|
| 120 |
+
|
| 121 |
+
### Step 3: Generate Top-N Recommendations
|
| 122 |
+
|
| 123 |
+
Same as User-Based CF — sort by predicted rating.
|
| 124 |
+
|
| 125 |
+
### Complexity
|
| 126 |
+
|
| 127 |
+
- Item similarity matrix: $O(n_i^2 \cdot n_u)$ (precomputed once)
|
| 128 |
+
- Per-user prediction: $O(n_i \cdot |I_u^+|)$
|
| 129 |
+
|
| 130 |
+
### Free parameters
|
| 131 |
+
|
| 132 |
+
- $k = 15$ (number of item neighbors)
|
| 133 |
+
- Similarity metric: adjusted cosine
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## 1.3 SVD (Matrix Factorization)
|
| 138 |
+
|
| 139 |
+
### Concept
|
| 140 |
+
|
| 141 |
+
Decompose the user-item rating matrix into two low-rank factor matrices that capture **latent features** (e.g., genre affinity, price sensitivity, brand loyalty).
|
| 142 |
+
|
| 143 |
+
### Problem Statement
|
| 144 |
+
|
| 145 |
+
Approximate the rating matrix $\mathbf{R}$ as a product of two lower-dimensional matrices:
|
| 146 |
+
|
| 147 |
+
$$\mathbf{R} \approx \mathbf{P} \cdot \mathbf{Q}^T$$
|
| 148 |
+
|
| 149 |
+
Where:
|
| 150 |
+
- $\mathbf{P} \in \mathbb{R}^{n_u \times f}$ — user factor matrix (each row = user's latent preferences)
|
| 151 |
+
- $\mathbf{Q} \in \mathbb{R}^{n_i \times f}$ — item factor matrix (each row = item's latent attributes)
|
| 152 |
+
- $f$ = number of latent factors (typically 20-100)
|
| 153 |
+
|
| 154 |
+
### Bias-Enhanced Prediction
|
| 155 |
+
|
| 156 |
+
The predicted rating includes user/item bias terms:
|
| 157 |
+
|
| 158 |
+
$$\hat{r}_{u,i} = \mu + b_u + b_i + \mathbf{p}_u \cdot \mathbf{q}_i^T$$
|
| 159 |
+
|
| 160 |
+
Where:
|
| 161 |
+
- $\mu$ = global mean rating
|
| 162 |
+
- $b_u$ = bias of user $u$ (tendency to rate higher/lower than average)
|
| 163 |
+
- $b_i$ = bias of item $i$ (tendency to be rated higher/lower than average)
|
| 164 |
+
- $\mathbf{p}_u$ = user $u$'s latent factor vector (row of $\mathbf{P}$)
|
| 165 |
+
- $\mathbf{q}_i$ = item $i$'s latent factor vector (row of $\mathbf{Q}$)
|
| 166 |
+
|
| 167 |
+
### Objective Function (to minimize)
|
| 168 |
+
|
| 169 |
+
$$L = \sum_{(u,i) \in \mathcal{K}} \left( r_{u,i} - \hat{r}_{u,i} \right)^2 + \lambda \left( b_u^2 + b_i^2 + \|\mathbf{p}_u\|^2 + \|\mathbf{q}_i\|^2 \right)$$
|
| 170 |
+
|
| 171 |
+
Where:
|
| 172 |
+
- $\mathcal{K}$ = set of all observed ratings
|
| 173 |
+
- $\lambda$ = regularization parameter (prevents overfitting)
|
| 174 |
+
|
| 175 |
+
### Optimization — Stochastic Gradient Descent
|
| 176 |
+
|
| 177 |
+
For each observed rating $(u, i)$, compute the error:
|
| 178 |
+
|
| 179 |
+
$$e_{u,i} = r_{u,i} - \hat{r}_{u,i}$$
|
| 180 |
+
|
| 181 |
+
Update parameters by moving in the direction that reduces error:
|
| 182 |
+
|
| 183 |
+
$$
|
| 184 |
+
\begin{aligned}
|
| 185 |
+
b_u &\leftarrow b_u + \eta \cdot (e_{u,i} - \lambda \cdot b_u) \\
|
| 186 |
+
b_i &\leftarrow b_i + \eta \cdot (e_{u,i} - \lambda \cdot b_i) \\
|
| 187 |
+
\mathbf{p}_u &\leftarrow \mathbf{p}_u + \eta \cdot (e_{u,i} \cdot \mathbf{q}_i - \lambda \cdot \mathbf{p}_u) \\
|
| 188 |
+
\mathbf{q}_i &\leftarrow \mathbf{q}_i + \eta \cdot (e_{u,i} \cdot \mathbf{p}_u - \lambda \cdot \mathbf{q}_i)
|
| 189 |
+
\end{aligned}
|
| 190 |
+
$$
|
| 191 |
+
|
| 192 |
+
Where $\eta$ is the learning rate.
|
| 193 |
+
|
| 194 |
+
### Algorithm
|
| 195 |
+
|
| 196 |
+
```
|
| 197 |
+
1. Initialize P, Q with small random values N(0, 0.1)
|
| 198 |
+
2. Initialize bu, bi = 0
|
| 199 |
+
3. For epoch = 1 to n_epochs:
|
| 200 |
+
a. Shuffle observed rating pairs
|
| 201 |
+
b. For each (u, i) in observed:
|
| 202 |
+
- Compute prediction error e_ui = r_ui - (mu + bu[u] + bi[i] + P[u]·Q[i])
|
| 203 |
+
- Update bu[u], bi[i], P[u], Q[i]
|
| 204 |
+
4. Return (P, Q, bu, bi) — cached for future predictions
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
### Prediction (after training)
|
| 208 |
+
|
| 209 |
+
For an unseen item $i$ for user $u$:
|
| 210 |
+
|
| 211 |
+
$$\hat{r}_{u,i} = \mu + b_u + b_i + \mathbf{p}_u \cdot \mathbf{q}_i^T$$
|
| 212 |
+
|
| 213 |
+
### Caching Strategy
|
| 214 |
+
|
| 215 |
+
The trained factors $(P, Q, b_u, b_i)$ are cached. The first call triggers 100 epochs of training. All subsequent calls use the cached factors — prediction is $O(n_i \cdot f)$ per user.
|
| 216 |
+
|
| 217 |
+
### Hyperparameters
|
| 218 |
+
|
| 219 |
+
| Parameter | Value | Effect |
|
| 220 |
+
|---|---|---|
|
| 221 |
+
| $f$ (factors) | 20 | Higher = more expressive, risk of overfitting |
|
| 222 |
+
| $n_\text{epochs}$ | 100 | Higher = better convergence |
|
| 223 |
+
| $\eta$ (lr) | 0.01 | Higher = faster convergence, may overshoot |
|
| 224 |
+
| $\lambda$ (reg) | 0.02 | Higher = stronger regularization |
|
| 225 |
+
|
| 226 |
+
---
|
| 227 |
+
|
| 228 |
+
## 1.4 KNN-Based CF
|
| 229 |
+
|
| 230 |
+
### Concept
|
| 231 |
+
|
| 232 |
+
A variant of User-Based CF that uses **sklearn's optimized NearestNeighbors** to efficiently find similar users, then weights their ratings by inverse distance.
|
| 233 |
+
|
| 234 |
+
### Step 1: Find Nearest Neighbors
|
| 235 |
+
|
| 236 |
+
Let $\mathbf{R}^\text{imp}$ be the mean-imputed matrix (same as User-Based CF).
|
| 237 |
+
|
| 238 |
+
For target user $u$, find $k+1$ nearest neighbors using cosine distance:
|
| 239 |
+
|
| 240 |
+
$$\text{dist}(\mathbf{R}^\text{imp}_{u,:}, \mathbf{R}^\text{imp}_{v,:}) = 1 - \cos(\mathbf{R}^\text{imp}_{u,:}, \mathbf{R}^\text{imp}_{v,:})$$
|
| 241 |
+
|
| 242 |
+
The nearest neighbor algorithm returns:
|
| 243 |
+
- $\text{indices}[0] = [u, v_1, v_2, \dots, v_k]$ — indice 0 is the user themselves
|
| 244 |
+
- $\text{distances}[0] = [0, d_1, d_2, \dots, d_k]$ — distances to each neighbor
|
| 245 |
+
|
| 246 |
+
Neighbors = $\text{indices}[0][1:]$ (exclude self).
|
| 247 |
+
|
| 248 |
+
### Step 2: Predict Rating
|
| 249 |
+
|
| 250 |
+
For each unseen item $i$:
|
| 251 |
+
|
| 252 |
+
$$\hat{r}_{u,i} = \frac{\sum_{v \in N_u} w_v \cdot r_{v,i}}{\sum_{v \in N_u} w_v}$$
|
| 253 |
+
|
| 254 |
+
Where weights are **inverse distances** (closer = higher weight):
|
| 255 |
+
|
| 256 |
+
$$w_v = \frac{1}{d_v + \epsilon}, \quad \epsilon = 10^{-6}$$
|
| 257 |
+
|
| 258 |
+
Only neighbors who rated item $i$ contribute ($r_{v,i} \neq \text{NaN}$).
|
| 259 |
+
|
| 260 |
+
### Complexity
|
| 261 |
+
|
| 262 |
+
- KNN index building: $O(n_u \cdot f_\text{tree})$ via sklearn's KD-tree/ball-tree
|
| 263 |
+
- Per-user neighbor search: $O(\log n_u)$
|
| 264 |
+
- Per-user prediction: $O(k \cdot n_i)$
|
| 265 |
+
|
| 266 |
+
### Free parameters
|
| 267 |
+
|
| 268 |
+
- $k = 10$ (number of neighbors)
|
| 269 |
+
- Metric: cosine distance
|
| 270 |
+
- Weighting: inverse distance
|
| 271 |
+
|
| 272 |
+
### Key Difference from User-Based CF
|
| 273 |
+
|
| 274 |
+
User-Based CF computes all pair-wise similarities explicitly (dense $n_u \times n_u$ matrix). KNN-CF uses a tree structure for approximate nearest neighbor search, which is **more efficient for large user bases** but slightly less precise.
|
| 275 |
+
|
| 276 |
+
---
|
| 277 |
+
|
| 278 |
+
## 1.5 Slope One
|
| 279 |
+
|
| 280 |
+
### Concept
|
| 281 |
+
|
| 282 |
+
A simple yet effective collaborative filtering algorithm based on the **average rating difference** between items. "If on average item A is rated 0.5 stars higher than item B, and you rate item B 4.0, you'll likely rate item A 4.5."
|
| 283 |
+
|
| 284 |
+
### Step 1: Precompute Deviation Matrix
|
| 285 |
+
|
| 286 |
+
For every pair of items $(i, j)$, compute:
|
| 287 |
+
|
| 288 |
+
$$d_{i,j} = \frac{\sum_{u \in U_{ij}} (r_{u,i} - r_{u,j})}{|U_{ij}|}$$
|
| 289 |
+
|
| 290 |
+
$$c_{i,j} = |U_{ij}|$$
|
| 291 |
+
|
| 292 |
+
Where:
|
| 293 |
+
- $U_{ij}$ = set of users who rated both items $i$ and $j$
|
| 294 |
+
- $d_{i,j}$ = average deviation (how much higher/lower item $i$ is rated compared to item $j$)
|
| 295 |
+
- $c_{i,j}$ = number of users who contributed to this deviation (confidence)
|
| 296 |
+
|
| 297 |
+
This produces two $n_i \times n_i$ matrices: $\mathbf{D}$ (deviations) and $\mathbf{C}$ (counts).
|
| 298 |
+
|
| 299 |
+
### Step 2: Predict Rating
|
| 300 |
+
|
| 301 |
+
For user $u$ and unseen item $i$:
|
| 302 |
+
|
| 303 |
+
Let $I_u^\text{rated} = \{j : r_{u,j} \text{ is observed}\}$.
|
| 304 |
+
|
| 305 |
+
$$\hat{r}_{u,i} = \frac{\sum_{j \in I_u^\text{rated}} (r_{u,j} + d_{i,j}) \cdot \mathbf{1}[c_{i,j} > 0]}{\sum_{j \in I_u^\text{rated}} \mathbf{1}[c_{i,j} > 0]}$$
|
| 306 |
+
|
| 307 |
+
Where $\mathbf{1}[c_{i,j} > 0]$ is an indicator function that is 1 when the deviation is well-defined (at least one user rated both items).
|
| 308 |
+
|
| 309 |
+
Only items $j$ with $c_{i,j} > 0$ contribute to the prediction.
|
| 310 |
+
|
| 311 |
+
### Complexity
|
| 312 |
+
|
| 313 |
+
- Precomputation: $O(n_i^2 \cdot n_u)$ (done once, cached)
|
| 314 |
+
- Per-user prediction: $O(n_i \cdot |I_u^\text{rated}|)$
|
| 315 |
+
|
| 316 |
+
### Key Properties
|
| 317 |
+
|
| 318 |
+
- **No hyperparameters** — fully deterministic
|
| 319 |
+
- **Incremental** — new ratings can be incorporated by updating $d_{i,j}$ and $c_{i,j}$ without retraining
|
| 320 |
+
- **Interpretable** — each prediction is the average of multiple "if you liked X, you'd probably rate Y this much" estimates
|
| 321 |
+
|
| 322 |
+
---
|
| 323 |
+
|
| 324 |
+
# 2. Content-Based Filtering
|
| 325 |
+
|
| 326 |
+
## 2.1 TF-IDF Similarity
|
| 327 |
+
|
| 328 |
+
### Concept
|
| 329 |
+
|
| 330 |
+
Build a **text profile** for each product from its attributes (category, subcategory, brand, name), then recommend products whose TF-IDF vectors are most similar to the user's profile (average of items they liked).
|
| 331 |
+
|
| 332 |
+
### Step 1: Build Text Features
|
| 333 |
+
|
| 334 |
+
For each product $p$, construct a text document:
|
| 335 |
+
|
| 336 |
+
$$\text{text}_p = \text{category}_p \oplus \text{subcategory}_p \oplus \text{brand}_p \oplus \text{name}_p$$
|
| 337 |
+
|
| 338 |
+
### Step 2: Compute TF-IDF Matrix
|
| 339 |
+
|
| 340 |
+
The **Term Frequency — Inverse Document Frequency** (TF-IDF) weight for term $t$ in product $p$:
|
| 341 |
+
|
| 342 |
+
$$\text{tf-idf}_{t,p} = \text{tf}_{t,p} \times \text{idf}_t$$
|
| 343 |
+
|
| 344 |
+
Where:
|
| 345 |
+
- $\text{tf}_{t,p}$ = raw count of term $t$ in product $p$'s text (normalized by document length)
|
| 346 |
+
- $\text{idf}_t = \log\left(\frac{n_i}{\text{df}_t}\right) + 1$ (where $\text{df}_t$ = number of products containing term $t$)
|
| 347 |
+
|
| 348 |
+
This produces a matrix $\mathbf{T} \in \mathbb{R}^{n_i \times m}$ where $m$ is the vocabulary size (number of unique words across all product texts).
|
| 349 |
+
|
| 350 |
+
### Step 3: Build User Profile
|
| 351 |
+
|
| 352 |
+
Let $I_u^+ = \{p_1, p_2, \dots, p_k\}$ be the set of products user $u$ rated $\geq 3.5$.
|
| 353 |
+
|
| 354 |
+
The user's content profile is the **centroid** (average) of their liked products' TF-IDF vectors:
|
| 355 |
+
|
| 356 |
+
$$\mathbf{c}_u = \frac{1}{|I_u^+|} \sum_{p \in I_u^+} \mathbf{T}_{p,:}$$
|
| 357 |
+
|
| 358 |
+
### Step 4: Score Products
|
| 359 |
+
|
| 360 |
+
For each product $q \notin I_u^+$, compute the cosine similarity between the user profile and the product's TF-IDF vector:
|
| 361 |
+
|
| 362 |
+
$$\text{score}_{u,q} = \cos(\mathbf{c}_u, \mathbf{T}_{q,:}) = \frac{\mathbf{c}_u \cdot \mathbf{T}_{q,:}}{\|\mathbf{c}_u\| \cdot \|\mathbf{T}_{q,:}\|}$$
|
| 363 |
+
|
| 364 |
+
### Step 5: Generate Recommendations
|
| 365 |
+
|
| 366 |
+
$$\text{Rec}(u) = \text{top-}n \text{ products with highest } \text{score}_{u,q} \text{ among products not in } I_u^+$$
|
| 367 |
+
|
| 368 |
+
### Complexity
|
| 369 |
+
|
| 370 |
+
- Vectorization: $O(n_i \cdot m)$
|
| 371 |
+
- Profile building: $O(|I_u^+| \cdot m)$
|
| 372 |
+
- Scoring: $O(n_i \cdot m)$
|
| 373 |
+
|
| 374 |
+
---
|
| 375 |
+
|
| 376 |
+
## 2.2 Feature Matching
|
| 377 |
+
|
| 378 |
+
### Concept
|
| 379 |
+
|
| 380 |
+
Score products based on explicit preference matches without requiring rating history. A **rule-based scoring** function assigns points for each attribute match.
|
| 381 |
+
|
| 382 |
+
### Scoring Function
|
| 383 |
+
|
| 384 |
+
For each product $p$, the total score is:
|
| 385 |
+
|
| 386 |
+
$$S_p = w_{\text{cat}} \cdot \mathbf{1}[\text{cat}_p \in \text{pref}_\text{cat}] + w_{\text{brand}} \cdot \mathbf{1}[\text{brand}_p \in \text{pref}_\text{brand}] + w_{\text{budget}} \cdot \mathbf{1}[\text{budget}_{\min} \leq \text{price}_p \leq \text{budget}_{\max}] + w_{\text{sub}} \cdot \mathbf{1}[\text{subcat}_p \in \text{pref}_\text{subcat}]$$
|
| 387 |
+
|
| 388 |
+
Where:
|
| 389 |
+
|
| 390 |
+
| Component | Weight | Condition |
|
| 391 |
+
|---|---|---|
|
| 392 |
+
| Category match | $w_{\text{cat}} = 40$ | Product category $\in$ user's preferred categories |
|
| 393 |
+
| Brand match | $w_{\text{brand}} = 30$ | Product brand $\in$ user's favorite brands |
|
| 394 |
+
| Budget fit | $w_{\text{budget}} = 20$ | Product price within user's budget range |
|
| 395 |
+
| Subcategory match | $w_{\text{sub}} = 10$ | Product subcategory matches (complex condition) |
|
| 396 |
+
|
| 397 |
+
### Complexity
|
| 398 |
+
|
| 399 |
+
$O(n_i)$ — a single pass over the product catalog.
|
| 400 |
+
|
| 401 |
+
### Cold-Start Advantage
|
| 402 |
+
|
| 403 |
+
Unlike TF-IDF, Feature Matching requires **zero rating history**. It only needs preference data (categories, brands, budget), making it suitable for new users.
|
| 404 |
+
|
| 405 |
+
---
|
| 406 |
+
|
| 407 |
+
# 3. Knowledge-Based Filtering
|
| 408 |
+
|
| 409 |
+
## 3.1 Constraint-Based
|
| 410 |
+
|
| 411 |
+
### Concept
|
| 412 |
+
|
| 413 |
+
Sequentially filter the product catalog by hard constraints, then sort by rating.
|
| 414 |
+
|
| 415 |
+
### Filtering Pipeline
|
| 416 |
+
|
| 417 |
+
Starting with the full product set $\mathcal{P}_0 = \mathcal{P}$:
|
| 418 |
+
|
| 419 |
+
| Step | Filter | Result |
|
| 420 |
+
|---|---|---|
|
| 421 |
+
| 1 | Budget max | $\mathcal{P}_1 = \{p \in \mathcal{P}_0 : \text{price}_p \leq B_{\max}\}$ |
|
| 422 |
+
| 2 | Budget min | $\mathcal{P}_2 = \{p \in \mathcal{P}_1 : \text{price}_p \geq B_{\min}\}$ |
|
| 423 |
+
| 3 | Category | $\mathcal{P}_3 = \{p \in \mathcal{P}_2 : \text{cat}_p \in C_{\text{pref}}\}$ (if $C_{\text{pref}}$ non-empty) |
|
| 424 |
+
| 4 | Brand | $\mathcal{P}_4 = \{p \in \mathcal{P}_3 : \text{brand}_p \in B_{\text{pref}}\}$ (only if match exists) |
|
| 425 |
+
| 5 | Min rating | $\mathcal{P}_5 = \{p \in \mathcal{P}_4 : \text{rating}_p \geq R_{\min}\}$ |
|
| 426 |
+
|
| 427 |
+
### Safety Mechanism
|
| 428 |
+
|
| 429 |
+
Steps 4 and 5 use a **soft filter**: the filter only activates if there's at least one matching product. Otherwise, the constraint is skipped to prevent empty results.
|
| 430 |
+
|
| 431 |
+
### Ranking
|
| 432 |
+
|
| 433 |
+
$$\text{Rec}(u) = \text{top-}n \text{ products from } \mathcal{P}_5 \text{ sorted by } \text{avg\_rating}_p \text{ descending}$$
|
| 434 |
+
|
| 435 |
+
### Complexity
|
| 436 |
+
|
| 437 |
+
$O(n_i)$ — each filter is a linear pass.
|
| 438 |
+
|
| 439 |
+
---
|
| 440 |
+
|
| 441 |
+
## 3.2 Rule-Based
|
| 442 |
+
|
| 443 |
+
### Concept
|
| 444 |
+
|
| 445 |
+
Score products using domain-specific **cross-selling rules** combined with preference signals.
|
| 446 |
+
|
| 447 |
+
### Cross-Selling Rules
|
| 448 |
+
|
| 449 |
+
The system defines rules mapping interacted categories to accessory recommendations:
|
| 450 |
+
|
| 451 |
+
$$\mathcal{R} = \{(c_{\text{base}}, \{a_1, a_2, \dots\}) : \text{ buying } c_{\text{base}} \text{ triggers recommending accessories}\}$$
|
| 452 |
+
|
| 453 |
+
Example: `Laptops → {Laptop Bag, Mouse, Keyboard, Cooling Pad}`
|
| 454 |
+
|
| 455 |
+
### Scoring Function
|
| 456 |
+
|
| 457 |
+
For product $p$ with category $\text{cat}_p$, subcategory $\text{sub}_p$, brand $\text{brand}_p$, and price $\text{price}_p$:
|
| 458 |
+
|
| 459 |
+
$$
|
| 460 |
+
S_p =
|
| 461 |
+
\begin{aligned}
|
| 462 |
+
& w_{\text{cross}} \cdot \mathbf{1}[\text{sub}_p \in \text{accessories}(\text{interacted\_cat})] \\
|
| 463 |
+
&+ w_{\text{cat}} \cdot \mathbf{1}[\text{cat}_p \in \text{pref}_\text{cat}] \\
|
| 464 |
+
&+ w_{\text{budget}} \cdot \mathbf{1}[\text{budget}_{\min} \leq \text{price}_p \leq \text{budget}_{\max}] \\
|
| 465 |
+
&+ w_{\text{brand}} \cdot \mathbf{1}[\text{brand}_p \in \text{pref}_\text{brand}]
|
| 466 |
+
\end{aligned}
|
| 467 |
+
$$
|
| 468 |
+
|
| 469 |
+
With weights:
|
| 470 |
+
- $w_{\text{cross}} = 50$ — strongest signal (direct cross-sell)
|
| 471 |
+
- $w_{\text{cat}} = 20$ — category preference
|
| 472 |
+
- $w_{\text{budget}} = 15$ — budget fit
|
| 473 |
+
- $w_{\text{brand}} = 10$ — brand loyalty
|
| 474 |
+
|
| 475 |
+
### Complexity
|
| 476 |
+
|
| 477 |
+
$O(n_i)$ — scores every product once.
|
| 478 |
+
|
| 479 |
+
---
|
| 480 |
+
|
| 481 |
+
## 3.3 Utility-Based
|
| 482 |
+
|
| 483 |
+
### Concept
|
| 484 |
+
|
| 485 |
+
Compute a **weighted multi-attribute utility** for each product, measuring overall satisfaction across four dimensions.
|
| 486 |
+
|
| 487 |
+
### Utility Function
|
| 488 |
+
|
| 489 |
+
$$U(p) = w_{\text{price}} \cdot u_{\text{price}}(p) + w_{\text{cat}} \cdot u_{\text{cat}}(p) + w_{\text{brand}} \cdot u_{\text{brand}}(p) + w_{\text{rating}} \cdot u_{\text{rating}}(p)$$
|
| 490 |
+
|
| 491 |
+
With default weights $\mathbf{w} = [0.2, 0.3, 0.2, 0.3]$ (summing to 1.0).
|
| 492 |
+
|
| 493 |
+
### Component Utility Functions
|
| 494 |
+
|
| 495 |
+
**Price utility** — Measures how close the price is to the center of the user's budget range:
|
| 496 |
+
|
| 497 |
+
$$u_{\text{price}}(p) = 1 - \frac{|\text{price}_p - \frac{B_{\min} + B_{\max}}{2}|}{\frac{B_{\max} - B_{\min}}{2}}$$
|
| 498 |
+
|
| 499 |
+
Clipped to $[0, 1]$. Scores 1.0 at the budget midpoint, 0.0 at the edges.
|
| 500 |
+
|
| 501 |
+
**Category utility** — Binary:
|
| 502 |
+
|
| 503 |
+
$$u_{\text{cat}}(p) = \begin{cases} 1 & \text{if } \text{cat}_p \in \text{pref}_\text{cat} \\ 0 & \text{otherwise} \end{cases}$$
|
| 504 |
+
|
| 505 |
+
**Brand utility** — Binary:
|
| 506 |
+
|
| 507 |
+
$$u_{\text{brand}}(p) = \begin{cases} 1 & \text{if } \text{brand}_p \in \text{pref}_\text{brand} \\ 0 & \text{otherwise} \end{cases}$$
|
| 508 |
+
|
| 509 |
+
**Rating utility** — Normalized product quality:
|
| 510 |
+
|
| 511 |
+
$$u_{\text{rating}}(p) = \frac{\text{avg\_rating}_p}{\max(\text{avg\_rating})}$$
|
| 512 |
+
|
| 513 |
+
### Final Score
|
| 514 |
+
|
| 515 |
+
$$U(p) \in [0, 1] \text{ for each product}$$
|
| 516 |
+
|
| 517 |
+
$$\text{Rec}(u) = \text{top-}n \text{ products with highest } U(p)$$
|
| 518 |
+
|
| 519 |
+
### Complexity
|
| 520 |
+
|
| 521 |
+
$O(n_i)$ — computes utility for every product.
|
| 522 |
+
|
| 523 |
+
### Advantages Over Constraint-Based
|
| 524 |
+
|
| 525 |
+
Unlike hard filtering (Constraint-Based), Utility-Based uses **soft scoring** — a product that slightly exceeds the budget but is in a preferred category and has high ratings can still rank highly. This produces more diverse recommendations.
|
| 526 |
+
|
| 527 |
+
---
|
| 528 |
+
|
| 529 |
+
# 4. Similarity Metrics
|
| 530 |
+
|
| 531 |
+
## 4.1 Cosine Similarity
|
| 532 |
+
|
| 533 |
+
### Definition
|
| 534 |
+
|
| 535 |
+
For two vectors $\mathbf{a}, \mathbf{b} \in \mathbb{R}^n$:
|
| 536 |
+
|
| 537 |
+
$$\cos(\mathbf{a}, \mathbf{b}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \cdot \|\mathbf{b}\|} = \frac{\sum_{i=1}^n a_i b_i}{\sqrt{\sum_{i=1}^n a_i^2} \sqrt{\sum_{i=1}^n b_i^2}}$$
|
| 538 |
+
|
| 539 |
+
### Properties
|
| 540 |
+
|
| 541 |
+
- Range: $[-1, 1]$ (for non-negative rating vectors: $[0, 1]$)
|
| 542 |
+
- Measures the **angle** between vectors, ignoring magnitude
|
| 543 |
+
- Two users who rate things proportionally (Alice: [4, 4, 4], Bob: [5, 5, 5]) have $\cos = 1$
|
| 544 |
+
|
| 545 |
+
### Used For
|
| 546 |
+
- User-Based CF: user-user similarity
|
| 547 |
+
- Content-Based: user profile vs product vector similarity
|
| 548 |
+
|
| 549 |
+
---
|
| 550 |
+
|
| 551 |
+
## 4.2 Pearson Correlation
|
| 552 |
+
|
| 553 |
+
### Definition
|
| 554 |
+
|
| 555 |
+
Measures linear correlation between two vectors by first **centering** each by its mean:
|
| 556 |
+
|
| 557 |
+
$$\text{pearson}(\mathbf{a}, \mathbf{b}) = \frac{\sum_{i=1}^n (a_i - \bar{a})(b_i - \bar{b})}{\sqrt{\sum_{i=1}^n (a_i - \bar{a})^2} \sqrt{\sum_{i=1}^n (b_i - \bar{b})^2}}$$
|
| 558 |
+
|
| 559 |
+
### Relationship to Cosine
|
| 560 |
+
|
| 561 |
+
Pearson is equivalent to cosine similarity on **mean-centered** vectors:
|
| 562 |
+
|
| 563 |
+
$$\text{pearson}(\mathbf{a}, \mathbf{b}) = \cos(\mathbf{a} - \bar{a}, \mathbf{b} - \bar{b})$$
|
| 564 |
+
|
| 565 |
+
### Why It Matters
|
| 566 |
+
|
| 567 |
+
Removes user bias: Alice rates everything 4-5, Bob rates 2-3, but they agree on relative preferences → Pearson captures this.
|
| 568 |
+
|
| 569 |
+
---
|
| 570 |
+
|
| 571 |
+
## 4.3 Adjusted Cosine Similarity
|
| 572 |
+
|
| 573 |
+
### Definition
|
| 574 |
+
|
| 575 |
+
Applied to **item-item** similarity. For items $i$ and $j$:
|
| 576 |
+
|
| 577 |
+
$$\text{adj\_cos}(i, j) = \frac{\sum_{u \in U_{ij}} (r_{u,i} - \mu_u)(r_{u,j} - \mu_u)}{\sqrt{\sum_{u \in U_{ij}} (r_{u,i} - \mu_u)^2} \sqrt{\sum_{u \in U_{ij}} (r_{u,j} - \mu_u)^2}}$$
|
| 578 |
+
|
| 579 |
+
Where:
|
| 580 |
+
- $U_{ij}$ = users who rated both items
|
| 581 |
+
- $\mu_u$ = user $u$'s mean rating
|
| 582 |
+
|
| 583 |
+
### Key Insight
|
| 584 |
+
|
| 585 |
+
This is identical to the Pearson formula but applied **column-wise** (item space) instead of row-wise (user space). The centering by user mean removes the same user-bias issue.
|
| 586 |
+
|
| 587 |
+
### Implementation
|
| 588 |
+
|
| 589 |
+
In code, this is computed by:
|
| 590 |
+
1. Centering each user's ratings: $\mathbf{R}^\text{centered}_{u,i} = r_{u,i} - \mu_u$ (NaN if unrated)
|
| 591 |
+
2. Imputing NaN with 0
|
| 592 |
+
3. Computing cosine similarity on the **transposed** matrix: $\text{sklearn\_cosine}((\mathbf{R}^\text{centered})^T)$
|
| 593 |
+
|
| 594 |
+
---
|
| 595 |
+
|
| 596 |
+
## 4.4 Jaccard Similarity
|
| 597 |
+
|
| 598 |
+
### Definition
|
| 599 |
+
|
| 600 |
+
For two sets $A$ and $B$:
|
| 601 |
+
|
| 602 |
+
$$J(A, B) = \frac{|A \cap B|}{|A \cup B|}$$
|
| 603 |
+
|
| 604 |
+
### Example
|
| 605 |
+
|
| 606 |
+
$A = \{\text{Electronics, Sports}\}$, $B = \{\text{Electronics, Books}\}$
|
| 607 |
+
$$J(A, B) = \frac{1}{3} \approx 0.33$$
|
| 608 |
+
|
| 609 |
+
### Why Not Used Here
|
| 610 |
+
|
| 611 |
+
Jaccard is designed for **binary data** (purchased/not purchased). Rating data is continuous (1-5 stars). Using Jaccard would discard valuable rating magnitude information.
|
| 612 |
+
|
| 613 |
+
---
|
| 614 |
+
|
| 615 |
+
# 5. Evaluation Metrics
|
| 616 |
+
|
| 617 |
+
## Notation
|
| 618 |
+
|
| 619 |
+
For evaluation, define:
|
| 620 |
+
|
| 621 |
+
- $\mathcal{Y}_\text{true}$ = set of actual ratings in test set
|
| 622 |
+
- $\mathcal{Y}_\text{pred}$ = set of corresponding predicted ratings
|
| 623 |
+
- $\text{Rec}_k(u)$ = top-$k$ recommended items for user $u$
|
| 624 |
+
- $\text{Rel}(u)$ = relevant items for user $u$ (rated $\geq 3.5$ in test set)
|
| 625 |
+
- $\mathcal{U}_\text{test}$ = test set users
|
| 626 |
+
|
| 627 |
+
---
|
| 628 |
+
|
| 629 |
+
## 5.1 RMSE
|
| 630 |
+
|
| 631 |
+
Root Mean Square Error:
|
| 632 |
+
|
| 633 |
+
$$\text{RMSE} = \sqrt{\frac{1}{n} \sum_{(u,i) \in \text{test}} (r_{u,i} - \hat{r}_{u,i})^2}$$
|
| 634 |
+
|
| 635 |
+
### Properties
|
| 636 |
+
- **Scale:** Same units as ratings (0-5 for a 5-star system)
|
| 637 |
+
- **Interpretation:** Average prediction error, **larger errors penalized quadratically**
|
| 638 |
+
- **Range:** $[0, 5]$ (for 1-5 ratings)
|
| 639 |
+
- **Lower is better**
|
| 640 |
+
|
| 641 |
+
### Why Square Errors?
|
| 642 |
+
|
| 643 |
+
An error of 2.0 contributes 4× as much as an error of 1.0 ($2^2 = 4, 1^2 = 1$). This heavily penalizes large mistakes — important because a recommendation with a wildly wrong predicted rating is worse than one slightly off.
|
| 644 |
+
|
| 645 |
+
---
|
| 646 |
+
|
| 647 |
+
## 5.2 MAE
|
| 648 |
+
|
| 649 |
+
Mean Absolute Error:
|
| 650 |
+
|
| 651 |
+
$$\text{MAE} = \frac{1}{n} \sum_{(u,i) \in \text{test}} |r_{u,i} - \hat{r}_{u,i}|$$
|
| 652 |
+
|
| 653 |
+
### Properties
|
| 654 |
+
- **Scale:** Same units as ratings
|
| 655 |
+
- **Interpretation:** Average absolute prediction error, **all errors weighted equally**
|
| 656 |
+
- **Range:** $[0, 5]$
|
| 657 |
+
- **Lower is better**
|
| 658 |
+
|
| 659 |
+
### RMSE vs MAE
|
| 660 |
+
|
| 661 |
+
If $\text{RMSE} \approx \text{MAE}$, prediction errors are evenly distributed. If $\text{RMSE} > \text{MAE}$ significantly, the model occasionally makes very large errors (outliers).
|
| 662 |
+
|
| 663 |
+
---
|
| 664 |
+
|
| 665 |
+
## 5.3 Precision@k
|
| 666 |
+
|
| 667 |
+
$$\text{Precision@k} = \frac{1}{|\mathcal{U}_\text{test}|} \sum_{u \in \mathcal{U}_\text{test}} \frac{|\text{Rec}_k(u) \cap \text{Rel}(u)|}{k}$$
|
| 668 |
+
|
| 669 |
+
### Properties
|
| 670 |
+
- **Range:** $[0, 1]$
|
| 671 |
+
- **Interpretation:** Of the top-$k$ recommendations, what fraction are relevant?
|
| 672 |
+
- **Higher is better**
|
| 673 |
+
|
| 674 |
+
### Example
|
| 675 |
+
|
| 676 |
+
If a user's top-5 recommendations contain 2 relevant items: $\text{Precision@5} = 2/5 = 0.4$
|
| 677 |
+
|
| 678 |
+
---
|
| 679 |
+
|
| 680 |
+
## 5.4 Recall@k
|
| 681 |
+
|
| 682 |
+
$$\text{Recall@k} = \frac{1}{|\mathcal{U}_\text{test}|} \sum_{u \in \mathcal{U}_\text{test}} \frac{|\text{Rec}_k(u) \cap \text{Rel}(u)|}{|\text{Rel}(u)|}$$
|
| 683 |
+
|
| 684 |
+
### Properties
|
| 685 |
+
- **Range:** $[0, 1]$
|
| 686 |
+
- **Interpretation:** Of all relevant items, what fraction appears in the top-$k$?
|
| 687 |
+
- **Higher is better**
|
| 688 |
+
|
| 689 |
+
### Example
|
| 690 |
+
|
| 691 |
+
If a user has 10 relevant items and 2 appear in the top-5: $\text{Recall@5} = 2/10 = 0.2$
|
| 692 |
+
|
| 693 |
+
---
|
| 694 |
+
|
| 695 |
+
## 5.5 F1@k
|
| 696 |
+
|
| 697 |
+
Harmonic mean of Precision and Recall:
|
| 698 |
+
|
| 699 |
+
$$\text{F1@k} = \frac{1}{|\mathcal{U}_\text{test}|} \sum_{u \in \mathcal{U}_\text{test}} \frac{2 \cdot \text{Precision@k}(u) \cdot \text{Recall@k}(u)}{\text{Precision@k}(u) + \text{Recall@k}(u)}$$
|
| 700 |
+
|
| 701 |
+
### Properties
|
| 702 |
+
- **Range:** $[0, 1]$
|
| 703 |
+
- **Interpretation:** Balanced measure — only high if BOTH precision and recall are high
|
| 704 |
+
- **Higher is better**
|
| 705 |
+
|
| 706 |
+
### Why Harmonic Mean?
|
| 707 |
+
|
| 708 |
+
The harmonic mean is more conservative than the arithmetic mean. If $\text{Precision} = 0.9$ but $\text{Recall} = 0.1$, the F1 score is $2 \cdot 0.9 \cdot 0.1 / (0.9 + 0.1) = 0.18$ — correctly reflecting that one metric is poor. Arithmetic mean would give $0.5$, misleadingly optimistic.
|
| 709 |
+
|
| 710 |
+
---
|
| 711 |
+
|
| 712 |
+
## 5.6 Coverage
|
| 713 |
+
|
| 714 |
+
$$\text{Coverage} = \frac{|\bigcup_{u \in \mathcal{U}_\text{test}} \text{Rec}_k(u)|}{n_i}$$
|
| 715 |
+
|
| 716 |
+
### Properties
|
| 717 |
+
- **Range:** $[0, 1]$
|
| 718 |
+
- **Interpretation:** What fraction of the total product catalog ever appears in anyone's top-$k$?
|
| 719 |
+
- **Higher is better**
|
| 720 |
+
|
| 721 |
+
### Significance
|
| 722 |
+
|
| 723 |
+
Low coverage means the system only recommends popular items (the "Harry Potter problem" — everyone is recommended the same blockbusters). High coverage means niche/long-tail items also have a chance to be recommended.
|
requirements.txt
CHANGED
|
@@ -2,3 +2,5 @@ Flask==3.0.0
|
|
| 2 |
pandas==2.1.4
|
| 3 |
numpy==1.26.2
|
| 4 |
scikit-learn==1.3.2
|
|
|
|
|
|
|
|
|
| 2 |
pandas==2.1.4
|
| 3 |
numpy==1.26.2
|
| 4 |
scikit-learn==1.3.2
|
| 5 |
+
gunicorn==22.0.0
|
| 6 |
+
python-dotenv==1.0.1
|