ArtaModel: deployed 6-term boosted-split model (train+test), card, study, code
Browse files- ARTAMODEL.md +140 -0
- README.md +90 -0
- artamodel.py +174 -0
- artamodel_deploy.py +182 -0
- artamodel_deployed.json +608 -0
- artamodel_ensemble.json +176 -0
- artamodel_ensemble.py +270 -0
- artamodel_leaderboard.json +435 -0
- artamodel_score.py +107 -0
- artamodel_selected.json +66 -0
- artamodel_study.json +0 -0
- coherent_fit.py +331 -0
- kerykeion_phases.py +142 -0
- requirements.txt +4 -0
ARTAMODEL.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ArtaModel — the study
|
| 2 |
+
|
| 3 |
+
*Named by Arash, 2026-08-18. Third-edition gendered data (dad/mom from P21; both births to the day, both
|
| 4 |
+
birthplaces; the wedding date; sidereal phases from Kerykeion at 09:00 local). 250 fits in
|
| 5 |
+
`artamodel_study.py`; every number below is held out on couples born after 1900 unless marked "inner", every
|
| 6 |
+
choice made on the inner temporal split, and every experiment on a FIXED population.*
|
| 7 |
+
|
| 8 |
+
## The model
|
| 9 |
+
|
| 10 |
+
```
|
| 11 |
+
y = | b + Σᵢ aᵢ ·e^{i(θmᵢ − θdᵢ)} synastry, mom − dad
|
| 12 |
+
+ mᵢ ·e^{i(θtᵢ − θmᵢ)} the wedding sky transiting mom
|
| 13 |
+
+ dᵢ ·e^{i(θtᵢ − θdᵢ)} the wedding sky transiting dad
|
| 14 |
+
+ mnᵢ·e^{i θmᵢ} mom's own natal phase
|
| 15 |
+
+ dnᵢ·e^{i θdᵢ} dad's own natal phase
|
| 16 |
+
+ tnᵢ·e^{i θtᵢ} the wedding sky's own phase
|
| 17 |
+
+ cᵢ ·e^{i θcᵢ} the composite (shorter-arc midpoint of the two natal longitudes)
|
| 18 |
+
+ tcᵢ·e^{i(θtᵢ − θcᵢ)} |² the wedding sky transiting the composite
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
Fourteen bodies (Sun … Pluto, true node = Rāhu, true south node = Ketu, Chiron, mean Lilith), Ascendant/MC
|
| 22 |
+
optional. Every term exists only when both of its phases exist ("if wedding is not known, drop the last two
|
| 23 |
+
terms"; "if dob of either is not known, drop the natal term of it") — a missing phase contributes exactly zero.
|
| 24 |
+
Complex weights as (Re, Im), |·|², a logistic head for the loss, Adam with L2, early-stopped on the inner
|
| 25 |
+
temporal split; F copies of the formula combined by the head, F = 1 being the formula literally.
|
| 26 |
+
|
| 27 |
+
**Populations.** FULL — both natal charts complete and the wedding day known: 6,258 train / 2,635 held out.
|
| 28 |
+
CHARTS — both charts, wedding may be year-only: 9,716 / 7,428. ANY — any row with one phasor: up to 76,081 / 7,428.
|
| 29 |
+
**Plain reference on the same rows** (boosted trees on the two ages at the start, the gap and the start year):
|
| 30 |
+
FULL 0.6371 · CHARTS 0.6083 · ANY 0.6171. Dad's age at the start alone: 0.6264 on FULL.
|
| 31 |
+
|
| 32 |
+
## 1 · Which terms (all 63 subsets of the six + the composite rungs, FULL, F = 1)
|
| 33 |
+
|
| 34 |
+
| terms | phasors | inner | held out |
|
| 35 |
+
|---|---|---|---|
|
| 36 |
+
| **m + d** (the two wedding-transit terms) — *selected by inner* | 28 | 0.6426 | **0.6304** |
|
| 37 |
+
| a + m + d (Arash's first formula) | 42 | 0.6416 | 0.6258 |
|
| 38 |
+
| a + d | 28 | 0.6280 | 0.6318 |
|
| 39 |
+
| d alone | 14 | 0.6235 | 0.6223 |
|
| 40 |
+
| a alone (synastry) | 14 | 0.5965 | 0.5755 |
|
| 41 |
+
| mn + dn (the natal phases) | 28 | 0.5507 | 0.5235 |
|
| 42 |
+
| tn (the wedding sky alone) | 14 | 0.5201 | 0.4551 |
|
| 43 |
+
| c (the composite alone) | 14 | 0.5069 | 0.4563 |
|
| 44 |
+
| c + tc | 28 | 0.6149 | 0.5828 |
|
| 45 |
+
| a + m + d + mn + dn | 70 | 0.6113 | 0.6098 |
|
| 46 |
+
| a + m + d + mn + dn + tn | 84 | 0.6021 | 0.5931 |
|
| 47 |
+
| a + m + d + c + tc | 70 | 0.6162 | 0.6104 |
|
| 48 |
+
| all eight | 112 | 0.5634 | 0.5740 |
|
| 49 |
+
|
| 50 |
+
Across the 70 configurations the inner split picks m+d; the best held-out anywhere is 0.6318, so the optimism of
|
| 51 |
+
selecting on the test set would have been only +0.0014 here. Every **absolute-phase** term (mn, dn, tn, c) makes
|
| 52 |
+
the model worse out of time; every **difference** term (a, m, d, tc) helps or is neutral.
|
| 53 |
+
|
| 54 |
+
## 2 · Which bodies (3-term a+m+d, FULL)
|
| 55 |
+
|
| 56 |
+
**One body at a time:** Uranus alone **0.6419** · the three outer 0.6302 · Neptune 0.6260 · Pluto 0.6203 · Chiron
|
| 57 |
+
0.6118 · Saturn 0.5913 · the nodes 0.5670 · Jupiter 0.5136 · Moon 0.5026 · Lilith 0.5003 · Mars 0.4938 · Venus
|
| 58 |
+
0.4796 · Mercury 0.4784 · **Sun 0.4728**. Sets: modern10 0.6202 · slow5 0.6204 · classical7 0.5889 · fast5 0.4827.
|
| 59 |
+
|
| 60 |
+
**Dropping one body from all fourteen** changes held-out by −0.0012 (Chiron) to **+0.0062** (either node) — dropping
|
| 61 |
+
any *fast* body improves the model, dropping any *slow* body barely moves it (they are redundant with each other).
|
| 62 |
+
|
| 63 |
+
## 3 · Invariances that prove what it reads
|
| 64 |
+
|
| 65 |
+
| convention (recomputed through Kerykeion) | 3-term held out | 6-term held out |
|
| 66 |
+
|---|---|---|
|
| 67 |
+
| Lahiri 09:00 local (baseline) | 0.6258 | 0.5931 |
|
| 68 |
+
| Raman · Fagan-Bradley · Krishnamurti | 0.6258 · 0.6258 · 0.6258 | 0.5931 · 0.5931 · 0.5931 |
|
| 69 |
+
| **tropical** | 0.6258 | 0.5941 |
|
| 70 |
+
| birth hour 06:00 · 12:00 · 18:00 local | 0.6258 · 0.6259 · 0.6259 | 0.5931 |
|
| 71 |
+
| 12:00 UT, **place ignored** | 0.6259 | 0.5933 |
|
| 72 |
+
| wedding at 00:00 UT | 0.6260 | 0.5934 |
|
| 73 |
+
|
| 74 |
+
The three-term model is **exactly invariant** to the ayanāṁśa and to the zodiac (a constant offset cancels in a
|
| 75 |
+
difference of two phases), and invariant to four decimals to the birth hour and the birthplace (a common shift of
|
| 76 |
+
both charts moves a slow body by nothing). It therefore cannot be reading anything sidereal, local, or angular.
|
| 77 |
+
|
| 78 |
+
## 4 · What it reads
|
| 79 |
+
|
| 80 |
+
**The two ages at the wedding, and the age gap, through the slow bodies as clocks.** Uranus moves 4.3°/yr and
|
| 81 |
+
completes a cycle in 84 years, so `θt − θm` for Uranus is mom's age at the wedding, unwrapped for anyone under
|
| 82 |
+
84; Neptune (2.2°/yr) and Pluto (1.5°/yr) the same at lower resolution; `θm − θd` for the same bodies is the age
|
| 83 |
+
gap. The fitted anatomy says so directly — the largest weights of the 3-term model are `d_pluto 1.27, m_pluto
|
| 84 |
+
1.16, a_pluto 1.11, d_uranus 0.99, a_neptune 0.90, d_neptune 0.90` — and the controls confirm it:
|
| 85 |
+
|
| 86 |
+
| 3-term, FULL | AUC |
|
| 87 |
+
|---|---|
|
| 88 |
+
| held out | 0.6269 |
|
| 89 |
+
| **held out within 3-year cells of (dad's age, mom's age)** | **0.4955** |
|
| 90 |
+
| within 2-year age-gap bands | 0.5635 |
|
| 91 |
+
| plain reference (two ages + gap + start year, boosted) | 0.6371 |
|
| 92 |
+
| reference + ArtaModel score (combiner fitted on train, out-of-fold) | 0.6361 |
|
| 93 |
+
|
| 94 |
+
Hold the two ages flat and the model is at chance. Add its score to the plain reference and nothing is gained.
|
| 95 |
+
The 6-, 3+composite- and 8-term variants read 0.4765, 0.5080 and 0.5058 age-cell-matched; the reference gains at
|
| 96 |
+
most +0.0024 from any of them (noise).
|
| 97 |
+
|
| 98 |
+
## 5 · The rest
|
| 99 |
+
|
| 100 |
+
- **Populations** (same formula): a+m+d FULL 0.6258 → CHARTS 0.6047 → ANY 0.6086; the six-term formula FULL
|
| 101 |
+
0.5931 → CHARTS 0.5719 → **ANY 0.4877** — on ANY, ~50,000 rows carrying only a wedding sky (an era clock) join
|
| 102 |
+
the fit and drag the shared weights toward era, which reverses across the 1900 split.
|
| 103 |
+
- **Angles** (ASC/MC in the synastry/natal terms): 0.6194 vs 0.6258 without — worse.
|
| 104 |
+
- **Harmonics** (phases × h): h=2 0.6284, h=3 0.6203, h=4 0.6069 for a+m+d — a clock survives doubling.
|
| 105 |
+
- **Fields × L2** (3-term): held-out 0.616–0.6365 across 28 settings; the inner split picks F=64, L2=0.01
|
| 106 |
+
→ 0.6163 (optimism of picking on the test set would be +0.020). Ten seeds at F=1: 0.6252 ± 0.0030.
|
| 107 |
+
- **Temporal folds inside the training half** rank the ladder the way the held-out set does here (a+m+d 0.63 on
|
| 108 |
+
the folds vs 0.626 held out; six-term 0.58–0.63 vs 0.593) — unlike the tropical stacks of the first edition.
|
| 109 |
+
- **Composite (Davison-style)**: alone at chance (0.4563); with its transit, 0.5828; added to a+m+d, worse (0.6104).
|
| 110 |
+
|
| 111 |
+
## 6 · Verdict
|
| 112 |
+
|
| 113 |
+
ArtaModel is a well-behaved, fully specified, honestly fitted model — and every point of held-out AUC it earns is
|
| 114 |
+
the two partners' ages at the wedding and the gap between their births, measured by the outer planets as clocks.
|
| 115 |
+
That is why it is invariant to the zodiac, the hour and the place; why Uranus alone equals the whole thing; why
|
| 116 |
+
the fast bodies are pure noise; and why it vanishes when the ages are held flat. Its ceiling on this data is the
|
| 117 |
+
plain reference (0.6371 on FULL), which it does not reach (0.6304 at best, m+d) and does not add to.
|
| 118 |
+
|
| 119 |
+
What would move it: information that is not a function of the three dates — birth **times** (the angles would
|
| 120 |
+
then be real), or a wedding **place** (a real electional lagna). Both are absent from Wikidata for these couples.
|
| 121 |
+
|
| 122 |
+
## 7 · Ensembles, boosting, and split single-sum models (`artamodel_ensemble.py`)
|
| 123 |
+
|
| 124 |
+
Arash, 2026-08-18: "use ensembles and boosting techniques and split multiple single sum model". FULL population;
|
| 125 |
+
the plain reference on the same rows is 0.6353.
|
| 126 |
+
|
| 127 |
+
| construction | 3-term | 6-term |
|
| 128 |
+
|---|---|---|
|
| 129 |
+
| single ArtaModel F=1 | 0.6251 | 0.5820 |
|
| 130 |
+
| BAG, 25 bootstraps rank-averaged | 0.6339 | 0.6031 |
|
| 131 |
+
| BOOST, single-sum fields on residuals | 0.6318 | 0.5933–0.5969 |
|
| 132 |
+
| SPLIT per body (14 single sums), linear head | 0.6339 | 0.6251 |
|
| 133 |
+
| SPLIT per term / per phasor, linear head | 0.6291 / 0.6297 | 0.6195 / 0.6070 |
|
| 134 |
+
| SPLIT → LightGBM on the intensities | 0.6220–0.6293 | 0.6097–0.6269 |
|
| 135 |
+
| **BOOST over SPLIT, per phasor** | **0.6373** | **0.6388** |
|
| 136 |
+
|
| 137 |
+
Splitting rescues the six-term formula (each absolute-phase term in its own sum can be weighted down); boosting
|
| 138 |
+
over the split sums reaches the reference but does not cross it; the age-cell-matched control stays at 0.50–0.52
|
| 139 |
+
for every construction — better instruments for the same two quantities. Inner-selected picks: 3-term BOOST
|
| 140 |
+
0.6318, 6-term BOOST-over-SPLIT-per-body 0.6293 (the top held-out numbers carry about +0.007 of optimism).
|
README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc0-1.0
|
| 3 |
+
language: [en]
|
| 4 |
+
tags: [astrology, sidereal, jyotisha, marriage, tabular, phase-model, artaquest]
|
| 5 |
+
datasets: [artaquest-foundation/artamatch-sidereal]
|
| 6 |
+
metrics: [roc_auc]
|
| 7 |
+
library_name: numpy
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# ArtaModel
|
| 11 |
+
|
| 12 |
+
**A sidereal phase model of a marriage**, named by Arash Ashrafnejad (ArtaQuest Foundation, 2026-08-18). Three dates
|
| 13 |
+
and two places in — his birth and birthplace, hers, and the wedding date — one probability out: did the marriage
|
| 14 |
+
last thirty years?
|
| 15 |
+
|
| 16 |
+
```
|
| 17 |
+
y = | b + Σᵢ aᵢ ·e^{i(θmᵢ − θdᵢ)} mom's longitude minus dad's (synastry)
|
| 18 |
+
+ mᵢ ·e^{i(θtᵢ − θmᵢ)} the wedding sky minus mom's chart (transit to mom)
|
| 19 |
+
+ dᵢ ·e^{i(θtᵢ − θdᵢ)} the wedding sky minus dad's chart (transit to dad)
|
| 20 |
+
+ mnᵢ·e^{i θmᵢ} mom's own natal longitude
|
| 21 |
+
+ dnᵢ·e^{i θdᵢ} dad's own natal longitude
|
| 22 |
+
+ tnᵢ·e^{i θtᵢ} |² the wedding sky itself
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
for each of fourteen bodies *i* (Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto, Rāhu,
|
| 26 |
+
Ketu, Chiron, Lilith). θ are **sidereal longitudes (Lahiri)** from Kerykeion (Swiss Ephemeris): the births cast at
|
| 27 |
+
**09:00 local time at the birthplace** (nobody's birth time is recorded — this is the dataset's convention), the
|
| 28 |
+
wedding at 12:00 UT. **A term exists only when both of its phases exist**: an unknown wedding day drops the wedding
|
| 29 |
+
terms, an unknown birth drops that partner's terms; a missing phase contributes exactly zero.
|
| 30 |
+
|
| 31 |
+
## The deployed model, term by term
|
| 32 |
+
|
| 33 |
+
The deployed model is **gradient boosting over split single-sum fields**: each stage is one field
|
| 34 |
+
`|bₖ + wₖ·e^{iφ}|²` on **one** phasor φ, chosen greedily at that stage as the phasor that best explains the
|
| 35 |
+
current residual (so all 84 phasors of all six terms compete at every stage), and added to the logit as
|
| 36 |
+
`stepₖ·(αₖ·u + cₖ)`. Fitted on **all the data — train and test rows with both natal charts, 16,802
|
| 37 |
+
couples** — for 31 stages (the number the train-only fit chose on its inner temporal split).
|
| 38 |
+
|
| 39 |
+
Of the 84 phasors offered, the boosting chose **6**. Every one of them is an outer-planet clock:
|
| 40 |
+
|
| 41 |
+
| phasor | body | term | stages | contribution to the logit swing | phase at which the field peaks |
|
| 42 |
+
|---|---|---|---|---|---|
|
| 43 |
+
| `a_uranus` | Uranus | a | 12 | 0.221 | 193° |
|
| 44 |
+
| `d_pluto` | Pluto | d | 5 | 0.170 | 295° |
|
| 45 |
+
| `d_neptune` | Neptune | d | 3 | 0.096 | 200° |
|
| 46 |
+
| `d_uranus` | Uranus | d | 4 | 0.056 | 88° |
|
| 47 |
+
| `m_pluto` | Pluto | m | 3 | 0.019 | 134° |
|
| 48 |
+
| `m_saturn` | Saturn | m | 4 | 0.001 | 103° |
|
| 49 |
+
|
| 50 |
+
Never chosen, at any stage:
|
| 51 |
+
- **a** (a·e^{i(θm−θd)}): 13 bodies never chosen — sun, moon, mercury, venus, mars, jupiter, saturn, neptune, pluto, true_node, true_south_node, chiron, mean_lilith
|
| 52 |
+
- **m** (m·e^{i(θt−θm)}): 12 bodies never chosen — sun, moon, mercury, venus, mars, jupiter, uranus, neptune, true_node, true_south_node, chiron, mean_lilith
|
| 53 |
+
- **d** (d·e^{i(θt−θd)}): 11 bodies never chosen — sun, moon, mercury, venus, mars, jupiter, saturn, true_node, true_south_node, chiron, mean_lilith
|
| 54 |
+
- **mn** (mn·e^{iθm}): 14 bodies never chosen — sun, moon, mercury, venus, mars, jupiter, saturn, uranus, neptune, pluto, true_node, true_south_node, chiron, mean_lilith
|
| 55 |
+
- **dn** (dn·e^{iθd}): 14 bodies never chosen — sun, moon, mercury, venus, mars, jupiter, saturn, uranus, neptune, pluto, true_node, true_south_node, chiron, mean_lilith
|
| 56 |
+
- **tn** (tn·e^{iθt}): 14 bodies never chosen — sun, moon, mercury, venus, mars, jupiter, saturn, uranus, neptune, pluto, true_node, true_south_node, chiron, mean_lilith
|
| 57 |
+
|
| 58 |
+
**Read plainly:** `a_uranus` is the age gap between the two births measured by Uranus (4.3°/yr); `d_pluto`,
|
| 59 |
+
`d_neptune`, `d_uranus` are the groom's age at the wedding measured by Pluto, Neptune and Uranus; `m_pluto`,
|
| 60 |
+
`m_saturn` are the bride's. The model uses no natal phase, no wedding-sky phase, and no fast body — the study
|
| 61 |
+
(`ARTAMODEL.md`) shows why: those terms are era clocks or noise, and they make the model worse out of time.
|
| 62 |
+
|
| 63 |
+
## What it scores, honestly
|
| 64 |
+
|
| 65 |
+
| on couples born after 1900 (temporal hold-out) | AUC |
|
| 66 |
+
|---|---|
|
| 67 |
+
| this construction, fitted on train alone (its inner temporal split chose 31 stages) | inner 0.6320 · **held-out ≈ 0.62–0.64** |
|
| 68 |
+
| the plain columns — two ages at the wedding, the gap, the start year (LightGBM) | 0.6189–0.6371 depending on the row population |
|
| 69 |
+
| the same model with the two ages held flat (AUC within 3-year age cells) | **≈ 0.50** |
|
| 70 |
+
|
| 71 |
+
The held-out AUC of ArtaModel is real, and every point of it is the two partners' ages at the wedding and the gap
|
| 72 |
+
between their births, read through the outer planets as clocks. It is exactly invariant to the ayanāṁśa (a
|
| 73 |
+
constant offset cancels in a phase difference), to the birth hour and to the birthplace; Uranus alone equals the
|
| 74 |
+
whole model; the Sun alone scores 0.47; and it adds nothing to a plain model of the ages. This card says so because
|
| 75 |
+
the study measured it, from every angle, on fixed populations — see `ARTAMODEL.md` and `artamodel_study.json`.
|
| 76 |
+
|
| 77 |
+
## Use
|
| 78 |
+
|
| 79 |
+
```python
|
| 80 |
+
from artamodel_score import predict # needs: numpy, kerykeion, timezonefinder
|
| 81 |
+
r = predict("1936-08-04", 37.943, 23.647, # dad: dob, lat, lon
|
| 82 |
+
"1924-05-14", 37.727, 26.909, # mom
|
| 83 |
+
"1968-06-15") # wedding date (YYYY-01-01 = year only -> wedding terms dropped)
|
| 84 |
+
r["probability"], r["terms"] # the probability, and the stage-by-stage account
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
`artamodel_deployed.json` holds every stage's weights; `artamodel.py` / `artamodel_ensemble.py` /
|
| 88 |
+
`artamodel_deploy.py` are the fit; `kerykeion_phases.py` the phase extraction. Data:
|
| 89 |
+
[artaquest-foundation/artamatch-sidereal](https://www.kaggle.com/datasets/artaquest-foundation/artamatch-sidereal);
|
| 90 |
+
competition: [artamatch-sidereal](https://www.kaggle.com/competitions/artamatch-sidereal). CC0.
|
artamodel.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
artamodel.py — ArtaModel, canonical implementation.
|
| 3 |
+
|
| 4 |
+
y = | b + Σ_i a_i e^{i(θm_i − θd_i)} synastry, mom − dad
|
| 5 |
+
+ m_i e^{i(θt_i − θm_i)} the wedding sky transiting mom
|
| 6 |
+
+ d_i e^{i(θt_i − θd_i)} the wedding sky transiting dad
|
| 7 |
+
+ mn_i e^{i θm_i} mom's own natal phase
|
| 8 |
+
+ dn_i e^{i θd_i} dad's own natal phase
|
| 9 |
+
+ tn_i e^{i θt_i} the wedding sky's own phase
|
| 10 |
+
+ c_i e^{i θc_i} the COMPOSITE: the shorter-arc midpoint of mom's and dad's natal
|
| 11 |
+
longitudes (Arash, 2026-08-18: "add the mid angle of natals as
|
| 12 |
+
term, like Davison compatibility")
|
| 13 |
+
+ tc_i e^{i(θt_i − θc_i)} |² the wedding sky transiting the composite
|
| 14 |
+
|
| 15 |
+
θ are sidereal longitudes (Kerykeion, Lahiri unless told otherwise): dad and mom at 09:00 local at each birthplace,
|
| 16 |
+
θt the marriage date at 12:00 UT. Every complex weight is held as (Re, Im); the model is a complex-linear map over
|
| 17 |
+
the phasors followed by |·|², a logistic head turns the standardised intensity into a probability for the loss,
|
| 18 |
+
and AUC reads the intensity's ranking. F fields = F copies of the formula combined by the head; F = 1 is the
|
| 19 |
+
formula literally.
|
| 20 |
+
|
| 21 |
+
THE PRESENCE RULE (Arash, 2026-08-18): a term exists only when both of its phases exist. "If wedding is not known,
|
| 22 |
+
drop the last two terms"; "if dob of either is not known, drop the natal term of it". A missing phase is NaN in the
|
| 23 |
+
phase matrix and contributes exactly zero to the sum (cos = sin = 0), so one parameter set serves every row and a
|
| 24 |
+
row is usable as long as it has at least one phasor.
|
| 25 |
+
|
| 26 |
+
TERMS are selectable by name -- "a", "m", "d", "mn", "dn", "tn" -- and BODIES by name, so the study can ablate.
|
| 27 |
+
"""
|
| 28 |
+
import os
|
| 29 |
+
import sys
|
| 30 |
+
|
| 31 |
+
import numpy as np
|
| 32 |
+
|
| 33 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 34 |
+
sys.path.insert(0, os.path.join(HERE, "..", "coherent"))
|
| 35 |
+
from coherent_fit import Coherent, auc # noqa: E402
|
| 36 |
+
|
| 37 |
+
TERMS = ("a", "m", "d", "mn", "dn", "tn")
|
| 38 |
+
TERMS8 = TERMS + ("c", "tc")
|
| 39 |
+
BODIES14 = ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto",
|
| 40 |
+
"true_node", "true_south_node", "chiron", "mean_lilith"]
|
| 41 |
+
ANGLES = ["ascendant", "medium_coeli"]
|
| 42 |
+
GROUPS = {"luminaries": ["sun", "moon"], "inner": ["mercury", "venus", "mars"], "social": ["jupiter", "saturn"],
|
| 43 |
+
"outer": ["uranus", "neptune", "pluto"], "nodes": ["true_node", "true_south_node"],
|
| 44 |
+
"points": ["chiron", "mean_lilith"], "angles": ANGLES}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def composite(D, M):
|
| 48 |
+
"""The shorter-arc midpoint of two longitudes, in degrees on [0, 360): dad + half the wrapped difference.
|
| 49 |
+
(The naive (θm+θd)/2 is ambiguous by 180 degrees whenever the pair straddles 0.)"""
|
| 50 |
+
diff = (M - D + 180.0) % 360.0 - 180.0
|
| 51 |
+
return (D + diff / 2.0) % 360.0
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def phase_matrix(D, M, W, all_bodies, bodies, terms, angles_in_natal=False):
|
| 55 |
+
"""(n, K) phases in degrees (NaN = the term does not exist for that row) and K labels."""
|
| 56 |
+
col = {b: j for j, b in enumerate(all_bodies)}
|
| 57 |
+
P, lab = [], []
|
| 58 |
+
use = list(bodies) + (ANGLES if angles_in_natal else [])
|
| 59 |
+
for t in terms:
|
| 60 |
+
for b in use:
|
| 61 |
+
j = col[b]
|
| 62 |
+
if t == "a":
|
| 63 |
+
P.append(M[:, j] - D[:, j])
|
| 64 |
+
elif t == "c":
|
| 65 |
+
P.append(composite(D[:, j], M[:, j]))
|
| 66 |
+
elif t == "tc":
|
| 67 |
+
if b in ANGLES: continue
|
| 68 |
+
P.append(W[:, j] - composite(D[:, j], M[:, j]))
|
| 69 |
+
elif t == "m":
|
| 70 |
+
if b in ANGLES: continue
|
| 71 |
+
P.append(W[:, j] - M[:, j])
|
| 72 |
+
elif t == "d":
|
| 73 |
+
if b in ANGLES: continue
|
| 74 |
+
P.append(W[:, j] - D[:, j])
|
| 75 |
+
elif t == "mn":
|
| 76 |
+
P.append(M[:, j])
|
| 77 |
+
elif t == "dn":
|
| 78 |
+
P.append(D[:, j])
|
| 79 |
+
elif t == "tn":
|
| 80 |
+
if b in ANGLES: continue
|
| 81 |
+
P.append(W[:, j])
|
| 82 |
+
else:
|
| 83 |
+
raise ValueError(t)
|
| 84 |
+
lab.append(f"{t}_{b}")
|
| 85 |
+
return (np.column_stack(P) if P else np.zeros((len(D), 0))), lab
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class ArtaModel:
|
| 89 |
+
"""One ArtaModel: a term set, a body set, F fields, and the fitted weights."""
|
| 90 |
+
|
| 91 |
+
def __init__(self, terms=TERMS, bodies=BODIES14, F=1, l2=1e-3, angles_in_natal=False, seed=0):
|
| 92 |
+
self.terms, self.bodies, self.F, self.l2, self.angles, self.seed = tuple(terms), list(bodies), F, l2, angles_in_natal, seed
|
| 93 |
+
self.model = None; self.labels = None; self.inner_auc = None; self.epochs = None
|
| 94 |
+
|
| 95 |
+
def _cs(self, P):
|
| 96 |
+
rad = np.pi / 180.0
|
| 97 |
+
return np.nan_to_num(np.cos(P * rad)), np.nan_to_num(np.sin(P * rad))
|
| 98 |
+
|
| 99 |
+
def fit(self, P, y, inner, lr=0.01, epochs=150, patience=15, batch=None):
|
| 100 |
+
"""Early-stopped on `inner` (a boolean mask: the inner temporal validation rows). Returns self.
|
| 101 |
+
|
| 102 |
+
The batch adapts to the population: at least sixteen gradient steps per epoch. A fixed 2,048 gave a
|
| 103 |
+
300-row self-test one step per epoch and 80 steps in all, and the planted phasor was not recovered."""
|
| 104 |
+
C, S = self._cs(P)
|
| 105 |
+
m = Coherent(C.shape[1], self.F, seed=self.seed)
|
| 106 |
+
rng = np.random.default_rng(1000 + self.seed)
|
| 107 |
+
idx = np.where(~inner)[0]
|
| 108 |
+
if batch is None:
|
| 109 |
+
batch = int(min(1024, max(32, len(idx) // 16)))
|
| 110 |
+
best, state, bad, best_ep = -1.0, None, 0, 0
|
| 111 |
+
for ep in range(epochs):
|
| 112 |
+
rng.shuffle(idx)
|
| 113 |
+
for s0 in range(0, len(idx), batch):
|
| 114 |
+
b = idx[s0:s0 + batch]
|
| 115 |
+
if len(b) >= 32:
|
| 116 |
+
m.step(C[b], S[b], y[b].astype(float), lr, self.l2)
|
| 117 |
+
a = auc(y[inner], m.logit(C[inner], S[inner])[0]) if inner.any() else 0.5
|
| 118 |
+
if a > best + 1e-5:
|
| 119 |
+
best, bad, best_ep = a, 0, ep
|
| 120 |
+
state = (m.A1.copy(), m.A2.copy(), m.br.copy(), m.bi.copy(), m.w.copy(), m.c, m.mu.copy(), m.sd.copy())
|
| 121 |
+
else:
|
| 122 |
+
bad += 1
|
| 123 |
+
if bad >= patience:
|
| 124 |
+
break
|
| 125 |
+
m.A1, m.A2, m.br, m.bi, m.w, m.c, m.mu, m.sd = state
|
| 126 |
+
self.model, self.inner_auc, self.epochs = m, best, best_ep
|
| 127 |
+
return self
|
| 128 |
+
|
| 129 |
+
def score(self, P):
|
| 130 |
+
C, S = self._cs(P)
|
| 131 |
+
return self.model.logit(C, S)[0]
|
| 132 |
+
|
| 133 |
+
def intensity(self, P):
|
| 134 |
+
"""|b + Σ w_k z_k|² per field, before the head: (n, F)."""
|
| 135 |
+
C, S = self._cs(P)
|
| 136 |
+
return self.model.fields(C, S)[2]
|
| 137 |
+
|
| 138 |
+
def weights(self, labels):
|
| 139 |
+
"""The fitted complex weights of field 0: {label: (modulus, phase_deg)} plus the bias and head weight."""
|
| 140 |
+
m = self.model
|
| 141 |
+
w = m.A1[0] - 1j * m.A2[0] # A1 = Re w, A2 = -Im w
|
| 142 |
+
out = {lab: (float(abs(w[k])), float(np.degrees(np.angle(w[k])))) for k, lab in enumerate(labels)}
|
| 143 |
+
out["_bias"] = (float(abs(m.br[0] + 1j * m.bi[0])), float(np.degrees(np.angle(m.br[0] + 1j * m.bi[0]))))
|
| 144 |
+
out["_head_w"] = (float(m.w[0]), 0.0)
|
| 145 |
+
return out
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def fit_ensemble(P, y, inner, Pte, seeds=3, **kw):
|
| 149 |
+
"""Mean held-out score over seeds, and the mean inner AUC."""
|
| 150 |
+
outs, ivs = [], []
|
| 151 |
+
for s in range(seeds):
|
| 152 |
+
am = ArtaModel(seed=s, **kw).fit(P, y, inner)
|
| 153 |
+
outs.append(am.score(Pte)); ivs.append(am.inner_auc)
|
| 154 |
+
return float(np.mean(ivs)), np.mean(outs, 0)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
if __name__ == "__main__":
|
| 158 |
+
rng = np.random.default_rng(0)
|
| 159 |
+
n, B = 400, len(BODIES14)
|
| 160 |
+
D = rng.uniform(0, 360, (n, B)); M = rng.uniform(0, 360, (n, B)); W = rng.uniform(0, 360, (n, B))
|
| 161 |
+
P, lab = phase_matrix(D, M, W, BODIES14, BODIES14, TERMS)
|
| 162 |
+
assert P.shape == (n, 6 * B) and len(lab) == 6 * B
|
| 163 |
+
# a planted signal in one phasor must be recovered
|
| 164 |
+
y = (np.cos(np.radians(M[:, 1] - D[:, 1])) + 0.3 * rng.normal(size=n) > 0).astype(int)
|
| 165 |
+
inner = np.arange(n) >= 300
|
| 166 |
+
am = ArtaModel(F=1).fit(P, y, inner)
|
| 167 |
+
w = am.weights(lab)
|
| 168 |
+
top = max((k for k in w if not k.startswith("_")), key=lambda k: w[k][0])
|
| 169 |
+
print(f" {6*B} phasors · planted signal on a_moon · largest fitted weight: {top} (|w|={w[top][0]:.3f}) · inner AUC {am.inner_auc:.3f}")
|
| 170 |
+
assert top == "a_moon", "the fit did not recover the planted phasor"
|
| 171 |
+
# a NaN phase contributes nothing: scores identical whether the column is NaN or absent
|
| 172 |
+
P2 = P.copy(); P2[:, 5] = np.nan
|
| 173 |
+
P3, _ = phase_matrix(D, M, W, BODIES14, [b for b in BODIES14 if b != BODIES14[5]] + [BODIES14[5]], TERMS)
|
| 174 |
+
print(" presence rule: a NaN phase zeroes its phasor (cos=sin=0)")
|
artamodel_deploy.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
artamodel_deploy.py — the deployable ArtaModel: 6-term, boosted over split single-sum fields (one phasor per
|
| 3 |
+
field), every stage recorded, serialised to JSON, and a scorer that needs only numpy.
|
| 4 |
+
|
| 5 |
+
Two fits are made:
|
| 6 |
+
LEADERBOARD fitted on the training rows that have both natal charts (9,553), applied to the 7,249 test rows;
|
| 7 |
+
nothing from the answer key -- this is what goes to Kaggle.
|
| 8 |
+
DEPLOYED the same construction fitted on train + test (Arash: "the deployed model should be trained on all
|
| 9 |
+
the data"), 9,553 + 7,249 rows; this is what goes to Hugging Face and to prod. It cannot be scored
|
| 10 |
+
on the held-out set, because the held-out set is inside it -- the leaderboard fit is its estimate.
|
| 11 |
+
|
| 12 |
+
THE MODEL, term by term (see explain()): for each of fourteen bodies i and each of six terms,
|
| 13 |
+
a_i e^{i(θm_i − θd_i)} mom's longitude minus dad's (synastry)
|
| 14 |
+
m_i e^{i(θt_i − θm_i)} the wedding-day longitude minus mom's (the wedding transiting mom)
|
| 15 |
+
d_i e^{i(θt_i − θd_i)} the wedding-day longitude minus dad's (the wedding transiting dad)
|
| 16 |
+
mn_i e^{i θm_i} mom's natal longitude
|
| 17 |
+
dn_i e^{i θd_i} dad's natal longitude
|
| 18 |
+
tn_i e^{i θt_i} the wedding-day longitude
|
| 19 |
+
θ sidereal (Lahiri) from Kerykeion, births at 09:00 local, the wedding at 12:00 UT. Each stage k of the boosted
|
| 20 |
+
model is ONE single-sum field u_k = |b_k + w_k z_j|² on ONE phasor j, chosen GREEDILY at that stage as the phasor
|
| 21 |
+
that best explains the current residual (so all 84 phasors of all six terms compete every time), added as
|
| 22 |
+
step_k · (α_k · u_k + c_k) to the logit; a term missing for a row (unknown wedding, unknown birth) contributes
|
| 23 |
+
zero. The score is the logit after the last stage; the probability is its sigmoid.
|
| 24 |
+
"""
|
| 25 |
+
import json
|
| 26 |
+
import os
|
| 27 |
+
import sys
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import pandas as pd
|
| 31 |
+
|
| 32 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 33 |
+
sys.path.insert(0, HERE)
|
| 34 |
+
from artamodel import BODIES14, TERMS, phase_matrix # noqa: E402
|
| 35 |
+
from artamodel_ensemble import Field, auc, cs # noqa: E402
|
| 36 |
+
|
| 37 |
+
PH = os.environ.get("AQ_PHASES", "/tmp/aq3feat/phases.npz")
|
| 38 |
+
OUT = os.environ.get("AQ_OUT", "/tmp/aq3sub")
|
| 39 |
+
os.makedirs(OUT, exist_ok=True)
|
| 40 |
+
TERM_TEXT = {"a": "mom's longitude minus dad's — the synastry angle between the two natal charts",
|
| 41 |
+
"m": "the wedding-day longitude minus mom's natal longitude — the wedding sky transiting mom",
|
| 42 |
+
"d": "the wedding-day longitude minus dad's natal longitude — the wedding sky transiting dad",
|
| 43 |
+
"mn": "mom's own natal longitude", "dn": "dad's own natal longitude",
|
| 44 |
+
"tn": "the wedding-day longitude itself"}
|
| 45 |
+
BODY_TEXT = {"sun": "Sun", "moon": "Moon", "mercury": "Mercury", "venus": "Venus", "mars": "Mars", "jupiter": "Jupiter",
|
| 46 |
+
"saturn": "Saturn", "uranus": "Uranus", "neptune": "Neptune", "pluto": "Pluto", "true_node": "Rāhu (true north node)",
|
| 47 |
+
"true_south_node": "Ketu (true south node)", "chiron": "Chiron", "mean_lilith": "Lilith (mean lunar apogee)"}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def pick_phasor(C, S, r, used_counts, K):
|
| 51 |
+
"""GREEDY stage selection: the phasor whose (cos, sin) best explain the current residual by least squares --
|
| 52 |
+
the two-parameter proxy of the single-sum field -- so every term competes at every stage. A fixed cycle
|
| 53 |
+
(stage k -> phasor k) with early stopping at ~37 stages had never even offered the natal and wedding-sky
|
| 54 |
+
phasors, which sit last in the label order; that is not a six-term model."""
|
| 55 |
+
best_j, best_r2 = 0, -1.0
|
| 56 |
+
rc = r - r.mean()
|
| 57 |
+
for j in range(K):
|
| 58 |
+
X = np.column_stack([C[:, j], S[:, j]])
|
| 59 |
+
Xc = X - X.mean(0)
|
| 60 |
+
try:
|
| 61 |
+
beta, *_ = np.linalg.lstsq(Xc, rc, rcond=None)
|
| 62 |
+
except Exception:
|
| 63 |
+
continue
|
| 64 |
+
r2 = 1.0 - float(((rc - Xc @ beta) ** 2).sum()) / max(1e-12, float((rc ** 2).sum()))
|
| 65 |
+
if r2 > best_r2:
|
| 66 |
+
best_r2, best_j = r2, j
|
| 67 |
+
return best_j
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def boost_recorded(P, y, inner, stages, nu=0.1, seed=0):
|
| 71 |
+
"""Gradient boosting over split single-sum fields, ONE phasor per stage chosen greedily against the current
|
| 72 |
+
residual, every stage recorded."""
|
| 73 |
+
C, S = cs(P); K = C.shape[1]; fit = ~inner
|
| 74 |
+
p0 = np.clip(y[fit].mean(), 1e-3, 1 - 1e-3); F0 = float(np.log(p0 / (1 - p0)))
|
| 75 |
+
F = np.full(len(y), F0)
|
| 76 |
+
rec = []; best, best_n, bad = -1.0, 0, 0
|
| 77 |
+
used_counts = np.zeros(K, int)
|
| 78 |
+
for k in range(stages):
|
| 79 |
+
p = 1 / (1 + np.exp(-F)); r = y - p
|
| 80 |
+
j = pick_phasor(C[fit], S[fit], r[fit], used_counts, K); used_counts[j] += 1
|
| 81 |
+
mask = np.zeros(K); mask[j] = 1.0
|
| 82 |
+
f = Field(K, seed * 1000 + k, mask).fit(C[fit], S[fit], r[fit], C[inner], S[inner], r[inner]) if inner.any() \
|
| 83 |
+
else Field(K, seed * 1000 + k, mask).fit(C[fit], S[fit], r[fit], C[fit][:64], S[fit][:64], r[fit][:64])
|
| 84 |
+
h = f.predict(C, S)
|
| 85 |
+
best_g, best_loss = 0.0, np.inf
|
| 86 |
+
for gam in (0.25, 0.5, 1.0, 2.0, 4.0):
|
| 87 |
+
Fx = F[fit] + nu * gam * h[fit]
|
| 88 |
+
loss = float(np.mean(np.logaddexp(0, -Fx * (2 * y[fit] - 1))))
|
| 89 |
+
if loss < best_loss:
|
| 90 |
+
best_loss, best_g = loss, gam
|
| 91 |
+
F = F + nu * best_g * h
|
| 92 |
+
rec.append({"stage": k + 1, "phasor": int(j), "step": nu * best_g, "alpha": float(f.alpha), "c": float(f.c),
|
| 93 |
+
"w_re": float(f.A1[j]), "w_im": float(-f.A2[j]), "b_re": float(f.br), "b_im": float(f.bi)})
|
| 94 |
+
if inner.any():
|
| 95 |
+
a = auc(y[inner], F[inner])
|
| 96 |
+
if a > best + 1e-5:
|
| 97 |
+
best, best_n, bad = a, k + 1, 0
|
| 98 |
+
else:
|
| 99 |
+
bad += 1
|
| 100 |
+
if bad >= 10:
|
| 101 |
+
break
|
| 102 |
+
n_keep = best_n if inner.any() else len(rec)
|
| 103 |
+
return {"F0": F0, "stages": rec[:n_keep], "inner_auc": best if inner.any() else None}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def score(model, P):
|
| 107 |
+
"""The logit of the deployed model for phase matrix P (degrees; NaN = the term is absent). numpy only."""
|
| 108 |
+
rad = np.pi / 180.0
|
| 109 |
+
C, S = np.nan_to_num(np.cos(P * rad)), np.nan_to_num(np.sin(P * rad))
|
| 110 |
+
F = np.full(len(P), model["F0"])
|
| 111 |
+
for st in model["stages"]:
|
| 112 |
+
j = st["phasor"]
|
| 113 |
+
# z = e^{iφ}; w z = (wr + i wi)(C + i S) = (wr C − wi S) + i(wr S + wi C)
|
| 114 |
+
Zr = st["w_re"] * C[:, j] - st["w_im"] * S[:, j] + st["b_re"]
|
| 115 |
+
Zi = st["w_re"] * S[:, j] + st["w_im"] * C[:, j] + st["b_im"]
|
| 116 |
+
u = Zr * Zr + Zi * Zi
|
| 117 |
+
F = F + st["step"] * (st["alpha"] * u + st["c"])
|
| 118 |
+
return F
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def explain(model, labels):
|
| 122 |
+
"""Term-by-term account: every phasor's meaning, whether the deployed model uses it, and with what weight.
|
| 123 |
+
The 'contribution' is Σ_stages step·|alpha|·|w|², the scale of the phasor's swing in the logit."""
|
| 124 |
+
rows = {}
|
| 125 |
+
for st in model["stages"]:
|
| 126 |
+
lab = labels[st["phasor"]]; term, body = lab.split("_", 1)
|
| 127 |
+
w = complex(st["w_re"], st["w_im"]); b = complex(st["b_re"], st["b_im"])
|
| 128 |
+
r = rows.setdefault(lab, {"phasor": lab, "term": term, "body": BODY_TEXT.get(body, body),
|
| 129 |
+
"meaning": f"{TERM_TEXT[term]}, for {BODY_TEXT.get(body, body)}",
|
| 130 |
+
"stages": 0, "contribution": 0.0, "phase_deg": None})
|
| 131 |
+
r["stages"] += 1
|
| 132 |
+
r["contribution"] += abs(st["step"] * st["alpha"]) * abs(w) ** 2
|
| 133 |
+
# the angle at which the field peaks: |b + w e^{iφ}|² is largest when arg(w) + φ = arg(b), i.e. φ* = arg(b) − arg(w)
|
| 134 |
+
r["phase_deg"] = float(np.degrees(np.angle(b) - np.angle(w)) % 360.0)
|
| 135 |
+
used = sorted(rows.values(), key=lambda r: -r["contribution"])
|
| 136 |
+
unused = [lab for lab in labels if lab not in rows]
|
| 137 |
+
return used, unused
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def main():
|
| 141 |
+
Z = np.load(PH, allow_pickle=True)
|
| 142 |
+
bodies = list(Z["bodies"]); ids = Z["id_test"]; y = Z["y_train"].astype(np.int64)
|
| 143 |
+
Dtr, Mtr, Wtr = Z["theta_dad_train"], Z["theta_mom_train"], Z["theta_wed_train"]
|
| 144 |
+
Dte, Mte, Wte = Z["theta_dad_test"], Z["theta_mom_test"], Z["theta_wed_test"]
|
| 145 |
+
ptr, pte, pn = Z["plain_train"], Z["plain_test"], list(Z["plain_names"])
|
| 146 |
+
later = Z["yr_train"].astype(int).max(1)
|
| 147 |
+
j1 = ptr[:, pn.index("start_is_jan1")] == 1.0; j1e = pte[:, pn.index("start_is_jan1")] == 1.0
|
| 148 |
+
Wtr = Wtr.copy(); Wte = Wte.copy(); Wtr[j1] = np.nan; Wte[j1e] = np.nan
|
| 149 |
+
B = [bodies.index(b) for b in BODIES14]
|
| 150 |
+
charts = np.isfinite(Dtr[:, B]).all(1) & np.isfinite(Mtr[:, B]).all(1)
|
| 151 |
+
P, labels = phase_matrix(Dtr, Mtr, Wtr, bodies, BODIES14, TERMS); Pe, _ = phase_matrix(Dte, Mte, Wte, bodies, BODIES14, TERMS)
|
| 152 |
+
K = len(labels)
|
| 153 |
+
# LEADERBOARD fit
|
| 154 |
+
lat = later[charts]; inner = lat > np.quantile(lat, 0.85)
|
| 155 |
+
lb = boost_recorded(P[charts], y[charts], inner, stages=4 * K, nu=0.1)
|
| 156 |
+
s_lb = score(lb, Pe)
|
| 157 |
+
print(f" leaderboard fit: {int(charts.sum()):,} rows · {len(lb['stages'])} stages kept · inner {lb['inner_auc']:.4f}")
|
| 158 |
+
from scipy.stats import rankdata
|
| 159 |
+
r01 = lambda v: (rankdata(v) - 1) / max(1.0, len(v) - 1)
|
| 160 |
+
pd.DataFrame({"id": ids, "lasted_30_years": r01(s_lb)}).to_csv(f"{OUT}/submission_artamodel6.csv", index=False)
|
| 161 |
+
json.dump({**lb, "labels": labels, "fitted_on": "train rows with both natal charts", "n_rows": int(charts.sum())},
|
| 162 |
+
open(f"{OUT}/artamodel_leaderboard.json", "w"), indent=1)
|
| 163 |
+
# DEPLOYED fit: train + test, the number of stages the leaderboard fit chose (no inner split possible)
|
| 164 |
+
sol = pd.read_csv(os.environ.get("AQ_SOL", "/tmp/aq3comp/solution.csv")).set_index("id")
|
| 165 |
+
yte = sol.loc[ids, "lasted_30_years"].to_numpy().astype(int)
|
| 166 |
+
Pall = np.vstack([P[charts], Pe]); yall = np.concatenate([y[charts], yte])
|
| 167 |
+
dep = boost_recorded(Pall, yall, np.zeros(len(yall), bool), stages=len(lb["stages"]), nu=0.1)
|
| 168 |
+
dep.update({"labels": labels, "terms": list(TERMS), "bodies": BODIES14, "fitted_on": "train + test rows with both natal charts",
|
| 169 |
+
"n_rows": int(len(yall)), "leaderboard_estimate": {"inner_auc": lb["inner_auc"], "note": "the same construction fitted on train alone; its held-out AUC is what the leaderboard reports"},
|
| 170 |
+
"phase_convention": {"zodiac": "sidereal, Lahiri", "engine": "Kerykeion 5.12.9 (Swiss Ephemeris)", "birth_time": "09:00 local at the birthplace",
|
| 171 |
+
"wedding_time": "12:00 UT", "presence_rule": "a term exists only when both of its phases exist; a missing phase contributes zero"}})
|
| 172 |
+
used, unused = explain(dep, labels)
|
| 173 |
+
dep["explanation"] = {"used": used, "unused": unused}
|
| 174 |
+
json.dump(dep, open(f"{OUT}/artamodel_deployed.json", "w"), indent=1)
|
| 175 |
+
print(f" deployed fit: {len(yall):,} rows · {len(dep['stages'])} stages · {len(used)} phasors carry weight, {len(unused)} unused")
|
| 176 |
+
print(f" top phasors by contribution:")
|
| 177 |
+
for r in used[:10]:
|
| 178 |
+
print(f" {r['phasor']:<20} {r['contribution']:8.3f} peak at φ={r['phase_deg']:6.1f}° {r['meaning'][:70]}")
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
if __name__ == "__main__":
|
| 182 |
+
main()
|
artamodel_deployed.json
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"F0": -0.2675045808335571,
|
| 3 |
+
"stages": [
|
| 4 |
+
{
|
| 5 |
+
"stage": 1,
|
| 6 |
+
"phasor": 35,
|
| 7 |
+
"step": 0.4,
|
| 8 |
+
"alpha": 0.2962068264262272,
|
| 9 |
+
"c": -0.3250943780296887,
|
| 10 |
+
"w_re": 0.15543886682420152,
|
| 11 |
+
"w_im": -0.29112648554866294,
|
| 12 |
+
"b_re": 0.8612165544831112,
|
| 13 |
+
"b_im": 0.37100357183798066
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"stage": 2,
|
| 17 |
+
"phasor": 36,
|
| 18 |
+
"step": 0.4,
|
| 19 |
+
"alpha": 0.36952438046659153,
|
| 20 |
+
"c": -0.1390818933772614,
|
| 21 |
+
"w_re": -0.3206126549000757,
|
| 22 |
+
"w_im": -0.3732272462576281,
|
| 23 |
+
"b_re": -0.3873964806923474,
|
| 24 |
+
"b_im": -0.28133755975435937
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"stage": 3,
|
| 28 |
+
"phasor": 7,
|
| 29 |
+
"step": 0.4,
|
| 30 |
+
"alpha": 0.511645759863731,
|
| 31 |
+
"c": -0.4866446214859608,
|
| 32 |
+
"w_re": 0.30976678844570504,
|
| 33 |
+
"w_im": 0.17084942474953138,
|
| 34 |
+
"b_re": 0.5722916747341479,
|
| 35 |
+
"b_im": 0.37448527919398444
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"stage": 4,
|
| 39 |
+
"phasor": 36,
|
| 40 |
+
"step": 0.4,
|
| 41 |
+
"alpha": 0.28539553490771224,
|
| 42 |
+
"c": -0.10925294880528959,
|
| 43 |
+
"w_re": -0.4637512051740837,
|
| 44 |
+
"w_im": 0.15968357227678315,
|
| 45 |
+
"b_re": -0.40333485010586123,
|
| 46 |
+
"b_im": 0.20652134764416352
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"stage": 5,
|
| 50 |
+
"phasor": 7,
|
| 51 |
+
"step": 0.4,
|
| 52 |
+
"alpha": 0.34873530136954234,
|
| 53 |
+
"c": -0.41548504275199516,
|
| 54 |
+
"w_re": 0.40474051239376696,
|
| 55 |
+
"w_im": 0.27445452735416326,
|
| 56 |
+
"b_re": 0.5102887827791266,
|
| 57 |
+
"b_im": 0.4315683113812937
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"stage": 6,
|
| 61 |
+
"phasor": 36,
|
| 62 |
+
"step": 0.4,
|
| 63 |
+
"alpha": -0.241037762135293,
|
| 64 |
+
"c": 0.13215887217515404,
|
| 65 |
+
"w_re": 0.21215114680381456,
|
| 66 |
+
"w_im": -0.5421753529848402,
|
| 67 |
+
"b_re": -0.5358308970672356,
|
| 68 |
+
"b_im": 0.6130860946629627
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"stage": 7,
|
| 72 |
+
"phasor": 7,
|
| 73 |
+
"step": 0.4,
|
| 74 |
+
"alpha": -0.3496820407898508,
|
| 75 |
+
"c": 0.06137999056834759,
|
| 76 |
+
"w_re": 0.1380069521851862,
|
| 77 |
+
"w_im": -0.4356930091878805,
|
| 78 |
+
"b_re": -0.4051369005715042,
|
| 79 |
+
"b_im": 0.5966729704744487
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"stage": 8,
|
| 83 |
+
"phasor": 37,
|
| 84 |
+
"step": 0.4,
|
| 85 |
+
"alpha": 0.6073940674491038,
|
| 86 |
+
"c": -0.25189132057139996,
|
| 87 |
+
"w_re": 0.04233981935566459,
|
| 88 |
+
"w_im": 0.5301270057768898,
|
| 89 |
+
"b_re": 0.5919403309774376,
|
| 90 |
+
"b_im": 0.1527753550072731
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"stage": 9,
|
| 94 |
+
"phasor": 35,
|
| 95 |
+
"step": 0.4,
|
| 96 |
+
"alpha": 0.24588611736940158,
|
| 97 |
+
"c": -0.19626978063935527,
|
| 98 |
+
"w_re": 0.23233638472479226,
|
| 99 |
+
"w_im": -0.1560666829354965,
|
| 100 |
+
"b_re": 0.48646135225126724,
|
| 101 |
+
"b_im": 0.6372359640426333
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"stage": 10,
|
| 105 |
+
"phasor": 7,
|
| 106 |
+
"step": 0.4,
|
| 107 |
+
"alpha": 0.2931814249521459,
|
| 108 |
+
"c": -0.3100593466978247,
|
| 109 |
+
"w_re": -0.38412437288784573,
|
| 110 |
+
"w_im": -0.19958758032052099,
|
| 111 |
+
"b_re": -0.5400458194422211,
|
| 112 |
+
"b_im": -0.3722948214410381
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"stage": 11,
|
| 116 |
+
"phasor": 37,
|
| 117 |
+
"step": 0.4,
|
| 118 |
+
"alpha": -0.4582008834794563,
|
| 119 |
+
"c": 0.20831408669194976,
|
| 120 |
+
"w_re": 0.21500649446516568,
|
| 121 |
+
"w_im": 0.3452875759252673,
|
| 122 |
+
"b_re": -0.5950527460559584,
|
| 123 |
+
"b_im": -0.36050393980345646
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"stage": 12,
|
| 127 |
+
"phasor": 7,
|
| 128 |
+
"step": 0.4,
|
| 129 |
+
"alpha": -0.3407943339757083,
|
| 130 |
+
"c": 0.0536989476773817,
|
| 131 |
+
"w_re": 0.42368713327013,
|
| 132 |
+
"w_im": 0.23017860913096239,
|
| 133 |
+
"b_re": -0.4619022763623476,
|
| 134 |
+
"b_im": -0.3595962872606437
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"stage": 13,
|
| 138 |
+
"phasor": 35,
|
| 139 |
+
"step": 0.4,
|
| 140 |
+
"alpha": -0.39081012792802494,
|
| 141 |
+
"c": 0.05701871153903491,
|
| 142 |
+
"w_re": -0.2619927832133936,
|
| 143 |
+
"w_im": 0.37777763453428553,
|
| 144 |
+
"b_re": 0.19643086084436306,
|
| 145 |
+
"b_im": 0.39600846374366316
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"stage": 14,
|
| 149 |
+
"phasor": 7,
|
| 150 |
+
"step": 0.4,
|
| 151 |
+
"alpha": 0.3367529793612754,
|
| 152 |
+
"c": -0.2391218389782931,
|
| 153 |
+
"w_re": -0.22962807417260392,
|
| 154 |
+
"w_im": -0.23915132001253248,
|
| 155 |
+
"b_re": -0.3283221604241518,
|
| 156 |
+
"b_im": -0.44641895137626997
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"stage": 15,
|
| 160 |
+
"phasor": 37,
|
| 161 |
+
"step": 0.4,
|
| 162 |
+
"alpha": 0.32323481489443756,
|
| 163 |
+
"c": -0.15654010469243135,
|
| 164 |
+
"w_re": 0.5242342419912024,
|
| 165 |
+
"w_im": -0.022562349938175484,
|
| 166 |
+
"b_re": 0.21576524294883775,
|
| 167 |
+
"b_im": -0.6266023624179726
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"stage": 16,
|
| 171 |
+
"phasor": 7,
|
| 172 |
+
"step": 0.4,
|
| 173 |
+
"alpha": 0.38246986737507255,
|
| 174 |
+
"c": -0.2147413276234277,
|
| 175 |
+
"w_re": -0.21617966794992996,
|
| 176 |
+
"w_im": -0.22459058106093363,
|
| 177 |
+
"b_re": -0.28516924787727604,
|
| 178 |
+
"b_im": -0.38220503519117693
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
"stage": 17,
|
| 182 |
+
"phasor": 35,
|
| 183 |
+
"step": 0.4,
|
| 184 |
+
"alpha": 0.4874469955022705,
|
| 185 |
+
"c": -0.25982562636618545,
|
| 186 |
+
"w_re": -0.06928813567325691,
|
| 187 |
+
"w_im": 0.08125049006772672,
|
| 188 |
+
"b_re": -0.5474920442041195,
|
| 189 |
+
"b_im": -0.4329639422253049
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"stage": 18,
|
| 193 |
+
"phasor": 7,
|
| 194 |
+
"step": 0.4,
|
| 195 |
+
"alpha": 0.23313309453069142,
|
| 196 |
+
"c": -0.20600477096021788,
|
| 197 |
+
"w_re": -0.3009257213497396,
|
| 198 |
+
"w_im": 0.13069129184720474,
|
| 199 |
+
"b_re": -0.6326839574295885,
|
| 200 |
+
"b_im": 0.17694929657938274
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"stage": 19,
|
| 204 |
+
"phasor": 37,
|
| 205 |
+
"step": 0.4,
|
| 206 |
+
"alpha": 0.28730901018115956,
|
| 207 |
+
"c": -0.15005369280276284,
|
| 208 |
+
"w_re": 0.03957746462383685,
|
| 209 |
+
"w_im": 0.3723175342063423,
|
| 210 |
+
"b_re": 0.6410602230773064,
|
| 211 |
+
"b_im": 0.28062304657742676
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"stage": 20,
|
| 215 |
+
"phasor": 7,
|
| 216 |
+
"step": 0.4,
|
| 217 |
+
"alpha": 0.28519353933712877,
|
| 218 |
+
"c": -0.17451424341084584,
|
| 219 |
+
"w_re": -0.2908225000410197,
|
| 220 |
+
"w_im": -0.08622435303219662,
|
| 221 |
+
"b_re": -0.4734675098521598,
|
| 222 |
+
"b_im": -0.21330737439902567
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"stage": 21,
|
| 226 |
+
"phasor": 37,
|
| 227 |
+
"step": 0.4,
|
| 228 |
+
"alpha": 0.2970907363736211,
|
| 229 |
+
"c": -0.1178773969954637,
|
| 230 |
+
"w_re": -0.3989069827974416,
|
| 231 |
+
"w_im": -0.07529477574269595,
|
| 232 |
+
"b_re": -0.35175675120207067,
|
| 233 |
+
"b_im": 0.49740270529990493
|
| 234 |
+
},
|
| 235 |
+
{
|
| 236 |
+
"stage": 22,
|
| 237 |
+
"phasor": 7,
|
| 238 |
+
"step": 0.4,
|
| 239 |
+
"alpha": 0.37066569007541217,
|
| 240 |
+
"c": -0.16185980179000573,
|
| 241 |
+
"w_re": -0.22809406940524715,
|
| 242 |
+
"w_im": -0.11886525281603842,
|
| 243 |
+
"b_re": -0.35322820761390417,
|
| 244 |
+
"b_im": -0.2570646508447083
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"stage": 23,
|
| 248 |
+
"phasor": 23,
|
| 249 |
+
"step": 0.4,
|
| 250 |
+
"alpha": -0.5451648207088828,
|
| 251 |
+
"c": 0.282245459221117,
|
| 252 |
+
"w_re": 0.047159568889624894,
|
| 253 |
+
"w_im": -0.2040079448298226,
|
| 254 |
+
"b_re": 0.3965514578657553,
|
| 255 |
+
"b_im": 0.6069872253964642
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"stage": 24,
|
| 259 |
+
"phasor": 7,
|
| 260 |
+
"step": 0.4,
|
| 261 |
+
"alpha": -0.25539641704723604,
|
| 262 |
+
"c": 0.02396375535599597,
|
| 263 |
+
"w_re": 0.32977133588371066,
|
| 264 |
+
"w_im": 0.08851392344856994,
|
| 265 |
+
"b_re": -0.4572718425152726,
|
| 266 |
+
"b_im": -0.2897221423013389
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"stage": 25,
|
| 270 |
+
"phasor": 23,
|
| 271 |
+
"step": 0.025,
|
| 272 |
+
"alpha": 0.008226382235465172,
|
| 273 |
+
"c": 0.0013692050922035478,
|
| 274 |
+
"w_re": -0.03428460210809726,
|
| 275 |
+
"w_im": 0.010812468399711284,
|
| 276 |
+
"b_re": -0.06326699564312806,
|
| 277 |
+
"b_im": -0.009243185562406862
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"stage": 26,
|
| 281 |
+
"phasor": 23,
|
| 282 |
+
"step": 0.4,
|
| 283 |
+
"alpha": -0.7420161660730024,
|
| 284 |
+
"c": 0.23764815749766757,
|
| 285 |
+
"w_re": 0.03982089300799257,
|
| 286 |
+
"w_im": 0.17240512070753275,
|
| 287 |
+
"b_re": -0.4892201750994508,
|
| 288 |
+
"b_im": -0.2952743889425063
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
"stage": 27,
|
| 292 |
+
"phasor": 7,
|
| 293 |
+
"step": 0.4,
|
| 294 |
+
"alpha": -0.14598679559107128,
|
| 295 |
+
"c": 0.031156280222117184,
|
| 296 |
+
"w_re": 0.36130904971699535,
|
| 297 |
+
"w_im": -0.010970602787876394,
|
| 298 |
+
"b_re": -0.726254710331812,
|
| 299 |
+
"b_im": -0.13817064498228462
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"stage": 28,
|
| 303 |
+
"phasor": 20,
|
| 304 |
+
"step": 0.025,
|
| 305 |
+
"alpha": -0.02300750361080351,
|
| 306 |
+
"c": 0.004733909131491805,
|
| 307 |
+
"w_re": 0.0585347629743131,
|
| 308 |
+
"w_im": -0.015347370541884548,
|
| 309 |
+
"b_re": -0.044904152761356585,
|
| 310 |
+
"b_im": 0.23175420943107453
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
"stage": 29,
|
| 314 |
+
"phasor": 20,
|
| 315 |
+
"step": 0.025,
|
| 316 |
+
"alpha": -0.012219946073628111,
|
| 317 |
+
"c": 0.0034756243389614753,
|
| 318 |
+
"w_re": 0.05050906649153878,
|
| 319 |
+
"w_im": 0.0022392988832080317,
|
| 320 |
+
"b_re": 0.06080097497476472,
|
| 321 |
+
"b_im": 0.29849834249303925
|
| 322 |
+
},
|
| 323 |
+
{
|
| 324 |
+
"stage": 30,
|
| 325 |
+
"phasor": 20,
|
| 326 |
+
"step": 0.4,
|
| 327 |
+
"alpha": -0.07245884705741887,
|
| 328 |
+
"c": 0.007937468536585873,
|
| 329 |
+
"w_re": 0.08686901653288612,
|
| 330 |
+
"w_im": -0.11891548353411593,
|
| 331 |
+
"b_re": 0.18705320187968782,
|
| 332 |
+
"b_im": 0.26558986783495725
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"stage": 31,
|
| 336 |
+
"phasor": 20,
|
| 337 |
+
"step": 0.025,
|
| 338 |
+
"alpha": -0.013620614511163988,
|
| 339 |
+
"c": 0.002195845127379097,
|
| 340 |
+
"w_re": 0.03885121657478746,
|
| 341 |
+
"w_im": 0.04417276467132002,
|
| 342 |
+
"b_re": -0.16787260736405163,
|
| 343 |
+
"b_im": 0.09081694318060904
|
| 344 |
+
}
|
| 345 |
+
],
|
| 346 |
+
"inner_auc": null,
|
| 347 |
+
"labels": [
|
| 348 |
+
"a_sun",
|
| 349 |
+
"a_moon",
|
| 350 |
+
"a_mercury",
|
| 351 |
+
"a_venus",
|
| 352 |
+
"a_mars",
|
| 353 |
+
"a_jupiter",
|
| 354 |
+
"a_saturn",
|
| 355 |
+
"a_uranus",
|
| 356 |
+
"a_neptune",
|
| 357 |
+
"a_pluto",
|
| 358 |
+
"a_true_node",
|
| 359 |
+
"a_true_south_node",
|
| 360 |
+
"a_chiron",
|
| 361 |
+
"a_mean_lilith",
|
| 362 |
+
"m_sun",
|
| 363 |
+
"m_moon",
|
| 364 |
+
"m_mercury",
|
| 365 |
+
"m_venus",
|
| 366 |
+
"m_mars",
|
| 367 |
+
"m_jupiter",
|
| 368 |
+
"m_saturn",
|
| 369 |
+
"m_uranus",
|
| 370 |
+
"m_neptune",
|
| 371 |
+
"m_pluto",
|
| 372 |
+
"m_true_node",
|
| 373 |
+
"m_true_south_node",
|
| 374 |
+
"m_chiron",
|
| 375 |
+
"m_mean_lilith",
|
| 376 |
+
"d_sun",
|
| 377 |
+
"d_moon",
|
| 378 |
+
"d_mercury",
|
| 379 |
+
"d_venus",
|
| 380 |
+
"d_mars",
|
| 381 |
+
"d_jupiter",
|
| 382 |
+
"d_saturn",
|
| 383 |
+
"d_uranus",
|
| 384 |
+
"d_neptune",
|
| 385 |
+
"d_pluto",
|
| 386 |
+
"d_true_node",
|
| 387 |
+
"d_true_south_node",
|
| 388 |
+
"d_chiron",
|
| 389 |
+
"d_mean_lilith",
|
| 390 |
+
"mn_sun",
|
| 391 |
+
"mn_moon",
|
| 392 |
+
"mn_mercury",
|
| 393 |
+
"mn_venus",
|
| 394 |
+
"mn_mars",
|
| 395 |
+
"mn_jupiter",
|
| 396 |
+
"mn_saturn",
|
| 397 |
+
"mn_uranus",
|
| 398 |
+
"mn_neptune",
|
| 399 |
+
"mn_pluto",
|
| 400 |
+
"mn_true_node",
|
| 401 |
+
"mn_true_south_node",
|
| 402 |
+
"mn_chiron",
|
| 403 |
+
"mn_mean_lilith",
|
| 404 |
+
"dn_sun",
|
| 405 |
+
"dn_moon",
|
| 406 |
+
"dn_mercury",
|
| 407 |
+
"dn_venus",
|
| 408 |
+
"dn_mars",
|
| 409 |
+
"dn_jupiter",
|
| 410 |
+
"dn_saturn",
|
| 411 |
+
"dn_uranus",
|
| 412 |
+
"dn_neptune",
|
| 413 |
+
"dn_pluto",
|
| 414 |
+
"dn_true_node",
|
| 415 |
+
"dn_true_south_node",
|
| 416 |
+
"dn_chiron",
|
| 417 |
+
"dn_mean_lilith",
|
| 418 |
+
"tn_sun",
|
| 419 |
+
"tn_moon",
|
| 420 |
+
"tn_mercury",
|
| 421 |
+
"tn_venus",
|
| 422 |
+
"tn_mars",
|
| 423 |
+
"tn_jupiter",
|
| 424 |
+
"tn_saturn",
|
| 425 |
+
"tn_uranus",
|
| 426 |
+
"tn_neptune",
|
| 427 |
+
"tn_pluto",
|
| 428 |
+
"tn_true_node",
|
| 429 |
+
"tn_true_south_node",
|
| 430 |
+
"tn_chiron",
|
| 431 |
+
"tn_mean_lilith"
|
| 432 |
+
],
|
| 433 |
+
"terms": [
|
| 434 |
+
"a",
|
| 435 |
+
"m",
|
| 436 |
+
"d",
|
| 437 |
+
"mn",
|
| 438 |
+
"dn",
|
| 439 |
+
"tn"
|
| 440 |
+
],
|
| 441 |
+
"bodies": [
|
| 442 |
+
"sun",
|
| 443 |
+
"moon",
|
| 444 |
+
"mercury",
|
| 445 |
+
"venus",
|
| 446 |
+
"mars",
|
| 447 |
+
"jupiter",
|
| 448 |
+
"saturn",
|
| 449 |
+
"uranus",
|
| 450 |
+
"neptune",
|
| 451 |
+
"pluto",
|
| 452 |
+
"true_node",
|
| 453 |
+
"true_south_node",
|
| 454 |
+
"chiron",
|
| 455 |
+
"mean_lilith"
|
| 456 |
+
],
|
| 457 |
+
"fitted_on": "train + test rows with both natal charts",
|
| 458 |
+
"n_rows": 16802,
|
| 459 |
+
"leaderboard_estimate": {
|
| 460 |
+
"inner_auc": 0.631962843164345,
|
| 461 |
+
"note": "the same construction fitted on train alone; its held-out AUC is what the leaderboard reports"
|
| 462 |
+
},
|
| 463 |
+
"phase_convention": {
|
| 464 |
+
"zodiac": "sidereal, Lahiri",
|
| 465 |
+
"engine": "Kerykeion 5.12.9 (Swiss Ephemeris)",
|
| 466 |
+
"birth_time": "09:00 local at the birthplace",
|
| 467 |
+
"wedding_time": "12:00 UT",
|
| 468 |
+
"presence_rule": "a term exists only when both of its phases exist; a missing phase contributes zero"
|
| 469 |
+
},
|
| 470 |
+
"explanation": {
|
| 471 |
+
"used": [
|
| 472 |
+
{
|
| 473 |
+
"phasor": "a_uranus",
|
| 474 |
+
"term": "a",
|
| 475 |
+
"body": "Uranus",
|
| 476 |
+
"meaning": "mom's longitude minus dad's \u2014 the synastry angle between the two natal charts, for Uranus",
|
| 477 |
+
"stages": 12,
|
| 478 |
+
"contribution": 0.2214107340167794,
|
| 479 |
+
"phase_deg": 192.5110089197106
|
| 480 |
+
},
|
| 481 |
+
{
|
| 482 |
+
"phasor": "d_pluto",
|
| 483 |
+
"term": "d",
|
| 484 |
+
"body": "Pluto",
|
| 485 |
+
"meaning": "the wedding-day longitude minus dad's natal longitude \u2014 the wedding sky transiting dad, for Pluto",
|
| 486 |
+
"stages": 5,
|
| 487 |
+
"contribution": 0.17033211706971024,
|
| 488 |
+
"phase_deg": 294.57847845844117
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"phasor": "d_neptune",
|
| 492 |
+
"term": "d",
|
| 493 |
+
"body": "Neptune",
|
| 494 |
+
"meaning": "the wedding-day longitude minus dad's natal longitude \u2014 the wedding sky transiting dad, for Neptune",
|
| 495 |
+
"stages": 3,
|
| 496 |
+
"contribution": 0.09592685587208412,
|
| 497 |
+
"phase_deg": 199.78290531086967
|
| 498 |
+
},
|
| 499 |
+
{
|
| 500 |
+
"phasor": "d_uranus",
|
| 501 |
+
"term": "d",
|
| 502 |
+
"body": "Uranus",
|
| 503 |
+
"meaning": "the wedding-day longitude minus dad's natal longitude \u2014 the wedding sky transiting dad, for Uranus",
|
| 504 |
+
"stages": 4,
|
| 505 |
+
"contribution": 0.05587273722088104,
|
| 506 |
+
"phase_deg": 87.88080007836476
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
"phasor": "m_pluto",
|
| 510 |
+
"term": "m",
|
| 511 |
+
"body": "Pluto",
|
| 512 |
+
"meaning": "the wedding-day longitude minus mom's natal longitude \u2014 the wedding sky transiting mom, for Pluto",
|
| 513 |
+
"stages": 3,
|
| 514 |
+
"contribution": 0.018853770064656265,
|
| 515 |
+
"phase_deg": 134.11924787097027
|
| 516 |
+
},
|
| 517 |
+
{
|
| 518 |
+
"phasor": "m_saturn",
|
| 519 |
+
"term": "m",
|
| 520 |
+
"body": "Saturn",
|
| 521 |
+
"meaning": "the wedding-day longitude minus mom's natal longitude \u2014 the wedding sky transiting mom, for Saturn",
|
| 522 |
+
"stages": 4,
|
| 523 |
+
"contribution": 0.0006326350047239863,
|
| 524 |
+
"phase_deg": 102.91973323230647
|
| 525 |
+
}
|
| 526 |
+
],
|
| 527 |
+
"unused": [
|
| 528 |
+
"a_sun",
|
| 529 |
+
"a_moon",
|
| 530 |
+
"a_mercury",
|
| 531 |
+
"a_venus",
|
| 532 |
+
"a_mars",
|
| 533 |
+
"a_jupiter",
|
| 534 |
+
"a_saturn",
|
| 535 |
+
"a_neptune",
|
| 536 |
+
"a_pluto",
|
| 537 |
+
"a_true_node",
|
| 538 |
+
"a_true_south_node",
|
| 539 |
+
"a_chiron",
|
| 540 |
+
"a_mean_lilith",
|
| 541 |
+
"m_sun",
|
| 542 |
+
"m_moon",
|
| 543 |
+
"m_mercury",
|
| 544 |
+
"m_venus",
|
| 545 |
+
"m_mars",
|
| 546 |
+
"m_jupiter",
|
| 547 |
+
"m_uranus",
|
| 548 |
+
"m_neptune",
|
| 549 |
+
"m_true_node",
|
| 550 |
+
"m_true_south_node",
|
| 551 |
+
"m_chiron",
|
| 552 |
+
"m_mean_lilith",
|
| 553 |
+
"d_sun",
|
| 554 |
+
"d_moon",
|
| 555 |
+
"d_mercury",
|
| 556 |
+
"d_venus",
|
| 557 |
+
"d_mars",
|
| 558 |
+
"d_jupiter",
|
| 559 |
+
"d_saturn",
|
| 560 |
+
"d_true_node",
|
| 561 |
+
"d_true_south_node",
|
| 562 |
+
"d_chiron",
|
| 563 |
+
"d_mean_lilith",
|
| 564 |
+
"mn_sun",
|
| 565 |
+
"mn_moon",
|
| 566 |
+
"mn_mercury",
|
| 567 |
+
"mn_venus",
|
| 568 |
+
"mn_mars",
|
| 569 |
+
"mn_jupiter",
|
| 570 |
+
"mn_saturn",
|
| 571 |
+
"mn_uranus",
|
| 572 |
+
"mn_neptune",
|
| 573 |
+
"mn_pluto",
|
| 574 |
+
"mn_true_node",
|
| 575 |
+
"mn_true_south_node",
|
| 576 |
+
"mn_chiron",
|
| 577 |
+
"mn_mean_lilith",
|
| 578 |
+
"dn_sun",
|
| 579 |
+
"dn_moon",
|
| 580 |
+
"dn_mercury",
|
| 581 |
+
"dn_venus",
|
| 582 |
+
"dn_mars",
|
| 583 |
+
"dn_jupiter",
|
| 584 |
+
"dn_saturn",
|
| 585 |
+
"dn_uranus",
|
| 586 |
+
"dn_neptune",
|
| 587 |
+
"dn_pluto",
|
| 588 |
+
"dn_true_node",
|
| 589 |
+
"dn_true_south_node",
|
| 590 |
+
"dn_chiron",
|
| 591 |
+
"dn_mean_lilith",
|
| 592 |
+
"tn_sun",
|
| 593 |
+
"tn_moon",
|
| 594 |
+
"tn_mercury",
|
| 595 |
+
"tn_venus",
|
| 596 |
+
"tn_mars",
|
| 597 |
+
"tn_jupiter",
|
| 598 |
+
"tn_saturn",
|
| 599 |
+
"tn_uranus",
|
| 600 |
+
"tn_neptune",
|
| 601 |
+
"tn_pluto",
|
| 602 |
+
"tn_true_node",
|
| 603 |
+
"tn_true_south_node",
|
| 604 |
+
"tn_chiron",
|
| 605 |
+
"tn_mean_lilith"
|
| 606 |
+
]
|
| 607 |
+
}
|
| 608 |
+
}
|
artamodel_ensemble.json
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"3-term single ArtaModel F=1": {
|
| 3 |
+
"held": 0.621164730673447,
|
| 4 |
+
"age_cell_matched": 0.4944578313253012,
|
| 5 |
+
"inner": 0.6442
|
| 6 |
+
},
|
| 7 |
+
"3-term BAG 25 bootstraps (rank-average)": {
|
| 8 |
+
"held": 0.6307294659909557,
|
| 9 |
+
"age_cell_matched": 0.4977037562012757,
|
| 10 |
+
"inner_mean": 0.6313
|
| 11 |
+
},
|
| 12 |
+
"3-term BOOST single-sum fields nu=0.05": {
|
| 13 |
+
"held": 0.631740420171482,
|
| 14 |
+
"age_cell_matched": 0.5033309709425939,
|
| 15 |
+
"inner": 0.6519,
|
| 16 |
+
"stages": 2
|
| 17 |
+
},
|
| 18 |
+
"3-term BOOST single-sum fields nu=0.1": {
|
| 19 |
+
"held": 0.6317839582498853,
|
| 20 |
+
"age_cell_matched": 0.5035294117647059,
|
| 21 |
+
"inner": 0.6521,
|
| 22 |
+
"stages": 2
|
| 23 |
+
},
|
| 24 |
+
"3-term BOOST single-sum fields nu=0.3": {
|
| 25 |
+
"held": 0.6318925131920378,
|
| 26 |
+
"age_cell_matched": 0.5050885896527285,
|
| 27 |
+
"inner": 0.6509,
|
| 28 |
+
"stages": 2
|
| 29 |
+
},
|
| 30 |
+
"3-term SPLIT per body (14 single sums), linear head": {
|
| 31 |
+
"held": 0.6351961825812856,
|
| 32 |
+
"age_cell_matched": 0.5039829907866761,
|
| 33 |
+
"inner": 0.6486
|
| 34 |
+
},
|
| 35 |
+
"3-term SPLIT per body -> LightGBM on the 14 intensities": {
|
| 36 |
+
"held": 0.6255905214700778,
|
| 37 |
+
"age_cell_matched": 0.4813607370659107
|
| 38 |
+
},
|
| 39 |
+
"3-term SPLIT per body -> LightGBM on intensities + plain columns": {
|
| 40 |
+
"held": 0.6212007221515938,
|
| 41 |
+
"age_cell_matched": 0.4757193479801559
|
| 42 |
+
},
|
| 43 |
+
"3-term BOOST over SPLIT per body (stage k -> group k)": {
|
| 44 |
+
"held": 0.6329722575364414,
|
| 45 |
+
"age_cell_matched": 0.4968674698795181,
|
| 46 |
+
"inner": 0.6474,
|
| 47 |
+
"stages": 8
|
| 48 |
+
},
|
| 49 |
+
"3-term SPLIT per term (3 single sums), linear head": {
|
| 50 |
+
"held": 0.6183614589319819,
|
| 51 |
+
"age_cell_matched": 0.4951381998582566,
|
| 52 |
+
"inner": 0.6484
|
| 53 |
+
},
|
| 54 |
+
"3-term SPLIT per term -> LightGBM on the 3 intensities": {
|
| 55 |
+
"held": 0.6181725036717113,
|
| 56 |
+
"age_cell_matched": 0.5008929836995039
|
| 57 |
+
},
|
| 58 |
+
"3-term SPLIT per term -> LightGBM on intensities + plain columns": {
|
| 59 |
+
"held": 0.6203479563225998,
|
| 60 |
+
"age_cell_matched": 0.48731396172927
|
| 61 |
+
},
|
| 62 |
+
"3-term BOOST over SPLIT per term (stage k -> group k)": {
|
| 63 |
+
"held": 0.6335063246315227,
|
| 64 |
+
"age_cell_matched": 0.49992912827781716,
|
| 65 |
+
"inner": 0.6463,
|
| 66 |
+
"stages": 5
|
| 67 |
+
},
|
| 68 |
+
"3-term SPLIT per phasor (42 single sums), linear head": {
|
| 69 |
+
"held": 0.6407121668611367,
|
| 70 |
+
"age_cell_matched": 0.5141318214032601,
|
| 71 |
+
"inner": 0.6503
|
| 72 |
+
},
|
| 73 |
+
"3-term SPLIT per phasor -> LightGBM on the 42 intensities": {
|
| 74 |
+
"held": 0.6297069016561885,
|
| 75 |
+
"age_cell_matched": 0.506392629340893
|
| 76 |
+
},
|
| 77 |
+
"3-term SPLIT per phasor -> LightGBM on intensities + plain columns": {
|
| 78 |
+
"held": 0.626497274516292,
|
| 79 |
+
"age_cell_matched": 0.4992487597448618
|
| 80 |
+
},
|
| 81 |
+
"3-term BOOST over SPLIT per phasor (stage k -> group k)": {
|
| 82 |
+
"held": 0.6388313218741111,
|
| 83 |
+
"age_cell_matched": 0.5225797306874557,
|
| 84 |
+
"inner": 0.6385,
|
| 85 |
+
"stages": 36
|
| 86 |
+
},
|
| 87 |
+
"6-term single ArtaModel F=1": {
|
| 88 |
+
"held": 0.5807439786837568,
|
| 89 |
+
"age_cell_matched": 0.5120056697377746,
|
| 90 |
+
"inner": 0.5655
|
| 91 |
+
},
|
| 92 |
+
"6-term BAG 25 bootstraps (rank-average)": {
|
| 93 |
+
"held": 0.6020613828854716,
|
| 94 |
+
"age_cell_matched": 0.4871438695960312,
|
| 95 |
+
"inner_mean": 0.5876
|
| 96 |
+
},
|
| 97 |
+
"6-term BOOST single-sum fields nu=0.05": {
|
| 98 |
+
"held": 0.5964200089398187,
|
| 99 |
+
"age_cell_matched": 0.4888164422395464,
|
| 100 |
+
"inner": 0.6332,
|
| 101 |
+
"stages": 47
|
| 102 |
+
},
|
| 103 |
+
"6-term BOOST single-sum fields nu=0.1": {
|
| 104 |
+
"held": 0.5991878697108491,
|
| 105 |
+
"age_cell_matched": 0.5050885896527285,
|
| 106 |
+
"inner": 0.6426,
|
| 107 |
+
"stages": 41
|
| 108 |
+
},
|
| 109 |
+
"6-term BOOST single-sum fields nu=0.3": {
|
| 110 |
+
"held": 0.5904163981818499,
|
| 111 |
+
"age_cell_matched": 0.4670446491849752,
|
| 112 |
+
"inner": 0.6599,
|
| 113 |
+
"stages": 47
|
| 114 |
+
},
|
| 115 |
+
"6-term SPLIT per body (14 single sums), linear head": {
|
| 116 |
+
"held": 0.6146363409437894,
|
| 117 |
+
"age_cell_matched": 0.49250177179305454,
|
| 118 |
+
"inner": 0.6461
|
| 119 |
+
},
|
| 120 |
+
"6-term SPLIT per body -> LightGBM on the 14 intensities": {
|
| 121 |
+
"held": 0.6027858565101037,
|
| 122 |
+
"age_cell_matched": 0.4847909284195606
|
| 123 |
+
},
|
| 124 |
+
"6-term SPLIT per body -> LightGBM on intensities + plain columns": {
|
| 125 |
+
"held": 0.6176416293690462,
|
| 126 |
+
"age_cell_matched": 0.48022678951098513
|
| 127 |
+
},
|
| 128 |
+
"6-term BOOST over SPLIT per body (stage k -> group k)": {
|
| 129 |
+
"held": 0.6300691384685045,
|
| 130 |
+
"age_cell_matched": 0.4945428773919206,
|
| 131 |
+
"inner": 0.6547,
|
| 132 |
+
"stages": 21
|
| 133 |
+
},
|
| 134 |
+
"6-term SPLIT per term (6 single sums), linear head": {
|
| 135 |
+
"held": 0.617382142421762,
|
| 136 |
+
"age_cell_matched": 0.4898086463501063,
|
| 137 |
+
"inner": 0.6499
|
| 138 |
+
},
|
| 139 |
+
"6-term SPLIT per term -> LightGBM on the 6 intensities": {
|
| 140 |
+
"held": 0.6170651852109855,
|
| 141 |
+
"age_cell_matched": 0.4919631467044649
|
| 142 |
+
},
|
| 143 |
+
"6-term SPLIT per term -> LightGBM on intensities + plain columns": {
|
| 144 |
+
"held": 0.6190958011877188,
|
| 145 |
+
"age_cell_matched": 0.4944578313253012
|
| 146 |
+
},
|
| 147 |
+
"6-term BOOST over SPLIT per term (stage k -> group k)": {
|
| 148 |
+
"held": 0.6311959039375838,
|
| 149 |
+
"age_cell_matched": 0.49593196314670446,
|
| 150 |
+
"inner": 0.6484,
|
| 151 |
+
"stages": 24
|
| 152 |
+
},
|
| 153 |
+
"6-term SPLIT per phasor (84 single sums), linear head": {
|
| 154 |
+
"held": 0.6108618798000731,
|
| 155 |
+
"age_cell_matched": 0.4908008504606662,
|
| 156 |
+
"inner": 0.635
|
| 157 |
+
},
|
| 158 |
+
"6-term SPLIT per phasor -> LightGBM on the 84 intensities": {
|
| 159 |
+
"held": 0.6084115567475314,
|
| 160 |
+
"age_cell_matched": 0.48289156626506025
|
| 161 |
+
},
|
| 162 |
+
"6-term SPLIT per phasor -> LightGBM on intensities + plain columns": {
|
| 163 |
+
"held": 0.6225875550756692,
|
| 164 |
+
"age_cell_matched": 0.4939759036144578
|
| 165 |
+
},
|
| 166 |
+
"6-term BOOST over SPLIT per phasor (stage k -> group k)": {
|
| 167 |
+
"held": 0.6400434219768609,
|
| 168 |
+
"age_cell_matched": 0.5133380581148121,
|
| 169 |
+
"inner": 0.6359,
|
| 170 |
+
"stages": 40
|
| 171 |
+
},
|
| 172 |
+
"REFERENCE plain columns (two ages, gap, start year), LightGBM": {
|
| 173 |
+
"held": 0.6331707911739608,
|
| 174 |
+
"age_cell_matched": 0.5098086463501063
|
| 175 |
+
}
|
| 176 |
+
}
|
artamodel_ensemble.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
artamodel_ensemble.py — ArtaModel as ensembles, as boosting, and as SPLIT single-sum models.
|
| 3 |
+
|
| 4 |
+
Arash, 2026-08-18: "use ensembles and boosting techniques and split multiple single sum model".
|
| 5 |
+
|
| 6 |
+
BAG many ArtaModels (F=1, the literal formula), each on a bootstrap of the rows with its own seed; the
|
| 7 |
+
held-out scores rank-averaged. Variance reduction for the one-field fit.
|
| 8 |
+
BOOST functional gradient boosting whose weak learner is ONE single-sum field |b + Σ_j w_j z_j|²: at each
|
| 9 |
+
stage the field is fitted by least squares to the current pseudo-residuals of the logistic loss
|
| 10 |
+
(r = y − p), scaled by a line-searched coefficient, shrunk by ν, and added; early-stopped on the inner
|
| 11 |
+
temporal split. Each stage is a complete ArtaModel-shaped sum; the model is a sum of K such squares.
|
| 12 |
+
SPLIT the phasors partitioned into groups G_k -- per BODY (each body's own terms), per TERM TYPE (all bodies
|
| 13 |
+
of one term), or per body×term (single phasors) -- and every group given its OWN single-sum field whose
|
| 14 |
+
weights are masked to the group. The K intensities are combined (i) by a linear head (the coherent
|
| 15 |
+
model with a block-diagonal weight matrix), and (ii) by LightGBM on the K intensities -- boosting over
|
| 16 |
+
the split single-sum models -- with the plain columns optionally beside them.
|
| 17 |
+
|
| 18 |
+
Everything on the FULL population unless stated (both natal charts + the wedding day known: 6,258 train / 2,635
|
| 19 |
+
held out), the same protocol as the study: choices on the inner temporal split, held out read for the record,
|
| 20 |
+
the age-cell-matched control beside every headline.
|
| 21 |
+
|
| 22 |
+
Usage: AQ_PHASES=/tmp/aq3feat/phases.npz AQ_SOL=/tmp/aq3comp/solution.csv AQ_OUT=/tmp/aq3feat python artamodel_ensemble.py
|
| 23 |
+
"""
|
| 24 |
+
import json
|
| 25 |
+
import os
|
| 26 |
+
import sys
|
| 27 |
+
import time
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import pandas as pd
|
| 31 |
+
from scipy.stats import rankdata
|
| 32 |
+
|
| 33 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 34 |
+
sys.path.insert(0, HERE)
|
| 35 |
+
sys.path.insert(0, os.path.join(HERE, "..", "coherent"))
|
| 36 |
+
from artamodel import ANGLES, BODIES14, TERMS, ArtaModel, auc, phase_matrix # noqa: E402
|
| 37 |
+
from coherent_fit import Coherent # noqa: E402
|
| 38 |
+
|
| 39 |
+
PH = os.environ.get("AQ_PHASES", "/tmp/aq3feat/phases.npz")
|
| 40 |
+
SOL = os.environ.get("AQ_SOL", "/tmp/aq3comp/solution.csv")
|
| 41 |
+
OUT = os.environ.get("AQ_OUT", "/tmp/aq3feat")
|
| 42 |
+
T0 = time.time()
|
| 43 |
+
R = {}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def log(*a):
|
| 47 |
+
print(f"[{time.time()-T0:6.0f}s]", *a, flush=True)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def r01(v):
|
| 51 |
+
r = rankdata(v); return (r - 1) / max(1.0, len(r) - 1)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def cs(P):
|
| 55 |
+
rad = np.pi / 180.0
|
| 56 |
+
return np.nan_to_num(np.cos(P * rad)), np.nan_to_num(np.sin(P * rad))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def matched(y, s, cell):
|
| 60 |
+
num = den = 0.0
|
| 61 |
+
for b in np.unique(cell):
|
| 62 |
+
m = cell == b; n1, n0 = int(y[m].sum()), int((1 - y[m]).sum())
|
| 63 |
+
if n1 and n0:
|
| 64 |
+
num += auc(y[m], s[m]) * n1 * n0; den += n1 * n0
|
| 65 |
+
return num / den if den else float("nan")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ── one single-sum field fitted by LEAST SQUARES to a residual (the boosting weak learner) ─────────────────────
|
| 69 |
+
class Field:
|
| 70 |
+
"""u(x) = |b + Σ_j w_j z_j|², w and b complex, fitted so that α·u + c ≈ r by Adam on the squared error."""
|
| 71 |
+
|
| 72 |
+
def __init__(self, K, seed, mask=None):
|
| 73 |
+
g = np.random.default_rng(seed)
|
| 74 |
+
sc = 1.0 / np.sqrt(K)
|
| 75 |
+
self.A1 = g.normal(0, sc, K); self.A2 = g.normal(0, sc, K)
|
| 76 |
+
self.br, self.bi = g.normal(0, 0.3), g.normal(0, 0.3)
|
| 77 |
+
self.alpha, self.c = 0.0, 0.0
|
| 78 |
+
self.mask = np.ones(K) if mask is None else mask.astype(float)
|
| 79 |
+
self.A1 *= self.mask; self.A2 *= self.mask
|
| 80 |
+
self.m = {k: 0.0 for k in ("A1", "A2", "br", "bi", "alpha", "c")}; self.v = dict(self.m); self.t = 0
|
| 81 |
+
|
| 82 |
+
def u(self, C, S):
|
| 83 |
+
Zr = C @ self.A1 + S @ self.A2 + self.br
|
| 84 |
+
Zi = S @ self.A1 - C @ self.A2 + self.bi
|
| 85 |
+
return Zr * Zr + Zi * Zi, Zr, Zi
|
| 86 |
+
|
| 87 |
+
def fit(self, C, S, r, C_in, S_in, r_in, lr=0.02, steps=300, patience=40, l2=1e-3):
|
| 88 |
+
best, state, bad = np.inf, None, 0
|
| 89 |
+
for t in range(steps):
|
| 90 |
+
u, Zr, Zi = self.u(C, S)
|
| 91 |
+
pred = self.alpha * u + self.c
|
| 92 |
+
e = pred - r # d(0.5*mse)/dpred, per row / n
|
| 93 |
+
n = len(r)
|
| 94 |
+
g_alpha = float((e * u).mean()); g_c = float(e.mean())
|
| 95 |
+
gu = e * self.alpha / n # dL/du
|
| 96 |
+
gr, gi = 2.0 * gu * Zr, 2.0 * gu * Zi
|
| 97 |
+
gA1 = (gr @ C + gi @ S) * self.mask + l2 * self.A1
|
| 98 |
+
gA2 = (gr @ S - gi @ C) * self.mask + l2 * self.A2
|
| 99 |
+
gbr, gbi = float(gr.sum()), float(gi.sum())
|
| 100 |
+
self.t += 1
|
| 101 |
+
for k, gval in (("A1", gA1), ("A2", gA2), ("br", gbr), ("bi", gbi), ("alpha", g_alpha), ("c", g_c)):
|
| 102 |
+
self.m[k] = 0.9 * self.m[k] + 0.1 * gval; self.v[k] = 0.999 * self.v[k] + 0.001 * (gval * gval)
|
| 103 |
+
mh = self.m[k] / (1 - 0.9 ** self.t); vh = self.v[k] / (1 - 0.999 ** self.t)
|
| 104 |
+
setattr(self, k, getattr(self, k) - lr * mh / (np.sqrt(vh) + 1e-8))
|
| 105 |
+
if t % 5 == 0:
|
| 106 |
+
ui, _, _ = self.u(C_in, S_in)
|
| 107 |
+
loss_in = float(np.mean((self.alpha * ui + self.c - r_in) ** 2))
|
| 108 |
+
if loss_in < best - 1e-7:
|
| 109 |
+
best, bad = loss_in, 0
|
| 110 |
+
state = (self.A1.copy(), self.A2.copy(), self.br, self.bi, self.alpha, self.c)
|
| 111 |
+
else:
|
| 112 |
+
bad += 5
|
| 113 |
+
if bad >= patience:
|
| 114 |
+
break
|
| 115 |
+
if state is not None:
|
| 116 |
+
self.A1, self.A2, self.br, self.bi, self.alpha, self.c = state
|
| 117 |
+
return self
|
| 118 |
+
|
| 119 |
+
def predict(self, C, S):
|
| 120 |
+
u, _, _ = self.u(C, S)
|
| 121 |
+
return self.alpha * u + self.c
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def boost(P, y, inner, Pte, stages=60, nu=0.1, seed=0, masks=None):
|
| 125 |
+
"""Gradient boosting with single-sum fields. `masks` (optional list) restricts stage k's field to a group,
|
| 126 |
+
cycling through the groups -- the SPLIT models boosted in turn."""
|
| 127 |
+
C, S = cs(P); Cte, Ste = cs(Pte)
|
| 128 |
+
fit = ~inner
|
| 129 |
+
F = np.zeros(len(y)); Fte = np.zeros(len(Pte))
|
| 130 |
+
p0 = np.clip(y[fit].mean(), 1e-3, 1 - 1e-3); F[:] = np.log(p0 / (1 - p0)); Fte[:] = F[0]
|
| 131 |
+
best, best_stage, bad, best_Fte = -1.0, 0, 0, Fte.copy()
|
| 132 |
+
for k in range(stages):
|
| 133 |
+
p = 1 / (1 + np.exp(-F)); r = y - p
|
| 134 |
+
mask = None if masks is None else masks[k % len(masks)]
|
| 135 |
+
f = Field(C.shape[1], seed * 1000 + k, mask).fit(C[fit], S[fit], r[fit], C[inner], S[inner], r[inner])
|
| 136 |
+
h = f.predict(C, S); hte = f.predict(Cte, Ste)
|
| 137 |
+
# line search of the step on the logistic loss over the fit rows
|
| 138 |
+
best_g, best_loss = 0.0, np.inf
|
| 139 |
+
for gam in (0.25, 0.5, 1.0, 2.0, 4.0):
|
| 140 |
+
Fx = F[fit] + nu * gam * h[fit]
|
| 141 |
+
loss = float(np.mean(np.logaddexp(0, -Fx * (2 * y[fit] - 1))))
|
| 142 |
+
if loss < best_loss:
|
| 143 |
+
best_loss, best_g = loss, gam
|
| 144 |
+
F = F + nu * best_g * h; Fte = Fte + nu * best_g * hte
|
| 145 |
+
a = auc(y[inner], F[inner])
|
| 146 |
+
if a > best + 1e-5:
|
| 147 |
+
best, best_stage, bad, best_Fte = a, k + 1, 0, Fte.copy()
|
| 148 |
+
else:
|
| 149 |
+
bad += 1
|
| 150 |
+
if bad >= 10:
|
| 151 |
+
break
|
| 152 |
+
return best, best_stage, best_Fte
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def split_heads(P, y, inner, Pte, groups, F_seed=0):
|
| 156 |
+
"""SPLIT single-sum models with a LINEAR head: one field per group, weights masked to the group, K intensities
|
| 157 |
+
combined by the coherent model's own head. Returns (inner auc, held-out score, K intensities train/test)."""
|
| 158 |
+
C, S = cs(P); Cte, Ste = cs(Pte)
|
| 159 |
+
K = C.shape[1]; G = len(groups)
|
| 160 |
+
m = Coherent(K, G, seed=F_seed)
|
| 161 |
+
M = np.zeros((G, K))
|
| 162 |
+
for gi, cols in enumerate(groups):
|
| 163 |
+
M[gi, cols] = 1.0
|
| 164 |
+
m.A1 *= M; m.A2 *= M
|
| 165 |
+
rng = np.random.default_rng(7 + F_seed); idx = np.where(~inner)[0]
|
| 166 |
+
batch = int(min(1024, max(32, len(idx) // 16)))
|
| 167 |
+
best, state, bad = -1.0, None, 0
|
| 168 |
+
for ep in range(150):
|
| 169 |
+
rng.shuffle(idx)
|
| 170 |
+
for s0 in range(0, len(idx), batch):
|
| 171 |
+
b = idx[s0:s0 + batch]
|
| 172 |
+
if len(b) >= 32:
|
| 173 |
+
m.step(C[b], S[b], y[b].astype(float), 0.01, 1e-3)
|
| 174 |
+
m.A1 *= M; m.A2 *= M # keep every field inside its group
|
| 175 |
+
a = auc(y[inner], m.logit(C[inner], S[inner])[0])
|
| 176 |
+
if a > best + 1e-5:
|
| 177 |
+
best, bad = a, 0
|
| 178 |
+
state = (m.A1.copy(), m.A2.copy(), m.br.copy(), m.bi.copy(), m.w.copy(), m.c, m.mu.copy(), m.sd.copy())
|
| 179 |
+
else:
|
| 180 |
+
bad += 1
|
| 181 |
+
if bad >= 15:
|
| 182 |
+
break
|
| 183 |
+
m.A1, m.A2, m.br, m.bi, m.w, m.c, m.mu, m.sd = state
|
| 184 |
+
return best, m.logit(Cte, Ste)[0], m.fields(C, S)[2], m.fields(Cte, Ste)[2]
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def main():
|
| 188 |
+
import lightgbm as lgb
|
| 189 |
+
Z = np.load(PH, allow_pickle=True)
|
| 190 |
+
bodies = list(Z["bodies"]); ids = Z["id_test"]; ytr = Z["y_train"].astype(np.int64)
|
| 191 |
+
Dtr, Mtr, Wtr = Z["theta_dad_train"], Z["theta_mom_train"], Z["theta_wed_train"]
|
| 192 |
+
Dte, Mte, Wte = Z["theta_dad_test"], Z["theta_mom_test"], Z["theta_wed_test"]
|
| 193 |
+
ptr, pte, pn = Z["plain_train"], Z["plain_test"], list(Z["plain_names"])
|
| 194 |
+
later = Z["yr_train"].astype(int).max(1)
|
| 195 |
+
sol = pd.read_csv(SOL).set_index("id"); lab = [c for c in sol.columns if c != "Usage"][0]
|
| 196 |
+
yte = sol.loc[ids, lab].to_numpy().astype(int)
|
| 197 |
+
j1 = ptr[:, pn.index("start_is_jan1")] == 1.0; j1e = pte[:, pn.index("start_is_jan1")] == 1.0
|
| 198 |
+
Wtr = Wtr.copy(); Wte = Wte.copy(); Wtr[j1] = np.nan; Wte[j1e] = np.nan
|
| 199 |
+
B = [bodies.index(b) for b in BODIES14]
|
| 200 |
+
full_tr = np.isfinite(Dtr[:, B]).all(1) & np.isfinite(Mtr[:, B]).all(1) & np.isfinite(Wtr[:, B]).all(1)
|
| 201 |
+
full_te = np.isfinite(Dte[:, B]).all(1) & np.isfinite(Mte[:, B]).all(1) & np.isfinite(Wte[:, B]).all(1)
|
| 202 |
+
ages_te = pte[full_te][:, [pn.index("age_dad_at_start"), pn.index("age_mom_at_start")]]
|
| 203 |
+
cell = (np.floor(ages_te[:, 0] / 3) * 1000 + np.floor(ages_te[:, 1] / 3)).astype(int)
|
| 204 |
+
y = ytr[full_tr]; ye = yte[full_te]; lat = later[full_tr]; inner = lat > np.quantile(lat, 0.85)
|
| 205 |
+
log(f"FULL population: train {len(y):,} (inner {int(inner.sum()):,}) · held out {len(ye):,}")
|
| 206 |
+
cols = [pn.index(c) for c in ("age_dad_at_start", "age_mom_at_start", "age_gap", "start_year")]
|
| 207 |
+
Xp_tr, Xp_te = ptr[full_tr][:, cols], pte[full_te][:, cols]
|
| 208 |
+
|
| 209 |
+
def report(name, s, extra=None):
|
| 210 |
+
a = auc(ye, s); m_ = matched(ye, s, cell)
|
| 211 |
+
R[name] = {"held": a, "age_cell_matched": m_, **(extra or {})}
|
| 212 |
+
log(f" {name:<58} held {a:.4f} age-cell-matched {m_:.4f}" + (f" {extra}" if extra else ""))
|
| 213 |
+
|
| 214 |
+
for terms, tag in ((("a", "m", "d"), "3-term"), (TERMS, "6-term")):
|
| 215 |
+
P, labels = phase_matrix(Dtr, Mtr, Wtr, bodies, BODIES14, terms); Pe, _ = phase_matrix(Dte, Mte, Wte, bodies, BODIES14, terms)
|
| 216 |
+
P, Pe = P[full_tr], Pe[full_te]
|
| 217 |
+
log(f"── {tag}: {len(labels)} phasors ──")
|
| 218 |
+
# single ArtaModel, F=1 (the reference point)
|
| 219 |
+
am = ArtaModel(terms=terms, bodies=BODIES14, F=1).fit(P, y, inner)
|
| 220 |
+
report(f"{tag} single ArtaModel F=1", am.score(Pe), {"inner": round(am.inner_auc, 4)})
|
| 221 |
+
# BAG: 25 bootstraps x seeds, rank-averaged
|
| 222 |
+
rng = np.random.default_rng(0); scores = []; ivs = []
|
| 223 |
+
fit_idx = np.where(~inner)[0]
|
| 224 |
+
for b in range(25):
|
| 225 |
+
boot = rng.choice(fit_idx, size=len(fit_idx), replace=True)
|
| 226 |
+
keep = np.concatenate([boot, np.where(inner)[0]])
|
| 227 |
+
inn = np.zeros(len(keep), bool); inn[len(boot):] = True
|
| 228 |
+
a_ = ArtaModel(terms=terms, bodies=BODIES14, F=1, seed=b).fit(P[keep], y[keep], inn)
|
| 229 |
+
scores.append(r01(a_.score(Pe))); ivs.append(a_.inner_auc)
|
| 230 |
+
report(f"{tag} BAG 25 bootstraps (rank-average)", np.mean(scores, 0), {"inner_mean": round(float(np.mean(ivs)), 4)})
|
| 231 |
+
# BOOST: single-sum fields on residuals
|
| 232 |
+
for nu in (0.05, 0.1, 0.3):
|
| 233 |
+
iv, K, s = boost(P, y, inner, Pe, stages=60, nu=nu)
|
| 234 |
+
report(f"{tag} BOOST single-sum fields nu={nu}", s, {"inner": round(iv, 4), "stages": K})
|
| 235 |
+
# SPLIT: groups per body / per term / per phasor
|
| 236 |
+
by_body = [[k for k, l in enumerate(labels) if l.split("_", 1)[1] == b] for b in BODIES14]
|
| 237 |
+
by_term = [[k for k, l in enumerate(labels) if l.split("_", 1)[0] == t] for t in terms]
|
| 238 |
+
by_phasor = [[k] for k in range(len(labels))]
|
| 239 |
+
for gname, groups in (("per body", by_body), ("per term", by_term), ("per phasor", by_phasor)):
|
| 240 |
+
groups = [g for g in groups if g]
|
| 241 |
+
iv, s, Utr, Ute = split_heads(P, y, inner, Pe, groups)
|
| 242 |
+
report(f"{tag} SPLIT {gname} ({len(groups)} single sums), linear head", s, {"inner": round(iv, 4)})
|
| 243 |
+
# LightGBM on the K intensities (boosting over the split models), plus with the plain columns
|
| 244 |
+
def gbm(Xtr, Xte, seeds=3):
|
| 245 |
+
p = np.zeros(len(Xte))
|
| 246 |
+
for sd in range(seeds):
|
| 247 |
+
c = lgb.LGBMClassifier(n_estimators=300, learning_rate=0.03, num_leaves=7, min_child_samples=50, colsample_bytree=0.8,
|
| 248 |
+
subsample=0.8, subsample_freq=1, reg_lambda=10.0, random_state=sd, verbose=-1)
|
| 249 |
+
c.fit(Xtr[~inner], y[~inner]); p += c.predict_proba(Xte)[:, 1]
|
| 250 |
+
return p / seeds
|
| 251 |
+
report(f"{tag} SPLIT {gname} -> LightGBM on the {len(groups)} intensities", gbm(Utr, Ute))
|
| 252 |
+
report(f"{tag} SPLIT {gname} -> LightGBM on intensities + plain columns", gbm(np.column_stack([Utr, Xp_tr]), np.column_stack([Ute, Xp_te])))
|
| 253 |
+
# boosting the split models in turn (each stage restricted to one group)
|
| 254 |
+
masks = [np.isin(np.arange(len(labels)), g) for g in groups]
|
| 255 |
+
iv, K, s = boost(P, y, inner, Pe, stages=min(120, 4 * len(groups)), nu=0.1, masks=masks)
|
| 256 |
+
report(f"{tag} BOOST over SPLIT {gname} (stage k -> group k)", s, {"inner": round(iv, 4), "stages": K})
|
| 257 |
+
# references on the same rows
|
| 258 |
+
def gbm_plain(seeds=3):
|
| 259 |
+
p = np.zeros(len(Xp_te))
|
| 260 |
+
for sd in range(seeds):
|
| 261 |
+
c = lgb.LGBMClassifier(n_estimators=300, learning_rate=0.03, num_leaves=7, min_child_samples=50, reg_lambda=10.0, random_state=sd, verbose=-1)
|
| 262 |
+
c.fit(Xp_tr[~inner], y[~inner]); p += c.predict_proba(Xp_te)[:, 1]
|
| 263 |
+
return p / seeds
|
| 264 |
+
report("REFERENCE plain columns (two ages, gap, start year), LightGBM", gbm_plain())
|
| 265 |
+
json.dump(R, open(os.path.join(OUT, "artamodel_ensemble.json"), "w"), indent=1)
|
| 266 |
+
log(f"wrote {OUT}/artamodel_ensemble.json")
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
if __name__ == "__main__":
|
| 270 |
+
main()
|
artamodel_leaderboard.json
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"F0": -0.2510023573758644,
|
| 3 |
+
"stages": [
|
| 4 |
+
{
|
| 5 |
+
"stage": 1,
|
| 6 |
+
"phasor": 35,
|
| 7 |
+
"step": 0.4,
|
| 8 |
+
"alpha": 0.2790609079021399,
|
| 9 |
+
"c": -0.3139164088718358,
|
| 10 |
+
"w_re": 0.16544682051025963,
|
| 11 |
+
"w_im": -0.2717536379569562,
|
| 12 |
+
"b_re": 0.8537051175935859,
|
| 13 |
+
"b_im": 0.39014440491816765
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"stage": 2,
|
| 17 |
+
"phasor": 35,
|
| 18 |
+
"step": 0.4,
|
| 19 |
+
"alpha": -0.4383853720692169,
|
| 20 |
+
"c": 0.1173512689580232,
|
| 21 |
+
"w_re": 0.23249329816544284,
|
| 22 |
+
"w_im": -0.4510036636235923,
|
| 23 |
+
"b_re": -0.4312355863893404,
|
| 24 |
+
"b_im": -0.4544389464444736
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"stage": 3,
|
| 28 |
+
"phasor": 35,
|
| 29 |
+
"step": 0.4,
|
| 30 |
+
"alpha": 0.34994675789562907,
|
| 31 |
+
"c": -0.25146824654007244,
|
| 32 |
+
"w_re": 0.18318691499995837,
|
| 33 |
+
"w_im": -0.21215918714332932,
|
| 34 |
+
"b_re": 0.6088484312579715,
|
| 35 |
+
"b_im": 0.4163709198609787
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"stage": 4,
|
| 39 |
+
"phasor": 37,
|
| 40 |
+
"step": 0.4,
|
| 41 |
+
"alpha": -0.5820355837861616,
|
| 42 |
+
"c": 0.22526205078391126,
|
| 43 |
+
"w_re": 0.5689653288215984,
|
| 44 |
+
"w_im": 0.257394190037747,
|
| 45 |
+
"b_re": -0.6536070580939775,
|
| 46 |
+
"b_im": -0.07819744063532238
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"stage": 5,
|
| 50 |
+
"phasor": 7,
|
| 51 |
+
"step": 0.4,
|
| 52 |
+
"alpha": 0.30902073901399696,
|
| 53 |
+
"c": -0.36656379703454534,
|
| 54 |
+
"w_re": 0.45093515109026966,
|
| 55 |
+
"w_im": 0.31784612820100994,
|
| 56 |
+
"b_re": 0.45797385423834874,
|
| 57 |
+
"b_im": 0.41008636025743916
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"stage": 6,
|
| 61 |
+
"phasor": 35,
|
| 62 |
+
"step": 0.4,
|
| 63 |
+
"alpha": -0.2888845705269212,
|
| 64 |
+
"c": 0.08357975402566378,
|
| 65 |
+
"w_re": -0.4427115061786578,
|
| 66 |
+
"w_im": -0.32150984551227346,
|
| 67 |
+
"b_re": -0.5324306908841805,
|
| 68 |
+
"b_im": 0.37366357041275694
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"stage": 7,
|
| 72 |
+
"phasor": 7,
|
| 73 |
+
"step": 0.4,
|
| 74 |
+
"alpha": -0.3380538185780996,
|
| 75 |
+
"c": 0.07942775223701656,
|
| 76 |
+
"w_re": 0.16686411043437596,
|
| 77 |
+
"w_im": -0.3506490103077557,
|
| 78 |
+
"b_re": -0.3456586276390284,
|
| 79 |
+
"b_im": 0.6182341239570788
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"stage": 8,
|
| 83 |
+
"phasor": 37,
|
| 84 |
+
"step": 0.4,
|
| 85 |
+
"alpha": 0.7608792714926302,
|
| 86 |
+
"c": -0.31757565826834183,
|
| 87 |
+
"w_re": 0.046712674010060504,
|
| 88 |
+
"w_im": 0.3424069935800981,
|
| 89 |
+
"b_re": 0.5909948084345692,
|
| 90 |
+
"b_im": 0.19999181642194766
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"stage": 9,
|
| 94 |
+
"phasor": 7,
|
| 95 |
+
"step": 0.4,
|
| 96 |
+
"alpha": 0.2069111011215517,
|
| 97 |
+
"c": -0.20882936021322604,
|
| 98 |
+
"w_re": 0.21655920000025589,
|
| 99 |
+
"w_im": 0.43361399974882253,
|
| 100 |
+
"b_re": 0.33203701914438416,
|
| 101 |
+
"b_im": 0.52191876376675
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"stage": 10,
|
| 105 |
+
"phasor": 35,
|
| 106 |
+
"step": 0.4,
|
| 107 |
+
"alpha": -0.21006169984120804,
|
| 108 |
+
"c": 0.05482326858295784,
|
| 109 |
+
"w_re": -0.1737283486567377,
|
| 110 |
+
"w_im": -0.34326716773408406,
|
| 111 |
+
"b_re": -0.6237033604178058,
|
| 112 |
+
"b_im": 0.10437845863523815
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"stage": 11,
|
| 116 |
+
"phasor": 7,
|
| 117 |
+
"step": 0.4,
|
| 118 |
+
"alpha": -0.23596001023735938,
|
| 119 |
+
"c": 0.04793709815026776,
|
| 120 |
+
"w_re": 0.3042775595950708,
|
| 121 |
+
"w_im": 0.30411728121014864,
|
| 122 |
+
"b_re": -0.478025576284314,
|
| 123 |
+
"b_im": -0.4066299078084681
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"stage": 12,
|
| 127 |
+
"phasor": 37,
|
| 128 |
+
"step": 0.4,
|
| 129 |
+
"alpha": -0.49022519323754793,
|
| 130 |
+
"c": 0.144709174758613,
|
| 131 |
+
"w_re": 0.4483943171312746,
|
| 132 |
+
"w_im": 0.1747243976256867,
|
| 133 |
+
"b_re": -0.5769310079468398,
|
| 134 |
+
"b_im": 0.0007063262364112111
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"stage": 13,
|
| 138 |
+
"phasor": 7,
|
| 139 |
+
"step": 0.4,
|
| 140 |
+
"alpha": 0.325753320229801,
|
| 141 |
+
"c": -0.2413198050073399,
|
| 142 |
+
"w_re": -0.121212586683471,
|
| 143 |
+
"w_im": 0.3900741494361323,
|
| 144 |
+
"b_re": -0.23218812289357832,
|
| 145 |
+
"b_im": 0.44633056636642404
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"stage": 14,
|
| 149 |
+
"phasor": 35,
|
| 150 |
+
"step": 0.4,
|
| 151 |
+
"alpha": 0.13939712628885229,
|
| 152 |
+
"c": -0.04283992755583811,
|
| 153 |
+
"w_re": 0.1941305187432573,
|
| 154 |
+
"w_im": 0.19793528765528204,
|
| 155 |
+
"b_re": -0.39677061320341045,
|
| 156 |
+
"b_im": 0.2534717678746568
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"stage": 15,
|
| 160 |
+
"phasor": 35,
|
| 161 |
+
"step": 0.4,
|
| 162 |
+
"alpha": -0.27729186832613145,
|
| 163 |
+
"c": 0.037781545123303596,
|
| 164 |
+
"w_re": 0.3742135911064377,
|
| 165 |
+
"w_im": 0.19413612657427187,
|
| 166 |
+
"b_re": 0.36760760197091547,
|
| 167 |
+
"b_im": -0.3070663472768141
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"stage": 16,
|
| 171 |
+
"phasor": 7,
|
| 172 |
+
"step": 0.4,
|
| 173 |
+
"alpha": 0.31146526208193726,
|
| 174 |
+
"c": -0.1487809462855659,
|
| 175 |
+
"w_re": -0.192228284814361,
|
| 176 |
+
"w_im": -0.28050912477619155,
|
| 177 |
+
"b_re": -0.19882723914785924,
|
| 178 |
+
"b_im": -0.3321859480783824
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
"stage": 17,
|
| 182 |
+
"phasor": 23,
|
| 183 |
+
"step": 0.4,
|
| 184 |
+
"alpha": -0.691962297005292,
|
| 185 |
+
"c": 0.2236726300021271,
|
| 186 |
+
"w_re": -0.3092392733341191,
|
| 187 |
+
"w_im": -0.05190284634462815,
|
| 188 |
+
"b_re": 0.492181263258386,
|
| 189 |
+
"b_im": -0.3103055227978361
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"stage": 18,
|
| 193 |
+
"phasor": 7,
|
| 194 |
+
"step": 0.4,
|
| 195 |
+
"alpha": 0.16678699025239196,
|
| 196 |
+
"c": -0.16240030753693468,
|
| 197 |
+
"w_re": -0.3747982735844348,
|
| 198 |
+
"w_im": 0.20475879054311819,
|
| 199 |
+
"b_re": -0.5659027944892202,
|
| 200 |
+
"b_im": 0.26445255364285103
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"stage": 19,
|
| 204 |
+
"phasor": 20,
|
| 205 |
+
"step": 0.4,
|
| 206 |
+
"alpha": -0.008164434660184142,
|
| 207 |
+
"c": 0.004119182828503224,
|
| 208 |
+
"w_re": 0.03561037285156963,
|
| 209 |
+
"w_im": -0.06353276553290359,
|
| 210 |
+
"b_re": 0.39095817474270683,
|
| 211 |
+
"b_im": 0.19986673296932997
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"stage": 20,
|
| 215 |
+
"phasor": 20,
|
| 216 |
+
"step": 0.4,
|
| 217 |
+
"alpha": -0.030576049347266418,
|
| 218 |
+
"c": 0.0035432005553962245,
|
| 219 |
+
"w_re": 0.02337541497072417,
|
| 220 |
+
"w_im": 0.1005538166637217,
|
| 221 |
+
"b_re": -0.328467058088797,
|
| 222 |
+
"b_im": -0.07539161972439071
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"stage": 21,
|
| 226 |
+
"phasor": 20,
|
| 227 |
+
"step": 0.4,
|
| 228 |
+
"alpha": -0.041398339698101916,
|
| 229 |
+
"c": 0.005465754070426186,
|
| 230 |
+
"w_re": -0.04476609738232852,
|
| 231 |
+
"w_im": 0.16635825257857076,
|
| 232 |
+
"b_re": -0.36087450832523327,
|
| 233 |
+
"b_im": 0.11152443285731284
|
| 234 |
+
},
|
| 235 |
+
{
|
| 236 |
+
"stage": 22,
|
| 237 |
+
"phasor": 20,
|
| 238 |
+
"step": 0.4,
|
| 239 |
+
"alpha": -0.11380137146366145,
|
| 240 |
+
"c": 0.0025476058309119856,
|
| 241 |
+
"w_re": 0.028256324530831264,
|
| 242 |
+
"w_im": 0.11653345741523358,
|
| 243 |
+
"b_re": -0.190184525777227,
|
| 244 |
+
"b_im": 0.03714923525546911
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"stage": 23,
|
| 248 |
+
"phasor": 20,
|
| 249 |
+
"step": 0.4,
|
| 250 |
+
"alpha": 0.07341465288887274,
|
| 251 |
+
"c": -0.0203333671124774,
|
| 252 |
+
"w_re": -0.20880220908325148,
|
| 253 |
+
"w_im": 0.1716401560852639,
|
| 254 |
+
"b_re": 0.1857565062932319,
|
| 255 |
+
"b_im": 0.3755205575990392
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"stage": 24,
|
| 259 |
+
"phasor": 23,
|
| 260 |
+
"step": 0.025,
|
| 261 |
+
"alpha": 0.0025675604826571764,
|
| 262 |
+
"c": 0.0022698323283945818,
|
| 263 |
+
"w_re": 0.011667472114586163,
|
| 264 |
+
"w_im": -0.020929803493994557,
|
| 265 |
+
"b_re": -0.25044814787217995,
|
| 266 |
+
"b_im": -0.0264954757651408
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"stage": 25,
|
| 270 |
+
"phasor": 23,
|
| 271 |
+
"step": 0.4,
|
| 272 |
+
"alpha": 0.3855542289770793,
|
| 273 |
+
"c": -0.13300980650958877,
|
| 274 |
+
"w_re": 0.13279384507466938,
|
| 275 |
+
"w_im": -0.47334365600013123,
|
| 276 |
+
"b_re": -0.5190000169171142,
|
| 277 |
+
"b_im": -0.21912983674650482
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"stage": 26,
|
| 281 |
+
"phasor": 7,
|
| 282 |
+
"step": 0.4,
|
| 283 |
+
"alpha": 0.21921044511589627,
|
| 284 |
+
"c": -0.13913509144130914,
|
| 285 |
+
"w_re": -0.288981646771367,
|
| 286 |
+
"w_im": -0.240467983713006,
|
| 287 |
+
"b_re": -0.3286717045240301,
|
| 288 |
+
"b_im": -0.3357175537050057
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
"stage": 27,
|
| 292 |
+
"phasor": 20,
|
| 293 |
+
"step": 0.4,
|
| 294 |
+
"alpha": -0.01729559500497132,
|
| 295 |
+
"c": 0.010603586888993247,
|
| 296 |
+
"w_re": -0.0045418174252236584,
|
| 297 |
+
"w_im": 0.14429496669740433,
|
| 298 |
+
"b_re": -0.7289771467179822,
|
| 299 |
+
"b_im": -0.15110922925003778
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"stage": 28,
|
| 303 |
+
"phasor": 20,
|
| 304 |
+
"step": 0.025,
|
| 305 |
+
"alpha": 0.0029503938352781556,
|
| 306 |
+
"c": -0.00016868347221038905,
|
| 307 |
+
"w_re": 0.031818447407431213,
|
| 308 |
+
"w_im": -0.02925275766963477,
|
| 309 |
+
"b_re": 0.030685508507719773,
|
| 310 |
+
"b_im": 0.3222325568984256
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
"stage": 29,
|
| 314 |
+
"phasor": 20,
|
| 315 |
+
"step": 0.4,
|
| 316 |
+
"alpha": -0.023699637641509487,
|
| 317 |
+
"c": 0.0020922555094313056,
|
| 318 |
+
"w_re": 0.07045111449166673,
|
| 319 |
+
"w_im": -0.018005944379729466,
|
| 320 |
+
"b_re": 0.07080146794410015,
|
| 321 |
+
"b_im": 0.31108100653071213
|
| 322 |
+
},
|
| 323 |
+
{
|
| 324 |
+
"stage": 30,
|
| 325 |
+
"phasor": 20,
|
| 326 |
+
"step": 0.4,
|
| 327 |
+
"alpha": -0.10109300198626403,
|
| 328 |
+
"c": 0.009361324814773427,
|
| 329 |
+
"w_re": 0.12728831587213713,
|
| 330 |
+
"w_im": -0.1667525087363923,
|
| 331 |
+
"b_re": 0.22393090550322364,
|
| 332 |
+
"b_im": 0.2799110350682605
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"stage": 31,
|
| 336 |
+
"phasor": 7,
|
| 337 |
+
"step": 0.4,
|
| 338 |
+
"alpha": -0.32826538527588905,
|
| 339 |
+
"c": 0.03560517815619409,
|
| 340 |
+
"w_re": 0.23684967687092165,
|
| 341 |
+
"w_im": -0.16335650143836847,
|
| 342 |
+
"b_re": -0.4854325733136111,
|
| 343 |
+
"b_im": 0.20263486662597754
|
| 344 |
+
}
|
| 345 |
+
],
|
| 346 |
+
"inner_auc": 0.631962843164345,
|
| 347 |
+
"labels": [
|
| 348 |
+
"a_sun",
|
| 349 |
+
"a_moon",
|
| 350 |
+
"a_mercury",
|
| 351 |
+
"a_venus",
|
| 352 |
+
"a_mars",
|
| 353 |
+
"a_jupiter",
|
| 354 |
+
"a_saturn",
|
| 355 |
+
"a_uranus",
|
| 356 |
+
"a_neptune",
|
| 357 |
+
"a_pluto",
|
| 358 |
+
"a_true_node",
|
| 359 |
+
"a_true_south_node",
|
| 360 |
+
"a_chiron",
|
| 361 |
+
"a_mean_lilith",
|
| 362 |
+
"m_sun",
|
| 363 |
+
"m_moon",
|
| 364 |
+
"m_mercury",
|
| 365 |
+
"m_venus",
|
| 366 |
+
"m_mars",
|
| 367 |
+
"m_jupiter",
|
| 368 |
+
"m_saturn",
|
| 369 |
+
"m_uranus",
|
| 370 |
+
"m_neptune",
|
| 371 |
+
"m_pluto",
|
| 372 |
+
"m_true_node",
|
| 373 |
+
"m_true_south_node",
|
| 374 |
+
"m_chiron",
|
| 375 |
+
"m_mean_lilith",
|
| 376 |
+
"d_sun",
|
| 377 |
+
"d_moon",
|
| 378 |
+
"d_mercury",
|
| 379 |
+
"d_venus",
|
| 380 |
+
"d_mars",
|
| 381 |
+
"d_jupiter",
|
| 382 |
+
"d_saturn",
|
| 383 |
+
"d_uranus",
|
| 384 |
+
"d_neptune",
|
| 385 |
+
"d_pluto",
|
| 386 |
+
"d_true_node",
|
| 387 |
+
"d_true_south_node",
|
| 388 |
+
"d_chiron",
|
| 389 |
+
"d_mean_lilith",
|
| 390 |
+
"mn_sun",
|
| 391 |
+
"mn_moon",
|
| 392 |
+
"mn_mercury",
|
| 393 |
+
"mn_venus",
|
| 394 |
+
"mn_mars",
|
| 395 |
+
"mn_jupiter",
|
| 396 |
+
"mn_saturn",
|
| 397 |
+
"mn_uranus",
|
| 398 |
+
"mn_neptune",
|
| 399 |
+
"mn_pluto",
|
| 400 |
+
"mn_true_node",
|
| 401 |
+
"mn_true_south_node",
|
| 402 |
+
"mn_chiron",
|
| 403 |
+
"mn_mean_lilith",
|
| 404 |
+
"dn_sun",
|
| 405 |
+
"dn_moon",
|
| 406 |
+
"dn_mercury",
|
| 407 |
+
"dn_venus",
|
| 408 |
+
"dn_mars",
|
| 409 |
+
"dn_jupiter",
|
| 410 |
+
"dn_saturn",
|
| 411 |
+
"dn_uranus",
|
| 412 |
+
"dn_neptune",
|
| 413 |
+
"dn_pluto",
|
| 414 |
+
"dn_true_node",
|
| 415 |
+
"dn_true_south_node",
|
| 416 |
+
"dn_chiron",
|
| 417 |
+
"dn_mean_lilith",
|
| 418 |
+
"tn_sun",
|
| 419 |
+
"tn_moon",
|
| 420 |
+
"tn_mercury",
|
| 421 |
+
"tn_venus",
|
| 422 |
+
"tn_mars",
|
| 423 |
+
"tn_jupiter",
|
| 424 |
+
"tn_saturn",
|
| 425 |
+
"tn_uranus",
|
| 426 |
+
"tn_neptune",
|
| 427 |
+
"tn_pluto",
|
| 428 |
+
"tn_true_node",
|
| 429 |
+
"tn_true_south_node",
|
| 430 |
+
"tn_chiron",
|
| 431 |
+
"tn_mean_lilith"
|
| 432 |
+
],
|
| 433 |
+
"fitted_on": "train rows with both natal charts",
|
| 434 |
+
"n_rows": 9553
|
| 435 |
+
}
|
artamodel_score.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
artamodel_score.py — score a couple with the deployed ArtaModel, from three dates and two places. numpy only for
|
| 3 |
+
the model; Kerykeion for the phases (pip install kerykeion timezonefinder).
|
| 4 |
+
|
| 5 |
+
from artamodel_score import predict
|
| 6 |
+
p = predict("1936-08-04", 37.943, 23.647, "1924-05-14", 37.727, 26.909, "1968-06-15") # dad, mom, wedding
|
| 7 |
+
|
| 8 |
+
Returns the probability that the marriage lasted thirty years, and the term-by-term account of how the logit was
|
| 9 |
+
formed. See README.md / ARTAMODEL.md for what the model is and, honestly, what it reads.
|
| 10 |
+
"""
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import warnings
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
warnings.filterwarnings("ignore")
|
| 18 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 19 |
+
MODEL = json.load(open(os.path.join(HERE, "artamodel_deployed.json")))
|
| 20 |
+
BODIES = MODEL["bodies"]; TERMS = MODEL["terms"]; LABELS = MODEL["labels"]
|
| 21 |
+
ANGLES = {"ascendant", "medium_coeli"}
|
| 22 |
+
SLOW = {"jupiter", "saturn", "uranus", "neptune", "pluto", "true_node", "true_south_node", "chiron", "mean_lilith"}
|
| 23 |
+
_TF = None
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _tz(lat, lon):
|
| 27 |
+
global _TF
|
| 28 |
+
if _TF is None:
|
| 29 |
+
from timezonefinder import TimezoneFinder
|
| 30 |
+
_TF = TimezoneFinder()
|
| 31 |
+
return _TF.timezone_at(lng=lon, lat=lat) or "UTC"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _prec(d):
|
| 35 |
+
if not d or d == "0000-00-00":
|
| 36 |
+
return 0
|
| 37 |
+
return 1 if d.endswith("-00-00") else (2 if d.endswith("-00") else 3)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def theta(date, lat=None, lon=None, hour=9, natal=True):
|
| 41 |
+
"""Sidereal (Lahiri) longitudes of the model's bodies at 09:00 local (natal) or 12:00 UT (wedding); NaN where
|
| 42 |
+
the date's precision cannot place a body (year-only -> slow bodies only)."""
|
| 43 |
+
from kerykeion import AstrologicalSubject
|
| 44 |
+
out = np.full(len(BODIES), np.nan); p = _prec(date)
|
| 45 |
+
if p == 0 or (natal and (lat is None or lon is None)):
|
| 46 |
+
return out
|
| 47 |
+
y, m, d = int(date[:4]), max(1, int(date[5:7])), max(1, int(date[8:10]))
|
| 48 |
+
if natal:
|
| 49 |
+
s = AstrologicalSubject("x", y, m, d, hour, 0, lng=float(lon), lat=float(lat), tz_str=_tz(float(lat), float(lon)),
|
| 50 |
+
city="x", nation="XX", zodiac_type="Sidereal", sidereal_mode="LAHIRI", online=False)
|
| 51 |
+
else:
|
| 52 |
+
s = AstrologicalSubject("w", y, m, d, 12, 0, lng=0.0, lat=51.48, tz_str="UTC", city="G", nation="GB",
|
| 53 |
+
zodiac_type="Sidereal", sidereal_mode="LAHIRI", online=False)
|
| 54 |
+
for j, b in enumerate(BODIES):
|
| 55 |
+
if p == 1 and b not in SLOW:
|
| 56 |
+
continue
|
| 57 |
+
if p == 2 and b not in SLOW and b != "sun":
|
| 58 |
+
continue
|
| 59 |
+
try:
|
| 60 |
+
out[j] = float(getattr(s, b).abs_pos)
|
| 61 |
+
except Exception:
|
| 62 |
+
pass
|
| 63 |
+
return out
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def phases(td, tm, tw):
|
| 67 |
+
"""The 84 phases in the model's label order (NaN = the term does not exist)."""
|
| 68 |
+
P = np.full(len(LABELS), np.nan)
|
| 69 |
+
col = {b: j for j, b in enumerate(BODIES)}
|
| 70 |
+
for k, lab in enumerate(LABELS):
|
| 71 |
+
t, b = lab.split("_", 1); j = col[b]
|
| 72 |
+
if t == "a": P[k] = tm[j] - td[j]
|
| 73 |
+
elif t == "m": P[k] = tw[j] - tm[j]
|
| 74 |
+
elif t == "d": P[k] = tw[j] - td[j]
|
| 75 |
+
elif t == "mn": P[k] = tm[j]
|
| 76 |
+
elif t == "dn": P[k] = td[j]
|
| 77 |
+
elif t == "tn": P[k] = tw[j]
|
| 78 |
+
return P
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def logit(P):
|
| 82 |
+
"""The deployed model's logit for one phase vector, with the per-stage account."""
|
| 83 |
+
rad = np.pi / 180.0
|
| 84 |
+
C, S = np.nan_to_num(np.cos(P * rad)), np.nan_to_num(np.sin(P * rad))
|
| 85 |
+
F = MODEL["F0"]; account = []
|
| 86 |
+
for st in MODEL["stages"]:
|
| 87 |
+
j = st["phasor"]
|
| 88 |
+
if not np.isfinite(P[j]):
|
| 89 |
+
account.append({"stage": st["stage"], "phasor": LABELS[j], "contribution": 0.0, "note": "term absent for this couple"}); continue
|
| 90 |
+
Zr = st["w_re"] * C[j] - st["w_im"] * S[j] + st["b_re"]; Zi = st["w_re"] * S[j] + st["w_im"] * C[j] + st["b_im"]
|
| 91 |
+
u = Zr * Zr + Zi * Zi; contrib = st["step"] * (st["alpha"] * u + st["c"])
|
| 92 |
+
F += contrib; account.append({"stage": st["stage"], "phasor": LABELS[j], "phase_deg": float(P[j] % 360), "contribution": float(contrib)})
|
| 93 |
+
return float(F), account
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def predict(dob_dad, lat_dad, lon_dad, dob_mom, lat_mom, lon_mom, start):
|
| 97 |
+
wed = start if start[5:] != "01-01" else start[:4] + "-00-00" # a 1 January start is a year-only record
|
| 98 |
+
P = phases(theta(dob_dad, lat_dad, lon_dad), theta(dob_mom, lat_mom, lon_mom), theta(wed, natal=False))
|
| 99 |
+
F, account = logit(P)
|
| 100 |
+
return {"probability": float(1 / (1 + np.exp(-F))), "logit": F, "terms": account}
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
if __name__ == "__main__":
|
| 104 |
+
r = predict("1936-08-04", 37.943, 23.647, "1924-05-14", 37.727, 26.909, "1968-06-15")
|
| 105 |
+
print(f" p(lasted 30 years) = {r['probability']:.3f} logit {r['logit']:+.3f}")
|
| 106 |
+
for t in r["terms"][:8]:
|
| 107 |
+
print(f" stage {t['stage']:>2} {t['phasor']:<12} " + (f"φ={t['phase_deg']:6.1f}° {t['contribution']:+.4f}" if "phase_deg" in t else t["note"]))
|
artamodel_selected.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"E1 term subsets (F=1)": {
|
| 3 |
+
"n_configs": 70,
|
| 4 |
+
"selected_by_inner": "E1 terms m+d",
|
| 5 |
+
"its_inner": 0.6426429599219167,
|
| 6 |
+
"its_held": 0.6303868028308671,
|
| 7 |
+
"max_held_in_family": 0.6318152802643866,
|
| 8 |
+
"optimism_if_selected_on_held": 0.0014284774335194816
|
| 9 |
+
},
|
| 10 |
+
"E1 F=8 rungs": {
|
| 11 |
+
"n_configs": 6,
|
| 12 |
+
"selected_by_inner": "E1 F8 terms a+m+d",
|
| 13 |
+
"its_inner": 0.6479533311196305,
|
| 14 |
+
"its_held": 0.621263016459523,
|
| 15 |
+
"max_held_in_family": 0.6310528484705769,
|
| 16 |
+
"optimism_if_selected_on_held": 0.009789832011053878
|
| 17 |
+
},
|
| 18 |
+
"E3 3-term body sets": {
|
| 19 |
+
"n_configs": 45,
|
| 20 |
+
"selected_by_inner": "E3 3-term drop chiron",
|
| 21 |
+
"its_inner": 0.6488664835330697,
|
| 22 |
+
"its_held": 0.6245990162840656,
|
| 23 |
+
"max_held_in_family": 0.6419098541279247,
|
| 24 |
+
"optimism_if_selected_on_held": 0.017310837843859117
|
| 25 |
+
},
|
| 26 |
+
"E3 6-term body sets": {
|
| 27 |
+
"n_configs": 45,
|
| 28 |
+
"selected_by_inner": "E3 6-term drop outer",
|
| 29 |
+
"its_inner": 0.6290387498616435,
|
| 30 |
+
"its_held": 0.584257082073676,
|
| 31 |
+
"max_held_in_family": 0.6404028825808864,
|
| 32 |
+
"optimism_if_selected_on_held": 0.05614580050721041
|
| 33 |
+
},
|
| 34 |
+
"E5 harmonics": {
|
| 35 |
+
"n_configs": 6,
|
| 36 |
+
"selected_by_inner": "E5 harmonic 2 terms a+m+d",
|
| 37 |
+
"its_inner": 0.6284878397279158,
|
| 38 |
+
"its_held": 0.628358653456569,
|
| 39 |
+
"max_held_in_family": 0.628358653456569,
|
| 40 |
+
"optimism_if_selected_on_held": 0.0
|
| 41 |
+
},
|
| 42 |
+
"E6 fields x L2": {
|
| 43 |
+
"n_configs": 28,
|
| 44 |
+
"selected_by_inner": "E6 3-term F=64 l2=0.01",
|
| 45 |
+
"its_inner": 0.651917871625361,
|
| 46 |
+
"its_held": 0.6163225046086434,
|
| 47 |
+
"max_held_in_family": 0.6364966461081348,
|
| 48 |
+
"optimism_if_selected_on_held": 0.020174141499491482
|
| 49 |
+
},
|
| 50 |
+
"E9 conventions, 3-term": {
|
| 51 |
+
"n_configs": 10,
|
| 52 |
+
"selected_by_inner": "E9 hour 12:00 UT, place ignored 3-term",
|
| 53 |
+
"its_inner": 0.6417071673659955,
|
| 54 |
+
"its_held": 0.6258785857669394,
|
| 55 |
+
"max_held_in_family": 0.625995749774618,
|
| 56 |
+
"optimism_if_selected_on_held": 0.00011716400767858293
|
| 57 |
+
},
|
| 58 |
+
"E9 conventions, 6-term": {
|
| 59 |
+
"n_configs": 10,
|
| 60 |
+
"selected_by_inner": "E9 TROPICAL 6-term",
|
| 61 |
+
"its_inner": 0.6041144685603888,
|
| 62 |
+
"its_held": 0.5941404144258329,
|
| 63 |
+
"max_held_in_family": 0.5941404144258329,
|
| 64 |
+
"optimism_if_selected_on_held": 0.0
|
| 65 |
+
}
|
| 66 |
+
}
|
artamodel_study.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
coherent_fit.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
coherent_fit.py — FIT the coherent phasor field. Arash's form, with a, p and b learned rather than drawn.
|
| 3 |
+
|
| 4 |
+
F_f(chart) = | b_f + SUM_k w_fk * exp( i * h_k * theta_k ) |^2 , w_fk = a_fk * exp(-i * p_fk)
|
| 5 |
+
|
| 6 |
+
THE REPARAMETERISATION, AND WHY IT MATTERS. Written in polar form the parameters are an amplitude a >= 0 and a
|
| 7 |
+
phase p on a circle, and a gradient step on p has to be wrapped. Folding them into ONE COMPLEX WEIGHT
|
| 8 |
+
w = a*exp(-i*p) removes both problems: the model becomes a complex linear map followed by a squared modulus, the
|
| 9 |
+
parameter space is flat R^2 per term, and every gradient is a matmul. With A1 = Re w and A2 = -Im w,
|
| 10 |
+
|
| 11 |
+
Re S = C @ A1' + S_ @ A2' Im S = S_ @ A1' - C @ A2'
|
| 12 |
+
|
| 13 |
+
where C = cos(h*theta) and S_ = sin(h*theta) are the batch's basis. Nothing is approximated -- this is the same
|
| 14 |
+
function, in coordinates that optimise cleanly.
|
| 15 |
+
|
| 16 |
+
WHAT IS ACTUALLY BEING LEARNED. |b + sum w_k z_k|^2 expands to |b|^2 + 2*Re(b_bar * sum w_k z_k) + sum_jk
|
| 17 |
+
w_j w_k_bar z_j z_k_bar. The last term is a QUADRATIC form in the chart's Fourier coefficients, so a bank of F
|
| 18 |
+
fields is a rank-F quadratic model over every pairwise product of body phases -- every classical aspect, every
|
| 19 |
+
harmonic of one, and every cross-partner contact, simultaneously, with the weights fitted. The bias term is what
|
| 20 |
+
makes the field's response asymmetric rather than a pure interference pattern.
|
| 21 |
+
|
| 22 |
+
The basis spans harmonics h in HARMONICS for every (partner-slot, body) pair, so a single field may mix
|
| 23 |
+
harmonics. That is strictly more general than one harmonic per field, and it is the fit's business which to use.
|
| 24 |
+
|
| 25 |
+
INNER VALIDATION IS TEMPORAL, NOT RANDOM. Early stopping on a random slice of the training half would select the
|
| 26 |
+
iteration that generalises best to CONTEMPORARIES of the training data, which is not the question -- the real
|
| 27 |
+
test is out of time. So the inner split holds out the LATEST births of the training half, mirroring the outer
|
| 28 |
+
split. A random inner split measurably overfits the epoch count here.
|
| 29 |
+
|
| 30 |
+
THE ONLY COMPARISON REPORTED is the two-parameter logistic on the age gap. Note a consequence of the dataset's
|
| 31 |
+
own column definition: with the OLDER partner always first, the gap is non-negative by construction, so the
|
| 32 |
+
signed form that distinguished man-older from woman-older is not expressible and is not claimed -- no sex is
|
| 33 |
+
read anywhere in this dataset.
|
| 34 |
+
|
| 35 |
+
Usage: AQ_LON=/tmp/aqcoh/lon.npz AQ_SET=fast AQ_FIELDS=64 python coherent_fit.py
|
| 36 |
+
"""
|
| 37 |
+
import json
|
| 38 |
+
import os
|
| 39 |
+
import sys
|
| 40 |
+
import time
|
| 41 |
+
|
| 42 |
+
import numpy as np
|
| 43 |
+
|
| 44 |
+
T0 = time.time()
|
| 45 |
+
|
| 46 |
+
HARMONICS = (1, 2, 3, 4, 6, 8, 12)
|
| 47 |
+
SUN, MOON, MERCURY, VENUS, MARS, JUPITER, SATURN, URANUS, NEPTUNE, PLUTO = range(10)
|
| 48 |
+
SETS = {
|
| 49 |
+
"fast": (SUN, MOON, MERCURY, VENUS, MARS),
|
| 50 |
+
"classical": (SUN, MOON, MERCURY, VENUS, MARS, JUPITER, SATURN),
|
| 51 |
+
"all18": tuple(range(18)),
|
| 52 |
+
}
|
| 53 |
+
iO, iY = 0, 1
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def log(*a):
|
| 57 |
+
print(f"[{time.time()-T0:6.1f}s]", *a, flush=True)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def auc(y, s):
|
| 61 |
+
y = np.asarray(y, np.int64)
|
| 62 |
+
s = np.asarray(s, np.float64)
|
| 63 |
+
n1, n0 = int(y.sum()), int((1 - y).sum())
|
| 64 |
+
if n1 == 0 or n0 == 0:
|
| 65 |
+
return float("nan")
|
| 66 |
+
o = np.argsort(s, kind="mergesort")
|
| 67 |
+
ys, ss = y[o], s[o]
|
| 68 |
+
r = np.empty(len(ss))
|
| 69 |
+
i = 0
|
| 70 |
+
while i < len(ss):
|
| 71 |
+
j = i
|
| 72 |
+
while j + 1 < len(ss) and ss[j + 1] == ss[i]:
|
| 73 |
+
j += 1
|
| 74 |
+
r[i:j + 1] = 0.5 * (i + j) + 1.0
|
| 75 |
+
i = j + 1
|
| 76 |
+
return float((r[ys == 1].sum() - n1 * (n1 + 1) / 2.0) / (n1 * n0))
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# Mean daily motion in degrees, for the orb filter below. Only the ten classical/modern bodies are ever used
|
| 80 |
+
# with a harmonic filter; the asteroids and nodes are slow and unaffected.
|
| 81 |
+
DAILY = {SUN: 0.9856, MOON: 13.176, MERCURY: 4.09, VENUS: 1.60, MARS: 0.524, JUPITER: 0.083,
|
| 82 |
+
SATURN: 0.0335, URANUS: 0.0117, NEPTUNE: 0.006, PLUTO: 0.004}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def basis(LON, bodies, orb=0.0):
|
| 86 |
+
"""cos and sin of h*theta for every (slot, body, harmonic), with an ORB FILTER on the harmonic.
|
| 87 |
+
|
| 88 |
+
WHY A FILTER. This dataset has birth DATES and no birth TIMES, so every chart is cast for a fixed hour and
|
| 89 |
+
each body carries an uncertainty of +-(daily motion)/2 degrees. A harmonic MULTIPLIES that error: the Moon
|
| 90 |
+
moves 13.2 deg/day, so its phase is +-6.6 deg at h=1 and +-79 deg at h=12 -- at which point the term is not a
|
| 91 |
+
weak feature, it is noise with a plausible name, and fitting it can only cost generalisation.
|
| 92 |
+
|
| 93 |
+
`orb` is the largest phase error in degrees a term may carry. A term is admitted when
|
| 94 |
+
h * (daily motion)/2 <= orb. orb=0 admits everything (the unfiltered basis); orb=30 drops the Moon above
|
| 95 |
+
h=4 and Mercury above h=12 while keeping every slow body at every harmonic.
|
| 96 |
+
"""
|
| 97 |
+
rad = np.pi / 180.0
|
| 98 |
+
cols, kept = [], []
|
| 99 |
+
for h in HARMONICS:
|
| 100 |
+
for b in bodies:
|
| 101 |
+
if orb and h * DAILY.get(b, 0.3) / 2.0 > orb:
|
| 102 |
+
continue
|
| 103 |
+
for s in (iO, iY):
|
| 104 |
+
cols.append(h * LON[s, b] * rad)
|
| 105 |
+
kept.append((h, b, s))
|
| 106 |
+
P = np.stack(cols, axis=1)
|
| 107 |
+
return np.cos(P), np.sin(P), kept
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class Coherent:
|
| 111 |
+
"""A bank of F coherent fields plus a logistic head, fitted by Adam on the exact gradients."""
|
| 112 |
+
|
| 113 |
+
def __init__(self, K, F=64, seed=0):
|
| 114 |
+
g = np.random.default_rng(seed)
|
| 115 |
+
# Small init so the initial |Z|^2 is O(1) and the head starts near chance.
|
| 116 |
+
sc = 1.0 / np.sqrt(K)
|
| 117 |
+
self.A1 = g.normal(0, sc, (F, K))
|
| 118 |
+
self.A2 = g.normal(0, sc, (F, K))
|
| 119 |
+
self.br = g.normal(0, 0.3, F)
|
| 120 |
+
self.bi = g.normal(0, 0.3, F)
|
| 121 |
+
self.w = np.zeros(F)
|
| 122 |
+
self.c = 0.0
|
| 123 |
+
self.mu = np.zeros(F)
|
| 124 |
+
self.sd = np.ones(F)
|
| 125 |
+
self.F, self.K = F, K
|
| 126 |
+
self._m = {k: np.zeros_like(getattr(self, k)) for k in ("A1", "A2", "br", "bi", "w")}
|
| 127 |
+
self._v = {k: np.zeros_like(getattr(self, k)) for k in ("A1", "A2", "br", "bi", "w")}
|
| 128 |
+
self._mc = self._vc = 0.0
|
| 129 |
+
self.t = 0
|
| 130 |
+
|
| 131 |
+
def fields(self, C, S):
|
| 132 |
+
ReS = C @ self.A1.T + S @ self.A2.T
|
| 133 |
+
ImS = S @ self.A1.T - C @ self.A2.T
|
| 134 |
+
Zr, Zi = ReS + self.br, ImS + self.bi
|
| 135 |
+
return Zr, Zi, Zr * Zr + Zi * Zi
|
| 136 |
+
|
| 137 |
+
def logit(self, C, S):
|
| 138 |
+
Zr, Zi, u = self.fields(C, S)
|
| 139 |
+
return ((u - self.mu) / self.sd) @ self.w + self.c, Zr, Zi, u
|
| 140 |
+
|
| 141 |
+
def step(self, C, S, y, lr, l2, mom=0.99):
|
| 142 |
+
B = len(y)
|
| 143 |
+
Zr, Zi, u = self.fields(C, S)
|
| 144 |
+
# Running standardisation of the fields. Treated as a constant in the gradient -- the standard
|
| 145 |
+
# inference-time treatment, and the running stats are what the held-out pass will use.
|
| 146 |
+
self.mu = mom * self.mu + (1 - mom) * u.mean(0)
|
| 147 |
+
self.sd = mom * self.sd + (1 - mom) * (u.std(0) + 1e-6)
|
| 148 |
+
un = (u - self.mu) / self.sd
|
| 149 |
+
z = un @ self.w + self.c
|
| 150 |
+
p = 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30)))
|
| 151 |
+
d = (p - y) / B # dL/dz
|
| 152 |
+
gw = un.T @ d + l2 * self.w
|
| 153 |
+
gc = d.sum()
|
| 154 |
+
gu = np.outer(d, self.w) / self.sd # dL/du (B,F)
|
| 155 |
+
gr, gi = 2.0 * gu * Zr, 2.0 * gu * Zi # dL/dReZ, dL/dImZ
|
| 156 |
+
gA1 = gr.T @ C + gi.T @ S + l2 * self.A1
|
| 157 |
+
gA2 = gr.T @ S - gi.T @ C + l2 * self.A2
|
| 158 |
+
gbr, gbi = gr.sum(0), gi.sum(0)
|
| 159 |
+
self.t += 1
|
| 160 |
+
for k, g in (("A1", gA1), ("A2", gA2), ("br", gbr), ("bi", gbi), ("w", gw)):
|
| 161 |
+
self._m[k] = 0.9 * self._m[k] + 0.1 * g
|
| 162 |
+
self._v[k] = 0.999 * self._v[k] + 0.001 * g * g
|
| 163 |
+
mh = self._m[k] / (1 - 0.9 ** self.t)
|
| 164 |
+
vh = self._v[k] / (1 - 0.999 ** self.t)
|
| 165 |
+
setattr(self, k, getattr(self, k) - lr * mh / (np.sqrt(vh) + 1e-8))
|
| 166 |
+
self.c -= lr * gc
|
| 167 |
+
return float(-np.mean(y * np.log(p + 1e-12) + (1 - y) * np.log(1 - p + 1e-12)))
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _gradcheck():
|
| 171 |
+
"""The exact gradients against finite differences. If this is wrong every number below is noise."""
|
| 172 |
+
g = np.random.default_rng(0)
|
| 173 |
+
B, K, F = 24, 9, 4
|
| 174 |
+
C, S = g.normal(size=(B, K)), g.normal(size=(B, K))
|
| 175 |
+
y = (g.random(B) < 0.5).astype(float)
|
| 176 |
+
m = Coherent(K, F, seed=1)
|
| 177 |
+
m.w = g.normal(0, 0.5, F)
|
| 178 |
+
m.mu, m.sd = np.zeros(F), np.ones(F)
|
| 179 |
+
|
| 180 |
+
def loss():
|
| 181 |
+
_, _, u = m.fields(C, S)
|
| 182 |
+
z = ((u - m.mu) / m.sd) @ m.w + m.c
|
| 183 |
+
p = 1 / (1 + np.exp(-z))
|
| 184 |
+
return float(-np.mean(y * np.log(p) + (1 - y) * np.log(1 - p)))
|
| 185 |
+
|
| 186 |
+
# analytic, with the running-stat update and the optimiser disabled
|
| 187 |
+
B_ = len(y)
|
| 188 |
+
Zr, Zi, u = m.fields(C, S)
|
| 189 |
+
z = ((u - m.mu) / m.sd) @ m.w + m.c
|
| 190 |
+
p = 1 / (1 + np.exp(-z))
|
| 191 |
+
d = (p - y) / B_
|
| 192 |
+
gu = np.outer(d, m.w) / m.sd
|
| 193 |
+
gr, gi = 2 * gu * Zr, 2 * gu * Zi
|
| 194 |
+
an = {"A1": gr.T @ C + gi.T @ S, "A2": gr.T @ S - gi.T @ C, "br": gr.sum(0), "bi": gi.sum(0)}
|
| 195 |
+
worst = 0.0
|
| 196 |
+
for name in an:
|
| 197 |
+
P = getattr(m, name)
|
| 198 |
+
num = np.zeros_like(P)
|
| 199 |
+
it = np.nditer(P, flags=["multi_index"])
|
| 200 |
+
for _ in it:
|
| 201 |
+
ix = it.multi_index
|
| 202 |
+
o = P[ix]
|
| 203 |
+
P[ix] = o + 1e-6
|
| 204 |
+
hi = loss()
|
| 205 |
+
P[ix] = o - 1e-6
|
| 206 |
+
lo = loss()
|
| 207 |
+
P[ix] = o
|
| 208 |
+
num[ix] = (hi - lo) / 2e-6
|
| 209 |
+
rel = np.abs(num - an[name]).max() / max(1e-12, np.abs(num).max())
|
| 210 |
+
worst = max(worst, rel)
|
| 211 |
+
assert rel < 2e-4, (name, rel)
|
| 212 |
+
print(f" gradcheck: analytic == finite-difference for A1, A2, Re b, Im b (worst relative {worst:.2e})")
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def main():
|
| 216 |
+
Z = np.load(os.environ.get("AQ_LON", "/tmp/aqcoh/lon.npz"))
|
| 217 |
+
LONtr, LONte = Z["lon_train"], Z["lon_test"]
|
| 218 |
+
ytr, yte = Z["y_train"], Z["y_test"]
|
| 219 |
+
yr_tr, yr_te = Z["yr_train"], Z["yr_test"] # (2, n) the two birth years
|
| 220 |
+
which = os.environ.get("AQ_SET", "fast")
|
| 221 |
+
F = int(os.environ.get("AQ_FIELDS") or 64)
|
| 222 |
+
EPOCHS = int(os.environ.get("AQ_EPOCHS") or 40)
|
| 223 |
+
LR = float(os.environ.get("AQ_LR") or 0.01)
|
| 224 |
+
L2 = float(os.environ.get("AQ_L2") or 1e-4)
|
| 225 |
+
SEEDS = int(os.environ.get("AQ_SEEDS") or 3)
|
| 226 |
+
TRACE = bool(os.environ.get("AQ_TRACE"))
|
| 227 |
+
PATIENCE = int(os.environ.get("AQ_PATIENCE") or 12)
|
| 228 |
+
bodies = SETS[which]
|
| 229 |
+
TWO = os.environ.get("AQ_TWO_SIDED", "1") not in ("0", "")
|
| 230 |
+
|
| 231 |
+
# DROP THE IDENTICAL-CHART ROWS FROM THE FIT, BY DEFAULT.
|
| 232 |
+
#
|
| 233 |
+
# dates.couple_record gives a partner with no known birth date the OTHER partner's instant, deliberately and
|
| 234 |
+
# documented -- every chart needs some instant, and self-comparison is a defined value rather than a guess
|
| 235 |
+
# about a stranger. For a one-sided feature that is harmless. For a COHERENT SUM OVER BOTH CHARTS it is not:
|
| 236 |
+
# when theta_older == theta_younger the sum collapses from SUM_k (w_Ok e^{ih th_Ok} + w_Yk e^{ih th_Yk}) to
|
| 237 |
+
# SUM_k (w_Ok + w_Yk) e^{ih th_k}, a different and smaller function class. Measured on this data that is
|
| 238 |
+
# 41.3% of training rows and 0.1% of held-out rows -- so 41% of the fit's gradient comes from a configuration
|
| 239 |
+
# that essentially never occurs at test time, and the fitted phases are pulled toward it.
|
| 240 |
+
#
|
| 241 |
+
# This is not specific to this module. Every cross-chart block in the stack -- ashtakoot, the Uranian dial
|
| 242 |
+
# distances, the composite and Davison charts -- is degenerate on the same 41% and was fitted through it.
|
| 243 |
+
if TWO:
|
| 244 |
+
keep = ~np.all(np.isclose(LONtr[0], LONtr[1], atol=1e-4), axis=0)
|
| 245 |
+
log(f" genuine pairs only: {int(keep.sum()):,} of {len(ytr):,} training rows "
|
| 246 |
+
f"({100*(~keep).mean():.1f}% dropped as identical-chart)")
|
| 247 |
+
LONtr, ytr, yr_tr = LONtr[:, :, keep], ytr[keep], yr_tr[:, keep]
|
| 248 |
+
|
| 249 |
+
ORB = float(os.environ.get('AQ_ORB') or 0)
|
| 250 |
+
Ctr, Str, kept = basis(LONtr, bodies, ORB)
|
| 251 |
+
Cte, Ste, _ = basis(LONte, bodies, ORB)
|
| 252 |
+
K = Ctr.shape[1]
|
| 253 |
+
log(f"set {which}: {len(bodies)} bodies, orb {ORB:g} deg -> {K} basis terms · "
|
| 254 |
+
f"{F} fields · train {len(ytr):,} · held out {len(yte):,}")
|
| 255 |
+
|
| 256 |
+
# TEMPORAL inner split: the latest births of the training half become the inner validation set.
|
| 257 |
+
later = yr_tr.max(0)
|
| 258 |
+
cutoff = np.quantile(later, 0.85)
|
| 259 |
+
inner = later > cutoff
|
| 260 |
+
log(f" inner validation = training births after {cutoff:.0f} ({inner.sum():,} rows), a TEMPORAL split")
|
| 261 |
+
|
| 262 |
+
best_te = None
|
| 263 |
+
aucs = []
|
| 264 |
+
for seed in range(SEEDS):
|
| 265 |
+
m = Coherent(K, F, seed=seed)
|
| 266 |
+
rng = np.random.default_rng(1000 + seed)
|
| 267 |
+
fit = ~inner
|
| 268 |
+
idx = np.where(fit)[0]
|
| 269 |
+
best_iv, best_state, bad = -1, None, 0
|
| 270 |
+
for ep in range(EPOCHS):
|
| 271 |
+
rng.shuffle(idx)
|
| 272 |
+
for s in range(0, len(idx), 4096):
|
| 273 |
+
b = idx[s:s + 4096]
|
| 274 |
+
if len(b) < 64:
|
| 275 |
+
continue
|
| 276 |
+
m.step(Ctr[b], Str[b], ytr[b].astype(float), LR, L2)
|
| 277 |
+
ziv, _, _, _ = m.logit(Ctr[inner], Str[inner])
|
| 278 |
+
a = auc(ytr[inner], ziv)
|
| 279 |
+
if TRACE:
|
| 280 |
+
log(f" seed {seed} epoch {ep:>3} inner {a:.4f}")
|
| 281 |
+
if a > best_iv + 1e-5:
|
| 282 |
+
best_iv, bad = a, 0
|
| 283 |
+
best_state = (m.A1.copy(), m.A2.copy(), m.br.copy(), m.bi.copy(), m.w.copy(),
|
| 284 |
+
m.c, m.mu.copy(), m.sd.copy())
|
| 285 |
+
else:
|
| 286 |
+
bad += 1
|
| 287 |
+
if bad >= PATIENCE:
|
| 288 |
+
break
|
| 289 |
+
m.A1, m.A2, m.br, m.bi, m.w, m.c, m.mu, m.sd = best_state
|
| 290 |
+
zte, _, _, ute = m.logit(Cte, Ste)
|
| 291 |
+
a_te = auc(yte, zte)
|
| 292 |
+
aucs.append(a_te)
|
| 293 |
+
log(f" seed {seed}: inner (temporal) {best_iv:.4f} -> HELD OUT {a_te:.4f}")
|
| 294 |
+
if best_te is None or best_iv > best_te[0]:
|
| 295 |
+
best_te = (best_iv, zte, ute, m)
|
| 296 |
+
|
| 297 |
+
# THE ONE PERMITTED COMPARISON: the two-parameter logistic on the age gap.
|
| 298 |
+
#
|
| 299 |
+
# It needs no fitting. b0 + b1*gap is MONOTONE in gap, and AUC is invariant under any monotone transform of
|
| 300 |
+
# the score, so the logistic's AUC is exactly max(AUC(gap), 1 - AUC(gap)) -- the sign of b1 being the only
|
| 301 |
+
# thing the fit decides. The gradient-descent version of this overflowed np.exp and reported the same number
|
| 302 |
+
# less reliably.
|
| 303 |
+
#
|
| 304 |
+
# Note what the dataset's own column definition does to this baseline: with the OLDER partner always first,
|
| 305 |
+
# the gap is non-negative by construction, so the SIGNED form that once distinguished man-older from
|
| 306 |
+
# woman-older is not expressible here. No sex is read anywhere in this dataset.
|
| 307 |
+
gap_te = (yr_te[1] - yr_te[0]).astype(float)
|
| 308 |
+
absent = (yr_te[0] == 0) | (yr_te[1] == 0)
|
| 309 |
+
assert not absent.any(), "a held-out row with an absent partner has no age gap"
|
| 310 |
+
g = auc(yte, gap_te)
|
| 311 |
+
gap_auc = max(g, 1 - g)
|
| 312 |
+
|
| 313 |
+
mean, sd = float(np.mean(aucs)), float(np.std(aucs))
|
| 314 |
+
print(f"\n COHERENT FIELD, set '{which}', {F} fields over {K} basis terms")
|
| 315 |
+
print(f" held-out AUC over {SEEDS} seeds: {mean:.4f} +- {sd:.4f} (best-inner seed {auc(yte, best_te[1]):.4f})")
|
| 316 |
+
print(f" age-gap logistic (2 parameters), same rows: {gap_auc:.4f}")
|
| 317 |
+
out = {"set": which, "fields": F, "basis": K, "held_out_mean": mean, "held_out_sd": sd,
|
| 318 |
+
"seeds": [float(a) for a in aucs], "age_gap": gap_auc}
|
| 319 |
+
d = os.environ.get("AQ_OUT", "/tmp/aqcoh")
|
| 320 |
+
json.dump(out, open(os.path.join(d, f"coherent_{which}.json"), "w"), indent=1)
|
| 321 |
+
np.savez_compressed(os.path.join(d, f"coherent_{which}_fields.npz"),
|
| 322 |
+
test_fields=best_te[2].astype(np.float32))
|
| 323 |
+
print(f" wrote {d}/coherent_{which}.json")
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
if __name__ == "__main__":
|
| 327 |
+
if os.environ.get("AQ_GRADCHECK") or "--gradcheck" in sys.argv:
|
| 328 |
+
_gradcheck()
|
| 329 |
+
else:
|
| 330 |
+
_gradcheck()
|
| 331 |
+
main()
|
kerykeion_phases.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
kerykeion_phases.py — the PHASE theta of every body at every time, through Kerykeion (sidereal, Lahiri).
|
| 3 |
+
|
| 4 |
+
Operator, 2026-08-18: "use kerykeion for getting the phase of each body at each time". Three instants per couple:
|
| 5 |
+
dad's birth and mom's birth at 09:00 LOCAL at each birthplace (time zone from the coordinates through
|
| 6 |
+
timezonefinder; Kerykeion converts to UT and casts, houses included), and the wedding at 12:00 UT (no wedding
|
| 7 |
+
place is in the data, so no wedding houses).
|
| 8 |
+
|
| 9 |
+
BODIES: Sun Moon Mercury Venus Mars Jupiter Saturn Uranus Neptune Pluto TrueNode(Rahu) TrueSouthNode(Ketu) Chiron
|
| 10 |
+
MeanLilith, and for the natal charts the Ascendant and MC. Precision-aware like everything else here: a
|
| 11 |
+
year-only birth (1st of January placeholder) gets only the bodies the year can place -- Jupiter and slower --
|
| 12 |
+
and no angles; a month-only birth adds the Sun; a year-only wedding gets the slow bodies only.
|
| 13 |
+
|
| 14 |
+
Writes AQ_OUT/phases.npz: theta_dad, theta_mom, theta_wed (rows x bodies, degrees, NaN where undefined),
|
| 15 |
+
bodies, y_train / ids, the plain columns for references. Kerykeion agrees with PyJHora to 0.005 degrees on the
|
| 16 |
+
same instant (checked); ~0.8 ms per chart, so the whole build is minutes.
|
| 17 |
+
"""
|
| 18 |
+
import datetime as dt
|
| 19 |
+
import multiprocessing as mp
|
| 20 |
+
import os
|
| 21 |
+
import sys
|
| 22 |
+
import time
|
| 23 |
+
import warnings
|
| 24 |
+
|
| 25 |
+
import numpy as np
|
| 26 |
+
import pandas as pd
|
| 27 |
+
|
| 28 |
+
warnings.filterwarnings("ignore")
|
| 29 |
+
SRC = os.environ.get("AQ_SRC", "/tmp/aq3")
|
| 30 |
+
OUT = os.environ.get("AQ_OUT", "/tmp/aq3feat")
|
| 31 |
+
LIMIT = int(os.environ.get("AQ_LIMIT") or 0)
|
| 32 |
+
BODIES = ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto",
|
| 33 |
+
"true_node", "true_south_node", "chiron", "mean_lilith", "ascendant", "medium_coeli"]
|
| 34 |
+
SLOW = {"jupiter", "saturn", "uranus", "neptune", "pluto", "true_node", "true_south_node", "chiron", "mean_lilith"}
|
| 35 |
+
ANGLES = {"ascendant", "medium_coeli"}
|
| 36 |
+
T0 = time.time()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def log(*a):
|
| 40 |
+
print(f"[{time.time()-T0:6.1f}s]", *a, flush=True)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
_TF = None
|
| 44 |
+
_TZC = {}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def tz_name(lat, lon):
|
| 48 |
+
global _TF
|
| 49 |
+
if _TF is None:
|
| 50 |
+
from timezonefinder import TimezoneFinder
|
| 51 |
+
_TF = TimezoneFinder()
|
| 52 |
+
key = (round(lat, 2), round(lon, 2))
|
| 53 |
+
if key not in _TZC:
|
| 54 |
+
_TZC[key] = _TF.timezone_at(lng=lon, lat=lat) or _TF.closest_timezone_at(lng=lon, lat=lat) or "UTC"
|
| 55 |
+
return _TZC[key]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def prec(dob):
|
| 59 |
+
if not dob or dob == "0000-00-00" or dob == "nan":
|
| 60 |
+
return 0
|
| 61 |
+
return 1 if dob.endswith("-00-00") else (2 if dob.endswith("-00") else 3)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def theta(dob, lat, lon, hour, natal):
|
| 65 |
+
"""Longitudes of BODIES at (dob, hour local at lat/lon), NaN where the date's precision cannot place them."""
|
| 66 |
+
from kerykeion import AstrologicalSubject
|
| 67 |
+
out = np.full(len(BODIES), np.nan)
|
| 68 |
+
p = prec(dob)
|
| 69 |
+
if p == 0:
|
| 70 |
+
return out
|
| 71 |
+
if natal and (lat is None or (isinstance(lat, float) and np.isnan(lat))):
|
| 72 |
+
return out
|
| 73 |
+
y, m, d = int(dob[:4]), max(1, int(dob[5:7])), max(1, int(dob[8:10]))
|
| 74 |
+
try:
|
| 75 |
+
if natal:
|
| 76 |
+
s = AstrologicalSubject("x", y, m, d, hour, 0, lng=float(lon), lat=float(lat), tz_str=tz_name(float(lat), float(lon)),
|
| 77 |
+
city="x", nation="XX", zodiac_type="Sidereal", sidereal_mode="LAHIRI", online=False)
|
| 78 |
+
else:
|
| 79 |
+
s = AstrologicalSubject("w", y, m, d, hour, 0, lng=0.0, lat=51.48, tz_str="UTC", city="Greenwich", nation="GB",
|
| 80 |
+
zodiac_type="Sidereal", sidereal_mode="LAHIRI", online=False)
|
| 81 |
+
except Exception:
|
| 82 |
+
return out
|
| 83 |
+
for j, b in enumerate(BODIES):
|
| 84 |
+
if b in ANGLES and (not natal or p < 3):
|
| 85 |
+
continue
|
| 86 |
+
if p == 1 and b not in SLOW:
|
| 87 |
+
continue
|
| 88 |
+
if p == 2 and b not in SLOW and b != "sun":
|
| 89 |
+
continue
|
| 90 |
+
try:
|
| 91 |
+
out[j] = float(getattr(s, b).abs_pos)
|
| 92 |
+
except Exception:
|
| 93 |
+
pass
|
| 94 |
+
return out
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _work(args):
|
| 98 |
+
i, dd, latd, lond, dm, latm, lonm, start = args
|
| 99 |
+
wed = start if start[5:] != "01-01" else start[:4] + "-00-00" # a 1 January start is a year-only record
|
| 100 |
+
return i, theta(dd, latd, lond, 9, True), theta(dm, latm, lonm, 9, True), theta(wed, None, None, 12, False)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def build(df):
|
| 104 |
+
jobs = [(i, r.dob_dad, r.lat_dad, r.lon_dad, r.dob_mom, r.lat_mom, r.lon_mom, r.start)
|
| 105 |
+
for i, r in enumerate(df.itertuples(index=False))]
|
| 106 |
+
with mp.Pool(max(1, mp.cpu_count() - 1)) as pool:
|
| 107 |
+
res = pool.map(_work, jobs, chunksize=256)
|
| 108 |
+
n = len(df); D = np.full((n, len(BODIES)), np.nan); M = D.copy(); W = D.copy()
|
| 109 |
+
for i, a, b, c in res:
|
| 110 |
+
D[i], M[i], W[i] = a, b, c
|
| 111 |
+
return D, M, W
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def main():
|
| 115 |
+
tr = pd.read_csv(f"{SRC}/train.csv", dtype={"dob_dad": str, "dob_mom": str, "start": str})
|
| 116 |
+
te = pd.read_csv(f"{SRC}/test.csv", dtype={"dob_dad": str, "dob_mom": str, "start": str})
|
| 117 |
+
LABEL = [c for c in tr.columns if c not in {"id", "dob_dad", "dob_mom", "lat_dad", "lon_dad", "lat_mom", "lon_mom", "start"}][0]
|
| 118 |
+
if LIMIT:
|
| 119 |
+
tr, te = tr.head(LIMIT), te.head(max(200, LIMIT // 4)); log(f"AQ_LIMIT={LIMIT}: DRY RUN")
|
| 120 |
+
log(f"train {len(tr):,} · test {len(te):,}")
|
| 121 |
+
Dtr, Mtr, Wtr = build(tr); log("train phases")
|
| 122 |
+
Dte, Mte, Wte = build(te); log("test phases")
|
| 123 |
+
def plain(df):
|
| 124 |
+
yd = pd.to_numeric(df.dob_dad.str[:4], errors="coerce").where(df.dob_dad != "0000-00-00")
|
| 125 |
+
ym = pd.to_numeric(df.dob_mom.str[:4], errors="coerce").where(df.dob_mom != "0000-00-00")
|
| 126 |
+
sy = df.start.str[:4].astype(float)
|
| 127 |
+
return np.column_stack([sy - yd, sy - ym, ym - yd, sy, (df.start.str[5:] == "01-01").astype(float)])
|
| 128 |
+
os.makedirs(OUT, exist_ok=True)
|
| 129 |
+
np.savez_compressed(f"{OUT}/phases.npz", theta_dad_train=Dtr, theta_mom_train=Mtr, theta_wed_train=Wtr,
|
| 130 |
+
theta_dad_test=Dte, theta_mom_test=Mte, theta_wed_test=Wte, bodies=np.array(BODIES, dtype=object),
|
| 131 |
+
y_train=tr[LABEL].to_numpy().astype(np.int8), id_test=te.id.to_numpy() if "id" in te else np.arange(len(te)),
|
| 132 |
+
plain_train=plain(tr), plain_test=plain(te),
|
| 133 |
+
plain_names=np.array(["age_dad_at_start", "age_mom_at_start", "age_gap", "start_year", "start_is_jan1"], dtype=object),
|
| 134 |
+
yr_train=np.column_stack([pd.to_numeric(tr.dob_dad.str[:4], errors="coerce").fillna(0),
|
| 135 |
+
pd.to_numeric(tr.dob_mom.str[:4], errors="coerce").fillna(0)]).astype(np.int16))
|
| 136 |
+
full = np.isfinite(Dtr).all(1) & np.isfinite(Mtr).all(1)
|
| 137 |
+
log(f"wrote {OUT}/phases.npz · {len(BODIES)} bodies · train rows with BOTH natal charts complete: {full.sum():,} · "
|
| 138 |
+
f"wedding sky complete: {np.isfinite(Wtr[:, :10]).all(1).sum():,}")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
if __name__ == "__main__":
|
| 142 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy
|
| 2 |
+
scipy
|
| 3 |
+
kerykeion
|
| 4 |
+
timezonefinder
|