0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

放物線とポケモン

0
Posted at
# Program Name: pokemon_parabola_sim_colab.py
# Creation Date: 2026-01-22
# Purpose: Interactive simulation with PDF export capability

# Target Simulation
# PokeAPI GET https://pokeapi.co/api/v2/pokemon/{id}/ で図鑑No.1〜151の種族値を取得し、
# 選択した2変数 (x, y) の関係を放物線(2次式) y_hat = a*x^2 + b*x + c で当てはめる。
# 結果を可視化し、予測値と誤差指標を print で表示し、PDFで保存・DLできる。

# ============================================================
# 1. README(print関数で出力:日本語。数式は LaTeX 禁止、プレーンテキストのみ)
# ============================================================

def print_documentation():
    txt = []
    txt.append("============================================================")
    txt.append("README / 使い方(日本語、数式はプレーンテキスト)")
    txt.append("============================================================")
    txt.append("目的:")
    txt.append("  ・図鑑No.1〜151の種族値を取得し、2変数の関係を放物線で近似する。")
    txt.append("  ・理論理解/検証/感度解析: 軸や正則化を変えて係数と誤差の変化を確認する。")
    txt.append("")
    txt.append("支配方程式(Theoretical Model):")
    txt.append("  y_hat = a*x^2 + b*x + c")
    txt.append("  係数ベクトル theta = [a, b, c]^T")
    txt.append("  最小二乗 + リッジ正則化(eps):")
    txt.append("    theta = (Phi^T Phi + eps*I)^(-1) Phi^T y")
    txt.append("    Phi = [x^2, x, 1]")
    txt.append("")
    txt.append("変数定義:")
    txt.append("  ・データ: 図鑑ID start_id..end_id の種族値")
    txt.append("  ・軸インデックス:")
    txt.append("      0: HP, 1: Attack, 2: Defense, 3: SpAtk, 4: SpDef, 5: Speed, 6: Total")
    txt.append("")
    txt.append("出力(print):")
    txt.append("  ・放物線係数 a,b,c / RMSE / 予測例(指定x_query)")
    txt.append("")
    txt.append("操作:")
    txt.append("  1) [Model Parameters] で eps を設定")
    txt.append("  2) [Observed / Input Data] で取得範囲と axis_x/axis_y を設定")
    txt.append("  3) Run Simulation / 実行 を押す(自動更新なし)")
    txt.append("  4) Save PDF / PDF保存 で図をPDF保存しDL")
    txt.append("  5) Show Log / 履歴表示, Clear Log / 履歴消去, Reset / リセット")
    txt.append("============================================================")
    print("\n".join(txt))


# ============================================================
# 2. Setup Code(from google.colab import files を含める。pip install 実行文字列はコメントに記述 / import / seed設定)
# ============================================================

# pip install requests numba ipywidgets matplotlib numpy  # (必要なら) / (if needed)

from google.colab import files  # 必須 / Mandatory
import time
import json
import math
import datetime
from typing import Dict, List, Tuple, Optional

import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display, clear_output

try:
    import requests
except Exception as e:
    raise RuntimeError("requests が import できません。pip install requests を実行してください。 / requests import failed. Please pip install requests.") from e

try:
    from numba import njit
except Exception as e:
    raise RuntimeError("numba が import できません。pip install numba を実行してください。 / numba import failed. Please pip install numba.") from e

plt.style.use('seaborn-v0_8-muted')  # 必須 / Mandatory

SEED_DEFAULT = 12345
np.random.seed(SEED_DEFAULT)

POKEAPI_POKEMON_URL = "https://pokeapi.co/api/v2/pokemon/{id}/"

STAT_KEYS = ["hp", "attack", "defense", "special-attack", "special-defense", "speed"]
ALL_LABELS = ["HP", "Attack", "Defense", "SpAtk", "SpDef", "Speed", "Total"]


# ============================================================
# 3. TheoreticalModel(計算・検証・計算過程の表示:数式はプレーンテキストのみ)
# ============================================================

def print_calculation_steps(params: Dict, results: Dict):
    lines = []
    lines.append("------------------------------------------------------------")
    lines.append("計算過程 / Calculation Steps")
    lines.append("------------------------------------------------------------")
    lines.append("[放物線近似 / Parabola Fit]")
    lines.append("公式 / Formula:")
    lines.append("  y_hat = a*x^2 + b*x + c")
    lines.append("  theta = (Phi^T Phi + eps*I)^(-1) Phi^T y")
    lines.append("  Phi = [x^2, x, 1]")
    lines.append(f"軸 / Axes: x={ALL_LABELS[int(params['axis_x'])]}, y={ALL_LABELS[int(params['axis_y'])]}")
    a, b, c = results["a"], results["b"], results["c"]
    lines.append(f"係数 / Coeff: a={a:.8f}, b={b:.8f}, c={c:.8f}")
    lines.append(f"RMSE / RMSE: {results['rmse']:.6f} [{ALL_LABELS[int(params['axis_y'])]}]")
    lines.append(f"予測例 / Example: x_query={results['x_query']:.4f} -> y_hat={results['y_query_hat']:.4f}")
    lines.append("------------------------------------------------------------")
    print("\n".join(lines))


def validate(params: Dict) -> Tuple[bool, List[str]]:
    msgs = []
    ok = True

    start_id = int(params.get("start_id", 1))
    end_id = int(params.get("end_id", 151))
    if not (1 <= start_id <= 151 and 1 <= end_id <= 151 and start_id <= end_id):
        ok = False
        msgs.append("警告: 図鑑ID範囲が不正(1..151, start<=end)。 / Warning: Invalid Pokedex range (1..151, start<=end).")

    timeout_s = float(params.get("timeout_s", 10.0))
    if timeout_s <= 0:
        ok = False
        msgs.append("警告: timeout_s は正である必要があります。 / Warning: timeout_s must be positive.")

    sleep_s = float(params.get("sleep_s", 0.05))
    if sleep_s < 0:
        ok = False
        msgs.append("警告: sleep_s は0以上である必要があります。 / Warning: sleep_s must be >= 0.")

    eps = float(params.get("ridge_eps", 1e-6))
    if eps < 0:
        ok = False
        msgs.append("警告: ridge_eps は0以上である必要があります。 / Warning: ridge_eps must be >= 0.")

    axis_x = int(params.get("axis_x", 5))
    axis_y = int(params.get("axis_y", 1))
    if not (0 <= axis_x <= 6 and 0 <= axis_y <= 6):
        ok = False
        msgs.append("警告: axis_x/axis_y は 0..6 で指定してください。 / Warning: axis_x/axis_y must be in 0..6.")
    if axis_x == axis_y:
        msgs.append("注意: axis_x と axis_y が同じです(近似は可能)。 / Note: axis_x equals axis_y (fit is still possible).")

    return ok, msgs


def fetch_pokemon_one(pokedex_id: int, timeout_s: float) -> Optional[Dict]:
    url = POKEAPI_POKEMON_URL.format(id=pokedex_id)
    try:
        r = requests.get(url, timeout=timeout_s)
        r.raise_for_status()
        data = r.json()
        name = data.get("name", "")

        stats = {k: 0 for k in STAT_KEYS}
        for s in data.get("stats", []):
            k = s["stat"]["name"]
            if k in stats:
                stats[k] = int(s["base_stat"])

        hp = stats["hp"]
        atk = stats["attack"]
        df = stats["defense"]
        spa = stats["special-attack"]
        spd = stats["special-defense"]
        spe = stats["speed"]
        total = int(hp + atk + df + spa + spd + spe)

        return {
            "id": int(pokedex_id),
            "name": str(name),
            "HP": int(hp),
            "Attack": int(atk),
            "Defense": int(df),
            "SpAtk": int(spa),
            "SpDef": int(spd),
            "Speed": int(spe),
            "Total": int(total),
        }
    except Exception as e:
        print(f"取得失敗 / Fetch failed: id={pokedex_id}, err={e}")
        return None


def fetch_gen1_stats(start_id: int, end_id: int, timeout_s: float, sleep_s: float,
                     use_cache: bool, cache: Dict[int, Dict]) -> List[Dict]:
    rows = []
    for i in range(start_id, end_id + 1):
        if use_cache and (i in cache):
            rows.append(cache[i])
            continue
        row = fetch_pokemon_one(i, timeout_s)
        if row is not None:
            cache[i] = row
            rows.append(row)
        time.sleep(sleep_s)
    return rows


def rows_to_matrix(rows: List[Dict]) -> np.ndarray:
    # columns: HP, Attack, Defense, SpAtk, SpDef, Speed, Total
    n = len(rows)
    M = np.zeros((n, 7), dtype=np.float64)
    for i, r in enumerate(rows):
        M[i, 0] = float(r["HP"])
        M[i, 1] = float(r["Attack"])
        M[i, 2] = float(r["Defense"])
        M[i, 3] = float(r["SpAtk"])
        M[i, 4] = float(r["SpDef"])
        M[i, 5] = float(r["Speed"])
        M[i, 6] = float(r["Total"])
    return M


def parabola_fit_ridge(x: np.ndarray, y: np.ndarray, ridge_eps: float) -> Tuple[float, float, float]:
    # theta = (Phi^T Phi + eps*I)^(-1) Phi^T y, Phi=[x^2,x,1]
    x = x.astype(np.float64)
    y = y.astype(np.float64)
    Phi = np.vstack([x*x, x, np.ones_like(x)]).T  # (n,3)
    A = Phi.T @ Phi
    if ridge_eps > 0:
        A = A + ridge_eps * np.eye(3)
    b = Phi.T @ y
    theta = np.linalg.solve(A, b)
    return float(theta[0]), float(theta[1]), float(theta[2])


@njit
def parabola_predict(a: float, b: float, c: float, x: float) -> float:
    return a*x*x + b*x + c


def run_theoretical_model(params: Dict, cache: Dict[int, Dict]) -> Dict:
    ok, msgs = validate(params)
    for m in msgs:
        print(m)
    if not ok:
        return {"ok": False, "error": "validation_failed"}

    start_id = int(params["start_id"])
    end_id = int(params["end_id"])
    timeout_s = float(params["timeout_s"])
    sleep_s = float(params["sleep_s"])
    use_cache = bool(int(params["use_cache"]))
    ridge_eps = float(params["ridge_eps"])
    axis_x = int(params["axis_x"])
    axis_y = int(params["axis_y"])
    x_query = float(params["x_query"])

    try:
        rows = fetch_gen1_stats(start_id, end_id, timeout_s, sleep_s, use_cache, cache)
        if len(rows) < 5:
            print("警告: データ数が不足しています(最低5件)。 / Warning: Not enough data (need at least 5).")
            return {"ok": False, "error": "not_enough_data"}

        M = rows_to_matrix(rows)
        x = M[:, axis_x]
        y = M[:, axis_y]

        a, b, c = parabola_fit_ridge(x, y, ridge_eps=ridge_eps)
        yhat = a*x*x + b*x + c
        rmse = float(np.sqrt(np.mean((yhat - y) ** 2)))

        y_query_hat = float(parabola_predict(a, b, c, x_query))

        # print table (simplified) / 一覧(簡略)
        print("------------------------------------------------------------")
        print("一覧(実測と予測) / List (true vs predicted)")
        print("------------------------------------------------------------")
        xlab = ALL_LABELS[axis_x]
        ylab = ALL_LABELS[axis_y]
        print(f"No  Name            {xlab:<7}  {ylab:<7}  {ylab}_hat")
        print("------------------------------------------------------------")
        for i, r in enumerate(rows):
            xi = x[i]
            yi = y[i]
            yhi = yhat[i]
            print(f"{r['id']:>3}  {r['name']:<14}  {xi:>7.1f}  {yi:>7.1f}  {yhi:>7.2f}")
        print("------------------------------------------------------------")
        print(f"式 / Equation: {ylab}_hat = a*{xlab}^2 + b*{xlab} + c")
        print(f"a={a:.8f}, b={b:.8f}, c={c:.8f}")
        print(f"RMSE={rmse:.6f} [{ylab}]")
        print(f"予測 / Prediction: x_query={x_query:.4f} -> {ylab}_hat={y_query_hat:.4f}")
        print("------------------------------------------------------------")

        results = {
            "ok": True,
            "rows": rows,
            "M": M,
            "x": x,
            "y": y,
            "yhat": yhat,
            "a": a, "b": b, "c": c,
            "rmse": rmse,
            "x_query": x_query,
            "y_query_hat": y_query_hat,
            "axis_x": axis_x,
            "axis_y": axis_y,
        }

        print_calculation_steps(params, results)
        return results

    except Exception as e:
        print(f"例外発生 / Exception: {e}")
        return {"ok": False, "error": str(e)}


# ============================================================
# 4. Visualizer(matplotlib:ghost line+最低2プロット同期更新)
# ============================================================

class VizState:
    def __init__(self):
        self.fig = None
        self.ax1 = None
        self.ax2 = None
        self.last = None
        self.ghost_alpha = 0.15


def _auto_log_scale(x: np.ndarray) -> bool:
    xmin = float(np.min(x))
    xmax = float(np.max(x))
    if xmin <= 0:
        return False
    return (xmax / max(xmin, 1e-12)) >= 1000


def update_plots(viz: VizState, results: Dict):
    x = results["x"]
    y = results["y"]
    yhat = results["yhat"]
    a, b, c = results["a"], results["b"], results["c"]
    axis_x = int(results["axis_x"])
    axis_y = int(results["axis_y"])
    xlab = ALL_LABELS[axis_x]
    ylab = ALL_LABELS[axis_y]

    if viz.fig is None:
        viz.fig, (viz.ax1, viz.ax2) = plt.subplots(1, 2, figsize=(14, 5))

    # ghost (previous fit) / ゴースト(前回結果)
    if viz.last is not None:
        L = viz.last
        viz.ax1.scatter(L["x"], L["y"], s=20, alpha=viz.ghost_alpha, label="_ghost")
        viz.ax1.plot(L["xs"], L["ys_fit"], alpha=viz.ghost_alpha, label="_ghost")
        viz.ax2.scatter(L["x"], L["resid"], s=20, alpha=viz.ghost_alpha, label="_ghost")
        viz.ax2.axhline(0.0, alpha=viz.ghost_alpha)

    viz.ax1.cla()
    viz.ax2.cla()
    viz.ax1.grid(True)
    viz.ax2.grid(True)

    # Plot1: scatter + parabola curve / 主結果
    viz.ax1.scatter(x, y, s=25, label="Samples")
    xs = np.linspace(float(np.min(x)), float(np.max(x)), 400)
    ys_fit = a*xs*xs + b*xs + c
    viz.ax1.plot(xs, ys_fit, linewidth=2.5, label="Parabola fit")

    viz.ax1.set_title("Plot1: Parabola fit")
    viz.ax1.set_xlabel(f"{xlab} [a.u.]")
    viz.ax1.set_ylabel(f"{ylab} [a.u.]")
    if _auto_log_scale(x):
        viz.ax1.set_xscale("log")
    if _auto_log_scale(y):
        viz.ax1.set_yscale("log")
    viz.ax1.legend()

    # Plot2: residuals / 補助指標(残差)
    resid = y - yhat
    viz.ax2.scatter(x, resid, s=25, label="Residual = y - y_hat")
    viz.ax2.axhline(0.0, linewidth=1.5, label="Zero")
    viz.ax2.set_title("Plot2: Residuals")
    viz.ax2.set_xlabel(f"{xlab} [a.u.]")
    viz.ax2.set_ylabel(f"Residual [{ylab}]")
    if _auto_log_scale(x):
        viz.ax2.set_xscale("log")
    viz.ax2.legend()

    viz.fig.tight_layout()
    plt.show()

    viz.last = {"x": x.copy(), "y": y.copy(), "xs": xs.copy(), "ys_fit": ys_fit.copy(), "resid": resid.copy()}


# ============================================================
# 5. UIBuilder(ipywidgets:FloatText等のみ+Run/Reset/Save_PDF/ShowLog/ClearLog)
# ============================================================

class SimulatorUI:
    def __init__(self):
        self.cache: Dict[int, Dict] = {}
        self.log: List[Dict] = []
        self.viz = VizState()
        self.out_main = widgets.Output()
        self.out_plot = widgets.Output()

        # ---------------------------
        # [Model Parameters]
        # ---------------------------
        self.ridge_eps = widgets.BoundedFloatText(
            value=1e-6, min=0.0, max=1.0, step=1e-6,
            description="ridge_eps",
            layout=widgets.Layout(width="340px")
        )
        self.axis_x = widgets.IntText(
            value=5, description="axis_x (0..6)",
            layout=widgets.Layout(width="340px")
        )
        self.axis_y = widgets.IntText(
            value=1, description="axis_y (0..6)",
            layout=widgets.Layout(width="340px")
        )
        self.x_query = widgets.BoundedFloatText(
            value=80.0, min=-1e6, max=1e6, step=1.0,
            description="x_query",
            layout=widgets.Layout(width="340px")
        )

        # ---------------------------
        # [Observed / Input Data]
        # ---------------------------
        self.start_id = widgets.IntText(value=1, description="start_id", layout=widgets.Layout(width="340px"))
        self.end_id = widgets.IntText(value=151, description="end_id", layout=widgets.Layout(width="340px"))

        # ---------------------------
        # [Environment / Options]
        # ---------------------------
        self.timeout_s = widgets.BoundedFloatText(value=10.0, min=0.1, max=120.0, step=0.1, description="timeout_s", layout=widgets.Layout(width="340px"))
        self.sleep_s = widgets.BoundedFloatText(value=0.05, min=0.0, max=2.0, step=0.01, description="sleep_s", layout=widgets.Layout(width="340px"))
        self.use_cache = widgets.IntText(value=1, description="use_cache(0/1)", layout=widgets.Layout(width="340px"))
        self.seed = widgets.IntText(value=SEED_DEFAULT, description="seed", layout=widgets.Layout(width="340px"))

        # Buttons
        self.btn_run = widgets.Button(description="Run Simulation / 実行", button_style="success")
        self.btn_reset = widgets.Button(description="Reset / リセット", button_style="warning")
        self.btn_save = widgets.Button(description="Save PDF / PDF保存", button_style="info")
        self.btn_showlog = widgets.Button(description="Show Log / 履歴表示", button_style="")
        self.btn_clearlog = widgets.Button(description="Clear Log / 履歴消去", button_style="danger")

        self.btn_run.on_click(self.on_run)
        self.btn_reset.on_click(self.on_reset)
        self.btn_save.on_click(self.on_save_pdf)
        self.btn_showlog.on_click(self.on_show_log)
        self.btn_clearlog.on_click(self.on_clear_log)

        self.ui = self._build_ui()

    def _collect_params(self) -> Dict:
        return {
            "ridge_eps": float(self.ridge_eps.value),
            "axis_x": int(self.axis_x.value),
            "axis_y": int(self.axis_y.value),
            "x_query": float(self.x_query.value),

            "start_id": int(self.start_id.value),
            "end_id": int(self.end_id.value),

            "timeout_s": float(self.timeout_s.value),
            "sleep_s": float(self.sleep_s.value),
            "use_cache": int(self.use_cache.value),
            "seed": int(self.seed.value),
        }

    def _build_ui(self):
        box_model = widgets.VBox([
            widgets.HTML("<b>[Model Parameters]</b>"),
            widgets.HTML("<i>Axis mapping: 0:HP 1:Attack 2:Defense 3:SpAtk 4:SpDef 5:Speed 6:Total</i>"),
            self.ridge_eps,
            self.axis_x,
            self.axis_y,
            self.x_query,
        ])
        box_data = widgets.VBox([
            widgets.HTML("<b>[Observed / Input Data]</b>"),
            self.start_id,
            self.end_id,
        ])
        box_env = widgets.VBox([
            widgets.HTML("<b>[Environment / Options]</b>"),
            self.timeout_s,
            self.sleep_s,
            self.use_cache,
            self.seed,
        ])
        box_buttons = widgets.HBox([self.btn_run, self.btn_reset, self.btn_save, self.btn_showlog, self.btn_clearlog])
        return widgets.VBox([widgets.HBox([box_model, box_data, box_env]), box_buttons, self.out_main, self.out_plot])

    def on_run(self, _):
        with self.out_main:
            clear_output(wait=True)
            params = self._collect_params()
            np.random.seed(int(params["seed"]))

            ts = datetime.datetime.now().isoformat(timespec="seconds")
            print(f"実行開始 / Run started: {ts}")

            results = run_theoretical_model(params, cache=self.cache)
            if not results.get("ok", False):
                print(f"失敗 / Failed: {results.get('error', 'unknown')}")
                return

            key_outputs = {
                "a": float(results["a"]),
                "b": float(results["b"]),
                "c": float(results["c"]),
                "rmse": float(results["rmse"]),
                "axis_x": int(params["axis_x"]),
                "axis_y": int(params["axis_y"]),
            }
            self.log.append({"timestamp": ts, "params": params, "key_outputs": key_outputs})
            print("ログ保存 / Log saved")

            with self.out_plot:
                clear_output(wait=True)
                update_plots(self.viz, results)

    def on_reset(self, _):
        self.ridge_eps.value = 1e-6
        self.axis_x.value = 5
        self.axis_y.value = 1
        self.x_query.value = 80.0

        self.start_id.value = 1
        self.end_id.value = 151

        self.timeout_s.value = 10.0
        self.sleep_s.value = 0.05
        self.use_cache.value = 1
        self.seed.value = SEED_DEFAULT

        self.log = []
        self.viz = VizState()
        with self.out_plot:
            clear_output(wait=True)
        with self.out_main:
            clear_output(wait=True)
            print("リセット完了 / Reset completed")
            print("入力初期化・ゴースト消去・ログ消去を実行しました。 / Inputs, ghost, and logs were cleared.")

    def on_save_pdf(self, _):
        with self.out_main:
            if self.viz.fig is None:
                print("警告: 保存する図がありません。 / Warning: No figure to save.")
                return
            ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = f"parabola_result_{ts}.pdf"
            try:
                self.viz.fig.savefig(filename, format="pdf")
                print(f"PDF保存完了 / PDF saved: {filename}")
                print("ダウンロード開始 / Starting download")
                files.download(filename)
            except Exception as e:
                print(f"保存失敗 / Save failed: {e}")

    def on_show_log(self, _):
        with self.out_main:
            print("------------------------------------------------------------")
            print("履歴表示 / Show Log")
            print("------------------------------------------------------------")
            if len(self.log) == 0:
                print("履歴が空です。 / Log is empty.")
                return
            for i, item in enumerate(self.log, 1):
                print(f"[{i}] timestamp={item['timestamp']}")
                print("  params=" + json.dumps(item["params"], ensure_ascii=False))
                print("  key_outputs=" + json.dumps(item["key_outputs"], ensure_ascii=False))
            print("------------------------------------------------------------")

    def on_clear_log(self, _):
        self.log = []
        with self.out_main:
            print("履歴消去完了 / Log cleared")


# ============================================================
# 6. Initialization(初回 print_documentation 呼び出し+UI起動)
# ============================================================

print_documentation()
ui_app = SimulatorUI()
display(ui_app.ui)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?