Загрузка данных
# 6.1
def audit_history(
clients: pd.DataFrame,
campaigns: pd.DataFrame,
) -> dict[str, object]:
campaign_clients = pd.Series(campaigns["client_id"].unique())
return {
"n_clients": int(len(clients)),
"n_campaign_rows": int(len(campaigns)),
"n_unique_campaign_clients": int(campaigns["client_id"].nunique()),
"n_products": int(campaigns["product_id"].nunique()),
"duplicate_client_ids": int(clients["client_id"].duplicated().sum()),
"duplicate_client_product_rows": int(
campaigns.duplicated(["client_id", "product_id"]).sum()
),
"missing_campaign_clients": int(
(~campaign_clients.isin(clients["client_id"])).sum()
),
}
# 6.2
def make_client_folds(
client_ids: pd.Series,
n_folds: int,
seed: int,
) -> pd.DataFrame:
ids = pd.Series(client_ids).dropna().drop_duplicates()
try:
ids = ids.sort_values(kind="mergesort")
except TypeError: # смешанные типы id: сортируем по строковому представлению
ids = ids.sort_values(kind="mergesort", key=lambda x: x.astype(str))
ids = ids.to_numpy()
n = len(ids)
rng = np.random.RandomState(seed)
perm = rng.permutation(n)
fold = np.empty(n, dtype=np.int64)
fold[perm] = np.arange(n) % int(n_folds)
return pd.DataFrame({"client_id": ids, "fold": fold})
# 6.3
def summarize_campaigns(
campaigns: pd.DataFrame,
) -> pd.DataFrame:
columns = [
"product_id",
"n_rows",
"n_offered",
"n_control",
"purchase_rate_offered",
"purchase_rate_control",
]
rows = []
for product_id, group in campaigns.groupby("product_id", sort=True):
offered = group[group["offered"] == 1]
control = group[group["offered"] == 0]
rows.append(
{
"product_id": product_id,
"n_rows": int(len(group)),
"n_offered": int(len(offered)),
"n_control": int(len(control)),
"purchase_rate_offered": float(offered["purchased"].mean())
if len(offered)
else float("nan"),
"purchase_rate_control": float(control["purchased"].mean())
if len(control)
else float("nan"),
}
)
out = pd.DataFrame(rows, columns=columns)
return out.sort_values("product_id").reset_index(drop=True)
# 6.4
def build_product_table(
clients: pd.DataFrame,
campaigns: pd.DataFrame,
product_id: int,
) -> pd.DataFrame:
camp = campaigns[campaigns["product_id"] == product_id]
camp = camp.drop_duplicates("client_id", keep="first")
cl = clients.drop_duplicates("client_id", keep="first")
# merge с левой таблицей clients сохраняет порядок clients
try:
out = cl.merge(camp, on="client_id", how="inner")
except ValueError: # разные dtype ключа: приводим обе стороны к строке
cl = cl.assign(client_id=cl["client_id"].astype(str))
camp = camp.assign(client_id=camp["client_id"].astype(str))
out = cl.merge(camp, on="client_id", how="inner")
return out.reset_index(drop=True)
# 6.5
def add_region_features(
clients: pd.DataFrame,
regions: pd.DataFrame,
) -> pd.DataFrame:
out = clients.copy()
reg = regions.drop_duplicates("region_code")
for col in reg.columns:
if col == "region_code":
continue
mapping = dict(zip(reg["region_code"], reg[col]))
name = col if col not in out.columns else f"{col}_region"
out[name] = out["region_code"].map(mapping)
return out
# 6.6
def make_product_candidates(
clients: pd.DataFrame,
products: pd.DataFrame,
) -> pd.DataFrame:
parts = []
for row in products.itertuples(index=False):
col = row.eligibility_column
if col not in clients.columns:
continue
sel = clients.loc[clients[col] == 1, ["client_id"]].copy()
sel["product_id"] = row.product_id
parts.append(sel)
if parts:
out = pd.concat(parts, ignore_index=True)
else:
out = pd.DataFrame({"client_id": [], "product_id": []})
out = out.drop_duplicates(["client_id", "product_id"])
out = out.sort_values(["client_id", "product_id"]).reset_index(drop=True)
return out[["client_id", "product_id"]]
# 6.7
def check_eligibility(
clients: pd.DataFrame,
offers: pd.DataFrame,
) -> pd.DataFrame:
out = offers[["client_id", "offer_id"]].copy()
cl = clients.drop_duplicates("client_id", keep="first").set_index("client_id")
elig_cols = {j: f"eligible_p{j}" for j in PRODUCT_IDS}
known = set(cl.index)
result = np.zeros(len(out), dtype=bool)
for i, (cid, oid) in enumerate(zip(out["client_id"], out["offer_id"])):
if cid not in known:
continue
try:
o = int(oid)
except (TypeError, ValueError):
continue
if o != oid:
continue
if o == 0:
result[i] = True
elif o in elig_cols:
col = elig_cols[o]
result[i] = bool(col in cl.columns and cl.at[cid, col] == 1)
out["is_eligible"] = result.astype(bool)
return out
# 6.8
def make_submission(
clients: pd.DataFrame,
offers: pd.DataFrame,
) -> pd.DataFrame:
if offers["client_id"].duplicated().any():
raise ValueError("duplicate client_id in offers")
mapping = dict(zip(offers["client_id"], offers["offer_id"]))
out = pd.DataFrame({"client_id": clients["client_id"].to_numpy()})
out["offer_id"] = out["client_id"].map(mapping).fillna(0).astype(int)
return out
# 6.9
def contact_statistics(
submission: pd.DataFrame,
) -> dict[str, object]:
offer = submission["offer_id"]
n_clients = int(len(submission))
n_contacts = int(offer.isin(PRODUCT_IDS).sum())
out = {
"n_clients": n_clients,
"n_no_contact": int((offer == 0).sum()),
"n_contacts": n_contacts,
"contact_rate": float(n_contacts / n_clients) if n_clients else 0.0,
}
for j in PRODUCT_IDS:
out[f"n_offer_{j}"] = int((offer == j).sum())
return out
# 6.10
def validate_submission_frame(
submission: pd.DataFrame,
clients: pd.DataFrame,
contact_fraction: float = 0.20,
) -> dict[str, object]:
sub_ids = submission["client_id"]
client_ids = clients["client_id"].drop_duplicates()
n_rows = int(len(submission))
n_unique_clients = int(sub_ids.nunique())
missing_clients = int((~client_ids.isin(sub_ids)).sum())
extra_clients = int((~sub_ids.drop_duplicates().isin(client_ids)).sum())
duplicate_clients = int(sub_ids.duplicated().sum())
invalid_offer_ids = int((~submission["offer_id"].isin(VALID_OFFER_IDS)).sum())
product_mask = submission["offer_id"].isin(PRODUCT_IDS).to_numpy()
known_mask = sub_ids.isin(client_ids).to_numpy()
eligible = check_eligibility(clients, submission)["is_eligible"].to_numpy()
ineligible_offers = int((known_mask & product_mask & ~eligible).sum())
n_contacts = int(product_mask.sum())
# 1e-9 компенсирует погрешность float (0.29 * 100 = 28.999999999999996)
max_contacts = int(math.floor(contact_fraction * len(clients) + 1e-9))
contact_limit_exceeded = bool(n_contacts > max_contacts)
is_valid = bool(
missing_clients == 0
and extra_clients == 0
and duplicate_clients == 0
and invalid_offer_ids == 0
and ineligible_offers == 0
and not contact_limit_exceeded
and n_rows == len(client_ids)
and n_unique_clients == len(client_ids)
)
return {
"is_valid": is_valid,
"n_rows": n_rows,
"n_unique_clients": n_unique_clients,
"missing_clients": missing_clients,
"extra_clients": extra_clients,
"duplicate_clients": duplicate_clients,
"invalid_offer_ids": invalid_offer_ids,
"ineligible_offers": ineligible_offers,
"n_contacts": n_contacts,
"max_contacts": max_contacts,
"contact_limit_exceeded": contact_limit_exceeded,
}