← Files Public Equity InvestingARCHIVED FILE
skills/earnings-deep-dive/scripts/model_diff.py
8.87 KB · Oct 2, 2026 · 00:03 UTC
#!/usr/bin/env python3
"""Produce a signal-oriented model diff using DriverRegistry + OutputRegistry.
This diff intentionally ignores "cell noise" and compares only mapped drivers and key outputs.
Notes:
- openpyxl does not recalc formulas. Output values may reflect cached Excel calculation state.
- If cached values are missing, the diff will show formulas instead.
"""
from __future__ import annotations
import sys
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
if __name__ == "__main__" and any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
print(
"Usage: python scripts/model_diff.py plan.json output/audit/WhatChanged_Diff.csv [new_model_path]"
)
print("Produce a signal-oriented model diff from driver and output registries.")
raise SystemExit(0)
import openpyxl
import pandas as pd
try:
from .utils.excel_utils import resolve_named_range
from .utils.io_utils import ensure_dir, read_json
from .utils.validation_utils import as_float_or_none, is_missing
except ImportError:
from utils.excel_utils import resolve_named_range
from utils.io_utils import ensure_dir, read_json
from utils.validation_utils import as_float_or_none, is_missing
def _read_csv(path: str) -> pd.DataFrame:
return pd.read_csv(path, dtype=str, keep_default_na=False)
def _get_cell(
wb: openpyxl.Workbook,
mapping_type: str,
sheet: str | None,
cell: str | None,
named_range: str | None,
) -> tuple[str, str]:
if mapping_type == "NamedRange":
if not named_range:
raise ValueError("NamedRange mapping requires NamedRange")
loc = resolve_named_range(wb, named_range)
return loc.sheet, loc.cell
if mapping_type == "Cell":
if not sheet or not cell:
raise ValueError("Cell mapping requires Worksheet + Cell")
return sheet, cell
raise ValueError(f"Unknown MappingType: {mapping_type}")
def _read_value_or_formula(
path: Path, mapping_type: str, sheet: str | None, cell: str | None, named_range: str | None
) -> Any:
# cached values
wb_val = openpyxl.load_workbook(path, data_only=True)
wb_for = openpyxl.load_workbook(path, data_only=False)
try:
s, c = _get_cell(wb_val, mapping_type, sheet, cell, named_range)
v = wb_val[s][c].value
if v is not None:
return v
# fallback to formula text
sf, cf = _get_cell(wb_for, mapping_type, sheet, cell, named_range)
f = wb_for[sf][cf].value
return f if f is not None else None
finally:
wb_val.close()
wb_for.close()
def write_diff(
plan: dict[str, Any], out_csv_path: Path, new_model_path: Path | None = None
) -> Path:
out_dir = Path(plan.get("outputs", {}).get("output_dir", "output"))
ensure_dir(str(out_dir / "audit"))
model = plan.get("inputs", {}).get("model", {})
old_path = Path(model.get("prior_model_xlsx"))
driver_reg_path = model.get("driver_registry_csv")
out_reg_path = model.get("output_registry_csv")
if new_model_path is None:
# pick most recent model in output/model
model_dir = out_dir / "model"
if model_dir.exists():
candidates = sorted(
model_dir.glob("*.xls*"), key=lambda p: p.stat().st_mtime, reverse=True
)
if candidates:
new_model_path = candidates[0]
if new_model_path is None:
raise ValueError("Could not determine new model path; pass new_model_path explicitly")
if not old_path.exists():
raise FileNotFoundError(f"Old model not found: {old_path}")
if not new_model_path.exists():
raise FileNotFoundError(f"New model not found: {new_model_path}")
if is_missing(driver_reg_path):
raise ValueError("inputs.model.driver_registry_csv is required for diff")
driver_reg = _read_csv(driver_reg_path)
out_reg = (
_read_csv(out_reg_path)
if out_reg_path and str(out_reg_path).upper() != "MISSING" and Path(out_reg_path).exists()
else pd.DataFrame()
)
# Driver updates source tags (best-effort)
norm = plan.get("inputs", {}).get("normalized", {})
driver_updates_path = norm.get("driver_updates_csv")
source_by_driver = {}
if driver_updates_path and Path(driver_updates_path).exists():
du = _read_csv(driver_updates_path)
for _, r in du.iterrows():
did = str(r.get("DriverID") or "").strip()
if did:
source_by_driver[did] = r.get("SourceTag")
items: list[dict[str, Any]] = []
# Drivers
for _, r in driver_reg.iterrows():
did = str(r.get("DriverID") or "").strip()
if not did:
continue
mapping_type = str(r.get("MappingType") or "NamedRange").strip() or "NamedRange"
ws = str(r.get("Worksheet") or "").strip() or None
cell = str(r.get("Cell") or "").strip() or None
nr = str(r.get("NamedRange") or "").strip() or None
units = str(r.get("Units") or "").strip()
old = _read_value_or_formula(old_path, mapping_type, ws, cell, nr)
new = _read_value_or_formula(new_model_path, mapping_type, ws, cell, nr)
old_f = as_float_or_none(old)
new_f = as_float_or_none(new)
delta = None
if old_f is not None and new_f is not None:
delta = new_f - old_f
items.append(
{
"Category": "Driver",
"Item": did,
"Old": old,
"New": new,
"Delta": delta,
"Units": units,
"PrimaryImpact": "MISSING",
"SourceTag": source_by_driver.get(did, "MISSING"),
"Notes": f"{mapping_type}:{nr or ''}{ws or ''}{cell or ''}",
}
)
# Outputs
if not out_reg.empty:
for _, r in out_reg.iterrows():
oid = str(r.get("OutputID") or "").strip()
if not oid:
continue
mapping_type = str(r.get("MappingType") or "NamedRange").strip() or "NamedRange"
ws = str(r.get("Worksheet") or "").strip() or None
cell = str(r.get("Cell") or "").strip() or None
nr = str(r.get("NamedRange") or "").strip() or None
units = str(r.get("Units") or "").strip()
old = _read_value_or_formula(old_path, mapping_type, ws, cell, nr)
new = _read_value_or_formula(new_model_path, mapping_type, ws, cell, nr)
old_f = as_float_or_none(old)
new_f = as_float_or_none(new)
delta = None
if old_f is not None and new_f is not None:
delta = new_f - old_f
items.append(
{
"Category": "Output",
"Item": oid,
"Old": old,
"New": new,
"Delta": delta,
"Units": units,
"PrimaryImpact": oid,
"SourceTag": "MISSING",
"Notes": "Output value may be cached; recalc in Excel",
}
)
# Rank by absolute delta (numeric), then category, then item
def _abs_delta(x):
d = x.get("Delta")
if d is None:
return -1
try:
return abs(float(d))
except Exception:
return -1
items_sorted = sorted(
items, key=lambda x: (_abs_delta(x), x["Category"], x["Item"]), reverse=True
)
out_rows = []
for idx, it in enumerate(items_sorted, start=1):
out_rows.append(
{
"Rank": idx,
"Category": it["Category"],
"Item": it["Item"],
"Old": it["Old"],
"New": it["New"],
"Delta": it["Delta"],
"Units": it["Units"],
"PrimaryImpact": it["PrimaryImpact"],
"SourceTag": it["SourceTag"],
"Notes": it["Notes"],
}
)
df_out = pd.DataFrame(
out_rows,
columns=[
"Rank",
"Category",
"Item",
"Old",
"New",
"Delta",
"Units",
"PrimaryImpact",
"SourceTag",
"Notes",
],
)
out_csv_path.parent.mkdir(parents=True, exist_ok=True)
df_out.to_csv(out_csv_path, index=False)
return out_csv_path
def main() -> int:
if len(sys.argv) < 3:
print(
"Usage: python scripts/model_diff.py plan.json output/audit/WhatChanged_Diff.csv [new_model_path]"
)
return 1
plan = read_json(sys.argv[1])
out_path = Path(sys.argv[2])
new_model = Path(sys.argv[3]) if len(sys.argv) >= 4 else None
write_diff(plan, out_path, new_model)
print(f"Wrote diff: {out_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 1b70576124fa00462faddc643832ccf04b82550cf02364444749d9f7df5d7d7d