Repository navigation
Expand file tree
/
Copy pathclub_preseason.py
More file actions
83 lines (72 loc) · 3.47 KB
/
Copy pathclub_preseason.py
File metadata and controls
83 lines (72 loc) · 3.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#!/usr/bin/env python3
"""五大联赛当前赛季季前概率预计算 → data/club/preseason_<码>.json。
网页联赛 tab 直读 JSON(毫秒级),避免请求内跑 5000 次模拟。
参赛名单直接取 ESPN 已发布的整季赛程,不再维护会过期的手工升班马表;模型按场路由:
一般对阵走纯顶级联赛模型,涉及近 7 季顶级池外新面孔时走已回测裁决的 feeder 合训模型。
用法:python3 club_preseason.py [E0 SP1 ...](无参=全部五联赛)
"""
from __future__ import annotations
import datetime as dt
import json
import os
import sys
import tempfile
import clubdata
import clubfixtures
import clubpredict
import clubsim
SEASON = "2026-27"
SIMS = 5000
def path_for(code: str) -> str:
return os.path.join(os.path.dirname(__file__), "data", "club", f"preseason_{code}.json")
def build(code: str, sims: int = SIMS) -> dict:
fx = clubfixtures.load_cached(code)
teams = sorted(set(fx.home_team) | set(fx.away_team)) if len(fx) else []
df = clubdata.load(code)
previous = df[df.date >= df.date.max() - __import__("pandas").Timedelta(days=330)]
expected = len(set(previous.home_team) | set(previous.away_team))
if len(teams) != expected:
raise RuntimeError(f"{code} ESPN 参赛名单不完整:{len(teams)},期望 {expected}")
newcomers = clubpredict.promoted_newcomers(code)
base = clubpredict.get_club_model(code, verbose=False)
promo = clubpredict.get_promoted_model(code, verbose=False) if newcomers else None
class RoutedModel:
def score_matrix(self, home, away, neutral=False):
m = promo if promo is not None and (home in newcomers or away in newcomers) else base
return m.score_matrix(home, away, neutral=neutral)
facts = df.iloc[0:0]
sim = clubsim.SeasonSimulator(RoutedModel(), teams, facts,
clubsim.remaining_pairs(facts, teams), sims=sims,
tiebreak=clubsim.LEAGUE_TIEBREAK.get(code, "gd"))
rows = sim.run()
return {
"league": clubdata.LEAGUES[code], "code": code, "season": SEASON, "mode": "preseason",
"sims": sims, "computed_at": dt.datetime.now().strftime("%Y-%m-%d %H:%M"),
"data_through": str(df.date.max().date()),
"source": "football-data.co.uk + ESPN scoreboard",
"teams": teams, "promoted_newcomers": sorted(newcomers),
"promoted_feeder": clubdata.FEEDER[code],
"promoted_feeder_weight": clubpredict.PROMOTED_FEEDER_W[code],
"rows": rows,
}
def main(codes: list[str] | None = None):
# 库调用方(warmup/测试)显式传 codes,不继承宿主进程的 argv;
# 只有直接 CLI 执行才读 sys.argv。
codes = list(codes) if codes is not None else (sys.argv[1:] or list(clubfixtures.LEAGUE_SLUG))
for code in codes:
print(f"[preseason] {code} 模拟 {SIMS} 次…", flush=True)
d = build(code)
path = path_for(code)
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix=f".preseason_{code}.")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(d, f, ensure_ascii=False, indent=1)
os.replace(tmp, path)
finally:
if os.path.exists(tmp):
os.remove(tmp)
top = d["rows"][0]
print(f"[preseason] {code} 完成:{top['team']} 夺冠 {top['title']*100:.1f}% "
f"→ {path_for(code)}", flush=True)
if __name__ == "__main__":
main(None)