SPB Git

spb/ka-ui

Public

kaid v2.2 : matrix factorization — boost COLLABORATIVE_MODEL (+0.25, aligné sur SIMILAR_USERS) des recommandations ALS du hub (profile.app.mf) ; e2e-test.py passe à 12 scénarios (cohorte synthétique → entraînement ALS réel → reco propagée jusqu'au rerank)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 26, 2026) parent 0127116 Browse files

2 changed files +58 −4

modified kaid/e2e-test.py +50 −2
@@ -211,13 +211,61 @@ check("filtrage collaboratif → co-favori détecté + boosté (SIMILAR_USERS)",
211 and "SIMILAR_USERS" in (reco_cf or {}).get("reasons", []), 211 and "SIMILAR_USERS" in (reco_cf or {}).get("reasons", []),
212 f"similar={similar} pos={pos_cf} reco={reco_cf}") 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 # --- nettoyage --------------------------------------------------------------- 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 for table in ("user_events", "favorites", "saved_searches", "hidden_items", 263 for table in ("user_events", "favorites", "saved_searches", "hidden_items",
217− "user_prefs", "pref_overrides"): 264+ "user_prefs", "pref_overrides", "mf_recs"):
218 con.execute(f"DELETE FROM {table} WHERE user_id=?", (uid,)) 265 con.execute(f"DELETE FROM {table} WHERE user_id=?", (uid,))
219 con.execute("DELETE FROM users WHERE email LIKE 'test-kaid-%@test.local'") 266 con.execute("DELETE FROM users WHERE email LIKE 'test-kaid-%@test.local'")
220 con.execute("DELETE FROM favorites WHERE item_id LIKE 'e2e:%'") 267 con.execute("DELETE FROM favorites WHERE item_id LIKE 'e2e:%'")
268+con.execute("DELETE FROM mf_recs WHERE item_id LIKE 'e2e:%'")
221 con.commit() 269 con.commit()
222 left = con.execute("SELECT COUNT(*) c FROM users WHERE ka_id IN (?,?)", (TA, TB)).fetchone()["c"] 270 left = con.execute("SELECT COUNT(*) c FROM users WHERE ka_id IN (?,?)", (TA, TB)).fetchone()["c"]
223 print(f"\nnettoyage : membres de test supprimés (restants={left})") 271 print(f"\nnettoyage : membres de test supprimés (restants={left})")
modified kaid/kaid.py +8 −2
@@ -292,8 +292,10 @@ def rerank(items: list, user, *, features_of, uid_of=None,
292 tail = items[max_considered:] 292 tail = items[max_considered:]
293 n = len(head) 293 n = len(head)
294 active = active_dims or set() 294 active = active_dims or set()
295− # co-favoris des membres semblables (filtrage collaboratif du hub) 295+ # signaux collaboratifs du hub : co-favoris (item-item) et
296+ # recommandations du modèle de matrix factorization (ALS, batch quotidien)
296 similar = {str(s) for s in (app_p.get("similar") or [])} 297 similar = {str(s) for s in (app_p.get("similar") or [])}
298+ mf = {str(s) for s in (app_p.get("mf") or [])}
297 scored = [] 299 scored = []
298 badged = 0 300 badged = 0
299 for i, it in enumerate(head): 301 for i, it in enumerate(head):
@@ -303,9 +305,13 @@ def rerank(items: list, user, *, features_of, uid_of=None,
303 profile.get("global") or {}, active) 305 profile.get("global") or {}, active)
304 except Exception: 306 except Exception:
305 p, reasons = None, [] 307 p, reasons = None, []
306− if similar and str(uid_of(it)) in similar: 308+ uid = str(uid_of(it))
309+ if similar and uid in similar:
307 p = min(1.0, (p if p is not None else 0.55) + 0.25) 310 p = min(1.0, (p if p is not None else 0.55) + 0.25)
308 reasons = (["SIMILAR_USERS"] + reasons)[:4] 311 reasons = (["SIMILAR_USERS"] + reasons)[:4]
312+ elif mf and uid in mf:
313+ p = min(1.0, (p if p is not None else 0.55) + 0.25)
314+ reasons = (["COLLABORATIVE_MODEL"] + reasons)[:4]
309 if p is None: 315 if p is None:
310 final = (1.0 - blend) * base + blend * 0.5 316 final = (1.0 - blend) * base + blend * 0.5
311 else: 317 else:
312 318
Menu

Theme