Files
photonAI/src/preprocess.py
2026-04-19 18:33:36 +08:00

320 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""清洗、划分、标准化与 DataLoader 构建。"""
from __future__ import annotations
import json
import logging
import pickle
from dataclasses import dataclass
from pathlib import Path
from typing import List, Sequence, Tuple
import numpy as np
import pandas as pd
import torch
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from torch.utils.data import DataLoader, TensorDataset
from src.config import AppConfig
from src.data import INPUT_COLUMNS, TARGET_COLUMNS, quality_report_before_clean
logger = logging.getLogger(__name__)
@dataclass
class ProcessedDataBundle:
"""训练用张量与 DataLoader以及 notebook 风格的 train/test 划分数据。"""
train_loader: DataLoader
test_loader: DataLoader
X_train: np.ndarray
X_test: np.ndarray
y_train: np.ndarray
y_test: np.ndarray
X_train_raw: np.ndarray
X_test_raw: np.ndarray
y_train_raw: np.ndarray
y_test_raw: np.ndarray
X_scaler: StandardScaler
y_scalers: List[StandardScaler]
feature_names: List[str]
target_names: List[str]
def clean_dataframe(
df: pd.DataFrame,
cfg: AppConfig,
report_lines: List[str],
) -> pd.DataFrame:
"""按 notebook 逻辑清洗:仅保留 V_pi < 阈值。"""
out = df.copy()
n0 = len(out)
vmax = float(cfg.data.filter_v_pi_max)
out = out[out["V_pi"] < vmax].reset_index(drop=True)
report_lines.append(f"V_pi 阈值过滤 (< {vmax}){n0} -> {len(out)}")
if len(out) == 0:
raise ValueError("清洗后样本数为 0请检查数据源或 V_pi 阈值设置。")
return out
def build_dataloaders(
X_train: np.ndarray,
y_train: np.ndarray,
X_test: np.ndarray,
y_test: np.ndarray,
batch_size: int,
num_workers: int,
) -> Tuple[DataLoader, DataLoader]:
def to_loader(X: np.ndarray, y: np.ndarray, shuffle: bool) -> DataLoader:
ds = TensorDataset(
torch.from_numpy(X).float(),
torch.from_numpy(y).float(),
)
return DataLoader(
ds,
batch_size=batch_size,
shuffle=shuffle,
num_workers=num_workers,
pin_memory=False,
)
return (
to_loader(X_train, y_train, shuffle=True),
to_loader(X_test, y_test, shuffle=False),
)
def _fit_target_scalers(y_train: np.ndarray) -> Tuple[List[StandardScaler], np.ndarray]:
scalers: List[StandardScaler] = []
scaled_cols = []
for i in range(y_train.shape[1]):
scaler = StandardScaler()
scaled_cols.append(scaler.fit_transform(y_train[:, i : i + 1]))
scalers.append(scaler)
return scalers, np.hstack(scaled_cols)
def transform_targets(y: np.ndarray, y_scalers: Sequence[StandardScaler]) -> np.ndarray:
cols = [scaler.transform(y[:, i : i + 1]) for i, scaler in enumerate(y_scalers)]
return np.hstack(cols)
def inverse_transform_targets(
y_scaled: np.ndarray, y_scalers: Sequence[StandardScaler]
) -> np.ndarray:
cols = [scaler.inverse_transform(y_scaled[:, i : i + 1]) for i, scaler in enumerate(y_scalers)]
return np.hstack(cols)
def save_scalers(
X_scaler: StandardScaler,
y_scalers: Sequence[StandardScaler],
run_dir: Path,
) -> None:
with (run_dir / "x_scaler.pkl").open("wb") as f:
pickle.dump(X_scaler, f)
with (run_dir / "y_scalers.pkl").open("wb") as f:
pickle.dump(list(y_scalers), f)
def load_scalers(run_dir: Path) -> Tuple[StandardScaler, List[StandardScaler]]:
with (run_dir / "x_scaler.pkl").open("rb") as f:
X_scaler = pickle.load(f)
with (run_dir / "y_scalers.pkl").open("rb") as f:
y_scalers = pickle.load(f)
return X_scaler, list(y_scalers)
def save_split_indices(
run_dir: Path,
i_train: np.ndarray,
i_test: np.ndarray,
) -> None:
payload = {
"train": i_train.astype(int).tolist(),
"test": i_test.astype(int).tolist(),
}
(run_dir / "split_indices.json").write_text(
json.dumps(payload, indent=2),
encoding="utf-8",
)
def load_split_indices(run_dir: Path) -> Tuple[np.ndarray, np.ndarray]:
path = run_dir / "split_indices.json"
if not path.is_file():
raise FileNotFoundError(f"未找到 {path}")
data = json.loads(path.read_text(encoding="utf-8"))
return (
np.asarray(data["train"], dtype=int),
np.asarray(data["test"], dtype=int),
)
def write_data_report_md(
path: Path,
raw_quality: dict,
report_lines: List[str],
basic_stats_before: pd.DataFrame,
basic_stats_after: pd.DataFrame,
) -> None:
lines = [
"# 数据与清洗报告",
"",
"## 清洗前质量摘要JSON",
"```json",
json.dumps(raw_quality, indent=2, ensure_ascii=False, default=str),
"```",
"",
"## 清洗步骤",
"\n".join(f"- {x}" for x in report_lines),
"",
"## 清洗前 describeCSV 文本块)",
"```text",
basic_stats_before.to_csv(),
"```",
"",
"## 清洗后 describeCSV 文本块)",
"```text",
basic_stats_after.to_csv(),
"```",
"",
]
path.write_text("\n".join(lines), encoding="utf-8")
def prepare_training_data(
df: pd.DataFrame,
cfg: AppConfig,
run_dir: Path,
) -> ProcessedDataBundle:
"""按 notebook 一致逻辑准备 train/test、scaler 与 DataLoader。"""
report_lines: List[str] = []
raw_q = quality_report_before_clean(df, 0.0, cfg.data.filter_v_pi_max)
stats_before = df.describe().T
cleaned = clean_dataframe(df, cfg, report_lines)
stats_after = cleaned.describe().T
X = cleaned[INPUT_COLUMNS].to_numpy(dtype=np.float64)
y = cleaned[TARGET_COLUMNS].to_numpy(dtype=np.float64)
all_idx = np.arange(len(X))
X_train_raw, X_test_raw, y_train_raw, y_test_raw, i_train, i_test = train_test_split(
X,
y,
all_idx,
test_size=cfg.data.test_size,
random_state=cfg.data.random_state,
)
report_lines.append(
f"train_test_split(test_size={cfg.data.test_size}, random_state={cfg.data.random_state}) "
f"-> {len(X_train_raw)}/{len(X_test_raw)}"
)
save_split_indices(run_dir, i_train, i_test)
X_scaler = StandardScaler()
X_train = X_scaler.fit_transform(X_train_raw)
X_test = X_scaler.transform(X_test_raw)
y_scalers, y_train = _fit_target_scalers(y_train_raw)
y_test = transform_targets(y_test_raw, y_scalers)
save_scalers(X_scaler, y_scalers, run_dir)
meta = {
"raw_quality": raw_q,
"cleaning_steps": report_lines,
"test_size": cfg.data.test_size,
"random_state": cfg.data.random_state,
"n_train": int(len(X_train)),
"n_test": int(len(X_test)),
}
(run_dir / "cleaning_meta.json").write_text(
json.dumps(meta, indent=2, ensure_ascii=False, default=str),
encoding="utf-8",
)
write_data_report_md(
run_dir / "data_report.md",
raw_q,
report_lines,
stats_before,
stats_after,
)
stats_after.to_csv(run_dir / "data_stats.csv", encoding="utf-8")
logger.info("预处理完成:%s", run_dir / "data_report.md")
train_loader, test_loader = build_dataloaders(
X_train,
y_train,
X_test,
y_test,
cfg.training.batch_size,
cfg.training.num_workers,
)
return ProcessedDataBundle(
train_loader=train_loader,
test_loader=test_loader,
X_train=X_train,
X_test=X_test,
y_train=y_train,
y_test=y_test,
X_train_raw=X_train_raw,
X_test_raw=X_test_raw,
y_train_raw=y_train_raw,
y_test_raw=y_test_raw,
X_scaler=X_scaler,
y_scalers=y_scalers,
feature_names=list(INPUT_COLUMNS),
target_names=list(TARGET_COLUMNS),
)
def rebuild_bundle_for_eval(
df: pd.DataFrame,
cfg: AppConfig,
run_dir: Path,
) -> ProcessedDataBundle:
"""使用训练时保存的切分索引与 scaler 重新构造 train/test 数据。"""
report_lines: List[str] = []
cleaned = clean_dataframe(df, cfg, report_lines)
X = cleaned[INPUT_COLUMNS].to_numpy(dtype=np.float64)
y = cleaned[TARGET_COLUMNS].to_numpy(dtype=np.float64)
i_train, i_test = load_split_indices(run_dir)
for name, idx in (("train", i_train), ("test", i_test)):
if len(idx) == 0 or int(idx.max()) >= len(X) or int(idx.min()) < 0:
raise ValueError(f"split_indices.json 与当前数据不兼容({name} 索引越界或为空)")
X_train_raw, X_test_raw = X[i_train], X[i_test]
y_train_raw, y_test_raw = y[i_train], y[i_test]
X_scaler, y_scalers = load_scalers(run_dir)
X_train = X_scaler.transform(X_train_raw)
X_test = X_scaler.transform(X_test_raw)
y_train = transform_targets(y_train_raw, y_scalers)
y_test = transform_targets(y_test_raw, y_scalers)
train_loader, test_loader = build_dataloaders(
X_train,
y_train,
X_test,
y_test,
cfg.training.batch_size,
cfg.training.num_workers,
)
return ProcessedDataBundle(
train_loader=train_loader,
test_loader=test_loader,
X_train=X_train,
X_test=X_test,
y_train=y_train,
y_test=y_test,
X_train_raw=X_train_raw,
X_test_raw=X_test_raw,
y_train_raw=y_train_raw,
y_test_raw=y_test_raw,
X_scaler=X_scaler,
y_scalers=y_scalers,
feature_names=list(INPUT_COLUMNS),
target_names=list(TARGET_COLUMNS),
)