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_dbfirst", "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_A", pd.Series(True, index=sc.index))):33 d = sc[mask].copy()34 if sample == "S_R0": d["region_final"] = d.region_dbfirst35 d["w_L"] = d.n.astype(float)36 d["w_M"] = d.n / d.store_id.map(sizes)37 d["w_T"] = np.where(d.store_id.map(sizes) > cut, 0.0, d.n.astype(float))38 for scheme in ("L", "M", "T"):39 g = d.groupby(["region_final", "category"])[f"w_{scheme}"].sum().reset_index()40 g.columns = ["region", "category", "n"]; g["sample"] = sample; g["scheme"] = scheme41 rows.append(g)42 out = pd.concat(rows, ignore_index=True)43 return out444546def main():47 sc, sm = store_category_table()48 rc = build(sc)49 rc.to_parquet(PROC / "rc_counts.parquet", index=False); register_output(PROC / "rc_counts.parquet", "features.rc")50 sc.to_parquet(PROC / "store_category.parquet", index=False); register_output(PROC / "store_category.parquet", "features.rc")51 sp = sm[["id", "region_final", "region_source", "region_conflict", "n_listings", "n_categories", "platform", "store_kind", "certified", "cert_adq", "cert_lpq",52 "first_source", "origin_class", "median_price", "main_category", "lat", "lng"]].copy()53 sp = sp[sp.n_listings > 0]54 sp.to_parquet(PROC / "store_panel.parquet", index=False); register_output(PROC / "store_panel.parquet", "features.rc")55 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()),56 "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()),57 "top1pct_cut": float(sc.groupby('store_id').n.sum().quantile(0.99)),58 "stores_by_region_S_R": sp[sp.region_final != "Inconnue"].region_final.value_counts().to_dict()}59 write_json(summ, EXP_DIR / "features_rc.json", "features.rc"); ledger("features.region_category", **summ); log.info(json.dumps(summ, ensure_ascii=False))606162if __name__ == "__main__":63 main()64