import pandas as pd def build_df( visits_df: pd.DataFrame, ads_activity_df: pd.DataFrame, surf_depth_df: pd.DataFrame, primary_device_df: pd.DataFrame, cloud_usage_df: pd.DataFrame, users_df: pd.DataFrame = pd.DataFrame(), ) -> pd.DataFrame: # 1. Создаём копии, чтобы не изменять исходные датафреймы visits_df = visits_df.copy() ads_df = ads_activity_df.copy() surf_df = surf_depth_df.copy() device_df = primary_device_df.copy() cloud_df = cloud_usage_df.copy() users_df = users_df.copy() users_df_empty = not users_df.empty # 2. Базовая очистка данных visits_df = visits_df.drop_duplicates( subset=["date", "daytime", "session_id", "user_id", "website_category"] ) ads_df = ads_df.drop_duplicates(subset=["user_id"]) if users_df_empty: users_df = users_df.drop_duplicates(subset=["user_id"]) # 3. Нормализация значений visits_df daytime_map = { "утро": "morning", "день": "day", "вечер": "evening", "ночь": "night", } visits_df["daytime"] = visits_df["daytime"].replace(daytime_map) # Нормализуем категории сайтов: "Category 18" -> "category_18" visits_df["website_category"] = ( visits_df["website_category"] .astype(str) .str.strip() .str.replace(" ", "_", regex=False) .str.lower() ) # Преобразуем date в datetime.date, чтобы корректно считать количество уникальных дней активности visits_df["date"] = pd.to_datetime(visits_df["date"], errors="coerce").dt.date # 4. Генерация признаков из visits_df на уровне пользователя (1 user_id = 1 строка) # 4.1. Признаки интенсивности base_stats_df = visits_df.groupby("user_id").agg( total_sessions=("session_id", "nunique"), unique_days=("date", "nunique"), n_unique_categories=("website_category", "nunique") ) # Среднее число сессий в день (страховка от деления на 0) base_stats_df["sessions_per_day"] = base_stats_df["total_sessions"] / base_stats_df[ "unique_days" ].replace(0, 1) # 4.2. Доли активности по времени суток (shares) daytime_shares_df = pd.crosstab( visits_df["user_id"], visits_df["daytime"], normalize="index", ) # Фиксируем набор колонок, чтобы при отсутствии какого-то времени суток в данных структура не менялась expected_daytimes = ["morning", "day", "evening", "night"] daytime_shares_df = daytime_shares_df.reindex( columns=expected_daytimes, fill_value=0.0 ) daytime_shares_df.columns = [ f"daytime_{c}_share" for c in daytime_shares_df.columns ] # Наиболее активное время суток (категориальный признак на основе максимум-доли) base_stats_df["most_active_daytime"] = ( daytime_shares_df.idxmax(axis=1) .str.replace("daytime_", "", regex=False) .str.replace("_share", "", regex=False) ) # 4.3. Доли активности по категориям сайтов webcat_shares_df = pd.crosstab( visits_df["user_id"], visits_df["website_category"], normalize="index", ) # Стабильный порядок колонок (для воспроизводимости результатов) webcat_cols = sorted(webcat_shares_df.columns) webcat_shares_df = webcat_shares_df.reindex(columns=webcat_cols, fill_value=0.0) webcat_shares_df.columns = [f"web_{c}_share" for c in webcat_shares_df.columns] # Максимальная доля среди категорий - признак концентрации интересов пользователя base_stats_df["top_category_share"] = webcat_shares_df.max(axis=1) # 4.4. Финальная сборка visits-признаков visits_features_df = pd.concat( [base_stats_df, daytime_shares_df, webcat_shares_df], axis=1 ).reset_index() # 5. Нормализация ads_activity (приводим к единым англоязычным категориям) ads_activity_map = { "очень редко": "very_rarely", "редко": "rarely", "умеренно": "moderately", "часто": "often", "очень часто": "very_often", } ads_df["ads_activity"] = ads_df["ads_activity"].replace(ads_activity_map) # 6. Нормализация surf_depth (унификация категорий глубины просмотра) surf_depth_map = { "поверхностно": "shallow", "средне": "medium", "глубоко": "deep", } surf_df["surf_depth"] = surf_df["surf_depth"].replace(surf_depth_map) # 7. Нормализация primary_device (унификация категорий устройства) primary_device_map = { "смартфон": "smartphone", "ПК": "pc", "ноутбук": "laptop", "планшет": "tablet", } device_df["primary_device"] = device_df["primary_device"].replace( primary_device_map ) # 7. Нормализация cloud_usage (bool -> category) cloud_usage_map = { False: "not", True: "yes", } cloud_df["cloud_usage"] = cloud_df["cloud_usage"].replace(cloud_usage_map) # 8. Объединение всех таблиц по user_id # Если передан users_df - используем его как базу (там целевая переменная), иначе собираем только признаки if users_df_empty: features_df = users_df.merge(visits_features_df, on="user_id", how="left") features_df = features_df.merge(ads_df, on="user_id", how="left") else: features_df = ads_df.merge(visits_features_df, on="user_id", how="left") features_df = features_df.merge(surf_df, on="user_id", how="left") features_df = features_df.merge(device_df, on="user_id", how="left") features_df = features_df.merge(cloud_df, on="user_id", how="left") return features_df