{"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":[{"sourceId":105399,"databundleVersionId":12733338,"sourceType":"competition"}],"dockerImageVersionId":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Импорт библиотек","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport polars as pl\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport lightgbm as lgb\nfrom sklearn.metrics import accuracy_score","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:25:13.324820Z","iopub.execute_input":"2026-01-19T15:25:13.325150Z","iopub.status.idle":"2026-01-19T15:25:13.330587Z","shell.execute_reply.started":"2026-01-19T15:25:13.325126Z","shell.execute_reply":"2026-01-19T15:25:13.329379Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Загрузка данных","metadata":{}},{"cell_type":"code","source":"TRAIN_PATH = '/kaggle/input/aeroclub-recsys-2025/train.parquet'\nTEST_PATH = '/kaggle/input/aeroclub-recsys-2025/test.parquet'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:25:19.847664Z","iopub.execute_input":"2026-01-19T15:25:19.848068Z","iopub.status.idle":"2026-01-19T15:25:19.852596Z","shell.execute_reply.started":"2026-01-19T15:25:19.848037Z","shell.execute_reply":"2026-01-19T15:25:19.851600Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_lazy = pl.scan_parquet(TRAIN_PATH)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:26:57.683683Z","iopub.execute_input":"2026-01-19T15:26:57.684070Z","iopub.status.idle":"2026-01-19T15:26:57.699646Z","shell.execute_reply.started":"2026-01-19T15:26:57.684040Z","shell.execute_reply":"2026-01-19T15:26:57.698719Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Загрузка данных в Polars и анализ признаков","metadata":{}},{"cell_type":"code","source":"schema = train_lazy.collect_schema()\nprint(\"Всего колонок:\", len(schema.names()))\nprint(\"\\nНазвания колонок:\")\nprint(schema.names())\n\nprint(\"\\nТипы ключевых колонок:\")\nkey_cols = ['ranker_id', 'selected', 'totalPrice', 'profileId', 'companyID', 'price']\nfor col in key_cols:\n    if col in schema.names():\n        print(f\"  {col}: {schema[col]}\")\n\nsample = train_lazy.head(5).collect()\nprint(sample)\n\nprint(\"\\nУникальные\")\nstats = (train_lazy\n    .select([\n        pl.col('ranker_id').n_unique().alias('ranker_id_unique'),\n        pl.col('selected').n_unique().alias('selected_unique'),\n        pl.col('profileId').n_unique().alias('profileId_unique'),\n        pl.col('companyID').n_unique().alias('companyID_unique'),\n    ])\n    .collect()\n)\nfor col in stats.columns:\n    print(f\"{col}: {stats[col][0]}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:27:42.152350Z","iopub.execute_input":"2026-01-19T15:27:42.152714Z","iopub.status.idle":"2026-01-19T15:27:43.875052Z","shell.execute_reply.started":"2026-01-19T15:27:42.152687Z","shell.execute_reply":"2026-01-19T15:27:43.874035Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"групповые признаки","metadata":{}},{"cell_type":"code","source":"group_stats = (train_lazy\n    .group_by('ranker_id')\n    .agg([\n        pl.len().alias('group_size'),\n        pl.col('totalPrice').mean().alias('price_mean'),\n        pl.col('totalPrice').median().alias('price_median'), \n        pl.col('totalPrice').min().alias('price_min'),\n        pl.col('totalPrice').max().alias('price_max'),\n        pl.col('totalPrice').std().alias('price_std'),\n        pl.col('profileId').n_unique().alias('unique_users_per_group'),\n        pl.n_unique('searchRoute').alias('unique_routes_per_group'),\n    ])\n    .with_columns([\n        (pl.col('price_median') / pl.col('price_min')).alias('price_median_vs_min'),\n        (pl.col('price_max') / pl.col('price_median')).alias('price_max_vs_median'),\n        (pl.col('price_std') / pl.col('price_mean')).alias('price_cv'),\n    ])\n)\n\ngroup_stats_df = group_stats.collect()\ngroup_stats_df.write_parquet('/kaggle/working/group_stats.parquet')\n\nprint(\"Групповые статистики (первые 10):\")\nprint(group_stats_df.head(10))\nprint()\nprint(\"Статистика по группам:\")\ngroup_summary = group_stats_df.describe()\nprint(group_summary)\n\nprint(\"сохранил в: /kaggle/working/group_stats.parquet\")\nprint(\"Размер:\", group_stats_df.shape)\nprint(\"Групп:\", len(group_stats_df))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:29:59.715883Z","iopub.execute_input":"2026-01-19T15:29:59.716666Z","iopub.status.idle":"2026-01-19T15:30:02.985208Z","shell.execute_reply.started":"2026-01-19T15:29:59.716635Z","shell.execute_reply":"2026-01-19T15:30:02.984319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"group_stats_lazy = pl.scan_parquet('/kaggle/working/group_stats.parquet')\n\n# Создаем ranking признаки\nranking_features = (train_lazy\n    # total price rank внутри ranker_id\n    .with_columns([\n        pl.col('totalPrice').rank(method='dense').over('ranker_id').alias('price_rank_dense'),\n        pl.col('totalPrice').rank(method='min').over('ranker_id').alias('price_rank_min'),\n        pl.col('Id').rank(method='dense').over('ranker_id').alias('position_in_group'),\n    ])\n    # Присоединяем групповые статистики\n    .join(group_stats_lazy, on='ranker_id', how='left')\n    .with_columns([\n        # Нормализованная цена относительно группы\n        (pl.col('totalPrice') / pl.col('price_mean')).alias('price_norm_mean'),\n        (pl.col('totalPrice') / pl.col('price_median')).alias('price_norm_median'),\n        (pl.col('price_rank_dense') / pl.col('group_size')).alias('price_rank_ratio'),\n        \n        # Кол-во пересадок\n        (pl.col('legs0_segments1_aircraft_code').is_not_null()\n         + pl.col('legs0_segments2_aircraft_code').is_not_null()\n         + pl.col('legs0_segments3_aircraft_code').is_not_null()).alias('num_stops_leg0'),\n    ])\n    # выбираем ключевые признаки\n    .select([\n        'ranker_id', 'Id', 'selected', \n        'price_rank_dense', 'price_rank_min', 'price_rank_ratio',\n        'price_norm_mean', 'price_norm_median', 'group_size',\n        'position_in_group', 'num_stops_leg0',\n        'profileId', 'companyID', 'totalPrice'\n    ])\n)\n\nprint(\"Ranking признаки (первые 10 строк):\")\nprint(ranking_features.head(10).collect())\n\nranking_features.sink_parquet('/kaggle/working/ranking_features.parquet')\nprint(\"сохранил в: /kaggle/working/ranking_features.parquet\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:31:24.742859Z","iopub.execute_input":"2026-01-19T15:31:24.743728Z","iopub.status.idle":"2026-01-19T15:31:51.082994Z","shell.execute_reply.started":"2026-01-19T15:31:24.743676Z","shell.execute_reply":"2026-01-19T15:31:51.081995Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ranking_features_lazy = pl.scan_parquet('/kaggle/working/ranking_features.parquet')\n\n# USER статистики (история предпочтений пользователя)\nuser_stats = (ranking_features_lazy\n    .group_by('profileId')\n    .agg([\n        pl.col('selected').mean().alias('user_selected_rate'),\n        pl.col('totalPrice').median().alias('user_price_median'),\n        pl.col('price_rank_dense').median().alias('user_price_rank_pref'),\n        pl.col('num_stops_leg0').median().alias('user_stops_pref'),\n        pl.col('group_size').median().alias('user_group_size_pref'),\n    ])\n)\n\n# COMPANY статистики\ncompany_stats = (ranking_features_lazy\n    .group_by('companyID')\n    .agg([\n        pl.col('selected').mean().alias('company_selected_rate'),\n        pl.col('totalPrice').mean().alias('company_avg_price'),\n        pl.col('price_rank_dense').mean().alias('company_price_rank_avg'),\n    ])\n)\n\n# Финальные признаки = ranking + user + company\nfinal_features = (ranking_features_lazy\n    .join(user_stats, on='profileId', how='left')\n    .join(company_stats, on='companyID', how='left')\n    .with_columns([\n        # Cross-признаки\n        (pl.col('price_rank_dense') * pl.col('user_price_rank_pref')).alias('price_user_interaction'),\n        (pl.col('price_norm_mean') - pl.col('user_price_median') / pl.col('company_avg_price')).alias('price_user_company_dev'),\n    ])\n    .select([\n        'ranker_id', 'Id', 'selected',\n        'price_rank_dense', 'price_rank_min', 'price_rank_ratio',\n        'price_norm_mean', 'price_norm_median', \n        'group_size', 'position_in_group', 'num_stops_leg0',\n        'user_selected_rate', 'user_price_rank_pref', 'user_stops_pref',\n        'company_selected_rate', 'company_avg_price',\n        'price_user_interaction', 'price_user_company_dev'\n    ])\n)\n\nprint(\"Финальные признаки (первые 10 строк):\")\nprint(final_features.head(10).collect())\nfinal_features.sink_parquet('/kaggle/working/final_features.parquet')\nprint(\"сохранил в: /kaggle/working/final_features.parquet\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:34:22.445757Z","iopub.execute_input":"2026-01-19T15:34:22.446163Z","iopub.status.idle":"2026-01-19T15:34:31.509947Z","shell.execute_reply.started":"2026-01-19T15:34:22.446136Z","shell.execute_reply":"2026-01-19T15:34:31.509039Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Выбор признаков","metadata":{}},{"cell_type":"code","source":"final_features_lazy = pl.scan_parquet('/kaggle/working/final_features.parquet')\n\n# Получаем все группы\nall_groups_df = (\n    final_features_lazy\n    .group_by('ranker_id')\n    .agg(pl.len().alias('group_size'))\n    .collect()\n)\n\ngroup_split_df = all_groups_df.sample(fraction=0.15, seed=42)\nval_ranker_ids = group_split_df['ranker_id'].to_list()\n\n# Разделяем данные\ntrain_features = final_features_lazy.filter(~pl.col('ranker_id').is_in(val_ranker_ids))\nval_features   = final_features_lazy.filter( pl.col('ranker_id').is_in(val_ranker_ids))\n\nfeature_cols = [\n    'price_rank_dense', 'price_rank_min', 'price_rank_ratio',\n    'price_norm_mean', 'price_norm_median', \n    'group_size', 'position_in_group', 'num_stops_leg0',\n    'user_selected_rate', 'user_price_rank_pref', 'user_stops_pref',\n    'company_selected_rate', 'company_avg_price',\n    'price_user_interaction', 'price_user_company_dev'\n]\n# Финальные признаки\ntrain_final = train_features.select(['ranker_id', 'selected'] + feature_cols)\nval_final   = val_features.select(['ranker_id', 'selected'] + feature_cols)\n\ntrain_final.sink_parquet('/kaggle/working/train_final.parquet')\nval_final.sink_parquet('/kaggle/working/val_final.parquet')\n\ntrain_stats = train_final.group_by('ranker_id').len().collect()\nval_stats   = val_final.group_by('ranker_id').len().collect()\n\nn_train_rows = train_final.select(pl.len()).collect().item()\nn_val_rows   = val_final.select(pl.len()).collect().item()\n\nprint(f\"Train: {len(train_stats)} групп, {n_train_rows:,} строк\")\nprint(f\"Val:   {len(val_stats)} групп, {n_val_rows:,} строк\")\nprint(f\"Признаков: {len(feature_cols)}\")\nprint()\nprint(\"Финальные признаки:\", feature_cols)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:35:35.771975Z","iopub.execute_input":"2026-01-19T15:35:35.772347Z","iopub.status.idle":"2026-01-19T15:35:45.780545Z","shell.execute_reply.started":"2026-01-19T15:35:35.772321Z","shell.execute_reply":"2026-01-19T15:35:45.779337Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Обучение модели","metadata":{}},{"cell_type":"code","source":"# Загружаем данные\ntrain_df = pl.read_parquet('/kaggle/working/train_final.parquet').to_pandas()\nval_df = pl.read_parquet('/kaggle/working/val_final.parquet').to_pandas()\n\nfeature_cols = [\n    'price_rank_dense', 'price_rank_min', 'price_rank_ratio',\n    'price_norm_mean', 'price_norm_median', \n    'group_size', 'position_in_group', 'num_stops_leg0',\n    'user_selected_rate', 'user_price_rank_pref', 'user_stops_pref',\n    'company_selected_rate', 'company_avg_price',\n    'price_user_interaction', 'price_user_company_dev'\n]\n\nX_train = train_df[feature_cols]\ny_train = train_df['selected']\nX_val = val_df[feature_cols]\ny_val = val_df['selected']\n\n# LightGBM RANKER\nlgb_params = {\n    'objective': 'lambdarank',\n    'metric': 'ndcg',\n    'boosting_type': 'gbdt',\n    'num_leaves': 31,\n    'learning_rate': 0.05,\n    'feature_fraction': 0.9,\n    'bagging_fraction': 0.8,\n    'bagging_freq': 5,\n    'verbose': -1,\n    'random_state': 42,\n}\n\n# Группы для ranking\ntrain_groups = train_df.groupby('ranker_id').size().values\nval_groups = val_df.groupby('ranker_id').size().values\n\nmodel = lgb.LGBMRanker(**lgb_params)\nmodel.fit(\n    X_train, y_train,\n    group=train_groups,\n    eval_set=[(X_val, y_val)],\n    eval_group=[val_groups],\n    callbacks=[lgb.early_stopping(100), lgb.log_evaluation(100)]\n)\n\nprint(\"модель закончила обучение\")\nprint()\nprint(\"Feature importance:\")\nfor name, importance in zip(feature_cols, model.feature_importances_):\n    print(f\"{name}: {importance:.1f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:37:59.610278Z","iopub.execute_input":"2026-01-19T15:37:59.610654Z","iopub.status.idle":"2026-01-19T15:42:03.564769Z","shell.execute_reply.started":"2026-01-19T15:37:59.610629Z","shell.execute_reply":"2026-01-19T15:42:03.563341Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Анализ ошибок","metadata":{}},{"cell_type":"code","source":"# Предсказания на валидации\nval_pred = model.predict(X_val)\nval_df['pred_score'] = val_pred\n\n# HitRate@3 (только группы >10)\nval_groups = []\nfor ranker_id, group in val_df.groupby('ranker_id'):\n    if len(group) > 10:\n        top3_idx = group.nlargest(3, 'pred_score').index\n        selected_in_top3 = group.loc[top3_idx, 'selected'].sum() > 0\n        val_groups.append({\n            'ranker_id': ranker_id,\n            'group_size': len(group),\n            'hit': int(selected_in_top3),\n            'selected_pred_rank': group[group['selected']==1]['pred_score'].rank(pct=True).iloc[0] if group['selected'].sum() > 0 else np.nan\n        })\n\nanalysis_df = pd.DataFrame(val_groups)\nhit_rate_3 = analysis_df['hit'].mean()\nprint(f\"HitRate@3 (группы >10): {hit_rate_3:.4f}\")\nprint(f\"Групп в анализе: {len(analysis_df):,}\")\nprint(f\"Ошибок: {(1-hit_rate_3)*100:.1f}%\")\n\n# Визуализация (графики)\nfig, axes = plt.subplots(1, 3, figsize=(18, 5))\n\n# График 1: HitRate по размерам групп\nbins = pd.cut(analysis_df['group_size'], bins=[0, 50, 100, 500, 1000, float('inf')], labels=['<50', '50-100', '100-500', '500-1K', '>1K'])\nhit_by_size = analysis_df.groupby(bins)['hit'].mean()\naxes[0].bar(hit_by_size.index, hit_by_size.values, color='skyblue', alpha=0.8)\naxes[0].set_title('HitRate@3 по размерам групп', fontweight='bold')\naxes[0].set_ylabel('HitRate@3')\naxes[0].tick_params(axis='x', rotation=45)\n\n# График 2: Где ошибается (позиция selected)\nmissed_groups = analysis_df[analysis_df['hit'] == 0]['selected_pred_rank'].dropna()\naxes[1].hist(np.clip(missed_groups, 0, 0.8), bins=20, color='red', alpha=0.7, edgecolor='black')\naxes[1].set_title('Позиция selected в ошибках (top-80%)', fontweight='bold')\naxes[1].set_xlabel('Предсказанный percentile')\naxes[1].set_ylabel('Ошибочные группы')\n\n# График 3: Feature importance (ТОП-10)\nimportance_df = pd.DataFrame({'feature': feature_cols, 'imp': model.feature_importances_}).nlargest(10, 'imp')\naxes[2].barh(importance_df['feature'], importance_df['imp'], color='green', alpha=0.8)\naxes[2].set_title('Top-10 Feature Importance', fontweight='bold')\naxes[2].set_xlabel('Importance')\n\nplt.tight_layout()\nplt.show()\n\nprint(f\"• HitRate@3 = {hit_rate_3:.3f} — baseline для улучшения\")\nprint(f\"• {len(analysis_df[analysis_df['group_size']>500])/len(analysis_df)*100:.1f}% групп большие\")\nprint(f\"• Модель недооценивает: {np.mean(missed_groups):.2f} percentile позиции selected\")\nprint(f\"• Топ-3 признака: {importance_df.head(3)['feature'].tolist()}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:45:08.305423Z","iopub.execute_input":"2026-01-19T15:45:08.305926Z","iopub.status.idle":"2026-01-19T15:45:42.370351Z","shell.execute_reply.started":"2026-01-19T15:45:08.305868Z","shell.execute_reply":"2026-01-19T15:45:42.369372Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Предсказания на Test","metadata":{}},{"cell_type":"code","source":"test_lazy = pl.scan_parquet(TEST_PATH)\n\n\n# Используем group_stats из train\ngroup_stats_df = pl.read_parquet('/kaggle/working/group_stats.parquet')\ngroup_stats_test = pl.scan_parquet('/kaggle/working/group_stats.parquet')\n\ntest_features = (test_lazy\n    .with_columns([\n        pl.col('totalPrice').rank(method='dense').over('ranker_id').alias('price_rank_dense'),\n        pl.col('totalPrice').rank(method='min').over('ranker_id').alias('price_rank_min'),\n        pl.col('Id').rank(method='dense').over('ranker_id').alias('position_in_group'),\n    ])\n    .join(group_stats_test, on='ranker_id', how='left')\n    .with_columns([\n        (pl.col('totalPrice') / pl.col('price_mean')).alias('price_norm_mean'),\n        (pl.col('totalPrice') / pl.col('price_median')).alias('price_norm_median'),\n        (pl.col('price_rank_dense') / pl.col('group_size')).alias('price_rank_ratio'),\n        (pl.col('legs0_segments1_aircraft_code').is_not_null()\n         + pl.col('legs0_segments2_aircraft_code').is_not_null()\n         + pl.col('legs0_segments3_aircraft_code').is_not_null()).alias('num_stops_leg0'),\n    ])\n    .select([\n        'ranker_id', 'Id',\n        'price_rank_dense', 'price_rank_min', 'price_rank_ratio',\n        'price_norm_mean', 'price_norm_median', \n        'group_size', 'position_in_group', 'num_stops_leg0',\n        'profileId', 'companyID', 'totalPrice'\n    ])\n)\n\n# Загружаем user/company stats из train\nranking_features_train = pl.scan_parquet('/kaggle/working/ranking_features.parquet')\nuser_stats = (ranking_features_train\n    .group_by('profileId')\n    .agg([\n        pl.col('selected').mean().alias('user_selected_rate'),\n        pl.col('totalPrice').median().alias('user_price_median'),\n        pl.col('price_rank_dense').median().alias('user_price_rank_pref'),\n        pl.col('num_stops_leg0').median().alias('user_stops_pref'),\n    ])\n)\n\ncompany_stats = (ranking_features_train\n    .group_by('companyID')\n    .agg([\n        pl.col('selected').mean().alias('company_selected_rate'),\n        pl.col('totalPrice').mean().alias('company_avg_price'),\n    ])\n)\n\n# Финальные признаки test\ntest_final = (test_features\n    .join(user_stats, on='profileId', how='left')\n    .join(company_stats, on='companyID', how='left')\n    .with_columns([\n        (pl.col('price_rank_dense') * pl.col('user_price_rank_pref').fill_null(1.0)).alias('price_user_interaction'),\n        (pl.col('price_norm_mean') - pl.col('user_price_median').fill_null(pl.col('totalPrice')) / pl.col('company_avg_price').fill_null(1.0)).alias('price_user_company_dev'),\n    ])\n    .select(feature_cols + ['ranker_id', 'Id'])\n)\n\ntest_df = test_final.collect().to_pandas()\nX_test = test_df[feature_cols]\n\ntest_pred = model.predict(X_test)\ntest_df['pred_score'] = test_pred\n\nprint(\"Test предсказания готовы\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T15:50:23.911393Z","iopub.execute_input":"2026-01-19T15:50:23.911872Z","iopub.status.idle":"2026-01-19T15:50:42.105700Z","shell.execute_reply.started":"2026-01-19T15:50:23.911840Z","shell.execute_reply":"2026-01-19T15:50:42.104868Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Submission","metadata":{}},{"cell_type":"code","source":"test_original = pl.read_parquet(TEST_PATH).select(['Id', 'ranker_id']).to_pandas()\ntest_original['original_order'] = range(len(test_original))\n\n# Создаем словарь для быстрого доступа к порядку\nid_to_order = dict(zip(test_original['Id'], test_original['original_order']))\n\nid_to_rank = {}\n\n# Создаем ранги для каждой группы\nfor ranker_id, group in test_df.groupby('ranker_id'):\n    # Сортируем по pred_score по убыванию (высший score = ранг 1)\n    group_sorted = group.sort_values('pred_score', ascending=False).reset_index(drop=True)\n    # Присваиваем ранги от 1 до N\n    for idx, row in group_sorted.iterrows():\n        id_to_rank[row['Id']] = int(idx + 1)  # ранг от 1 до N\n\n# Создаем submission в исходном порядке test.csv\nsubmission_final = test_original.copy()\nsubmission_final['selected'] = submission_final['Id'].map(id_to_rank)\n\n# Проверяем, что все Id получили ранг\nmissing_ranks = submission_final['selected'].isna().sum()\nif missing_ranks > 0:\n    print(f\"ОШИБКА: {missing_ranks} строк не получили ранг!\")\n    print(submission_final[submission_final['selected'].isna()].head())\n\nsubmission_final = submission_final[['Id', 'ranker_id', 'selected']]\n\ninvalid_groups = 0\nfor ranker_id, group in submission_final.groupby('ranker_id'):\n    ranks = sorted(group['selected'].values)\n    expected_ranks = list(range(1, len(group) + 1))\n    if ranks != expected_ranks:\n        invalid_groups += 1\n        if invalid_groups <= 5:\n            print(f\"Невалидные ранги для ranker_id {ranker_id}\")\n            print(f\"Размер группы: {len(group)}\")\n            print(f\"Ожидалось: {expected_ranks[:10]}...\" if len(expected_ranks) > 10 else f\"  Ожидалось: {expected_ranks}\")\n            print(f\"Получено: {ranks[:10]}...\" if len(ranks) > 10 else f\"  Получено: {ranks}\")\n\nif invalid_groups == 0:\n    print(\"Все ранги валидны для всех групп\")\nelse:\n    print(f\"Найдено {invalid_groups} групп с невалидными рангами\")\n\n# Проверка порядка строк\nif len(submission_final) == len(test_original):\n    if (submission_final['Id'].values == test_original['Id'].values).all():\n        print(\"Порядок строк соответствует test.csv\")\n    else:\n        print(\"Порядок строк НЕ соответствует test.csv\")\n        # Исправляем порядок\n        submission_final = submission_final.set_index('Id').loc[test_original['Id']].reset_index()\n        print(\"Порядок строк исправлен\")\nelse:\n    print(f\"Количество строк не совпадает: submission={len(submission_final)}, test={len(test_original)}\")\n\n# Показываем пример для одной полной группы\nexample_ranker_id = submission_final['ranker_id'].iloc[0]\nexample_group = submission_final[submission_final['ranker_id'] == example_ranker_id].sort_values('selected')\nprint(f\"\\nПример полной группы (ranker_id={example_ranker_id}, размер={len(example_group)}):\")\nprint(example_group[['Id', 'ranker_id', 'selected']].head(min(20, len(example_group))))\nif len(example_group) > 20:\n    print(f\"... (всего {len(example_group)} строк в группе)\")\n\nsubmission_final.to_csv('/kaggle/working/submission.csv', index=False)\nprint(\"\\nсохранил сабмит в: /kaggle/working/submission.csv\")\n\nprint(f\"\\nИтоговая статистика:\")\nprint(f\"  Test групп: {test_df['ranker_id'].nunique()}\")\nprint(f\"  Сабмит строк: {len(submission_final):,}\")\nprint(f\"  Формат колонок: {submission_final.columns.tolist()}\")\nprint(f\"\\nПример первых 10 строк submission:\")\nprint(submission_final.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T19:18:06.257809Z","iopub.execute_input":"2026-01-19T19:18:06.258376Z","iopub.status.idle":"2026-01-19T19:23:58.466318Z","shell.execute_reply.started":"2026-01-19T19:18:06.258340Z","shell.execute_reply":"2026-01-19T19:23:58.465180Z"}},"outputs":[],"execution_count":null}]}