{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# NVIDIA Kumo Tabular 실습 노트북 (Colab)\n\n이 노트북은 NVIDIA가 2026년 9월 말 공개한 tabular foundation model **Kumo Tabular**를 Google Colab에서 돌려보는 실습입니다.\n\n**구성**\n\n| 파트 | 내용 | 데이터 |\n|---|---|---|\n| 0 | 환경 준비 (GPU 확인, 설치) | - |\n| 1 | 기본 예시: classification | sklearn breast cancer |\n| 2 | 기본 예시: regression + quantile 출력 | sklearn diabetes |\n| 3 | 소재 예시 A: 강철 항복강도 regression (small data) | matbench_steels (312개 합금) |\n| 4 | 소재 예시 B: 금속/비금속 classification | matbench_expt_is_metal (4,921개 조성) |\n| 5 | 내 CSV 데이터에 적용하는 템플릿 | 사용자 데이터 |\n\n**런타임 설정:** 메뉴의 `런타임 → 런타임 유형 변경 → T4 GPU`를 선택하세요. CPU에서도 동작하지만 파트 4는 상당히 느려집니다."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Kumo Tabular는 어떤 모델인가?\n\n기존 tabular ML(XGBoost, Random Forest 등)은 데이터셋마다 모델을 **처음부터 학습**하고 hyperparameter를 튜닝해야 합니다. Kumo Tabular는 LLM의 few-shot prompting과 같은 **in-context learning(ICL)** 방식을 씁니다.\n\n- 라벨이 달린 행들(**context**)과 예측하고 싶은 행들(**query**)을 함께 넣으면, **forward pass 한 번**으로 query의 라벨을 예측합니다.\n- 가중치 업데이트, hyperparameter tuning, feature engineering이 필요 없습니다.\n- Structural Causal Model(SCM)로 생성한 **인공 테이블 수천만 개**로만 pretraining되었습니다. 즉 \"테이블이라는 것이 일반적으로 어떻게 생겼는지\"를 미리 배워 둔 모델입니다.\n- 구조는 TabPFN, TabICL 계열과 같은 Transformer입니다. column attention(같은 열 안에서 값의 분포 파악) → row attention(한 행 안에서 feature 간 상호작용) → in-context attention(query 행이 context 행을 참조) 순서로 처리합니다.\n\n**모델 크기:** `small`(약 28M), `medium`(약 62M), `large`(약 215M, 기본값). classification과 regression은 별도 가중치이며, 전부 T4 GPU 하나에 여유 있게 올라갑니다.\n\n**소재 연구 관점에서의 의미:** 실험 데이터처럼 행이 수십~수천 개인 small data 영역에서 별도 튜닝 없이 강한 baseline을 즉시 얻을 수 있고, regression에서는 999개 quantile을 출력하므로 **예측 불확실성**도 함께 얻습니다. 이 점은 active learning이나 Bayesian optimization의 surrogate model 후보로서 흥미로운 부분입니다."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "---\n# 파트 0. 환경 준비\n\n`structured-data-models`(`sdm`) 라이브러리는 **Python 3.11 이상, PyTorch 2.7 이상**을 요구합니다. 현재 Colab 기본 환경은 이 조건을 만족하므로 별도 PyTorch 설치는 필요 없습니다. 먼저 GPU와 버전을 확인합니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "import sys, torch\nprint(\"Python :\", sys.version.split()[0])\nprint(\"PyTorch:\", torch.__version__)\nprint(\"CUDA   :\", torch.cuda.is_available())\nif torch.cuda.is_available():\n    print(\"GPU    :\", torch.cuda.get_device_name(0))\n    print(f\"VRAM   : {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "라이브러리를 GitHub에서 설치합니다. 소재 예시(파트 3, 4)에 쓸 `matminer`(소재 데이터셋 + composition featurizer)도 함께 설치합니다. 1~2분 정도 걸립니다.\n\n> `ERROR: pip's dependency resolver ...` 형태의 빨간 메시지는 대부분 **경고**이며 설치는 완료된 상태입니다. requests 버전 충돌은 위 셀에서 버전을 고정해 방지했습니다.\n>\n> 설치 후 import 오류가 나면 `런타임 → 세션 다시 시작` 후 아래 셀부터 다시 실행하세요. matminer가 의존하는 pymatgen이 numpy 버전을 바꾸는 경우가 가끔 있습니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "!pip install -q git+https://github.com/NVIDIA/structured-data-models.git\n!pip install -q matminer \"requests==2.32.4\"   # Colab 기본 패키지와 requests 버전 맞춤"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "import time\nimport numpy as np\nimport pandas as pd\nimport torch\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, roc_auc_score, mean_absolute_error, r2_score\n\nimport sdm\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nuse_amp = device.type == \"cuda\"   # GPU에서만 float16 autocast 사용\nprint(\"device:\", device)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### 공통 헬퍼 함수\n\n`sdm`의 입출력은 `TableTensor`라는 PyTorch tensor subclass입니다. 몇 가지 핵심만 알면 됩니다.\n\n- `sdm.TableTensor.from_pandas(df, stypes, device)`: DataFrame을 GPU 위의 테이블 tensor로 변환합니다.\n- `stypes`(semantic types): 각 열이 `\"numerical\"`인지 `\"categorical\"`인지 지정하는 dict입니다. `sdm.infer_stypes(df)`로 자동 추론하고, 필요한 열만 `overrides`로 바꿉니다.\n- **task는 target의 stype으로 결정됩니다.** target이 `categorical`이면 classification, `numerical`이면 regression입니다. 그래서 정수 0/1 라벨은 반드시 `categorical`로 override해야 합니다. 안 그러면 regression으로 처리됩니다.\n- 인덱싱: `table[행 인덱스]`, `table[:, \"target\"]`(열 선택), `table.drop_columns(\"target\")`.\n- 출력도 `TableTensor`입니다. classification은 **클래스별 확률 열**, regression은 **`q001`~`q999` 999개 quantile 열**로 나옵니다. `.to_pandas()`로 DataFrame 변환이 가능합니다.\n\n아래 함수들은 이 과정을 반복하지 않도록 묶어 둔 것입니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "def make_table(df, target, task):\n    # DataFrame -> TableTensor. task에 맞게 target stype을 강제합니다.\n    stype = \"categorical\" if task == \"classification\" else \"numerical\"\n    stypes = sdm.infer_stypes(df, overrides={target: stype})\n    return sdm.TableTensor.from_pandas(df=df, stypes=stypes, device=device)\n\n\ndef run_icl(model, table, target, idx_context, idx_query, num_estimators=8, seed=0):\n    # context 행으로 query 행을 예측하는 ICL forward pass 1회.\n    ic = torch.as_tensor(np.asarray(idx_context), device=device)\n    iq = torch.as_tensor(np.asarray(idx_query), device=device)\n    gen = torch.Generator(device=device).manual_seed(seed)   # 재현성\n    with torch.no_grad(), torch.amp.autocast(device.type, dtype=torch.float16, enabled=use_amp):\n        out = model(\n            x_context=table[ic].drop_columns(target),\n            y_context=table[ic][:, target],\n            x_query=table[iq].drop_columns(target),\n            num_estimators=num_estimators,\n            generator=gen,\n        )\n    return out\n\n\ndef class_proba(out):\n    # classification 출력 -> 열 이름이 클래스 라벨(문자열)인 확률 DataFrame\n    p = out.to_pandas()\n    p.columns = [str(c) for c in p.columns]\n    return p.astype(float)\n\n\ndef quantiles(out, qs=(\"q050\", \"q500\", \"q950\")):\n    # regression 출력 -> 평균 예측과 지정한 quantile들\n    res = {\"mean\": out.numerical.float().mean(dim=-1).cpu().numpy()}\n    for q in qs:\n        res[q] = out[q].numerical.float().squeeze(-1).cpu().numpy()\n    return pd.DataFrame(res)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "---\n# 파트 1. 기본 예시: Classification (유방암 진단 데이터)\n\nsklearn에 내장된 breast cancer 데이터셋(569행, 수치형 feature 30개, 이진 라벨)입니다. 모델 카드의 공식 예제와 같은 데이터인데, 여기서는 random split과 평가 지표를 추가했습니다.\n\n**모델 로딩:** `sdm.models.KumoTabular(task=..., size=..., device=...)`\n- `task=\"classification\"`으로 지정하면 classifier 가중치만 내려받습니다. 생략하면 두 task 가중치를 모두 받습니다.\n- 첫 실행 시 Hugging Face Hub(`nvidia/Kumo-Tabular`)에서 가중치를 자동 다운로드합니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "from sklearn.datasets import load_breast_cancer\n\ndf_bc = load_breast_cancer(as_frame=True).frame        # 'target' 열: 0=malignant, 1=benign\nprint(df_bc.shape)\nprint(df_bc[\"target\"].value_counts())\n\ntable_bc = make_table(df_bc, \"target\", \"classification\")\nidx_tr, idx_te = train_test_split(np.arange(len(df_bc)), test_size=0.3,\n                                  stratify=df_bc[\"target\"], random_state=0)\n\nclf = sdm.models.KumoTabular(task=\"classification\", size=\"large\", device=device)\nprint(clf)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "이제 예측합니다. 다시 강조하면, 아래 셀에는 **학습 단계가 없습니다.** train 행 398개를 context로 넘기고 test 행 171개를 바로 예측합니다.\n\n`num_estimators=8`은 **ensembling** 설정입니다. 내부 전처리 recipe가 열 순서 섞기(shuffle), 서로 다른 수치 변환(power transform, rank-Gaussian 등)을 무작위로 적용한 8개의 view를 만들고, 각 view의 예측을 평균냅니다. 값을 키우면 대체로 조금 더 정확해지고 그만큼 느려집니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "t0 = time.perf_counter()\nout = run_icl(clf, table_bc, \"target\", idx_tr, idx_te, num_estimators=8)\nprint(f\"추론 시간: {time.perf_counter() - t0:.2f}s\")\n\nproba = class_proba(out)\nprint(proba.head())\n\ny_true = df_bc[\"target\"].to_numpy()[idx_te]\ny_pred = proba.idxmax(axis=1).astype(int).to_numpy()\nprint(f\"\\nAccuracy: {accuracy_score(y_true, y_pred):.4f}\")\nprint(f\"ROC-AUC : {roc_auc_score(y_true, proba['1']):.4f}\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### 비교: 튜닝 없는 Random Forest / Gradient Boosting\n\n기준점 삼아 sklearn 기본 모델과 비교해 봅니다. 공정하게 하기 위해 baseline도 hyperparameter tuning 없이 기본값으로 돌립니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "from sklearn.ensemble import RandomForestClassifier, HistGradientBoostingClassifier\n\nX = df_bc.drop(columns=\"target\").to_numpy(); y = df_bc[\"target\"].to_numpy()\nfor name, m in [(\"RandomForest\", RandomForestClassifier(n_estimators=500, random_state=0)),\n                (\"HistGB\", HistGradientBoostingClassifier(random_state=0))]:\n    m.fit(X[idx_tr], y[idx_tr])\n    p = m.predict_proba(X[idx_te])[:, 1]\n    print(f\"{name:13s} Acc={accuracy_score(y[idx_te], p > 0.5):.4f}  AUC={roc_auc_score(y[idx_te], p):.4f}\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### fit / predict 인터페이스 (KV cache)\n\n같은 context로 여러 번 예측할 때는 `fit()` + `predict()`를 쓰면 됩니다. 이름은 sklearn과 같지만 실제로 **가중치를 학습하지 않습니다.** context 행들의 attention key/value를 한 번 계산해서 캐시해 두는 것뿐입니다. 이후 `predict()`는 query만 처리하므로 빠릅니다. 이는 query 행이 context만 참조하고 context는 query를 보지 않는 구조 덕분입니다. 다 쓰면 `clear()`로 캐시를 비웁니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "itr = torch.as_tensor(idx_tr, device=device); ite = torch.as_tensor(idx_te, device=device)\nwith torch.no_grad(), torch.amp.autocast(device.type, dtype=torch.float16, enabled=use_amp):\n    clf.fit(x=table_bc[itr].drop_columns(\"target\"), y=table_bc[itr][:, \"target\"], num_estimators=8)\n    t0 = time.perf_counter()\n    out2 = clf.predict(table_bc[ite].drop_columns(\"target\"))\n    print(f\"predict 시간(캐시 사용): {time.perf_counter() - t0:.3f}s\")\nclf.clear()\nprint(\"ROC-AUC:\", round(roc_auc_score(y_true, class_proba(out2)[\"1\"]), 4))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "---\n# 파트 2. 기본 예시: Regression과 Quantile 출력 (당뇨병 진행도)\n\nsklearn diabetes 데이터(442행, feature 10개, 1년 후 질병 진행도 수치)입니다.\n\nKumo Tabular의 regression은 점 하나만 예측하지 않고 **예측 분포의 999개 quantile**(`q001`, `q002`, ..., `q999`)을 출력합니다. 여기서 다음을 얻을 수 있습니다.\n- **점 예측:** 999개 quantile의 평균(≈ 분포의 평균), 또는 `q500`(중앙값)\n- **예측 구간:** 예를 들어 `q050`~`q950`은 90% 예측 구간\n- 출력은 내부 recipe가 target 표준화를 역변환한 뒤라서 **원래 단위 그대로**입니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "from sklearn.datasets import load_diabetes\n\ndf_db = load_diabetes(as_frame=True).frame\ntable_db = make_table(df_db, \"target\", \"regression\")\nidx_tr, idx_te = train_test_split(np.arange(len(df_db)), test_size=0.3, random_state=0)\n\nreg = sdm.models.KumoTabular(task=\"regression\", size=\"large\", device=device)\nout = run_icl(reg, table_db, \"target\", idx_tr, idx_te, num_estimators=8)\n\nprint(\"출력 열 개수:\", len(out.to_pandas().columns), \"→\", list(out.to_pandas().columns[:3]), \"...\")\npred = quantiles(out)\ny_true = df_db[\"target\"].to_numpy()[idx_te]\nprint(f\"MAE={mean_absolute_error(y_true, pred['mean']):.2f}   R2={r2_score(y_true, pred['mean']):.4f}\")\ncover = np.mean((y_true >= pred[\"q050\"]) & (y_true <= pred[\"q950\"]))\nprint(f\"90% 예측구간 실제 coverage: {cover:.1%}  (잘 calibrate되어 있다면 90% 근처)\")"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "from sklearn.ensemble import RandomForestRegressor\nX = df_db.drop(columns=\"target\").to_numpy(); y = df_db[\"target\"].to_numpy()\nrf = RandomForestRegressor(n_estimators=500, random_state=0).fit(X[idx_tr], y[idx_tr])\np = rf.predict(X[idx_te])\nprint(f\"RandomForest  MAE={mean_absolute_error(y[idx_te], p):.2f}   R2={r2_score(y[idx_te], p):.4f}\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "---\n# 파트 3. 소재 예시 A: 강철 항복강도 예측 (small data regression)\n\n**데이터:** Matbench 벤치마크의 `matbench_steels`입니다. 312개 강철 합금의 **원소 조성 → 항복강도(MPa)** 데이터로, 실험 소재 데이터가 흔히 그렇듯 행 수가 적습니다.\n\n**Featurization:** 조성 문자열(예: `Fe0.62C0.00095Mn0.00052...`)을 pymatgen `Composition`으로 파싱하고, 각 원소의 **atomic fraction을 그대로 feature 열**로 씁니다. 모든 시료가 Fe 기반이라서 원소 평균 물성 기반의 Magpie descriptor보다 원소 함량 자체가 더 직접적인 정보이기 때문입니다.\n\n이 파트에서 확인할 것은 세 가지입니다.\n1. 기본 성능 (Random Forest 대비)\n2. **learning curve**: context 행이 25, 50, 100, 200개일 때 성능 변화. small data에서의 강점을 확인합니다.\n3. **불확실성**: 90% 예측 구간의 coverage와 시각화"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "from matminer.datasets import load_dataset\nfrom pymatgen.core import Composition\n\ndf_st = load_dataset(\"matbench_steels\")       # 열: composition, yield strength\nprint(df_st.shape); print(df_st.head(3))\n\n# 조성 -> 원소별 atomic fraction 열\nfracs = [{str(el): amt for el, amt in Composition(c).fractional_composition.items()}\n         for c in df_st[\"composition\"]]\nX_st = pd.DataFrame(fracs).fillna(0.0)\nX_st = X_st.loc[:, X_st.std() > 0]            # 전부 같은 값인 열 제거\nX_st.columns = [f\"x_{c}\" for c in X_st.columns]\ndata_st = X_st.copy()\ndata_st[\"target\"] = df_st[\"yield strength\"].to_numpy()\n\nprint(\"feature 수:\", X_st.shape[1], \"| 원소:\", [c[2:] for c in X_st.columns])\nprint(data_st[\"target\"].describe().round(1))"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "table_st = make_table(data_st, \"target\", \"regression\")\nidx_tr, idx_te = train_test_split(np.arange(len(data_st)), test_size=0.25, random_state=0)\ny_all = data_st[\"target\"].to_numpy(); X_all = X_st.to_numpy()\n\nout = run_icl(reg, table_st, \"target\", idx_tr, idx_te, num_estimators=16)\npred_st = quantiles(out)\ny_te = y_all[idx_te]\nprint(f\"Kumo Tabular  MAE={mean_absolute_error(y_te, pred_st['mean']):6.1f} MPa   R2={r2_score(y_te, pred_st['mean']):.3f}\")\n\nrf = RandomForestRegressor(n_estimators=500, random_state=0).fit(X_all[idx_tr], y_all[idx_tr])\np_rf = rf.predict(X_all[idx_te])\nprint(f\"RandomForest  MAE={mean_absolute_error(y_te, p_rf):6.1f} MPa   R2={r2_score(y_te, p_rf):.3f}\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Parity plot과 90% 예측 구간\n\n오차 막대는 각 예측의 `q050`~`q950` 구간입니다. 구간 폭이 넓은 시료는 모델이 확신하지 못하는 시료입니다. Bayesian optimization에서 exploration 대상을 고를 때 이런 정보를 그대로 활용할 수 있습니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "lo = pred_st[\"mean\"] - pred_st[\"q050\"]; hi = pred_st[\"q950\"] - pred_st[\"mean\"]\ncover = np.mean((y_te >= pred_st[\"q050\"]) & (y_te <= pred_st[\"q950\"]))\n\nfig, ax = plt.subplots(1, 2, figsize=(11, 4.5))\nax[0].errorbar(y_te, pred_st[\"mean\"], yerr=[lo.clip(0), hi.clip(0)], fmt=\"o\", ms=4, alpha=0.6, elinewidth=0.8)\nlim = [y_all.min() * 0.9, y_all.max() * 1.05]\nax[0].plot(lim, lim, \"k--\", lw=1); ax[0].set_xlim(lim); ax[0].set_ylim(lim)\nax[0].set_xlabel(\"True yield strength (MPa)\"); ax[0].set_ylabel(\"Predicted (MPa)\")\nax[0].set_title(f\"Kumo Tabular parity (90% PI coverage = {cover:.0%})\")\n\nwidth = pred_st[\"q950\"] - pred_st[\"q050\"]; err = np.abs(y_te - pred_st[\"mean\"])\nax[1].scatter(width, err, s=14, alpha=0.6)\nax[1].set_xlabel(\"90% interval width (MPa)\"); ax[1].set_ylabel(\"|error| (MPa)\")\nax[1].set_title(\"Uncertainty vs. actual error\")\nplt.tight_layout(); plt.show()\n\nprint(\"구간 폭과 실제 오차의 Spearman 상관:\",\n      round(pd.Series(width).corr(pd.Series(err), method=\"spearman\"), 3))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Learning curve: context 크기에 따른 성능\n\n실험 데이터는 수십 개 수준에서 시작하는 경우가 많습니다. context 행 수를 25 → 200으로 늘려 가며 Kumo Tabular와 Random Forest의 MAE를 비교합니다. 각 크기마다 무작위 subset 3번의 평균입니다. test set은 위와 같은 78개로 고정합니다.\n\n> GPU 기준 1분 내외입니다. 시간이 부족하면 `seeds`를 줄이세요."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "sizes, seeds = [25, 50, 100, 200], [0, 1, 2]\nrows = []\nfor n in sizes:\n    for s in seeds:\n        sub = np.random.default_rng(s).choice(idx_tr, size=n, replace=False)\n        o = run_icl(reg, table_st, \"target\", sub, idx_te, num_estimators=8, seed=s)\n        mae_k = mean_absolute_error(y_te, quantiles(o)[\"mean\"])\n        m = RandomForestRegressor(n_estimators=300, random_state=s).fit(X_all[sub], y_all[sub])\n        mae_r = mean_absolute_error(y_te, m.predict(X_all[idx_te]))\n        rows.append({\"n_context\": n, \"seed\": s, \"Kumo Tabular\": mae_k, \"RandomForest\": mae_r})\n\nlc = pd.DataFrame(rows).groupby(\"n_context\")[[\"Kumo Tabular\", \"RandomForest\"]].mean()\nprint(lc.round(1))\nlc.plot(marker=\"o\", figsize=(6, 4), logx=True, ylabel=\"Test MAE (MPa)\", title=\"Learning curve: matbench_steels\")\nplt.xticks(sizes, sizes); plt.grid(alpha=0.3); plt.show()"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "---\n# 파트 4. 소재 예시 B: 금속 / 비금속 분류 (Magpie descriptor)\n\n**데이터:** `matbench_expt_is_metal`. 실험적으로 측정된 화합물 4,921개의 **조성 → 금속 여부(True/False)** 데이터입니다.\n\n**Featurization:** 이번에는 다양한 화학계가 섞여 있으므로 matminer의 **Magpie** descriptor를 씁니다. 원소별 물성(전기음성도, 원자 반경, valence electron 수 등)을 조성 가중으로 통계 처리(mean, min, max, range, avg deviation, mode)한 132개 feature입니다. Kumo Tabular는 한 번에 최대 500개 열을 쓰므로 범위 안입니다.\n\ncontext가 약 3,900행이라 파트 1보다 큰 규모의 ICL입니다. 그래도 학습 시간은 여전히 0이고, 추론만 수 초 걸립니다.\n\n> Magpie featurization은 Colab CPU에서 1~2분 걸립니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "from matminer.featurizers.conversions import StrToComposition\nfrom matminer.featurizers.composition import ElementProperty\n\ndf_m = load_dataset(\"matbench_expt_is_metal\")        # 열: composition, is_metal\nprint(df_m.shape); print(df_m[\"is_metal\"].value_counts())\n\ndf_m = StrToComposition(target_col_id=\"comp\").featurize_dataframe(df_m, \"composition\", ignore_errors=True)\nep = ElementProperty.from_preset(\"magpie\")\ndf_m = ep.featurize_dataframe(df_m, \"comp\", ignore_errors=True)\n\nfeat_cols = ep.feature_labels()\ndata_m = df_m[feat_cols].copy()\ndata_m.columns = [c.replace(\"MagpieData \", \"\").replace(\" \", \"_\") for c in feat_cols]\ndata_m[\"target\"] = np.where(df_m[\"is_metal\"], \"metal\", \"nonmetal\")   # 문자열 라벨 -> categorical\ndata_m = data_m.dropna(subset=[\"target\"]).reset_index(drop=True)\nprint(\"feature 수:\", data_m.shape[1] - 1, \"| 결측치 포함 행:\", int(data_m.isna().any(axis=1).sum()))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "Magpie featurization 중 일부 원소 물성이 없으면 NaN이 생깁니다. Kumo Tabular는 결측치를 **imputation 없이** 내부적으로 별도 처리하므로 그대로 넣어도 됩니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "table_m = make_table(data_m, \"target\", \"classification\")\nidx_tr, idx_te = train_test_split(np.arange(len(data_m)), test_size=0.2,\n                                  stratify=data_m[\"target\"], random_state=0)\n\nt0 = time.perf_counter()\nout = run_icl(clf, table_m, \"target\", idx_tr, idx_te, num_estimators=8)\nprint(f\"context {len(idx_tr)}행 → query {len(idx_te)}행 추론: {time.perf_counter() - t0:.1f}s\")\n\nproba = class_proba(out)\ny_te = data_m[\"target\"].to_numpy()[idx_te]\nprint(f\"Kumo Tabular  Acc={accuracy_score(y_te, proba.idxmax(axis=1)):.4f}  \"\n      f\"AUC={roc_auc_score(y_te == 'metal', proba['metal']):.4f}\")\n\nXm = data_m.drop(columns=\"target\")\nhgb = HistGradientBoostingClassifier(random_state=0).fit(Xm.iloc[idx_tr], data_m[\"target\"].iloc[idx_tr])\np = hgb.predict_proba(Xm.iloc[idx_te])[:, list(hgb.classes_).index(\"metal\")]\nprint(f\"HistGB        Acc={accuracy_score(y_te, np.where(p > 0.5, 'metal', 'nonmetal')):.4f}  \"\n      f\"AUC={roc_auc_score(y_te == 'metal', p):.4f}\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### 확률 calibration 확인\n\n분류 확률이 실제 빈도와 맞는지 reliability diagram으로 확인합니다. 대각선에 가까울수록 \"0.8이라고 하면 실제로 80% 정도 맞는\" 잘 calibrate된 확률입니다. 스크리닝에서 확률값으로 후보를 거를 계획이라면 꼭 확인해야 하는 부분입니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "from sklearn.calibration import calibration_curve\nfy, mp = calibration_curve(y_te == \"metal\", proba[\"metal\"], n_bins=10)\nfy2, mp2 = calibration_curve(y_te == \"metal\", p, n_bins=10)\nplt.figure(figsize=(4.8, 4.5))\nplt.plot([0, 1], [0, 1], \"k--\", lw=1)\nplt.plot(mp, fy, \"o-\", label=\"Kumo Tabular\"); plt.plot(mp2, fy2, \"s-\", label=\"HistGB\")\nplt.xlabel(\"Predicted P(metal)\"); plt.ylabel(\"Observed fraction\"); plt.legend(); plt.title(\"Reliability diagram\")\nplt.show()"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "---\n# 파트 5. 내 CSV 데이터에 적용하기 (템플릿)\n\n자신의 실험 데이터나 계산 데이터를 쓰려면 아래 셀의 설정만 바꾸면 됩니다.\n\n- `CSV_PATH`: 왼쪽 파일 탭에서 업로드한 파일 경로\n- `TARGET`: 예측할 열 이름\n- `TASK`: `\"regression\"` 또는 `\"classification\"`\n- target 열이 **비어 있는(NaN) 행은 자동으로 query**가 됩니다. 즉 측정값이 있는 행으로 측정 안 된 후보들을 예측하는 구조입니다. 실험 계획에서 그대로 쓰기 좋은 형태입니다.\n\n**주의사항**\n- feature는 numerical/categorical 열만 직접 처리됩니다. ID나 시료명 같은 열은 `DROP_COLS`에 넣어 빼세요. categorical로 들어가면 noise가 됩니다.\n- classification은 forward pass 한 번에 클래스 10개까지 처리하며, 그 이상은 라이브러리가 error-correcting output codes(ECOC)로 자동 확장합니다."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "CSV_PATH = \"my_data.csv\"      # ← 수정\nTARGET   = \"capacity\"         # ← 수정\nTASK     = \"regression\"       # ← \"regression\" 또는 \"classification\"\nDROP_COLS = []                # ← 예: [\"sample_id\", \"date\"]\n\nimport os\nif os.path.exists(CSV_PATH):\n    df_u = pd.read_csv(CSV_PATH).drop(columns=DROP_COLS)\n    has_y = df_u[TARGET].notna().to_numpy()\n    if TASK == \"classification\":\n        df_u[TARGET] = df_u[TARGET].astype(\"string\")\n    table_u = make_table(df_u, TARGET, TASK)\n    model_u = clf if TASK == \"classification\" else reg\n    out_u = run_icl(model_u, table_u, TARGET, np.where(has_y)[0], np.where(~has_y)[0], num_estimators=16)\n\n    res = df_u.loc[~has_y].reset_index(drop=True)\n    if TASK == \"regression\":\n        res = pd.concat([res, quantiles(out_u).add_prefix(\"pred_\")], axis=1)\n        res[\"pred_PI90_width\"] = res[\"pred_q950\"] - res[\"pred_q050\"]   # 불확실성 지표\n    else:\n        res = pd.concat([res, class_proba(out_u).add_prefix(\"P_\")], axis=1)\n    res.to_csv(\"predictions.csv\", index=False)\n    display(res.head())\n    print(\"predictions.csv 저장 완료\")\nelse:\n    print(f\"{CSV_PATH} 파일이 없습니다. 파일을 업로드하고 경로를 수정하세요.\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "---\n# 정리 및 실전 주의점\n\n**잘 맞는 상황**\n- 행이 수십~수만 개인 tabular 데이터에서 튜닝 없이 빠르게 강한 baseline이 필요할 때\n- 데이터가 계속 추가되는 상황. 새 행이 들어오면 **재학습 없이** context에 추가만 하면 됩니다. active learning 루프에서 surrogate를 매번 다시 학습할 필요가 없다는 뜻입니다.\n- regression에서 예측 분포(quantile)가 필요할 때. 예측 구간, acquisition function 계산 등에 쓸 수 있습니다.\n\n**조심할 상황**\n- **외삽(extrapolation):** ICL 모델은 context에 있는 데이터 분포를 기준으로 예측합니다. 소재 탐색처럼 \"지금까지 본 것보다 더 좋은 물성\"을 찾는 문제에서는 target 범위 밖 예측이 보수적으로 나오는 경향이 있을 수 있습니다. 반드시 hold-out으로 검증하세요.\n- **분포 차이:** query가 context와 다른 화학계, 다른 공정 조건에서 오면 정확도가 떨어질 수 있습니다. 공식 블로그도 이 점을 limitation으로 명시합니다.\n- **학습 범위를 크게 넘는 테이블:** pretraining은 최대 6만 행, 100열 수준에서 이뤄졌습니다. 그보다 훨씬 크면 `num_estimators`별로 context를 나눠 넣는 방식을 고려하세요(공식 문서의 ensembling 섹션 참고).\n- **T4 메모리:** context가 수만 행이면 OOM이 날 수 있습니다. `size=\"small\"` 또는 `\"medium\"`으로 바꾸거나, `num_estimators`를 줄이거나, context를 subsample하세요.\n\n**참고 링크**\n- 모델 카드: https://huggingface.co/nvidia/Kumo-Tabular\n- 라이브러리: https://github.com/NVIDIA/structured-data-models\n- 문서: https://nvidia.github.io/structured-data-models\n- 공식 블로그: https://huggingface.co/blog/nvidia/kumo-tabular\n- 라이선스: 가중치 OpenMDW-1.1(상업적 사용 가능), 코드 Apache-2.0"
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "provenance": [],
   "gpuType": "T4"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}