“最近10场”是体育数据里最常被用到、也最少被定义的词。用户以为它是一个明确的数字,系统却要先回答一串问题:这10场是球队的,还是球员自己的?球员缺阵的那几场算不算?替补上场三分钟的比赛要不要计入平均?中途取消的比赛怎么办?只要其中一条没有定好,同一句话就会在不同系统里得到不同的数字,而且每一个数字都“有道理”。这篇文章不讲宏大的架构,只把“最近10场”拆成一张可以落地的规则表,逐条说明半岛体育AI是怎样处理的。
先分清两个“最近10场”
第一种叫球队窗口:取球队最近10场比赛,再看球员在这10场里的表现,缺阵的场次按“没有数据”处理。第二种叫球员窗口:取球员自己最近10次有出场记录的比赛,不管球队间隔了多少场。
假设示例队甲最近12场里,中场球员陈某(虚构)因伤缺席了中间4场。按球队窗口,他只有6场数据;按球员窗口,可以往前多找4场,凑满10场,但这10场跨越的时间更长,包含了他伤前的状态。两种算法没有对错,只是回答的问题不同:前者问“球队近期这段时间里他的贡献”,后者问“他近期每次上场的表现”。
半岛体育AI的默认做法,是在用户没有说明时采用球员窗口,并在答案里写明“按该球员最近10次上场记录计算,覆盖日期从某月某日到某月某日”。如果用户的问法里出现“球队最近10场”“这段时间”这类措辞,则切换成球队窗口。这个默认值本身不是技术问题,而是产品约定,重要的是它必须被明说。
一张窗口定义规则表
下面这张表是我们处理边界情况时使用的规则骨架,所有数据均为示例口径,实际取值以数据源的字段为准。
| 情形 | 默认处理 | 原因 | 用户可调整 |
|---|---|---|---|
| 球员缺阵(未进入比赛名单) | 不计入,也不占用10场名额 | 没有上场就没有表现可言 | 可改为球队窗口 |
| 进入名单但未出场 | 不计入 | minutes = 0,会拉低所有平均值 | 可选“含未出场” |
| 替补出场时间很短 | 计入,但在答案里提示出场时间偏低 | 样本被拉低的是场均,而不是场次 | 可设最低出场时间 |
| 比赛加时 | 计入,并标注含加时 | 加时会增加出场时间与技术统计 | 可选“仅常规时间” |
| 季后赛或杯赛 | 默认仅含常规联赛,并明示 | 不同阶段对手强度与节奏不同 | 可选“含全部赛事” |
| 比赛中途取消或延期 | 取消的不计;延期已补赛的按补赛日期排序 | 排序依据应为实际比赛日期 | 无 |
| 赛季初场次不足10 | 按实际场次计算,并写明“仅N场” | 不能用少于10场的数据冒充10场 | 可选跨赛季补足 |
| 数据源尚未更新到最新一场 | 回答中写明数据截止日期 | 避免用户误以为包含了昨天的比赛 | 无 |
这张表最有意义的部分是最后一列:默认值可以有,但不能不可见、不可改。用户如果发现数字与别处对不上,第一步应该是打开口径,而不是怀疑系统算错了。
把规则写成SQL会长什么样
规则最终要落到查询上。假设有一张球员逐场记录表 player_game_stats,包含 player_id、game_id、game_date、minutes、points、is_playoff、is_cancelled 等字段,球员窗口的查询可以写成这样:
SELECT AVG(points) AS avg_points,
AVG(minutes) AS avg_minutes,
COUNT(*) AS games_counted,
MIN(game_date) AS window_start,
MAX(game_date) AS window_end
FROM (
SELECT points, minutes, game_date
FROM player_game_stats
WHERE player_id = 'P0012'
AND minutes > 0
AND is_cancelled = 0
AND is_playoff = 0
ORDER BY game_date DESC
LIMIT 10
) AS w;
几个细节值得说明。ORDER BY game_date DESC 依据的是比赛实际日期,而不是比赛编号或录入顺序,否则补赛会被排错位置。minutes > 0 是“出场”的判定,如果数据源用 appearances 或 played 之类的字段,要换成对应条件。返回的 window_start 与 window_end 不是装饰,它们是答案里“覆盖日期”的来源。
球队窗口的写法则要先取球队的比赛,再与球员记录左连接:
SELECT AVG(s.points) AS avg_points,
COUNT(s.player_id) AS games_played,
COUNT(g.game_id) AS team_games
FROM (
SELECT game_id
FROM games
WHERE team_id = 'T03'
AND is_cancelled = 0
AND is_playoff = 0
ORDER BY game_date DESC
LIMIT 10
) AS g
LEFT JOIN player_game_stats s
ON s.game_id = g.game_id
AND s.player_id = 'P0012'
AND s.minutes > 0;
这里 games_played 会小于 team_games,两者之差就是球员缺席的场次,可以直接写进回答:“球队最近10场中,他出场6场”。这也是球队窗口更适合回答“这段时间他有多稳定”的原因。
加权平均还是简单平均
窗口取对了,平均值还可能算错。场均得分用简单平均没问题;但如果指标是命中率、传球成功率这类比率,把每场的百分比直接平均,就会给出场次不同、出手次数不同的比赛相同的权重。比如某场只投了2次、命中2次,命中率100%;另一场投了20次、命中8次,命中率40%,两场“平均”是70%,而按总数计算是22次出手命中10次,约45%。差距不小。
所以在半岛体育AI的查询规划里,比率类指标默认按总数相除,先求分子之和与分母之和,再相除;只有明确要求“逐场平均”的时候才做简单平均。这是数据口径的问题,与模型的语言能力无关,也是为什么统计要交给数据库计算,而不是让语言模型心算。关于这种分工,可以参考大模型会算错平均数怎么办一文。
缺数据和没上场,是两件不同的事
窗口规则里最容易被混淆的,是“球员没上场”与“数据源没记录”。前者是事实,后者是数据缺口,处理方式完全相反:没上场的比赛不该占用名额,缺记录的比赛却不该被悄悄跳过,否则窗口会不知不觉往更早的日期滑动。
为了区分这两种情况,比赛表和球员记录表之间需要一个显式的出场状态字段,例如 appearance_status,取值可以是 played、bench_unused、absent_injury、absent_other、data_missing。下面是一个虚构的球员最近若干场的状态示例:
| game_date(示例) | appearance_status | minutes | points | 是否进入球员窗口 |
|---|---|---|---|---|
| 3月14日 | played | 31 | 18 | 是 |
| 3月10日 | bench_unused | 0 | 0 | 否 |
| 3月7日 | absent_injury | 0 | 0 | 否 |
| 3月3日 | data_missing | 空 | 空 | 否,且触发提示 |
| 2月28日 | played | 6 | 2 | 是,提示出场时间偏低 |
第四行是关键:当窗口里出现 data_missing,系统应该在答案里补一句“3月3日一场缺少记录,未计入”,而不是让用户以为10场都是完整的。如果没有这个状态字段,只靠 minutes 是否为空来判断,就分不清“没上场”和“没数据”,窗口的边界会悄悄出错。
这类设计属于典型的数据工程细节,对最终用户不可见,却直接决定答案是否值得信任。它与答案证据链的思路一脉相承:凡是影响数字的前提条件,都应当被记录下来,并在需要的时候展示出来。
一次完整的例子:同一句话,两种窗口,两个答案
把前面的内容合起来,看一个完整的虚构例子。用户问:“边锋周某最近10场场均得分是多少?”
- 实体识别:系统把“周某”对应到示例队乙的边锋,编号 P0031(虚构)。如果库里存在两个候选,会先让用户确认。
- 窗口选择:问法中没有“球队”字样,采用球员窗口;赛事默认仅含常规联赛。
- 查询执行:返回 games_counted = 9,因为其中一场记录缺失。
- 结果检查:9不等于10,触发提示。
- 答案生成:“按周某最近可用的9次上场记录计算,场均得分为示例值;有1场比赛缺少数据,未计入。”
如果用户接着追问“那按球队最近10场呢”,系统会改用球队窗口重新查询,并展示他在这10场里的出场场次。两个答案都正确,只是回答了不同的问题,重要的是用户清楚这一点。
用户能做什么:几种更清楚的问法
系统再谨慎,最好的方式仍然是让问题本身更清晰。下面几种问法,能帮助半岛体育AI直接选中正确的窗口:
- “球员A最近10次上场的场均得分”:明确是球员窗口;
- “球员A在球队最近10场比赛中的表现”:明确是球队窗口,缺阵场次会显示;
- “球员A最近10场常规赛,不含加时”:直接给出了赛事类型和时间范围;
- “球员A最近30天的场均得分”:换成日期窗口,避免场次数量的歧义。
如果这些都没有说明,系统会采用默认口径并在答案顶部标出,用户可以一键改成别的口径的做法,是我们在产品设计中想要达到的目标。至于“时间”本身的理解,可以继续参考问“最近10场”和问“本赛季”为什么不是一个问题。
赛季交界处的窗口
还有一种边界情况,发生在新赛季开始的头几周。这时“最近10场”可能一半在新赛季、一半在上赛季,中间隔着几个月的休赛期,球员可能换了球队、换了位置,甚至换了教练体系。把这10场直接平均,得到的数字并不能说明“最近的状态”。
我们的处理思路分三层:默认不跨赛季,新赛季场次不足10场时按实际场次回答并明示;用户明确要求跨赛季时才补足,并在答案里标出每场所属的赛季与所属球队;如果跨赛季窗口里包含转会,则额外提示球队发生了变化。这样做的代价是答案会长一些,好处是用户不会在不知情时得到一个“混合口径”的数字。
这一节也提醒我们,窗口不是一个孤立的过滤条件,它和实体的时间变化、赛季划分是缠在一起的。球员的所属球队随时间变化的问题,我们放在知识图谱如何保存时间关系里讨论。
窗口只是起点,样本量才是提醒
把10场取对,并不意味着这个数字值得被信任。10场比赛里出现一两场极端的高分或低分,就足以改变平均值;缺阵、对手强弱、主客场分布的变化,也都会在小样本中被放大。一个负责任的系统,除了报出平均值,还应该报出样本数、最高最低值或者标准差,必要时提示“样本较小,不宜过度解读”。
所以,“最近10场”这个看似简单的词,其实是一个入口:它把身份识别、时间排序、口径约定、计算方式和不确定性表达,全部牵了出来。把它定义清楚,用户看到的就不再是一个孤零零的数字,而是一个知道自己从哪些比赛算出来的数字。这样的答案,才配得上被追问。