"""
=============================================================================
第 1 讲 · 代码 4：谁的偏好？—— 口味不同的人、被选来标注的人、以及没有答案的投票
=============================================================================

对应讲义：lectures/lecture01_intro_motivation.md 第 4.6 节（第 4、15~17 讲的预告）

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
03 号文件里，众包和专家的分歧来自"偏差"：众包标注员被长度和格式带偏了，专家是对的。
但并不是所有分歧都是偏差。有人喜欢简洁的回答，有人喜欢详尽的回答 —— 两种口味都合理。
这时，"从人的偏好里学"就多了一个绕不开的问题：**学谁的？**

    演示 1：两群口味相反的人 —— 把他们的比较混在一起拟合一个奖励模型，会发生什么？
            多数人的口味胜出；少数人最喜欢的回答被排在后面；而且混合后的数据
            **不再服从** Bradley–Terry 模型（单一的分数解释不了它）。
    演示 2：标注员不等于用户 —— 被雇来标注的人和真正使用产品的人口味分布不同，
            学到的奖励代表的是标注员，而不是用户。
    演示 3：没有答案的投票（孔多塞悖论）—— 三群人，每群人的偏好都很合理，
            但"多数人的偏好"是一个循环：A 胜 B、B 胜 C、C 胜 A。任何单一的排名都不能代表这个群体。
            这是第 16 讲阿罗不可能定理的入口。

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    python3 04_whose_preferences.py        # 约 1 秒

只用到 Python 标准库，以及同目录下的 toy_world.py。
"""

import math
import random

from toy_world import (BY_KEY, KEYS, Annotator, bar, bt_log_likelihood, fit_bradley_terry, mean,
                       pad, print_header, sigmoid)

CONCISE = Annotator(name="简洁派", group="简洁派", taste=-4.0)     # 喜欢简洁：detail 越小越好
DETAILED = Annotator(name="详尽派", group="详尽派", taste=+4.0)    # 喜欢详尽：detail 越大越好


def mixture_prob(groups, a, b):
    """
    一个混合人群里，"随机找一个人，他选 a 不选 b"的概率。

    【思路】先按人群的比例抽一个人群，再按这个人群的 BT 模型选：
            P(a ≻ b) = Σ_g 比例_g · σ(效用_g(a) − 效用_g(b))
    【参数】groups：list[(Annotator, 比例)]，比例之和为 1；a、b：回答编号
    【返回】float
    【边界】混合的结果一般**不能**写成 σ(s_a − s_b) 的形式 —— 这正是演示 1 要展示的。
    【偏好学习联系】把很多标注员的数据混在一起训练一个奖励模型，隐含的就是这个"随机找一个人"的模型。
        第 3 讲会证明混合破坏 IIA；第 4 讲的因子模型、第 17 讲的多元对齐给出不同的处理办法。
    """
    return math.fsum(share * sigmoid(g.utility(BY_KEY[a]) - g.utility(BY_KEY[b])) for g, share in groups)


def collect_mixed(groups, n, rng, keys=KEYS):
    """
    从混合人群收集 n 条比较：每次随机挑两个不同的回答（均匀），再按人群比例抽一个人来选。

    【参数】groups：list[(Annotator, 比例)]；n：条数；rng：random.Random；keys：候选回答
    【返回】list[(赢家, 输家)]
    """
    people = [g for g, _ in groups]
    weights = [w for _, w in groups]
    data = []
    for _ in range(n):
        a, b = rng.sample(keys, 2)
        judge = rng.choices(people, weights=weights)[0]
        if rng.random() < sigmoid(judge.utility(BY_KEY[a]) - judge.utility(BY_KEY[b])):
            data.append((a, b))
        else:
            data.append((b, a))
    return data


# =============================================================================
# 演示 1：两群口味相反的人
# =============================================================================
def demo_two_tastes(rng):
    print_header("演示 1：两群口味相反的人 —— 一个奖励模型能代表所有人吗？")
    groups = [(CONCISE, 0.6), (DETAILED, 0.4)]
    print("用户里 60% 是“简洁派”，40% 是“详尽派”。两群人都识货（只看真实质量，没有长度、格式偏差），")
    print("只是口味相反。看看他们心里对三个好回答（A 简洁、C 适中、B 详尽）的效用：\n")
    trio = ["A", "C", "B"]
    print(f"  {pad('人群', 8)} " + " ".join(pad(k + '（' + BY_KEY[k].title.split('：')[0] + '）', 15, 'right')
                                           for k in trio))
    for g, share in groups:
        print(f"  {pad(g.name, 8)} " + " ".join(f"{g.utility(BY_KEY[k]):>15.2f}" for k in trio)
              + f"    （占 {share:.0%}）")

    data = collect_mixed(groups, 8000, rng)
    pooled = fit_bradley_terry(data, KEYS)
    order = sorted(KEYS, key=lambda k: -pooled[k])
    print(f"\n把 8000 条混合比较放在一起拟合一个 BT 奖励模型，排名：{' > '.join(order)}")

    print("\n如果系统对这个问题只给一个回答，每群人的满意程度（他们心里的效用）：\n")
    print(f"  {pad('系统给出', 18)} {pad('简洁派', 8, 'right')} {pad('详尽派', 8, 'right')} "
          f"{pad('平均（加权）', 13, 'right')} {pad('最差的一群', 11, 'right')}")
    for k in trio:
        uc, ud = CONCISE.utility(BY_KEY[k]), DETAILED.utility(BY_KEY[k])
        avg = 0.6 * uc + 0.4 * ud
        print(f"  {pad(k + '（' + BY_KEY[k].title.split('：')[0] + '）', 18)} {uc:>8.2f} {ud:>8.2f} {avg:>13.2f} {min(uc, ud):>11.2f}")
    print(f"  {pad('个性化（各给所爱）', 18)} {CONCISE.utility(BY_KEY['A']):>8.2f} {DETAILED.utility(BY_KEY['B']):>8.2f} "
          f"{0.6 * CONCISE.utility(BY_KEY['A']) + 0.4 * DETAILED.utility(BY_KEY['B']):>13.2f} "
          f"{min(CONCISE.utility(BY_KEY['A']), DETAILED.utility(BY_KEY['B'])):>11.2f}")

    print("\n混合数据还服从 Bradley–Terry 模型吗？比较真实的混合胜率和 BT 模型的预测：\n")
    print(f"  {pad('一对', 8)} {pad('真实胜率', 9, 'right')} {pad('BT 预测', 8, 'right')}")
    for a, b in [("A", "B"), ("A", "C"), ("C", "B")]:
        print(f"  {a + ' ≻ ' + b:8s} {mixture_prob(groups, a, b):>9.3f} {sigmoid(pooled[a] - pooled[b]):>8.3f}")
    # 一致性检验：BT 要求 logit P(A≻B) = logit P(A≻C) + logit P(C≻B)
    logit = lambda p: math.log(p / (1 - p))
    lhs = logit(mixture_prob(groups, "A", "B"))
    rhs = logit(mixture_prob(groups, "A", "C")) + logit(mixture_prob(groups, "C", "B"))
    print(f"\n  BT 模型要求：logit P(A≻B) = logit P(A≻C) + logit P(C≻B)。真实数据：{lhs:.2f} vs {rhs:.2f}")
    print("""
观察：
  · 合在一起的奖励模型把 A（多数人最喜欢的）排第一，把详尽派最喜欢的 B 排在 C 后面。
    如果系统只给一个回答，它会给 A：多数人很满意，少数人很不满意。
  · 选哪个“才对”？按加权平均（功利主义）选 A；按“照顾最差的那群人”（罗尔斯式）选 C 这个折中方案；
    最好的办法是**个性化**：知道你是谁，就给你喜欢的（两群人都拿到最高的效用）。
    这些都不是技术问题，而是价值选择（第 15~17 讲）；个性化需要“用户 × 回答”的模型（第 4 讲：因子模型）。
  · 混合数据**不再服从** Bradley–Terry 模型：如果存在一组分数 s，必须有
    logit P(A≻B) = (s_A − s_C) + (s_C − s_B) = logit P(A≻C) + logit P(C≻B)，但真实数据差得很远，
    所以 BT 的预测在几对上都偏了。第 3 讲会证明：每群人各自满足 IIA，混在一起就不满足了。
    一个单一的奖励模型在“平均掉”分歧的同时，也把数据的结构弄错了。""")


# =============================================================================
# 演示 2：标注员不等于用户
# =============================================================================
def demo_annotators_vs_users(rng):
    print_header("演示 2：标注员不等于用户")
    annotators = [(CONCISE, 0.85), (DETAILED, 0.15)]
    users = [(CONCISE, 0.35), (DETAILED, 0.65)]
    print("被雇来做标注的人里 85% 是简洁派（比如都是赶时间的工程师）；真正的用户里只有 35% 是简洁派。")
    data = collect_mixed(annotators, 8000, rng)
    reward = fit_bradley_terry(data, KEYS)
    top = max(KEYS, key=lambda k: reward[k])
    users_reward = fit_bradley_terry(collect_mixed(users, 8000, rng), KEYS)
    user_top = max(KEYS, key=lambda k: users_reward[k])
    print(f"  用标注员的数据学出的奖励模型，第一名：{top}（{BY_KEY[top].title.split('：')[0]}）")
    print(f"  如果能用用户的数据学，第一名会是：{user_top}（{BY_KEY[user_top].title.split('：')[0]}）")
    p = mixture_prob(users, user_top, top)
    print(f"  随机找一个用户，他更喜欢 {user_top} 而不是 {top} 的概率：{p:.1%}")
    print("""
观察：
  · 奖励模型学到的是“被问到的人”的偏好。谁被问到，是由招聘渠道、语言、时区、报酬决定的，
    往往和产品的真实用户很不一样。
  · 真实的例子：Santurkar 等（2023）用美国的民意调查题目测试语言模型，发现模型的“观点”和很多
    人群差别很大，而经过人类反馈微调的模型更偏向某些特定人群（例如受教育程度高、收入高的人群）。
  · 对应到课程：第 7、10 讲讲“问谁、问什么”最有效率；第 15 讲讲“被观察到的行为”和“真实偏好”的差别；
    第 17 讲讲公平：只追求效率的采样，可能系统性地忽略少数人群。""")


# =============================================================================
# 演示 3：孔多塞悖论
# =============================================================================
def demo_condorcet(rng):
    print_header("演示 3：没有答案的投票 —— 孔多塞悖论")
    # 三群人，各占三分之一，每群人的偏好都是一个合理的排名（效用 6 / 3 / 0，选择很坚定）
    tables = {
        "第一群": {"A": 6.0, "B": 3.0, "C": 0.0},     # A > B > C
        "第二群": {"A": 0.0, "B": 6.0, "C": 3.0},     # B > C > A
        "第三群": {"A": 3.0, "B": 0.0, "C": 6.0},     # C > A > B
    }
    for name, t in tables.items():
        order = sorted(t, key=lambda k: -t[k])
        print(f"  {name}（占 1/3）：{' > '.join(order)}")

    def group_prob(a, b):
        """三群人混合后，P(a ≻ b)。"""
        return mean([sigmoid(t[a] - t[b]) for t in tables.values()])

    keys = ["A", "B", "C"]
    data = []
    for _ in range(9000):
        a, b = rng.sample(keys, 2)
        t = rng.choice(list(tables.values()))
        data.append((a, b) if rng.random() < sigmoid(t[a] - t[b]) else (b, a))
    scores = fit_bradley_terry(data, keys)

    print(f"\n  {pad('一对', 8)} {pad('群体胜率', 9, 'right')} {pad('多数人选', 9, 'right')} {pad('BT 预测', 8, 'right')}")
    for a, b in [("A", "B"), ("B", "C"), ("C", "A")]:
        p = group_prob(a, b)
        winner = a if p > 0.5 else b
        print(f"  {a + ' vs ' + b:8s} {p:>9.3f} {winner:>9s} {sigmoid(scores[a] - scores[b]):>8.3f}")
    print(f"\n  BT 拟合出的分数：" + "，".join(f"{k} = {scores[k]:+.3f}" for k in keys))
    ll_bt = bt_log_likelihood(data, scores)
    ll_truth = mean([math.log(group_prob(w, l)) for w, l in data])
    print(f"  平均对数似然：BT 模型 {ll_bt:.3f}（等于 log 0.5 = {math.log(0.5):.3f}，相当于瞎猜），"
          f"真实的群体模型 {ll_truth:.3f}")
    print("""
观察：
  · 多数人认为 A 比 B 好，多数人认为 B 比 C 好，多数人又认为 C 比 A 好 —— 群体的偏好是一个循环。
    每个人都是“理性”的（各自的排名都没有循环），但“多数决”把它们合成了一个不理性的东西。
    这就是孔多塞（Condorcet）在 1785 年发现的悖论。
  · BT 模型只能给每个回答一个分数，分数一定能排出先后，所以它根本表示不了循环。
    它能做的最好的事，是给三个回答几乎一样的分数，把每一对都预测成五五开 —— 和瞎猜一样。
  · 这不是 BT 模型或者某个算法的缺陷：第 16 讲的阿罗不可能定理证明，**没有任何**把个人排名合成
    群体排名的规则能同时满足几条看起来都很合理的要求。“对齐到人类的偏好”里的“人类”是复数，
    这是整门课最后一个单元（聚合）要面对的问题。""")


def main():
    rng = random.Random(1785)        # 1785：孔多塞发表关于投票悖论的论文的年份
    demo_two_tastes(rng)
    demo_annotators_vs_users(rng)
    demo_condorcet(rng)


if __name__ == "__main__":
    main()
