Abdallah4z commited on
Commit
f9ade9b
·
1 Parent(s): 44f6b36

Production readiness: port 7860, Dockerfile, HF Spaces config

Browse files
.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
- # TasteEngine
 
 
 
 
 
 
 
 
 
2
 
3
- A multi-approach recommender system for e-commerce platforms that generates personalized product recommendations using collaborative filtering, content-based filtering, and knowledge-based techniques, with comprehensive evaluation and comparison.
4
 
5
- ## Features
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
- ```bash
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
- - Flask 3.0
87
- - pandas 2.1
88
- - numpy 1.26
89
- - scikit-learn 1.3
 
 
 
90
 
91
- ## License
92
 
93
- MIT
 
 
 
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
- return jsonify({"error": str(e)}), 500
 
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
- return jsonify({"method": method, "error": str(e)})
 
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
- yield json_module.dumps({"type": "error", "message": str(e)}) + "\n"
 
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
- return jsonify({"error": str(e)}), 500
 
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
- return f'<div class="empty-state"><div class="empty-icon">❌</div><p>{str(e)}</p></div>'
 
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
- app.run(debug=True, host="0.0.0.0", port=5000)
 
 
 
 
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