| 211 |
211 |
and "SIMILAR_USERS" in (reco_cf or {}).get("reasons", []), |
| 212 |
212 |
f"similar={similar} pos={pos_cf} reco={reco_cf}") |
| 213 |
213 |
|
|
214 |
+# 12. matrix factorization (ALS) : cohorte croisée → le modèle recommande à A |
|
215 |
+# l'annonce aimée par les membres au profil proche, jusqu'au rerank |
|
216 |
+import shutil |
|
217 |
+import subprocess |
|
218 |
+extra_uids = [] |
|
219 |
+for ka, email in [("ka-9900000004", "test-kaid-d@test.local"), |
|
220 |
+ ("ka-9900000005", "test-kaid-e@test.local"), |
|
221 |
+ ("ka-9900000006", "test-kaid-f@test.local")]: |
|
222 |
+ con.execute("DELETE FROM users WHERE email=?", (email,)) |
|
223 |
+ con.execute("INSERT INTO users (email, name, ka_id) VALUES (?,?,?)", |
|
224 |
+ (email, "Membre Test MF", ka)) |
|
225 |
+ extra_uids.append(con.execute("SELECT id FROM users WHERE ka_id=?", (ka,)).fetchone()["id"]) |
|
226 |
+uid_d, uid_e, uid_f = extra_uids |
|
227 |
+fav = "INSERT OR IGNORE INTO favorites (user_id, app, item_id, title) VALUES (?,?,?,?)" |
|
228 |
+for uid, items in [(uid_a, ["e2e:x1", "e2e:x2"]), |
|
229 |
+ (uid_d, ["e2e:x1", "e2e:x2", "e2e:x3"]), |
|
230 |
+ (uid_e, ["e2e:x2", "e2e:x3", "e2e:x4"]), |
|
231 |
+ (uid_f, ["e2e:x1", "e2e:x3"])]: |
|
232 |
+ for it in items: |
|
233 |
+ con.execute(fav, (uid, "lou-ka", it, f"Annonce {it}")) |
|
234 |
+con.commit() |
|
235 |
+node = shutil.which("node") or "/opt/homebrew/bin/node" |
|
236 |
+run = subprocess.run([node, "scripts/kaid-mf.mjs", "--app", "lou-ka"], |
|
237 |
+ cwd=os.path.expanduser("~/apps/groupe-ka"), |
|
238 |
+ capture_output=True, text=True, timeout=120) |
|
239 |
+mf_rows = [r["item_id"] for r in con.execute( |
|
240 |
+ "SELECT item_id FROM mf_recs WHERE user_id=? AND app='lou-ka' ORDER BY rank", |
|
241 |
+ (uid_a,)).fetchall()] |
|
242 |
+st, prefs = call("lou-ka", "GET", "/api/sso/prefs") |
|
243 |
+mf_in_prefs = (prefs.get("profile") or {}).get("app", {}).get("mf") or [] |
|
244 |
+kaid.invalidate_prefs(TA) |
|
245 |
+items_mf = ([{"uid": f"q{i}", "city": "Québec", "unit_type": "5 1/2", |
|
246 |
+ "price": 1500} for i in range(6)] |
|
247 |
+ + [{"uid": "e2e:x3", "city": "Québec", "unit_type": "5 1/2", |
|
248 |
+ "price": 1500}]) |
|
249 |
+out_mf, _ = kaid.rerank( |
|
250 |
+ list(items_mf), {"ka_id": TA}, |
|
251 |
+ features_of=lambda it: {"city": it["city"], "price": it["price"]}) |
|
252 |
+pos_mf = [o["uid"] for o in out_mf].index("e2e:x3") |
|
253 |
+reco_mf = next((o.get("ka_reco") for o in out_mf if o["uid"] == "e2e:x3"), None) |
|
254 |
+mf_reason_ok = reco_mf is not None and any( |
|
255 |
+ r0 in ("COLLABORATIVE_MODEL", "SIMILAR_USERS") for r0 in reco_mf.get("reasons", [])) |
|
256 |
+check("matrix factorization → ALS entraîné, reco x3 propagée + boostée", |
|
257 |
+ "e2e:x3" in mf_rows and "e2e:x3" in mf_in_prefs and pos_mf < 6 and mf_reason_ok, |
|
258 |
+ f"stdout={run.stdout.strip()[-160:]} mf_rows={mf_rows[:5]} " |
|
259 |
+ f"prefs_mf={mf_in_prefs[:5]} pos={pos_mf} reco={reco_mf}") |
|
260 |
+ |
| 214 |
261 |
# --- nettoyage --------------------------------------------------------------- |
| 215 |
|
−for uid in (uid_a, uid_b, uid_c): |
|
262 |
+for uid in (uid_a, uid_b, uid_c, uid_d, uid_e, uid_f): |
| 216 |
263 |
for table in ("user_events", "favorites", "saved_searches", "hidden_items", |
| 217 |
|
− "user_prefs", "pref_overrides"): |
|
264 |
+ "user_prefs", "pref_overrides", "mf_recs"): |
| 218 |
265 |
con.execute(f"DELETE FROM {table} WHERE user_id=?", (uid,)) |
| 219 |
266 |
con.execute("DELETE FROM users WHERE email LIKE 'test-kaid-%@test.local'") |
| 220 |
267 |
con.execute("DELETE FROM favorites WHERE item_id LIKE 'e2e:%'") |
|
268 |
+con.execute("DELETE FROM mf_recs WHERE item_id LIKE 'e2e:%'") |
| 221 |
269 |
con.commit() |
| 222 |
270 |
left = con.execute("SELECT COUNT(*) c FROM users WHERE ka_id IN (?,?)", (TA, TB)).fetchone()["c"] |
| 223 |
271 |
print(f"\nnettoyage : membres de test supprimés (restants={left})") |