{"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":"## Гипотеза 1: компактная символьная ветка сохранит большую часть выигрыша H0 при меньшем времени обучения\n\n### Идея гипотезы\n\nРезультаты H0 показали, что добавление `char TF-IDF` признаков к `word TF-IDF` даёт устойчивый прирост качества, но заметно увеличивает время обучения.\n\nПоэтому в гипотезе 1 проверяется, можно ли уменьшить размер символьной части признакового пространства и сохранить большую часть качества H0. Предположение состоит в том, что значительная часть полезного сигнала содержится не во всех символьных n-граммах, а только в наиболее частотных и устойчивых паттернах.\n\n### Что именно меняется\n\nПо сравнению с H0:\n- сохраняется модель `LogisticRegression`;\n- сохраняется `word TF-IDF`;\n- уменьшается объём `char TF-IDF` признаков.\n\nЦель — получить более выгодное соотношение качества и времени обучения.\n\n### Ожидаемый эффект\n\nОжидается, что H1:\n1. будет обучаться быстрее, чем H0;\n2. сохранит большую часть прироста H0 относительно skeleton;\n3. не ухудшит сильно качество на leaderboard.","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"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 LogisticRegression\nfrom sklearn.metrics import f1_score\n\nwarnings.filterwarnings(\"ignore\")\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":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T17:26:51.947860Z","iopub.execute_input":"2026-03-12T17:26:51.948141Z","iopub.status.idle":"2026-03-12T17:26:53.161366Z","shell.execute_reply.started":"2026-03-12T17:26:51.948115Z","shell.execute_reply":"2026-03-12T17:26:53.160220Z"}},"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-12T17:26:55.329651Z","iopub.execute_input":"2026-03-12T17:26:55.330142Z","iopub.status.idle":"2026-03-12T17:26:59.881316Z","shell.execute_reply.started":"2026-03-12T17:26:55.330106Z","shell.execute_reply":"2026-03-12T17:26:59.880267Z"}},"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-12T17:27:01.839768Z","iopub.execute_input":"2026-03-12T17:27:01.840742Z","iopub.status.idle":"2026-03-12T17:27:02.187679Z","shell.execute_reply.started":"2026-03-12T17:27:01.840703Z","shell.execute_reply":"2026-03-12T17:27:02.186716Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def 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-12T18:30:54.122662Z","iopub.execute_input":"2026-03-12T18:30:54.123665Z","iopub.status.idle":"2026-03-12T18:30:54.133912Z","shell.execute_reply.started":"2026-03-12T18:30:54.123615Z","shell.execute_reply":"2026-03-12T18:30:54.132877Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from scipy.sparse import hstack\nimport time\nimport gc\n\n\n# H1: compact char branch\n\noof_pred_h1 = np.zeros(len(train_df), dtype=np.float32)\ntest_pred_h1 = np.zeros(len(test_df), dtype=np.float32)\n\nfold_logs_h1 = []\n\nglobal_start_h1 = 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    # same word branch as H0\n    word_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    # compact char branch\n    char_vectorizer = TfidfVectorizer(\n        analyzer=\"char_wb\",\n        ngram_range=(4, 5),\n        min_df=8,\n        max_df=0.95,\n        max_features=30000,\n        sublinear_tf=True,\n        strip_accents=\"unicode\",\n        lowercase=True\n    )\n\n    X_train_word = word_vectorizer.fit_transform(X_train_text)\n    X_val_word = word_vectorizer.transform(X_val_text)\n    X_test_word = word_vectorizer.transform(test_df[TEXT_COL].values)\n\n    X_train_char = char_vectorizer.fit_transform(X_train_text)\n    X_val_char = char_vectorizer.transform(X_val_text)\n    X_test_char = char_vectorizer.transform(test_df[TEXT_COL].values)\n\n    X_train = hstack([X_train_word, X_train_char]).tocsr()\n    X_val = hstack([X_val_word, X_val_char]).tocsr()\n    X_test = hstack([X_test_word, X_test_char]).tocsr()\n\n    model = LogisticRegression(\n        C=4.0,\n        solver=\"liblinear\",\n        max_iter=1000,\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_h1[val.index] = val_pred\n    test_pred_h1 += 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    total_elapsed = time.time() - global_start_h1\n\n    folds_done = fold + 1\n    avg_fold_time = total_elapsed / folds_done\n    folds_left = N_SPLITS - folds_done\n    eta_remaining = avg_fold_time * folds_left\n    eta_total = avg_fold_time * N_SPLITS\n\n    fold_logs_h1.append({\n        \"fold\": fold,\n        \"train_size\": int(len(trn)),\n        \"val_size\": int(len(val)),\n        \"word_features\": int(X_train_word.shape[1]),\n        \"char_features\": int(X_train_char.shape[1]),\n        \"total_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        \"avg_fold_time_sec_so_far\": float(avg_fold_time),\n        \"eta_remaining_sec\": float(eta_remaining),\n    })\n\n    print(\n        f\"Fold {fold + 1}/{N_SPLITS} | \"\n        f\"word={X_train_word.shape[1]:,} | \"\n        f\"char={X_train_char.shape[1]:,} | \"\n        f\"total={X_train.shape[1]:,} | \"\n        f\"best_thr={fold_best_thr:.3f} | \"\n        f\"best_f1={fold_best_f1:.6f} | \"\n        f\"fold_time={fold_time:.1f}s | \"\n        f\"elapsed={total_elapsed/60:.2f}m | \"\n        f\"avg/fold={avg_fold_time:.1f}s | \"\n        f\"ETA={eta_remaining/60:.2f}m | \"\n        f\"est_total={eta_total/60:.2f}m\"\n    )\n\n    del trn, val\n    del X_train_word, X_val_word, X_test_word\n    del X_train_char, X_val_char, X_test_char\n    del X_train, X_val, X_test\n    del model, word_vectorizer, char_vectorizer, val_pred, test_fold_pred\n    gc.collect()\n\ntotal_time_h1 = time.time() - global_start_h1\nprint(f\"\\nTotal H1 training time: {total_time_h1/60:.2f} min\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T17:38:37.909340Z","iopub.execute_input":"2026-03-12T17:38:37.910152Z","iopub.status.idle":"2026-03-12T18:08:38.285051Z","shell.execute_reply.started":"2026-03-12T17:38:37.910112Z","shell.execute_reply":"2026-03-12T18:08:38.283767Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# H1 metrics\n\nfold_df_h1 = pd.DataFrame(fold_logs_h1)\n\noof_best_thr_h1, oof_best_f1_h1, thr_df_h1 = find_best_threshold(\n    train_df[TARGET_COL].values,\n    oof_pred_h1\n)\n\noof_f1_at_05_h1 = f1_score(\n    train_df[TARGET_COL].values,\n    (oof_pred_h1 >= 0.5).astype(int)\n)\n\nmetrics_summary_h1 = pd.DataFrame([\n    {\"metric\": \"OOF F1 @ best threshold\", \"value\": float(oof_best_f1_h1)},\n    {\"metric\": \"OOF best threshold\", \"value\": float(oof_best_thr_h1)},\n    {\"metric\": \"OOF F1 @ 0.5\", \"value\": float(oof_f1_at_05_h1)},\n    {\"metric\": \"Mean fold best F1\", \"value\": float(fold_df_h1[\"fold_best_f1\"].mean())},\n    {\"metric\": \"Std fold best F1\", \"value\": float(fold_df_h1[\"fold_best_f1\"].std())},\n    {\"metric\": \"Mean fold threshold\", \"value\": float(fold_df_h1[\"fold_best_threshold\"].mean())},\n    {\"metric\": \"Std fold threshold\", \"value\": float(fold_df_h1[\"fold_best_threshold\"].std())},\n    {\"metric\": \"Mean total features\", \"value\": float(fold_df_h1[\"total_features\"].mean())},\n    {\"metric\": \"Total training minutes\", \"value\": float(total_time_h1 / 60)}\n])\n\nprint(\"Fold logs H1:\")\ndisplay(fold_df_h1)\n\nprint(\"\\nMetrics summary H1:\")\ndisplay(metrics_summary_h1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T18:12:34.209110Z","iopub.execute_input":"2026-03-12T18:12:34.209753Z","iopub.status.idle":"2026-03-12T18:12:41.876692Z","shell.execute_reply.started":"2026-03-12T18:12:34.209706Z","shell.execute_reply":"2026-03-12T18:12:41.875706Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save H1 predictions\n\noof_h1_df = pd.DataFrame({\n    ID_COL: train_df[ID_COL],\n    TARGET_COL: train_df[TARGET_COL],\n    \"oof_pred_h1\": oof_pred_h1\n})\n\ntest_h1_df = pd.DataFrame({\n    ID_COL: test_df[ID_COL],\n    \"test_pred_h1\": test_pred_h1\n})\n\noof_h1_df.to_csv(\"oof_h1_word_char.csv\", index=False)\ntest_h1_df.to_csv(\"test_pred_h1_word_char.csv\", index=False)\n\nprint(\"Saved:\")\nprint(\"- oof_h1_word_char.csv\")\nprint(\"- test_pred_h1_word_char.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T18:13:50.203506Z","iopub.execute_input":"2026-03-12T18:13:50.204379Z","iopub.status.idle":"2026-03-12T18:13:54.361982Z","shell.execute_reply.started":"2026-03-12T18:13:50.204313Z","shell.execute_reply":"2026-03-12T18:13:54.360830Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# H1 submission\n\nsubmission_h1 = sample_sub.copy()\nsubmission_h1[\"prediction\"] = (test_pred_h1 >= oof_best_thr_h1).astype(int)\n\nsubmission_h1_path = \"submission.csv\"\nsubmission_h1.to_csv(submission_h1_path, index=False)\n\nprint(f\"Saved submission: {submission_h1_path}\")\nsubmission_h1.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-12T18:14:25.911441Z","iopub.execute_input":"2026-03-12T18:14:25.911786Z","iopub.status.idle":"2026-03-12T18:14:26.410720Z","shell.execute_reply.started":"2026-03-12T18:14:25.911761Z","shell.execute_reply":"2026-03-12T18:14:26.409602Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Результат: Private Score: 0.65125,  Public Score: 0.64574","metadata":{}},{"cell_type":"markdown","source":"## Гипотеза 1: Анализ результата\n\n### Идея гипотезы\n\nРезультаты H0 показали, что добавление `char TF-IDF` признаков к `word TF-IDF` даёт устойчивый прирост качества, однако заметно увеличивает время обучения.  \nПоэтому в гипотезе 1 проверялось, можно ли уменьшить размер символьной ветки и сохранить большую часть выигрыша H0 при более низкой вычислительной стоимости.\n\nПо сравнению с H0 в H1:\n- сохраняется модель `LogisticRegression`;\n- сохраняется основная `word TF-IDF` ветка;\n- символьная ветка делается более компактной:\n  - вместо `char_wb (3,5)` используется `char_wb (4,5)`;\n  - увеличивается `min_df`;\n  - уменьшается число char-признаков.\n\nТаким образом, H1 — это не попытка просто повысить score любой ценой, а проверка более практичного компромисса между качеством и стоимостью обучения.\n\n### Результаты H1\n\n- **OOF F1 @ best threshold:** `0.644325`\n- **Лучший threshold:** `0.26`\n- **Mean fold best F1:** `0.644779`\n- **Std fold best F1:** `0.002493`\n- **Mean total features:** `180000`\n- **Total training minutes:** `27.68`\n- **Public LB:** `0.64574`\n- **Private LB:** `0.65125`\n\n### Сравнение со skeleton\n\n- **Skeleton OOF F1:** `0.636853`\n- **H1 OOF F1:** `0.644325`\n- **Прирост по OOF:** `+0.007472`\n\n- **Skeleton Public LB:** `0.63667`\n- **H1 Public LB:** `0.64574`\n- **Прирост по Public LB:** `+0.00907`\n\n- **Skeleton Private LB:** `0.64268`\n- **H1 Private LB:** `0.65125`\n- **Прирост по Private LB:** `+0.00857`\n\nСледовательно, даже облегчённая символьная ветка даёт устойчивое улучшение по сравнению с исходным baseline.\n\n### Сравнение с H0\n\n- **H0 OOF F1:** `0.645697`\n- **H1 OOF F1:** `0.644325`\n- **Разница по OOF:** `-0.001372`\n\n- **H0 Public LB:** `0.64677`\n- **H1 Public LB:** `0.64574`\n- **Разница по Public LB:** `-0.00103`\n\n- **H0 Private LB:** `0.65256`\n- **H1 Private LB:** `0.65125`\n- **Разница по Private LB:** `-0.00131`\n\nПотеря качества относительно H0 очень небольшая: H1 почти сохраняет весь выигрыш H0, но при этом обучается заметно быстрее.\n\n### Анализ CV-LB gap\n\nДля H1:\n- **Public - OOF gap:** `0.64574 - 0.644325 = +0.001415`\n- **Private - OOF gap:** `0.65125 - 0.644325 = +0.006925`\n- **Private - Public gap:** `0.65125 - 0.64574 = +0.00551`\n\nЭти разрывы остаются небольшими и сопоставимыми с предыдущими сильными решениями.  \nЭто означает, что локальная валидация по-прежнему хорошо отражает поведение модели на leaderboard. H1 не даёт тревожного роста CV-LB gap, а значит не выглядит как локально переобученное решение.\n\n### Вывод по H1\n\nГипотеза 1 **подтвердилась частично, но содержательно**.\n\nH1 не превзошла H0 по качеству, однако:\n- сохранила почти весь выигрыш H0;\n- осталась заметно лучше skeleton;\n- сократила время обучения с `46.54` до `27.68` минут.\n\nТаким образом, H1 показывает, что полезный сигнал действительно содержится в символьных/subword-признаках, но для получения сильного результата не обязательно использовать максимально широкую char-ветку. Более компактная символьная часть позволяет получить гораздо более выгодное соотношение качества и вычислительной стоимости.","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}