1"""Phase 4 features — region × category count matrices under the three weighting schemes and the sample variants.23Outputs data/processed/rc_counts.parquet (long format): sample ∈ {S_R, S_R0, S_A}, scheme ∈ {L, M, T}, region, category, n4plus store-level table data/processed/store_panel.parquet used by every model (one row per store in S_A with region, weights, size).5"""6from __future__ import annotations7import json8import numpy as np, pandas as pd9from src.common import ROOT, P, REGIONS, get_logger, ledger, register_output, write_json, PROC_DIR, EXP_DIR, LABEL, BASE_PROC, BASE_EXP1011log = get_logger("features.rc")12PROC = PROC_DIR131415def store_category_table():16 li = pd.read_parquet(P["interim"] / "listings_clean.parquet")17 sm = pd.read_parquet(P["interim"] / "stores_master.parquet")18 a = li[li.f_analysis_assort]19 if LABEL == "model":20 cm = pd.read_parquet(BASE_PROC / "category_model.parquet").set_index("uid").category_model21 a = a.assign(category=a.uid.map(cm).fillna(a.category))22 sc = a.groupby(["store_id", "category"]).size().rename("n").reset_index()23 sc = sc.merge(sm[["id", "region_final", "region_source", "n_listings", "platform", "store_kind", "certified", "first_source", "origin_class"]], left_on="store_id", right_on="id").drop(columns="id")24 sc = sc.merge(sm[["id", "region_pipfirst", "region_db"]], left_on="store_id", right_on="id").drop(columns="id")25 return sc, sm262728def build(sc: pd.DataFrame) -> pd.DataFrame:29 rows = []30 sizes = sc.groupby("store_id").n.sum()31 cut = sizes.quantile(0.99)32 for sample, mask in (("S_R", sc.region_final != "Inconnue"), ("S_R0", sc.region_db.notna()), ("S_Rpip", sc.region_pipfirst != "Inconnue"), ("S_A", pd.Series(True, index=sc.index))):33 d = sc[mask].copy()34 if sample == "S_R0": d["region_final"] = d.region_db # platform label only (preregistered S_R0)35 if sample == "S_Rpip": d["region_final"] = d.region_pipfirst36 d["w_L"] = d.n.astype(float)37 d["w_M"] = d.n / d.store_id.map(sizes)38 d["w_T"] = np.where(d.store_id.map(sizes) > cut, 0.0, d.n.astype(float))39 for scheme in ("L", "M", "T"):40 g = d.groupby(["region_final", "category"])[f"w_{scheme}"].sum().reset_index()41 g.columns = ["region", "category", "n"]; g["sample"] = sample; g["scheme"] = scheme42 rows.append(g)43 out = pd.concat(rows, ignore_index=True)44 return out454647def main():48 sc, sm = store_category_table()49 rc = build(sc)50 rc.to_parquet(PROC / "rc_counts.parquet", index=False); register_output(PROC / "rc_counts.parquet", "features.rc")51 sc.to_parquet(PROC / "store_category.parquet", index=False); register_output(PROC / "store_category.parquet", "features.rc")52 sp = sm[["id", "region_final", "region_source", "region_conflict", "n_listings", "n_categories", "platform", "store_kind", "certified", "cert_adq", "cert_lpq",53 "first_source", "origin_class", "median_price", "main_category", "lat", "lng", "postal_prefix", "mrc_pip"]].copy()54 sp = sp[sp.n_listings > 0]55 sp.to_parquet(PROC / "store_panel.parquet", index=False); register_output(PROC / "store_panel.parquet", "features.rc")56 summ = {"stores_S_A": int(sp.shape[0]), "stores_S_R": int((sp.region_final != "Inconnue").sum()), "stores_S_R0": int(sm.region_db.notna().sum()),57 "listings_S_R": int(rc.query("sample=='S_R' and scheme=='L'").n.sum()), "listings_S_A": int(rc.query("sample=='S_A' and scheme=='L'").n.sum()),58 "top1pct_cut": float(sc.groupby('store_id').n.sum().quantile(0.99)),59 "stores_by_region_S_R": sp[sp.region_final != "Inconnue"].region_final.value_counts().to_dict()}60 write_json(summ, EXP_DIR / "features_rc.json", "features.rc"); ledger("features.region_category", **summ); log.info(json.dumps(summ, ensure_ascii=False))616263if __name__ == "__main__":64 main()65