SPB Git forge

spb/ka-ui

Public
30commits 1branches 0releases
145.7 MBsize
maindefault branch
27 days agolast push
Python 33.5% JavaScript 30.1% TypeScript 25% CSS 10% Shell 1.4%

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 28 days ago (Aug 26, 2026) parent 0127116

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 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})")
modified kaid/kaid.py +8 −2
@@ -292,8 +292,10 @@ def rerank(items: list, user, *, features_of, uid_of=None,
292 292 tail = items[max_considered:]
293 293 n = len(head)
294 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 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 299 scored = []
298 300 badged = 0
299 301 for i, it in enumerate(head):
@@ -303,9 +305,13 @@ def rerank(items: list, user, *, features_of, uid_of=None,
303 305 profile.get("global") or {}, active)
304 306 except Exception:
305 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 310 p = min(1.0, (p if p is not None else 0.55) + 0.25)
308 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 315 if p is None:
310 316 final = (1.0 - blend) * base + blend * 0.5
311 317 else:
312 318