| """ |
| artamodel_full_stack.py — the full ArtaModel stack: per-body single models PLUS the sum models PLUS the aspect grids, |
| every member fitted on the rows it has, out-of-fold, stacked. |
| |
| Arash, 2026-08-19: "the per body models are in addition to the sum models. so aspects would be there too." |
| |
| MEMBERS (each a coherent field |b + Σ w z|² with a logistic head, early-stopped on the inner temporal split of its |
| OWN population, train scores out-of-fold over two temporal halves, test scores from a fit on all its rows): |
| per-phasor 126 single-phasor models (9 terms x 14 bodies) -- artamodel_split_models.py |
| per-term 9 sums over the 14 bodies of one term, e.g. |b + Σ_i a_i e^{i(θm_i − θd_i)}|²: the cross-body |
| interference terms cos(φ_i − φ_j) inside the square are the aspects between the phasors |
| formula the 3-term (a+m+d), 6-term and 9-term sums over all bodies |
| aspect grids the classical inter-body aspects written out explicitly: SYN θm_i − θd_j for all i ≠ j (182 |
| phasors), WM θt_i − θm_j, WD θt_i − θd_j -- the whole synastry and transit grids as sums |
| The stacker is LightGBM over all member scores (NaN where a member has nothing for the couple) with and without |
| the plain columns; held out read once beside the age-cell-matched control. |
| |
| Usage: AQ_PHASES=/tmp/aq3feat/phases.npz AQ_SOL=/tmp/aq3comp/solution.csv AQ_OUT=/tmp/aq3feat python artamodel_full_stack.py |
| """ |
| import json |
| import os |
| import sys |
| import time |
|
|
| import numpy as np |
| import pandas as pd |
| from scipy.stats import rankdata |
|
|
| HERE = os.path.dirname(os.path.abspath(__file__)) |
| sys.path.insert(0, HERE); sys.path.insert(0, os.path.join(HERE, "..", "coherent")) |
| from artamodel import BODIES14, TERMS9, TERMS, auc, phase_matrix |
| from coherent_fit import Coherent |
|
|
| PH = os.environ.get("AQ_PHASES", "/tmp/aq3feat/phases.npz") |
| SOL = os.environ.get("AQ_SOL", "/tmp/aq3comp/solution.csv") |
| OUT = os.environ.get("AQ_OUT", "/tmp/aq3feat") |
| T0 = time.time() |
|
|
|
|
| def log(*a): |
| print(f"[{time.time()-T0:6.0f}s]", *a, flush=True) |
|
|
|
|
| def matched(y, s, cell): |
| num = den = 0.0 |
| for b in np.unique(cell): |
| m = cell == b; n1, n0 = int(y[m].sum()), int((1 - y[m]).sum()) |
| if n1 and n0: |
| num += auc(y[m], s[m]) * n1 * n0; den += n1 * n0 |
| return num / den if den else float("nan") |
|
|
|
|
| def _fit(P, Y, inner, seed=0): |
| C = np.nan_to_num(np.cos(np.radians(P))); S = np.nan_to_num(np.sin(np.radians(P))) |
| m = Coherent(C.shape[1], 1, seed=seed); rng = np.random.default_rng(seed); idx = np.where(~inner)[0] |
| bs = int(min(1024, max(32, len(idx) // 16))); best, state, bad = -1.0, None, 0 |
| for ep in range(120): |
| rng.shuffle(idx) |
| for s0 in range(0, len(idx), bs): |
| b = idx[s0:s0 + bs] |
| if len(b) >= 16: |
| m.step(C[b], S[b], Y[b].astype(float), 0.01, 1e-3) |
| a = auc(Y[inner], m.logit(C[inner], S[inner])[0]) if inner.sum() > 20 else 0.5 |
| if a > best + 1e-5: |
| best, bad = a, 0; state = (m.A1.copy(), m.A2.copy(), m.br.copy(), m.bi.copy(), m.w.copy(), m.c, m.mu.copy(), m.sd.copy()) |
| else: |
| bad += 1 |
| if bad >= 12: |
| break |
| m.A1, m.A2, m.br, m.bi, m.w, m.c, m.mu, m.sd = state |
| return m, best |
|
|
|
|
| def member(P, y, later, Pte, name, min_rows=300): |
| """One stack member on the rows where ANY of its phasors exists: OOF train scores (two temporal halves) and |
| test scores (fit on all its rows). Missing phasors inside a row contribute zero (the presence rule).""" |
| ok = np.isfinite(P).any(1); oke = np.isfinite(Pte).any(1); n = int(ok.sum()) |
| s_tr = np.full(len(P), np.nan); s_te = np.full(len(Pte), np.nan) |
| if n < min_rows or len(np.unique(y[ok])) < 2: |
| return s_tr, s_te, n, float("nan") |
| Pk, Y, L = P[ok], y[ok], later[ok]; order = np.argsort(L); half = np.zeros(n, bool); half[order[n // 2:]] = True |
| for h in (False, True): |
| fit = half == h; oth = ~fit; lf = L[fit]; inner = lf > np.quantile(lf, 0.85) |
| m, _ = _fit(Pk[fit], Y[fit], inner) |
| Co = np.nan_to_num(np.cos(np.radians(Pk[oth]))); So = np.nan_to_num(np.sin(np.radians(Pk[oth]))) |
| tmp = np.full(n, np.nan); tmp[oth] = m.logit(Co, So)[0]; s_tr[np.where(ok)[0][oth]] = tmp[oth] |
| inner = L > np.quantile(L, 0.85); m, best = _fit(Pk, Y, inner) |
| s_te[oke] = m.logit(np.nan_to_num(np.cos(np.radians(Pte[oke]))), np.nan_to_num(np.sin(np.radians(Pte[oke]))))[0] |
| return s_tr, s_te, n, best |
|
|
|
|
| def grid(A, B, bodies_idx): |
| """Inter-body phase differences A_i − B_j for all i != j -> (n, 182) and labels.""" |
| cols, lab = [], [] |
| for i in bodies_idx: |
| for j in bodies_idx: |
| if i == j: continue |
| cols.append(A[:, i] - B[:, j]); lab.append(f"{BODIES14[bodies_idx.index(i)]}-{BODIES14[bodies_idx.index(j)]}") |
| return np.column_stack(cols), lab |
|
|
|
|
| def main(): |
| import lightgbm as lgb |
| Z = np.load(PH, allow_pickle=True) |
| bodies = list(Z["bodies"]); ids = Z["id_test"]; y = Z["y_train"].astype(np.int64) |
| Dtr, Mtr, Wtr = Z["theta_dad_train"], Z["theta_mom_train"], Z["theta_wed_train"] |
| Dte, Mte, Wte = Z["theta_dad_test"], Z["theta_mom_test"], Z["theta_wed_test"] |
| ptr, pte, pn = Z["plain_train"], Z["plain_test"], list(Z["plain_names"]) |
| later = Z["yr_train"].astype(int).max(1) |
| sol = pd.read_csv(SOL).set_index("id"); lab = [c for c in sol.columns if c != "Usage"][0] |
| yte = sol.loc[ids, lab].to_numpy().astype(int) |
| j1 = ptr[:, pn.index("start_is_jan1")] == 1.0; j1e = pte[:, pn.index("start_is_jan1")] == 1.0 |
| Wtr = Wtr.copy(); Wte = Wte.copy(); Wtr[j1] = np.nan; Wte[j1e] = np.nan |
| P, labels = phase_matrix(Dtr, Mtr, Wtr, bodies, BODIES14, TERMS9); Pe, _ = phase_matrix(Dte, Mte, Wte, bodies, BODIES14, TERMS9) |
| B = [bodies.index(b) for b in BODIES14] |
| members_tr, members_te, names, meta = [], [], [], [] |
|
|
| def add(Ptr_, Pte_, name): |
| s_tr, s_te, n, iv = member(Ptr_, y, later, Pte_, name) |
| members_tr.append(s_tr); members_te.append(s_te); names.append(name) |
| a = auc(yte[np.isfinite(s_te)], s_te[np.isfinite(s_te)]) if np.isfinite(s_te).sum() > 100 else float("nan") |
| meta.append({"member": name, "phasors": int(Ptr_.shape[1]), "n_train": n, "inner": iv, "held_on_its_rows": a, "n_test": int(np.isfinite(s_te).sum())}) |
| log(f" {name:<46} phasors {Ptr_.shape[1]:>4} rows {n:>6,} inner {iv if np.isnan(iv) else round(iv,4)} held(on its rows) {a if np.isnan(a) else round(a,4)}") |
|
|
| |
| for j, l in enumerate(labels): |
| add(P[:, [j]], Pe[:, [j]], f"phasor {l}") |
| |
| for t in TERMS9: |
| cols = [j for j, l in enumerate(labels) if l.split("_", 1)[0] == t] |
| add(P[:, cols], Pe[:, cols], f"SUM term {t} over 14 bodies") |
| |
| for sub, nm in ((("a", "m", "d"), "3-term"), (TERMS, "6-term"), (TERMS9, "9-term")): |
| cols = [j for j, l in enumerate(labels) if l.split("_", 1)[0] in sub] |
| add(P[:, cols], Pe[:, cols], f"SUM {nm} over all bodies") |
| |
| G, gl = grid(Mtr, Dtr, B); Ge, _ = grid(Mte, Dte, B); add(G, Ge, "ASPECTS synastry grid θm_i − θd_j (i≠j)") |
| G, gl = grid(Wtr, Mtr, B); Ge, _ = grid(Wte, Mte, B); add(G, Ge, "ASPECTS wedding→mom grid θt_i − θm_j") |
| G, gl = grid(Wtr, Dtr, B); Ge, _ = grid(Wte, Dte, B); add(G, Ge, "ASPECTS wedding→dad grid θt_i − θd_j") |
| Str = np.column_stack(members_tr).astype(np.float32); Ste = np.column_stack(members_te).astype(np.float32) |
| log(f"{Str.shape[1]} members") |
| np.savez_compressed(os.path.join(OUT, "fullstack_members.npz"), S_train=Str, S_test=Ste, names=np.array(names), y=y, yte=yte, later=later, ids=ids) |
| ages_te = pte[:, [pn.index("age_dad_at_start"), pn.index("age_mom_at_start")]] |
| cell = (np.floor(np.nan_to_num(ages_te[:, 0]) / 3) * 1000 + np.floor(np.nan_to_num(ages_te[:, 1]) / 3)).astype(int) |
| cols = [pn.index(c) for c in ("age_dad_at_start", "age_mom_at_start", "age_gap", "start_year")] |
| R = {"members": meta} |
| def stack(Xtr, Xte, name): |
| p = np.zeros(len(Xte)) |
| for sd in range(3): |
| c = lgb.LGBMClassifier(n_estimators=400, learning_rate=0.03, num_leaves=15, min_child_samples=100, colsample_bytree=0.6, |
| subsample=0.8, subsample_freq=1, reg_lambda=10.0, random_state=sd, verbose=-1) |
| c.fit(Xtr, y); p += c.predict_proba(Xte)[:, 1] |
| p /= 3; R[name] = {"held": auc(yte, p), "age_cell_matched": matched(yte, p, cell)} |
| log(f" {name:<58} held {auc(yte, p):.4f} age-cell-matched {matched(yte, p, cell):.4f}") |
| return p |
| per = [i for i, n in enumerate(names) if n.startswith("phasor ")]; sums = [i for i, n in enumerate(names) if n.startswith("SUM")] |
| asp = [i for i, n in enumerate(names) if n.startswith("ASPECTS")] |
| stack(Str[:, per], Ste[:, per], "STACK per-phasor only (126)") |
| stack(Str[:, sums], Ste[:, sums], "STACK sum models only (12)") |
| stack(Str[:, asp], Ste[:, asp], "STACK aspect grids only (3)") |
| stack(Str, Ste, "STACK all members (per-phasor + sums + aspects)") |
| p_all = stack(np.column_stack([Str, ptr[:, cols]]), np.column_stack([Ste, pte[:, cols]]), "STACK all members + plain columns") |
| stack(ptr[:, cols], pte[:, cols], "REFERENCE plain columns alone") |
| r01 = lambda v: (rankdata(v) - 1) / max(1.0, len(v) - 1) |
| pd.DataFrame({"id": ids, lab: r01(p_all)}).to_csv(os.path.join(OUT, "submission_fullstack.csv"), index=False) |
| json.dump(R, open(os.path.join(OUT, "artamodel_full_stack.json"), "w"), indent=1) |
| log(f"wrote {OUT}/artamodel_full_stack.json and submission_fullstack.csv") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|