| """ |
| artamodel_split_models.py — one single-sum model per (term, body), each trained on the rows where its phasor |
| exists, stacked. |
| |
| Arash, 2026-08-19: "ensure each model is trained with the data that is available. for example the dad's natal model |
| should be trained with almost all the data since missing dad natals is rare. also train per body model |
| |b + a_i e^{i(θm_i − θd_i)}|, and again for day-missing cases only train the outer planets." |
| |
| So: for every phasor j of the nine-term model (a m d mn dn tn c mw dw × 14 bodies = 126), a field |
| u_j = |b_j + w_j e^{iφ_j}|² with its own logistic head, fitted on EVERY training row where φ_j exists -- the |
| precision-aware NaNs already say which: a year-only birth has only its slow bodies, a year-only wedding no wedding |
| sky, an absent partner nothing. Each model's early stopping uses the inner temporal split of ITS OWN population. |
| Train-side scores are OUT-OF-FOLD (two temporal halves) so the stacker never sees an in-sample score; a LightGBM |
| stacker then combines the 126 scores (NaN where a phasor is absent for the couple) -- with and without the plain |
| columns -- and everything is read on the held-out half 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_split_models.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, ArtaModel, auc, phase_matrix |
|
|
| 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() |
| SLOW = {"jupiter", "saturn", "uranus", "neptune", "pluto", "true_node", "true_south_node", "chiron", "mean_lilith"} |
|
|
|
|
| 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 one_model(p, y, later, pte, seed=0): |
| """A K=1 field on this phasor's own rows: returns (train scores out-of-fold, test scores, n_rows, inner auc).""" |
| ok = np.isfinite(p); n = int(ok.sum()) |
| s_tr = np.full(len(p), np.nan); s_te = np.full(len(pte), np.nan) |
| if n < 300 or len(np.unique(y[ok])) < 2: |
| return s_tr, s_te, n, float("nan") |
| P = p[ok][:, None]; Y = y[ok]; L = 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 |
| inner = np.zeros(int(fit.sum()), bool); lf = L[fit]; inner[lf > np.quantile(lf, 0.85)] = True |
| am = ArtaModel(terms=("x",), bodies=["x"], F=1, seed=seed) |
| C = np.nan_to_num(np.cos(np.radians(P[fit]))); S = np.nan_to_num(np.sin(np.radians(P[fit]))) |
| from coherent_fit import Coherent |
| m = Coherent(1, 1, seed=seed); rng = np.random.default_rng(seed); idx = np.where(~inner)[0] |
| best, state, bad = -1.0, None, 0 |
| for ep in range(120): |
| rng.shuffle(idx) |
| for s0 in range(0, len(idx), max(32, len(idx) // 16)): |
| b = idx[s0:s0 + max(32, len(idx) // 16)] |
| if len(b) >= 16: m.step(C[b], S[b], Y[fit][b].astype(float), 0.01, 1e-3) |
| a = auc(Y[fit][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 |
| Co = np.cos(np.radians(P[oth])); So = np.sin(np.radians(P[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) |
| C = np.cos(np.radians(P)); S = np.sin(np.radians(P)) |
| m = Coherent(1, 1, seed=seed); rng = np.random.default_rng(seed); idx = np.where(~inner)[0] |
| best, state, bad = -1.0, None, 0 |
| for ep in range(120): |
| rng.shuffle(idx) |
| for s0 in range(0, len(idx), max(32, len(idx) // 16)): |
| b = idx[s0:s0 + max(32, len(idx) // 16)] |
| 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 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 |
| oke = np.isfinite(pte) |
| s_te[oke] = m.logit(np.cos(np.radians(pte[oke][:, None])), np.sin(np.radians(pte[oke][:, None])))[0] |
| return s_tr, s_te, n, best |
|
|
|
|
| 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) |
| log(f"{len(labels)} phasors (9 terms x 14 bodies) · train {len(y):,} · held out {len(yte):,}") |
| Str = np.full((len(y), len(labels)), np.nan, np.float32); Ste = np.full((len(yte), len(labels)), np.nan, np.float32) |
| info = [] |
| for j, labj in enumerate(labels): |
| s_tr, s_te, n, iv = one_model(P[:, j], y, later, Pe[:, j]) |
| Str[:, j], Ste[:, j] = s_tr, s_te |
| a_te = auc(yte[np.isfinite(s_te)], s_te[np.isfinite(s_te)]) if np.isfinite(s_te).sum() > 100 else float("nan") |
| info.append({"phasor": labj, "n_train": n, "inner": iv, "held": a_te, "n_test": int(np.isfinite(s_te).sum())}) |
| if j % 14 == 0: |
| log(f" {labj:<20} rows {n:>6,} inner {iv if np.isnan(iv) else round(iv,4)} held {a_te if np.isnan(a_te) else round(a_te,4)}") |
| log("all per-phasor models fitted") |
| 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 = {"per_phasor": info} |
|
|
| 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:<64} held {auc(yte, p):.4f} age-cell-matched {matched(yte, p, cell):.4f}") |
| return p |
| six = [j for j, l in enumerate(labels) if l.split("_", 1)[0] in TERMS] |
| stack(Str[:, six], Ste[:, six], "STACK of the 84 six-term per-phasor models (LightGBM, NaN-native)") |
| stack(Str, Ste, "STACK of all 126 nine-term per-phasor models") |
| stack(np.column_stack([Str, ptr[:, cols]]), np.column_stack([Ste, pte[:, cols]]), "STACK of 126 + the plain columns") |
| stack(ptr[:, cols], pte[:, cols], "REFERENCE: plain columns alone (LightGBM)") |
| |
| log(" best single per-phasor models by held-out (each on its own rows):") |
| for r in sorted([r for r in info if not np.isnan(r["held"])], key=lambda r: -r["held"])[:12]: |
| log(f" {r['phasor']:<18} rows {r['n_train']:>6,} held {r['held']:.4f} (n_test {r['n_test']:,})") |
| p_all = stack(np.column_stack([Str, ptr[:, cols]]), np.column_stack([Ste, pte[:, cols]]), "STACK 126 + plain (for the submission)") |
| 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_split126.csv"), index=False) |
| json.dump(R, open(os.path.join(OUT, "artamodel_split_models.json"), "w"), indent=1) |
| log(f"wrote {OUT}/artamodel_split_models.json and submission_split126.csv") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|