# -*- coding: utf-8 -*-
"""
ICM 2020 D — The Wisdom of Teams（团队协作的智慧）
真实赛题：COMAP 国际交叉学科建模竞赛（ICM）D 题——足球传球网络与团队协作分析。
  T1 为球员间传球建立网络（节点=球员，边=传球），识别二元/三元结构、阵型。
  T2 跨尺度/跨时间指标（微观成对→宏观全体；每分钟→整场/赛季）。
  T3 确定反映成功团队合作的绩效指标（打法多样性、协调性、贡献分配；适应性/节奏/流程）。
  T4 建模型捕捉团队合作的结构-配置-动态；给 Huskies 教练结构性策略建议。
  T5 推广：如何设计更有效的（通用）团队，通用团队绩效模型还需哪些维度。

真源模型（纯标准库，确定性；SEED=2020 锚定随机流，便于逐次复现）：
  采用「确定性合成传球网络」方法，参数取自真实赛题数据量级
  （366 球员 / 23,429 传球 / 59,271 事件；本模型聚焦 Huskies 11 人首发网络，
   赛季 38 场、场均 618 次传球，与真实量级一致）：
    · 11 名首发按 4-3-3 阵型布置于 105×68 球场坐标，角色 GK/DEF/MID/FWD。
    · 传球生成：每场按角色权重选传球者，按「角色亲和矩阵 × 空间邻近
      (exp(-d/σ))」选接球者，累计成加权有向邻接 W[i][j]（i→j 传球量）。
    · 网络指标：出/入度、强度、密度、互惠性、加权聚类系数(Fagiolo)、
      加权介数(Brandes/Dijkstra)、特征向量中心性、直径/平均路径长。
    · 二元结构：最强传球对（dyad）；三元结构：三元普查（transitive/cyclic/
      divergent/convergent 等 16 类归并）。
    · 阵型识别：按 x 坐标带聚类 → 1-4-3-3。
    · 团队绩效指标(TPI)：归一化加权合成
        div=打法多样性(出边分布香农熵) / coord=协调性(0.5密度+0.5互惠)
        / balance=贡献均衡(1-Gini) / adapt=适应性(上下半场 TPI 稳定度)
        / tempo=节奏(每分钟传球)。权重 [0.25,0.20,0.25,0.15,0.15]。
    · 对手对照：8 支对手队（参数扰动）各算 TPI，取均值对照 Huskies。
    · 教练建议：敏感度 what-if（跨线传球 +15% → TPI 增量）。
    · 通用团队设计：由网络科学提炼原则（密度最优区间、模块性、桥接角色、
      中心化–分布式均衡）。

输出：assets/problems/data/icm2020d.csv（9 行队伍面板：Huskies + 8 对手 + TPI 与分项）
作者说明：本模型为方法论演示用确定性合成网络（种子 2020），几何/传球参数取自公开赛事量级，
         非 Huskies 真实逐球数据；真实参赛应以官方逐球事件数据(csv)标定，本模型聚焦
         「传球网络-结构指标-团队绩效-策略建议-通用推广」的可复现链路。
"""
import os
import math
import csv
import random

SEED = 2020
HERE = os.path.dirname(os.path.abspath(__file__))

# ---------------------------------------------------------------------------
# 0. 模型常数（显式声明，便于敏感性分析）
# ---------------------------------------------------------------------------
PITCH_W = 105.0
PITCH_H = 68.0
N_MATCHES = 38
PASS_PER_MATCH = 618          # 场均传球；38×618 = 23484 ≈ 真实 23429 量级
SIGMA = 40.0                  # 空间邻近尺度（球场单位）
TH = 150.0                   # 显著边阈值（赛季累计传球；用于二值结构指标）
ROLES = ["GK", "DEF", "MID", "FWD"]

# Huskies 首发 11 人（4-3-3，攻击方向 +x；坐标为确定性合成）
_SQUAD = [
    (0, "GK1", "GK", 8, 34),
    (1, "DF1", "DEF", 22, 12),
    (2, "DF2", "DEF", 22, 27),
    (3, "DF3", "DEF", 22, 41),
    (4, "DF4", "DEF", 22, 56),
    (5, "MF1", "MID", 48, 20),
    (6, "MF2", "MID", 48, 34),
    (7, "MF3", "MID", 48, 48),
    (8, "FW1", "FWD", 78, 22),
    (9, "FW2", "FWD", 78, 34),
    (10, "FW3", "FWD", 78, 46),
]

# 传球者角色权重（Huskies）
ROLE_W_HUSK = {"GK": 0.04, "DEF": 0.30, "MID": 0.42, "FWD": 0.24}
# 角色亲和矩阵 AFF[from][to]：接球者角色偏好
AFF_HUSK = {
    "GK":  {"GK": 0.00, "DEF": 0.85, "MID": 0.15, "FWD": 0.00},
    "DEF": {"GK": 0.05, "DEF": 0.20, "MID": 0.65, "FWD": 0.10},
    "MID": {"GK": 0.02, "DEF": 0.18, "MID": 0.45, "FWD": 0.35},
    "FWD": {"GK": 0.00, "DEF": 0.05, "MID": 0.55, "FWD": 0.40},
}

# TPI 权重
W_DIV, W_COORD, W_BAL, W_ADAPT, W_TEMPO = 0.25, 0.20, 0.25, 0.15, 0.15


# ---------------------------------------------------------------------------
# 1. 传球网络模拟
# ---------------------------------------------------------------------------
def _squad():
    return [(i, nm, role, x, y) for (i, nm, role, x, y) in _SQUAD]


def _simulate(seed, role_w, aff, ppm, sigma, n_matches=N_MATCHES):
    rng = random.Random(seed)
    squad = _squad()
    n = len(squad)
    W = {}
    W1 = {}     # 上半场(≤45min)
    W2 = {}     # 下半场(>45min)
    per_min = [0] * 91
    for _m in range(n_matches):
        for _ in range(ppm):
            # 选传球者：按角色权重 × 同角色内均匀
            pw = [role_w[squad[i][2]] / 1.0 for i in range(n)]
            tot = sum(pw)
            pw = [p / tot for p in pw]
            i = rng.choices(range(n), weights=pw, k=1)[0]
            ri = squad[i][2]
            xi, yi = squad[i][3], squad[i][4]
            # 选接球者：AFF × 邻近
            cands = []
            wts = []
            for j in range(n):
                if j == i:
                    continue
                rj = squad[j][2]
                xj, yj = squad[j][3], squad[j][4]
                d = math.hypot(xi - xj, yi - yj)
                w = aff[ri][rj] * math.exp(-d / sigma)
                cands.append(j)
                wts.append(w)
            s = sum(wts)
            if s <= 0:
                continue
            wts = [w / s for w in wts]
            j = rng.choices(cands, weights=wts, k=1)[0]
            W[(i, j)] = W.get((i, j), 0.0) + 1.0
            mn = int(min(90, max(1, rng.gauss(45, 22))))
            per_min[mn] += 1
            if mn <= 45:
                W1[(i, j)] = W1.get((i, j), 0.0) + 1.0
            else:
                W2[(i, j)] = W2.get((i, j), 0.0) + 1.0
    return W, W1, W2, per_min


# ---------------------------------------------------------------------------
# 2. 网络指标
# ---------------------------------------------------------------------------
def _adj_bin(W, n):
    A = [[0] * n for _ in range(n)]
    for (i, j), v in W.items():
        if v > 0:
            A[i][j] = 1
    return A


def _entropy_norm(W, n):
    """每球员出边分布香农熵，归一化到 0-1；团队多样性=均值。"""
    hs = []
    for i in range(n):
        s = sum(W.get((i, j), 0.0) for j in range(n) if j != i)
        if s <= 0:
            hs.append(0.0)
            continue
        h = 0.0
        for j in range(n):
            if j == i:
                continue
            p = W.get((i, j), 0.0) / s
            if p > 0:
                h -= p * math.log(p)
        hs.append(h / math.log(n - 1) if n > 1 else 0.0)
    return sum(hs) / n


def _gini(vals):
    xs = sorted(vals)
    m = len(xs)
    if m == 0 or sum(xs) == 0:
        return 0.0
    cum = 0.0
    for k, x in enumerate(xs, 1):
        cum += k * x
    return (2.0 * cum) / (m * sum(xs)) - (m + 1.0) / m


def _clustering(W, n):
    """加权聚类系数(Fagiolo)，权重先归一化到 [0,1]，取均值。"""
    maxw = max(W.values()) if W else 1.0
    maxw = maxw or 1.0
    cs = []
    for i in range(n):
        si = (sum(W.get((i, j), 0.0) for j in range(n)) +
              sum(W.get((j, i), 0.0) for j in range(n))) / maxw
        if si == 0:
            cs.append(0.0)
            continue
        num = 0.0
        for j in range(n):
            if j == i:
                continue
            for k in range(n):
                if k == i or k == j:
                    continue
                wij = W.get((i, j), 0.0) / maxw
                wik = W.get((i, k), 0.0) / maxw
                wjk = W.get((j, k), 0.0) / maxw
                if wij > 0 and wik > 0 and wjk > 0:
                    num += (wij * wik * wjk) ** (1.0 / 3.0)
        den = si * (si - 1.0) if si > 1.0 else 0.0
        cv = num / den if den > 0 else 0.0
        cs.append(min(1.0, max(0.0, cv)))
    return sum(cs) / n


def _brandes(W, n):
    """加权介数中心性（距离=1/(1+w)）。返回列表。"""
    import heapq
    bet = [0.0] * n
    for s in range(n):
        dist = [float("inf")] * n
        dist[s] = 0.0
        prev = [[] for _ in range(n)]
        pq = [(0.0, s)]
        while pq:
            du, u = heapq.heappop(pq)
            if du > dist[u]:
                continue
            for (i, j), w in W.items():
                if i != u:
                    continue
                v = j
                nd = du + 1.0 / (1.0 + w)
                if nd < dist[v]:
                    dist[v] = nd
                    prev[v] = [u]
                    heapq.heappush(pq, (nd, v))
                elif abs(nd - dist[v]) < 1e-9:
                    prev[v].append(u)
        # accumulation
        delta = [0.0] * n
        stk = [v for v in range(n) if v != s]
        # topological by distance
        stk.sort(key=lambda x: -dist[x])
        for w in stk:
            for p in prev[w]:
                if dist[w] < float("inf"):
                    c = (1.0 + delta[w]) / 1.0
                    delta[p] += c
            if w != s:
                bet[w] += delta[w] / 2.0
    return bet


def _eigenvector(W, n, iters=60):
    v = [1.0] * n
    for _ in range(iters):
        nv = [0.0] * n
        for (i, j), w in W.items():
            nv[j] += w * v[i]
        s = math.sqrt(sum(x * x for x in nv)) or 1.0
        v = [x / s for x in nv]
    return v


def _triad_class(A, i, j, k):
    e = lambda a, b: 1 if A[a][b] > 0 else 0
    m = sum(1 for (a, b) in [(i, j), (i, k), (j, k)] if e(a, b) and e(b, a))
    a = sum(1 for (x, y) in [(i, j), (i, k), (j, i), (j, k), (k, i), (k, j)]
            if (e(x, y) or e(y, x)) and not (e(x, y) and e(y, x)))
    t = m * 2 + a
    if t == 0:
        return "003"
    if t == 1:
        return "012"
    if t == 2:
        if m == 1:
            return "102"
        outs = [e(i, j) + e(i, k), e(j, i) + e(j, k), e(k, i) + e(k, j)]
        return "021D" if max(outs) == 2 else "021U"
    if t == 3:
        if m == 1:
            return "120"
        outs = [e(i, j) + e(i, k), e(j, i) + e(j, k), e(k, i) + e(k, j)]
        ins = [e(j, i) + e(k, i), e(i, j) + e(k, j), e(i, k) + e(j, k)]
        if all(o == 1 and ins[idx] == 1 for idx, o in enumerate(outs)):
            return "030C"
        return "030T"
    if t == 4:
        return "201" if m == 2 else "120"
    if t == 5:
        return "210"
    return "300"


def _triadic(W, n, th=TH):
    from itertools import combinations
    A = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            if i != j and W.get((i, j), 0.0) >= th:
                A[i][j] = 1
    cnt = {}
    for (i, j, k) in combinations(range(n), 3):
        c = _triad_class(A, i, j, k)
        cnt[c] = cnt.get(c, 0) + 1
    # 归并展示类
    show = {
        "divergent(021D)": cnt.get("021D", 0),
        "convergent(021U)": cnt.get("021U", 0),
        "transitive(030T)": cnt.get("030T", 0),
        "cyclic(030C)": cnt.get("030C", 0),
        "dense(120/201/210/300)": cnt.get("120", 0) + cnt.get("201", 0) + cnt.get("210", 0) + cnt.get("300", 0),
        "open(003/012/102)": cnt.get("003", 0) + cnt.get("012", 0) + cnt.get("102", 0),
    }
    return show, cnt


def _metrics(W, squad, th=TH):
    n = len(squad)
    # 强度（加权，全网络）
    strength = [sum(W.get((i, j), 0.0) for j in range(n)) +
                sum(W.get((j, i), 0.0) for j in range(n)) for i in range(n)]
    total = sum(W.values())
    # 显著边（二值，用于结构指标）：赛季累计传球 ≥ TH
    A = [[0] * n for _ in range(n)]
    sig = 0
    mutual = 0
    for i in range(n):
        for j in range(n):
            if i == j:
                continue
            if W.get((i, j), 0.0) >= th:
                A[i][j] = 1
                sig += 1
                if W.get((j, i), 0.0) >= th:
                    mutual += 1
    dens = sig / (n * (n - 1)) if n > 1 else 0.0
    recip = (mutual / sig) if sig else 0.0
    # 多样性（出边分布熵，加权全网络）
    div = _entropy_norm(W, n)
    # 贡献均衡
    gini = _gini(strength)
    balance = 1.0 - gini
    # 聚类（权重归一化）
    clust = _clustering(W, n)
    # 介数 / 特征向量（加权全网络）
    bet = _brandes(W, n)
    eig = _eigenvector(W, n)
    # 直径 / 平均路径（跳数，基于显著边二值图）
    Dhop = [[float("inf")] * n for _ in range(n)]
    for i in range(n):
        Dhop[i][i] = 0
    for i in range(n):
        for j in range(n):
            if A[i][j]:
                Dhop[i][j] = 1
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if Dhop[i][k] + Dhop[k][j] < Dhop[i][j]:
                    Dhop[i][j] = Dhop[i][k] + Dhop[k][j]
    ds = [Dhop[i][j] for i in range(n) for j in range(n)
          if i != j and Dhop[i][j] < float("inf")]
    diam = max(ds) if ds else 0.0
    avgpath = (sum(ds) / len(ds)) if ds else 0.0
    # 阵型（按 x 坐标带聚类）
    bands = {"GK": 0, "DEF": 0, "MID": 0, "FWD": 0}
    for (i, nm, role, x, y) in squad:
        if x < 15:
            bands["GK"] += 1
        elif x < 35:
            bands["DEF"] += 1
        elif x < 60:
            bands["MID"] += 1
        else:
            bands["FWD"] += 1
    formation = "%d-%d-%d-%d" % (bands["GK"], bands["DEF"], bands["MID"], bands["FWD"])
    # 最强 dyad（互传总量，全网络）
    dyads = []
    for i in range(n):
        for j in range(i + 1, n):
            wij = W.get((i, j), 0.0) + W.get((j, i), 0.0)
            if wij > 0:
                dyads.append((squad[i][1], squad[j][1], wij))
    dyads.sort(key=lambda t: -t[2])
    # 三元结构（显著边二值图）
    triadic, _ = _triadic(W, n, th)
    return dict(
        n=n, strength=strength, total_passes=total, density=dens,
        reciprocity=recip, diversity=div, gini=gini, balance=balance,
        clustering=clust, betweenness=bet, eigenvector=eig,
        diameter=diam, avgpath=avgpath, formation=formation,
        top_dyads=dyads[:6], triadic=triadic,
    )


def _tpi_components(m, adaptability=None, tempo=None):
    div = m["diversity"]
    coord = 0.5 * m["density"] + 0.5 * m["reciprocity"]
    balance = m["balance"]
    adapt = adaptability if adaptability is not None else 0.8
    t = tempo if tempo is not None else 7.0
    tempo_n = min(t / 9.0, 1.0)
    tpi = (W_DIV * div + W_COORD * coord + W_BAL * balance +
           W_ADAPT * adapt + W_TEMPO * tempo_n)
    return dict(div=div, coord=coord, balance=balance, adapt=adapt,
                tempo=tempo_n, tpi=tpi)


# ---------------------------------------------------------------------------
# 3. 主入口
# ---------------------------------------------------------------------------
def gen_icm2020d(seed=SEED, out_csv="icm2020d.csv"):
    squad = _squad()
    n = len(squad)
    W, W1, W2, per_min = _simulate(seed, ROLE_W_HUSK, AFF_HUSK, PASS_PER_MATCH, SIGMA)
    m = _metrics(W, squad)

    # 节奏（每分钟平均传球）
    tempo = sum(per_min) / float(N_MATCHES) / 90.0 * 90.0 / N_MATCHES
    # 实际：场均传球 / 90 分钟
    tempo = m["total_passes"] / float(N_MATCHES) / 90.0

    # 适应性：上下半场结构与贡献稳定度（与阈值无关，用加权全网络指标）
    m1 = _metrics(W1, squad)
    m2 = _metrics(W2, squad)
    d1, b1 = m1["diversity"], m1["balance"]
    d2, b2 = m2["diversity"], m2["balance"]
    adaptability = (max(0.0, 1.0 - 0.5 * (abs(d2 - d1) / d1 + abs(b2 - b1) / b1))
                    if (d1 > 0 and b1 > 0) else 0.0)
    c1 = _tpi_components(m1, adaptability, tempo)
    c2 = _tpi_components(m2, adaptability, tempo)
    tpi1, tpi2 = c1["tpi"], c2["tpi"]

    comp = _tpi_components(m, adaptability, tempo)
    TPI = comp["tpi"]

    # 对手对照：8 队（参数扰动）
    opp_defs = [
        ("Opp-A 控球型",   {"GK": 0.03, "DEF": 0.25, "MID": 0.48, "FWD": 0.24}, 660, 36),
        ("Opp-B 防守型",   {"GK": 0.06, "DEF": 0.40, "MID": 0.34, "FWD": 0.20}, 560, 44),
        ("Opp-C 边路型",   {"GK": 0.04, "DEF": 0.28, "MID": 0.40, "FWD": 0.28}, 600, 38),
        ("Opp-D 长传型",   {"GK": 0.05, "DEF": 0.35, "MID": 0.35, "FWD": 0.25}, 540, 55),
        ("Opp-E 均衡型",   {"GK": 0.04, "DEF": 0.30, "MID": 0.42, "FWD": 0.24}, 618, 40),
        ("Opp-F 高位逼抢", {"GK": 0.03, "DEF": 0.22, "MID": 0.45, "FWD": 0.30}, 650, 34),
        ("Opp-G 快速反击", {"GK": 0.05, "DEF": 0.33, "MID": 0.37, "FWD": 0.25}, 580, 50),
        ("Opp-H 传控渗透", {"GK": 0.03, "DEF": 0.26, "MID": 0.46, "FWD": 0.25}, 640, 37),
    ]
    opponents = []
    for idx, (nm, rw, ppm, sg) in enumerate(opp_defs):
        Wo, _, _, _ = _simulate(seed + 10 + idx, rw, AFF_HUSK, ppm, sg)
        mo = _metrics(Wo, squad)
        to = mo["total_passes"] / float(N_MATCHES) / 90.0
        co = _tpi_components(mo, 0.80, to)
        opponents.append(dict(name=nm, tpi=co["tpi"], div=co["div"], coord=co["coord"],
                              balance=co["balance"], adapt=co["adapt"], tempo=co["tempo"],
                              density=mo["density"], reciprocity=mo["reciprocity"],
                              gini=mo["gini"], formation=mo["formation"],
                              clustering=mo["clustering"], total_passes=mo["total_passes"]))
    opp_mean = sum(o["tpi"] for o in opponents) / len(opponents)

    # what-if：传球分布向均值均衡化 15% → 贡献更均衡、打法更多样（正增益）
    mean_w = m["total_passes"] / float(n * (n - 1))
    Ww = {}
    for (i, j), v in W.items():
        Ww[(i, j)] = v * 0.85 + mean_w * 0.15
    mw = _metrics(Ww, squad)
    cw = _tpi_components(mw, adaptability, tempo)
    whatif_delta = cw["tpi"] - TPI

    # 敏感性：TPI 对各参数扰动
    sens = []
    # 多样性权重 ±0.05
    base_div_w = W_DIV
    for dw, label in [(+0.05, "多样性权重+0.05"), (-0.05, "多样性权重-0.05")]:
        tpi_s = (base_div_w + dw) * m["diversity"] + W_COORD * comp["coord"] + W_BAL * comp["balance"] + W_ADAPT * comp["adapt"] + W_TEMPO * comp["tempo"]
        sens.append((label, round((tpi_s - TPI) * 100, 2)))
    # 传球量 ±20%
    for mult, label in [(1.20, "传球量+20%"), (0.80, "传球量-20%")]:
        Ws = {(i, j): v * mult for (i, j), v in W.items()}
        ms = _metrics(Ws, squad)
        cs = _tpi_components(ms, adaptability, tempo * mult)
        sens.append((label, round((cs["tpi"] - TPI) * 100, 2)))
    # σ 邻近 ±20%
    for mult, label in [(1.20, "邻近尺度σ+20%"), (0.80, "邻近尺度σ-20%")]:
        Ws, _, _, _ = _simulate(seed, ROLE_W_HUSK, AFF_HUSK, PASS_PER_MATCH, SIGMA * mult)
        ms = _metrics(Ws, squad)
        cs = _tpi_components(ms, adaptability, tempo)
        sens.append((label, round((cs["tpi"] - TPI) * 100, 2)))

    # 时序（每分钟平均传球，按场均）
    temporal = [per_min[t] / float(N_MATCHES) for t in range(1, 91)]

    # ---- 写 CSV（9 行队伍面板）----
    OUT_DIR = os.path.normpath(os.path.join(HERE, "..", "assets", "problems", "data"))
    os.makedirs(OUT_DIR, exist_ok=True)
    csv_path = os.path.join(OUT_DIR, out_csv)
    cols = ["team", "formation", "total_passes", "density", "reciprocity",
            "diversity", "gini", "balance", "clustering", "tempo_ppm", "TPI"]
    rows = [("Huskies(主队)", m["formation"], round(m["total_passes"], 0),
             round(m["density"], 3), round(m["reciprocity"], 3),
             round(m["diversity"], 3), round(m["gini"], 3), round(m["balance"], 3),
             round(m["clustering"], 3), round(tempo, 2), round(TPI, 3))]
    for o in opponents:
        rows.append((o["name"], o["formation"], round(o["total_passes"], 0),
                     round(o["density"], 3), round(o["reciprocity"], 3),
                     round(o["div"], 3), round(o["gini"], 3), round(o["balance"], 3),
                     round(o["clustering"], 3), round(o["tempo"] * 9.0, 2), round(o["tpi"], 3)))
    with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
        w = csv.writer(f)
        w.writerow(cols)
        for r in rows:
            w.writerow(r)

    D = dict(
        SEED=SEED, squad=squad, n=n, metrics=m, comp=comp, TPI=TPI,
        tempo=tempo, adaptability=adaptability, tpi_h1=tpi1, tpi_h2=tpi2,
        opponents=opponents, opp_mean_tpi=opp_mean,
        whatif_delta=whatif_delta, whatif_tpi=cw["tpi"],
        sensitivity=sens, temporal=temporal,
        csv_path=csv_path,
        summary=dict(
            total_passes=round(m["total_passes"], 0),
            formation=m["formation"],
            density=round(m["density"], 3),
            reciprocity=round(m["reciprocity"], 3),
            diversity=round(m["diversity"], 3),
            gini=round(m["gini"], 3),
            balance=round(m["balance"], 3),
            clustering=round(m["clustering"], 3),
            diameter=round(m["diameter"], 3),
            avgpath=round(m["avgpath"], 3),
            tpi=round(TPI, 3),
            opp_mean_tpi=round(opp_mean, 3),
            whatif_delta=round(whatif_delta, 3),
            adapt=round(adaptability, 3),
            top_dyad=m["top_dyads"][0],
            triadic=m["triadic"],
        ),
    )
    return D


if __name__ == "__main__":
    D = gen_icm2020d()
    s = D["summary"]
    m = D["metrics"]
    print("=== Huskies 传球网络（4-3-3，赛季 %d 场）===" % N_MATCHES)
    print("总传球量 = %d（场均 %.0f，≈真实 23429 量级）" % (int(s["total_passes"]), s["total_passes"] / N_MATCHES))
    print("识别阵型 = %s" % s["formation"])
    print("密度 = %.3f  互惠性 = %.3f" % (s["density"], s["reciprocity"]))
    print("打法多样性(熵归一) = %.3f  贡献 Gini = %.3f → 均衡 = %.3f" % (s["diversity"], s["gini"], s["balance"]))
    print("加权聚类系数 = %.3f  直径 = %.3f  平均路径 = %.3f" % (s["clustering"], s["diameter"], s["avgpath"]))
    print("最强传球对 = %s ↔ %s（%d 次）" % (s["top_dyad"][0], s["top_dyad"][1], int(s["top_dyad"][2])))
    print("三元结构：", s["triadic"])
    c = D["comp"]
    print("\nTPI 分项：多样性=%.3f 协调性=%.3f 均衡=%.3f 适应性=%.3f 节奏=%.3f"
          % (c["div"], c["coord"], c["balance"], c["adapt"], c["tempo"]))
    print("Huskies TPI = %.3f  对手均值 TPI = %.3f" % (s["tpi"], s["opp_mean_tpi"]))
    print("适应性（上下半场稳定度）= %.3f" % s["adapt"])
    print("What-if 传球分布均衡化15%% → TPI 增量 = %+.3f（→ %.3f）" % (s["whatif_delta"], D["whatif_tpi"]))
    print("\n敏感性（TPI 变化百分点）：")
    for lbl, d in D["sensitivity"]:
        print("  %-18s %+.2f" % (lbl, d))
    print("\n对手 TPI：")
    for o in D["opponents"]:
        print("  %-14s TPI=%.3f  阵型=%s" % (o["name"], o["tpi"], o["formation"]))
    print("\nCSV ->", D["csv_path"])
