Files
bet/pinnacle_experiments.py
2025-10-25 16:39:27 +08:00

114 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import numpy as np
import pandas as pd
from scipy.special import logit as sp_logit
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss
def compute_metrics(
df: pd.DataFrame,
n_bins: int = 10,
bin_strategy: str = "uniform", # 'uniform' or 'quantile'
include_draws: bool = True,
eps: float = 1e-6,
) -> dict:
"""
计算预测评估指标并拟合校准关系。
参数:
- df: 包含至少两列: 'win_prob' (预测主胜概率), 'res' (取 'won','refunded','lost')
- n_bins: ECE 分箱数
- bin_strategy: 'uniform' (等宽) 或 'quantile' (等频)
- include_draws: 若 True, 将 'draw' 视为非胜 (y=0)。若 False, 丢弃 'draw' 行。
- eps: 概率裁剪下限,用于数值稳定
返回:
dict 包含 logloss, brier, ece, accuracy, reg_alpha, reg_beta, ece_bins, n_samples
"""
# 处理 refunded
if include_draws:
mask = df["res"].isin(["won", "refunded", "lost"])
else:
mask = df["res"].isin(["won", "lost"])
df = df[mask].copy()
# 标签: won=1, others=0 (包括 refunded)
y = df["res"].map({"won": 1, "refunded": 0, "lost": 0}).astype(int).values
p = df["win_prob"].astype(float).values
# 裁剪概率以保证数值稳定
p_clip = np.clip(p, eps, 1 - eps)
# logloss: 使用 sklearn 实现以获得更稳健的数值行为
try:
logloss = float(log_loss(y, p_clip, labels=[0, 1]))
except Exception:
# 备用实现
logloss = float(-np.mean(y * np.log(p_clip) + (1 - y) * np.log(1 - p_clip)))
# brier score
brier = float(np.mean((p_clip - y) ** 2))
# ECE 计算(支持 uniform 或 quantile
if bin_strategy == "quantile":
# quantile bin edges
try:
edges = np.unique(np.percentile(p_clip, np.linspace(0, 100, n_bins + 1)))
if len(edges) - 1 <= 0:
# fallback to uniform
bin_idxs = np.minimum((p_clip * n_bins).astype(int), n_bins - 1)
else:
# searchsorted to assign bins
bin_idxs = np.clip(
np.searchsorted(edges, p_clip, side="right") - 1, 0, len(edges) - 2
)
except Exception:
bin_idxs = np.minimum((p_clip * n_bins).astype(int), n_bins - 1)
else:
bin_idxs = np.minimum((p_clip * n_bins).astype(int), n_bins - 1)
ece = 0.0
total = len(y)
bin_stats = []
for b in range(n_bins):
idx = bin_idxs == b
count = int(idx.sum())
if count == 0:
bin_stats.append(
{"count": 0, "mean_pred": float("nan"), "emp_freq": float("nan")}
)
continue
mean_pred = float(p_clip[idx].mean())
emp_freq = float(y[idx].mean())
ece += abs(mean_pred - emp_freq) * count
bin_stats.append({"count": count, "mean_pred": mean_pred, "emp_freq": emp_freq})
ece = float(ece / total) if total > 0 else float("nan")
# accuracy
acc = float(np.mean((p_clip >= 0.5) == (y == 1)))
# 校准拟合: 使用 LogisticRegression 拟合 logit(E[y]) = alpha + beta * logit(p)
X = sp_logit(p_clip).reshape(-1, 1)
clf = LogisticRegression(C=1e6, solver="lbfgs", max_iter=200)
clf.fit(X, y)
alpha = float(clf.intercept_[0])
beta = float(clf.coef_[0][0])
return {
"logloss": logloss,
"brier": brier,
"ece": ece,
"accuracy": acc,
"reg_alpha": alpha,
"reg_beta": beta,
# 'ece_bins': bin_stats,
"n_samples": int(total),
}
if __name__ == "__main__":
df = pd.read_feather("data/p_res.feather")
df["win_prob"] = df["power_p"]
res = compute_metrics(df)
print(res)