{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# 표 파운데이션 모델과 소재 데이터 실습\n",
        "\n",
        "합성 데이터 실습입니다. 실제 소재 물성을 나타내지 않습니다. 기본 모델은 실행 검증했으며 TabPFN 추론은 미검증입니다. 전체 해설은 함께 제공한 Markdown을 참고하세요."
      ],
      "id": "cell-00"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 1 설치\n",
        "아래 셀은 필요할 때만 실행합니다. TabPFN은 별도로 설치하며 최초 사용 시 체크포인트 다운로드가 필요할 수 있습니다."
      ],
      "id": "cell-01"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# %pip install numpy pandas scikit-learn\n",
        "# %pip install tabpfn"
      ],
      "id": "cell-02"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 2 데이터와 평가 함수\n",
        "이 셀은 독립 실행에 필요한 전체 함수를 정의합니다. 파일 다운로드나 학습은 아직 하지 않습니다."
      ],
      "id": "cell-03"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "\"\"\"교육용 합성 소재 데이터 비교. 기본 실행은 TabPFN 다운로드 없이 동작합니다.\"\"\"\n",
        "import argparse\n",
        "import json\n",
        "import platform\n",
        "from importlib.metadata import version, PackageNotFoundError\n",
        "from pathlib import Path\n",
        "from time import perf_counter\n",
        "\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "from sklearn.base import clone\n",
        "from sklearn.dummy import DummyRegressor\n",
        "from sklearn.ensemble import RandomForestRegressor, HistGradientBoostingRegressor\n",
        "from sklearn.gaussian_process import GaussianProcessRegressor\n",
        "from sklearn.gaussian_process.kernels import ConstantKernel, Matern, WhiteKernel\n",
        "from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score\n",
        "from sklearn.model_selection import train_test_split\n",
        "from sklearn.pipeline import make_pipeline\n",
        "from sklearn.preprocessing import StandardScaler\n",
        "\n",
        "\n",
        "def make_materials(n=240, seed=42):\n",
        "    \"\"\"실제 NCM 물성식이 아닙니다. 단위와 변수만 소재 실험을 본뜬 장난감입니다.\"\"\"\n",
        "    rng = np.random.default_rng(seed)\n",
        "    ni = rng.uniform(0.50, 0.95, n)\n",
        "    temp = rng.uniform(680, 850, n)\n",
        "    hours = rng.uniform(6, 20, n)\n",
        "    X = pd.DataFrame({\"ni_fraction\": ni, \"temperature_C\": temp, \"hours\": hours})\n",
        "    y = (160 + 65 * (ni - 0.5) - 0.002 * (temp - 770)**2\n",
        "         + 5 * np.log(hours / 6) + 0.08 * (ni - 0.7) * (temp - 770)\n",
        "         + rng.normal(0, 3, n))\n",
        "    return X, pd.Series(y, name=\"capacity_mAh_g\")\n",
        "\n",
        "\n",
        "def make_models():\n",
        "    kernel = ConstantKernel(1.0, (0.01, 100)) * Matern(\n",
        "        length_scale=np.ones(3), length_scale_bounds=(0.01, 100), nu=2.5\n",
        "    ) + WhiteKernel(0.1, (1e-5, 10))\n",
        "    return {\n",
        "        \"Mean\": DummyRegressor(),\n",
        "        \"RandomForest\": RandomForestRegressor(n_estimators=200, min_samples_leaf=2,\n",
        "                                               random_state=42, n_jobs=1),\n",
        "        \"HistGBDT\": HistGradientBoostingRegressor(max_iter=150, max_leaf_nodes=15,\n",
        "                                                  min_samples_leaf=10, random_state=42),\n",
        "        \"GPR\": make_pipeline(StandardScaler(), GaussianProcessRegressor(\n",
        "            kernel=kernel, normalize_y=True, random_state=42)),\n",
        "    }\n",
        "\n",
        "\n",
        "def run_benchmark(include_tabpfn=False, device=\"cpu\", seeds=(42, 43, 44)):\n",
        "    X, y = make_materials()\n",
        "    idx = np.arange(len(X))\n",
        "    splits = [(f\"random_{s}\", *train_test_split(idx, test_size=0.25, random_state=s))\n",
        "              for s in seeds]\n",
        "    # 평가 전에 임계값을 고정하고 고Ni 영역을 학습에서 제외합니다。\n",
        "    splits.append((\"high_Ni_holdout\", idx[X.ni_fraction < 0.84], idx[X.ni_fraction >= 0.84]))\n",
        "    rows = []\n",
        "    for split, train, test in splits:\n",
        "        models = {name: clone(model) for name, model in make_models().items()}\n",
        "        if include_tabpfn:\n",
        "            from tabpfn import TabPFNRegressor\n",
        "            from tabpfn.constants import ModelVersion\n",
        "            # 논문 버전과 현재 기본값의 혼동을 줄이기 위해 v2를 지정합니다.\n",
        "            models[\"TabPFN_v2\"] = TabPFNRegressor.create_default_for_version(\n",
        "                ModelVersion.V2, device=device)\n",
        "        for name, model in models.items():\n",
        "            start = perf_counter()\n",
        "            model.fit(X.iloc[train], y.iloc[train])\n",
        "            fit_s = perf_counter() - start\n",
        "            start = perf_counter()\n",
        "            pred = model.predict(X.iloc[test])\n",
        "            predict_s = perf_counter() - start\n",
        "            rows.append(dict(split=split, model=name, n_train=len(train), n_test=len(test),\n",
        "                             MAE=mean_absolute_error(y.iloc[test], pred),\n",
        "                             RMSE=float(np.sqrt(mean_squared_error(y.iloc[test], pred))),\n",
        "                             R2=r2_score(y.iloc[test], pred), fit_s=fit_s, predict_s=predict_s))\n",
        "    return pd.DataFrame(rows), X, y\n",
        "\n",
        "\n",
        "def environment():\n",
        "    result = {\"python\": platform.python_version(), \"platform\": platform.platform()}\n",
        "    for name in [\"numpy\", \"pandas\", \"scikit-learn\", \"torch\", \"tabpfn\"]:\n",
        "        try:\n",
        "            result[name] = version(name)\n",
        "        except PackageNotFoundError:\n",
        "            result[name] = \"not installed\"\n",
        "    return result\n",
        "\n",
        "\n"
      ],
      "id": "cell-04"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 3 기본 모델 비교\n",
        "동일한 분할을 모든 모델에 적용합니다. GPR 최적화 경고가 나오면 결과 해석에 기록하세요."
      ],
      "id": "cell-05"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "scores, X, y = run_benchmark()\n",
        "display(X.assign(capacity_mAh_g=y).head())\n",
        "display(scores.round(3))"
      ],
      "id": "cell-06"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 4 분할별 평균 오차\n",
        "무작위 분할 세 번은 같은 데이터의 재분할입니다. 독립 데이터셋 세 개의 결과가 아닙니다."
      ],
      "id": "cell-07"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "summary = scores.assign(scenario=scores[\"split\"].str.replace(r\"random_\\d+\", \"random\", regex=True))\n",
        "display(summary.groupby([\"scenario\", \"model\"])[[\"MAE\", \"RMSE\", \"R2\"]].mean().round(3))"
      ],
      "id": "cell-08"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 5 TabPFN 추가\n",
        "아래 스위치를 True로 바꾸면 TabPFN v2를 포함해 전체 비교를 실행합니다. CPU 또는 준비된 CUDA 환경을 선택하세요. 이 단계는 작성 환경에서 실행하지 않았습니다."
      ],
      "id": "cell-09"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "RUN_TABPFN = False\n",
        "if RUN_TABPFN:\n",
        "    scores_tabpfn, _, _ = run_benchmark(include_tabpfn=True, device=\"cpu\")\n",
        "    display(scores_tabpfn.round(3))\n",
        "else:\n",
        "    print(\"TabPFN 비교를 건너뜁니다. 설치 후 RUN_TABPFN=True로 변경하세요.\")"
      ],
      "id": "cell-10"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 6 실행 환경 기록\n",
        "결과와 함께 사용 패키지 및 모델 버전을 남기세요."
      ],
      "id": "cell-11"
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "print(json.dumps(environment(), indent=2))"
      ],
      "id": "cell-12"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 생각해 볼 점\n",
        "\n",
        "1. 고Ni 영역을 제외하면 오차가 왜 커질까요?\n",
        "2. 매끄러운 합성 함수에서 GPR이 유리한 결과를 실제 데이터로 일반화할 수 있을까요?\n",
        "3. 반복 측정이 학습과 평가에 섞이면 어떤 문제가 생길까요?\n",
        "\n",
        "[TabPFN 공식 코드](https://github.com/PriorLabs/TabPFN) · [원리 논문](https://arxiv.org/abs/2207.01848) · [TabICL](https://arxiv.org/abs/2502.05564)"
      ],
      "id": "cell-13"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.12"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}