用Poisson泊松分布做足球比分概率
很多朋友对足球预测模型有浓厚的兴趣,其中泊松分布就是一个可用于足球预测的经典的模型。
简单来说,泊松模型就是帮你生成一张比分概率表。再把比分概率加起来,就能得到主胜、平局、客胜、总进球、双方进球这些更容易理解的结果。

这篇教程会从鸟叫足球数据 API 数据出发,完成一条可以运行的 Poisson 建模链路:
联赛与赛季
-> 历史完赛比分
-> 联赛进球基准
-> 球队主客场攻防强度
-> 主客队进球期望
-> 比分概率矩阵
-> 胜平负与总进球概率教学案例使用 NJSTATS API 中的德甲 2025/2026 赛季,模拟:
拜仁慕尼黑(主) vs 多特蒙德(客)当前五大联赛处于休赛期,所以这是使用完整赛季统计进行的下一场对阵模拟,不是把赛季结束数据回填到一场已经结束的历史比赛。
一、最终会得到什么
真实运行结果:
项目 | 数值 |
|---|---|
德甲完赛样本 | 304 场 |
拜仁主队进球期望 | 2.384 |
多特客队进球期望 | 1.350 |
主胜概率 | 60.34% |
平局概率 | 19.00% |
客胜概率 | 20.66% |
总进球超过 2.5 概率 | 72.02% |
双方进球概率 | 67.25% |
最可能的比分:
排名 | 比分 | 概率 |
|---|---|---|
1 | 2-1 | 9.17% |
2 | 1-1 | 7.70% |
3 | 3-1 | 7.29% |
4 | 2-0 | 6.79% |
5 | 2-2 | 6.19% |
这些结果不是从接口直接拿到的,而是程序根据 API 返回的真实比分数据计算出来的。
二、Poisson 模型解决什么问题
Poisson 分布适合回答下面这个问题:
如果主队本场平均可能进 2.384 球,
客队本场平均可能进 1.350 球,
那么双方实际进 0、1、2、3 球的概率分别是多少?足球进球只能是整数,但球队的进球期望可以是小数。
Poisson 会把一个进球期望转换成一组进球数量概率。主队算一次、客队算一次,再把双方组合,就得到完整比分矩阵。
例如:
主队进 2 球的概率 × 客队进 1 球的概率
= 2-1 比分的概率比分矩阵中的结果继续相加,就可以得到:
- 主胜概率
- 平局概率
- 客胜概率
- 总进球超过 2.5 概率
- 总进球不超过 2.5 概率
- 双方都有进球的概率
所以 Poisson 不是凭空产生一个赛果,而是先生成比分分布,再汇总成更容易使用的结果。
三、需要调用哪些 API
这个版本只需要两个接口。
API文档地址:https://www.njstats.cn/client/data-api/docs/1-0
1. 联赛列表
GET https://api.njstats.cn/api/v1/league/list用途:找到联赛和赛季。
需要读取:
字段 | 用途 |
|---|---|
| 联赛 ID |
| 根据“德甲”找到目标联赛 |
| 查询对应赛季比赛 |
| 显示赛季名称 |
脚本不会写死联赛或赛季 ID,而是先根据 nameCn=德甲 找到当前接口返回的联赛记录,再从 seasons[] 中选择拥有足够完赛数据的赛季。
ID 应被视为当前 API 环境中的不透明字符串。如果你的接口返回 ID 与旧截图、旧缓存或文章示例不同,应始终使用同一次联赛列表和比赛列表请求返回的 ID,不能混用不同环境的 ID。
2. 比赛列表,也就是联赛赛程
API文档地址:https://www.njstats.cn/client/data-api/docs/1-1
GET https://api.njstats.cn/api/v1/match/list请求示例:
?seasonId=YOUR_SEASON_ID&page=1&pageSize=100程序会从第 1 页读取到最后一页,而不是只取前 100 场。
需要的返回字段:
字段 | 用途 |
|---|---|
| 比赛去重 |
| 正式回测时按时间排序 |
| 只保留 |
| 识别主客队 |
| 根据球队名称找到 ID |
| 计算进球和失球均值 |
当前 API 返回的各种 ID 是字符串。程序统一把 ID 当作字符串保存,不参与数学运算。
四、为什么不直接调用球队统计接口
球队统计接口可以让产品快速展示场均进球和场均失球。
但教学版选择从比赛列表自行计算,有三个原因:
- 可以让新手看懂每个数字从哪些比赛产生。
- 可以严格区分球队主场和客场表现。
- 做历史回测时,可以只使用目标比赛之前的记录,避免使用赛季结束后的统计预测过去比赛。
正式产品可以把球队统计接口作为快捷数据源,但历史模型必须确认它是否提供对应比赛时点的历史快照。
五、准备运行
示例脚本:
football-model-blog/demos/poisson-score/njstats_poisson_match.py程序只使用 Python 自带功能,不需要安装第三方库。
进入目录:
cd football-model-blog/demos/poisson-score复制密钥模板:
cp njstats.env.example njstats.env在 njstats.env 中填写自己的 Key:
NJSTATS_API_KEY=YOUR_API_KEY真实密钥文件已经被 .gitignore 排除。不要把 Key 写进文章、程序源码或公开仓库。
运行:
python3 njstats_poisson_match.py使用已经单独保存的密钥文件:
python3 njstats_poisson_match.py \
--env-file "/你的路径/njstats.env"六、第一步:清洗可用于模型的比赛
接口返回赛季的全部比赛,但模型只能使用已经正常结束并且拥有比分的记录。
程序保留:
realStatus == -10
并且 homeCount、awayCount 不为空德甲 2025/2026 赛季最终纳入 304 场比赛。
取消、推迟、未开始或没有最终比分的记录不会进入统计。
七、第二步:计算联赛进球基准
先计算整个联赛的主队场均进球和客队场均进球。
公式:
联赛主队场均进球 = 所有主队进球之和 / 完赛场次
联赛客队场均进球 = 所有客队进球之和 / 完赛场次API 实际数据得到:
指标 | 数值 |
|---|---|
德甲主队场均进球 | 1.776 |
德甲客队场均进球 | 1.461 |
德甲场均总进球 | 3.237 |
联赛基准非常重要。
一支球队场均进 2 球,在场均总进球较低的联赛可能很强;放到高进球联赛里,含义可能不同。Poisson 不应该把所有联赛当成同一个进球环境。
八、第三步:计算球队主客场表现
本场模拟设定拜仁为主队、多特为客队,所以只读取:
- 拜仁的主场比赛
- 多特的客场比赛
API 数据计算结果:
球队与场地 | 样本 | 场均进球 | 场均失球 |
|---|---|---|---|
拜仁主场 | 17 场 | 4.000 | 1.118 |
多特客场 | 17 场 | 1.765 | 1.059 |
不能把拜仁全部比赛的场均进球直接当成主场能力,也不能把多特全部比赛混在客场能力里。
主客场拆分是这个基础模型最重要的改进之一。
九、第四步:把场均数据变成攻防强度
球队的场均进球还要与联赛平均水平比较。
拜仁主场进攻强度
拜仁主场场均进球 / 德甲主队场均进球
= 4.000 / 1.776
= 2.252大于 1,表示拜仁主场进球能力高于德甲主队平均水平。
拜仁主场防守弱度
拜仁主场场均失球 / 德甲客队场均进球
= 1.118 / 1.461
= 0.765小于 1,表示拜仁主场失球少于联赛客队平均水平,防守相对更好。
多特客场进攻强度
多特客场场均进球 / 德甲客队场均进球
= 1.765 / 1.461
= 1.208多特客场防守弱度
多特客场场均失球 / 德甲主队场均进球
= 1.059 / 1.776
= 0.596最终得到:
强度指标 | 数值 |
|---|---|
拜仁主场进攻强度 | 2.252 |
拜仁主场防守弱度 | 0.765 |
多特客场进攻强度 | 1.208 |
多特客场防守弱度 | 0.596 |
十、第五步:计算两队进球期望
主队进球期望:
联赛主队场均进球
× 主队主场进攻强度
× 客队客场防守弱度
= 1.776 × 2.252 × 0.596
= 2.384客队进球期望:
联赛客队场均进球
× 客队客场进攻强度
× 主队主场防守弱度
= 1.461 × 1.208 × 0.765
= 1.350现在 Poisson 模型最关键的两个输入已经得到:
homeGoalExpectation = 2.384
awayGoalExpectation = 1.350这里的进球期望是根据历史进球均值估计的模型参数,不是基于每次射门质量计算的 xG。
十一、第六步:生成比分概率
Poisson 单队进球概率公式:
P(X = k) = e^(-λ) × λ^k / k!其中:
λ是球队进球期望k是实际进球数,例如 0、1、2、3P(X = k)是恰好进k球的概率
比分概率由双方进球概率相乘:
P(主队 2-1 客队)
= P(主队进 2 球) × P(客队进 1 球)程序计算双方 0-8 球的所有组合,并把矩阵重新归一化为 100%。
最可能比分:
比分 | 概率 |
|---|---|
2-1 | 9.17% |
1-1 | 7.70% |
3-1 | 7.29% |
2-0 | 6.79% |
2-2 | 6.19% |
1-0 | 5.70% |
3-0 | 5.40% |
1-2 | 5.20% |
最可能比分只有 9.17%,这很好地说明了足球比分本身具有较大不确定性。
正确的展示方式是一组比分分布,而不是宣称某个比分一定发生。
十二、第七步:汇总成更容易使用的概率
主胜概率
把比分矩阵中所有主队进球大于客队进球的格子相加:
1-0、2-0、2-1、3-0、3-1、3-2……
= 60.34%平局概率
0-0、1-1、2-2、3-3……
= 19.00%客胜概率
0-1、0-2、1-2、1-3……
= 20.66%完整汇总:
输出 | 概率 |
|---|---|
主胜 | 60.34% |
平局 | 19.00% |
客胜 | 20.66% |
总进球超过 2.5 | 72.02% |
总进球不超过 2.5 | 27.98% |
双方进球 | 67.25% |
主胜、平局、客胜相加等于 100%;超过与不超过 2.5 的概率也应相加等于 100%。这是最基础的数据检查。
十三、输出文件怎么看
运行后生成:
football-model-blog/outputs/poisson-api/poisson_match_result.json
保存:
- API 来源
- 联赛和赛季
- 主客队 ID
- 所有中间输入
- 攻防强度
- 主客队进球期望
- 最可能比分
- 汇总概率
这个文件适合网站、应用或后续模型直接读取。
poisson_score_matrix.csv
保存 0-0 到 8-8 的全部比分概率,可以直接用 Excel 打开,也可以制作比分热力图。
最终 HTML 成果页



生成可交互页面:
python3 football-model-blog/demos/poisson-score/generate_poisson_dashboard.py打开:
football-model-blog/showcase/poisson-api-dashboard.html这个单文件页面已经内置真实 API 模型结果,无需安装网页环境。页面可以:
- 展示最可能比分和胜、平、负概率
- 查看
0-0到6-6的比分概率热力矩阵 - 拖动主客队进球期望,实时重算全部概率
- 一键切换“势均力敌”和“对攻模式”实验场景
- 下载完整模型结果 JSON 和
0-0到8-8比分矩阵 CSV
十四、怎样换成其他联赛和球队
例如模拟英超阿森纳主场对曼城:
python3 njstats_poisson_match.py \
--league 英超 \
--home-team 阿森纳 \
--away-team 曼彻斯特城指定固定赛季:
python3 njstats_poisson_match.py \
--league 德甲 \
--season-id YOUR_SEASON_ID \
--home-team 拜仁慕尼黑 \
--away-team 多特蒙德程序会根据比赛列表中的名称找到统一球队 ID。
十五、正式预测未来比赛怎么用
教学示例使用完整赛季统计模拟一场新对阵。
真正预测未来赛程时,流程应改成:
按日期读取未来比赛
-> 获得 matchId、matchTime、homeId、awayId
-> 只读取 matchTime 之前已经结束的历史比赛
-> 计算当时的联赛基准与双方主客场强度
-> 生成主客队进球期望
-> 生成比分矩阵与汇总概率
-> 保存赛前结果
-> 赛后关联真实比分做回测最重要的一行是:
历史比赛的 matchTime 必须小于目标比赛的 matchTime否则你会用未来比赛或赛季结束统计预测过去,产生数据泄露。
新赛季样本不足怎么办
新赛季刚开始时,球队主客场样本很少。可以:
- 使用上赛季数据作为基础。
- 随着本赛季比赛增加,逐渐提高本赛季权重。
- 为升班马设置联赛平均先验,避免只有一两场就出现极端值。
- 规定最小样本数,例如主场和客场至少各 5 场。
十六、怎样加入近期状态
基础模型使用整个赛季的主客场数据,稳定但反应较慢。
可以再计算最近 5 场或 8 场进失球,并进行加权:
调整后进球期望
= 赛季模型进球期望 × 70%
+ 近期模型进球期望 × 30%70% / 30% 只是教学起点,必须通过历史回测决定,不能凭感觉认定为最优。
球队近期比赛接口可以用于未来赛前展示;如果要做历史回测,仍然要确认返回的是目标比赛当时的历史状态,而不是今天的近期状态。
十七、赛后怎样回测
不要只挑一场比赛看结果。
每场历史比赛都应保存:
字段 | 用途 |
|---|---|
| 关联模型输出和真实结果 |
| 说明特征截止到什么时间 |
| 主队赛前进球期望 |
| 客队赛前进球期望 |
| 主胜概率 |
| 平局概率 |
| 客胜概率 |
| 赛后真实比分 |
推荐检查:
- 胜平负概率的 Brier Score
- 概率区间是否校准
- 比分分布的对数损失
- 不同联赛分别表现如何
- 加入近期状态前后是否真的改善
例如模型给出主胜概率 50%-60% 的比赛,长期主胜比例也应接近这个区间。概率校准比单场是否猜中更重要。
十八、Poisson 模型的边界
这个基础模型默认:
- 主客队进球相互独立
- 球队进球能力在统计窗口中相对稳定
- 历史场均进球能够代表下一场进球期望
它没有自动考虑:
- 伤停和阵容变化
- 赛程密度与轮换
- 红牌等比赛内事件
- 战术克制
- 近期换帅
- 比分之间的低比分相关性
因此 Poisson 最适合做透明的基准模型。
下一步可以使用 Dixon-Coles 修正 0-0、1-0、0-1、1-1 等低比分,再与基础 Poisson 做滚动回测比较。
十九、常见问题
为什么拜仁进球期望不是场均 4 球?
因为模型还会结合多特的客场防守表现和联赛平均进球环境。球队自己的场均进球只是输入之一。
主胜 60.34% 是否表示拜仁一定赢?
不是。它表示在当前模型和当前输入下,主胜比分格子的总概率为 60.34%。其余结果仍然占 39.66%。
为什么 2-1 只有 9.17%?
因为概率分散在很多比分上。即使主胜总体占优,具体某个比分仍然可能只有个位数概率。
可以直接把这个概率放进产品吗?
可以作为基础模型结果和教学功能,但上线前应完成历史滚动回测、概率校准和数据截止时间检查。
为什么只先做一个联赛?
不同联赛进球环境不同。先在单联赛跑通,可以避免把不同基准混在一起。后续程序已经支持切换其他联赛。
总结
这篇教程真正完成的不是一个数学公式,而是一条真实的数据产品链路:
NJSTATS API
-> 304 场完赛数据
-> 联赛与球队攻防强度
-> 主客队进球期望
-> 81 个比分概率
-> 胜平负、总进球与双方进球概率
-> JSON、CSV 与后续回测对于新手来说,Poisson 最大的价值是透明。
每个结果都能追溯到 API 中的比赛,每个中间数字都能重新计算,每个概率都能在赛后验证。这正是从“会调用接口”走向“能用数据做模型”的第一步。
数据来源:njstats.cn
评论交流