{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":7163,"databundleVersionId":44582},{"sourceType":"datasetVersion","sourceId":14641403,"datasetId":9353051,"databundleVersionId":15480749}],"dockerImageVersionId":31259,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"### Установка библиотек ###\n\n#!pip install -q duckdb\n#!pip install -q duckdb pyarrow\n\n### Импорт библиотек ###\n\nimport os\nimport time\nimport subprocess\nimport duckdb\nimport pandas as pd\nimport pyarrow as pa\nimport pyarrow.parquet as pq\nfrom IPython.display import display\nimport lightgbm as lgb\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import log_loss, classification_report\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import log_loss, classification_report\nimport numpy as np\n\n### КОНФИГУРАЦИЯ ###\n\n# Путь к сырым данным\nRAW_INPUT_DIR = '/kaggle/input/kkbox-churn-prediction-challenge'\n\n# Путь к подготовленным в parquet данным\nSILVER_DATASET_DIR = '/kaggle/input/kkbox-silver-parquet-kucheriavykh'\n\n# Рабочая папка\nWORKING_DIR = '/kaggle/working'","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-03-23T10:46:33.373503Z","iopub.execute_input":"2026-03-23T10:46:33.377707Z","iopub.status.idle":"2026-03-23T10:46:33.391499Z","shell.execute_reply.started":"2026-03-23T10:46:33.377584Z","shell.execute_reply":"2026-03-23T10:46:33.390342Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### Объявление функций для ETL \n\ndef get_data_paths():\n    \"\"\"\n    Возвращает словарь с путями к файлам для загрузки во фреймы данных.\n    Определяет наиболее доступное хранение сырых данных.\n    \"\"\"\n    \n    # Файлы, которые функция будет искать \n    files = {\n        'train': 'train.parquet',\n        'members': 'members.parquet',\n        'transactions': 'transactions.parquet',\n        'user_logs': 'user_logs.parquet'\n    }\n    \n    paths = {}\n    missing_files = []\n\n    # --- СТРАТЕГИЯ 1: ИЩЕМ В ПОДКЛЮЧЕННОМ ДАТАСЕТЕ ---\n    if os.path.exists(SILVER_DATASET_DIR):\n        all_found = True\n        temp_paths = {}\n        for key, filename in files.items():\n            full_path = f\"{SILVER_DATASET_DIR}/{filename}\"\n            if os.path.exists(full_path):\n                temp_paths[key] = full_path\n            else:\n                all_found = False\n                break\n        \n        if all_found:\n            print(f\"Найден подключенный датасет в {SILVER_DATASET_DIR}\")\n            return temp_paths\n    \n    print(\"Готовый датасет не найден или неполный.\")\n\n\n    # --- СТРАТЕГИЯ 2: ЗАПУСК ETL НА СЛУЧАЙ ОТСУСТВИЯ ПОДКЛЮЧЕННОГО ДАТАСЕТА ---\n    print(f\"Готовые файлы недоступны. Отсутствуют: {missing_files}\")\n    print(\"Запуск процесса генерации (займет 20-30 минут)...\")\n    \n    # Запускаем функцию генерации\n    run_fallback_etl()\n    \n    # После генерации пути будут в working\n    for key, filename in files.items():\n        paths[key] = f\"{WORKING_DIR}/{filename}\"\n        \n    return paths\n\n\n# КОД ETL НА СЛУЧАЙ ОТСУСТВИЯ ПОДКЛЮЧЕННОГО ДАТАСЕТА \ndef run_fallback_etl():\n    \"\"\"Содержит логику загрузки данных для фреймов разного размера\n    Для большого обработка потоком (порциями), для маленьких полная загрузка\"\"\"\n    \n    # Обычная загрузка\n    def process_simple(archive, csv_n, parquet_n):\n        if os.path.exists(f\"{WORKING_DIR}/{parquet_n}\"): return\n        print(f\"Processing {parquet_n}...\")\n        os.system(f\"7z x {RAW_INPUT_DIR}/{archive} -o{WORKING_DIR} -y > /dev/null\")\n        con = duckdb.connect()\n        con.execute(f\"COPY (SELECT * FROM '{WORKING_DIR}/{csv_n}') TO '{WORKING_DIR}/{parquet_n}' (FORMAT PARQUET, COMPRESSION 'ZSTD')\")\n        os.remove(f\"{WORKING_DIR}/{csv_n}\")\n\n    # Потоковая обработка \n    def process_streaming(archive, parquet_n):\n        if os.path.exists(f\"{WORKING_DIR}/{parquet_n}\"): return\n        print(f\"Streaming {parquet_n}...\")\n        proc = subprocess.Popen(['7z', 'x', f\"{RAW_INPUT_DIR}/{archive}\", '-so'], stdout=subprocess.PIPE)\n        chunk_iter = pd.read_csv(proc.stdout, chunksize=1_000_000)\n        writer = None\n        for chunk in chunk_iter:\n            table = pa.Table.from_pandas(chunk)\n            if writer is None: writer = pq.ParquetWriter(f\"{WORKING_DIR}/{parquet_n}\", table.schema, compression='ZSTD')\n            writer.write_table(table)\n        if writer: writer.close()\n        proc.terminate()\n\n    # Запуск\n    process_simple('train.csv.7z', 'train.csv', 'train.parquet')\n    process_simple('members_v3.csv.7z', 'members_v3.csv', 'members.parquet')\n    process_simple('transactions.csv.7z', 'transactions.csv', 'transactions.parquet')\n    process_streaming('user_logs.csv.7z', 'user_logs.parquet')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T10:46:33.393702Z","iopub.execute_input":"2026-03-23T10:46:33.394154Z","iopub.status.idle":"2026-03-23T10:46:33.422099Z","shell.execute_reply.started":"2026-03-23T10:46:33.394122Z","shell.execute_reply":"2026-03-23T10:46:33.420610Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Получаем пути к файлам\ndata_paths = get_data_paths()\n\ncon = duckdb.connect()\n\n# Создаём views\nfor table_name, file_path in data_paths.items():\n    con.execute(f\"CREATE OR REPLACE VIEW {table_name} AS SELECT * FROM '{file_path}'\")\n\n\n# Вывод данных для ознакомления \nprint(\"Train:\")\ndf_train_sample = con.execute(\"SELECT * FROM train LIMIT 5\").df()\ndisplay(df_train_sample)\n\nprint(\"\\nMembers:\")\ndf_members_sample = con.execute(\"SELECT * FROM members LIMIT 5\").df()\ndisplay(df_members_sample)\n\nprint(\"\\nTransactions:\")\ndf_trans_sample = con.execute(\"SELECT * FROM transactions LIMIT 5\").df()\ndisplay(df_trans_sample)\n\nprint(\"\\nUser_logs:\")\ndf_logs_sample = con.execute(\"SELECT * FROM user_logs LIMIT 5\").df()\ndisplay(df_logs_sample)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T10:46:33.423420Z","iopub.execute_input":"2026-03-23T10:46:33.423806Z","iopub.status.idle":"2026-03-23T10:46:34.558420Z","shell.execute_reply.started":"2026-03-23T10:46:33.423714Z","shell.execute_reply":"2026-03-23T10:46:34.556787Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### --- Создание простых признаков --- ###\n\n# Тюнинг DuckDB\ncon.execute(\"SET preserve_insertion_order=false\") # для ускорения записи в parquet \ncon.execute(\"SET threads=1\") # для страховки на случай лимита по ядрам в Kaggle Notebook \ncon.execute(\"SET memory_limit='10GB'\")\n\nstart_time = time.time()\n\n### Агрегация Логов ###\n\nprint(\"\\nОбработка User Logs\")\nlogs_query = f\"\"\"\n    SELECT \n        msno,\n        COUNT(DISTINCT date) as logs_active_days,\n        SUM(total_secs) as logs_total_secs,\n        SUM(total_secs) / NULLIF(COUNT(DISTINCT date), 0) as logs_daily_avg_secs,\n        CAST(SUM(num_100) AS DOUBLE) / (SUM(num_25 + num_50 + num_75 + num_985 + num_100) + 1) as logs_completion_rate\n    FROM user_logs\n    WHERE msno IN (SELECT msno FROM train)\n    GROUP BY msno\n\"\"\"\n\ncon.execute(f\"COPY ({logs_query}) TO '/kaggle/working/temp_logs.parquet' (FORMAT PARQUET)\")\nprint(\"\\nЛоги посчитаны\")\n\n### Агрегация Транзакций ###\n\nprint(\"\\nОбработка Transactions\")\n\ntrans_stat_query = f\"\"\"\n    SELECT \n        msno,\n        COUNT(*) as trans_count,\n        SUM(plan_list_price) as trans_total_paid,\n        AVG(payment_plan_days) as trans_avg_plan_days\n    FROM transactions\n    WHERE msno IN (SELECT msno FROM train)\n    GROUP BY msno\n\"\"\"\ncon.execute(f\"COPY ({trans_stat_query}) TO '/kaggle/working/temp_trans_stat.parquet' (FORMAT PARQUET)\")\n\ntrans_last_query = f\"\"\"\n    SELECT \n        msno,\n        is_auto_renew as last_auto_renew,\n        is_cancel as last_is_cancel,\n        transaction_date as last_trans_date_int\n    FROM transactions\n    WHERE msno IN (SELECT msno FROM train)\n    QUALIFY ROW_NUMBER() OVER (PARTITION BY msno ORDER BY transaction_date DESC) = 1\n\"\"\"\ncon.execute(f\"COPY ({trans_last_query}) TO '/kaggle/working/temp_trans_last.parquet' (FORMAT PARQUET)\")\nprint(\"\\nТранзакции посчитаны\")\n\n### Сборка ###\n\nprint(\"\\nСборка Master Table\")\n\nfinal_query = f\"\"\"\nSELECT \n    t.msno,\n    t.is_churn,\n    \n    m.city,\n    m.gender,\n    m.registered_via,\n    CASE WHEN bd < 5 OR bd > 80 THEN NULL ELSE bd END as age_clean,\n    date_diff('day', TRY_CAST(strptime(CAST(registration_init_time AS VARCHAR), '%Y%m%d') AS DATE), DATE '2017-02-01') as tenure_days,\n    \n    COALESCE(l.logs_active_days, 0) as logs_active_days,\n    COALESCE(l.logs_total_secs, 0) as logs_total_secs,\n    COALESCE(l.logs_daily_avg_secs, 0) as logs_daily_avg_secs,\n    COALESCE(l.logs_completion_rate, 0) as logs_completion_rate,\n    \n    COALESCE(tr.trans_count, 0) as trans_count,\n    COALESCE(tr.trans_total_paid, 0) as trans_total_paid,\n    COALESCE(tr.trans_avg_plan_days, 0) as trans_avg_plan_days,\n    \n    COALESCE(tl.last_auto_renew, 0) as last_auto_renew,\n    COALESCE(tl.last_is_cancel, 0) as last_is_cancel,\n    CASE \n        WHEN tl.last_trans_date_int IS NOT NULL \n        THEN date_diff('day', strptime(CAST(tl.last_trans_date_int AS VARCHAR), '%Y%m%d'), DATE '2017-02-01')\n        ELSE 999 \n    END as days_since_last_trans\n\nFROM train t\nLEFT JOIN members m ON t.msno = m.msno\nLEFT JOIN '/kaggle/working/temp_logs.parquet' l ON t.msno = l.msno\nLEFT JOIN '/kaggle/working/temp_trans_stat.parquet' tr ON t.msno = tr.msno\nLEFT JOIN '/kaggle/working/temp_trans_last.parquet' tl ON t.msno = tl.msno\n\"\"\"\n\ndf_train = con.execute(final_query).df()\n\n# Удаляем временные файлы\nos.system(\"rm /kaggle/working/temp_*.parquet\")\n\nprint(f\"\\nМастер-таблица готова. Размер: {df_train.shape}\")\nprint(f\"Общее время: {(time.time() - start_time):.2f} сек\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T10:46:34.560686Z","iopub.execute_input":"2026-03-23T10:46:34.561174Z","iopub.status.idle":"2026-03-23T10:56:52.280897Z","shell.execute_reply.started":"2026-03-23T10:46:34.561139Z","shell.execute_reply":"2026-03-23T10:56:52.279456Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### --- Обучение базовой модели LightGBM на простых данных --- ###\n\n# Помечаем категориальные признаки для нативной поддержки LightGBM\ncategorical_cols = ['city', 'gender', 'registered_via']\n\nfor col in categorical_cols:\n    df_train[col] = df_train[col].astype('category')\n\n# Подготавливаем датасеты для обучения\nX = df_train.drop(['msno', 'is_churn'], axis=1)\ny = df_train['is_churn']\n\n# Нарезаем датасеты \nX_train, X_val, y_train, y_val = train_test_split(\n    X, y, test_size=0.2, random_state=42, stratify=y\n)\n\n# Конвертируем датасеты в формат для LightGBM\ndtrain = lgb.Dataset(X_train, label=y_train, categorical_feature=categorical_cols)\ndval = lgb.Dataset(X_val, label=y_val, reference=dtrain, categorical_feature=categorical_cols)\n\nparams = {\n    'objective': 'binary',           # Определяем цель как задачу классификации\n    'metric': 'binary_logloss',      # Указываем Бинарную перекрёстную энтропию как метрику\n    'boosting_type': 'gbdt',         # Далее - стандартные параметры\n    'learning_rate': 0.05,\n    'num_leaves': 31, \n    'max_depth': -1,\n    'feature_fraction': 0.8,\n    'bagging_fraction': 0.8,\n    'bagging_freq': 5,\n    'verbose': -1,\n    'n_jobs': -1 \n}\n\n# Запуск обучения \nmodel = lgb.train(\n    params,\n    dtrain,\n    valid_sets=[dtrain, dval],\n    valid_names=['train', 'valid'],\n    num_boost_round=1000,\n    callbacks=[\n        lgb.early_stopping(stopping_rounds=50),     # Если ошибка перестала падать - стоп\n        lgb.log_evaluation(period=200)              # Логируем каждые 200 шагов (а не все) \n    ]\n)\n\n# Оценка результата\npreds = model.predict(X_val)\nscore = log_loss(y_val, preds)\n\n# Итоговый признак оттока\ny_pred_binary = (preds > 0.5).astype(int)\nprint(\"\\nОтчёт по классификации модели с простыми признаками:\")\nprint(classification_report(y_val, y_pred_binary))\n\nprint(f\"Итоговая ошибка модели с простыми признаками на валидационном датасете: {score:.5f}\")\n\nlgb.plot_importance(model, max_num_features=15, importance_type='gain', figsize=(10, 6), title='Важность признаков (простые)')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T10:56:52.284216Z","iopub.execute_input":"2026-03-23T10:56:52.284670Z","iopub.status.idle":"2026-03-23T10:58:01.416533Z","shell.execute_reply.started":"2026-03-23T10:56:52.284625Z","shell.execute_reply":"2026-03-23T10:58:01.413128Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### --- Создание продвинутых признаков --- ###\n\n# Очистка рабочей директории - иначе лимит по хранению\nos.system(\"rm -rf /kaggle/working/*\") \n\nstart_time = time.time()\n\n# Снова достаём данные и создаём коннектор duckdb т.к. при очистке они сносятся\ndata_paths = get_data_paths()\ncon = duckdb.connect()\n\n# Создаём views\nfor table_name, file_path in data_paths.items():\n    con.execute(f\"CREATE OR REPLACE VIEW {table_name} AS SELECT * FROM '{file_path}'\")\n\n# Kaggle Notebook падает по лимиту на оперативку при попытки записи нового датасета логов\ncon.execute(\"SET memory_limit='10GB'\")\ncon.execute(\"SET threads=1\") \ncon.execute(\"SET preserve_insertion_order=false\") \n\n### Агрегация Логов ###\n\nprint(\"\\nОбработка User Logs\")\n\n# Подготовка значений для дальнейшего расчёта динамики и отклонения \nlogs_query = f\"\"\"\n    SELECT \n        msno,\n        COUNT(DISTINCT date) as logs_active_days,\n        SUM(total_secs) as logs_total_secs,\n        SUM(total_secs * total_secs) as logs_sum_sq,\n        \n        -- Окно 1 (после января 2017)\n        SUM(CASE WHEN date >= 20170101 THEN total_secs ELSE 0 END) as secs_w1,\n        COUNT(CASE WHEN date >= 20170101 THEN 1 ELSE NULL END) as days_w1,\n        \n        -- Окно 2 (между декабрём 2016 и январём 2017)\n        SUM(CASE WHEN date >= 20161201 AND date < 20170101 THEN total_secs ELSE 0 END) as secs_w2\n        \n    FROM user_logs\n    WHERE msno IN (SELECT msno FROM train)\n    GROUP BY msno\n\"\"\"\ncon.execute(f\"COPY ({logs_query}) TO '/kaggle/working/temp_logs_safe.parquet' (FORMAT PARQUET)\")\nprint(\"\\nЛоги посчитаны\")\n\n### --- Агрегация Транзакций --- ###\n\nprint(\"\\nОбработка Transactions\")\n\n# Общая статистика\ntrans_stat_query = f\"\"\"\n    SELECT \n        msno,\n        COUNT(*) as trans_count,\n        SUM(plan_list_price) as trans_total_paid,\n        AVG(payment_plan_days) as trans_avg_plan_days\n    FROM transactions\n    WHERE msno IN (SELECT msno FROM train)\n    GROUP BY msno\n\"\"\"\ncon.execute(f\"COPY ({trans_stat_query}) TO '/kaggle/working/temp_trans_stat.parquet' (FORMAT PARQUET)\")\n\n# Последний статус\ntrans_last_query = f\"\"\"\n    SELECT \n        msno,\n        is_auto_renew as last_auto_renew,\n        is_cancel as last_is_cancel,\n        transaction_date as last_trans_date_int,\n        plan_list_price,\n        actual_amount_paid,\n        (plan_list_price - actual_amount_paid) as last_discount\n    FROM transactions\n    WHERE msno IN (SELECT msno FROM train)\n    QUALIFY ROW_NUMBER() OVER (PARTITION BY msno ORDER BY transaction_date DESC) = 1\n\"\"\"\ncon.execute(f\"COPY ({trans_last_query}) TO '/kaggle/working/temp_trans_last.parquet' (FORMAT PARQUET)\")\nprint(\"\\nТранзакции посчитаны\")\n\n### --- Сборка --- ###\n\nprint(\"\\nСборка Master Table...\")\n\nfinal_query = f\"\"\"\nSELECT \n    t.msno,\n    t.is_churn,\n    \n    -- Профиль клиента\n    m.city,\n    m.gender,\n    m.registered_via,\n    CASE WHEN bd < 5 OR bd > 80 THEN NULL ELSE bd END as age_clean,\n    date_diff('day', TRY_CAST(strptime(CAST(registration_init_time AS VARCHAR), '%Y%m%d') AS DATE), DATE '2017-02-01') as tenure_days,\n    \n    -- Логи\n    COALESCE(l.logs_active_days, 0) as logs_active_days,\n    COALESCE(l.logs_total_secs, 0) as logs_total_secs,\n    CASE \n        WHEN l.logs_active_days > 1 THEN \n            SQRT(ABS((l.logs_sum_sq / l.logs_active_days) - POWER(l.logs_total_secs / l.logs_active_days, 2)))\n        ELSE 0 \n    END as logs_secs_stddev,\n    (COALESCE(l.secs_w1, 0) - COALESCE(l.secs_w2, 0)) as trend_secs_diff,\n    \n    -- Данные по платежу \n    COALESCE(ts.trans_count, 0) as trans_count,\n    COALESCE(ts.trans_total_paid, 0) as trans_total_paid,\n    COALESCE(ts.trans_avg_plan_days, 0) as trans_avg_plan_days,\n    COALESCE(tl.last_auto_renew, 0) as last_auto_renew,\n    COALESCE(tl.last_is_cancel, 0) as last_is_cancel,\n    COALESCE(tl.last_discount, 0) as last_discount,\n    \n    CASE \n        WHEN tl.last_trans_date_int IS NOT NULL \n        THEN date_diff('day', strptime(CAST(tl.last_trans_date_int AS VARCHAR), '%Y%m%d'), DATE '2017-02-01')\n        ELSE 999 \n    END as days_since_last_trans,\n    \n    -- Взаимодействие с сервисом\n    CASE \n        WHEN l.days_w1 > 0 THEN (tl.actual_amount_paid / l.days_w1)\n        ELSE 0 \n    END as price_per_active_day\n\nFROM train t\nLEFT JOIN members m ON t.msno = m.msno\nLEFT JOIN '/kaggle/working/temp_logs_safe.parquet' l ON t.msno = l.msno\nLEFT JOIN '/kaggle/working/temp_trans_stat.parquet' ts ON t.msno = ts.msno\nLEFT JOIN '/kaggle/working/temp_trans_last.parquet' tl ON t.msno = tl.msno\n\"\"\"\n\ndf_train_advanced = con.execute(final_query).df()\n\n# Удаляем временные файлы\nos.system(\"rm /kaggle/working/temp_*.parquet\")\n\nprint(f\"\\nРасширенная мастер-таблица готова. Размер: {df_train_advanced.shape}\")\nprint(f\"Общее время: {(time.time() - start_time):.2f} сек\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T10:58:01.422353Z","iopub.execute_input":"2026-03-23T10:58:01.424772Z","iopub.status.idle":"2026-03-23T11:08:29.054548Z","shell.execute_reply.started":"2026-03-23T10:58:01.424554Z","shell.execute_reply":"2026-03-23T11:08:29.051233Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### --- Обучение базовой модели LightGBM на расширенных данных --- ###\n\n# Помечаем категориальные признаки для нативной поддержки LightGBM\ncategorical_cols = ['city', 'gender', 'registered_via']\n\nfor col in categorical_cols:\n    df_train_advanced[col] = df_train_advanced[col].astype('category')\n\n# Подготавливаем датасеты для обучения\nX = df_train_advanced.drop(['msno', 'is_churn'], axis=1)\ny = df_train_advanced['is_churn']\n\n# Нарезаем датасеты\nX_train, X_val, y_train, y_val = train_test_split(\n    X, y, test_size=0.2, random_state=42, stratify=y\n)\n\n# Конвертируем датасеты в формат для LightGBM\ndtrain = lgb.Dataset(X_train, label=y_train, categorical_feature=categorical_cols)\ndval = lgb.Dataset(X_val, label=y_val, reference=dtrain, categorical_feature=categorical_cols)\n\nparams = {\n    'objective': 'binary',             # Настройки идентичны как у варианта с простыми признаками\n    'metric': 'binary_logloss',\n    'boosting_type': 'gbdt',\n    'learning_rate': 0.05,\n    'num_leaves': 31,\n    'max_depth': -1,\n    'feature_fraction': 0.8,\n    'bagging_fraction': 0.8,\n    'bagging_freq': 5,\n    'n_jobs': -1,\n    'verbose': -1\n}\n\n# Запуск обучения \nmodel_adv = lgb.train(\n    params,\n    dtrain,\n    valid_sets=[dtrain, dval],\n    valid_names=['train', 'valid'],\n    num_boost_round=1000,\n    callbacks=[\n        lgb.early_stopping(stopping_rounds=50),\n        lgb.log_evaluation(period=200)\n    ]\n)\n\n\n# Оценка результата\npreds = model_adv.predict(X_val)\nLightGBM_fin_loss = log_loss(y_val, preds)\nprint(f\"\\nИтоговая ошибка модели с продвинутыми признаками на валидационном датасете {LightGBM_fin_loss:.5f}\")\n\n# Итоговый признак оттока\ny_pred_binary = (preds > 0.5).astype(int)\nprint(\"\\nОтчёт по классификации модели с продвинутыми признаками:\")\nprint(classification_report(y_val, y_pred_binary))\n\nplt.figure(figsize=(12, 8))\nlgb.plot_importance(model_adv, max_num_features=20, importance_type='gain', height=0.5, \n                    title='Важность признаков (продвинутые)', xlabel='Gain Value')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T11:08:29.060100Z","iopub.execute_input":"2026-03-23T11:08:29.063702Z","iopub.status.idle":"2026-03-23T11:09:47.149840Z","shell.execute_reply.started":"2026-03-23T11:08:29.063428Z","shell.execute_reply":"2026-03-23T11:09:47.148785Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### --- Подготовка данных для самописной нейросети --- ###\n\n# Работаем с копией, чтобы не сломать оригинал \ndf_nn = df_train_advanced.copy()\n\n### МАТЕМАТИЧЕСКАЯ АДАПТАЦИЯ ###\n\n### Определение групп признаков\n# Числовые (для масштабирования)\nnum_cols = [\n    'age_clean', 'tenure_days', \n    'logs_active_days', 'logs_total_secs', 'logs_secs_stddev', 'trend_secs_diff',\n    'trans_count', 'trans_total_paid', 'trans_avg_plan_days', \n    'days_since_last_trans', 'price_per_active_day', 'last_discount',\n    'last_auto_renew', 'last_is_cancel' # Тоже нормализуем, хуже не будет\n]\n\n# Категориальные (для One-Hot)\ncat_cols = ['city', 'gender', 'registered_via']\n\n# Обработка пропусков\ndf_nn[num_cols] = df_nn[num_cols].fillna(0)\n\n# Кодирование One-Hot\ndf_nn = pd.get_dummies(df_nn, columns=cat_cols, drop_first=True, dummy_na=True)\n\n# Создаём \"трафарет\" с признаками, которые пойдут в обучающую выборку\nfeature_cols = [c for c in df_nn.columns if c not in ['msno', 'is_churn']]\n\n# Нормализация чисел (масштабирование среднего к нулю, стандартное отклонение к 1)\nscaler = StandardScaler()\n\n# Масштабируем только числовые колонки\ndf_nn[num_cols] = scaler.fit_transform(df_nn[num_cols])\n\n### ПРИВЕДЕНИЕ К ТИПУ ДАННЫХ ДЛЯ ОБУЧЕНИЯ НЕЙРОСЕТИ ###\n\n# Создаём тензоры\nX = df_nn[feature_cols].values.astype(np.float32)\ny = df_nn['is_churn'].values.astype(np.float32)\n\n### СОЗДАНИЕ КОНВЕЙЕРА ОБУЧЕНИЯ ###\n\nbatch_size = 2048\n\n# Разделение\nX_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)\n\n# Сцепка данных - ответов\ntrain_dataset = torch.utils.data.TensorDataset(torch.tensor(X_train), torch.tensor(y_train))\nval_dataset = torch.utils.data.TensorDataset(torch.tensor(X_val), torch.tensor(y_val))\n\n# Итератор\ntrain_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\nval_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)\n\nprint(f\"Данные для обучения нейросети готовы, число колонок: {X_train.shape[1]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T11:09:47.151458Z","iopub.execute_input":"2026-03-23T11:09:47.151947Z","iopub.status.idle":"2026-03-23T11:09:54.974128Z","shell.execute_reply.started":"2026-03-23T11:09:47.151911Z","shell.execute_reply":"2026-03-23T11:09:54.973011Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### --- Нейросеть --- ###\n\n# Проверка вдруг у Kaggle доступны GPU \ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Доступно: {device}\")\n\n# Определение архитектуры модели \nclass ChurnModel(nn.Module):\n    def __init__(self, input_dim):\n        super(ChurnModel, self).__init__()\n        \n        self.net = nn.Sequential(\n            # Слой 1\n            nn.Linear(input_dim, 128), # Аффинное преобразование, 128 нейронов \n            nn.BatchNorm1d(128),       # Повторная нормализация против Внутреннего ковариантного смещения\n            nn.ReLU(),                 # Нелинейная активация \n            nn.Dropout(0.3),           # Выключение 30% случайных нейронов в каждой эпохе против коадаптации весов \n            \n            # Слой 2\n            nn.Linear(128, 64),        # Понижаем кол-во нейронов до 64 \n            nn.BatchNorm1d(64),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            \n            # Выход\n            nn.Linear(64, 1),          # На выходе один нейрон \n            nn.Sigmoid()               # Финальная нормировка сигнала \n        )\n\n    def forward(self, x):\n        return self.net(x)\n\n# Инициализация\nmodel = ChurnModel(input_dim=X_train.shape[1]).to(device)\n\n# Минимизируем Бинарную перекрёстную энтропию\ncriterion = nn.BCELoss() \n\n# Метод оптимизации - Градиентный спуск, но только батчевый \noptimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) \n\n\n### ЗАПУСК ЦИКЛА ОБУЧЕНИЯ\n\n# Определяем кол-во эпох обучения\nepochs = 20\n\n# Инициализируем хранение истории обучения \nhistory = {'train_loss': [], 'val_loss': []}\n\nfor epoch in range(epochs):\n    \n    # Режим с DropOut и Повторной нормализацией - обучение\n    model.train()\n\n    # Обнуляем ошибку для эпохи\n    running_loss = 0.0\n\n    # Запуск итератора \n    for inputs, labels in train_loader:\n\n        # Перенос из оперативки на GPU (если доступна)\n        inputs, labels = inputs.to(device), labels.to(device)\n\n        # Обнуление градиента (по умолчанию PyTorch этого не делает)\n        optimizer.zero_grad()\n\n        # Прямой проход - предсказание нейросети\n        outputs = model(inputs)\n\n        # Оценка ошибки\n        loss = criterion(outputs.squeeze(), labels)\n\n        # Вычисление производных по Обратному распространению ошибки \n        loss.backward()\n\n        # Вычисление и применение сдвигов весов и смещений по Методу оптимизации\n        optimizer.step()\n\n        # Считаем ошибку в эпохе\n        running_loss += loss.item() * inputs.size(0)\n    \n    # Оценка эпохи на отложенной выборке без DropOut и Повторной нормализации \n    model.eval()\n    val_loss = 0.0\n\n    # Отключаем вычисление производных \n    with torch.no_grad():\n\n        # Итерируемся по отложенной выборке \n        for inputs, labels in val_loader:\n            inputs, labels = inputs.to(device), labels.to(device)\n            outputs = model(inputs)\n            loss = criterion(outputs.squeeze(), labels)\n            val_loss += loss.item() * inputs.size(0)\n    \n    # Оценка эпохи\n    epoch_train_loss = running_loss / len(train_loader.dataset)\n    epoch_val_loss = val_loss / len(val_loader.dataset)\n\n    # Обновление истории обучения \n    history['train_loss'].append(epoch_train_loss)\n    history['val_loss'].append(epoch_val_loss)\n    \n    if (epoch + 1) % 2 == 0:\n        print(f\"Эпоха {epoch+1}/{epochs} | Обучение: {epoch_train_loss:.4f} | Валидация: {epoch_val_loss:.4f}\")\n\n\n### ИТОГИ ОБУЧЕНИЯ \n\n# Включаем полноценный режим \nmodel.eval()\n\n# Выключаем вычисление производных\nwith torch.no_grad():\n\n    # Полная оценка модели на проверочной выборке \n    y_val_preds_nn = model(torch.tensor(X_val).to(device)).cpu().numpy().squeeze()\n\n# Расчёт итоговой метрики для сравнения самописной нейросети и LightGBM\nnn_loss = log_loss(y_val, y_val_preds_nn)\n\n# Финальный вердикт модели по оттоку с порогом отсечения 0,5\ny_pred_binary_nn = (y_val_preds_nn > 0.5).astype(int)\n\nprint(f\"Ошибка самописной неросети: {nn_loss:.5f}\")\nprint(f\"Ошибка LightGBM: {LightGBM_fin_loss:.5f}\")\n\nprint(\"\\nОтчёт по самописной нейросети:\")\nprint(classification_report(y_val, y_pred_binary_nn))\n\n# График обучения\nplt.figure(figsize=(10, 5))\nplt.plot(history['train_loss'], label='Ошибка при обучении')\nplt.plot(history['val_loss'], label='Ошибка при валидации')\nplt.title('История обучения самописной нейросети')\nplt.xlabel('Эпоха')\nplt.ylabel('Ошибка')\nplt.legend()\nplt.grid(True)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T11:09:54.975799Z","iopub.execute_input":"2026-03-23T11:09:54.976253Z","iopub.status.idle":"2026-03-23T11:17:50.439112Z","shell.execute_reply.started":"2026-03-23T11:09:54.976220Z","shell.execute_reply":"2026-03-23T11:17:50.437876Z"}},"outputs":[],"execution_count":null}]}