artamodel / artamodel_split_models.py
artaquest's picture
flatten: study files at the root like the rest
c92950c verified
Raw
History Blame Contribute Delete
9.31 kB
"""
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 # noqa: E402
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]
# out-of-fold on two temporal halves
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]
# the model for the test rows: fitted on all of the phasor's rows, inner = latest 15%
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)")
# by term: which term's per-body models carry the most, on their own populations
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()