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


from pathlib import Path
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from solution import add_region_features, make_client_folds, build_product_table, make_product_candidates

DATA = Path("data")
PARAMS = dict(max_iter=200, learning_rate=0.03, max_leaf_nodes=15, min_samples_leaf=1000,
              l2_regularization=1.0, random_state=42)
PRODUCTS = [1, 2, 3, 4]   # продукт 5 исключён по валидации
SHARE = 0.20

history = pd.read_csv(DATA / "history_clients.csv")
campaigns = pd.read_csv(DATA / "campaign_history.csv")
products = pd.read_csv(DATA / "products.csv", encoding="utf-8-sig")
regions = pd.read_csv(DATA / "region_profiles.csv")

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

# фолды по клиентам, фолд 0 откладываем
folds = make_client_folds(history["client_id"], n_folds=5, seed=42)
test_ids = folds.loc[folds["fold"] == 0, "client_id"]
train = history[~history["client_id"].isin(test_ids)]
test = history[history["client_id"].isin(test_ids)]

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

for j in PRODUCTS:
    t = build_product_table(train, 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_test.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"]

# лучший продукт на клиента, score > 0, не более 20%
best = cands.sort_values(["score", "client_id"], ascending=[False, True]).drop_duplicates("client_id")
best = best[best["score"] > 0].head(int(SHARE * len(test)))

# IPW-прибыль на отложенных клиентах
rows = best.merge(campaigns, on=["client_id", "product_id"])
p = rows["assignment_probability"]
w = rows["offered"] / p - (1 - rows["offered"]) / (1 - p)
profit = (w * rows["purchased"] * rows["product_id"].map(prod["margin_rub"]) - rows["product_id"].map(prod["contact_cost_rub"])).sum()
print(f"holdout: {len(test)} клиентов, контактов: {len(best)}, IPW-прибыль: {profit / 1000:.0f} тыс. руб.")
print(best["product_id"].value_counts().sort_index().to_dict())