{"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":"","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-06-08T03:57:22.863976Z","iopub.execute_input":"2026-06-08T03:57:22.864442Z","iopub.status.idle":"2026-06-08T03:59:45.678003Z","shell.execute_reply.started":"2026-06-08T03:57:22.864415Z","shell.execute_reply":"2026-06-08T03:59:45.677268Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =====================================================\n# 模型 B：表格特征 + 放射组学 CT 特征\n# 接在放射组学提取脚本之后运行（此时 /kaggle/working/radiomics_features.csv 已存在）\n#\n# 训练逻辑与 cat2.py 完全一致（同样的参数、同样的 GroupKFold、同样的指标），\n# 唯一区别是多了 ct_ 特征列 —— 这样模型 A vs 模型 B 才是公平对比。\n#\n# 判断 CT 有没有用：比较本脚本打印的「整体 OOF OSIC」与模型 A 的「整体 OOF OSIC」。\n# =====================================================\nimport warnings\nwarnings.filterwarnings(\"ignore\")\nimport os, json\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.metrics import mean_absolute_error, mean_squared_error\nfrom catboost import CatBoostRegressor\n\n# ---------- 路径配置 ----------\nTRAIN_MODIFIED = \"/kaggle/input/datasets/tluollv/train-modified/train_modified.csv\"                       # cat2.py 用的底表（模型A用的同一张）\nRADIOMICS_CSV  = \"/kaggle/input/datasets/tluollv/train-modified/radiomics_features.csv\"   # 放射组学脚本的输出\nTEST_FEAT_CSV  = \"/kaggle/input/datasets/tluollv/train-modified/submission_with_features.csv\"  # 你 test 处理脚本的输出\nRAW_BASE       = \"/kaggle/input/competitions/osic-pulmonary-fibrosis-progression\"\n\n\ndef osic_metric(y_true, y_pred, confidence=100):\n    sigma = np.maximum(confidence, 70)\n    delta = np.minimum(np.abs(y_true - y_pred), 1000)\n    score = -np.sqrt(2) * delta / sigma - np.log(np.sqrt(2) * sigma)\n    return np.mean(score)\n\n\n# =====================================================\n# PART 1：合并放射组学特征 → train_modified_B\n# =====================================================\ntrain = pd.read_csv(TRAIN_MODIFIED)\nn_train_before = len(train)\n\n# 放射组学表里 5 个 test 病人在 train/test 各出现一次，必须按 split 过滤+去重，\n# 否则 merge 一对多会把训练集行数撑翻倍，导致 OOF 分数失真。\nrad_all   = pd.read_csv(RADIOMICS_CSV)\nrad_train = (rad_all[rad_all['split'] == 'train']\n             .drop(columns=['split'])\n             .drop_duplicates(subset='Patient'))\ntrain = train.merge(rad_train, on='Patient', how='left')     # 左连接：坏CT病人 ct_ 列为 NaN\n\nassert len(train) == n_train_before, \\\n    f\"训练集行数变了！merge后{len(train)} != 原{n_train_before}，merge 出问题了\"\n\nct_cols = [c for c in train.columns if c.startswith('ct_')]\nprint(f\"合并后 shape: {train.shape}（行数应不变={n_train_before}），新增 CT 特征 {len(ct_cols)} 列\")\nprint(f\"CT 特征缺失的病人数（坏CT，交给CatBoost当缺失值）: \"\n      f\"{train[train[ct_cols[0]].isna()]['Patient'].nunique()}\")\n\n# 目标变量（与 cat2.py 一致）\ntrain[\"FVC_delta\"] = train[\"FVC\"] - train[\"Base_FVC\"]\n\n# 特征列：除这三列外全是特征（与 cat2.py 同样的自动推导，所以 ct_ 列会自动纳入）\nexclude_cols = {\"Patient\", \"FVC\", \"FVC_delta\"}\nfeature_cols = sorted(list(set(train.columns) - exclude_cols))\n\n# 类别特征（Sex / SmokingStatus 是文本）\ncat_features = [c for c in feature_cols\n                if train[c].dtype == 'object' or train[c].dtype.name == 'category']\nprint(f\"特征数: {len(feature_cols)}  其中类别特征: {cat_features}\")\n\n# 保存特征列，供 test 端严格对齐（避免写死 feature_cols 的坑）\njson.dump(feature_cols, open('/kaggle/working/feature_cols_B.json', 'w'))\n\n\n# =====================================================\n# PART 2：5 折 CV 训练（与 cat2.py 完全一致）\n# =====================================================\ngroups = train[\"Patient\"]\noof_fvc_pred = np.zeros(len(train))\noof_conf_pred = np.zeros(len(train))\nCONF_FACTOR = 0.8\n\ngkf = GroupKFold(n_splits=5)\nfor fold, (tr_idx, va_idx) in enumerate(gkf.split(train, groups=groups), start=1):\n    tr, va = train.iloc[tr_idx], train.iloc[va_idx]\n\n    model = CatBoostRegressor(\n        iterations=1500, learning_rate=0.02, depth=4,\n        loss_function='MultiQuantile:alpha=0.15,0.50,0.85',\n        random_seed=42, verbose=0,\n        cat_features=cat_features if cat_features else None,\n    )\n    model.fit(tr[feature_cols], tr[\"FVC_delta\"].values,\n              eval_set=(va[feature_cols], va[\"FVC_delta\"].values),\n              early_stopping_rounds=50, verbose=0)\n\n    preds = model.predict(va[feature_cols])\n    d_low, d_mid, d_high = preds[:, 0], preds[:, 1], preds[:, 2]\n    oof_fvc_pred[va_idx]  = va[\"Base_FVC\"].values + d_mid\n    oof_conf_pred[va_idx] = (d_high - d_low) * CONF_FACTOR\n\n    val_score = osic_metric(va[\"FVC\"].values, oof_fvc_pred[va_idx],\n                            confidence=np.maximum(oof_conf_pred[va_idx], 70))\n    print(f\"Fold {fold}  val OSIC: {val_score:.5f}\")\n    model.save_model(f\"/kaggle/working/catboost_B_fold{fold}.cbm\")  # 不覆盖模型A\n\n# ---------- 模型 B 的整体 OOF（与模型 A 对比的关键数字）----------\ny_true = train[\"FVC\"].values\noverall_score = osic_metric(y_true, oof_fvc_pred,\n                            confidence=np.maximum(oof_conf_pred, 70))\nprint(\"\\n\" + \"=\" * 55)\nprint(f\"【模型 B】整体 OOF MAE : {mean_absolute_error(y_true, oof_fvc_pred):.3f}\")\nprint(f\"【模型 B】整体 OOF OSIC: {overall_score:.5f}   ← 和模型A的OOF OSIC对比看CT有没有用\")\nprint(\"=\" * 55)\n\n\n# =====================================================\n# PART 3：Confidence 事后标定（网格搜索，与 cat2.py 一致）\n# =====================================================\nbest_score, best_c, best_base = -9999, 1.0, 0\nfor c in np.arange(0.5, 2.0, 0.05):\n    for base in range(0, 300, 10):\n        adj = np.maximum(oof_conf_pred * c + base, 70)\n        s = osic_metric(y_true, oof_fvc_pred, confidence=adj)\n        if s > best_score:\n            best_score, best_c, best_base = s, c, base\nprint(f\"标定后 OOF OSIC: {best_score:.5f}  (best_c={best_c:.2f}, best_base={best_base})\")\n\n\n# =====================================================\n# CT 放射组学特征 —— 随机化检验\n# 目的：判断「模型B 比 模型A 的那点提升」到底来自真实CT信息，\n#       还是仅仅因为多加了22个列让模型蹭到一点运气。\n#\n# 方法：把 ct_ 列的值按行整体打乱（每个病人领走别人的CT特征），\n#       其余完全不变，重跑同样的CV。打乱多次得到「随机CT」的OOF分布。\n#\n# 判读：\n#   真CT 明显 > 随机CT分布   → CT带有真实信息，有用\n#   真CT 落在随机CT分布之内  → 那点提升是噪声列效应，CT其实没用\n#\n# 前置：需要先跑过模型B脚本，本脚本复用其中的 train / feature_cols / cat_features /\n#       osic_metric / 各超参数。直接接在模型B（PART1~3）之后运行即可。\n# =====================================================\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import GroupKFold\nfrom catboost import CatBoostRegressor\n\nN_SHUFFLE = 8          # 打乱次数（建议 5~10）\nct_cols = [c for c in train.columns if c.startswith('ct_')]\ngroups = train[\"Patient\"]\ny_true = train[\"FVC\"].values\n\n\ndef run_cv(df):\n    \"\"\"对给定特征表跑一遍与模型B完全相同的CV，返回整体OOF OSIC（标定前）。\"\"\"\n    oof_fvc  = np.zeros(len(df))\n    oof_conf = np.zeros(len(df))\n    gkf = GroupKFold(n_splits=5)\n    for tr_idx, va_idx in gkf.split(df, groups=groups):\n        tr, va = df.iloc[tr_idx], df.iloc[va_idx]\n        m = CatBoostRegressor(\n            iterations=1500, learning_rate=0.02, depth=4,\n            loss_function='MultiQuantile:alpha=0.15,0.50,0.85',\n            random_seed=42, verbose=0,\n            cat_features=cat_features if cat_features else None,\n        )\n        m.fit(tr[feature_cols], tr[\"FVC_delta\"].values,\n              eval_set=(va[feature_cols], va[\"FVC_delta\"].values),\n              early_stopping_rounds=50, verbose=0)\n        p = m.predict(va[feature_cols])\n        oof_fvc[va_idx]  = va[\"Base_FVC\"].values + p[:, 1]\n        oof_conf[va_idx] = (p[:, 2] - p[:, 0]) * 0.8\n    return osic_metric(y_true, oof_fvc, confidence=np.maximum(oof_conf, 70))\n\n\n# ---------- 真 CT（基准，就是模型B本身）----------\nreal_score = run_cv(train)\nprint(f\"真 CT (模型B) OOF OSIC: {real_score:.5f}\\n\")\n\n# ---------- 多次打乱 CT 列 ----------\nshuffle_scores = []\nfor i in range(N_SHUFFLE):\n    df_shuf = train.copy()\n    # 把 ct_ 这一整块按行随机重排（保持各ct列之间的对应关系，只打乱\"哪个病人对哪份CT\"）\n    perm = np.random.RandomState(i).permutation(len(df_shuf))\n    df_shuf[ct_cols] = df_shuf[ct_cols].values[perm]\n    s = run_cv(df_shuf)\n    shuffle_scores.append(s)\n    print(f\"  随机CT 第{i+1}次: {s:.5f}\")\n\nshuffle_scores = np.array(shuffle_scores)\n\n# ---------- 汇总判读 ----------\nprint(\"\\n\" + \"=\" * 55)\nprint(f\"真 CT          : {real_score:.5f}\")\nprint(f\"随机CT 均值±标准差: {shuffle_scores.mean():.5f} ± {shuffle_scores.std():.5f}\")\nprint(f\"随机CT 范围     : [{shuffle_scores.min():.5f}, {shuffle_scores.max():.5f}]\")\nprint(\"-\" * 55)\n# 真CT比多少比例的随机CT更好\nbetter = (real_score > shuffle_scores).mean()\nprint(f\"真CT 优于 {better*100:.0f}% 的随机CT\")\nif real_score > shuffle_scores.max():\n    print(\"→ 真CT 超出所有随机CT：CT 带有真实信息，提升可信 ✅\")\nelif real_score > shuffle_scores.mean():\n    print(\"→ 真CT 高于随机均值但未超全部：CT 可能有微弱信息，但不强\")\nelse:\n    print(\"→ 真CT 落在随机CT分布内：那点提升来自噪声列效应，CT 实质没用\")\nprint(\"=\" * 55)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}