#!/usr/bin/env python3
"""Bab 05 — Probability, Statistics, Gradient, and Optimization Playground.

Tujuan:
- standard library only;
- mudah diketik ulang pembaca;
- bisa jalan di terminal, VS Code, Jupyter, Google Colab, dan Kaggle;
- menunjukkan probabilitas, statistik, loss, finite difference, gradient descent,
  dan training model linear mini dari nol.
"""
from __future__ import annotations

import math
import random
from statistics import mean, median

SEED = 42
random.seed(SEED)


def title(text: str) -> None:
    print("\n" + text)
    print("-" * len(text))


def simulate_die(n: int = 1000) -> dict[int, int]:
    counts = {i: 0 for i in range(1, 7)}
    for _ in range(n):
        counts[random.randint(1, 6)] += 1
    return counts


def complement(p: float) -> float:
    return 1 - p


def independent_and(p_a: float, p_b: float) -> float:
    return p_a * p_b


def either_or(p_a: float, p_b: float, p_both: float = 0.0) -> float:
    return p_a + p_b - p_both


def conditional_probability(count_a_and_b: int, count_b: int) -> float:
    if count_b == 0:
        raise ValueError("count_b tidak boleh nol")
    return count_a_and_b / count_b


def bayes(prior: float, likelihood: float, evidence: float) -> float:
    if evidence == 0:
        raise ValueError("evidence tidak boleh nol")
    return likelihood * prior / evidence


def variance(values: list[float]) -> float:
    m = mean(values)
    return mean([(x - m) ** 2 for x in values])


def std(values: list[float]) -> float:
    return math.sqrt(variance(values))


def percentile(values: list[float], pct: float) -> float:
    """Nearest-rank-ish linear interpolation percentile for learning purposes."""
    if not values:
        raise ValueError("values kosong")
    xs = sorted(values)
    pos = (len(xs) - 1) * pct / 100
    lo = math.floor(pos)
    hi = math.ceil(pos)
    if lo == hi:
        return xs[int(pos)]
    weight = pos - lo
    return xs[lo] * (1 - weight) + xs[hi] * weight


def require_same_length(xs: list[float], ys: list[float], name_x: str = "xs", name_y: str = "ys") -> None:
    if len(xs) != len(ys):
        raise ValueError(f"{name_x} dan {name_y} harus sama panjang")
    if not xs:
        raise ValueError(f"{name_x} dan {name_y} tidak boleh kosong")


def covariance(xs: list[float], ys: list[float]) -> float:
    require_same_length(xs, ys)
    mx, my = mean(xs), mean(ys)
    return mean([(x - mx) * (y - my) for x, y in zip(xs, ys)])


def correlation(xs: list[float], ys: list[float]) -> float:
    denom = std(xs) * std(ys)
    if denom == 0:
        raise ValueError("korelasi tidak terdefinisi jika salah satu variabel konstan")
    return covariance(xs, ys) / denom


def mae(predictions: list[float], actuals: list[float]) -> float:
    require_same_length(predictions, actuals, "predictions", "actuals")
    return mean([abs(p - a) for p, a in zip(predictions, actuals)])


def mse(predictions: list[float], actuals: list[float]) -> float:
    require_same_length(predictions, actuals, "predictions", "actuals")
    return mean([(p - a) ** 2 for p, a in zip(predictions, actuals)])


def one_dim_loss(x: float) -> float:
    return (x - 3) ** 2


def one_dim_grad(x: float) -> float:
    return 2 * (x - 3)


def finite_difference(f, x: float, h: float = 1e-5) -> float:
    return (f(x + h) - f(x)) / h


def gradient_descent(start: float, lr: float, steps: int) -> list[tuple[int, float, float, float]]:
    x = start
    history = []
    for step in range(steps):
        history.append((step, x, one_dim_loss(x), one_dim_grad(x)))
        x = x - lr * one_dim_grad(x)
    history.append((steps, x, one_dim_loss(x), one_dim_grad(x)))
    return history


def linear_predict(x: float, w: float, b: float) -> float:
    return w * x + b


def linear_training_step(xs: list[float], ys: list[float], w: float, b: float, lr: float) -> tuple[float, float, float]:
    require_same_length(xs, ys)
    preds = [linear_predict(x, w, b) for x in xs]
    errors = [p - y for p, y in zip(preds, ys)]
    loss = mean([e ** 2 for e in errors])
    grad_w = mean([2 * e * x for e, x in zip(errors, xs)])
    grad_b = mean([2 * e for e in errors])
    w = w - lr * grad_w
    b = b - lr * grad_b
    return w, b, loss


def train_linear_model(xs: list[float], ys: list[float], lr: float = 0.01, steps: int = 80) -> list[tuple[int, float, float, float]]:
    w, b = 0.0, 0.0
    history = []
    for step in range(steps + 1):
        preds = [linear_predict(x, w, b) for x in xs]
        loss = mse(preds, ys)
        if step % 10 == 0 or step == steps:
            history.append((step, w, b, loss))
        if step < steps:
            w, b, _ = linear_training_step(xs, ys, w, b, lr)
    return history


def main() -> None:
    print("Bab 05 — Probability, Statistics, Gradient Playground")
    print("=" * 72)
    print(f"seed: {SEED}")

    title("1) Simulasi dadu dan frekuensi relatif")
    counts = simulate_die(1000)
    for face, count in counts.items():
        print(f"angka {face}: {count:4d}  peluang≈{count/1000:.3f}")

    title("2) Aturan probabilitas kecil")
    p_spam = 0.18
    print("P(spam):", p_spam)
    print("P(bukan spam):", complement(p_spam))
    print("P(koin1 gambar DAN koin2 gambar):", independent_and(0.5, 0.5))
    print("P(A ATAU B) dengan overlap 0.10:", either_or(0.30, 0.40, 0.10))

    title("3) Probabilitas bersyarat dan Bayes")
    print("P(beli | masuk keranjang) =", conditional_probability(80, 200))
    # contoh: prior fraud 1%, detektor menangkap 90% fraud, alarm total 5%
    print("P(fraud | alarm) ≈", round(bayes(prior=0.01, likelihood=0.90, evidence=0.05), 3))

    title("4) Statistik penjualan es teh")
    penjualan = [42, 18, 35, 30, 16, 38, 45, 29, 34, 31, 120]
    print("data:", penjualan)
    print("mean:", round(mean(penjualan), 3))
    print("median:", round(median(penjualan), 3))
    print("variance:", round(variance(penjualan), 3))
    print("std:", round(std(penjualan), 3))
    print("p90:", round(percentile(penjualan, 90), 3))

    title("5) Sampling error kecil")
    population = list(range(1, 101))
    for sample_size in [5, 10, 30]:
        sample = random.sample(population, sample_size)
        print(f"n={sample_size:2d} mean_sampel={mean(sample):6.2f} mean_populasi={mean(population):6.2f}")

    title("6) Korelasi kasar")
    suhu = [28, 29, 30, 31, 32, 33, 34]
    es_teh = [20, 22, 27, 31, 35, 39, 41]
    print("corr(suhu, es_teh):", round(correlation(suhu, es_teh), 3))

    title("7) Loss MAE dan MSE")
    actuals = [40, 20, 36, 32]
    preds = [32, 24, 35, 30]
    print("MAE:", round(mae(preds, actuals), 3))
    print("MSE:", round(mse(preds, actuals), 3))

    title("8) Turunan analitik vs finite difference")
    for x in [-4, 0, 3, 5]:
        print(f"x={x: .1f} loss={one_dim_loss(x): .3f} grad={one_dim_grad(x): .3f} finite≈{finite_difference(one_dim_loss, x): .3f}")

    title("9) Gradient descent: learning rate dibandingkan")
    for lr in [0.01, 0.1, 1.1]:
        hist = gradient_descent(start=-4.0, lr=lr, steps=12)
        final = hist[-1]
        print(f"lr={lr:<4} x_akhir={final[1]: .4f} loss_akhir={final[2]: .4f}")

    title("10) Detail descent lr=0.1")
    for step, x, l, g in gradient_descent(start=-4.0, lr=0.1, steps=10):
        print(f"step={step:02d} x={x: .4f} loss={l: .4f} grad={g: .4f}")

    title("11) Training model linear mini y≈2x+1")
    xs = [0, 1, 2, 3, 4, 5]
    ys = [1, 3, 5, 7, 9, 11]
    for step, w, b, loss in train_linear_model(xs, ys, lr=0.03, steps=80):
        print(f"step={step:03d} w={w: .3f} b={b: .3f} loss={loss: .4f}")

    print("\nChallenge: ubah data, seed, learning rate, dan jumlah step. Amati apa yang berubah.")


if __name__ == "__main__":
    main()
