research(rectification): angle-timing A1 — real vs shuffled and age-preserving jittered dates, 0/9 tests significant (BUG-1141)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f507570448
commit
915c3e829d
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"case_count": 77,
|
||||
"dataset": "v5",
|
||||
"event_cutoff": "2026-09-14",
|
||||
"holdout_sha256": "ab365a3e3f4953d4b538f2c0889dc98ab89444a28340c3a7b29e2a864cdd687a",
|
||||
"jitter_null": {
|
||||
"null": "each event moved 1-3 years earlier/later, precision kept",
|
||||
"shuffles": 50,
|
||||
"table": {
|
||||
"SA": {
|
||||
"1.0": {
|
||||
"10": {
|
||||
"jitter_mean": 0.493909,
|
||||
"percentile_beaten": 8.0,
|
||||
"real_mean_rank": 0.532143
|
||||
},
|
||||
"30": {
|
||||
"jitter_mean": 0.506271,
|
||||
"percentile_beaten": 0.0,
|
||||
"real_mean_rank": 0.556061
|
||||
},
|
||||
"60": {
|
||||
"jitter_mean": 0.527833,
|
||||
"percentile_beaten": 0.0,
|
||||
"real_mean_rank": 0.584632
|
||||
}
|
||||
},
|
||||
"2.0": {
|
||||
"10": {
|
||||
"jitter_mean": 0.489409,
|
||||
"percentile_beaten": 2.0,
|
||||
"real_mean_rank": 0.547403
|
||||
},
|
||||
"30": {
|
||||
"jitter_mean": 0.509255,
|
||||
"percentile_beaten": 0.0,
|
||||
"real_mean_rank": 0.574675
|
||||
},
|
||||
"60": {
|
||||
"jitter_mean": 0.535249,
|
||||
"percentile_beaten": 0.0,
|
||||
"real_mean_rank": 0.592154
|
||||
}
|
||||
},
|
||||
"3.0": {
|
||||
"10": {
|
||||
"jitter_mean": 0.521558,
|
||||
"percentile_beaten": 88.0,
|
||||
"real_mean_rank": 0.491883
|
||||
},
|
||||
"30": {
|
||||
"jitter_mean": 0.537597,
|
||||
"percentile_beaten": 66.0,
|
||||
"real_mean_rank": 0.526082
|
||||
},
|
||||
"60": {
|
||||
"jitter_mean": 0.561689,
|
||||
"percentile_beaten": 72.0,
|
||||
"real_mean_rank": 0.548647
|
||||
}
|
||||
}
|
||||
},
|
||||
"SP": {
|
||||
"1.0": {
|
||||
"10": {
|
||||
"jitter_mean": 0.473714,
|
||||
"percentile_beaten": 0.0,
|
||||
"real_mean_rank": 0.521429
|
||||
},
|
||||
"30": {
|
||||
"jitter_mean": 0.477385,
|
||||
"percentile_beaten": 7.0,
|
||||
"real_mean_rank": 0.511255
|
||||
},
|
||||
"60": {
|
||||
"jitter_mean": 0.488568,
|
||||
"percentile_beaten": 10.0,
|
||||
"real_mean_rank": 0.518074
|
||||
}
|
||||
},
|
||||
"2.0": {
|
||||
"10": {
|
||||
"jitter_mean": 0.495104,
|
||||
"percentile_beaten": 25.0,
|
||||
"real_mean_rank": 0.512338
|
||||
},
|
||||
"30": {
|
||||
"jitter_mean": 0.500193,
|
||||
"percentile_beaten": 30.0,
|
||||
"real_mean_rank": 0.512446
|
||||
},
|
||||
"60": {
|
||||
"jitter_mean": 0.515154,
|
||||
"percentile_beaten": 32.0,
|
||||
"real_mean_rank": 0.526407
|
||||
}
|
||||
},
|
||||
"3.0": {
|
||||
"10": {
|
||||
"jitter_mean": 0.4975,
|
||||
"percentile_beaten": 71.0,
|
||||
"real_mean_rank": 0.485065
|
||||
},
|
||||
"30": {
|
||||
"jitter_mean": 0.501734,
|
||||
"percentile_beaten": 60.0,
|
||||
"real_mean_rank": 0.49513
|
||||
},
|
||||
"60": {
|
||||
"jitter_mean": 0.516921,
|
||||
"percentile_beaten": 56.0,
|
||||
"real_mean_rank": 0.511959
|
||||
}
|
||||
}
|
||||
},
|
||||
"TR": {
|
||||
"1.0": {
|
||||
"10": {
|
||||
"jitter_mean": 0.519,
|
||||
"percentile_beaten": 50.0,
|
||||
"real_mean_rank": 0.516558
|
||||
},
|
||||
"30": {
|
||||
"jitter_mean": 0.520831,
|
||||
"percentile_beaten": 62.0,
|
||||
"real_mean_rank": 0.513745
|
||||
},
|
||||
"60": {
|
||||
"jitter_mean": 0.524524,
|
||||
"percentile_beaten": 62.0,
|
||||
"real_mean_rank": 0.520833
|
||||
}
|
||||
},
|
||||
"2.0": {
|
||||
"10": {
|
||||
"jitter_mean": 0.497753,
|
||||
"percentile_beaten": 62.0,
|
||||
"real_mean_rank": 0.494481
|
||||
},
|
||||
"30": {
|
||||
"jitter_mean": 0.505554,
|
||||
"percentile_beaten": 70.0,
|
||||
"real_mean_rank": 0.492749
|
||||
},
|
||||
"60": {
|
||||
"jitter_mean": 0.510961,
|
||||
"percentile_beaten": 60.0,
|
||||
"real_mean_rank": 0.504437
|
||||
}
|
||||
},
|
||||
"3.0": {
|
||||
"10": {
|
||||
"jitter_mean": 0.496377,
|
||||
"percentile_beaten": 20.0,
|
||||
"real_mean_rank": 0.512013
|
||||
},
|
||||
"30": {
|
||||
"jitter_mean": 0.502379,
|
||||
"percentile_beaten": 52.0,
|
||||
"real_mean_rank": 0.499892
|
||||
},
|
||||
"60": {
|
||||
"jitter_mean": 0.509171,
|
||||
"percentile_beaten": 54.0,
|
||||
"real_mean_rank": 0.507143
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"node": "mean",
|
||||
"orbs": [
|
||||
1.0,
|
||||
2.0,
|
||||
3.0
|
||||
],
|
||||
"python_version": "3.13.5",
|
||||
"radii": [
|
||||
10,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"techniques": {
|
||||
"SA": "solar-arc ASC / MC to natal Sun..Saturn, 0/90/180",
|
||||
"SP": "secondary-progressed ASC / MC (quotidian date method, same location) to natal Sun..Saturn, 0/90/180",
|
||||
"TR": "transit Saturn / Jupiter / mean Rahu-Ketu (+Mars for day events) to natal ASC / MC, 0/90/180"
|
||||
},
|
||||
"zodiac": "tropical (aspects are zodiac-independent)"
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
{
|
||||
"case_count": 77,
|
||||
"dataset": "v5",
|
||||
"event_cutoff": "2026-09-14",
|
||||
"holdout_sha256": "ab365a3e3f4953d4b538f2c0889dc98ab89444a28340c3a7b29e2a864cdd687a",
|
||||
"node": "mean",
|
||||
"orbs": [
|
||||
1.0,
|
||||
2.0,
|
||||
3.0
|
||||
],
|
||||
"python_version": "3.13.5",
|
||||
"radii": [
|
||||
10,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"subsets": {
|
||||
"note": "descriptive robustness; significance is judged only on the pre-registered A1 tests",
|
||||
"radius": 30,
|
||||
"shuffles": 50,
|
||||
"subsets": {
|
||||
"era=1900_and_later": {
|
||||
"cases": 70,
|
||||
"table": {
|
||||
"SA": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 16.0,
|
||||
"real_mean_rank": 0.542976,
|
||||
"shuffle_mean": 0.511831
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 2.0,
|
||||
"real_mean_rank": 0.572738,
|
||||
"shuffle_mean": 0.510495
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 30.0,
|
||||
"real_mean_rank": 0.529167,
|
||||
"shuffle_mean": 0.506257
|
||||
}
|
||||
},
|
||||
"SP": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 44.0,
|
||||
"real_mean_rank": 0.509167,
|
||||
"shuffle_mean": 0.50255
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 54.0,
|
||||
"real_mean_rank": 0.492857,
|
||||
"shuffle_mean": 0.493438
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 66.0,
|
||||
"real_mean_rank": 0.484167,
|
||||
"shuffle_mean": 0.495136
|
||||
}
|
||||
},
|
||||
"TR": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 60.0,
|
||||
"real_mean_rank": 0.495714,
|
||||
"shuffle_mean": 0.498952
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 74.0,
|
||||
"real_mean_rank": 0.482976,
|
||||
"shuffle_mean": 0.501769
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 74.0,
|
||||
"real_mean_rank": 0.483452,
|
||||
"shuffle_mean": 0.508669
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"era=lmt_before_1900": {
|
||||
"cases": 7,
|
||||
"table": {
|
||||
"SA": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 2.0,
|
||||
"real_mean_rank": 0.686905,
|
||||
"shuffle_mean": 0.500762
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 20.0,
|
||||
"real_mean_rank": 0.594048,
|
||||
"shuffle_mean": 0.503024
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 60.0,
|
||||
"real_mean_rank": 0.495238,
|
||||
"shuffle_mean": 0.506714
|
||||
}
|
||||
},
|
||||
"SP": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 24.0,
|
||||
"real_mean_rank": 0.532143,
|
||||
"shuffle_mean": 0.466405
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 0.0,
|
||||
"real_mean_rank": 0.708333,
|
||||
"shuffle_mean": 0.485786
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 6.0,
|
||||
"real_mean_rank": 0.604762,
|
||||
"shuffle_mean": 0.468667
|
||||
}
|
||||
},
|
||||
"TR": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 2.0,
|
||||
"real_mean_rank": 0.694048,
|
||||
"shuffle_mean": 0.51519
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 19.0,
|
||||
"real_mean_rank": 0.590476,
|
||||
"shuffle_mean": 0.505405
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 0.0,
|
||||
"real_mean_rank": 0.664286,
|
||||
"shuffle_mean": 0.498833
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"precision=day": {
|
||||
"cases": 77,
|
||||
"table": {
|
||||
"SA": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 12.0,
|
||||
"real_mean_rank": 0.518506,
|
||||
"shuffle_mean": 0.497093
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 10.0,
|
||||
"real_mean_rank": 0.537229,
|
||||
"shuffle_mean": 0.503887
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 44.0,
|
||||
"real_mean_rank": 0.509199,
|
||||
"shuffle_mean": 0.501392
|
||||
}
|
||||
},
|
||||
"SP": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 4.0,
|
||||
"real_mean_rank": 0.553788,
|
||||
"shuffle_mean": 0.494117
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 16.0,
|
||||
"real_mean_rank": 0.529004,
|
||||
"shuffle_mean": 0.501039
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 14.0,
|
||||
"real_mean_rank": 0.520887,
|
||||
"shuffle_mean": 0.501656
|
||||
}
|
||||
},
|
||||
"TR": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 54.0,
|
||||
"real_mean_rank": 0.49513,
|
||||
"shuffle_mean": 0.497506
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 69.0,
|
||||
"real_mean_rank": 0.480519,
|
||||
"shuffle_mean": 0.498214
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 44.0,
|
||||
"real_mean_rank": 0.50487,
|
||||
"shuffle_mean": 0.500452
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"precision=month": {
|
||||
"cases": 77,
|
||||
"table": {
|
||||
"SA": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 16.0,
|
||||
"real_mean_rank": 0.521104,
|
||||
"shuffle_mean": 0.498039
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 12.0,
|
||||
"real_mean_rank": 0.529329,
|
||||
"shuffle_mean": 0.503056
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 94.0,
|
||||
"real_mean_rank": 0.474675,
|
||||
"shuffle_mean": 0.506771
|
||||
}
|
||||
},
|
||||
"SP": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 60.0,
|
||||
"real_mean_rank": 0.491017,
|
||||
"shuffle_mean": 0.499344
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 64.0,
|
||||
"real_mean_rank": 0.49145,
|
||||
"shuffle_mean": 0.498271
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 22.0,
|
||||
"real_mean_rank": 0.512446,
|
||||
"shuffle_mean": 0.497991
|
||||
}
|
||||
},
|
||||
"TR": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 8.0,
|
||||
"real_mean_rank": 0.524784,
|
||||
"shuffle_mean": 0.499639
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 16.0,
|
||||
"real_mean_rank": 0.517532,
|
||||
"shuffle_mean": 0.498294
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 50.0,
|
||||
"real_mean_rank": 0.499784,
|
||||
"shuffle_mean": 0.499158
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"precision=year": {
|
||||
"cases": 77,
|
||||
"table": {
|
||||
"SA": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 20.0,
|
||||
"real_mean_rank": 0.529978,
|
||||
"shuffle_mean": 0.504385
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 2.0,
|
||||
"real_mean_rank": 0.557251,
|
||||
"shuffle_mean": 0.502032
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 10.0,
|
||||
"real_mean_rank": 0.545887,
|
||||
"shuffle_mean": 0.507058
|
||||
}
|
||||
},
|
||||
"SP": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 80.0,
|
||||
"real_mean_rank": 0.474026,
|
||||
"shuffle_mean": 0.49887
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 62.0,
|
||||
"real_mean_rank": 0.492316,
|
||||
"shuffle_mean": 0.499013
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 62.0,
|
||||
"real_mean_rank": 0.488961,
|
||||
"shuffle_mean": 0.495091
|
||||
}
|
||||
},
|
||||
"TR": {
|
||||
"1.0": {
|
||||
"percentile_beaten": 50.0,
|
||||
"real_mean_rank": 0.5,
|
||||
"shuffle_mean": 0.5
|
||||
},
|
||||
"2.0": {
|
||||
"percentile_beaten": 50.0,
|
||||
"real_mean_rank": 0.5,
|
||||
"shuffle_mean": 0.5
|
||||
},
|
||||
"3.0": {
|
||||
"percentile_beaten": 50.0,
|
||||
"real_mean_rank": 0.5,
|
||||
"shuffle_mean": 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"techniques": {
|
||||
"SA": "solar-arc ASC / MC to natal Sun..Saturn, 0/90/180",
|
||||
"SP": "secondary-progressed ASC / MC (quotidian date method, same location) to natal Sun..Saturn, 0/90/180",
|
||||
"TR": "transit Saturn / Jupiter / mean Rahu-Ketu (+Mars for day events) to natal ASC / MC, 0/90/180"
|
||||
},
|
||||
"zodiac": "tropical (aspects are zodiac-independent)"
|
||||
}
|
||||
@@ -251,3 +251,27 @@ def stable_json(payload: Any) -> str:
|
||||
|
||||
def sha256_of(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def jittered_events(case: dict[str, Any], rng: Random) -> list[dict[str, Any]]:
|
||||
"""Second null: each event moved 1-3 years earlier or later (same precision, month/day kept),
|
||||
which keeps the age structure of real biographies; clamped to after birth and before the cutoff."""
|
||||
birth = birth_local(case).date()
|
||||
out = []
|
||||
for raw in case["events"]:
|
||||
text = str(raw.get("date") or "")
|
||||
p = str(raw.get("precision") or "")
|
||||
try:
|
||||
year = int(text[:4])
|
||||
except ValueError:
|
||||
out.append(dict(raw)); continue
|
||||
shift = rng.choice((-1, 1)) * rng.randint(1, 3)
|
||||
new_year = min(max(year + shift, birth.year + 1), EVENT_CUTOFF.year - 1)
|
||||
if p == "day" and len(text) >= 10:
|
||||
m, d = int(text[5:7]), int(text[8:10]); d = min(d, calendar.monthrange(new_year, m)[1]); new = f"{new_year:04d}-{m:02d}-{d:02d}"
|
||||
elif p == "month" and len(text) >= 7:
|
||||
new = f"{new_year:04d}-{text[5:7]}"
|
||||
else:
|
||||
new = f"{new_year:04d}"
|
||||
out.append({**raw, "date": new})
|
||||
return out
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"""Angle-timing research runner (BUG-1141).
|
||||
|
||||
A1: for each technique × orb, compare where the truth minute ranks inside its
|
||||
window when events keep their real dates versus shuffled dates. Pre-registered
|
||||
tests: TR / SP / SA × orbs 1° / 2° / 3° (9 tests), primary radius ±30, with
|
||||
±10 / ±60 reported. Score per candidate = number of events with a contact within
|
||||
the orb; truth rank = share of other window minutes scoring higher (ties half);
|
||||
0.5 = chance. Real mean rank is compared with the shuffled-date distribution.
|
||||
|
||||
A2 (only for techniques significant in A1): see run_a2.
|
||||
|
||||
Usage:
|
||||
PYTHONHASHSEED=0 python3 -m scripts.research.angle_timing_research --stages a1 \
|
||||
--shuffles 200 --json-out docs/research/angle_timing_a1_2026_10_01.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from random import Random
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.research import angle_timing_lib as at # noqa: E402
|
||||
|
||||
RADII = (10, 30, 60)
|
||||
PRIMARY_RADIUS = 30
|
||||
N_TESTS = len(at.TECHNIQUES) * len(at.ORBS)
|
||||
BONFERRONI_PERCENTILE = 100.0 * (1.0 - 0.05 / N_TESTS)
|
||||
UNCORRECTED_PERCENTILE = 95.0
|
||||
|
||||
|
||||
def ranks_for(chart: at.CaseChart, raw_events: list[dict[str, Any]] | None) -> dict[str, dict[str, dict[str, float]]]:
|
||||
events = chart.events(raw_events)
|
||||
out: dict[str, dict[str, dict[str, float]]] = {}
|
||||
for tech in at.TECHNIQUES:
|
||||
seps = chart.separations(tech, events)
|
||||
out[tech] = {}
|
||||
for orb in at.ORBS:
|
||||
scores = at.hit_counts(seps, orb) if seps.shape[0] else np.zeros(len(chart.offsets))
|
||||
out[tech][str(orb)] = {str(r): at.truth_rank(scores, r) for r in RADII}
|
||||
return out
|
||||
|
||||
|
||||
def run_a1(cases: list[dict[str, Any]], shuffles: int, log) -> dict[str, Any]:
|
||||
charts = [at.CaseChart(case) for case in cases]
|
||||
real = {chart.case_id: ranks_for(chart, None) for chart in charts}
|
||||
usable = {chart.case_id: {t: int(chart.separations(t, chart.events()).shape[0]) for t in at.TECHNIQUES} for chart in charts}
|
||||
shuffled_means: dict[str, dict[str, dict[str, list[float]]]] = {
|
||||
t: {str(o): {str(r): [] for r in RADII} for o in at.ORBS} for t in at.TECHNIQUES}
|
||||
started = time.perf_counter()
|
||||
for rep in range(shuffles):
|
||||
per_case = []
|
||||
for chart in charts:
|
||||
rng = Random(f"angle-timing|{chart.case_id}|{rep}")
|
||||
per_case.append(ranks_for(chart, at.shuffled_events(chart.case, rng)))
|
||||
for t in at.TECHNIQUES:
|
||||
for o in at.ORBS:
|
||||
for r in RADII:
|
||||
shuffled_means[t][str(o)][str(r)].append(round(float(np.mean([pc[t][str(o)][str(r)] for pc in per_case])), 6))
|
||||
if (rep + 1) % 20 == 0:
|
||||
log(f"shuffle {rep + 1}/{shuffles} ({time.perf_counter() - started:.0f}s)")
|
||||
table: dict[str, Any] = {}
|
||||
for t in at.TECHNIQUES:
|
||||
table[t] = {}
|
||||
for o in at.ORBS:
|
||||
table[t][str(o)] = {}
|
||||
for r in RADII:
|
||||
real_mean = round(float(np.mean([real[cid][t][str(o)][str(r)] for cid in real])), 6)
|
||||
dist = np.array(shuffled_means[t][str(o)][str(r)])
|
||||
# percentile: share of shuffles the real dates beat (lower rank is better)
|
||||
pct = round(100.0 * float(((dist > real_mean).sum() + 0.5 * (dist == real_mean).sum()) / len(dist)), 3)
|
||||
table[t][str(o)][str(r)] = {
|
||||
"real_mean_rank": real_mean,
|
||||
"shuffle_mean": round(float(dist.mean()), 6),
|
||||
"shuffle_sd": round(float(dist.std(ddof=1)), 6) if len(dist) > 1 else 0.0,
|
||||
"shuffle_p05": round(float(np.percentile(dist, 5)), 6),
|
||||
"percentile_beaten": pct,
|
||||
"significant_uncorrected": bool(pct >= UNCORRECTED_PERCENTILE),
|
||||
"significant_bonferroni": bool(pct >= BONFERRONI_PERCENTILE),
|
||||
}
|
||||
primary = {t: {str(o): table[t][str(o)][str(PRIMARY_RADIUS)] for o in at.ORBS} for t in at.TECHNIQUES}
|
||||
significant = sorted(f"{t}@{o}" for t in at.TECHNIQUES for o in at.ORBS
|
||||
if primary[t][str(o)]["significant_bonferroni"])
|
||||
return {
|
||||
"tests": N_TESTS,
|
||||
"primary_radius": PRIMARY_RADIUS,
|
||||
"bonferroni_percentile": round(BONFERRONI_PERCENTILE, 4),
|
||||
"uncorrected_percentile": UNCORRECTED_PERCENTILE,
|
||||
"shuffles": shuffles,
|
||||
"table": table,
|
||||
"significant_primary_bonferroni": significant,
|
||||
"usable_events_per_case": usable,
|
||||
"real_per_case": real,
|
||||
"shuffled_means": shuffled_means,
|
||||
}
|
||||
|
||||
|
||||
def _precision_filter(raw_events: list[dict[str, Any]], precision: str) -> list[dict[str, Any]]:
|
||||
return [e for e in raw_events if str(e.get("precision")) == precision]
|
||||
|
||||
|
||||
def run_jitter_null(cases: list[dict[str, Any]], shuffles: int, log) -> dict[str, Any]:
|
||||
"""Second null (age-structure preserving): real ranks vs events moved ±1-3 years, all radii."""
|
||||
charts = [at.CaseChart(case) for case in cases]
|
||||
real = [ranks_for(chart, None) for chart in charts]
|
||||
dist: dict[str, dict[str, dict[str, list[float]]]] = {t: {str(o): {str(r): [] for r in RADII} for o in at.ORBS} for t in at.TECHNIQUES}
|
||||
for rep in range(shuffles):
|
||||
per = [ranks_for(c, at.jittered_events(c.case, Random(f"angle-timing-jitter|{c.case_id}|{rep}"))) for c in charts]
|
||||
for t in at.TECHNIQUES:
|
||||
for o in at.ORBS:
|
||||
for r in RADII:
|
||||
dist[t][str(o)][str(r)].append(round(float(np.mean([pc[t][str(o)][str(r)] for pc in per])), 6))
|
||||
if (rep + 1) % 10 == 0:
|
||||
log(f"jitter {rep + 1}/{shuffles}")
|
||||
table: dict[str, Any] = {}
|
||||
for t in at.TECHNIQUES:
|
||||
table[t] = {}
|
||||
for o in at.ORBS:
|
||||
table[t][str(o)] = {}
|
||||
for r in RADII:
|
||||
real_mean = round(float(np.mean([x[t][str(o)][str(r)] for x in real])), 6)
|
||||
d = np.array(dist[t][str(o)][str(r)])
|
||||
pct = round(100.0 * float(((d > real_mean).sum() + 0.5 * (d == real_mean).sum()) / len(d)), 3)
|
||||
table[t][str(o)][str(r)] = {"real_mean_rank": real_mean, "jitter_mean": round(float(d.mean()), 6),
|
||||
"percentile_beaten": pct}
|
||||
return {"shuffles": shuffles, "null": "each event moved 1-3 years earlier/later, precision kept", "table": table}
|
||||
|
||||
|
||||
def run_subsets(cases: list[dict[str, Any]], shuffles: int, log) -> dict[str, Any]:
|
||||
"""Robustness: real vs shuffled by event precision and by LMT era (primary radius only)."""
|
||||
charts = [at.CaseChart(case) for case in cases]
|
||||
lmt = {chart.case_id for chart in charts if chart.local.year < 1900}
|
||||
subsets: dict[str, tuple[list[at.CaseChart], str | None]] = {
|
||||
"precision=day": (charts, "day"), "precision=month": (charts, "month"), "precision=year": (charts, "year"),
|
||||
"era=lmt_before_1900": ([c for c in charts if c.case_id in lmt], None),
|
||||
"era=1900_and_later": ([c for c in charts if c.case_id not in lmt], None),
|
||||
}
|
||||
out: dict[str, Any] = {}
|
||||
for name, (members, precision) in subsets.items():
|
||||
def ranks(chart: at.CaseChart, raw: list[dict[str, Any]]):
|
||||
events = _precision_filter(raw, precision) if precision else raw
|
||||
return ranks_for(chart, events)
|
||||
real = [ranks(c, list(c.case["events"])) for c in members]
|
||||
dist: dict[str, dict[str, list[float]]] = {t: {str(o): [] for o in at.ORBS} for t in at.TECHNIQUES}
|
||||
for rep in range(shuffles):
|
||||
per = [ranks(c, at.shuffled_events(c.case, Random(f"angle-timing-sub|{name}|{c.case_id}|{rep}"))) for c in members]
|
||||
for t in at.TECHNIQUES:
|
||||
for o in at.ORBS:
|
||||
dist[t][str(o)].append(round(float(np.mean([pc[t][str(o)][str(PRIMARY_RADIUS)] for pc in per])), 6))
|
||||
out[name] = {"cases": len(members), "table": {}}
|
||||
for t in at.TECHNIQUES:
|
||||
out[name]["table"][t] = {}
|
||||
for o in at.ORBS:
|
||||
real_mean = round(float(np.mean([r[t][str(o)][str(PRIMARY_RADIUS)] for r in real])), 6)
|
||||
d = np.array(dist[t][str(o)])
|
||||
pct = round(100.0 * float(((d > real_mean).sum() + 0.5 * (d == real_mean).sum()) / len(d)), 3)
|
||||
out[name]["table"][t][str(o)] = {"real_mean_rank": real_mean, "shuffle_mean": round(float(d.mean()), 6),
|
||||
"percentile_beaten": pct}
|
||||
log(f"subset {name} done")
|
||||
return {"radius": PRIMARY_RADIUS, "shuffles": shuffles, "subsets": out,
|
||||
"note": "descriptive robustness; significance is judged only on the pre-registered A1 tests"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--stages", default="a1")
|
||||
parser.add_argument("--shuffles", type=int, default=200)
|
||||
parser.add_argument("--subset-shuffles", type=int, default=50)
|
||||
parser.add_argument("--limit", type=int, default=0)
|
||||
parser.add_argument("--json-out", required=True)
|
||||
parser.add_argument("--quiet", action="store_true")
|
||||
args = parser.parse_args()
|
||||
log = (lambda _m: None) if args.quiet else (lambda m: print(m, file=sys.stderr, flush=True))
|
||||
cases = at.load_cases()
|
||||
if args.limit:
|
||||
cases = cases[: args.limit]
|
||||
payload: dict[str, Any] = {
|
||||
"dataset": "v5",
|
||||
"holdout_sha256": at.sha256_of(at.HOLDOUT_V5),
|
||||
"case_count": len(cases),
|
||||
"zodiac": "tropical (aspects are zodiac-independent)",
|
||||
"node": "mean",
|
||||
"techniques": {
|
||||
"TR": "transit Saturn / Jupiter / mean Rahu-Ketu (+Mars for day events) to natal ASC / MC, 0/90/180",
|
||||
"SP": "secondary-progressed ASC / MC (quotidian date method, same location) to natal Sun..Saturn, 0/90/180",
|
||||
"SA": "solar-arc ASC / MC to natal Sun..Saturn, 0/90/180",
|
||||
},
|
||||
"orbs": list(at.ORBS),
|
||||
"radii": list(RADII),
|
||||
"event_cutoff": at.EVENT_CUTOFF.isoformat(),
|
||||
"python_version": platform.python_version(),
|
||||
}
|
||||
stages = {s.strip() for s in args.stages.split(",") if s.strip()}
|
||||
if "a1" in stages:
|
||||
payload["a1"] = run_a1(cases, args.shuffles, log)
|
||||
if "jitter" in stages:
|
||||
payload["jitter_null"] = run_jitter_null(cases, args.subset_shuffles, log)
|
||||
if "subsets" in stages:
|
||||
payload["subsets"] = run_subsets(cases, args.subset_shuffles, log)
|
||||
out = Path(args.json_out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(at.stable_json(payload), encoding="utf-8")
|
||||
log(f"wrote {out}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -81,3 +81,21 @@ def test_separation_matrices_have_one_row_per_usable_event():
|
||||
assert tr.shape[0] == sum(1 for e in events if e.precision != "year")
|
||||
assert sp.shape == (len(events), len(chart.offsets))
|
||||
assert np.isfinite(sp).all() and (sp >= 0).all() and (sp <= 45).all()
|
||||
|
||||
|
||||
def test_a1_runner_is_deterministic_and_reports_every_preregistered_test():
|
||||
from scripts.research import angle_timing_research as runner
|
||||
|
||||
cases = at.load_cases()[:2]
|
||||
first = runner.run_a1(cases, 3, lambda _m: None)
|
||||
second = runner.run_a1(cases, 3, lambda _m: None)
|
||||
assert at.stable_json(first) == at.stable_json(second)
|
||||
assert first["tests"] == 9
|
||||
assert first["bonferroni_percentile"] == pytest.approx(100 * (1 - 0.05 / 9), abs=1e-3)
|
||||
for tech in at.TECHNIQUES:
|
||||
for orb in at.ORBS:
|
||||
for radius in runner.RADII:
|
||||
cell = first["table"][tech][str(orb)][str(radius)]
|
||||
assert 0.0 <= cell["real_mean_rank"] <= 1.0
|
||||
assert 0.0 <= cell["percentile_beaten"] <= 100.0
|
||||
assert len(first["shuffled_means"][tech][str(orb)][str(radius)]) == 3
|
||||
|
||||
Reference in New Issue
Block a user