关于 f1verse
f1verse 是面向 F1 数据分析者和 Agent 的 Python 库及 MCP 服务,可将赛事实况与历史数据整理为赛事进程、轮胎策略和冠军情景等分析。它还提供数据覆盖与质量报告,并标明不同年代的数据缺口。
主要功能
- 01赛事进程与超车分析
- 02轮胎衰退与策略分析
- 03数据质量和修订追踪
- 04历史赛季与冠军情景
- 05遥测、赛道与天气数据
- 06生成可核验的赛事叙述
适用场景
- 01分析比赛中的领先变化和攻防
- 02检查数据是否适合发布
- 03比较赛季走势与冠军形势
- 04为赛事报道生成有依据的摘要
MCP 相关
自带 MCP 服务,向 MCP 客户端提供 8 个用于 F1 数据分析的工具。Python 库也可导出 MCP 方言的工具 JSON Schema。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
21收录的工具列表 · 更新于 2 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| year | integer | 是 | 赛季年份,例如 2026。提供 2023 年及之后的比赛数据。 |
| round | integer | 是 | 赛季中的轮次编号,从 1 开始。 |
详细介绍
f1verse
文档 · MCP 服务 · llms.txt · 更新日志 · 贡献指南 · 安全 · 许可证
Formula 1 数据的故事层。 数据库负责获取和整理数据—— f1verse 则告诉你 发生了什么:领先位置变化、领跑圈数、赛事时间线、 轮胎策略、比赛节奏——以及 接下来会发生什么:基于赛季模拟得出的冠军概率, 并附带该模拟自身的回测结果。
零依赖。 仅使用标准库。完整的实时计时数据覆盖自 2023 年起; 逐圈比赛数据可追溯至 1996 年,进站数据可追溯至 2011 年,成绩、 排位赛和积分榜可追溯至 1950 年——每条答案都会说明数据来自哪个年代, 以及该年代缺少哪些数据。
pip install f1verse
import f1verse
race = f1verse.load(2026, 12) # year, round — no other library needed
race.laps_led() # {'ANT': 32, 'NOR': 31, 'HAM': 9}
race.leader_runs() # [{'abbr': 'NOR', 'from': 1, 'to': 4}, ...]
race.results()[7] # {'abbr': 'HUL', 'gap': '+1 LAP', ...}
race.race_pace() # median pace — pit/SC/VSC laps excluded by default
race.story() # one call, whole story, plain JSON
race.championship_prediction() # per-lap "if it ended now" title projection
race.team_radio() # timestamped clip URLs (nothing downloaded)
f1verse.championship_projection(2026) # who wins the title, 20,000 seasons
f1verse.title_scenarios(2026) # and who is mathematically out
交给 AI Agent
f1verse 自带 MCP 服务。无需安装,也没有依赖:
{"mcpServers": {"f1verse": {"command": "uvx", "args": ["--from", "f1verse", "f1verse-mcp"]}}}
全部设置就是这些——服务仅使用标准库,因此大约 140 ms 就能启动并响应 tools/list,
而不必先在临时环境中解包一整套科学计算依赖。只有八个工具,而不是
八十个:模型可以选对工具。
对于其他 LLM 流程,库会自我描述:
f1verse.tools() # MCP-dialect JSON schemas
f1verse.tools("openai") # function-calling dialect
f1verse.call_tool("f1_race_story", {"year": 2026, "round": 12})
错误信息是为调用者编写的,让他们无需阅读本页面就能修复问题:
f1verse.call_tool("f1_race_summary", {})
# LookupError: unknown tool 'f1_race_summary' — available: f1_race_story, ...
f1verse.load_session(2026, 12, "Qualy")
# LookupError: ... listing the sessions that weekend actually had
整个周末,而不只是正赛
f1verse.sessions(2026, 12)
# Practice 1 · Sprint Qualifying · Sprint · Qualifying · Race
q = f1verse.load_session(2026, 12, "Qualifying")
q.results()[0]
# {'abbr': 'NOR', 'q1': 72.695, 'q1_gap': 0.085, 'q3': 71.163, 'q3_gap': 0.0,
# 'best': 71.163, 'eliminated_in': None, ...}
q.segments()["q1"]
# {'fastest': 'PIA', 'advanced': [...16 codes...], 'eliminated': [...],
# 'cut_margin': 0.022}
每种赛事都会使用其实际对应的分类方式。排位赛的差距是相对于
该环节 的最快圈速计算的——上面的杆位车手在 Q1 落后 0.085 s——
因为单独一列“与领先者的差距”会错误呈现该环节的情况。冲刺赛会加载为 Race;
练习赛则以最快圈速表呈现。
这些数据可以放心发布吗?
race.quality_report()
# {'state': 'final', # provisional → settled → final, or corrected
# 'coverage': {'overall': 0.9955, 'sectors': 0.9824, 'compound': 1.0},
# 'missing': ['STR.lap_46.lap_duration', ...],
# 'source_age_seconds': 312,
# 'revisions': [], # source rewrites this install has observed
# 'crosscheck': {...},
# 'publishable': True}
crosscheck 用于回答 不同来源是否一致。quality_report 还会补充判定结果
未涉及的三项信息:数据的完整程度、数据副本的时效性,以及分类结果是否仍为临时结果。
挥动方格旗并不意味着最终分类已经确定——赛后检查中的取消资格和处罚可能数小时后才公布, 并会直接改写数据行。因此,在赛事最终确认之前,赛事干事仍可更改的数据行不会被永久缓存; 而任何检测到的更改都会被记录:
before = race.snapshot() # hashed, comparable, JSON — you persist it
...
f1verse.diff(before, race.refresh().snapshot())
# {'changed': True,
# 'changes': [{'abbr': 'HAM', 'field': 'position', 'before': 4, 'after': None},
# {'abbr': 'HAM', 'field': 'gap', 'before': '+8.1s', 'after': 'DSQ'}]}
f1verse.revisions() # every source rewrite observed, with the
f1verse.vintage(rec) # superseded body when it was small enough
我们特意不提供 as_of= 时间回溯功能:f1verse 可以告诉你它现在看到什么,
以及何时发现值发生变化,但无法重建本系统从未获取过的值。
比赛实际是如何展开的
race = f1verse.load(2025, 24)
race.running_order()[30]
# ['PIA', 'VER', 'NOR', 'LEC', 'RUS', ...] who was where, lap by lap
race.position_changes()[1]
# {'lap': 2, 'moves': 18, 'biggest': {'abbr': 'PIA', 'gained': 17,
# 'from': 19, 'to': 2}}
race.battles()[0]
# {'ahead': 'HAD', 'behind': 'OCO', 'from': 2, 'to': 14, 'laps': 13,
# 'closest': 0.529} <- a thirteen-lap fight the results table hides
battles 会找出这样的车手组合:至少连续三圈排名相邻,且两车相差不超过 1.5 s。
一场争夺第八名、持续了三分之一赛程的缠斗不会出现在成绩分类表中——
但它往往是整个下午最精彩的部分。
实时数据源出现之前的比赛
old = f1verse.load_archive(2008, 18) # Brazil, the last-corner title
old.coverage
# {'lap_times': True, 'pit_stops': False, 'stints': False,
# 'note': 'lap times only'}
old.leader_runs()
# [{'abbr': 'MAS', 'from': 1, 'to': 9}, {'abbr': 'TRU', 'from': 10, 'to': 11},
# {'abbr': 'MAS', 'from': 12, 'to': 38}, ...]
old.laps_led() # {'MAS': 64, 'TRU': 2, 'ALO': 2, 'RAI': 3}
coverage 区块并非摆设。2008 年的任何数据都没有轮胎使用阶段信息,
所以 ArchiveRace 没有 stints()——而不是返回一个空字典,让人误以为“没有发生进站”。
那个年代记录了什么,你就能获取什么;没有记录的内容,也会明确说明。
跨赛季比较
f1verse.season_shape(2025)
# {'rounds': 24, 'final_margin': 2.0,
# 'lead_changes': [{'round': 5, 'from': 'NOR', 'to': 'PIA'},
# {'round': 20, 'from': 'PIA', 'to': 'NOR'}], ...}
f1verse.title_margins(top=5)
# 2025 NOR 423.0 vs VER 421.0 margin 2.0 (0.08 of a win)
# 2008 HAM 98.0 vs MAS 97.0 margin 1.0 (0.10 of a win)
# 2012 VET 281.0 vs ALO 278.0 margin 3.0 (0.12 of a win)
积分制度多次变化,因此原始分差无法用于比较不同时代——1958 年的一分就相当于一次胜利的大部分分值。 每一行还会包含以胜场数衡量的差距,这种比较方式不受规则变化影响。
超车发生的位置
f1verse.overtake_hotspots(race)[0]
# {'from_s': 3510.0, 'to_s': 3540.0, 'signals': 69, 'drivers': [...]}
计时数据流会为每辆赛车发布一个 OvertakeState,它几乎从不变化——一万九千条记录中只有约一百次状态转换。这种稀疏性正是它的价值:这些状态转换是值得关注时刻的免费索引,来自为比赛计时的同一数据流。可以把它理解为“这里发生了什么”,然后对照 running_order 确认。
不止一场比赛
f1verse.career("max_verstappen")
# {'starts': 245, 'wins': 71, 'podiums': 131, 'poles': 48, ...} 1950-present
f1verse.milestones("max_verstappen")
# [{'stat': 'poles', 'current': 48, 'target': 50, 'remaining': 2}]
f1verse.circuit_profile(2026, 13)
# corners, marshal sectors, track outline, and pit loss split by track state
# {'normal': 25.43, 'sc': 16.11, 'vsc': 18.4} <- what an undercut costs here
# plus historic record: 75 races held, pole-to-win rate 0.30
# Geometry is also interpreted, but never overclaimed: every corner carries
# its lap position, preceding-run share and local heading deflection; mini-
# and marshal-sector boundaries carry lap percentages.
profile = f1verse.circuit_profile(2026, 13)
profile["layout"]["corners"][0]
# {'number': 1, 'progress_pct': 8.412, 'local_deflection_deg': 72.6, ...}
# What a map cannot say, the cars can. This measures the circuit from the
# session's own telemetry: the height profile, where overtakes actually
# happen, how much of the lap is flat out and where it is braked, and every
# numbered corner as the car experienced it.
f1verse.circuit_survey(2026, 13)["corners"]["corners"][0]
# {'corner': 1, 'apex_speed_kph': 103, 'radius_m': 40.0,
# 'lateral_load_g': 2.09, 'gear_at_apex': 2, 'severity': 'medium'}
# Published specifications are stored rather than derived, and the
# measurement audits them rather than replacing them.
f1verse.circuit_profile(2026, 13, measure=True)["audit"]
# {'verdict': 'agrees', 'checked_age_days': 0,
# 'checks': [{'field': 'length_m', 'published': 4259, 'measured': 4274.4,
# 'off_by_percent': 0.36, 'state': 'agrees'}, ...]}
# The published facts and the audit are reachable on their own, so a caller
# can ask what is claimed without paying for a telemetry survey.
f1verse.circuit_facts("Austin")
# {'length_m': 5513, 'race_laps': 56, 'race_distance_km': 308.405,
# 'provenance': {'length_m': 'curated', ...},
# 'source': 'formula1.com official circuit page, 2026 united-states',
# 'checked': '2026-09-01'}
# None for a circuit nobody has curated yet - it does not invent a length
f1verse.circuit_audit(2026, 13)
# the same verdict as profile(measure=True)["audit"], on its own
f1verse.circuit_directory()
# every venue recorded in F1 results, with stable id, city, country and
# coordinates; unlike a current map, it does not pretend an old layout has
# today's geometry
f1verse.circuit_history("monza")
# every F1 event at the venue, recent winners and starting positions,
# pole-to-win conversion, and the drivers with the most wins
f1verse.head_to_head(2026)
# teammate quali/race scores per constructor
f1verse.standings(2026)
轮胎寿命,以及赛事干事撤销的判罚
f1verse.stint_degradation(race)[3]
# {'driver': 'NOR', 'stint': 2, 'compound': 'HARD', 'tyre_age_at_start': 0,
# 'clean_laps_used': 18, 'degradation_s_per_lap': 0.041}
f1verse.circuit_abrasion(race)
# {'factor': 1.4, 'verdict': 'abrasive', 'samples': 55}
f1verse.tyre_outlook(race) # projected to the cliff
赛车会在整场比赛中随着燃油消耗而越来越快,因此原始单圈时间会低估每个赛段的性能衰减。衰减率仅根据按燃油量归一化的干净单圈拟合;如果一个赛段的干净单圈太少,则会返回
{'degradation_s_per_lap': None, 'reason': 'too few clean laps'},而不是给出一个基于噪声拟合的数值。
f1verse.lap_deletions(session.race_control)
# [{'car_number': 55, 'lap_time': '1:25.773', 'stands': True,
# 'reason': 'TRACK LIMITS AT TURN 3 LAP 3', ...}]
恢复有效的单圈会保留撤销判罚的记录,而不是悄悄将其丢弃。在将最快单圈或排位位置视为最终结果之前,请先检查这一点。
直接从数据流获取实时计时
from f1verse.sources import liveclient
with liveclient.LiveFeed() as feed:
for topic, patch, stamp in feed.messages():
...
liveclient.run("session-{n}.jsonl") # record, rotate on turnover, reconnect
通过标准库编写的 WebSocket 连接官方 SignalR 数据流——无需 websocket 包,也无需 SignalR 客户端。连接时需要遵循三项未公开说明的要求(仅通过预检请求下发的亲和性 Cookie、官方应用的用户代理,以及订阅时的调用 ID);它们都集中在一处,因此调用方不必各自重新发现这些要求。
记录的帧带有毫秒级到达时间戳——这是存档与截图的区别——因此 replay() 可以按真实速度重放一场比赛。数据流并非单圈表格,因此 f1verse.sources.timing.laps_from_stream 会重建单圈数据:到达顺序并不可靠,分段时间会在下一圈开始后才到达,而排位赛数据中还会出现虚假的单圈时间,实际上代表两次试跑之间的间隔。每行重建的单圈数据都带有其来源信息。
按计划运行
f1verse.status(2026)
# {'latest_race': {'round': 12, 'meeting': 'Dutch Grand Prix'},
# 'next': {'round': 13, 'session': 'Practice 1'}, 'next_in_hours': 125.7}
f1verse.due(2026, processed=[...session keys you already handled...])
# sessions finished, settled (45 min past the flag) and not yet processed —
# nothing published twice, nothing missed after downtime
缓存由策略驱动,而非一概而论:单圈和遥测数据不可变,因此会永久缓存;赛程每隔几小时过期——若将赛季赛历缓存一整年,取消的分站就会在余下时间里一直被隐藏;赛事干事仍可修改的记录则会持续过期,直到赛段结束。运维人员可使用 f1verse.cache_info() 和 f1verse.clear_cache(older_than=...);clear_cache 不会删除修订日志。
遥测、赛道位置、状况
f1verse.lap_telemetry(race, "NOR", 40)
# per-sample speed, throttle, brake, gear, RPM and DRS state for one lap
f1verse.lap_trace(race, "NOR", 40) # x/y/z coordinates of that lap
f1verse.top_speeds(race) # fastest reading per driver
f1verse.weather_summary(race)
# {'track_c': {'min': 25.8, 'max': 38.2}, 'rain': True, 'samples': 191}
遥测数据频率很高,因此这些接口会限定时间窗口并在服务端进行筛选,而不是下载整个赛段后再在本地裁剪。
有依据的解说
facts = f1verse.race_facts(race) # all numbers computed and formatted here
f1verse.brief(race) # deterministic text, no model required
result = f1verse.narrate(
race,
generate=lambda prompt: my_model(prompt),
cache_dir=".cache/narration",
)
# {'text': '...', 'source': 'generated' | 'cache' | 'template', ...}
narrate 接受任意文本生成回调;f1verse 不依赖任何模型 SDK。草稿会根据结构化事实表进行核验。未知数字和车手代码会被拒绝,最多重试生成两次;如果核验仍未通过,则返回确定性摘要。可选缓存仅支持完全匹配,并且只存储已核验的文本。
谁将赢得冠军——以及谁仍有机会
这是两个不同的问题,把它们混为一谈,就会让预测结果暗示有人已经出局,尽管算术结果并非如此。
f1verse.title_scenarios(2026)
# arithmetic, not a forecast — maximum points left is a fixed number
# {'rounds_left': 11, 'sprints_left': 1, 'max_points_available': 283,
# 'drivers': [{'driver': 'RUS', 'gap_to_leader': 59.0,
# 'still_possible': True, 'needs_avg_per_round': 5.4}, ...],
# 'still_alive': 23}
f1verse.championship_projection(2026)
# the rest of the season, played out 20,000 times
# {'drivers': [{'driver': 'ANT', 'title_probability': 0.954,
# 'points_now': 242.0, 'projected_points_median': 446,
# 'projected_points_p10': 390, 'projected_points_p90': 490,
# 'races_in_sample': 11, 'measured_dnf_rate': 0.08}, ...],
# 'assumptions': {'ignores': ['car development', 'weather', ...], ...}}
模拟的完赛名次从车手实际取得过的名次中重新抽样,而不是从其平均名次附近的曲线中抽取。这个区别决定了冠军归属:交替取得胜利和退赛,与每个周末都拿第四名,是两种截然不同的情况,即使两者的平均名次相同。退赛按每位车手实际测得的退赛率触发,冲刺赛按独立积分表计分,平局则依照规则先比较胜场,再比较第二名次数。
每次模拟还会先重新抽取车手自身的水平:先对其成绩进行自助抽样,再以此版本的车手参加整个赛季的模拟。十二场比赛的样本量很小,把它当作定论,会让预测显得比实际情况更有把握。
f1verse.backtest_projection() # does the model deserve to be believed?
# {'by_confidence': {'over_90': {'n': 5, 'correct': 5, 'rate': 1.0},
# 'under_60': {'n': 2, 'correct': 0, 'rate': 0.0}}, ...}
在 2019—2025 年的第 12 站重放模拟,模型在声称胜率达到 90% 以上时,5 次中有 5 次预测正确;两次预测失误的赛季,模型给出的胜率分别为 53% 和 57%,而这两个赛季都战至最后一站。要看各个胜率区间,而不是只看标题:模型给出 55% 的胜率却预测失误,并不代表模型做错了什么。
championship_prediction(位于feeds中)是另一回事——这是 F1 自己的实时“如果现在结束”积分榜。championship_projection则是本模型的预测结果。 一个报告现状,另一个预测未来。
预测、进站判定、官方文档
f1verse.win_probabilities({"NOR": 1, "ANT": 2, "RUS": 3},
year=2026, upto_round=12, circuit_id="monza")
# every probability ships with its own evidence:
# grid base rate measured over 233 real races (pole wins 54.1%)
# blended with that circuit's pole-to-win conversion (Monza: 0.30)
# scaled by recent form (average finish over the last 5 rounds)
f1verse.pit_exchanges(race, pit_loss_s=22.74)
# [{'lap': 17, 'driver': 'RUS', 'rival': 'PIA', 'verdict': 'worked',
# 'gain_s': 3.54}, ...]
# neutralised laps (red flag / SC / VSC) and same-lap covering stops are
# excluded — calling those undercuts would be wrong
f1verse.fia_documents(2026) # stewards' decisions, classified
f1verse.power_unit_documents(2026) # "who changed which engine part"
为什么要开发它
原始计时数据需要大量领域知识才能真正发挥作用:
- 被列入最终成绩的圈差,不能直接比较被套圈的赛车。 落后一圈的赛车显示的数字可能比领先一圈完赛的赛车还小。→
format_gap会应用转播惯例(+1 LAP)。 - “谁领跑了比赛”需要推导得出。 领跑权的变更、领跑圈数及其发生时刻都不会以这种形式发布。
→
leader_runs、laps_led、timeline。 - 比赛节奏需要应用规则,不能只设一个阈值:进站圈、出站圈以及安全车(SC)/虚拟安全车(VSC)期间完成的圈数都必须剔除,否则得出的数字毫无意义。
→
race_pace默认会应用这些规则。 - 网页和视频工作流需要纯 JSON。 f1verse 的所有输出都是可安全序列化的 Python 数据。
- 数据质量应能通过代码检查,而不是靠查看日志。
→
crosscheck和quality_report会返回结构化判定结果。 - 比赛结束后,成绩仍可能变更。 如果缓存把最终成绩视为不可变,赛事干事的决定就会被忽略。 → 可修订的行会在赛段最终确定前到期;所有变更都会被记录。
其他实时计时数据源
官方实时计时档案发布了多个很少被展示的数据源。f1verse 会解析其中三个,并采用与库中其他部分相同的缓存和限速规范:
f1verse.championship_prediction(session)
# per-lap "if the race ended now" projection of both championships,
# including the moments the projected champion changed
f1verse.team_radio(session)
# timestamped team-radio clips: [{'t', 'utc', 'driver_number', 'url'}]
# URLs only — nothing is downloaded or redistributed
f1verse.timing_stats(session)
# personal bests, best sectors, speed-trap figures
持续运行
缓存策略、限速和调度请参阅 OPERATIONS.md。
测试
pip install -e ".[test]" && pytest -q
数据来源
| 层级 | 提供的数据 |
|---|---|
| 比赛数据 | 圈数、轮胎使用情况、进站、位置、成绩、超车 |
| 实时计时档案 | 冠军预测、车队无线电、计时统计 |
| 历史记录 | 职业生涯、赛道纪录、积分榜——自 1950 年起 |
| 赛道几何数据 | 赛道轮廓、弯道、赛道工作人员分区、进站时间损失 |
这些都是运行时读取的公开端点。具体主机名请参阅 src/f1verse/sources/,上游数据权利与本项目自身许可证之间的关系请参阅 NOTICE。
设计原则
- 零必需依赖。 原生加载器直接读取公开 REST 端点和官方实时计时档案,并使用自有的磁盘缓存和适当的请求间隔。
- 所有返回内容都是可安全序列化为 JSON 的纯 Python 数据。
- F1 领域规则是默认行为,而非可选项。
- 尽可能交叉验证——例如,被套圈赛车的圈差既按惯例计算,也会与第二个来源核实。
- 仅包含代码。 不会捆绑或再分发计时数据、媒体或图像;数据由最终用户获取。
许可证
Apache-2.0。请参阅 LICENSE 和 NOTICE。
可用于个人或商业用途;可以阅读、修改、再分发,也可以基于此构建产品或托管服务。请保留许可证和声明,说明重大变更,并遵守许可证中的专利条款。使用 f1verse 的软件或服务无需公开源代码。
本许可证适用于 f1verse 自身的源代码。它在运行时读取的 Formula 1 数据归各自的权利持有人所有,并受其各自条款约束。
路线图
- 扩大交叉验证覆盖范围
- 偏差检测:预期范围、实际值、证据
- 本地化包
非官方粉丝项目。与 Formula 1、FIA、FOM 或任何 F1 车队均无关联,也未获其认可。F1、FORMULA 1 及相关标志均为 Formula One Licensing BV 的商标。本库仅包含代码,不包含或再分发计时数据、媒体或图片;数据由最终用户从公开访问的端点获取,并受相应提供方的条款约束。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。