Imported from superlff888/aiAutoTest (
.qoder/skills/bus-price-quantile/SKILL.md). Install upstream withnpx skills add superlff888/aiAutoTest --skill bus-price-quantile. Copyright stays with the author.
使用方法
# 离线自测(不连数据库,34 项断言:业务口径 + 24/48/96 粒度自适应)
python scripts/bus_quantile.py --demo
# 离线自测并导出 Excel(想看 4 个 Sheet 长什么样时用)
python scripts/bus_quantile.py --demo --out output/_demo.xlsx
# 复算(**默认连测试环境 test**);交易中心与交易日取自 sqls/节点分时价格_JSON数组.sql
python scripts/bus_quantile.py
# 复算生产环境(必须显式指定)
python scripts/bus_quantile.py --connection prod
# 控制台只看指定时段
python scripts/bus_quantile.py --period 40
# 只看控制台不生成 Excel
python scripts/bus_quantile.py --no-excel
Windows PowerShell 下须先设置 $env:PYTHONUTF8=1,否则中文输出乱码。
参数
| 参数 | 必填 | 默认 | 说明 |
|---|---|---|---|
--center |
否 | — | 仅用于与 SQL 中写死的 trade_center_id 做一致性校验,不符则报错退出 |
--date |
否 | — | 仅用于与 SQL 中写死的 date 做一致性校验,不符则报错退出 |
--connection |
否 | test |
数据库环境,test / prod。默认走测试环境(业务方 2026-09-09 确认),查生产必须显式传 --connection prod;非 prod 时控制台会额外提示,防止把测试数据当生产结果 |
--period |
否 | 全部 | 只输出指定时段序号(0-based);仅收窄控制台,Excel 仍为全量 |
--sql-file |
否 | 内置 | 覆盖内置 SQL 模板路径 |
--out |
否 | 自动 | Excel 输出路径;与 --demo 同用时必须显式指定才会写文件 |
--no-excel |
否 | 输出 | 跳过 Excel |
--rows |
否 | 8 |
仅截断同母线分组明细(组数可达数百,全量在 Excel);分位表与宽度表始终全量,要收窄时段请用 --period |
--demo |
否 | — | 离线自测,两层共 34 项断言:第一层 9 节点 × 4 时段验业务口径(含 4 个单口径节点),第二层 12 节点 × 24/48/96 时段验粒度自适应;默认不写任何文件 |
查询范围由 SQL 文件决定
交易中心与交易日写死在 sqls/节点分时价格_JSON数组.sql 的 WHERE 里,由业务方直接维护该文件,脚本不做占位符注入。换查询范围就改 SQL;脚本用 parse_sql_params 反解这两个值,作为控制台标签与 Excel 文件名(母线分位_<中心>_<日期>.xlsx)的唯一来源,因此不会出现「标签说 A、实际查 B」的错标。
--center / --date 保留为可选的一致性校验:传了且与 SQL 不符会直接报错退出,用于防止误以为在查另一个范围。日期也支持 SQL 里写 BETWEEN '起' AND '止',此时标签为 起~止。
业务口径
以下口径由业务方多轮澄清确认(最近一轮 2026-09-09),是本技能的判定基准,不得自行修改。
| 项 | 口径 |
|---|---|
| 同母线判定 | 两节点各自可用口径集合相同,且该集合内每条全分时序列逐时段恒等。不是某个时刻同价 |
| 口径集合规则 | 四种组合:甲乙均双口径 → 日前与实时两条序列都要恒等;甲乙均仅日前 → 只比日前,恒等即同母线;甲乙均仅实时 → 只比实时,恒等即同母线;口径集合不同(如甲仅实时、乙仅日前)→ 必然不同母线,即使序列数值恰好完全相同也不得合并。故节点级口径不对称是合法数据形态,不报错 |
| 整体未披露 | 若某口径在全库范围内一行都没有,则该口径不进入输出集合(A9 的降级场景) |
| 时段粒度 | 24 / 48 / 96 三种,同一交易中心内唯一,由 data.$.interval(分钟)声明 |
| 名单机制 | 查询时实时比对,无预生成名单表、无名单查询接口 |
| 恒等判定 | 数值直接相等,不设精度容差。全程 Decimal 承接,杜绝浮点误差导致的静默漏判 |
| 分位定位 | 线性插值法:h = (N−1) × x%(0-based),取相邻两样本点按小数部分加权。业务方 2026-09-09 变更口径(原为最近秩法),分位值可为样本之间的小数,不再保证为实际披露价 |
| 区间开闭 | 统一闭区间,价格恰等于端值的节点算落在区间内 |
| 样本构成 | 库内该中心该日期全部节点。0 价与负价属合法披露值必须计入;停牌/检修等未披露价格节点不会在库内存在,故无任何过滤规则 |
| 分位口径 | 逐时刻横截面计算:先按全序列划分等价组(与时刻无关),再对指定时刻取每组价格算分位。同母线组每时刻只记一次。某口径的样本只含具备该口径的组,故日前与实时的 N 可以不相等 |
| 单位 | 库内为 元/kWh,×1000 换算为 元/MWh(固定物理换算,1 MWh = 1000 kWh,不随中心变化) |
| 节点标识 | 只认 market_node_id(业务方 2026-09-04 确认)。name 仅用于展示,不参与归并、不参与同母线判定、不做名称归一化 —— node_id 不同但名称相似的记录一律视为两个独立节点,即使业务上疑似同一实体 |
小样本天然退化,无需额外降级规则:N=10 时 P05=最小值、P95=最大值、W90=极差;N=1 时全部分位点相等、所有宽度为 0。
计算流程
| # | 步骤 | 关键点 |
|---|---|---|
| 1 | 执行 SQL 取数 | 见 sqls/节点分时价格_JSON数组.sql,仅 SELECT |
| 2 | 按 market_node_id 归并双 type 行 |
合成节点对象 {日前序列, 实时序列}。漏掉这步 N 会虚增、判定退化为单口径 |
| 3 | 全局判定输出口径集合 | 某口径全库无任何行 → 整体未披露,不输出该口径 |
| 4 | 校验时段数唯一 | 只统计节点实际具备的口径(缺失某口径不报错);混入多种粒度则报警中止,不静默按短的比 |
| 5 | 划分同母线等价组 | 哈希分桶,key = (该节点口径集合, 集合内各口径序列) |
| 6 | 逐时刻算分位 | 每口径只取具备该口径的组 → N 个值 → 升序 → 线性插值 |
| 7 | 输出 | 控制台摘要 + Excel 4 Sheet |
为什么第 2 步是关键陷阱
同一节点在库内有 2 行(type=11 日前存全时段、type=12 实时存全时段),唯一键是 (trade_center_id, date, type, market_node_id)。若把每行当独立节点:
- 样本单位从「节点」变成「行」,N 虚增
- 日前行只会和日前行比、实时行只会和实时行比(同节点的日前与实时数据本来就不同,不可能互相恒等),等于把「双口径都要恒等」偷偷降级成「单口径恒等」
- 分位值整体偏移,且不报错 —— 属静默错误
--demo 内置了这组反面对照:同样 14 行数据,正确归并得 7 组(日前 N=6、实时 N=5),错误做法得 9 桶,且日前与实时价格被混进了同一个池子。
为什么第 5 步用哈希分桶
两两比较是 O(n²):500 个节点要比 12.5 万次,每次比 96 个元素 ≈ 1200 万次比较。哈希分桶只需 500 次哈希,dict 天然把相同序列归到一起。
为什么分桶 key 必须带上「口径集合」
只用序列做 key 会静默错判:一个「仅实时 [400,410]」的节点与一个「仅日前 [400,410]」的节点,序列元组完全相同,会被并进同一组 —— 但按业务口径它们必然不同母线。所以 key 是 (口径集合, 各序列...),口径集合不同就绝不会同桶。--demo 里的「辛」节点(仅实时,数值与「己庚」的日前序列完全相同)专门钉住这条。
为什么全程用 Decimal
json.loads 默认把数值解析成 float,而 float 下 0.305 × 1000 = 305.00000000000006。两个库内完全相同的值经 float 承接后可能出现末位差异,导致恒等判定静默漏判同母线。脚本用 json.loads(raw, parse_float=Decimal, parse_int=Decimal) 从源头精确承接。
「不设精度容差」这条口径成立的前提就是数值承接不引入误差。
SQL 如何适配 24/48/96 时段
关键手段:用 JSON_EXTRACT(data,'$.energy') 整体取出数组,不展开成列。
96 列展开写法: $.energy[0] ... $.energy[95] ← 时段数编码进列结构
24 点时后 72 列全为 NULL
本技能写法: $.energy ← 整个数组取出,长度由数据决定
未来出 288 点也不用改 SQL
配套两点:
data->>'$.interval'直接给出粒度声明(60→24点、30→48点、15→96点),不靠数 NULL 猜- 用
->>而非JSON_EXTRACT:后者返回 JSON 类型,若interval在 JSON 中存为数字而 CASE 里写字符串字面量,会因 JSON 类型不同而全部落到 ELSE 分支
粒度 列只认识 60/30/15,出现其它值显示"异常"并附实际时段数,让脏数据显形而非静默通过;此时 价格序列 列仍完整返回数组,数据不丢。
离线自测如何覆盖这三种粒度
--demo 分两层,共 34 项断言:
| 层 | 数据 | 覆盖 |
|---|---|---|
| 第一层 | 9 节点 × 4 时段 | 业务口径:同母线判定、口径集合规则、线性插值分位、Decimal 精度、归并必要性反面对照。断言依赖手算的具体数值 |
| 第二层 | 12 节点 × 24 / 48 / 96 时段 | 粒度自适应:时段数识别、首末时刻标签(00:00 与 23:00 / 23:30 / 23:45)、interval 与 1440/时段数 自洽、混粒度中止分支、--rows 截断语义 |
第二层不手写 96 个时段的数值,而是用规律序列:节点值 = 基准 + 时段序号 × 1。所有节点同形状、仅基准不同,于是每时刻的横截面样本都是同一组基准整体平移,可得两条强性质:
- 9 个分位点必须随时段线性递增(
P_x(t) = P_x(0) + t) - 4 个宽度指标必须全时段恒定(平移不改变离散度)
长序列下插值一旦定位错误(off-by-one、把时段序号混进 h 计算),宽度就会随时段漂移而被抓住 —— 无需手算 96×2×13 个值。基准值也特意挑过:日前口径 N=9 时升序基准恰为 [100,200,…,900]、相邻间隔恒为 100,插值结果 = 基准[lo] + frac×100,可精确手算比对。
分组结果与粒度无关(三种粒度均为 12 节点 → 10 组、日前 N=9、实时 N=8),这条本身也是断言之一。
为什么第一层的 4 时段不足以覆盖粒度
4 < 8(--rows 默认值),也小于任何真实粒度。曾因此漏掉一个缺陷:print_summary 里写过 max_rows=max(args.rows, 时段数),把 --rows 顶成时段数而完全失效 —— 4 时段下 max(8,4)=8 行为恰好正常,离线自测从结构上不可能发现,直到 48 点真实数据才暴露(且该 max() 在 --period 场景下 max(8,1)=8 也不提供任何保护,属逻辑上无价值的代码,已移除)。第二层的 96 时段加上专门的截断断言堵住了这个口子。
输出格式
控制台
- 概要:数据库环境、时段粒度、可用口径、库内行数、节点数、同母线组数、去重效果、各口径样本量 N(两口径可不相等,并标注有多少组无此口径而不参与)
- 同母线分组明细:组号、组内节点数、本组口径、组内节点名(组内节点多的排前面)。受
--rows截断,超出部分提示「其余 X 组见 Excel」 - 分位区间与宽度:按口径分别输出(日前一套、实时一套),每套含时段序号、时刻、P05/P15/P25/P35/P50/P65/P75/P85/P95,以及 W90/W70/W50/W30;表头标注该口径的 N,时段始终全量输出不截断
日前与实时两套都算。若页面只展示一套,另一套可作参考;某口径整体未披露时自动退化为只输出一套。两套的 N 可以不相等(各自只统计具备该口径的组),控制台与 Excel 均按口径分别标注 N。
Excel(4 Sheet)
| Sheet | 内容 |
|---|---|
母线分组 |
每节点一行:组号、组内节点数、market_node_id、节点名称、本组口径、日前序列、实时序列。节点不具备的口径列填「(该节点无此口径)」。便于查某节点归属哪组 |
分位区间 |
口径 × 时段:时段序号、时刻、样本量N(按口径各自统计)、9 个分位点、4 个宽度指标 |
组价格序列 |
每组只展开它实际具备的口径(单口径组就只有一行),时段列头为 HH:MM,便于与页面逐组对照 |
运行参数 |
中心、日期、环境、粒度、时段数、可用口径、行数、节点数、组数、样本量N(按口径)、单位与换算系数、同母线判定与分位口径说明、运行时间 |
默认输出到 output/母线分位_<中心>_<日期>.xlsx。
--demo 模式默认不写文件,保持 output/ 只存真实复算产物;需要查看 Excel 格式时用 --demo --out <路径> 显式指定。
数据校验(与口径冲突时报错中止)
脚本不会静默吞掉异常数据,以下情况抛 BusQuantileError 并退出码 2:
| 情况 | 冲突的口径 |
|---|---|
| 查询返回 0 行 | SQL 中写死的中心/日期有误,或该日无数据 |
| 同一节点同一口径出现重复行 | 同一 (date, type, market_node_id) 应唯一 |
market_node_id 为空 |
节点身份只认该列,不降级到 name;为空即硬阻塞,报错并提示改用填充分界日之后的交易日 |
数组内含 null 元素 |
业务方确认不含 null |
| 某节点无任何可用口径序列 | 无法参与同母线判定 |
| 一批数据混入多种时段数 | 交易中心内粒度唯一 |
出现未知 type(非 11/12) |
仅日前/实时两个口径 |
SQL 中残留 {center_id} / {trade_date} 占位符 |
查询范围须写死在 SQL 里,本技能不做注入 |
依赖与配置
- 数据库配置:读
.env的TEST_MYSQL_*/PROD_MYSQL_*(HOST/PORT/USER/PASSWORD/DATABASE),与db-connector技能共用同一套配置,无需新增。默认连test,两套均已实测连通且market_day_price_details表都存在 - Python 依赖:
pymysql、python-dotenv(连接);openpyxl(Excel 输出,缺失时自动跳过并告警) - MySQL 版本:5.7.8+ 即可(用
JSON_EXTRACT/JSON_LENGTH/->>)。不依赖JSON_TABLE(需 8.0.4+) scripts/db_executor.py从db-connector复制而来,保持技能自包含,不跨技能目录 import
表结构假设
| 字段 | 说明 |
|---|---|
market_day_price_details |
日价格明细表 |
trade_center_id |
交易中心ID |
date |
交易日(MySQL 保留字,SQL 中加反引号) |
market_node_id |
节点ID,脚本按它归并同一节点的日前/实时两行。该列是采集程序关联主数据表 market_node_info 回填的派生字段,2026-07 中旬才上线且未回补历史,早期日期为 NULL(各中心分界日不同,如 center 2 为 2026-07-12);部分 type(如 center 3 的 31/32)也不写此列。选日期前须先确认该列填充率 |
name |
节点名称 |
type |
int 列,11=日前价格、12=实时价格 |
data |
JSON,含 $.energy(价格数组,元/kWh)与 $.interval(粒度,分钟) |
已知限制
- 单次运行只处理一个交易中心 + 一个交易日。跨日对照需分次执行,且复算须逐日切开(不同日的分组与分位互相独立)
market_node_id在历史日期上可能全为 NULL,此时技能直接报错中止(不降级到name,业务方已确认节点身份只认market_node_id)。该列由采集程序关联主数据表market_node_info回填,2026-07 中旬才上线且未回补历史,各中心分界日不同(center 2 = 2026-07-12、center 34 = 07-16、center 5 = 07-15、center 12 = 07-19);center 1/3/38 自 2025 年起即有值,但 center 3 的 type 31/32 不写此列- 主数据存在疑似重复登记:prod center=3 / 2026-08-01 复算结果里,
嘉化兴港变+秀舟纸业构成一个仅日前组,而嘉化兴港变(私)+秀舟纸业(秀舟热电)构成一个仅实时组 —— 疑似同一实体被登记成两个market_node_id、各带一半口径;market_node_info里中能热电相关也有 3 条记录 3 个 ID(两条同秒创建)。按「只认 node_id + 口径集合相同才可比」的口径,它们必然是不同节点、不同组,本技能不做名称归一化 —— 若被测系统另有合并逻辑,双方 N 与分位值会不一致,属主数据治理问题,需业务方处理 - 节点级口径不对称是常态,两口径 N 不相等:prod center=3 / 2026-08-01 有 15 个仅日前、72 个仅实时节点(占 469 个的 18.6%),2026-09-01 为 17 / 71。已按「口径集合相同才可比」处理,不再报错中止。该日复算结果:48 点粒度、469 节点 → 413 组,日前 N=348、实时 N=399。若被测系统对单口径节点另有处理(如直接排除出样本),对照会出现系统性偏差,需与开发确认
- test 与 prod 数据形态可能不同,默认结果不能直接当生产结论:默认连
test。已实测center=3 / 2026-08-01两环境聚合结果完全一致(851 行 / 469 节点 / 413 组 / 日前 N=348、实时 N=399,前 12 组成员逐个相同),该切片可放心用 test;但center=2 / 2026-07-01不同 —— test 为 480 节点 → 160 组,prod 同切片market_node_id全为 NULL(早于该中心 2026-07-12 的填充分界日)会直接报错中止。控制台摘要与 Excel运行参数均记录环境名,引用结果前必须先确认环境 - 分位值只与「组」有关,不输出组内各节点的单独贡献
- 分组展示形态(页面在哪展示、什么形态)业务方暂未定义,本技能只输出分组数据本身,不做 UI 层断言