{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport lightgbm as lgb\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import mean_squared_error\nfrom sklearn.preprocessing import LabelEncoder\nimport os\n\nPATH = \"/kaggle/input/competitions/avito-demand-prediction\"\n\nprint(\"Загрузка данных...\")\ntrain = pd.read_csv(os.path.join(PATH, 'train.csv'), parse_dates=['activation_date'])\ntest = pd.read_csv(os.path.join(PATH, 'test.csv'), parse_dates=['activation_date'])\n\nprint(f\"Размер train: {train.shape}\")\nprint(f\"Размер test: {test.shape}\")\n\n# Посмотрим на первые строки и пропуски\nprint(train.info())","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-07-03T17:34:53.645102Z","iopub.execute_input":"2026-07-03T17:34:53.64539Z","iopub.status.idle":"2026-07-03T17:35:32.552752Z","shell.execute_reply.started":"2026-07-03T17:34:53.645367Z","shell.execute_reply":"2026-07-03T17:35:32.552064Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Объединяем для однородной обработки категорий\ntarget = train['deal_probability']\ntrain_len = len(train)\ndf = pd.concat([train.drop(['deal_probability'], axis=1), test], axis=0).reset_index(drop=True)\n\n# 1. Работа с датой\ndf['weekday'] = df['activation_date'].dt.weekday\ndf['day'] = df['activation_date'].dt.day\n\n# 2. Простейшее заполнение пропусков в цене\ndf['price'] = df['price'].fillna(df['price'].median())\n\n# 3. Кодируем категориальные признаки\ncat_cols = ['user_type', 'parent_category_name', 'category_name', 'region', 'city']\nfor col in cat_cols:\n    le = LabelEncoder()\n    df[col] = le.fit_transform(df[col].astype(str))\n\n# Выделяем числовые и закодированные признаки для бейзлайна\nfeatures = ['price', 'item_seq_number', 'user_type', 'parent_category_name', \n            'category_name', 'region', 'city', 'weekday', 'day']\n\nX_train = df.iloc[:train_len][features]\nX_test = df.iloc[train_len:][features]\ny_train = target\n\n# Разделяем на train/val для валидации\nX_tr, X_val, y_tr, y_val = train_test_split(X_train, y_train, test_size=0.2, random_state=42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T17:37:04.857502Z","iopub.execute_input":"2026-07-03T17:37:04.858111Z","iopub.status.idle":"2026-07-03T17:37:09.19734Z","shell.execute_reply.started":"2026-07-03T17:37:04.858079Z","shell.execute_reply":"2026-07-03T17:37:09.196351Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Создаем датасеты для LightGBM\nlgb_train = lgb.Dataset(X_tr, y_tr)\nlgb_val = lgb.Dataset(X_val, y_val, reference=lgb_train)\n\nparams = {\n    'objective': 'regression',\n    'metric': 'rmse',\n    'learning_rate': 0.1,\n    'num_leaves': 31,\n    'seed': 42,\n    'verbose': -1\n}\n\nprint(\"Обучение LightGBM...\")\nmodel = lgb.train(\n    params,\n    lgb_train,\n    num_boost_round=1000,\n    valid_sets=[lgb_train, lgb_val],\n    callbacks=[lgb.early_stopping(stopping_rounds=50), lgb.log_evaluation(100)]\n)\n\n# Проверяем локальный скор\nval_preds = model.predict(X_val)\nrmse = np.sqrt(mean_squared_error(y_val, val_preds))\nprint(f\"Локальный RMSE на валидации: {rmse:.5f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T17:37:20.040906Z","iopub.execute_input":"2026-07-03T17:37:20.041382Z","iopub.status.idle":"2026-07-03T17:37:49.229905Z","shell.execute_reply.started":"2026-07-03T17:37:20.041351Z","shell.execute_reply":"2026-07-03T17:37:49.229131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.decomposition import TruncatedSVD\n\nprint(\"Обработка текстовых признаков...\")\n\n# Заполняем пропуски пустыми строками\ndf['title'] = df['title'].fillna('').astype(str)\ndf['description'] = df['description'].fillna('').astype(str)\n\n# 1. Считаем простые длины текстов (в символах и словах)\ndf['title_len'] = df['title'].apply(len)\ndf['description_len'] = df['description'].apply(len)\ndf['title_word_count'] = df['title'].apply(lambda x: len(x.split()))\ndf['description_word_count'] = df['description'].apply(lambda x: len(x.split()))\n\n# 2. Настраиваем TF-IDF + SVD для Title\nprint(\"Выделяем фичи из title...\")\ntfidf_title = TfidfVectorizer(max_features=5000, stop_words=None) # Для русского языка можно без стоп-слов на базовом этапе\nsvd_title = TruncatedSVD(n_components=5, random_state=42)\n\ntitle_tfidf = tfidf_title.fit_transform(df['title'])\ntitle_svd = svd_title.fit_transform(title_tfidf)\n\n# 3. Настраиваем TF-IDF + SVD для Description\nprint(\"Выделяем фичи из description...\")\ntfidf_desc = TfidfVectorizer(max_features=10000, stop_words=None)\nsvd_desc = TruncatedSVD(n_components=5, random_state=42)\n\ndesc_tfidf = tfidf_desc.fit_transform(df['description'])\ndesc_svd = svd_desc.fit_transform(desc_tfidf)\n\n# Переносим полученные SVD-компоненты в наш основной датафрейм\nfor i in range(5):\n    df[f'title_svd_{i}'] = title_svd[:, i]\n    df[f'desc_svd_{i}'] = desc_svd[:, i]\n\nprint(\"Текстовые фичи успешно добавлены!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T17:39:39.732554Z","iopub.execute_input":"2026-07-03T17:39:39.733018Z","iopub.status.idle":"2026-07-03T17:41:02.837522Z","shell.execute_reply.started":"2026-07-03T17:39:39.732986Z","shell.execute_reply":"2026-07-03T17:41:02.836771Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Расширяем список фич\ntext_features = ['title_len', 'description_len', 'title_word_count', 'description_word_count'] + \\\n                [f'title_svd_{i}' for i in range(5)] + \\\n                [f'desc_svd_{i}' for i in range(5)]\n\nall_features = features + text_features\n\n# Пересобираем матрицы\nX_train_new = df.iloc[:train_len][all_features]\ny_train_new = target\n\nX_tr_n, X_val_n, y_tr_n, y_val_n = train_test_split(X_train_new, y_train_new, test_size=0.2, random_state=42)\n\nlgb_train_n = lgb.Dataset(X_tr_n, y_tr_n)\nlgb_val_n = lgb.Dataset(X_val_n, y_val_n, reference=lgb_train_n)\n\nprint(\"Обучение новой модели с текстовыми фичами...\")\nmodel_n = lgb.train(\n    params,\n    lgb_train_n,\n    num_boost_round=1000,\n    valid_sets=[lgb_train_n, lgb_val_n],\n    callbacks=[lgb.early_stopping(stopping_rounds=50), lgb.log_evaluation(100)]\n)\n\nval_preds_n = model_n.predict(X_val_n)\nrmse_n = np.sqrt(mean_squared_error(y_val_n, val_preds_n))\nprint(f\"Новый локальный RMSE на валидации: {rmse_n:.5f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T17:41:06.550684Z","iopub.execute_input":"2026-07-03T17:41:06.551761Z","iopub.status.idle":"2026-07-03T17:41:56.079514Z","shell.execute_reply.started":"2026-07-03T17:41:06.551718Z","shell.execute_reply":"2026-07-03T17:41:56.078402Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Генерация агрегированных признаков...\")\n\n# 1. Группировка по категориям: считаем среднюю цену и стандартное отклонение\ncat_price_stats = df.groupby('category_name')['price'].agg(['mean', 'std']).reset_index()\ncat_price_stats.columns = ['category_name', 'cat_price_mean', 'cat_price_std']\n\n# 2. Группировка по регионам и категориям вместе\nreg_cat_price_stats = df.groupby(['region', 'category_name'])['price'].agg(['mean']).reset_index()\nreg_cat_price_stats.columns = ['region', 'category_name', 'reg_cat_price_mean']\n\n# Объединяем эти данные с основным датафреймом\ndf = pd.merge(df, cat_price_stats, on='category_name', how='left')\ndf = pd.merge(df, reg_cat_price_stats, on=['region', 'category_name'], how='left')\n\n# 3. Считаем относительные фичи (насколько цена отличается от средней)\ndf['price_to_mean_cat'] = df['price'] / (df['cat_price_mean'] + 1)\ndf['price_to_mean_reg_cat'] = df['price'] / (df['reg_cat_price_mean'] + 1)\n\n# Заполняем возможные пустоты, которые могли появиться при делении или расчете std\ndf['cat_price_std'] = df['cat_price_std'].fillna(0)\n\nprint(\"Агрегации успешно добавлены!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T17:42:39.947804Z","iopub.execute_input":"2026-07-03T17:42:39.948577Z","iopub.status.idle":"2026-07-03T17:42:42.469873Z","shell.execute_reply.started":"2026-07-03T17:42:39.948546Z","shell.execute_reply":"2026-07-03T17:42:42.469261Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Добавляем новые фичи к общему списку\nagg_features = ['cat_price_mean', 'cat_price_std', 'reg_cat_price_mean', 'price_to_mean_cat', 'price_to_mean_reg_cat']\nfinal_features = all_features + agg_features\n\n# Пересобираем матрицы\nX_train_final = df.iloc[:train_len][final_features]\ny_train_final = target\n\nX_tr_f, X_val_f, y_tr_f, y_val_f = train_test_split(X_train_final, y_train_final, test_size=0.2, random_state=42)\n\nlgb_train_f = lgb.Dataset(X_tr_f, y_tr_f)\nlgb_val_f = lgb.Dataset(X_val_f, y_val_f, reference=lgb_train_f)\n\nprint(\"Обучение модели с агрегациями...\")\nmodel_f = lgb.train(\n    params,\n    lgb_train_f,\n    num_boost_round=1500, # Увеличили количество раундов\n    valid_sets=[lgb_train_f, lgb_val_f],\n    callbacks=[lgb.early_stopping(stopping_rounds=50), lgb.log_evaluation(100)]\n)\n\nval_preds_f = model_f.predict(X_val_f)\nrmse_f = np.sqrt(mean_squared_error(y_val_f, val_preds_f))\nprint(f\"Итоговый локальный RMSE после агрегаций: {rmse_f:.5f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T17:42:51.732643Z","iopub.execute_input":"2026-07-03T17:42:51.733349Z","iopub.status.idle":"2026-07-03T17:44:10.666858Z","shell.execute_reply.started":"2026-07-03T17:42:51.733323Z","shell.execute_reply":"2026-07-03T17:44:10.666304Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torchvision.models as models\nimport torchvision.transforms as transforms\nfrom PIL import Image\nimport zipfile\nimport io\nfrom tqdm import tqdm\nfrom sklearn.decomposition import IncrementalPCA\n\nprint(\"Проверяем доступность GPU...\")\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Используем устройство: {device}\")\n\n\nresnet = models.resnet18(pretrained=True)\nmodel_features = torch.nn.Sequential(*(list(resnet.children())[:-1]))\nmodel_features = model_features.to(device)\nmodel_features.eval()\n\n\npreprocess = transforms.Compose([\n    transforms.Resize(256),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n])\n\nZIP_PATH = \"/kaggle/input/competitions/avito-demand-prediction/train_jpg.zip\"\n\nprint(\"Начинаем тестовое извлечение эмбеддингов...\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T18:53:05.831717Z","iopub.execute_input":"2026-07-03T18:53:05.832438Z","iopub.status.idle":"2026-07-03T18:53:15.00908Z","shell.execute_reply.started":"2026-07-03T18:53:05.832404Z","shell.execute_reply":"2026-07-03T18:53:15.008341Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom sklearn.decomposition import IncrementalPCA\n\n# 1. Создаем базовый признак: есть ли вообще картинка\ndf['has_image'] = df['image'].notnull().astype(int)\n\n# 2. Отбираем объявления, у которых картинки есть, чтобы обработать их батчами\n# Для прототипа ограничимся первыми 30 000 картинок, чтобы код отработал за пару минут\nsub_df = df[df['has_image'] == 1].head(30000).copy()\nimage_ids = sub_df['image'].values\n\n# Настраиваем IPCA для сжатия 512 признаков от ResNet до 3 главных компонент\nipca = IncrementalPCA(n_components=3)\nfeatures_list = []\nvalid_indices = []\n\nprint(\"Открываем архив и извлекаем эмбеддинги...\")\nbatch_size = 64\ncurrent_batch = []\n\nwith zipfile.ZipFile(ZIP_PATH, 'r') as archive:\n    for idx, img_id in enumerate(tqdm(image_ids)):\n        img_name = f\"data/competition_files/train_jpg/{img_id}.jpg\"\n        \n        try:\n            # Читаем файл из zip без распаковки на диск\n            img_data = archive.read(img_name)\n            img = Image.open(io.BytesIO(img_data)).convert('RGB')\n            tensor = preprocess(img)\n            current_batch.append(tensor)\n            valid_indices.append(sub_df.index[idx]) # запоминаем глобальный индекс строки в df\n        except KeyError:\n            # Если конкретной картинки почему-то нет в архиве, пропускаем\n            continue\n            \n        if len(current_batch) == batch_size or (idx == len(image_ids) - 1 and len(current_batch) > 0):\n            # Собираем батч в один тензор и отправляем на GPU\n            batch_tensor = torch.stack(current_batch).to(device)\n            \n            with torch.no_grad():\n                # Прогоняем через ResNet и убираем лишние размерности (squeeze)\n                batch_features = model_features(batch_tensor).squeeze().cpu().numpy()\n                \n            if len(current_batch) == 1:\n                batch_features = batch_features.reshape(1, -1)\n                \n            features_list.append(batch_features)\n            current_batch = []\n\n# Объединяем все извлеченные фичи\nif len(features_list) > 0:\n    all_img_features = np.vstack(features_list)\n    print(f\"Извлечено эмбеддингов: {all_img_features.shape}\")\n    \n    # Сжимаем 512 признаков до 3 компонент\n    compressed_features = ipca.fit_transform(all_img_features)\n    \n    # Инициализируем новые колонки в основном датафрейме нулями\n    for i in range(3):\n        df[f'img_pca_{i}'] = 0.0\n        \n    # Записываем сжатые фичи картинок строго в те строки, где они были извлечены\n    df.loc[valid_indices, ['img_pca_0', 'img_pca_1', 'img_pca_2']] = compressed_features\n    print(\"Эмбеддинги успешно сжаты и добавлены в датафрейм!\")\nelse:\n    print(\"Ошибка: не удалось прочитать картинки. Проверь пути внутри архива.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T18:57:04.730722Z","iopub.execute_input":"2026-07-03T18:57:04.732118Z","iopub.status.idle":"2026-07-03T19:04:39.600624Z","shell.execute_reply.started":"2026-07-03T18:57:04.732084Z","shell.execute_reply":"2026-07-03T19:04:39.599862Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Добавляем новые признаки к финальному списку\nimage_features = ['has_image', 'img_pca_0', 'img_pca_1', 'img_pca_2']\nfinal_features_with_img = final_features + image_features\n\n# Пересобираем матрицы для обучения\nX_train_img = df.iloc[:train_len][final_features_with_img]\ny_train_img = target\n\nX_tr_i, X_val_i, y_tr_i, y_val_i = train_test_split(X_train_img, y_train_img, test_size=0.2, random_state=42)\n\nlgb_train_i = lgb.Dataset(X_tr_i, y_tr_i)\nlgb_val_i = lgb.Dataset(X_val_i, y_val_i, reference=lgb_train_i)\n\nprint(\"Обучение модели с добавлением эмбеддингов изображений...\")\nmodel_i = lgb.train(\n    params,\n    lgb_train_i,\n    num_boost_round=1500,\n    valid_sets=[lgb_train_i, lgb_val_i],\n    callbacks=[lgb.early_stopping(stopping_rounds=50), lgb.log_evaluation(100)]\n)\n\nval_preds_i = model_i.predict(X_val_i)\nrmse_i = np.sqrt(mean_squared_error(y_val_i, val_preds_i))\nprint(f\"RMSE после интеграции картинок: {rmse_i:.5f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T19:04:59.496994Z","iopub.execute_input":"2026-07-03T19:04:59.497278Z","iopub.status.idle":"2026-07-03T19:06:43.163439Z","shell.execute_reply.started":"2026-07-03T19:04:59.497257Z","shell.execute_reply":"2026-07-03T19:06:43.162652Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import string\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.decomposition import TruncatedSVD\n\nprint(\"Запуск продвинутого текстового анализа...\")\n\n# 1. Вытаскиваем мета-фичи из сырого текста\nprint(\"Генерируем мета-признаки...\")\n\n# Количество знаков препинания и восклицаний\ndf['title_excl_count'] = df['title'].apply(lambda x: x.count('!'))\ndf['desc_excl_count'] = df['description'].apply(lambda x: x.count('!'))\ndf['desc_punct_count'] = df['description'].apply(lambda x: sum(1 for char in x if char in string.punctuation))\n\n# Количество заглавных букв (КАПС) и цифр\ndf['title_upper_ratio'] = df['title'].apply(lambda x: sum(1 for c in x if c.isupper()) / (len(x) + 1))\ndf['desc_digit_count'] = df['description'].apply(lambda x: sum(1 for c in x if c.isdigit()))\n\n# 2. Настраиваем мощный TF-IDF + SVD для Title (15 компонент)\nprint(\"Выделяем n-граммы и SVD для Title...\")\n# sublinear_tf сглаживает аномально длинные тексты\ntfidf_title_adv = TfidfVectorizer(max_features=15000, ngram_range=(1, 2), sublinear_tf=True)\nsvd_title_adv = TruncatedSVD(n_components=15, random_state=42)\n\ntitle_tfidf_adv = tfidf_title_adv.fit_transform(df['title'])\ntitle_svd_adv = svd_title_adv.fit_transform(title_tfidf_adv)\n\n# 3. Настраиваем мощный TF-IDF + SVD для Description (25 компонент)\nprint(\"Выделяем n-граммы и SVD для Description...\")\ntfidf_desc_adv = TfidfVectorizer(max_features=35000, ngram_range=(1, 2), sublinear_tf=True)\nsvd_desc_adv = TruncatedSVD(n_components=25, random_state=42)\n\ndesc_tfidf_adv = tfidf_desc_adv.fit_transform(df['description'])\ndesc_svd_adv = svd_desc_adv.fit_transform(desc_tfidf_adv)\n\n# Записываем новые компоненты в DataFrame\nfor i in range(15):\n    df[f'title_svd_adv_{i}'] = title_svd_adv[:, i]\n\nfor i in range(25):\n    df[f'desc_svd_adv_{i}'] = desc_svd_adv[:, i]\n\nprint(\"Продвинутые текстовые фичи готовы!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T19:06:53.223785Z","iopub.execute_input":"2026-07-03T19:06:53.2247Z","iopub.status.idle":"2026-07-03T19:11:30.186513Z","shell.execute_reply.started":"2026-07-03T19:06:53.224656Z","shell.execute_reply":"2026-07-03T19:11:30.185659Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Собираем полный список новых текстовых фич\nadv_text_features = [\n    'title_excl_count', 'desc_excl_count', 'desc_punct_count', \n    'title_upper_ratio', 'desc_digit_count'\n] + [f'title_svd_adv_{i}' for i in range(15)] + [f'desc_svd_adv_{i}' for i in range(25)]\n\n# Наш итоговый мега-список признаков (базовые + агрегации + старый текст заменяем новым)\nmega_features = features + agg_features + adv_text_features\n\n# Добавляем фичи картинок, только если они созданы в df\nimg_cols = ['has_image', 'img_pca_0', 'img_pca_1', 'img_pca_2']\nif all(col in df.columns for col in img_cols):\n    mega_features += img_cols\n    print(\"Фичи картинок успешно подключены к общему пулу!\")\n\n# Нарезаем выборки\nX_train_mega = df.iloc[:train_len][mega_features]\ny_train_mega = target\n\nX_tr_m, X_val_m, y_tr_m, y_val_m = train_test_split(X_train_mega, y_train_mega, test_size=0.2, random_state=42)\n\nlgb_train_m = lgb.Dataset(X_tr_m, y_tr_m)\nlgb_val_m = lgb.Dataset(X_val_m, y_val_m, reference=lgb_train_m)\n\nprint(f\"Итоговое количество признаков для модели: {len(mega_features)}\")\nprint(\"Запуск финального обучения...\")\n\nmodel_mega = lgb.train(\n    params,\n    lgb_train_m,\n    num_boost_round=1500,\n    valid_sets=[lgb_train_m, lgb_val_m],\n    callbacks=[lgb.early_stopping(stopping_rounds=50), lgb.log_evaluation(100)]\n)\n\nval_preds_m = model_mega.predict(X_val_m)\nrmse_m = np.sqrt(mean_squared_error(y_val_m, val_preds_m))\nprint(f\"Финальный RMSE после тюнинга текста: {rmse_m:.5f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T19:14:14.399811Z","iopub.execute_input":"2026-07-03T19:14:14.400517Z","iopub.status.idle":"2026-07-03T19:16:56.161311Z","shell.execute_reply.started":"2026-07-03T19:14:14.400486Z","shell.execute_reply":"2026-07-03T19:16:56.160381Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import KFold\n\nprint(\"Запуск продвинутого кодирования таргета (Target Encoding)...\")\n\n# Создаем колонки под новые признаки и заполняем их глобальным средним таргетом\nglobal_mean = target.mean()\nte_cols = ['category_name', 'region', 'city', 'user_type']\n\nfor col in te_cols:\n    df[f'{col}_te'] = global_mean\n\n# Выделяем отдельно трейн-часть датафрейма для KFold расчетa\ntrain_df = df.iloc[:train_len].copy()\ntrain_df['target'] = target.values\n\n# Настраиваем 5-фолдовую схему для расчета средних без заглядывания в будущее\nkf = KFold(n_splits=5, shuffle=True, random_state=42)\n\nfor train_idx, val_idx in kf.split(train_df):\n    X_tr_fold = train_df.iloc[train_idx]\n    X_val_fold = train_df.iloc[val_idx]\n    \n    for col in te_cols:\n        # Считаем среднее на обучающих фолдах\n        mean_val = X_tr_fold.groupby(col)['target'].mean()\n        # Маппим на валидационный фолд\n        train_df.iloc[val_idx, train_df.columns.get_loc(f'{col}_te')] = X_val_fold[col].map(mean_val)\n\n# Для тестовой (и валидационной общей) выборки считаем среднее по всему трейну\nfor col in te_cols:\n    full_mean = train_df.groupby(col)['target'].mean()\n    df[f'{col}_te'] = df[col].map(full_mean).fillna(global_mean)\n\n# Возвращаем OOF-расчитанные значения обратно в общий df для трейн-части\ndf.iloc[:train_len, [df.columns.get_loc(f'{col}_te') for col in te_cols]] = train_df[[f'{col}_te' for col in te_cols]].values\n\nprint(\"Генерируем признаки активности пользователей...\")\n# Сколько всего объявлений выставил этот пользователь за всё время (популярность юзера)\nuser_counts = df.groupby('user_id').size().reset_index(name='user_active_count')\ndf = pd.merge(df, user_counts, on='user_id', how='left')\n\n# Отношение текущего номера объявления к общему числу объявлений юзера\ndf['user_item_ratio'] = df['item_seq_number'] / (df['user_active_count'] + 1)\n\nprint(\"Продвинутые фичи успешно сгенерированы!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T19:18:07.62961Z","iopub.execute_input":"2026-07-03T19:18:07.630428Z","iopub.status.idle":"2026-07-03T19:18:21.583255Z","shell.execute_reply.started":"2026-07-03T19:18:07.630396Z","shell.execute_reply":"2026-07-03T19:18:21.582383Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Обновляем пулл фич\nte_features = [f'{col}_te' for col in te_cols] + ['user_active_count', 'user_item_ratio']\nmega_features_v2 = mega_features + te_features\n\n# Пересобираем матрицы\nX_train_v2 = df.iloc[:train_len][mega_features_v2]\ny_train_v2 = target\n\nX_tr_v2, X_val_v2, y_tr_v2, y_val_v2 = train_test_split(X_train_v2, y_train_v2, test_size=0.2, random_state=42)\n\nlgb_train_v2 = lgb.Dataset(X_tr_v2, y_tr_v2)\nlgb_val_v2 = lgb.Dataset(X_val_v2, y_val_v2, reference=lgb_train_v2)\n\n# Обновляем параметры для более глубокого обучения\nparams_v2 = {\n    'objective': 'regression',\n    'metric': 'rmse',\n    'learning_rate': 0.05, # Снизили в 2 раза\n    'num_leaves': 63,      # Увеличили сложность дерева с 31 до 63\n    'feature_fraction': 0.8, # Модель будет брать 80% случайных фич на дерево (защита от оверфита)\n    'seed': 42,\n    'verbose': -1\n}\n\nprint(f\"Новое количество признаков: {len(mega_features_v2)}\")\nprint(\"Запуск долгого аккуратного обучения на GPU...\")\n\nmodel_v2 = lgb.train(\n    params_v2,\n    lgb_train_v2,\n    num_boost_round=2500, # Увеличили раунды\n    valid_sets=[lgb_train_v2, lgb_val_v2],\n    callbacks=[lgb.early_stopping(stopping_rounds=50), lgb.log_evaluation(100)]\n)\n\nval_preds_v2 = model_v2.predict(X_val_v2)\nrmse_v2 = np.sqrt(mean_squared_error(y_val_v2, val_preds_v2))\nprint(f\"Новейший локальный RMSE: {rmse_v2:.5f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T19:19:04.049578Z","iopub.execute_input":"2026-07-03T19:19:04.050418Z","iopub.status.idle":"2026-07-03T19:23:54.430751Z","shell.execute_reply.started":"2026-07-03T19:19:04.050387Z","shell.execute_reply":"2026-07-03T19:23:54.430004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Запуск экстраординарного инжиниринга признаков...\")\n\n# --- 1. ТЕКСТОВО-ВИЗУАЛЬНОЕ СВЯЗЫВАНИЕ (Cross-Features) ---\n# Перемножаем главные компоненты текста и картинок, чтобы поймать несоответствие\n# Например, если текст позитивный (высокий SVD), а картинка темная/плохая (низкий PCA)\nfor t_idx in range(3):\n    for img_idx in range(3):\n        df[f'cross_title_img_{t_idx}_{img_idx}'] = df[f'title_svd_adv_{t_idx}'] * df[f'img_pca_{img_idx}']\n        df[f'cross_desc_img_{t_idx}_{img_idx}'] = df[f'desc_svd_adv_{t_idx}'] * df[f'img_pca_{img_idx}']\n\n# --- 2. ЭФФЕКТ ЦЕНОВОГО ДАВЛЕНИЯ (Pricing Pressure) ---\n# Насколько цена отклоняется от медианы в процентах — это мы делали.\n# А теперь найдем Z-score (сигма-отклонение) цены внутри конкретного города и категории.\n# Это подсветит аномально дешевые (мошенники/срочно) и дорогие (не купят) товары.\n\n# Считаем среднее и стандартное отклонение для каждой группы city + category_name\ngroup_stats = df.groupby(['city', 'category_name'])['price'].agg(['mean', 'std']).reset_index()\ngroup_stats.columns = ['city', 'category_name', 'group_price_mean', 'group_price_std']\n\ndf = pd.merge(df, group_stats, on=['city', 'category_name'], how='left')\ndf['group_price_std'] = df['group_price_std'].fillna(0)\n\n# Считаем Z-score: (цена - среднее) / std\ndf['price_z_score'] = (df['price'] - df['group_price_mean']) / (df['group_price_std'] + 1e-5)\n\n# --- 3. СЧЁТЧИКИ УНИКАЛЬНОСТИ ОБЪЯВЛЕНИЯ (Uniqueness) ---\n# Перекупщики часто копируют описания и цены. Посчитаем, сколько раз точная копия \n# этой цены встречается внутри этой же категории в тот же день.\ndf['date_str'] = df['activation_date'].dt.date.astype(str)\nuniqueness_stats = df.groupby(['date_str', 'category_name', 'price']).size().reset_index(name='price_duplication_vibe')\n\ndf = pd.merge(df, uniqueness_stats, on=['date_str', 'category_name', 'price'], how='left')\ndf.drop(['date_str'], axis=1, inplace=True)\n\nprint(\"Экстраординарные признаки успешно сгенерированы!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T19:24:09.847308Z","iopub.execute_input":"2026-07-03T19:24:09.847969Z","iopub.status.idle":"2026-07-03T19:24:22.971216Z","shell.execute_reply.started":"2026-07-03T19:24:09.847886Z","shell.execute_reply":"2026-07-03T19:24:22.97046Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Собираем новые кросс-фичи и ценовые аномалии\nextra_features = [f'cross_title_img_{t}_{i}' for t in range(3) for i in range(3)] + \\\n                 [f'cross_desc_img_{d}_{i}' for d in range(3) for i in range(3)] + \\\n                 ['price_z_score', 'price_duplication_vibe']\n\nultimate_features = mega_features_v2 + extra_features\n\n# Пересобираем матрицы\nX_train_ult = df.iloc[:train_len][ultimate_features]\ny_train_ult = target\n\nX_tr_u, X_val_u, y_tr_u, y_val_u = train_test_split(X_train_ult, y_train_ult, test_size=0.2, random_state=42)\n\nlgb_train_u = lgb.Dataset(X_tr_u, y_tr_u)\nlgb_val_u = lgb.Dataset(X_val_u, y_val_u, reference=lgb_train_u)\n\n# Экстремальные параметры для финального штурма\nparams_ultimate = {\n    'objective': 'regression',\n    'metric': 'rmse',\n    'learning_rate': 0.03,  # Еще плавнее шаг для точечной настройки\n    'num_leaves': 127,      # Мощные, разветвленные деревья для 80+ признаков\n    'feature_fraction': 0.7, # Берем 70% случайных фич (чтобы кросс-фичи не доминировали)\n    'bagging_fraction': 0.8, # Беггинг датасета для стабильности\n    'bagging_freq': 1,\n    'seed': 42,\n    'verbose': -1\n}\n\nprint(f\"Итого признаков на борту: {len(ultimate_features)}\")\nprint(\"Запуск финального штурма на GPU...\")\n\nmodel_ultimate = lgb.train(\n    params_ultimate,\n    lgb_train_u,\n    num_boost_round=3000,\n    valid_sets=[lgb_train_u, lgb_val_u],\n    callbacks=[lgb.early_stopping(stopping_rounds=70), lgb.log_evaluation(100)]\n)\n\nval_preds_u = model_ultimate.predict(X_val_u)\nrmse_u = np.sqrt(mean_squared_error(y_val_u, val_preds_u))\nprint(f\"Экстраординарный итоговый RMSE: {rmse_u:.5f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T19:24:32.732509Z","iopub.execute_input":"2026-07-03T19:24:32.732993Z","iopub.status.idle":"2026-07-03T19:31:57.601234Z","shell.execute_reply.started":"2026-07-03T19:24:32.732951Z","shell.execute_reply":"2026-07-03T19:31:57.600222Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Подготовка финального сабмита...\")\n\n# 1. Выделяем тестовую часть из нашего общего обработанного DataFrame\nX_test_final = df.iloc[train_len:][ultimate_features]\n\n# 2. Делаем предсказания нашей лучшей моделью\nprint(\"Генерация предсказаний для тестовой выборки...\")\ntest_preds = model_ultimate.predict(X_test_final)\n\n# 3. Жесткий постпроцессинг (Clip)\n# Так как модель регрессионная, она чисто математически может выдать значения -0.02 или 1.05.\n# Ограничиваем их строгими рамками [0, 1], чтобы не получить штраф на Kaggle\ntest_preds = np.clip(test_preds, 0.0, 1.0)\n\n# 4. Загружаем шаблон сабмита и подменяем ответы\nsub_path = os.path.join(PATH, 'sample_submission.csv')\nsubmission = pd.read_csv(sub_path)\nsubmission['deal_probability'] = test_preds\n\nsubmission.to_csv('submission.csv', index=False)\n\nprint(\"Файл submission.csv успешно создан и сохранен!\")\nprint(submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-03T19:33:28.586647Z","iopub.execute_input":"2026-07-03T19:33:28.587096Z","iopub.status.idle":"2026-07-03T19:34:58.925558Z","shell.execute_reply.started":"2026-07-03T19:33:28.587069Z","shell.execute_reply":"2026-07-03T19:34:58.924767Z"}},"outputs":[],"execution_count":null}]}