SSM-JTK – CSV-only Scorecard How-To (5)

Scope. How to reproduce back-tests and publish per-body scorecards using only daily CSVs and simple, transparent rules. This page is conceptual and self-contained; no downloads are offered here.


5.1 Prepare a reference CSV (observation-only)

  • Frame & time. geocentric, sidereal (Lahiri), sampled daily at 05:30 IST.
  • Columns. date (ISO YYYY-MM-DD), L_actual_deg (degrees in [0,360)).
  • Source. Your own tables or a reputable public ephemeris (verification only).
    Attribution text (copyable):
    Angles cross-checked for observation-only verification against a public ephemeris service; kernels and evaluations are our own derived work under this document's terms.

5.2 Fit & select (summary, ready to implement)

  1. Choose windows & anchor
    TRAIN = [YYYY-.. .. YYYY-..], TEST = [YYYY-.. .. YYYY-..] (disjoint).
    t0 := midpoint(TRAIN) and t := days_since(date, t0).
  2. Unwrap for OLS & events
    Carry +-360 so y(t) is continuous.
  3. Design
  • Fixed-n: y = L_unwrapped - n*t ; regressors [1, sin(wt), cos(wt), ...]
  • Free-n: y = L_unwrapped ; regressors [1, t, sin(wt), cos(wt), ...]
    Export rule: roll the learned slope into n_deg_per_day in the manifest; do not export a separate slope term.
  1. Model score & gate
    BIC = k*log(N) + N*log(max(RSS/N, 1e-16))
    loss = 1.0*MAE_deg + 0.3*cusp_MAE + 0.4*MAE_speed
    Admit extras iff DeltaBIC >= 6 AND loss decreases.
  2. Export manifest (schema per §4.1).
  3. Re-evaluate on TEST and compute metrics
    MAE_deg, P90_deg (or P95 for Moon), MAX_deg, misclass_rate, rasi_cross_MAE_days, station_date_MAE_days, cusp_dist_MAE_deg.

5.3 Publish the scorecard (template)

Per-body one-liner (public post):
SSM-JTK {planet} — TRAIN {YYYY-..} | TEST {YYYY-..}
Model={selected_model}; MAE={MAE_deg:.2f} deg; P90={P90_deg:.2f} deg;
CrossMAE={rasi_cross_MAE_days:.2f} d; StationMAE={station_date_MAE_days:.2f} d
Manifest: per-body manifest (mid-anchor; DeltaBIC>=6 & event-loss gate)
(For Moon, you may replace P90 with P95 and foreground cusp/crossing metrics.)


5.4 What to keep locally (per planet)

  • Manifest. Per-body JSON manifest (immutable once published).
  • Back-test summary. Single CSV row with key metrics and ranges.
  • Back-test time-series. date, L_actual_deg, L_model_deg, deg_err, rasi_actual, rasi_model.
  • Reproduction notes. TRAIN/TEST spans, sampling mode, anchors, acceptance gates.

5.5 CSV-only pathway (no manifests)

If you only share daily “golden” angles, third parties can still generate scorecards by comparing:

  • Predicted (from golden). planet,date,lon_sidereal_lahiri_deg
  • Reference (their ephemeris CSV). planet,date,L_actual_deg

Required alignment (per body/day):
wrap360(x) = x - 360*floor(x/360)
wrap180(d) = ((d + 180) % 360) - 180
rasi(x) = floor( wrap360(x) / 30 )
cusp_dist(x) = min( wrap360(x) % 30 , 30 - (wrap360(x) % 30) )
err_deg = | wrap180(L_pred - L_actual) |

Event timing (CSV-only).

  • 30 deg crossings: unwrap both series; linearly interpolate on the k*30 grid; pair nearest events with a cap (e.g., 60 d); average |Delta t|.
  • Stations: central-difference speed on unwrapped series; pick local minima; pair with a cap (e.g., 90 d); average |Delta t|.
  • Nodes identity (optional check): ensure Ketu(t) = wrap360(Rahu(t) + 180) per day.

5.6 Tiny reference script (CSV-only scorecard)

# scorecard_csv_only.py (plain ASCII; reads two CSVs with identical date/planet coverage)
# Usage: python scorecard_csv_only.py golden.csv reference.csv Venus
import sys, csv, math, datetime as dt

def wrap360(x): return (x % 360.0 + 360.0) % 360.0
def wrap180(d):
    y = (d + 180.0) % 360.0
    return y - 180.0
def rasi(x): return int(math.floor(wrap360(x) / 30.0))
def cusp_dist(x):
    q = wrap360(x) % 30.0
    return min(q, 30.0 - q)

def read_csv(path):
    rows = []
    with open(path, newline="", encoding="utf-8") as f:
        for r in csv.DictReader(f):
            rows.append({
                "planet": r["planet"].strip().lower(),
                "date": dt.date.fromisoformat(r["date"]),
                "L": float(r.get("lon_sidereal_lahiri_deg") or r.get("L_actual_deg"))
            })
    return rows

def group_by(rows, planet):
    return [r for r in rows if r["planet"] == planet.lower()]

def unwrap(series):
    y = [series[0]]
    for i in range(1, len(series)):
        x = series[i]; best = x
        for k in (-1, 0, 1):
            cand = x + 360.0 * k
            if abs(cand - y[-1]) < abs(best - y[-1]): best = cand
        y.append(best)
    return y

def central_diff(y):
    if len(y) < 3: return [0.0]*len(y)
    return [y[1]-y[0]] + [(y[i+1]-y[i-1])/2.0 for i in range(1,len(y)-1)] + [y[-1]-y[-2]]

def detect_crossings(t, y_unwrapped, step=30.0):
    cr = []
    y0, y1 = min(y_unwrapped[0], y_unwrapped[-1]), max(y_unwrapped[0], y_unwrapped[-1])
    kmin = math.floor(y0/step) - 1; kmax = math.ceil(y1/step) + 1
    for k in range(int(kmin), int(kmax)+1):
        target = k*step
        for i in range(1, len(y_unwrapped)):
            s0 = y_unwrapped[i-1] - target
            s1 = y_unwrapped[i]   - target
            if s0 == 0.0: cr.append(t[i-1]); continue
            if s0*s1 < 0.0:
                frac = (target - y_unwrapped[i-1]) / (y_unwrapped[i] - y_unwrapped[i-1])
                tau  = t[i-1] + frac*(t[i]-t[i-1])
                cr.append(tau)
    return cr

def pair_events(A, B, cap_days):
    used, pairs = set(), []
    for a in A:
        jbest, dbest = None, None
        for j,b in enumerate(B):
            if j in used: continue
            d = abs(b - a)
            if dbest is None or d < dbest:
                jbest, dbest = j, d
        if dbest is not None and dbest <= cap_days:
            pairs.append((a, B[jbest])); used.add(jbest)
    return pairs

def main(golden_path, reference_path, planet):
    G = group_by(read_csv(golden_path), planet)
    R = group_by(read_csv(reference_path), planet)
    if len(G) != len(R):
        raise SystemExit("Mismatched coverage; align date/planet first.")

    G.sort(key=lambda r: r["date"]); R.sort(key=lambda r: r["date"])
    dates = [r["date"] for r in G]
    Lg = [r["L"] for r in G]; Lr = [r["L"] for r in R]

    # pointwise degree metrics
    errs = [abs(wrap180(Lg[i]-Lr[i])) for i in range(len(dates))]
    mae = sum(errs)/len(errs)
    p90 = sorted(errs)[int(0.90*len(errs))-1]
    mxx = max(errs)
    mis = sum(1 for i in range(len(dates)) if rasi(Lg[i])!=rasi(Lr[i]))/len(dates)

    # cusp MAE
    cusp_mae = sum(abs(cusp_dist(Lg[i]) - cusp_dist(Lr[i])) for i in range(len(dates))) / len(dates)

    # events (unwrapped)
    t   = [(d - dates[0]).days for d in dates]
    yG  = unwrap(Lg); yR = unwrap(Lr)

    # crossings
    cG  = detect_crossings(t, yG, 30.0)
    cR  = detect_crossings(t, yR, 30.0)
    Cp  = pair_events(cR, cG, cap_days=60.0)
    cross_mae = (sum(abs(a-b) for a,b in Cp)/len(Cp)) if Cp else float("nan")

    # stations (simple)
    vG, vR = central_diff(yG), central_diff(yR)
    sG = [t[i] for i in range(1,len(vG)-1) if abs(vG[i])<=abs(vG[i-1]) and abs(vG[i])<=abs(vG[i+1])]
    sR = [t[i] for i in range(1,len(vR)-1) if abs(vR[i])<=abs(vR[i-1]) and abs(vR[i])<=abs(vR[i+1])]
    Sp = pair_events(sR, sG, cap_days=90.0)
    stat_mae = (sum(abs(a-b) for a,b in Sp)/len(Sp)) if Sp else float("nan")

    print(f"Planet={planet}")
    print(f"MAE_deg={mae:.6f}  P90_deg={p90:.6f}  MAX_deg={mxx:.6f}  misclass_rate={mis:.6f}")
    print(f"cusp_dist_MAE_deg={cusp_mae:.6f}")
    print(f"rasi_cross_MAE_days={cross_mae:.6f}")
    print(f"station_date_MAE_days={stat_mae:.6f}")

if __name__ == "__main__":
    if len(sys.argv) != 4:
        print("Usage: python scorecard_csv_only.py golden.csv reference.csv PlanetName")
        sys.exit(2)
    main(sys.argv[1], sys.argv[2], sys.argv[3])

Notes. Identical date coverage expected; pairing caps are 60 d (crossings) and 90 d (stations). If events are too sparse in the chosen window, printing NaN is acceptable.


Publishing checklist (quick)

  • Frame/time match confirmed (05:30 IST, geocentric, sidereal Lahiri).
  • Manifests exported (mid-anchor; parsimony gate met).
  • Scorecards computed on a disjoint TEST span and formatted via §5.3.
  • Optional CSV-only audit with the tiny script in §5.6 for third-party verification.

Navigation
Back: SSM-JTK – Locked Kernels – Tiny Reproduction Kit (4)
Next: SSM-JTK – Methodology — short walkthrough (6)


Directory of Pages
SSM-JTK – Series index & links


Disclaimer
The contents in the Shunyaya Symbolic Mathematics Jyotish Transit Kernel (SSM-JTK) materials are research/observation material. They are not astrological advice, not a scientific ephemeris, and not operational guidance. Do not use for safety-critical, medical, legal, or financial decisions. Use at your own discretion; no warranties are provided; results depend on correct implementation and inputs.


Explore Further
https://github.com/OMPSHUN