{"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":10737,"databundleVersionId":290346}],"dockerImageVersionId":31286,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Skeleton\n## Идея реализация\n\nВ качестве skeleton для задачи **Quora Insincere Questions Classification** используется классический **bag-of-words / TF-IDF baseline** с линейной моделью.\n\nОсновная идея реализации:\n1. считать `train.csv` и `test.csv`;\n2. выполнить базовую текстовую предобработку;\n3. преобразовать вопрос в **TF-IDF-представление на уровне слов**;\n4. обучить **Logistic Regression** как базовый бинарный классификатор;\n5. получить вероятности положительного класса;\n6. подобрать или использовать фиксированный порог для перевода вероятностей в бинарное решение;\n7. оценить качество по **F1-score** на локальной валидации и сравнить его с leaderboard.\n\nПочему выбран именно такой подход:\n- задача является чисто **NLP classification** без изображений и табличных числовых признаков, поэтому TF-IDF baseline здесь естественен;\n- линейные модели с TF-IDF часто служат сильной отправной точкой в задачах токсичности и модерации текста;\n- такой baseline быстро обучается, легко интерпретируется и хорошо подходит для первого этапа проекта;\n- его удобно расширять в рамках гипотез, не перестраивая весь pipeline с нуля.\n\nВ качестве локальной проверки используется разбиение train на обучение и валидацию с сохранением доли классов. Это особенно важно из-за дисбаланса классов и выбора F1-score в качестве основной метрики.\n","metadata":{}},{"cell_type":"markdown","source":"## Реализация","metadata":{}},{"cell_type":"code","source":"import os\nimport gc\nimport time\nimport json\nimport random\nimport warnings\n\nimport numpy as np\nimport pandas as pd\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.linear_model import SGDClassifier\nfrom sklearn.metrics import f1_score\n\nwarnings.filterwarnings(\"ignore\")\n\n\nSEED = 42\nN_SPLITS = 5\n\nDATA_DIR = \"/kaggle/input/competitions/quora-insincere-questions-classification\"\n\nTRAIN_PATH = os.path.join(DATA_DIR, \"train.csv\")\nTEST_PATH = os.path.join(DATA_DIR, \"test.csv\")\nSAMPLE_SUB_PATH = os.path.join(DATA_DIR, \"sample_submission.csv\")\n\nID_COL = \"qid\"\nTEXT_COL = \"question_text\"\nTARGET_COL = \"target\"\n\nrandom.seed(SEED)\nnp.random.seed(SEED)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-03-12T14:46:59.400116Z","iopub.execute_input":"2026-03-12T14:46:59.400382Z","iopub.status.idle":"2026-03-12T14:46:59.407345Z","shell.execute_reply.started":"2026-03-12T14:46:59.400364Z","shell.execute_reply":"2026-03-12T14:46:59.406210Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load data\n\ntrain_df = pd.read_csv(TRAIN_PATH)\ntest_df = pd.read_csv(TEST_PATH)\nsample_sub = pd.read_csv(SAMPLE_SUB_PATH)\n\ntrain_df[TEXT_COL] = train_df[TEXT_COL].fillna(\"\")\ntest_df[TEXT_COL] = test_df[TEXT_COL].fillna(\"\")\n\nprint(\"train shape:\", train_df.shape)\nprint(\"test shape :\", test_df.shape)\nprint(\"sample sub :\", sample_sub.shape)\nprint()\nprint(\"train columns:\", train_df.columns.tolist())\nprint(\"test columns :\", test_df.columns.tolist())\n\npositive_rate = train_df[TARGET_COL].mean()\nprint(f\"\\nPositive rate (target=1): {positive_rate:.6f} ({positive_rate*100:.2f}%)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T14:47:02.081983Z","iopub.execute_input":"2026-03-12T14:47:02.082268Z","iopub.status.idle":"2026-03-12T14:47:05.598951Z","shell.execute_reply.started":"2026-03-12T14:47:02.082248Z","shell.execute_reply":"2026-03-12T14:47:05.598046Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create folds\n\nskf = StratifiedKFold(\n    n_splits=N_SPLITS,\n    shuffle=True,\n    random_state=SEED\n)\n\ntrain_df[\"fold\"] = -1\n\nfor fold, (_, val_idx) in enumerate(skf.split(train_df[TEXT_COL], train_df[TARGET_COL])):\n    train_df.loc[val_idx, \"fold\"] = fold\n\nprint(train_df[\"fold\"].value_counts().sort_index())\nprint()\nprint(train_df.groupby(\"fold\")[TARGET_COL].mean())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T14:47:06.408005Z","iopub.execute_input":"2026-03-12T14:47:06.408270Z","iopub.status.idle":"2026-03-12T14:47:06.608835Z","shell.execute_reply.started":"2026-03-12T14:47:06.408252Z","shell.execute_reply":"2026-03-12T14:47:06.608063Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Threshold search helper\n\ndef find_best_threshold(y_true, y_prob, thresholds=None):\n    if thresholds is None:\n        thresholds = np.arange(0.05, 0.951, 0.01)\n\n    scores = []\n    for thr in thresholds:\n        pred = (y_prob >= thr).astype(int)\n        score = f1_score(y_true, pred)\n        scores.append(score)\n\n    scores = np.array(scores)\n    best_idx = int(np.argmax(scores))\n\n    best_thr = float(thresholds[best_idx])\n    best_score = float(scores[best_idx])\n\n    threshold_df = pd.DataFrame({\n        \"threshold\": thresholds,\n        \"f1\": scores\n    })\n\n    return best_thr, best_score, threshold_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T14:47:12.280064Z","iopub.execute_input":"2026-03-12T14:47:12.280379Z","iopub.status.idle":"2026-03-12T14:47:12.287382Z","shell.execute_reply.started":"2026-03-12T14:47:12.280355Z","shell.execute_reply":"2026-03-12T14:47:12.286450Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Skeleton: TF-IDF(word) + LogisticRegression\n\noof_pred_skel = np.zeros(len(train_df), dtype=np.float32)\ntest_pred_skel = np.zeros(len(test_df), dtype=np.float32)\n\nfold_logs_skel = []\n\nglobal_start = time.time()\n\nfor fold in range(N_SPLITS):\n    fold_start = time.time()\n\n    trn = train_df[train_df[\"fold\"] != fold]\n    val = train_df[train_df[\"fold\"] == fold]\n\n    X_train_text = trn[TEXT_COL].values\n    y_train = trn[TARGET_COL].values\n\n    X_val_text = val[TEXT_COL].values\n    y_val = val[TARGET_COL].values\n\n    vectorizer = TfidfVectorizer(\n        analyzer=\"word\",\n        ngram_range=(1, 2),\n        min_df=3,\n        max_df=0.9,\n        max_features=150000,\n        sublinear_tf=True,\n        strip_accents=\"unicode\",\n        lowercase=True\n    )\n\n    X_train = vectorizer.fit_transform(X_train_text)\n    X_val = vectorizer.transform(X_val_text)\n    X_test = vectorizer.transform(test_df[TEXT_COL].values)\n\n    model = SGDClassifier(\n        loss=\"log_loss\",\n        penalty=\"l2\",\n        alpha=1e-5,\n        max_iter=20,\n        tol=1e-3,\n        random_state=SEED\n    )\n\n    model.fit(X_train, y_train)\n\n    val_pred = model.predict_proba(X_val)[:, 1]\n    test_fold_pred = model.predict_proba(X_test)[:, 1]\n\n    oof_pred_skel[val.index] = val_pred\n    test_pred_skel += test_fold_pred / N_SPLITS\n\n    fold_best_thr, fold_best_f1, _ = find_best_threshold(y_val, val_pred)\n\n    fold_time = time.time() - fold_start\n\n    fold_logs_skel.append({\n        \"fold\": fold,\n        \"train_size\": int(len(trn)),\n        \"val_size\": int(len(val)),\n        \"n_features\": int(X_train.shape[1]),\n        \"fold_best_threshold\": float(fold_best_thr),\n        \"fold_best_f1\": float(fold_best_f1),\n        \"fold_time_sec\": float(fold_time)\n    })\n\n    print(\n        f\"Fold {fold} | \"\n        f\"features={X_train.shape[1]:,} | \"\n        f\"best_thr={fold_best_thr:.3f} | \"\n        f\"best_f1={fold_best_f1:.6f} | \"\n        f\"time={fold_time:.1f}s\"\n    )\n\n    del trn, val\n    del X_train, X_val, X_test\n    del model, vectorizer, val_pred, test_fold_pred\n    gc.collect()\n\ntotal_time = time.time() - global_start\nprint(f\"\\nTotal training time: {total_time/60:.2f} min\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T14:47:31.865359Z","iopub.execute_input":"2026-03-12T14:47:31.865675Z","iopub.status.idle":"2026-03-12T14:52:07.598467Z","shell.execute_reply.started":"2026-03-12T14:47:31.865654Z","shell.execute_reply":"2026-03-12T14:52:07.597802Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# OOF metrics\n\nfold_df_skel = pd.DataFrame(fold_logs_skel)\n\noof_best_thr_skel, oof_best_f1_skel, thr_df_skel = find_best_threshold(\n    train_df[TARGET_COL].values,\n    oof_pred_skel\n)\n\noof_f1_at_05_skel = f1_score(\n    train_df[TARGET_COL].values,\n    (oof_pred_skel >= 0.5).astype(int)\n)\n\nmetrics_summary_skel = pd.DataFrame([\n    {\"metric\": \"OOF F1 @ best threshold\", \"value\": float(oof_best_f1_skel)},\n    {\"metric\": \"OOF best threshold\", \"value\": float(oof_best_thr_skel)},\n    {\"metric\": \"OOF F1 @ 0.5\", \"value\": float(oof_f1_at_05_skel)},\n    {\"metric\": \"Mean fold best F1\", \"value\": float(fold_df_skel[\"fold_best_f1\"].mean())},\n    {\"metric\": \"Std fold best F1\", \"value\": float(fold_df_skel[\"fold_best_f1\"].std())},\n    {\"metric\": \"Mean fold threshold\", \"value\": float(fold_df_skel[\"fold_best_threshold\"].mean())},\n    {\"metric\": \"Std fold threshold\", \"value\": float(fold_df_skel[\"fold_best_threshold\"].std())},\n    {\"metric\": \"Positive rate\", \"value\": float(positive_rate)},\n    {\"metric\": \"Total training minutes\", \"value\": float(total_time / 60)}\n])\n\nprint(\"Fold logs:\")\ndisplay(fold_df_skel)\n\nprint(\"\\nMetrics summary:\")\ndisplay(metrics_summary_skel)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T14:52:07.599964Z","iopub.execute_input":"2026-03-12T14:52:07.600237Z","iopub.status.idle":"2026-03-12T14:52:12.232610Z","shell.execute_reply.started":"2026-03-12T14:52:07.600218Z","shell.execute_reply":"2026-03-12T14:52:12.231644Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Threshold curve\n\nimport matplotlib.pyplot as plt\n\nplt.figure(figsize=(10, 4))\nplt.plot(thr_df_skel[\"threshold\"], thr_df_skel[\"f1\"])\nplt.axvline(oof_best_thr_skel, linestyle=\"--\")\nplt.title(\"Skeleton OOF F1 vs threshold\")\nplt.xlabel(\"Threshold\")\nplt.ylabel(\"F1\")\nplt.grid(True)\nplt.show()\n\nprint(f\"Best OOF threshold: {oof_best_thr_skel:.3f}\")\nprint(f\"Best OOF F1       : {oof_best_f1_skel:.6f}\")\nprint(f\"OOF F1 @ 0.5      : {oof_f1_at_05_skel:.6f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T14:52:12.233438Z","iopub.execute_input":"2026-03-12T14:52:12.233658Z","iopub.status.idle":"2026-03-12T14:52:12.340980Z","shell.execute_reply.started":"2026-03-12T14:52:12.233639Z","shell.execute_reply":"2026-03-12T14:52:12.339951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save predictions for later comparison\n\noof_skel_df = pd.DataFrame({\n    ID_COL: train_df[ID_COL],\n    TARGET_COL: train_df[TARGET_COL],\n    \"oof_pred_skel\": oof_pred_skel\n})\n\ntest_skel_df = pd.DataFrame({\n    ID_COL: test_df[ID_COL],\n    \"test_pred_skel\": test_pred_skel\n})\n\noof_skel_df.to_csv(\"oof_skeleton.csv\", index=False)\ntest_skel_df.to_csv(\"test_pred_skeleton.csv\", index=False)\n\nprint(\"Saved:\")\nprint(\"- oof_skeleton.csv\")\nprint(\"- test_pred_skeleton.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T14:52:12.342671Z","iopub.execute_input":"2026-03-12T14:52:12.342905Z","iopub.status.idle":"2026-03-12T14:52:14.880880Z","shell.execute_reply.started":"2026-03-12T14:52:12.342886Z","shell.execute_reply":"2026-03-12T14:52:14.879913Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Submission\n\nsubmission_skel = sample_sub.copy()\nsubmission_skel[\"prediction\"] = (test_pred_skel >= oof_best_thr_skel).astype(int)\n\nsubmission_path = \"submission.csv\"\nsubmission_skel.to_csv(submission_path, index=False)\n\nprint(f\"Saved submission: {submission_path}\")\nsubmission_skel.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T14:52:14.881899Z","iopub.execute_input":"2026-03-12T14:52:14.882181Z","iopub.status.idle":"2026-03-12T14:52:15.155577Z","shell.execute_reply.started":"2026-03-12T14:52:14.882154Z","shell.execute_reply":"2026-03-12T14:52:15.154956Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CV vs LB gap\n\nPUBLIC_LB = 0.63667   \nPRIVATE_LB = 0.64268  \n\ncv_value = float(oof_best_f1_skel)\n\ngap_summary = pd.DataFrame([\n    {\"metric\": \"Skeleton OOF F1\", \"value\": cv_value},\n    {\"metric\": \"Skeleton Public LB\", \"value\": PUBLIC_LB},\n    {\"metric\": \"Skeleton Private LB\", \"value\": PRIVATE_LB},\n    {\"metric\": \"Public - OOF Gap\", \"value\": None if PUBLIC_LB is None else float(PUBLIC_LB - cv_value)},\n    {\"metric\": \"Abs(Public - OOF Gap)\", \"value\": None if PUBLIC_LB is None else float(abs(PUBLIC_LB - cv_value))}\n])\n\ndisplay(gap_summary)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T14:52:15.156251Z","iopub.execute_input":"2026-03-12T14:52:15.156424Z","iopub.status.idle":"2026-03-12T14:52:15.165174Z","shell.execute_reply.started":"2026-03-12T14:52:15.156409Z","shell.execute_reply":"2026-03-12T14:52:15.164164Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Skeleton: сравнение CV и LB, анализ результатов\n\nВ качестве скелета был выбран простой и воспроизводимый baseline: **TF-IDF по словам (1–2-граммы) + LogisticRegression** с **5-fold Stratified Cross-Validation**.  \nЦель этого этапа — собрать решение, которое стабильно доходит до leaderboard, и получить надёжную отправную точку для дальнейших гипотез.\n\n### Основные результаты\n\n- **OOF F1 @ best threshold:** `0.636853`\n- **Лучший OOF threshold:** `0.27`\n- **OOF F1 @ 0.5:** `0.578433`\n- **Средний лучший F1 по фолдам:** `0.637168`\n- **Std лучшего F1 по фолдам:** `0.001205`\n- **Средний threshold по фолдам:** `0.26`\n- **Std threshold по фолдам:** `0.015811`\n- **Доля положительного класса:** `0.06187`\n- **Public LB:** `0.63667`\n- **Private LB:** `0.64268`\n\n### Сравнение CV и LB\n\n- **Public - OOF gap:** `0.63667 - 0.636853 = -0.000183`\n- **|Public - OOF gap|:** `0.000183`\n- **Private - OOF gap:** `0.64268 - 0.636853 = 0.005827`\n- **Private - Public gap:** `0.64268 - 0.63667 = 0.00601`\n\n### Анализ результатов\n\nПолученный baseline можно считать **стабильным и надёжным**.  \nРезультаты по фолдам очень близки друг к другу: средний лучший F1 по фолдам равен `0.637168`, а стандартное отклонение составляет всего `0.001205`. Это означает, что модель ведёт себя устойчиво на разных разбиениях и не зависит слишком сильно от конкретного фолда.\n\nНаиболее важное наблюдение связано с **подбором threshold**.  \nЕсли использовать стандартный порог `0.5`, то модель показывает OOF F1 = `0.578433`. Однако подбор оптимального threshold на OOF-предсказаниях повышает результат до `0.636853`. Прирост составляет:\n\n**`0.636853 - 0.578433 = +0.058420`**\n\nЭто существенное улучшение. Следовательно, в данной задаче качество определяется не только моделью, но и корректным выбором decision threshold. Такой результат ожидаем, поскольку данные **сильно несбалансированы**: доля положительного класса составляет только около `6.19%`.\n\nЕщё один важный результат — почти полное совпадение **локальной валидации и public leaderboard**.  \nРазрыв между OOF F1 и Public LB составляет всего `-0.000183`, что является крайне малой величиной. Это означает, что выбранная схема валидации очень хорошо отражает поведение модели на leaderboard. Иными словами, текущий CV можно считать хорошо согласованным с конкурсной метрикой.\n\n**Private LB** также находится близко к локальной оценке и даже немного выше, чем OOF и Public LB. Это говорит о том, что baseline хорошо обобщается и не демонстрирует явных признаков переобучения.\n\n### Вывод\n\nСобранный skeleton можно считать **сильной отправной точкой** для дальнейших экспериментов, потому что:\n\n1. решение успешно доходит до leaderboard;\n2. baseline показывает стабильные результаты на cross-validation;\n3. разрыв между CV и Public LB практически отсутствует;\n4. подбор threshold даёт большой прирост качества и является важной частью решения.\n\nСледовательно, следующий шаг — не просто подбор гиперпараметров, а содержательное развитие baseline через новую идею или новое представление признаков.","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}