清洗策略与清洗报告
default 与 strict 预设会在推理前清洗数据:排序、去重、对齐网格、补齐缺失、处理缺失值代码等。每一处改动都作为一条带稳定 id 的「决定」写进清洗报告,不会静默修改数据。你可以按 id 采纳或拒绝决定,也可以用 /v1/clean 免费预演。
1. 清洗预设
用 options.cleaning.preset 选择预设:
| 预设 | 行为 | /v1/forecast | /v1/clean |
|---|---|---|---|
default | 自动应用常规修复;不确定的改动只提议、不执行。 | 支持 | 支持 |
strict | 只自动执行结构性修复(排序、去掉完全相同的重复行、对齐网格等)。删除数据、补值、改写数值的决定都降为提议;未采纳时拒绝请求。 | 支持 | 支持 |
raw | 不清洗,数据原样交给模型,只做结构校验;不返回清洗报告。 | 支持 | 不支持,返回 CLEANING_OPTION_UNSUPPORTED |
raw 的输入要求见 raw 预设。
clean_series: false 表示不允许修复:数据需要任何修复时,请求以 TIMESTAMP_IRREGULAR 或 SERIES_DISCONTINUOUS 拒绝,并附带报告说明原因。
2. 生效策略 effective_policy
每个成功响应都会回显实际生效的策略:
"effective_policy": {
"id": "default@2026-13",
"preset": "default",
"version": "2026-13",
"source": "platform"
}| 字段 | 说明 |
|---|---|
id | 预设@版本,可以记录下来,用于复现同一次清洗。 |
preset | default、strict 或 raw。 |
version | 规则集版本。同一版本的规则与阈值不变;规则调整会发布新版本。 |
source | 策略来自哪里,按优先级:request(请求中的 options.cleaning.preset 或 version)> key(该密钥的默认值)> org(组织默认值)> platform(平台默认,即 default@2026-13)。预设和版本分别解析,source 取设置了其中任一项的最高层级。 |
密钥或组织的默认值由平台配置,目前没有自助设置入口。请求里显式传 preset 或 version 总是优先。
策略版本
| 版本 | 状态 |
|---|---|
2026-13 | 默认(不传 version 时使用)。在 2026-12 基础上,把 10% 缺失占比上限改为按缺失位置判断,并逐项列出健康分扣分;见缺失判断规则。 |
2026-12 | 已弃用,将在公告后移除。在 2026-11 基础上固定预测起点:末尾至多 3 个空值按未知处理,预测仍从您最后一个时间戳的下一步开始。可通过 options.cleaning.version: "2026-12" 继续使用,响应 warnings[] 会带 cleaning_version_deprecated。 |
2026-11 | 已弃用,将在公告后移除。仍可通过 options.cleaning.version: "2026-11" 使用,响应 warnings[] 会带 cleaning_version_deprecated。 |
2026-10 | 已弃用,将在公告后移除。最初的规则集(缺口一律线性填补),用法与提醒同上。 |
建议不传 version,让请求跟随平台默认。移除日期会提前公告。
2026-11 保留 2026-10 的全部规则,并新增:
- 逐段补值。 每个缺口单独选方法:短缺口用 PCHIP(保形、不过冲);较长缺口在季节周期被确认时用季节补值(取相邻周期同一相位),否则用线性。
imputed动作的每条gaps[]都写明method、tier与reason;series[].seasonality写明检验的周期以及是否采用。 - 补值的物理约束。 非负序列的补值截到 0 以上(
constraint.nonnegative),截到声明的范围内(constraint.bounds),几乎总为 0 的相位(如夜间光伏出力)补值保持为 0(constraint.structural_zero)。观测值从不修改;违反约束的观测值会被标记(negative_in_nonnegative、out_of_bounds)。每条约束都是可拒绝的决定。 - 工作日。 没有周末日期的日频数据放到
B网格上:周末不算缺口,预测时间戳跳过周末(决定grid.business_days,问题business_days_pattern)。拒绝该决定即可保留自然日。 - 更多数据检查。 不是普通数字的
y文本会被报告(thousands_separator、decimal_comma、ambiguous_separator、number_format_mixed、non_numeric_value),保持缺失而不猜测;常数序列与全零序列给出警告(constant_series、all_zero_series);所有无法解析的时间戳在一次错误中全部列出。
2026-11 的七个参数(impute_method、seasonal_period、nonnegative、bounds、structural_zero、business_days、long_gap)、每个版本逐步执行的规则与阈值,以及报告中每个决定与问题代码的含义,见清洗策略版本参考。
3. 清洗报告 cleaning_report/1
清洗报告出现在三个地方:
/v1/forecast的成功响应(raw预设除外);/v1/clean的响应;- 数据被清洗阶段拒绝时,错误响应的
error.details.cleaning_report。
顶层结构:
| 字段 | 说明 |
|---|---|
schema_version | 固定为 cleaning_report/1。 |
policy | 本次使用的清洗参数,包括 id、clean_series、重复处理方式、补值上限、异常值阈值、默认时区,以及你传入的 accept / decline。 |
overall | 汇总:series_count、input_points、output_points、health_score(0 至 100)、health_tier、forecastable。 |
series[] | 每个实体一项:freq(推断结果、实际使用值、来源与置信度)、timezone、issues[](发现的问题)、actions[](执行的操作及位置)、health、input / output 点数。 |
warnings[] | 不阻断请求、但值得注意的情况,例如 duplicate_conflict、off_grid_dropped、segment_split_skipped。 |
blocking[] | 导致请求被拒的原因,每项带 code 与 unique_id。成功时为空。 |
decisions[] | 所有改动数据的决定,见下一节。 |
默认的 2026-13 下,health_tier 按缺失判断规则分级:被该规则拒绝的实体为 Critical,请求以 SERIES_DISCONTINUOUS 拒绝;带 missing_pattern 提醒被接受的实体为 Warning;health.factors 列出各项扣分。已弃用的版本仍按缺失占比(缺口加空值)分级:低于 1% 为 Healthy,1% 至 10% 为 Warning,达到 10% 为 Critical。异常值只在 issues[] 中标记,不会被修改。
4. 决定 decisions[]
每条决定的字段:
| 字段 | 说明 |
|---|---|
id | 稳定标识,格式为 规则:unique_id[:选择器],例如 gap.impute:meter-001、value.sentinel:meter-001:-9999。同样的数据与策略每次得到同样的 id。 |
rule | 规则名,见下表。 |
class | structural(只调整结构,不增删观测值)、drop(删除数据)、fabricate(生成或置空数值)、semantic(按含义改写数值)。 |
status | applied(已执行)、proposed(只提议,未执行)、declined(被你拒绝)。 |
action | 具体操作,例如 keep_last、linear。 |
before / after | 执行前后的计数或取值。 |
reversible | 是否可以通过 decline 撤销。 |
evidence | 做出该决定的依据,因规则而异。 |
规则一览:
| 规则 | 类别 | 操作 | 什么时候出现 |
|---|---|---|---|
time.duplicates | structural / drop | keep_last | 同一时间戳出现多行,保留最后一行。值都相同为 structural,值不同为 drop。 |
time.reorder | structural | stable_sort | 行没有按时间排序。 |
grid.snap | structural | nearest_slot | 时间戳略偏离网格(例如多出几秒),对齐到最近的格点。 |
grid.off_grid | drop | drop | 时间戳落在两个格点之间,删除该行。超过 10% 的行偏离网格时改为拒绝请求。 |
gap.edge_trim | drop | drop | 序列首尾的空值,直接删除(从 2026-12 起,末尾空值见 gap.masked_tail)。 |
gap.masked_tail | fabricate | mask_for_model | 2026-12:至多 3 个末尾空值保留在网格上、不作为数据使用,预测仍从你的最后一个时间戳之后开始;更长的末尾空值以 SERIES_DISCONTINUOUS(trailing_gap_too_long)拒绝。 |
window.context | drop | keep_latest | 保留规则:API 对长于模型最大上下文的序列直接拒绝(SERIES_TOO_LONG),不会只保留最近的部分,因此 API 响应里不会出现这条决定。 |
gap.split | drop | keep_latest_segment | 序列中有很长的缺口,只保留缺口之后的最新一段。 |
grid.regrid | structural | insert_empty_slots | 网格上缺少时间戳,插入空的格点。 |
gap.impute | fabricate | linear / pchip / seasonal / mixed | 补齐缺失值:2026-10 一律线性插值,2026-11 起逐段选择方法。 |
value.sentinel | fabricate | null | 数值是常见的缺失值代码(例如 -9999、65535),置为空值后再补齐。 |
value.tokens | semantic | null | 文本单元格中的缺失标记(例如 N/A),置为空值。 |
value.dash_token | semantic | zero | 文本单元格中的单独破折号,提议按 0 处理。 |
value.tokens 与 value.dash_token 只针对文本形式的单元格(例如上传的 CSV);JSON 请求里 y 是数字或 null,通常不会出现这两条。
下面是各规则的前后对比。示例均为逐小时数据,实体为 meter-001;决定内容来自清洗服务在策略版本 2026-12、default 预设下的实际输出。
time.duplicates
// 输入:10:00 出现两次,值不同
{"ds": "2026-09-01T10:00:00Z", "y": 145.7}
{"ds": "2026-09-01T10:00:00Z", "y": 999.0}
// 结果:只保留最后一行 999.0,并附带 duplicate_conflict 警告;
// 999.0 远离其他值,另有一条 outlier_flagged_not_modified 警告(数值不修改)
{"id": "time.duplicates:meter-001", "rule": "time.duplicates", "class": "drop",
"status": "applied", "action": "keep_last",
"before": {"duplicate_rows": 1, "conflicting": 1}, "after": {"duplicate_rows": 0}}两行值完全相同时,class 为 structural,conflicting 为 0,不产生警告。
time.reorder
// 输入:11:00 排在 10:00 之前
{"ds": "2026-09-01T11:00:00Z", "y": 146.9}
{"ds": "2026-09-01T10:00:00Z", "y": 145.7}
// 结果:按时间稳定排序
{"id": "time.reorder:meter-001", "rule": "time.reorder", "class": "structural",
"status": "applied", "action": "stable_sort",
"before": {"out_of_order": 1}, "after": {"out_of_order": 0}}grid.snap
// 输入:10:00:20,比格点晚 20 秒
{"ds": "2026-09-01T10:00:20Z", "y": 145.7}
// 结果:对齐到 10:00:00
{"id": "grid.snap:meter-001", "rule": "grid.snap", "class": "structural",
"status": "applied", "action": "nearest_slot",
"before": {"off_slot": 1}, "after": {"off_slot": 0}}grid.off_grid
// 输入:逐小时数据中夹着一行 10:30
{"ds": "2026-09-01T10:00:00Z", "y": 145.7}
{"ds": "2026-09-01T10:30:00Z", "y": 141.0}
{"ds": "2026-09-01T11:00:00Z", "y": 146.9}
// 结果:删除 10:30 这一行,并附带 off_grid_dropped 警告
{"id": "grid.off_grid:meter-001", "rule": "grid.off_grid", "class": "drop",
"status": "applied", "action": "drop",
"before": {"rows": 1}, "after": {"rows": 0}, "evidence": {"ratio": 0.020408}}evidence.ratio 是偏离网格的行占比;超过 10% 时请求以 FREQ_INFERENCE_FAILED 拒绝。
gap.edge_trim
// 输入:前两行为空值
{"ds": "2026-09-01T00:00:00Z", "y": null}
{"ds": "2026-09-01T01:00:00Z", "y": null}
{"ds": "2026-09-01T02:00:00Z", "y": 146.9}
// 结果:删除首尾的空值,序列从 02:00 开始
{"id": "gap.edge_trim:meter-001", "rule": "gap.edge_trim", "class": "drop",
"status": "applied", "action": "drop",
"before": {"empty_edge_slots": 2}, "after": {"empty_edge_slots": 0}}window.context
// 输入:24 个点,模型最多使用 16 个
// 结果:只保留最近 16 个点
{"id": "window.context:meter-001", "rule": "window.context", "class": "drop",
"status": "applied", "action": "keep_latest",
"before": {"points": 24}, "after": {"points": 16}, "evidence": {"max_context": 16}}API 不会产生这条决定:长于模型最大上下文的序列直接返回 422 SERIES_TOO_LONG,由你决定发送哪些点。模型的最大上下文见模型与能力上限。
gap.split
// 输入:128 个观测值,第 40 至 51 小时整段缺失,options.gap_split_min_steps = 8
// 结果:丢弃缺口之前的 40 个点,保留之后的 88 个点,并附带 segment_split 警告
{"id": "gap.split:meter-001", "rule": "gap.split", "class": "drop",
"status": "applied", "action": "keep_latest_segment",
"before": {"points": 128}, "after": {"points": 88}, "reversible": true,
"evidence": {"min_steps": 8, "threshold_source": "option",
"dropped_observed": 40, "observed": 128}}- 缺口长度达到
evidence.min_steps才会拆分。未传options.gap_split_min_steps时自动取阈值(threshold_source: "auto"),取以下三者中最大的:补值上限加一、三个季节周期、序列跨度的 5%。 - 只有丢弃的观测值不超过一半时才会自动执行;否则只提议(
proposed),改为补齐缺口,并附带segment_split_skipped警告。 - 可以用
accept强制拆分,或用decline撤销拆分。
grid.regrid 与 gap.impute
缺少时间戳时,这两条决定成对出现:先插入空格点,再插值补齐。
// 输入:05:00 之后直接是 07:00,缺少 06:00
{"ds": "2026-09-01T05:00:00Z", "y": 138.8}
{"ds": "2026-09-01T07:00:00Z", "y": 138.2}
// 结果:插入 06:00,并用 PCHIP 补齐(短缺口,见策略版本参考)
{"id": "grid.regrid:meter-001", "rule": "grid.regrid", "class": "structural",
"status": "applied", "action": "insert_empty_slots",
"before": {"slots": 11}, "after": {"slots": 12}}
{"id": "gap.impute:meter-001", "rule": "gap.impute", "class": "fabricate",
"status": "applied", "action": "pchip",
"before": {"missing": 1}, "after": {"filled": 1},
"evidence": {"methods": {"pchip": 1}, "gaps": 1, "seasonal_period": 24,
"seasonality_accepted": false}}y 为 null 的行只产生 gap.impute。补齐的具体位置记录在 series[].actions[] 中。缺口长于补值上限(3 步)时仍会补齐,但附带 long_gap_filled 警告。
value.sentinel
// 输入:48 个点里有一个 -9999,其他值都在 132 至 148 之间
{"ds": "2026-09-02T06:00:00Z", "y": -9999}
// 结果:-9999 置为空值,再由 gap.impute 补齐
{"id": "value.sentinel:meter-001:-9999", "rule": "value.sentinel", "class": "fabricate",
"status": "applied", "action": "null",
"before": {"value": -9999.0, "count": 1}, "after": {"value": null, "count": 1},
"reversible": true,
"evidence": {"source": "builtin", "detached": true, "nearest_other": 132.0,
"spread": 16.0, "share": 0.020833}}- 内置的候选值为
9999、-9999、99999、-99999、-999、999999、65535、-32768。 - 只有当该值远离其他数据(
detached)且占比不超过 5%(share)时才自动执行;否则只提议,数值保持不变。 - 选择器是数值本身,所以同一实体的不同代码各有一个
id。 - 如果
-9999是真实数值,用decline保留它;报告会给出outlier_flagged_not_modified警告。
value.tokens 与 value.dash_token
// 输入(文本单元格):两个 "N/A",一个 "-"
// value.tokens:缺失标记置为空值,再补齐
{"id": "value.tokens:meter-001", "rule": "value.tokens", "class": "semantic",
"status": "applied", "action": "null",
"before": {"cells": 2}, "after": {"missing": 2}}
// value.dash_token:默认只提议按 0 处理
{"id": "value.dash_token:meter-001", "rule": "value.dash_token", "class": "semantic",
"status": "proposed", "action": "zero",
"before": {"cells": 1, "token": "-"}, "after": {"value": 0.0, "count": 1}}破折号可能表示 0,也可能表示缺失。未采纳时按缺失值处理;把 value.dash_token:meter-001 放进 accept 后按 0 处理。
5. 采纳与拒绝
在请求中按 id 列出要采纳或拒绝的决定:
"options": {
"cleaning": {
"preset": "strict",
"accept": ["gap.impute:meter-001"],
"decline": ["value.sentinel:meter-001:-9999"]
}
}accept让提议的决定执行:value.sentinel、value.dash_token、gap.split,以及strict预设下被降为提议的任何决定。decline撤销可撤销的决定(reversible: true):value.sentinel与gap.split,2026-11起还有grid.business_days与constraint.*。撤回gap.masked_tail(2026-12)则会以trailing_gap_too_long拒绝请求。- 同一个
id不要同时出现在两个列表中。 - 列出的
id在本次数据中不存在时,报告附带decision_not_applicable警告,请求照常处理。 raw预设不能与accept/decline同时使用。
strict 预设示例
下面的数据缺少一个时间戳,还有一个 -9999。default 预设会直接补齐;strict 预设把 gap.impute 与 value.sentinel 都降为提议,并拒绝请求:
// 422 SERIES_DISCONTINUOUS,error.details.cleaning_report 中:
"decisions": [
{"id": "value.sentinel:meter-001:-9999", "status": "proposed", ...},
{"id": "grid.regrid:meter-001", "status": "applied", ...},
{"id": "gap.impute:meter-001", "status": "proposed", ...}
],
"blocking": [
{"code": "cleaning_required", "unique_id": "meter-001", "reason": "series_discontinuous"}
]只采纳 gap.impute:meter-001 仍会被拒,因为 value.sentinel 还是提议状态。两个 id 都放进 accept 后请求通过,三条决定都为 applied。先用 /v1/clean 拿到 id,再在预测请求里采纳,是使用 strict 的推荐流程。