# 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)
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme