"""Hypothesis registry: confirmatory family {H1, H2, H3, H5-T1, H5-T2} with Holm step-down; exploratory/validation outputs listed with status. Writes experiments/hypothesis_registry.json and reports/hypothesis_registry.md.""" from __future__ import annotations import json import numpy as np from src.common import ROOT, get_logger, write_json, PROC_DIR, EXP_DIR, P log = get_logger("models.summary"); EXP = EXP_DIR J = lambda n: json.load(open(EXP / n)) def holm(p): keys = sorted(p, key=lambda k: p[k]); m = len(keys); adj = {}; running = 0 for i, k in enumerate(keys): running = max(running, (m - i) * p[k]); adj[k] = min(1.0, running) return adj def main(label="rule"): h1 = {(r["sample"], r["scheme"]): r for r in J("h1_raa.json")["results"]}; h2 = J("h2_scale.json"); h3 = J("h3_longtail.json"); h5 = J("h5_hedonic.json")["models"][0]; h7 = J("h7_complexity.json"); h4 = J("h4_dynamics.json"); q = J("qdaci.json") pM, pT, pL = h1[("S_R", "M")]["p_perm"], h1[("S_R", "T")]["p_perm"], h1[("S_R", "L")]["p_perm"] c2 = h2["correlations"]["H_rarefied15 ~ log_population"] raw = {"H1": max(pM, pT), "H2": None, "H3": None, "H5-T1": h5["p_region_wildboot"], "H5-T2": h5["p_reseller_wildboot"]} # H2 and H3 have CI/threshold decisions; for Holm we convert H2 to a two-sided permutation-free p via Fisher z (reported as approximate) n2 = c2["n"]; z = np.arctanh(c2["rho"]) * np.sqrt(max(n2 - 3, 1)); from scipy.stats import norm; raw["H2"] = float(2 * (1 - norm.cdf(abs(z)))) raw["H3"] = 0.0 if h3["decision_prereg"] else 1.0 # threshold criterion with bootstrap CIs; p-value placeholder for the Holm ordering only adj = holm({k: v for k, v in raw.items()}) reg = [ {"id": "H1", "statement": "Online assortment composition differs across regions beyond what merchant counts imply", "primary_statistic": f"Cramér's V (M) = {h1[('S_R','M')]['cramers_v']:.3f}, store-reshuffle p = {pM:.4f}; (T) V = {h1[('S_R','T')]['cramers_v']:.3f}, p = {pT:.3f}; (L) p = {pL:.3f}", "p_raw": raw["H1"], "p_holm": adj["H1"], "status": "partially supported" if pM < 0.05 and pT >= 0.05 else ("supported" if adj["H1"] < 0.05 else "not supported"), "note": "Preregistered criterion required p < 0.05 under both merchant-equal and trimmed-listing weights; met for merchant-equal only. Specialization is a property of the merchant population, not of listing volumes."}, {"id": "H2", "statement": "Regional diversity is associated with regional scale", "primary_statistic": f"Spearman rho(rarefied entropy, log population) = {c2['rho']:.2f}, 95% CI [{c2['ci'][0]:.2f}; {c2['ci'][1]:.2f}], n = {c2['n']}", "p_raw": raw["H2"], "p_holm": adj["H2"], "status": "inconclusive" if c2["ci"][0] < 0 < c2["ci"][1] else "supported", "note": "CI includes zero and economically meaningful values in both directions; n = 16 regions. No association with business counts or density either (see table)."}, {"id": "H3", "statement": "Merchant catalogue sizes are highly skewed", "primary_statistic": f"Gini = {h3['gini']:.3f} [{h3['gini_ci'][0]:.3f}; {h3['gini_ci'][1]:.3f}]; top-10% share = {h3['top10pct_share']:.3f} [{h3['top10_ci'][0]:.3f}; {h3['top10_ci'][1]:.3f}]", "p_raw": None, "p_holm": None, "status": "supported" if h3["decision_prereg"] else "not supported", "note": "Both preregistered thresholds (Gini >= 0.70, top-10% >= 0.50) exceeded with CIs entirely above them; power-law tail favoured over log-normal on the tail."}, {"id": "H4", "statement": "Related diversification (exploratory)", "primary_statistic": f"LPM density coefficient {h4['lpm_density_coef']:.3f} (SE {h4['se_cluster_store']:.3f}), placebo p = {h4['placebo_p']:.3f}; {h4['entries']} entries; {len(h4['region_level_entries'])} region-level entries", "p_raw": None, "p_holm": None, "status": "exploratory: consistent with relatedness at the store level; region level untestable", "note": "Fewer than 10 region-level entries (preregistered untestability rule); store-level association over 5 weeks, no causal claim."}, {"id": "H5-T1", "statement": "Matched-product prices are geographically structured", "primary_statistic": f"Wald({h5['df_region']}) = {h5['wald_region']:.1f}, wild-cluster-bootstrap p = {h5['p_region_wildboot']:.4f}; {h5['groups']} groups, {h5['stores']} stores, {h5['regions_with_ge5_stores']} regions with >= 5 stores", "p_raw": raw["H5-T1"], "p_holm": adj["H5-T1"], "status": "supported" if adj["H5-T1"] < 0.05 else "not supported", "note": "Region effects identified only through groups spanning regions; posted prices, no shipping/taxes."}, {"id": "H5-T2", "statement": "Matched-product prices are competitively structured (reseller vs producer)", "primary_statistic": f"reseller coefficient {h5['coef']['reseller']['b']:.3f} (SE {h5['coef']['reseller']['se_cluster']:.3f}), wild-bootstrap p = {h5['p_reseller_wildboot']:.4f}", "p_raw": raw["H5-T2"], "p_holm": adj["H5-T2"], "status": "supported" if adj["H5-T2"] < 0.05 else "not supported", "note": "Resellers/collectives post lower prices than producers for the same product."}, {"id": "H6", "statement": "Local-origin price premium", "primary_statistic": "not estimated", "p_raw": None, "p_holm": None, "status": "untestable (declared ex ante)", "note": "No product-level provenance; a store-certification contrast is reported descriptively and is not H6."}, {"id": "H7", "statement": "Validated digital-assortment complexity indicator", "primary_statistic": f"ECI bootstrap rank stability median rho = {h7['V1_bootstrap_rank_stability']['eci_rho_median']:.2f} (criterion 0.8); lambda2 null p = {h7['V2_reshuffle_null']['p_lambda2']:.2f}; LOO predictive rho = {h2['H7_V3']['loo_pred_spearman']:.2f}", "p_raw": None, "p_holm": None, "status": "not validated: ECI unstable at 17 x 20; omitted from QDACI", "note": "Fitness and plain diversity are more stable but still below the criterion; the region-category matrix is not more nested than the store-reshuffle null."}, {"id": "QDACI", "statement": "Composite index", "primary_statistic": f"median agreement across weightings rho = {q['composite_agreement_spearman']['median']:.2f} (rule 0.7); bootstrap rank stability {q['bootstrap_rank_stability_equal_weights_median_rho']:.2f}", "p_raw": None, "p_holm": None, "status": "components published; composite withheld" if not q["publish_composite"] else "composite published", "note": q["note"]}, ] write_json({"labelling": label, "holm_family": list(raw), "registry": reg}, EXP / f"hypothesis_registry_{label}.json", "summary") md = [f"# Hypothesis registry ({label} category labelling)", "", "| id | statement | primary statistic | p raw | p Holm | status |", "|---|---|---|---|---|---|"] md += [f"| {r['id']} | {r['statement']} | {r['primary_statistic']} | {'' if r['p_raw'] is None else f'{r['p_raw']:.4f}'} | {'' if r['p_holm'] is None else f'{r['p_holm']:.4f}'} | **{r['status']}** |" for r in reg] md += ["", "Notes:"] + [f"- {r['id']}: {r['note']}" for r in reg] (ROOT / "reports" / f"hypothesis_registry_{label}.md").write_text("\n".join(md)); log.info("\n".join(f"{r['id']}: {r['status']}" for r in reg)) if __name__ == "__main__": import sys; main(sys.argv[1] if len(sys.argv) > 1 else "rule")