阅读时间 16 分钟

用Poisson泊松分布做足球比分概率

如何制作泊松分布足球预测模型
用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[].seasonId

查询对应赛季比赛

seasons[].season

显示赛季名称

脚本不会写死联赛或赛季 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 场。

需要的返回字段:

字段

用途

matchId

比赛去重

matchTime

正式回测时按时间排序

realStatus

只保留 -10 的完赛比赛

homeIdawayId

识别主客队

homeNameawayName

根据球队名称找到 ID

homeCountawayCount

计算进球和失球均值

当前 API 返回的各种 ID 是字符串。程序统一把 ID 当作字符串保存,不参与数学运算。

四、为什么不直接调用球队统计接口

球队统计接口可以让产品快速展示场均进球和场均失球。

但教学版选择从比赛列表自行计算,有三个原因:

  1. 可以让新手看懂每个数字从哪些比赛产生。
  2. 可以严格区分球队主场和客场表现。
  3. 做历史回测时,可以只使用目标比赛之前的记录,避免使用赛季结束后的统计预测过去比赛。

正式产品可以把球队统计接口作为快捷数据源,但历史模型必须确认它是否提供对应比赛时点的历史快照。

五、准备运行

示例脚本:

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、3
  • P(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-08-8 的全部比分概率,可以直接用 Excel 打开,也可以制作比分热力图。

最终 HTML 成果页

生成可交互页面:

python3 football-model-blog/demos/poisson-score/generate_poisson_dashboard.py

打开:

football-model-blog/showcase/poisson-api-dashboard.html

这个单文件页面已经内置真实 API 模型结果,无需安装网页环境。页面可以:

  • 展示最可能比分和胜、平、负概率
  • 查看 0-06-6 的比分概率热力矩阵
  • 拖动主客队进球期望,实时重算全部概率
  • 一键切换“势均力敌”和“对攻模式”实验场景
  • 下载完整模型结果 JSON 和 0-08-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

否则你会用未来比赛或赛季结束统计预测过去,产生数据泄露。

新赛季样本不足怎么办

新赛季刚开始时,球队主客场样本很少。可以:

  1. 使用上赛季数据作为基础。
  2. 随着本赛季比赛增加,逐渐提高本赛季权重。
  3. 为升班马设置联赛平均先验,避免只有一两场就出现极端值。
  4. 规定最小样本数,例如主场和客场至少各 5 场。

十六、怎样加入近期状态

基础模型使用整个赛季的主客场数据,稳定但反应较慢。

可以再计算最近 5 场或 8 场进失球,并进行加权:

调整后进球期望
= 赛季模型进球期望 × 70%
+ 近期模型进球期望 × 30%

70% / 30% 只是教学起点,必须通过历史回测决定,不能凭感觉认定为最优。

球队近期比赛接口可以用于未来赛前展示;如果要做历史回测,仍然要确认返回的是目标比赛当时的历史状态,而不是今天的近期状态。

十七、赛后怎样回测

不要只挑一场比赛看结果。

每场历史比赛都应保存:

字段

用途

matchId

关联模型输出和真实结果

featureCutoff

说明特征截止到什么时间

homeGoalExpectation

主队赛前进球期望

awayGoalExpectation

客队赛前进球期望

homeWinProbability

主胜概率

drawProbability

平局概率

awayWinProbability

客胜概率

homeCountawayCount

赛后真实比分

推荐检查:

  • 胜平负概率的 Brier Score
  • 概率区间是否校准
  • 比分分布的对数损失
  • 不同联赛分别表现如何
  • 加入近期状态前后是否真的改善

例如模型给出主胜概率 50%-60% 的比赛,长期主胜比例也应接近这个区间。概率校准比单场是否猜中更重要。

十八、Poisson 模型的边界

这个基础模型默认:

  • 主客队进球相互独立
  • 球队进球能力在统计窗口中相对稳定
  • 历史场均进球能够代表下一场进球期望

它没有自动考虑:

  • 伤停和阵容变化
  • 赛程密度与轮换
  • 红牌等比赛内事件
  • 战术克制
  • 近期换帅
  • 比分之间的低比分相关性

因此 Poisson 最适合做透明的基准模型。

下一步可以使用 Dixon-Coles 修正 0-01-00-11-1 等低比分,再与基础 Poisson 做滚动回测比较。

十九、常见问题

为什么拜仁进球期望不是场均 4 球?

因为模型还会结合多特的客场防守表现和联赛平均进球环境。球队自己的场均进球只是输入之一。

主胜 60.34% 是否表示拜仁一定赢?

不是。它表示在当前模型和当前输入下,主胜比分格子的总概率为 60.34%。其余结果仍然占 39.66%。

为什么 2-1 只有 9.17%?

因为概率分散在很多比分上。即使主胜总体占优,具体某个比分仍然可能只有个位数概率。

可以直接把这个概率放进产品吗?

可以作为基础模型结果和教学功能,但上线前应完成历史滚动回测、概率校准和数据截止时间检查。

为什么只先做一个联赛?

不同联赛进球环境不同。先在单联赛跑通,可以避免把不同基准混在一起。后续程序已经支持切换其他联赛。

总结

这篇教程真正完成的不是一个数学公式,而是一条真实的数据产品链路:

NJSTATS API
-> 304 场完赛数据
-> 联赛与球队攻防强度
-> 主客队进球期望
-> 81 个比分概率
-> 胜平负、总进球与双方进球概率
-> JSON、CSV 与后续回测

对于新手来说,Poisson 最大的价值是透明。

每个结果都能追溯到 API 中的比赛,每个中间数字都能重新计算,每个概率都能在赛后验证。这正是从“会调用接口”走向“能用数据做模型”的第一步。

数据来源:njstats.cn