This commit is contained in:
2025-12-16 01:51:05 +03:00
parent a1bc89c481
commit c963b1e5ac
123 changed files with 5644 additions and 3802 deletions

View File

@@ -1,12 +1,14 @@
from __future__ import annotations
"""Утилиты для предварительного EDA: загрузка CSV, нормализация признаков и агрегации."""
from pathlib import Path
from typing import Dict, Iterable, List
import numpy as np
import pandas as pd
# Paths and column groups
# Пути и группировки колонок, которые используются во всех агрегациях
DATA_PATH = Path("dataset/ds.csv")
CATEGORIES: List[str] = ["ent", "super", "transport", "shopping", "hotel", "avia"]
@@ -28,7 +30,7 @@ CAT_COLS = ["gender_cd", "device_platform_cd"]
def safe_divide(numerator: pd.Series | float, denominator: pd.Series | float) -> pd.Series:
"""Divide with protection against zero (works for Series and scalars)."""
"""Деление с защитой от нулей, чтобы не получить inf/NaN."""
if isinstance(denominator, pd.Series):
denom = denominator.replace(0, np.nan)
else:
@@ -37,12 +39,14 @@ def safe_divide(numerator: pd.Series | float, denominator: pd.Series | float) ->
def normalize_gender(series: pd.Series) -> pd.Series:
# Приводим строковые значения пола к единому набору кодов
cleaned = series.fillna("UNKNOWN").astype(str).str.strip().str.upper()
mapping = {"M": "M", "MALE": "M", "F": "F", "FEMALE": "F"}
return cleaned.map(mapping).fillna("UNKNOWN")
def normalize_device(series: pd.Series) -> pd.Series:
# Схлопываем варианты платформ в читаемые подписи
cleaned = series.fillna("unknown").astype(str).str.strip()
lowered = cleaned.str.lower().str.replace(" ", "").str.replace("_", "")
mapping = {"android": "Android", "ios": "iOS", "ipados": "iPadOS", "ipad": "iPadOS"}
@@ -52,6 +56,7 @@ def normalize_device(series: pd.Series) -> pd.Series:
def add_age_group(df: pd.DataFrame) -> pd.DataFrame:
# Делим пользователей по возрастным корзинам для срезов
bins = [0, 25, 35, 45, 55, np.inf]
labels = ["<25", "25-34", "35-44", "45-54", "55+"]
df["age_group"] = pd.cut(df["age"], bins=bins, labels=labels, right=False)
@@ -59,6 +64,7 @@ def add_age_group(df: pd.DataFrame) -> pd.DataFrame:
def add_totals(df: pd.DataFrame) -> pd.DataFrame:
# Считаем суммарные показы/клики/заказы и CTR/CR метрики
df["active_imp_total"] = df[ACTIVE_IMP_COLS].sum(axis=1)
df["passive_imp_total"] = df[PASSIVE_IMP_COLS].sum(axis=1)
df["active_click_total"] = df[ACTIVE_CLICK_COLS].sum(axis=1)
@@ -75,6 +81,7 @@ def add_totals(df: pd.DataFrame) -> pd.DataFrame:
def add_flags(df: pd.DataFrame) -> pd.DataFrame:
# Создаём бинарные флаги наличия коммуникаций и заказов по клиенту
df["has_active_comm"] = (df[ACTIVE_IMP_COLS + ACTIVE_CLICK_COLS].sum(axis=1) > 0).astype(int)
df["has_passive_comm"] = (df[PASSIVE_IMP_COLS + PASSIVE_CLICK_COLS].sum(axis=1) > 0).astype(int)
df["has_any_order"] = (df[ORDER_COLS].sum(axis=1) > 0).astype(int)
@@ -83,6 +90,7 @@ def add_flags(df: pd.DataFrame) -> pd.DataFrame:
def load_data(path: Path | str = DATA_PATH) -> pd.DataFrame:
# Базовая загрузка CSV: приводим даты/категориальные поля и добавляем сводные метрики
df = pd.read_csv(path)
df["business_dt"] = pd.to_datetime(df["business_dt"])
df["gender_cd"] = normalize_gender(df["gender_cd"])
@@ -94,6 +102,7 @@ def load_data(path: Path | str = DATA_PATH) -> pd.DataFrame:
def describe_zero_share(df: pd.DataFrame, cols: Iterable[str]) -> pd.DataFrame:
# Формируем компактную статистику по выбранным числовым столбцам
stats = []
for col in cols:
series = df[col]
@@ -117,6 +126,7 @@ def describe_zero_share(df: pd.DataFrame, cols: Iterable[str]) -> pd.DataFrame:
def build_daily(df: pd.DataFrame) -> pd.DataFrame:
# Агрегируем метрики по дням, добавляя суммарные показатели и день недели
agg_cols = ACTIVE_IMP_COLS + PASSIVE_IMP_COLS + ACTIVE_CLICK_COLS + PASSIVE_CLICK_COLS + ORDER_COLS
daily = df.groupby("business_dt")[agg_cols].sum().reset_index()
daily = add_totals(daily)
@@ -125,6 +135,7 @@ def build_daily(df: pd.DataFrame) -> pd.DataFrame:
def build_client(df: pd.DataFrame) -> pd.DataFrame:
# Строим клиентские агрегаты и метаданные (мода по кат. полям, медиана возраста)
agg_spec: Dict[str, str] = {col: "sum" for col in ACTIVE_IMP_COLS + PASSIVE_IMP_COLS + ACTIVE_CLICK_COLS + PASSIVE_CLICK_COLS + ORDER_COLS}
meta_spec: Dict[str, str | callable] = {
"age": "median",