"""第二篇实践：独立可运行的合成打印任务回归项目，数据生成规则 v1。"""

import numpy as np
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.impute import SimpleImputer
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import GridSearchCV, KFold, cross_val_score, train_test_split
from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler


def make_data():
    rng = np.random.default_rng(2026)
    pages = rng.integers(5, 81, 500)
    queue = rng.integers(0, 21, 500)
    color = rng.integers(0, 2, 500)
    duration = (20 + 2*pages + 1.5*queue + color*(10+0.8*pages)
                + 25*(pages > 50) + rng.normal(0, 3, 500))
    x = np.column_stack([pages, queue, np.where(color, "color", "mono")]).astype(object)
    x[:, :2] = x[:, :2].astype(float)
    x[rng.random(500) < 0.05, 0] = np.nan
    x[rng.random(500) < 0.05, 1] = np.nan
    ids = np.arange(500)
    return train_test_split(x, duration, ids, test_size=0.2, random_state=2026)


def preprocessing():
    return ColumnTransformer([
        ("numeric", make_pipeline(SimpleImputer(strategy="median", add_indicator=True),
                                   StandardScaler()), [0, 1]),
        ("category", OneHotEncoder(handle_unknown="ignore", sparse_output=False), [2]),
    ])


def run():
    x_train, x_test, y_train, y_test, ids_train, ids_test = make_data()
    folds = KFold(n_splits=5, shuffle=True, random_state=2026)
    searches = {}
    candidates = [
        ("岭回归", Ridge(), {"model__alpha": [0.1, 1.0, 10.0]}),
        ("随机森林", RandomForestRegressor(n_estimators=150, min_samples_leaf=3,
                                            random_state=2026, n_jobs=1),
         {"model__max_depth": [4, 8, None]}),
    ]
    baseline = DummyRegressor()
    baseline_scores = -cross_val_score(baseline, x_train, y_train, cv=folds,
                                       scoring="neg_mean_absolute_error")
    for name, estimator, grid in candidates:
        pipeline = Pipeline([("prepare", preprocessing()), ("model", estimator)])
        search = GridSearchCV(pipeline, grid, cv=folds,
                              scoring="neg_mean_absolute_error", refit=True, n_jobs=1)
        search.fit(x_train, y_train)
        searches[name] = search
    name = max(searches, key=lambda key: searches[key].best_score_)
    selected = searches[name]
    prediction = selected.predict(x_test)
    baseline.fit(x_train, y_train)
    baseline_prediction = baseline.predict(x_test)
    return (x_test, y_test, ids_test, prediction, baseline_prediction,
            baseline_scores, searches, name)


def main():
    x_test, y_test, ids, prediction, baseline_prediction, baseline_scores, searches, name = run()
    print(f"环境：scikit-learn {sklearn.__version__}；NumPy {np.__version__}")
    print("数据规则 v1；种子 2026；开发集 400 条；测试集 100 条")
    print(f"均值基线五折 MAE：{baseline_scores.mean():.3f} 秒")
    for key, search in searches.items():
        print(f"{key}：最佳五折 MAE={-search.best_score_:.3f} 秒；"
              f"参数={search.best_params_}")
    errors = prediction-y_test
    print(f"固定模型：{name}")
    print(f"测试 MAE：模型={mean_absolute_error(y_test, prediction):.3f} 秒；"
          f"基线={mean_absolute_error(y_test, baseline_prediction):.3f} 秒")
    pages = x_test[:, 0].astype(float)
    masks = [("黑白", x_test[:, 2] == "mono"), ("彩色", x_test[:, 2] == "color"),
             ("页数缺失", np.isnan(pages)), ("页数完整", ~np.isnan(pages))]
    for label, mask in masks:
        if mask.any():
            print(f"{label}：{mask.sum()} 条，MAE={np.abs(errors[mask]).mean():.3f} 秒")
    print("绝对误差最大的三条：")
    for index in np.argsort(-np.abs(errors), kind="stable")[:3]:
        print(f"ID={ids[index]}，页数={pages[index]:g}，模式={x_test[index, 2]}，"
              f"实际={y_test[index]:.1f}，预测={prediction[index]:.1f}，"
              f"误差={errors[index]:+.1f} 秒")


if __name__ == "__main__":
    main()
