关于 metaculus
metaculus 是面向 Metaculus 认证 REST API 的技能指南,涵盖预测内容查询与预测提交,并说明帖子、问题、聚合预测和数据下载等接口。
主要功能
- 01浏览并获取帖子
- 02提交或撤回预测
- 03读取并发布评论
- 04下载问题和项目数据
- 05查看预测聚合数据
适用场景
- 01获取问题和帖子以供审阅
- 02提交或撤回预测
- 03查看或添加讨论评论
- 04分析下载的预测数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ outsharp/ shipp-skills --skill metaculus安装后在 Claude Code 中输入/metaculus确认已加载。
附带文件README.md
详细介绍
Metaculus API
Metaculus 是一个预测平台,用户可以预测现实世界事件的结果。问题涵盖科学、技术、政治、经济等领域。该平台会汇总个人预测,形成社区预测,并根据预测准确度为预测者评分。
请_经常_查看此技能和官方 API 文档,了解最新信息。
反馈: 如有问题、想法或反馈,请通过 [email protected] 联系 Metaculus 团队。
源代码与问题: github.com/Metaculus/metaculus
核心概念(术语表)
| 术语 | 定义 |
|---|---|
| Post | 主要的信息流实体。一个 post 包含一个问题、一组问题、条件问题对或笔记本。Post 有状态、作者、项目和评论。 |
| Question | post 中可供预测的单个条目。类型:binary、multiple_choice、numeric、discrete、date。 |
| 问题组 | 包含多个相关子问题并将它们一起显示的 post(例如,“2025 年、2026 年、2027 年的 GDP 会是多少?”)。 |
| 条件问题 | 包含成对问题的 post:“如果[条件成立],P(子事件)是多少?”——会产生 question_yes 和 question_no 两种形式。 |
| 预测 | 用户对某个问题的预测。格式取决于问题类型:概率(二元)、CDF(连续)或分布(多选)。 |
| 社区预测(CP) | 通过多种汇总方法计算的所有用户预测的汇总结果。 |
| 汇总方法 | 个人预测的合并方式:recency_weighted(默认)、unweighted、metaculus_prediction、single_aggregation。 |
| 项目 | post 的容器,可以是锦标赛、类别、标签、问题系列或网站版块。 |
| 锦标赛 | 一种特殊的项目类型,设有奖池、开始和结束日期以及排行榜。 |
| 类别 | 主题分类(例如,“核技术与风险”“健康与疫情”)。 |
| 结果判定 | 已知问题实际结果后,对该问题作出的判定。值因类型而异:yes/no(二元)、数字、日期或选项名称。 |
| 策划状态 | post 的编辑状态:draft、pending、rejected、approved。 |
| 缩放 | 定义连续型问题的取值范围如何映射到 CDF。包括 range_min、range_max、zero_point(用于对数刻度)和边界。 |
| CDF | 累积分布函数——连续型预测的格式。由 201 个浮点数组成(离散型为 inbound_outcome_count + 1)。 |
| 范围内结果数 | 问题范围内可能结果的数量(不包括超出边界的结果)。连续型默认值为 200;离散型的值较小。 |
基础 URL
所有端点均通过以下地址提供:
https://www.metaculus.com
以下所有路径均相对于此基础地址(例如,GET /api/posts/ 表示 GET https://www.metaculus.com/api/posts/)。
身份验证
所有 API 请求都需要身份验证。 未经身份验证的请求将被拒绝。
获取令牌
- 登录 Metaculus。
- 前往账户设置 → API 访问权限。
- 复制(或生成)你的 API 令牌。
使用令牌
为每个请求添加 Authorization 标头。令牌前必须加上字面字符串 Token,后接一个空格:
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
示例:curl
curl -s "https://www.metaculus.com/api/posts/?limit=5&order_by=-published_at" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq .
示例:Python
import os, requests
TOKEN = os.environ["METACULUS_API_TOKEN"]
HEADERS = {"Authorization": f"Token {TOKEN}"}
BASE = "https://www.metaculus.com"
resp = requests.get(f"{BASE}/api/posts/", headers=HEADERS, params={"limit": 5})
resp.raise_for_status()
data = resp.json()
环境变量
切勿将令牌硬编码。请将其存储为环境变量或放在 .env 文件中:
METACULUS_API_TOKEN=your-token-here
速率限制
Metaculus 会限制请求频率以防止滥用。如果收到 429 Too Many Requests 响应,请在重试前实施指数退避。
REST API 端点概览
信息流(帖子)
| 方法 | 端点 | 描述 | 身份验证 |
|---|---|---|---|
GET | /api/posts/ | 获取带筛选条件的分页帖子信息流 | 必需 |
GET | /api/posts/{postId}/ | 获取单个帖子的完整详情 | 必需 |
问题与预测
| 方法 | 端点 | 描述 | 身份验证 |
|---|---|---|---|
POST | /api/questions/forecast/ | 提交一个或多个问题的预测 | 必需 |
POST | /api/questions/withdraw/ | 撤回有效预测 | 必需 |
评论
| 方法 | 端点 | 描述 | 身份验证 |
|---|---|---|---|
GET | /api/comments/ | 获取评论(按帖子或作者筛选) | 必需 |
POST | /api/comments/create/ | 在帖子上创建新评论 | 必需 |
实用工具与数据
| 方法 | 端点 | 描述 | 身份验证 |
|---|---|---|---|
GET | /api/posts/{postId}/download-data/ | 将问题数据下载为包含 CSV 文件的 ZIP 压缩包 | 必需 |
GET | /api/projects/{projectId}/download-data/ | 将完整项目数据下载为包含 CSV 文件的 ZIP 压缩包 | 必需(管理员/白名单用户) |
数据模型
帖子 → 问题层级结构
帖子是信息流中的顶层实体。每个帖子恰好包含以下内容之一:
question— 单个问题(binary、numeric、date、multiple choice 或 discrete)group_of_questions— 多个相关子问题conditional— 条件问题对(条件 + 子问题,生成 question_yes 和 question_no)- 笔记本(不包含问题内容)
其他字段将为 null。例如,包含单个 binary 问题的帖子会填充 question,并将 conditional/group_of_questions 设为 null。
帖子
| 字段 | 类型 | 描述 |
|---|---|---|
id | integer | 帖子的唯一 ID |
title | string | 完整标题 |
short_title | string | 适用于 URL 的短标题 |
slug | string | URL 别名 |
author_id | integer | 作者的用户 ID |
author_username | string | 作者的用户名 |
projects | object | 关联的项目(见下文) |
created_at | datetime | 帖子的创建时间 |
published_at | datetime? | 帖子的发布时间 |
open_time | datetime? | 问题开放预测的时间 |
edited_at | datetime | 最后编辑时间戳 |
curation_status | string | draft、pending、rejected 或 approved |
comment_count | integer | 评论数量 |
status | string | open、upcoming、closed、resolved、draft、pending、rejected |
nr_forecasters | integer | 预测者人数(去重) |
question | Question? | 单个问题(如适用) |
conditional | Conditional? | 条件问题对(如适用) |
group_of_questions | GroupOfQuestions? | 问题组(如适用) |
user_permission | string | forecaster 或 viewer |
vote | object | { score: int, user_vote: string? } |
forecasts_count | integer | 预测总数 |
帖子项目对象
| 字段 | 类型 | 描述 |
|---|---|---|
site_main | Project[] | 站点级项目关联 |
tournament | Project[] | 此帖子所属的锦标赛 |
category | Category[] | 类别 |
tag | Tag[] | 标签 |
question_series | Project[] | 问题系列 |
default_project | Project | 帖子的主要/默认项目 |
项目
| 字段 | 类型 | 描述 |
|---|---|---|
id | integer | 项目 ID |
type | string | site_main、tournament 等 |
name | string | 显示名称 |
slug | string? | URL 别名 |
prize_pool | string | 奖池金额(例如,“0.00”) |
start_date | datetime? | 开始日期 |
close_date | datetime? | 结束日期 |
is_ongoing | boolean? | 项目是否正在进行 |
default_permission | string | 默认用户权限(例如,“forecaster”) |
visibility | string | normal、not_in_main_feed、unlisted |
问题
| 字段 | 类型 | 描述 |
|---|---|---|
id | integer | 唯一问题 ID(用于预测提交,而非帖子 ID) |
title | string | 问题标题 |
description | string | 完整描述(除非设置 include_descriptions=true,否则可以省略) |
type | string | binary、multiple_choice、numeric、discrete、date |
status | string | upcoming、open、closed、resolved |
resolution | string? | 结算值(未结算时为 null) |
resolution_criteria | string | 问题的结算方式 |
fine_print | string | 其他结算详情 |
created_at | datetime | 创建时间戳 |
open_time | datetime | 开放预测的时间 |
scheduled_close_time | datetime | 计划停止预测的时间 |
actual_close_time | datetime | 实际停止预测的时间 |
scheduled_resolve_time | datetime | 计划结算的时间 |
actual_resolve_time | datetime? | 实际结算的时间 |
options | string[] | 当前选项(仅限多选题) |
all_options_ever | string[] | 曾经存在过的所有选项(仅限多选题) |
open_upper_bound | boolean | 上界是否开放 |
open_lower_bound | boolean | 下界是否开放 |
inbound_outcome_count | integer? | 范围内离散结果的数量(连续型默认值为 200) |
unit | string | 显示单位(例如,“$”“°C”) |
label | string? | 子问题的标签 |
scaling | QuestionScaling | 范围和缩放参数 |
aggregations | object | 社区预测聚合(见下文) |
QuestionScaling
| 字段 | 类型 | 描述 |
|---|---|---|
range_min | float? | 输入范围的下界 |
range_max | float? | 输入范围的上界 |
zero_point | float? | 对数缩放的零点(null = 线性缩放) |
open_upper_bound | boolean? | 上界是否开放 |
open_lower_bound | boolean? | 下界是否开放 |
inbound_outcome_count | integer? | 范围内结果的数量 |
continuous_range | string[]? | 计算 CDF 的实数值位置 |
聚合
每个问题都包含一个 aggregations 对象,其中最多有四种方法:
recency_weighted— 默认方法;近期预测的权重更高unweighted— 所有预测的权重相同metaculus_prediction— Metaculus 的专有预测single_aggregation— Beta;仅限管理员使用
每种方法都包含:
| 字段 | 类型 | 描述 |
|---|---|---|
history | object[] | 聚合预测的时间序列 |
latest | object? | 最新的聚合预测 |
score_data | object? | 评分信息 |
每个 history/latest 条目都包含:
| 字段 | 类型 | 描述 |
|---|---|---|
start_time | datetime | 此聚合时段的开始时间 |
end_time | datetime? | 此聚合时段的结束时间 |
forecast_values | float[] | 聚合预测值(二元问题为概率,连续型问题为 CDF) |
forecaster_count | integer | 提交预测的预测者数量 |
centers | float[] | 中位数/中心值 |
interval_lower_bounds | float[] | 置信区间下界 |
interval_upper_bounds | float[] | 置信区间上界 |
means | float? | 均值 |
条件问题
| 字段 | 类型 | 描述 |
|---|---|---|
id | integer | 条件问题 ID |
condition | Question | 条件问题 |
condition_child | Question | 子问题 |
question_yes | Question | “如果条件 = Yes”变体 |
question_no | Question | “如果条件 = No”变体 |
GroupOfQuestions
| Field | Type | Description |
|---|---|---|
id | integer | 分组 ID |
description | string | 分组描述 |
resolution_criteria | string | 判定标准 |
fine_print | string | 补充详情 |
group_variable | string | 子问题之间有所不同的变量 |
graph_type | string | multiple_choice_graph 或 fan_graph |
questions | Question[] | 子问题 |
Comment
| Field | Type | Description |
|---|---|---|
id | integer | 评论 ID |
author | object | { id, username, is_bot, is_staff } |
parent_id | integer? | 父评论 ID(用于回复) |
root_id | integer? | 讨论串中的根评论 ID |
created_at | datetime | 创建时间戳 |
text | string | 评论内容 |
on_post | integer | 评论所属的帖子 ID |
included_forecast | boolean | 是否包含用户最近一次预测 |
is_private | boolean | 评论是否为私密评论 |
vote_score | integer | 总投票分数 |
user_vote | integer | 当前用户的投票(-1、0、1) |
端点:详细参考
GET /api/posts/ — 获取帖子动态
返回带有丰富筛选和排序选项的分页帖子列表。
查询参数:
| Parameter | Type | Description |
|---|---|---|
limit | integer | 每页条目数(默认值因情况而异) |
offset | integer | 分页偏移量 |
tournaments | string[] | 按锦标赛 slug 筛选(例如 metaculus-cup、aibq3) |
statuses | string[] | 按状态筛选:upcoming、closed、resolved、open |
forecast_type | string[] | 按类型筛选:binary、multiple_choice、numeric、discrete、date、conditional、group_of_questions、notebook |
categories | string[] | 按类别 slug 筛选(例如 nuclear、health-pandemics) |
forecaster_id | integer | 此用户进行过预测的帖子 |
not_forecaster_id | integer | 此用户尚未进行预测的帖子 |
for_main_feed | boolean | 按是否适合主动态筛选 |
with_cp | boolean | 包含社区预测(默认值:false)。对于分组,仅返回排名前 3 个子问题的 CP。 |
include_cp_history | boolean | 按每种聚合方法包含完整 CP 历史(默认值:false) |
include_descriptions | boolean | 包含 description、fine_print、resolution_criteria 字段 |
order_by | string | 排序字段。添加 - 前缀表示降序。可选项见下文。 |
open_time__gt | datetime | 开放时间晚于此值(也支持 __gte、__lt、__lte) |
published_at__gt | datetime | 发布时间晚于此值(也支持 __gte、__lt、__lte) |
scheduled_resolve_time__gt | datetime | 计划判定时间晚于此值(也支持 __gte、__lt、__lte) |
order_by 可选项:
| Value | Description |
|---|---|
published_at | 发布时间 |
open_time | 开始接受预测的时间 |
vote_score | 社区投票分数 |
comment_count | 评论数 |
forecasts_count | 预测数 |
scheduled_close_time | 计划关闭时间 |
scheduled_resolve_time | 计划判定时间 |
user_last_forecasts_date | 用户最近一次进行预测的时间 |
unread_comment_count | 未读评论数 |
weekly_movement | 每周概率变化 |
divergence | 分歧指标 |
hotness | 综合热度分数(3.5 天后逐渐降低) |
score | 用户预测表现(需要 forecaster_id) |
响应:
{
"next": "https://www.metaculus.com/api/posts/?limit=20&offset=20",
"previous": null,
"results": [ /* array of Post objects */ ]
}
示例:获取开放的二元问题,按最新优先排序:
curl -s "https://www.metaculus.com/api/posts/?statuses=open&forecast_type=binary&order_by=-published_at&limit=10&with_cp=true" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {id, title, status}'
示例:获取某个锦标赛的问题:
curl -s "https://www.metaculus.com/api/posts/?tournaments=metaculus-cup&with_cp=true&limit=20" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq .
示例:获取你尚未进行预测的问题:
# Replace YOUR_USER_ID with your actual Metaculus user ID
curl -s "https://www.metaculus.com/api/posts/?not_forecaster_id=YOUR_USER_ID&statuses=open&limit=10" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq .
GET /api/posts/{postId}/ — 获取帖子详情
返回单个帖子的完整详情,包括所有子问题和汇总数据。
路径参数:
| 参数 | 类型 | 描述 |
|---|---|---|
postId | 整数 | 帖子 ID |
示例:
curl -s "https://www.metaculus.com/api/posts/12345/" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq .
POST /api/questions/forecast/ — 提交预测
提交一个或多个预测。请求正文是一个JSON 数组,包含预测对象。
重要:
question字段填写的是问题 ID,而不是帖子 ID。请从post.question.id、post.group_of_questions.questions[].id或post.conditional.question_yes.id/post.conditional.question_no.id获取问题 ID。
二元预测
[
{
"question": 12345,
"probability_yes": 0.63
}
]
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
question | 整数 | 是 | 问题 ID |
probability_yes | 浮点数 | 是 | 介于 0 和 1 之间的概率 |
end_time | 日期时间 | 否 | 自动撤回时间戳 |
多项选择预测
[
{
"question": 12345,
"probability_yes_per_category": {
"Futurama": 0.5,
"Paperclipalypse": 0.3,
"Singularia": 0.2
}
}
]
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
question | 整数 | 是 | 问题 ID |
probability_yes_per_category | 对象 | 是 | 选项名称 → 概率的映射。总和必须为 1.0。 |
end_time | 日期时间 | 否 | 自动撤回时间戳 |
连续型预测(数值 / 日期 / 离散)
[
{
"question": 12345,
"continuous_cdf": [0.0, 0.00005, 0.00010, "... 201 values total ...", 1.0]
}
]
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
question | 整数 | 是 | 问题 ID |
continuous_cdf | 浮点数[] | 是 | CDF 数组(参见下方的 CDF 生成部分) |
distribution_input | 对象 | 否 | 用于前端显示的滑块值 |
end_time | 日期时间 | 否 | 自动撤回时间戳 |
条件预测
分别为“如果是”和“如果否”问题提交预测:
[
{ "question": 111, "probability_yes": 0.499 },
{ "question": 222, "probability_yes": 0.501 }
]
其中,111 是 conditional.question_yes.id,222 是 conditional.question_no.id。
分组预测
在单个请求中为多个子问题提交预测:
[
{ "question": 1, "probability_yes": 0.11 },
{ "question": 2, "probability_yes": 0.22 },
{ "question": 3, "probability_yes": 0.33 }
]
示例:使用 curl 提交二元预测:
curl -s -X POST "https://www.metaculus.com/api/questions/forecast/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[{"question": 12345, "probability_yes": 0.75}]'
响应:
| 状态 | 描述 |
|---|---|
201 | 预测提交成功 |
400 | 请求格式无效 |
POST /api/questions/withdraw/ — 撤回预测
撤回有效的预测。请求正文是一个包含撤回对象的 JSON 数组。
[
{ "question": 12345 },
{ "question": 12346 }
]
示例:
curl -s -X POST "https://www.metaculus.com/api/questions/withdraw/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[{"question": 12345}]'
GET /api/comments/ — 获取评论
使用筛选条件获取评论。post 和 author 至少需要提供一个。
查询参数:
| 参数 | 类型 | 必填 | 描述 |
|---|---|---|---|
post | 整数 | post 或 author 之一 | 用于筛选的帖子 ID |
author | 整数 | post 或 author 之一 | 用于筛选的作者用户 ID |
limit | 整数 | 否 | 要获取的评论数量 |
offset | 整数 | 否 | 分页偏移量 |
is_private | 布尔值 | 否 | 筛选私密或公开评论(默认值:false) |
use_root_comments_pagination | 布尔值 | 否 | 若为 true,分页仅应用于根评论;所有子评论均会包含在内 |
sort | 字符串 | 否 | created_at(升序)或 -created_at(降序) |
focus_comment_id | 整数 | 否 | 将此评论置于结果顶部 |
响应:
{
"total_count": 42,
"count": 15,
"next": "https://www.metaculus.com/api/comments/?post=123&limit=10&offset=10",
"previous": null,
"results": [ /* array of Comment objects */ ]
}
total_count— 根评论和子评论的总数count— 根评论总数
示例:
curl -s "https://www.metaculus.com/api/comments/?post=12345&sort=-created_at&limit=20" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq .
POST /api/comments/create/ — 创建评论
请求正文:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
on_post | 整数 | 是 | 要评论的帖子 ID |
text | 字符串 | 是 | 评论内容 |
included_forecast | 布尔值 | 是 | 是否包含用户最近一次预测 |
is_private | 布尔值 | 是 | 评论是否为私密评论 |
parent | 整数 | 否 | 父评论 ID(用于回复) |
示例:
curl -s -X POST "https://www.metaculus.com/api/comments/create/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"on_post": 12345,
"text": "I updated my forecast based on the latest data.",
"included_forecast": false,
"is_private": false
}'
响应:
| 状态 | 描述 |
|---|---|
201 | 评论创建成功(返回 Comment 对象) |
400 | 请求格式无效 |
GET /api/posts/{postId}/download-data/ — 下载问题数据
将预测数据下载为包含 CSV 文件的 ZIP 文件。
路径参数:
| 参数 | 类型 | 描述 |
|---|---|---|
postId | integer | 帖子 ID(URL 中 /questions/ 后面的数字) |
查询参数:
| 参数 | 类型 | 必填 | 描述 |
|---|---|---|---|
sub_question | integer | 否 | 分组问题或条件问题的子问题 ID |
aggregation_methods | string[] | 否 | 要包含的方法:recency_weighted、unweighted、metaculus_prediction、single_aggregation。默认仅使用 recency_weighted |
include_bots | boolean | 否 | 重新计算聚合时是否包含机器人预测 |
user_ids | string[] | 否 | 指定用户 ID(仅限白名单用户)。需要指定 aggregation_methods。 |
minimize | boolean | 否 | 默认值:true。如果为 false,则包含所有数据点(可能生成较大的文件) |
include_comments | boolean | 否 | 默认值:false。如果为 true,则添加 comment_data.csv |
include_scores | boolean | 否 | 默认值:false。如果为 true,则添加 score_data.csv |
ZIP 内容:
| 文件 | 描述 |
|---|---|
question_data.csv | 问题元数据、缩放信息和结算信息 |
forecast_data.csv | 单个预测和聚合预测 |
comment_data.csv | 评论(如果 include_comments=true) |
score_data.csv | 分数(如果 include_scores=true) |
示例:
curl -s "https://www.metaculus.com/api/posts/12345/download-data/?include_comments=true" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-o question_data.zip
GET /api/projects/{projectId}/download-data/ — 下载项目数据
将整个项目的数据下载为 ZIP 文件。仅限站点管理员和白名单用户使用。
路径参数:
| 参数 | 类型 | 描述 |
|---|---|---|
projectId | integer | 项目 ID |
查询参数:
| 参数 | 类型 | 必填 | 描述 |
|---|---|---|---|
include_comments | boolean | 否 | 是否包含评论 CSV |
include_scores | boolean | 否 | 是否包含分数 CSV |
生成连续 CDF
连续、数值、日期和离散问题都需要以 CDF(累积分布函数)的形式提交预测。本节说明如何生成有效的 CDF。
CDF 规则
-
长度: CDF 必须恰好包含
inbound_outcome_count + 1个值。大多数连续问题需要 201 个值。离散问题的值取决于具体问题。 -
边界:
- 如果
open_lower_bound == false(闭合):第一个值必须为 0.0 - 如果
open_lower_bound == true(开放):第一个值必须 ≥ 0.001(下界以下至少有 0.1% 的概率质量) - 如果
open_upper_bound == false(闭合):最后一个值必须为 1.0 - 如果
open_upper_bound == true(开放):最后一个值必须 ≤ 0.999(上界以上至少有 0.1% 的概率质量)
- 如果
-
单调性: CDF 每一步都必须至少递增
0.01 / inbound_outcome_count(标准的 200 个区间中,每一步即0.00005)。 -
最大步长: 任意两个相邻值的差不得超过
0.2 * (200 / inbound_outcome_count)。
理解缩放方式
CDF 值对应于内部 [0, 1] 范围内均匀间隔的点。将现实世界的值映射到 CDF 位置的方法如下:
- 线性缩放(
zero_point为null):internal = (value - range_min) / (range_max - range_min) - 对数缩放(已设置
zero_point):需要进行对数转换(见下方函数) - 日期问题:先将 ISO 日期转换为 Unix 时间戳,再应用缩放
Python:将名义值转换为 CDF 位置
import datetime
import numpy as np
def nominal_location_to_cdf_location(
nominal_location: str | float,
question_data: dict,
) -> float:
"""Takes a location in nominal format (e.g. 123, "123",
or datetime in iso format) and scales it to metaculus's
"internal representation" range [0,1] incorporating question scaling"""
if question_data["type"] == "date":
scaled_location = datetime.datetime.fromisoformat(
nominal_location
).timestamp()
else:
scaled_location = float(nominal_location)
scaling = question_data["scaling"]
range_min = scaling.get("range_min")
range_max = scaling.get("range_max")
zero_point = scaling.get("zero_point")
if zero_point is not None:
# logarithmically scaled question
deriv_ratio = (range_max - zero_point) / (range_min - zero_point)
unscaled_location = (
np.log(
(scaled_location - range_min) * (deriv_ratio - 1)
+ (range_max - range_min)
)
- np.log(range_max - range_min)
) / np.log(deriv_ratio)
else:
# linearly scaled question
unscaled_location = (scaled_location - range_min) / (
range_max - range_min
)
return unscaled_location
Python:根据百分位数生成 CDF
def generate_continuous_cdf(
percentiles: dict,
question_data: dict,
below_lower_bound: float = None,
above_upper_bound: float = None,
) -> list[float]:
"""
Takes a set of percentiles and returns a corresponding CDF with
inbound_outcome_count + 1 values (typically 201).
percentiles: dict mapping percentile keys to nominal values
Keys must end in a number interpretable as a float in (0, 100).
Values are in the question's real-world scale.
Example:
{
"percentile_05": 25,
"percentile_25": 500,
"percentile_50": 650,
"percentile_75": 700,
"percentile_95": 990,
}
below_lower_bound: probability mass below range_min (for open lower bound)
above_upper_bound: probability mass above range_max (for open upper bound)
"""
percentile_locations = []
if below_lower_bound is not None:
percentile_locations.append((0.0, below_lower_bound))
if above_upper_bound is not None:
percentile_locations.append((1.0, 1 - above_upper_bound))
for percentile, nominal_location in percentiles.items():
height = float(str(percentile).split("_")[-1]) / 100
location = nominal_location_to_cdf_location(
nominal_location, question_data
)
percentile_locations.append((location, height))
percentile_locations.sort()
first_point, last_point = percentile_locations[0], percentile_locations[-1]
if (first_point[0] > 0.0) or (last_point[0] < 1.0):
raise ValueError(
"Percentiles must encompass bounds of the question"
)
def get_cdf_at(location):
previous = percentile_locations[0]
for i in range(1, len(percentile_locations)):
current = percentile_locations[i]
if previous[0] <= location <= current[0]:
return previous[1] + (current[1] - previous[1]) * (
location - previous[0]
) / (current[0] - previous[0])
previous = current
n_points = (question_data.get("inbound_outcome_count") or 200) + 1
continuous_cdf = [get_cdf_at(i / (n_points - 1)) for i in range(n_points)]
return continuous_cdf
Python:标准化用于提交的 CDF
此函数可确保 CDF 满足所有验证规则。它会添加一个小的线性分量以保证单调性,并遵守边界约束。
def standardize_cdf(cdf, question_data: dict):
"""
Standardize a CDF for submission:
- No mass outside closed bounds
- Minimum mass outside open bounds
- Strictly increasing by at least the minimum step
- Caps maximum step size
"""
lower_open = question_data["open_lower_bound"]
upper_open = question_data["open_upper_bound"]
inbound_outcome_count = question_data.get("inbound_outcome_count") or 200
default_inbound_outcome_count = 200
cdf = np.asarray(cdf, dtype=float)
if not cdf.size:
return []
scale_lower_to = 0 if lower_open else cdf[0]
scale_upper_to = 1.0 if upper_open else cdf[-1]
rescaled_inbound_mass = scale_upper_to - scale_lower_to
def standardize(F: float, location: float) -> float:
rescaled_F = (F - scale_lower_to) / rescaled_inbound_mass
if lower_open and upper_open:
return 0.988 * rescaled_F + 0.01 * location + 0.001
elif lower_open:
return 0.989 * rescaled_F + 0.01 * location + 0.001
elif upper_open:
return 0.989 * rescaled_F + 0.01 * location
return 0.99 * rescaled_F + 0.01 * location
for i, value in enumerate(cdf):
cdf[i] = standardize(value, i / (len(cdf) - 1))
pmf = np.diff(cdf, prepend=0, append=1)
cap = 0.2 * (default_inbound_outcome_count / inbound_outcome_count)
def cap_pmf(scale: float) -> np.ndarray:
return np.concatenate(
[pmf[:1], np.minimum(cap, scale * pmf[1:-1]), pmf[-1:]]
)
def capped_sum(scale: float) -> float:
return float(cap_pmf(scale).sum())
lo = hi = scale = 1.0
while capped_sum(hi) < 1.0:
hi *= 1.2
for _ in range(100):
scale = 0.5 * (lo + hi)
s = capped_sum(scale)
if s < 1.0:
lo = scale
else:
hi = scale
if s == 1.0 or (hi - lo) < 2e-5:
break
pmf = cap_pmf(scale)
pmf[1:-1] *= (cdf[-1] - cdf[0]) / pmf[1:-1].sum()
cdf = np.cumsum(pmf)[:-1]
cdf = np.round(cdf, 10)
return cdf.tolist()
端到端示例:提交连续预测
import os, requests, numpy as np
TOKEN = os.environ["METACULUS_API_TOKEN"]
HEADERS = {"Authorization": f"Token {TOKEN}"}
BASE = "https://www.metaculus.com"
# 1. Fetch the question
post_id = 12345
resp = requests.get(f"{BASE}/api/posts/{post_id}/", headers=HEADERS)
resp.raise_for_status()
post = resp.json()
question = post["question"]
# 2. Define your percentile beliefs (in real-world units)
percentiles = {
"percentile_05": 25,
"percentile_25": 40,
"percentile_50": 55,
"percentile_75": 70,
"percentile_95": 90,
}
# 3. Generate and standardize the CDF
raw_cdf = generate_continuous_cdf(
percentiles,
question,
below_lower_bound=0.01,
above_upper_bound=0.01,
)
final_cdf = standardize_cdf(raw_cdf, question)
# 4. Submit
resp = requests.post(
f"{BASE}/api/questions/forecast/",
headers={**HEADERS, "Content-Type": "application/json"},
json=[{"question": question["id"], "continuous_cdf": final_cdf}],
)
resp.raise_for_status()
print("Forecast submitted!")
常见模式
浏览动态
# Newest open questions
curl -s "https://www.metaculus.com/api/posts/?statuses=open&order_by=-published_at&limit=10&with_cp=true" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {id, title, status}'
获取帖子的社区预测
curl -s "https://www.metaculus.com/api/posts/12345/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
| jq '.question.aggregations.recency_weighted.latest'
查找热门问题
curl -s "https://www.metaculus.com/api/posts/?order_by=-hotness&statuses=open&limit=10&with_cp=true" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {id, title}'
按类别查找问题
curl -s "https://www.metaculus.com/api/posts/?categories=nuclear&statuses=open&with_cp=true&limit=10" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {id, title}'
在锦标赛中查找问题
curl -s "https://www.metaculus.com/api/posts/?tournaments=aibq3&with_cp=true&limit=50" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {id, title, status}'
提交二元预测
curl -s -X POST "https://www.metaculus.com/api/questions/forecast/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[{"question": 12345, "probability_yes": 0.75}]'
提交多项选择预测
curl -s -X POST "https://www.metaculus.com/api/questions/forecast/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[{
"question": 12345,
"probability_yes_per_category": {
"Option A": 0.4,
"Option B": 0.35,
"Option C": 0.25
}
}]'
撤回预测
curl -s -X POST "https://www.metaculus.com/api/questions/withdraw/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[{"question": 12345}]'
阅读帖子评论
curl -s "https://www.metaculus.com/api/comments/?post=12345&sort=-created_at&limit=20" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {author: .author.username, text: .text}'
发表评论
curl -s -X POST "https://www.metaculus.com/api/comments/create/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"on_post": 12345,
"text": "My analysis suggests a higher probability because...",
"included_forecast": true,
"is_private": false
}'
回复评论
curl -s -X POST "https://www.metaculus.com/api/comments/create/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"on_post": 12345,
"parent": 67890,
"text": "Good point — I updated my forecast accordingly.",
"included_forecast": false,
"is_private": false
}'
Python:遍历所有未关闭问题的分页
import os, requests
TOKEN = os.environ["METACULUS_API_TOKEN"]
HEADERS = {"Authorization": f"Token {TOKEN}"}
BASE = "https://www.metaculus.com"
all_posts = []
offset = 0
limit = 100
while True:
resp = requests.get(
f"{BASE}/api/posts/",
headers=HEADERS,
params={
"statuses": "open",
"limit": limit,
"offset": offset,
"with_cp": True,
},
)
resp.raise_for_status()
data = resp.json()
results = data["results"]
all_posts.extend(results)
if data["next"] is None:
break
offset += limit
print(f"Fetched {len(all_posts)} open posts")
Python:查找变动较大的问题
import os, requests
TOKEN = os.environ["METACULUS_API_TOKEN"]
HEADERS = {"Authorization": f"Token {TOKEN}"}
BASE = "https://www.metaculus.com"
resp = requests.get(
f"{BASE}/api/posts/",
headers=HEADERS,
params={
"statuses": "open",
"order_by": "-weekly_movement",
"forecast_type": "binary",
"with_cp": True,
"limit": 10,
},
)
resp.raise_for_status()
for post in resp.json()["results"]:
q = post.get("question")
if q and q.get("aggregations"):
latest = q["aggregations"].get("recency_weighted", {}).get("latest")
if latest:
prob = latest.get("centers", [None])[0]
print(f"[{post['id']}] {post['title']} — CP: {prob}")
问题类型速查
| 类型 | 预测格式 | 关键字段 |
|---|---|---|
binary | probability_yes: float(0–1) | — |
multiple_choice | probability_yes_per_category: {option: float}(总和 = 1.0) | options、all_options_ever |
numeric | continuous_cdf: float[](201 个值) | scaling、open_lower_bound、open_upper_bound、unit |
date | continuous_cdf: float[](201 个值) | scaling(range_min/max 为 unix 时间戳) |
discrete | continuous_cdf: float[](inbound_outcome_count + 1 个值) | inbound_outcome_count、scaling |
帖子状态生命周期
draft → pending → approved → open → closed → resolved
→ rejected
| 状态 | 描述 |
|---|---|
draft | 作者仍在编辑 |
pending | 已提交,等待策展审核 |
rejected | 被策展人拒绝 |
open | 已获批准,正在接受预测 |
upcoming | 已获批准,但尚未开放预测 |
closed | 不再接受预测;等待结果判定 |
resolved | 最终结果已确定 |
错误处理
| 状态码 | 含义 | 操作 |
|---|---|---|
200 | 成功 | — |
201 | 已创建 | — |
400 | 请求错误 | 检查请求格式、CDF 有效性和必填字段 |
401 | 未授权 | 检查你的 Authorization: Token ... 标头 |
403 | 禁止访问 | 你没有权限(例如,下载项目数据) |
404 | 未找到 | 检查帖子/问题/项目 ID |
429 | 请求过于频繁 | 实现指数退避并重试 |
500 | 服务器错误 | 延迟后重试;如果问题持续存在,请联系 Metaculus |
使用提示
- 列出帖子时,如果需要社区预测,务必传入
with_cp=true——默认情况下聚合结果为空。 - 仅在需要历史 CP 数据时使用
include_cp_history=true——这会显著增大响应内容。 - 仅在需要时使用
include_descriptions=true——描述可能很长。 - 问题 ID ≠ 帖子 ID——预测使用
question.id,而非post.id。务必先获取帖子以取得问题 ID。 - 对于群组帖子,列表端点中的
with_cp=true只会返回排名前 3 的子问题的 CP。要获取所有子问题,请使用详情端点(/api/posts/{postId}/)。 - 生成 CDF 是最棘手的部分——请使用提供的 Python 函数或根据它们进行调整。提交前务必运行
standardize_cdf()。 - 组合筛选条件以执行有针对性的查询——例如,
statuses=open&forecast_type=binary&categories=nuclear可高效缩小结果范围。 - 合理分页——使用
limit和offset。不要一次获取所有帖子。 order_by=score排序需要forecaster_id——它会按特定用户的表现对问题进行排名。- 日期问题在
scaling.range_min和scaling.range_max中使用 unix 时间戳——将 ISO 日期转换为时间戳后,再映射到 CDF 位置。 - 遵守速率限制——收到 429 响应时实施退避。
资源
| 资源 | URL |
|---|---|
| Metaculus 平台 | metaculus.com |
| API 文档 | metaculus.com/api |
| 账户设置(API Token) | metaculus.com/accounts/settings |
| GitHub 问题 | github.com/Metaculus/metaculus |
| 联系方式 | [email protected] |