"""tksynthetic.py
最小二乗プログラムの動作確認用の内部生成データユーティリティ。
:doc:`tksynthetic_usage`
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Mapping, Optional, Sequence, Union
import csv
import numpy as np
ArrayLike = Sequence[float] | np.ndarray
[ドキュメント]
@dataclass
class SyntheticData:
"""合成データとその生成情報を保持するデータクラス。
概要:
合成データとその生成情報を保持するデータクラス。
詳細説明:
x, y (ノイズ付き), y_clean (ノイズなし), y_noise (ノイズ成分) などのデータ配列に加え、
データ生成に使用された真のパラメータやノイズの標準偏差などの情報もカプセル化する。
:param x: x軸のデータ配列。
:type x: numpy.ndarray
:param y: ノイズを含むy軸のデータ配列。
:type y: numpy.ndarray
:param y_clean: ノイズを含まない真のy軸データ配列。
:type y_clean: numpy.ndarray
:param y_noise: yに加算されたノイズ成分のデータ配列。
:type y_noise: numpy.ndarray
:param true_params: データ生成に使用された真のモデルパラメータを格納した辞書。
:type true_params: dict
:param noise_std: データ生成に使用されたノイズの標準偏差。
:type noise_std: float
:param seed: 乱数生成に使用されたシード値。指定がない場合はNone。
:type seed: Optional[int]
"""
x: np.ndarray
y: np.ndarray
y_clean: np.ndarray
y_noise: np.ndarray
true_params: dict
noise_std: float
seed: Optional[int] = None
[ドキュメント]
def make_x_grid(
xmin: float = 0.0,
xmax: float = 1.0,
n: int = 50,
*,
kind: str = "linear",
) -> np.ndarray:
"""線形または対数スケールのxグリッド配列を生成する。
概要:
指定された範囲と要素数で、線形または対数スケールのxグリッド配列を生成する。
詳細説明:
'linear'を指定するとnp.linspaceを用いて等間隔のグリッドを生成し、
'log'を指定するとnp.geomspaceを用いて対数等間隔のグリッドを生成する。
'log'の場合はxminとxmaxが正である必要がある。
:param xmin: グリッドの最小値。
:type xmin: float
:param xmax: グリッドの最大値。
:type xmax: float
:param n: グリッドの要素数。
:type n: int
:param kind: グリッドの種類。'linear' (線形) または 'log' (対数) を指定。
:type kind: str
:raises ValueError: kindが'linear'または'log'ではない場合、または'log'でxmin/xmaxが0以下の場合。
:returns: 生成されたxグリッド配列。
:rtype: numpy.ndarray
"""
if kind == "linear":
return np.linspace(float(xmin), float(xmax), int(n))
if kind == "log":
if xmin <= 0 or xmax <= 0:
raise ValueError("log grid requires xmin > 0 and xmax > 0")
return np.geomspace(float(xmin), float(xmax), int(n))
raise ValueError("kind must be 'linear' or 'log'")
[ドキュメント]
def generate_noisy_data(
model_func: Callable[[np.ndarray, Mapping[str, float]], ArrayLike],
x: ArrayLike,
true_params: Mapping[str, float],
*,
noise_std: float = 0.05,
seed: Optional[int] = 0,
noise: str = "normal",
relative_noise: bool = False,
) -> SyntheticData:
"""モデル関数からノイズ付きデータを生成する。
概要:
与えられたモデル関数と真のパラメータに基づいて、指定されたタイプのノイズを含む合成データを生成する。
詳細説明:
1. モデル関数 `model_func(x_array, true_params)` を使用して、ノイズを含まない `y_clean` を計算します。
2. 指定された `noise_std` と `seed` を用いて、正規分布または一様分布に従うノイズ `eps` を生成します。
3. `relative_noise` が `True` の場合、ノイズは `y_clean` の絶対値に比例して適用されます。
4. `y_clean` にノイズを加算して、最終的なノイズ付きデータ `y` を生成します。
5. 生成されたすべてのデータを `SyntheticData` オブジェクトとして返します。
:param model_func: `y_clean = model_func(x_array, true_params)` の形式で、x配列とパラメータ辞書を受け取り、y配列を返すCallableオブジェクト。
:type model_func: Callable[[numpy.ndarray, Mapping[str, float]], ArrayLike]
:param x: x軸の入力データ。numpy.ndarrayまたはfloatのシーケンス。
:type x: ArrayLike
:param true_params: モデル関数の真のパラメータを格納した辞書。
:type true_params: Mapping[str, float]
:param noise_std: ノイズの標準偏差。
:type noise_std: float
:param seed: 乱数生成のシード値。同じシード値を使用すると再現可能なノイズが生成される。Noneの場合は非再現的。
:type seed: Optional[int]
:param noise: 生成するノイズのタイプ。'normal' (正規分布) または 'uniform' (一様分布) を指定。
:type noise: str
:param relative_noise: ノイズを相対的に適用するかどうか。Trueの場合、ノイズはy_cleanの絶対値に比例してスケールされる。
:type relative_noise: bool
:raises ValueError: model_funcの出力サイズとxのサイズが一致しない場合、またはnoiseが'normal'でも'uniform'でもない場合。
:returns: 生成された合成データと関連情報を含むSyntheticDataオブジェクト。
:rtype: SyntheticData
"""
rng = np.random.default_rng(seed)
x_arr = np.asarray(x, dtype=float).reshape(-1)
p = {k: float(v) for k, v in true_params.items()}
y_clean = np.asarray(model_func(x_arr, p), dtype=float).reshape(-1)
if y_clean.size != x_arr.size:
raise ValueError("model_func output size and x size mismatch")
if noise == "normal":
eps = rng.normal(0.0, float(noise_std), size=x_arr.size)
elif noise == "uniform":
half_width = np.sqrt(3.0) * float(noise_std)
eps = rng.uniform(-half_width, half_width, size=x_arr.size)
else:
raise ValueError("noise must be 'normal' or 'uniform'")
if relative_noise:
y_noise = eps * np.maximum(np.abs(y_clean), np.finfo(float).eps)
else:
y_noise = eps
y = y_clean + y_noise
return SyntheticData(
x=x_arr,
y=y,
y_clean=y_clean,
y_noise=y_noise,
true_params=p,
noise_std=float(noise_std),
seed=seed,
)
[ドキュメント]
def generate_replicates(
model_func: Callable[[np.ndarray, Mapping[str, float]], ArrayLike],
x: ArrayLike,
true_params: Mapping[str, float],
*,
noise_std: float = 0.05,
seed: Optional[int] = 0,
n_replicates: int = 3,
) -> SyntheticData:
"""同じx値に対する繰り返し測定(レプリケート)をシミュレートしたノイズ付きデータを生成する。
概要:
与えられたx値とモデル関数に対して、複数回の繰り返し測定に見立てたノイズ付きデータを生成する。
詳細説明:
この関数は、単一のxデータセットとモデル関数から `y_clean` を計算し、
その `y_clean` に `n_replicates` 回独立した正規ノイズを加えることで、
繰り返し測定のデータセットをシミュレートします。
結果として、x軸のデータは複製され、各x値に対応する複数のy値が存在するデータセットが生成されます。
:param model_func: `y_clean = model_func(x_array, true_params)` の形式で、x配列とパラメータ辞書を受け取り、y配列を返すCallableオブジェクト。
:type model_func: Callable[[numpy.ndarray, Mapping[str, float]], ArrayLike]
:param x: x軸の入力データ(レプリケート元)。numpy.ndarrayまたはfloatのシーケンス。
:type x: ArrayLike
:param true_params: モデル関数の真のパラメータを格納した辞書。
:type true_params: Mapping[str, float]
:param noise_std: ノイズの標準偏差。
:type noise_std: float
:param seed: 乱数生成のシード値。同じシード値を使用すると再現可能なノイズが生成される。Noneの場合は非再現的。
:type seed: Optional[int]
:param n_replicates: 生成する繰り返し測定の数。
:type n_replicates: int
:returns: 生成された合成データと関連情報を含むSyntheticDataオブジェクト。xとyの配列サイズは元のn_replicates倍になる。
:rtype: SyntheticData
"""
rng = np.random.default_rng(seed)
x_arr = np.asarray(x, dtype=float).reshape(-1)
p = {k: float(v) for k, v in true_params.items()}
y_clean = np.asarray(model_func(x_arr, p), dtype=float).reshape(-1)
xs = []
ys = []
ycs = []
yns = []
for _ in range(int(n_replicates)):
eps = rng.normal(0.0, float(noise_std), size=x_arr.size)
xs.append(x_arr)
ys.append(y_clean + eps)
ycs.append(y_clean)
yns.append(eps)
return SyntheticData(
x=np.concatenate(xs),
y=np.concatenate(ys),
y_clean=np.concatenate(ycs),
y_noise=np.concatenate(yns),
true_params=p,
noise_std=float(noise_std),
seed=seed,
)
[ドキュメント]
def save_xy_csv(
path: Union[str, Path],
data: SyntheticData,
*,
include_clean: bool = True,
) -> None:
"""合成データをCSVファイルに保存する。
概要:
`SyntheticData` オブジェクトに含まれるx, yデータをCSVファイルとして保存する。
詳細説明:
指定されたファイルパスにCSVファイルを書き込みます。
`include_clean` が `True` の場合、`y_clean` (ノイズを含まない真値) と
`y_noise` (ノイズ成分) の列もCSVに含まれます。
指定されたパスの親ディレクトリが存在しない場合は、自動的に作成されます。
:param path: 保存先のファイルパス。文字列またはPathオブジェクト。
:type path: Union[str, Path]
:param data: 保存する `SyntheticData` オブジェクト。
:type data: SyntheticData
:param include_clean: `y_clean` と `y_noise` の列をCSVに含めるかどうか。デフォルトはTrue。
:type include_clean: bool
:returns: なし
:rtype: None
"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
fieldnames = ["x", "y"]
if include_clean:
fieldnames += ["y_clean", "y_noise"]
with path.open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for i in range(data.x.size):
row = {
"x": float(data.x[i]),
"y": float(data.y[i]),
}
if include_clean:
row["y_clean"] = float(data.y_clean[i])
row["y_noise"] = float(data.y_noise[i])
writer.writerow(row)
[ドキュメント]
def synthetic_summary(data: SyntheticData) -> str:
"""合成データの概要を整形された文字列として返す。
概要:
`SyntheticData` オブジェクトの主要な情報を人間が読める形式の文字列として生成する。
詳細説明:
データセットのサンプル数、xとyの範囲、ノイズの標準偏差、乱数シード、
そしてデータ生成に使用された真のパラメータのリストを含む詳細な概要を生成します。
この出力はデバッグやログ記録に役立ちます。
:param data: 概要を生成する `SyntheticData` オブジェクト。
:type data: SyntheticData
:returns: データの概要を示す整形済み文字列。
:rtype: str
"""
lines = [
"SyntheticData",
f" N = {data.x.size}",
f" x range = [{np.nanmin(data.x):.6g}, {np.nanmax(data.x):.6g}]",
f" y range = [{np.nanmin(data.y):.6g}, {np.nanmax(data.y):.6g}]",
f" noise_std = {data.noise_std:.6g}",
f" seed = {data.seed}",
" true_params:",
]
for k, v in data.true_params.items():
lines.append(f" {k:16s} = {v:.10g}")
return "\n".join(lines)