SPB Git

spb/wp38_uqo

Public
4.1 KB · 42 lines python
1"""Phase 5 — External validation / selection audit: Fabri-Ka stores per region vs ISQ business locations (all, retail 44-45,2manufacturing 31-33) and population; discovery-source and platform mix by region; category mix by platform. Output experiments/external_validation.json"""3from __future__ import annotations4import json5import numpy as np, pandas as pd6from scipy import stats7from src.common import ROOT, get_logger, ledger, write_json, PROC_DIR, EXP_DIR, P89log = get_logger("validation.external")10PROC = PROC_DIR111213def main():14    ext = pd.read_csv(ROOT / "data" / "external" / "regions_external.csv").set_index("region_name")15    sp = pd.read_parquet(PROC / "store_panel.parquet"); sc = pd.read_parquet(PROC / "store_category.parquet")16    r = sp[sp.region_final != "Inconnue"].groupby("region_final").agg(stores=("id", "size"), listings=("n_listings", "sum")).join(ext, how="right").fillna({"stores": 0, "listings": 0})17    rows = []18    for reg, x in r.iterrows():19        rows.append({"region": reg, "stores": int(x.stores), "listings": int(x.listings), "businesses": int(x.business_locations_with_employees) if not np.isnan(x.business_locations_with_employees) else None,20                     "stores_per_1000_businesses": float(x.stores / x.business_locations_with_employees * 1000) if x.business_locations_with_employees else None,21                     "retail_locations": int(x.business_locations_with_employees_retail_44_45) if not np.isnan(x.business_locations_with_employees_retail_44_45) else None,22                     "mfg_locations": int(x.business_locations_with_employees_mfg_31_33) if not np.isnan(x.business_locations_with_employees_mfg_31_33) else None,23                     "population": int(x.population), "stores_per_100k_pop": float(x.stores / x.population * 1e5)})24    d = pd.DataFrame(rows).dropna(subset=["businesses"])25    corr = {k: {"spearman": round(float(stats.spearmanr(d.stores, d[k]).correlation), 3), "pearson_log": round(float(np.corrcoef(np.log(d.stores.clip(1)), np.log(d[k]))[0, 1]), 3)} for k in ("businesses", "retail_locations", "mfg_locations", "population")}26    cov = d.stores_per_1000_businesses; cv = {"min": round(float(cov.min()), 2), "max": round(float(cov.max()), 2), "ratio_max_min": round(float(cov.max() / cov.min()), 2), "cv": round(float(cov.std() / cov.mean()), 3)}27    # selection audit: source and platform mix by region28    sp2 = sp[sp.region_final != "Inconnue"]; src = pd.crosstab(sp2.region_final, sp2.first_source.where(sp2.first_source.isin(sp2.first_source.value_counts().head(8).index), "other"), normalize="index").round(3)29    plat = pd.crosstab(sp2.region_final, sp2.platform.where(sp2.platform.isin(["shopify", "woocommerce", "wix"]), "other"), normalize="index").round(3)30    catplat = pd.crosstab(sc.platform.where(sc.platform.isin(["shopify", "woocommerce", "wix"]), "other"), sc.category, values=sc.n / sc.groupby("store_id").n.transform("sum"), aggfunc="sum", normalize="index").round(3)31    unknown = sp[sp.region_final == "Inconnue"]32    out = {"rows": rows, "correlations_stores_vs_external": corr, "coverage_ratio_dispersion": cv, "unknown_region": {"stores": int(len(unknown)), "listings": int(unknown.n_listings.sum()), "top_sources": unknown.first_source.value_counts().head(5).to_dict()},33           "source_mix_by_region": src.to_dict("index"), "platform_mix_by_region": plat.to_dict("index"), "category_mix_by_platform": catplat.to_dict("index"),34           "note": "Business locations = ISQ, locations with employees, December 2025 (all industries; retail NAICS 44-45; manufacturing 31-33); population = ISQ 1 July 2025 provisional. Fabri-Ka stores are online storefronts of Quebec-based merchants discovered through certification registries, associations and search; coverage ratios are not sampling rates."}35    src.to_csv(PROC / "source_mix_by_region.csv"); plat.to_csv(PROC / "platform_mix_by_region.csv"); catplat.to_csv(PROC / "category_mix_by_platform.csv")36    write_json(out, EXP_DIR / "external_validation.json", "external"); ledger("validation.external", corr=corr, cv=cv)37    log.info(json.dumps({"corr": corr, "coverage": cv, "unknown": out["unknown_region"]}, ensure_ascii=False))383940if __name__ == "__main__":41    main()42
Files · main
Menu

Theme