mcpField
M
metaculus

metaculus

github.com/outsharp其他441 次安装31 周前更新
代码仓库

关于 metaculus

metaculus 是面向 Metaculus 认证 REST API 的技能指南,涵盖预测内容查询与预测提交,并说明帖子、问题、聚合预测和数据下载等接口。

主要功能

  1. 01浏览并获取帖子
  2. 02提交或撤回预测
  3. 03读取并发布评论
  4. 04下载问题和项目数据
  5. 05查看预测聚合数据

适用场景

  1. 01获取问题和帖子以供审阅
  2. 02提交或撤回预测
  3. 03查看或添加讨论评论
  4. 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 有状态、作者、项目和评论。
Questionpost 中可供预测的单个条目。类型: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 请求都需要身份验证。 未经身份验证的请求将被拒绝。

获取令牌
  1. 登录 Metaculus。
  2. 前往账户设置 → API 访问权限。
  3. 复制(或生成)你的 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。

帖子
字段类型描述
idinteger帖子的唯一 ID
titlestring完整标题
short_titlestring适用于 URL 的短标题
slugstringURL 别名
author_idinteger作者的用户 ID
author_usernamestring作者的用户名
projectsobject关联的项目(见下文)
created_atdatetime帖子的创建时间
published_atdatetime?帖子的发布时间
open_timedatetime?问题开放预测的时间
edited_atdatetime最后编辑时间戳
curation_statusstringdraft、pending、rejected 或 approved
comment_countinteger评论数量
statusstringopen、upcoming、closed、resolved、draft、pending、rejected
nr_forecastersinteger预测者人数(去重)
questionQuestion?单个问题(如适用)
conditionalConditional?条件问题对(如适用)
group_of_questionsGroupOfQuestions?问题组(如适用)
user_permissionstringforecaster 或 viewer
voteobject{ score: int, user_vote: string? }
forecasts_countinteger预测总数
帖子项目对象
字段类型描述
site_mainProject[]站点级项目关联
tournamentProject[]此帖子所属的锦标赛
categoryCategory[]类别
tagTag[]标签
question_seriesProject[]问题系列
default_projectProject帖子的主要/默认项目
项目
字段类型描述
idinteger项目 ID
typestringsite_main、tournament 等
namestring显示名称
slugstring?URL 别名
prize_poolstring奖池金额(例如,“0.00”)
start_datedatetime?开始日期
close_datedatetime?结束日期
is_ongoingboolean?项目是否正在进行
default_permissionstring默认用户权限(例如,“forecaster”)
visibilitystringnormal、not_in_main_feed、unlisted
问题
字段类型描述
idinteger唯一问题 ID(用于预测提交,而非帖子 ID)
titlestring问题标题
descriptionstring完整描述(除非设置 include_descriptions=true,否则可以省略)
typestringbinary、multiple_choice、numeric、discrete、date
statusstringupcoming、open、closed、resolved
resolutionstring?结算值(未结算时为 null)
resolution_criteriastring问题的结算方式
fine_printstring其他结算详情
created_atdatetime创建时间戳
open_timedatetime开放预测的时间
scheduled_close_timedatetime计划停止预测的时间
actual_close_timedatetime实际停止预测的时间
scheduled_resolve_timedatetime计划结算的时间
actual_resolve_timedatetime?实际结算的时间
optionsstring[]当前选项(仅限多选题)
all_options_everstring[]曾经存在过的所有选项(仅限多选题)
open_upper_boundboolean上界是否开放
open_lower_boundboolean下界是否开放
inbound_outcome_countinteger?范围内离散结果的数量(连续型默认值为 200)
unitstring显示单位(例如,“$”“°C”)
labelstring?子问题的标签
scalingQuestionScaling范围和缩放参数
aggregationsobject社区预测聚合(见下文)
QuestionScaling
字段类型描述
range_minfloat?输入范围的下界
range_maxfloat?输入范围的上界
zero_pointfloat?对数缩放的零点(null = 线性缩放)
open_upper_boundboolean?上界是否开放
open_lower_boundboolean?下界是否开放
inbound_outcome_countinteger?范围内结果的数量
continuous_rangestring[]?计算 CDF 的实数值位置
聚合

每个问题都包含一个 aggregations 对象,其中最多有四种方法:

  • recency_weighted — 默认方法;近期预测的权重更高
  • unweighted — 所有预测的权重相同
  • metaculus_prediction — Metaculus 的专有预测
  • single_aggregation — Beta;仅限管理员使用

每种方法都包含:

字段类型描述
historyobject[]聚合预测的时间序列
latestobject?最新的聚合预测
score_dataobject?评分信息

每个 history/latest 条目都包含:

字段类型描述
start_timedatetime此聚合时段的开始时间
end_timedatetime?此聚合时段的结束时间
forecast_valuesfloat[]聚合预测值(二元问题为概率,连续型问题为 CDF)
forecaster_countinteger提交预测的预测者数量
centersfloat[]中位数/中心值
interval_lower_boundsfloat[]置信区间下界
interval_upper_boundsfloat[]置信区间上界
meansfloat?均值
条件问题
字段类型描述
idinteger条件问题 ID
conditionQuestion条件问题
condition_childQuestion子问题
question_yesQuestion“如果条件 = Yes”变体
question_noQuestion“如果条件 = No”变体
GroupOfQuestions
FieldTypeDescription
idinteger分组 ID
descriptionstring分组描述
resolution_criteriastring判定标准
fine_printstring补充详情
group_variablestring子问题之间有所不同的变量
graph_typestringmultiple_choice_graph 或 fan_graph
questionsQuestion[]子问题
Comment
FieldTypeDescription
idinteger评论 ID
authorobject{ id, username, is_bot, is_staff }
parent_idinteger?父评论 ID(用于回复)
root_idinteger?讨论串中的根评论 ID
created_atdatetime创建时间戳
textstring评论内容
on_postinteger评论所属的帖子 ID
included_forecastboolean是否包含用户最近一次预测
is_privateboolean评论是否为私密评论
vote_scoreinteger总投票分数
user_voteinteger当前用户的投票(-1、0、1)

端点:详细参考

GET /api/posts/ — 获取帖子动态

返回带有丰富筛选和排序选项的分页帖子列表。

查询参数:

ParameterTypeDescription
limitinteger每页条目数(默认值因情况而异)
offsetinteger分页偏移量
tournamentsstring[]按锦标赛 slug 筛选(例如 metaculus-cup、aibq3)
statusesstring[]按状态筛选:upcoming、closed、resolved、open
forecast_typestring[]按类型筛选:binary、multiple_choice、numeric、discrete、date、conditional、group_of_questions、notebook
categoriesstring[]按类别 slug 筛选(例如 nuclear、health-pandemics)
forecaster_idinteger此用户进行过预测的帖子
not_forecaster_idinteger此用户尚未进行预测的帖子
for_main_feedboolean按是否适合主动态筛选
with_cpboolean包含社区预测(默认值:false)。对于分组,仅返回排名前 3 个子问题的 CP。
include_cp_historyboolean按每种聚合方法包含完整 CP 历史(默认值:false)
include_descriptionsboolean包含 description、fine_print、resolution_criteria 字段
order_bystring排序字段。添加 - 前缀表示降序。可选项见下文。
open_time__gtdatetime开放时间晚于此值(也支持 __gte、__lt、__lte)
published_at__gtdatetime发布时间晚于此值(也支持 __gte、__lt、__lte)
scheduled_resolve_time__gtdatetime计划判定时间晚于此值(也支持 __gte、__lt、__lte)

order_by 可选项:

ValueDescription
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 文件。

路径参数:

参数类型描述
postIdinteger帖子 ID(URL 中 /questions/ 后面的数字)

查询参数:

参数类型必填描述
sub_questioninteger否分组问题或条件问题的子问题 ID
aggregation_methodsstring[]否要包含的方法:recency_weighted、unweighted、metaculus_prediction、single_aggregation。默认仅使用 recency_weighted
include_botsboolean否重新计算聚合时是否包含机器人预测
user_idsstring[]否指定用户 ID(仅限白名单用户)。需要指定 aggregation_methods。
minimizeboolean否默认值:true。如果为 false,则包含所有数据点(可能生成较大的文件)
include_commentsboolean否默认值:false。如果为 true,则添加 comment_data.csv
include_scoresboolean否默认值: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 文件。仅限站点管理员和白名单用户使用。

路径参数:

参数类型描述
projectIdinteger项目 ID

查询参数:

参数类型必填描述
include_commentsboolean否是否包含评论 CSV
include_scoresboolean否是否包含分数 CSV

生成连续 CDF

连续、数值、日期和离散问题都需要以 CDF(累积分布函数)的形式提交预测。本节说明如何生成有效的 CDF。

CDF 规则
  1. 长度: CDF 必须恰好包含 inbound_outcome_count + 1 个值。大多数连续问题需要 201 个值。离散问题的值取决于具体问题。

  2. 边界:

    • 如果 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% 的概率质量)
  3. 单调性: CDF 每一步都必须至少递增 0.01 / inbound_outcome_count(标准的 200 个区间中,每一步即 0.00005)。

  4. 最大步长: 任意两个相邻值的差不得超过 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}")

问题类型速查

类型预测格式关键字段
binaryprobability_yes: float(0–1)—
multiple_choiceprobability_yes_per_category: {option: float}(总和 = 1.0)options、all_options_ever
numericcontinuous_cdf: float[](201 个值)scaling、open_lower_bound、open_upper_bound、unit
datecontinuous_cdf: float[](201 个值)scaling(range_min/max 为 unix 时间戳)
discretecontinuous_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]

评论

支持 Markdown