"""第 6 章：从零训练带 L2 正则项的一元线性回归；只用标准库。"""

from math import sqrt
from statistics import mean


# 与导读使用同一份教学数据，按同一固定划分进行实验。
TRAIN = [(5, 37), (10, 47), (15, 66), (20, 81), (25, 92), (30, 112)]
VALIDATION = [(8, 43), (18, 76), (28, 103)]
TEST = [(12, 58), (22, 85), (26, 101)]


def predict(model, pages):
    z = (pages - model["center"]) / model["scale"]
    return model["weight"] * z + model["bias"]


def mae(model, data):
    return mean(abs(predict(model, x) - y) for x, y in data)


def mse(model, data):
    return mean((predict(model, x) - y)**2 for x, y in data)


def fit(data, regularization=0.0, rate=0.1, steps=300):
    center = mean(x for x, _ in data)
    scale = sqrt(mean((x-center)**2 for x, _ in data))
    if scale == 0:
        raise ValueError("训练页数没有变化，无法据此确定斜率")
    z_values = [(x-center)/scale for x, _ in data]
    weight, bias = 0.0, 0.0
    model = {"center": center, "scale": scale, "weight": weight, "bias": bias}
    history = [mse(model, data)]
    for _ in range(steps):
        errors = [weight*z + bias - y for z, (_, y) in zip(z_values, data)]
        dw = 2*mean(e*z for e, z in zip(errors, z_values)) + 2*regularization*weight
        db = 2*mean(errors)
        weight, bias = weight-rate*dw, bias-rate*db
        model.update(weight=weight, bias=bias)
        history.append(mse(model, data) + regularization*weight**2)
    return model, history


def raw_parameters(model):
    slope = model["weight"] / model["scale"]
    intercept = model["bias"] - slope * model["center"]
    return slope, intercept


def main():
    baseline = mean(y for _, y in TRAIN)
    baseline_mae = mean(abs(baseline-y) for _, y in VALIDATION)
    print(f"均值基线的验证 MAE：{baseline_mae:.3f} 秒")
    candidates = []
    for strength in [0.0, 0.1, 1.0]:
        model, _ = fit(TRAIN, regularization=strength)
        score = mae(model, VALIDATION)
        candidates.append((score, strength, model))
        slope, intercept = raw_parameters(model)
        print(f"λ={strength:.1f}：斜率={slope:.4f}，截距={intercept:.4f}，验证 MAE={score:.3f}")
    _, strength, selected = min(candidates, key=lambda item: item[0])
    print(f"选择 λ={strength:.1f}；参数到此固定")
    print(f"训练 MSE：{mse(selected, TRAIN):.3f} 秒²")
    print(f"测试 MAE：{mae(selected, TEST):.3f} 秒")
    for x, y in TEST:
        predicted = predict(selected, x)
        print(f"{x} 页：预测 {predicted:.1f}，实际 {y}，误差 {predicted-y:+.1f} 秒")
    print(f"24 页的预测耗时：{predict(selected, 24):.1f} 秒")


if __name__ == "__main__":
    main()
