"""
=============================================================================
第 11 讲 · 代码 3：遗憾的下界、KL-UCB，以及"问题难度"如何影响遗憾
=============================================================================

对应讲义：lectures/lecture11_bandits_ucb.md 第 6–7 节

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
UCB1 的遗憾按 ln T 增长。能不能更好？有没有一个"任何算法都做不到更好"的极限？

1. **Lai–Robbins 下界**（1985）：任何"在所有伯努利老虎机上都表现合理"的算法，都满足
        liminf_{T→∞} Regret(T) / ln T ≥ C = Σ_{a: Δ_a>0} Δ_a / kl(μ_a, μ*)
   对数增长是不可避免的；常数 C 由"次优臂与最优臂有多难区分"（KL 散度）决定。
   **KL-UCB** 用 KL 散度构造置信上界，恰好达到这个常数——渐近最优。
2. **问题难度（差距 Δ）如何影响遗憾**：Δ 很大时容易区分，遗憾小；Δ 很小时虽然难区分，
   但拉错了也亏得少，遗憾同样小；最"坏"的是中间的 Δ ≈ sqrt(K ln T / T)。
3. **先探索后承诺（ETC，即 A/B 测试）**：探索量事先定死；即使知道 Δ、把 m 调到最好，也只能与 UCB 打平或更差。

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    cd code/lecture11
    python 03_regret_bounds.py      （约 75 秒；KL-UCB 每一步都要做二分查找，比较慢）
"""

import numpy as np

from bandits import (DEFAULT_MEANS, explore_then_commit, kl_ucb, lai_robbins_constant, pad,
                     simulate, ucb1)

np.set_printoptions(precision=3, suppress=True)

# 一个"点击率"式的老虎机：4 个广告，点击率只有 2%～5%
CTR_MEANS = np.array([0.02, 0.03, 0.04, 0.05])


def two_armed(gap, center=0.5):
    """
    构造一个两臂老虎机：两个臂的成功率分别为 center - gap/2 和 center + gap/2。

    【边界】需要 0 ≤ center ± gap/2 ≤ 1，否则不是合法的概率。
    """
    return np.array([center - gap / 2, center + gap / 2])


def main():
    # ---------------------------------------------------------------------
    print("=" * 78)
    print("演示 1：Lai–Robbins 下界与 KL-UCB（重复 100 次）")
    print("=" * 78)
    cps = [1000, 10000, 100000]
    agents = [("UCB1（c = 2）", ucb1(2.0)), ("UCB1（c = 0.5）", ucb1(0.5)),
              ("UCB1（c = 0.25）", ucb1(0.25)), ("KL-UCB", kl_ucb())]
    for label, m in [("默认老虎机（成功率 0.2～0.7）", DEFAULT_MEANS), ("点击率老虎机（成功率 2%～5%）", CTR_MEANS)]:
        C = lai_robbins_constant(m)
        print(f"\n  {label}：μ = {m}")
        print(f"  Lai–Robbins 常数 C = Σ Δ_a / kl(μ_a, μ*) = {C:.1f}；对应的\"渐近下界\" C·ln T = "
              + "、".join(f"{C * np.log(T):.0f}" for T in cps) + "（T = 10³、10⁴、10⁵）")
        print("   " + pad("策略", 18) + "     遗憾 R(T)                    R(T) / ln T")
        print("   " + pad("", 18) + "  T=10³   T=10⁴   T=10⁵       T=10³  T=10⁴  T=10⁵")
        for name, pol in agents:
            res = simulate(pol, m, T=100000, n_runs=100, seed=7, checkpoints=cps)
            r = res["regret"].mean(axis=0)
            ratio = r / np.log(cps)
            print("   " + pad(name, 18) + "".join(f"{x:8.1f}" for x in r) + "    " + "".join(f"{x:7.1f}" for x in ratio))
    print("\n  观察：")
    print("  - 点击率老虎机上，KL-UCB 的遗憾只有 UCB1 的几分之一。成功率接近 0 时，伯努利分布的方差 μ(1-μ) 很小，")
    print("    霍夫丁不等式（按最坏的方差 1/4 来算）给出的上界非常松，UCB1 因此做了大量不必要的探索；")
    print("    KL 散度\"知道\"伯努利分布的形状，给出的上界紧得多。")
    print("  - KL-UCB 的 R(T)/ln T 随 T 增大从下方逐渐接近 Lai–Robbins 常数 C（T = 10⁵ 时分别为 8.1 和 9.5，")
    print("    C 为 9.2 和 15.4）。理论保证它的极限恰好等于 C——这正是\"渐近最优\"的含义。")
    print("    UCB1 在点击率老虎机上的 R(T)/ln T 还在快速增长，离它的对数阶段还远。")
    print("  - 在默认老虎机上，UCB1（c = 0.25）的遗憾甚至**低于** C·ln T！这并不违反下界：")
    print("    下界说的是 T → ∞ 时的极限，有限的 T 下可以低于它；而且下界只约束\"在所有老虎机上都合理\"的算法。")
    print("    c < 0.5 的 UCB 在某些老虎机上会付出很大的代价（02 演示 5 中已经看到了\"最差 1% 的实验\"），")
    print("    它在这里的低遗憾，是用其他问题上的风险\"换\"来的。")
    print("  - 所以 C 的真正含义是：**要想在所有问题上都表现合理，在这个问题上就必须付出至少 C·ln T 的遗憾**。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 2：遗憾如何依赖于差距 Δ（两臂老虎机，T = 10⁴，重复 200 次）")
    print("=" * 78)
    T = 10000
    print("     Δ      UCB1(c=0.5)   KL-UCB    随机拉的遗憾 ΔT/2    渐近下界 C·ln T")
    worst = (0, 0)
    for gap in [0.005, 0.01, 0.02, 0.03, 0.05, 0.1, 0.2, 0.4]:
        m = two_armed(gap)
        r_ucb = simulate(ucb1(0.5), m, T=T, n_runs=200, seed=8)["regret"].mean()
        r_kl = simulate(kl_ucb(), m, T=T, n_runs=200, seed=8)["regret"].mean()
        worst = max(worst, (r_ucb, gap))
        print(f"   {gap:5.3f}     {r_ucb:8.1f}    {r_kl:8.1f}       {gap * T / 2:8.1f}            {lai_robbins_constant(m) * np.log(T):8.1f}")
    print(f"  UCB1 遗憾最大的差距：Δ ≈ {worst[1]}（遗憾 {worst[0]:.1f}）；理论上最坏的差距约为 "
          f"sqrt(K ln T / T) = {np.sqrt(2 * np.log(T) / T):.3f}")
    print("  观察：")
    print("  - Δ 很大（0.2、0.4）：两个臂很容易区分，很快就锁定最好的臂，遗憾很小。")
    print("  - Δ 很小（0.005、0.01）：几乎无法区分，但就算一直拉错也只亏 Δ × T，遗憾同样很小。")
    print("    此时\"渐近下界\"C·ln T 远大于 ΔT/2——下界根本还没\"生效\"：T 不够大，渐近结论不适用。")
    print("  - 最\"坏\"的是中间的 Δ：既难以快速区分，拉错的代价又不可忽略。")
    print(f"    这给出了**与问题无关的**遗憾界：无论 Δ 是多少，UCB 的遗憾都不超过 O(sqrt(K T ln T))；")
    print(f"    而任何算法在最坏的问题上都至少有 Ω(sqrt(K T)) 的遗憾（极小极大下界）。")
    print("  - 所以遗憾界有两种说法：与问题有关的 O(Σ ln T / Δ_a)（对具体的老虎机），")
    print("    和与问题无关的 O(sqrt(K T ln T))（对最坏的老虎机）。两者都是 UCB 的保证。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 3：先探索后承诺（ETC，也就是 A/B 测试）与 UCB（两臂，T = 10⁴，重复 300 次）")
    print("=" * 78)
    ms = [20, 50, 100, 200, 500, 1000]
    gaps = [0.02, 0.05, 0.1, 0.2]
    print("   " + pad("策略", 22) + "".join(f"   Δ={g:<5}" for g in gaps) + "   各 Δ 中最坏的")
    rows = [(f"ETC：每个臂探索 {m} 次", explore_then_commit(m)) for m in ms] + [("UCB1（c = 0.5）", ucb1(0.5))]
    table = {}
    for name, pol in rows:
        vals = [simulate(pol, two_armed(g), T=T, n_runs=300, seed=9)["regret"].mean() for g in gaps]
        table[name] = vals
        print("   " + pad(name, 22) + "".join(f"{v:10.1f}" for v in vals) + f"      {max(vals):8.1f}")
    etc_names = [n for n in table if n.startswith("ETC")]
    best_etc = [min(table[n][k] for n in etc_names) for k in range(len(gaps))]
    print("   " + pad("（每个 Δ 下最好的 ETC）", 22) + "".join(f"{v:10.1f}" for v in best_etc))
    print("  观察：")
    print("  - 最佳的 m 随 Δ 变化很大（大约 ∝ ln(TΔ²)/Δ²）：Δ 小要多探索，Δ 大要少探索。")
    print("    m 太小 → 有相当的概率选错臂，此后每一步都亏 Δ，而且再也纠正不过来；m 太大 → 在差的臂上浪费太多次。")
    print("  - 即使我们\"作弊\"——知道 Δ、为每个 Δ 挑出最好的 m——ETC 也只是与 UCB1 打平或更差。")
    print("    这与理论一致：调到最优的 ETC 的遗憾仍比 UCB 大一个常数倍。原因是 ETC 的探索量是**事先定死**的，")
    print("    而 UCB 会根据已经看到的数据随时调整：差距明显时早早停止探索，差距不明显时继续收集证据。")
    print("  - 任何固定的 m，最坏情况都明显比 UCB1 差。")
    print("    实践中的 A/B 测试就是 ETC：先分流做实验，再全量上线。它简单、容易解释、方便做统计检验，")
    print("    所以仍被广泛使用；但如果目标是\"实验期间的总收益\"，老虎机算法（UCB、下一讲的汤普森采样）通常更好。")


if __name__ == "__main__":
    main()
