"""
=============================================================================
第 2 讲 · 代码 1：偏好关系与随机偏好 —— 从"排序"到"排序上的概率分布"
=============================================================================

对应讲义：lectures/lecture02_choice_data_bt.md 第 2 节

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
在写任何模型之前，先要回答：什么叫"一个人的偏好"？经济学和决策理论的标准答案是：
偏好是选项上的一个**全序**（total order）—— 任何两个选项都能比（完备性），而且不会绕圈（传递性）。

但第 1 讲已经看到，真实的人并不总是这样：同一对问两次答案会变，三个选项之间会出现循环。
所以这门课把偏好建模成**排序上的概率分布**（随机偏好）：每次做选择时，从这个分布里抽一个排序，
按它来选。这个文件把这件事做成看得见的小实验：

    演示 1：两条公理 —— 完备性与传递性。一个有"察觉阈值"的人（Luce 的"咖啡里的糖"）
            会出现"无差异"不传递：a ~ b、b ~ c，却 a ≻ c。
    演示 2：随机偏好 = 排序上的分布。3 个选项只有 6 种排序；四种不同的分布
            （确定的、均匀的、Plackett–Luce、孔多塞混合）给出怎样的两两胜率和多选一概率。
    演示 3：随机传递性 —— 弱 / 中 / 强三种版本。BT 模型一定满足最强的那种；
            混合人群可能只满足弱的；孔多塞混合连弱的都不满足。这给了我们一个**检验** BT 是否合适的方法。

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    python3 01_preference_relations.py        # 约 1 秒

只用到 Python 标准库，以及第 1 讲的 toy_world.py、本目录的 choice_data.py。
"""

import itertools
import math

from choice_data import choice_prob  # 同时把第 1 讲加进了 sys.path
from toy_world import pad, print_header, sigmoid


# =============================================================================
# 演示 1：两条公理
# =============================================================================
def is_complete(items, prefers):
    """
    完备性：任意两个不同的选项 a、b，至少有一个"不差于"另一个（a ≿ b 或 b ≿ a）。

    【参数】items：选项列表；prefers(a, b)：返回 True 表示 a 不差于 b（a ≿ b）
    【返回】bool
    【边界】"不差于"包括"一样好"。严格偏好 ≻ 和无差异 ~ 都可以由它定义：
        a ≻ b ⇔ (a ≿ b 且 不是 b ≿ a)；a ~ b ⇔ (a ≿ b 且 b ≿ a)。
    """
    return all(prefers(a, b) or prefers(b, a) for a, b in itertools.combinations(items, 2))


def find_intransitive(items, relation):
    """
    找一个违反传递性的三元组：relation(a, b) 且 relation(b, c)，但不是 relation(a, c)。

    【参数】items：选项列表；relation(a, b)：返回 bool
    【返回】找到时返回 (a, b, c)，否则返回 None
    【Python 小知识】itertools.permutations(items, 3)：列出所有"有顺序的三元组"（不重复取）。
    """
    for a, b, c in itertools.permutations(items, 3):
        if relation(a, b) and relation(b, c) and not relation(a, c):
            return (a, b, c)
    return None


def demo_axioms():
    print_header("演示 1：两条公理 —— 完备性与传递性，以及“咖啡里的糖”")
    # 一个看得见效用的人：效用越大越好。"不差于"就是效用 ≥。
    utils = {"A": 5.4, "B": 5.0, "C": 4.8, "D": 3.0}
    weak = lambda a, b: utils[a] >= utils[b]
    print(f"按效用比较 {utils}：")
    print(f"  完备吗？{is_complete(list(utils), weak)}   违反传递性的三元组：{find_intransitive(list(utils), weak)}")

    # Luce（1956）的例子：一杯咖啡加 0、1、2、……、10 粒糖。差一两粒尝不出来（"无差异"），
    # 差很多才尝得出来。设察觉阈值为 2.5 粒：差 ≤ 2.5 粒就觉得一样。
    cups = [f"{k}粒" for k in range(0, 11)]
    sugar = {c: int(c[:-1]) for c in cups}
    threshold = 2.5
    # 这个人喜欢甜（糖越多越好），但只有差得够多才分得出来
    strict = lambda a, b: sugar[a] - sugar[b] > threshold          # a ≻ b
    indiff = lambda a, b: abs(sugar[a] - sugar[b]) <= threshold    # a ~ b
    weak_s = lambda a, b: strict(a, b) or indiff(a, b)             # a ≿ b
    print(f"\n咖啡里的糖：0~10 粒，察觉阈值 {threshold} 粒（差得不够多就尝不出来）")
    print(f"  “不差于”完备吗？{is_complete(cups, weak_s)}")
    print(f"  严格偏好 ≻ 违反传递性吗？{find_intransitive(cups, strict)}   （None 表示没有违反）")
    bad = find_intransitive(cups, indiff)
    print(f"  无差异 ~ 违反传递性的例子：{bad[0]} ~ {bad[1]}，{bad[1]} ~ {bad[2]}，但 {bad[0]} 和 {bad[2]} 不是无差异")
    chain = " ~ ".join(cups[::2])
    print(f"  一条无差异链：{chain}（相邻两杯差 2 粒，都尝不出来），可是 0粒 和 10粒 的差别一尝就知道。")
    print("""
观察：
  · 用一个数（效用）给选项打分，再按大小比较，得到的偏好一定完备、传递 —— 这就是“效用表示”。
    反过来，经济学里有一个经典定理：（有限个选项时）一个偏好关系能用效用表示，当且仅当它完备且传递。
  · 但人的感知有阈值：“差不多”的东西分不出来。无差异关系不传递，一长串“差不多”累积起来就差很多了。
    Luce（1956）把这种结构叫做“半序”（semiorder）。它说明“确定的全序”是一个理想化的假设。
  · 这门课的处理办法不是放弃效用，而是加入**随机性**：差得少的时候选择接近五五开，差得多时几乎确定。
    第 1 讲的 σ(效用差) 就是这样一条“软化”的阈值。""")


# =============================================================================
# 演示 2：随机偏好 = 排序上的分布
# =============================================================================
ITEMS = ["a", "b", "c"]
ORDERS = list(itertools.permutations(ITEMS))      # 3! = 6 种排序


def prob_prefers(dist, x, y):
    """
    在排序分布 dist 下，"x 排在 y 前面"的概率 P(x ≻ y) = Σ_{x 排在 y 前面的排序} P(排序)。

    【参数】dist：dict {排序元组: 概率}；x、y：选项
    【返回】float
    """
    return math.fsum(p for order, p in dist.items() if order.index(x) < order.index(y))


def prob_choice(dist, j, options):
    """
    在排序分布下，从集合 options 里选 j 的概率 = 所有"j 在 options 里排最前"的排序的概率之和。

    【思路】这就是教材里的公式 p(j | S) = Σ_{≻: j ≻ k 对所有 k ∈ S\\{j}} p(≻)。
        选择数据（两两比较、多选一、排序）都是同一个"排序分布"的不同"侧面"（边缘分布）。
    """
    def top_in(order):
        return min(options, key=order.index)
    return math.fsum(p for order, p in dist.items() if top_in(order) == j)


def plackett_luce_dist(scores):
    """
    Plackett–Luce 模型给出的排序分布：P(排序) = Π_每一步 softmax。

    【参数】scores：dict {选项: 分数}
    【返回】dict {排序元组: 概率}
    """
    dist = {}
    for order in ORDERS:
        p = 1.0
        for i in range(len(order) - 1):
            p *= choice_prob(scores, order[i], order[i:])
        dist[order] = p
    return dist


def show_distribution(name, dist):
    """打印一个排序分布，以及它导出的两两胜率和三选一概率。"""
    print(f"\n  【{name}】")
    text = "  ".join(f"{''.join(o)}:{p:.2f}" for o, p in dist.items() if p > 1e-12)
    print(f"    排序的概率：{text}")
    pairs = "  ".join(f"P({x}≻{y})={prob_prefers(dist, x, y):.2f}" for x, y in [("a", "b"), ("b", "c"), ("a", "c")])
    print(f"    两两胜率：  {pairs}")
    choices = "  ".join(f"{j}:{prob_choice(dist, j, ITEMS):.2f}" for j in ITEMS)
    print(f"    从 {{a,b,c}} 选一个：{choices}")


DISTRIBUTIONS = {
    "确定的偏好（总是 a≻b≻c）": {o: (1.0 if o == ("a", "b", "c") else 0.0) for o in ORDERS},
    "均匀分布（完全没有偏好）": {o: 1 / 6 for o in ORDERS},
    "Plackett–Luce（分数 a=1, b=0, c=−1）": plackett_luce_dist({"a": 1.0, "b": 0.0, "c": -1.0}),
    "孔多塞混合（abc、bca、cab 各 1/3）": {o: (1 / 3 if o in [("a", "b", "c"), ("b", "c", "a"), ("c", "a", "b")] else 0.0)
                                       for o in ORDERS},
}
# 两群人：第 1 讲代码 04 的简洁派（60%）和详尽派（40%），每群人内部都是 Plackett–Luce。
# 这里 a = 回答 A（简洁）、b = 回答 C（适中）、c = 回答 B（详尽），分数是两群人心里的效用。
_CONCISE = plackett_luce_dist({"a": 6.60, "b": 4.80, "c": 3.24})
_DETAILED = plackett_luce_dist({"a": 4.20, "b": 4.80, "c": 6.84})
DISTRIBUTIONS["两群人（60% 简洁派 + 40% 详尽派）"] = {o: 0.6 * _CONCISE[o] + 0.4 * _DETAILED[o] for o in ORDERS}


def demo_random_preferences():
    print_header("演示 2：随机偏好 = 排序上的概率分布")
    print("3 个选项 a、b、c 一共有 3! = 6 种排序。一个“随机偏好”就是给这 6 种排序各分配一个概率（共 5 个自由参数）。")
    print("任何一种选择数据，都是从这个分布里推出来的：")
    print("  P(x ≻ y) = 所有“x 排在 y 前面”的排序的概率之和；P(从 S 选 j) = 所有“j 在 S 里排最前”的排序的概率之和。")
    for name, dist in DISTRIBUTIONS.items():
        show_distribution(name, dist)
    print("""
观察：
  · 同一个排序分布，可以导出两两胜率、多选一概率、排序概率 —— 不同的数据形式只是同一个随机偏好的不同“侧面”。
    这就是为什么第 3 节可以把五种数据放进同一个框架。
  · Plackett–Luce 只用 2 个自由参数（分数差）就确定了全部 6 种排序的概率。一般的随机偏好需要 5 个。
    选项一多，差距是天文数字（M = 10 时 9 个 vs 3628799 个，第 1 讲代码 02）。
  · 孔多塞混合：每个人的偏好都是一个全序，可是 a 以 0.67 胜 b、b 以 0.67 胜 c、c 又以 0.67 胜 a，绕成一个圈。
  · “两群人”：每群人内部都是 Plackett–Luce，混在一起以后，三选一时适中的 b 很少被选中（两群人都有更爱的），
    两两比较时 b 却能以过半的概率胜过 c。这类结构“每个选项一个分数”的模型表示不了（下一个演示给出检验方法）。""")


# =============================================================================
# 演示 3：随机传递性
# =============================================================================
def stochastic_transitivity(p):
    """
    检查一个两两胜率矩阵满足哪种"随机传递性"。

    【思路】对所有满足 P(x≻y) ≥ 0.5 且 P(y≻z) ≥ 0.5 的三元组 (x, y, z)，看 P(x≻z)：
            弱随机传递性（WST）：P(x≻z) ≥ 0.5                       —— 多数决不会绕圈
            中随机传递性（MST）：P(x≻z) ≥ min(P(x≻y), P(y≻z))
            强随机传递性（SST）：P(x≻z) ≥ max(P(x≻y), P(y≻z))       —— "跨两级"的胜率不低于任何一级
        BT 模型一定满足 SST：P(x≻z) = σ(Δ_xy + Δ_yz)，两个非负的差加起来只会更大。
        所以如果数据明显违反 SST，BT 模型就不合适。
    【参数】p：dict {(x, y): P(x ≻ y)}，需要包含所有有序对
    【返回】dict {"WST": bool, "MST": bool, "SST": bool, "反例": 第一个违反 SST 的三元组或 None}
    【边界】"≥" 用 1e-12 的容差比较，避免浮点误差造成的假违反。
    """
    keys = sorted({x for x, _ in p})
    ok = {"WST": True, "MST": True, "SST": True, "反例": None}
    eps = 1e-12
    for x, y, z in itertools.permutations(keys, 3):
        pxy, pyz, pxz = p[(x, y)], p[(y, z)], p[(x, z)]
        if pxy >= 0.5 and pyz >= 0.5:
            if pxz < 0.5 - eps:
                ok["WST"] = False
            if pxz < min(pxy, pyz) - eps:
                ok["MST"] = False
            if pxz < max(pxy, pyz) - eps:
                ok["SST"] = False
                if ok["反例"] is None:
                    ok["反例"] = (x, y, z, round(pxy, 3), round(pyz, 3), round(pxz, 3))
    return ok


def demo_stochastic_transitivity():
    print_header("演示 3：随机传递性 —— 一个检验 BT 是否合适的工具")
    print("对每个分布，算出所有两两胜率，检查三种随机传递性：")
    print("  WST：x 多数胜 y、y 多数胜 z ⇒ x 多数胜 z；  MST：P(x≻z) ≥ 较小的那个；  SST：P(x≻z) ≥ 较大的那个\n")
    print(f"  {pad('分布', 38)} {'WST':>5s} {'MST':>5s} {'SST':>5s}   违反 SST 的例子 (x, y, z, P(x≻y), P(y≻z), P(x≻z))")
    for name, dist in DISTRIBUTIONS.items():
        p = {(x, y): prob_prefers(dist, x, y) for x, y in itertools.permutations(ITEMS, 2)}
        r = stochastic_transitivity(p)
        mark = lambda b: "是" if b else "否"
        print(f"  {pad(name, 38)} {pad(mark(r['WST']), 5, 'right')} {pad(mark(r['MST']), 5, 'right')} "
              f"{pad(mark(r['SST']), 5, 'right')}   {r['反例'] if r['反例'] else ''}")

    # 一个 BT 模型（任意分数）和一个 Thurstone 模型（正态噪声）
    scores = {"a": 1.3, "b": 0.2, "c": -0.9}
    p_bt = {(x, y): sigmoid(scores[x] - scores[y]) for x, y in itertools.permutations(ITEMS, 2)}
    r = stochastic_transitivity(p_bt)
    print(f"  {pad('BT 模型（分数 1.3, 0.2, −0.9）', 38)} {pad('是' if r['WST'] else '否', 5, 'right')} "
          f"{pad('是' if r['MST'] else '否', 5, 'right')} {pad('是' if r['SST'] else '否', 5, 'right')}")
    print("""
观察：
  · BT 模型（以及 Plackett–Luce）一定满足强随机传递性：a 胜 b 的把握、b 胜 c 的把握，叠加起来 a 胜 c 只会更有把握。
  · “两群人”满足 WST 和 MST，却违反 SST：a 以 0.66 胜 b、b 以 0.54 胜 c，a 胜 c 却只有 0.61 ——
    比“跨一级”的 0.66 还低。原因是详尽派强烈地偏爱 c，把 a 胜 c 的概率拉了下来。
    把这样的数据当成一个 BT 模型去拟合，就会出现第 1 讲代码 04 里“预测偏了”的现象。
  · 孔多塞混合连 WST 都违反：多数决绕成一个圈。
  · 这些性质都只依赖两两胜率，可以直接在数据上估计和检验（第 4.3 节）。真实的偏好数据常常大体满足 WST、
    但在一些三元组上违反 SST，这是“需要比 BT 更丰富的模型”的信号（第 3、4 讲）。""")


def main():
    demo_axioms()
    demo_random_preferences()
    demo_stochastic_transitivity()


if __name__ == "__main__":
    main()
