Загрузка данных


# ---------- ML-блок: T-learner по продуктам, политика по ожидаемой прибыли ----------
PARAMS = dict(max_iter=200, learning_rate=0.03, max_leaf_nodes=15, min_samples_leaf=1000,
              l2_regularization=1.0, random_state=SEED)
PRODUCTS = [j for j in PRODUCT_IDS if j not in EXCLUDED_PRODUCTS]


def run(data_dir: Path, output_path: Path) -> None:
    np.random.seed(SEED)

    # 1. загрузка
    history = pd.read_csv(data_dir / "history_clients.csv")
    campaigns = pd.read_csv(data_dir / "campaign_history.csv")
    products = pd.read_csv(data_dir / "products.csv", encoding="utf-8-sig")
    regions = pd.read_csv(data_dir / "region_profiles.csv")
    public = pd.read_csv(data_dir / "public_clients.csv")

    # 2. признаки: колонки клиента + региональные, кроме client_id и eligible_*
    history = add_region_features(history, regions)
    public = add_region_features(public, regions)
    features = [c for c in history.columns if c != "client_id" and not c.startswith("eligible_")]

    # 3. кандидаты: public-клиент × доступный ему продукт
    prod = products.set_index("product_id")
    cands = make_product_candidates(public, products)
    cands = cands[cands["product_id"].isin(PRODUCTS)].copy()
    cands["score"] = 0.0
    x_public = public.set_index("client_id")[features]

    # 4. две модели на продукт (offered = 1 / offered = 0), score = uplift × маржа − стоимость
    for j in PRODUCTS:
        t = build_product_table(history, campaigns, j)
        t = t[t[f"eligible_p{j}"] == 1]
        m1 = HistGradientBoostingClassifier(**PARAMS).fit(t.loc[t["offered"] == 1, features], t.loc[t["offered"] == 1, "purchased"])
        m0 = HistGradientBoostingClassifier(**PARAMS).fit(t.loc[t["offered"] == 0, features], t.loc[t["offered"] == 0, "purchased"])
        m = cands["product_id"] == j
        x = x_public.loc[cands.loc[m, "client_id"]]
        cands.loc[m, "score"] = (m1.predict_proba(x)[:, 1] - m0.predict_proba(x)[:, 1]) * prod.loc[j, "margin_rub"] - prod.loc[j, "contact_cost_rub"]
        print(f"продукт {j} готов")

    # 5. лучший продукт на клиента, только score > 0, не более лимита
    best = cands.sort_values(["score", "client_id"], ascending=[False, True]).drop_duplicates("client_id")
    best = best[best["score"] > 0].head(int(math.floor(CONTACT_FRACTION * len(public))))

    # 6. сборка, проверка, сохранение
    offers = best.rename(columns={"product_id": "offer_id"})[["client_id", "offer_id"]]
    submission = make_submission(public, offers)
    diagnostics = validate_submission_frame(submission, public, CONTACT_FRACTION)
    if not diagnostics["is_valid"]:
        raise ValueError(f"Generated submission is invalid: {diagnostics}")
    output_path.parent.mkdir(parents=True, exist_ok=True)
    submission.to_csv(output_path, index=False)
    print(f"Submission saved to: {output_path}")
    print(f"Validation: {diagnostics}")
    print(f"Contacts: {contact_statistics(submission)}")


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--data-dir", type=Path, required=True)
    parser.add_argument("--output", type=Path, default=Path("submission.csv"))
    return parser.parse_args()


if __name__ == "__main__":
    args = parse_args()
    run(data_dir=args.data_dir, output_path=args.output)