跳转至

报告查询与导出

任务型入口:案例 6:保存后继续查询 · 案例 7:同次分析的多种交付。

外部 Agent、业务元数据、证据与跨会话使用见外部 Agent 指南。 评分卡另见评分卡,本页旧章节及锚点继续保留。

Reporting:Stable

本页的结构化 report 读取和 Excel/HTML 导出属于 Stable Reporting 能力。评分卡能力的状态 单独标注在对应章节。

适用场景

你可以继续筛选、复核和组合 Report 中的结果,再导出 Excel/HTML 归档或交付。 Scorecard 则将已拟合的分箱规则和逻辑回归系数转换为评分映射和 SQL。

1. 获得 Report

下面的示例已纳入文档测试。它会定义 report,后续导出调用都使用这个对象:

import polars as pl

from mars.analysis import profile_risk

df = pl.DataFrame(
    {
        "apply_dt": [
            "2026-01-03",
            "2026-01-10",
            "2026-01-17",
            "2026-01-24",
            "2026-02-03",
            "2026-02-10",
            "2026-02-17",
            "2026-02-24",
            "2026-03-03",
            "2026-03-10",
            "2026-03-17",
            "2026-03-24",
        ],
        "month": ["2026-01"] * 4 + ["2026-02"] * 4 + ["2026-03"] * 4,
        "income": [
            3200,
            3600,
            -999,
            None,
            3300,
            4200,
            -999,
            5800,
            3400,
            4300,
            None,
            6100,
        ],
        "utilization": [
            0.12,
            0.18,
            0.52,
            0.61,
            0.14,
            0.29,
            0.54,
            0.58,
            0.16,
            0.31,
            0.56,
            0.63,
        ],
        "segment": [
            "new",
            "repeat",
            "vip",
            "vip",
            "new",
            "repeat",
            "vip",
            "vip",
            "new",
            "repeat",
            "vip",
            "vip",
        ],
        "target": [0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1],
    }
)

risk_profile = profile_risk(
    df,
    target="target",
    features=["income", "utilization", "segment"],
    group_col="month",
    time_col="apply_dt",
    binning_type="native",
    method="quantile",
    n_bins=4,
    missing_values=[-999],
    special_values=[-999],
    psi_include_missing=False,
    psi_include_special=False,
)

report = risk_profile.report
summary = report.summary_table
binner = risk_profile.binner

常见对象与字段:

Report 状态 常用字段
MarsProfileReport Stable overview_table、dq_tables、stats_tables、comparison_tables、report_meta
MarsBinningReport Stable summary_table、detail_table、trend_tables
CorrelationReport Stable features、pairs、correlation_decisions 等公共表;相关性指南
ScoreCrossReport(mars.analysis) Stable cells、边际、overall、bins;模型分交叉指南
MarsRuleReport Experimental summary_table、detail_tables、metadata;公共查询、桥接关联、保存恢复
MarsMonitoringReport Experimental 监控汇总、分箱统计、表现覆盖率和元数据
MarsModelingReport Experimental 多样本切片的汇总、明细、趋势和元数据

查询已有报告和交给 AI

画像、分箱、相关性与模型分交叉报告提供 Stable describe()、get_table()、to_ai_context() 和 get_feature()。 这些方法只查询已计算结果,不调用 LLM、不重新计算统计,也不修改原报告。

规则报告也满足公共 Report 契约,summary 汇总一次挖掘的结果。 按成员特征查询时,接口先找到相关规则,再读取规则指标,不为每个特征复制指标。 候选、切片、显式高级分析和 no_rules 状态见 规则报告与外部 Agent。

"""报告查询与 AI 上下文示例,不调用模型,不读写原始数据文件。"""

import json

import polars as pl

from mars.analysis import MarsBinEvaluator, profile_stats

data = pl.DataFrame(
    {
        "income": [2000.0, 3000.0, 4000.0, None, 5000.0, 6000.0, 7000.0, 8000.0],
        "target": [1, 1, 0, None, 1, 0, 0, 0],
        "month": ["2026-01"] * 4 + ["2026-02"] * 4,
    }
)
result = MarsBinEvaluator(binner_params={"n_bins": 3}).evaluate(
    data,
    target="target",
    features=["income"],
    group_col="month",
    batch_size=1,
    feature_data_source={"application": ["income"]},
    feature_metadata={"income": {"display_name": "月收入"}},
)
report = result.report
description = report.describe()
top_features = report.get_table("summary", sort_by="iv", descending=True, limit=10)
bins = report.get_table(
    "detail", features="income", filters={"bin_index": {"op": "ge", "value": 0}}
)
context_json = report.to_ai_context(features="income", tables=["summary"], max_chars=16000)
assert json.loads(context_json)["evidence"][0]["reference"] == "summary"
for evidence in json.loads(context_json)["evidence"]:
    replayed = report.get_table(evidence["reference"], **evidence["query"])
    assert replayed.to_dicts() == evidence["rows"]
feature_data = report.get_feature("income")
summary_view = report.show_summary(sources="application", columns=["feature", "iv", "ks"], limit=10)
assert summary_view.data.columns.tolist() == ["feature", "iv", "ks"]
native_summary = report.get_table("summary")
assert native_summary.columns[:4] == ["feature", "display_name", "dtype", "data_source"]

profile = profile_stats(data, features=["income"], metrics=["missing", "mean"], group_col="month")
overview = profile.get_table("overview", columns=["feature", "missing_rate", "mean"], limit=10)
profile_context = profile.to_ai_context(tables=["dq.missing"], features="income")
numeric_context = json.loads(profile.to_ai_context(columns=["mean"], limit=1))
assert numeric_context["evidence"][0]["identities"] == [{"feature": "income"}]
assert set(numeric_context["description"]["feature_metadata"]) == {"income"}

describe() 返回表粒度、行数、字段类型、指标单位、实际参数和限制。画像表名为 overview、 dq.<metric>、stats.<metric>、comparison.<metric>;分箱表名为 summary、detail、 trend.<metric>,可选附表为 missing_by_day 和 risk_corr_reference。没有计算的表不在目录中。

get_table() 保持所选表的 Pandas/Polars 类型。支持 features、columns、filters、 sort_by、descending、offset、limit 和 sources。筛选是字段到标量相等条件,或 {"op": "gt", "value": 0.1};操作符只允许 eq/ne/lt/le/gt/ge/in/not_in/is_null/is_not_null。 没有 SQL 或表达式执行入口。排序在列投影之前,排序配合 limit 就是 Top-K。 sources 使用 feature_metadata 中的来源,旧来源参数会转换为同一格式后合并;未知来源会报错。

画像和分箱报告中,按 feature 统计的原生表(包括直接访问的属性)默认采用以下列顺序: feature → display_name → dtype → data_source → 其他维度与指标。已有 target/y 前缀保留。dtype 来自当前输入的原始 schema,参考类型另见 benchmark_dtype; 用户的 feature_metadata 仍只接受 display_name、description、data_source、unit。 未提供显示名时,原表保留 null,展示时可以使用英文名;未知来源记为 UNMAPPED。 非空身份信息存在冲突时会报错。长描述和单位保存在集中字典中,不重复写入每个分箱行。 关系表保留 feature_a/feature_b 等原有角色字段。

columns=None 返回默认 schema;显式 columns=[...] 仅返回所列字段并保持指定顺序, show_* 与快照 show_table() 也遵守这个规则。可以按未输出的列筛选/排序,错误字段会报错。 例如 profile.show_overview(sort_by="missing_rate", columns=["feature", "display_name", "missing_rate"]) 按真实缺失率全局排序,不会被 dtype 前置排序掩盖。组值不能与身份等保留列同名。

新报告的分箱趋势目录没有 trend.bad_rate;读取 detail.bad_rate 或同组 bin_type="汇总组" 的总体箱统计。历史快照保存过的趋势表仍保留。读取旧文件先严格校验磁盘版本、schema 和完整性, 再在内存补齐身份列;没有可靠原始类型时 dtype 保留 null,不采用旧趋势的聚合 Float64。

画像/分箱的 AI JSON 默认只包含 overview/summary 前 10 行;规则报告默认包含 summary 一行和最多三条 最终验证证据,不默认输出 expression。预算为 16,000 个 Unicode 字符,按字符计数,不按 token 计数。 可以按表、特征和列缩小范围。省略的参数和行有明确引用及原因;预算连说明都容不下时抛 ValueError,不会输出无效 JSON。日期使用 ISO-8601;浮点非有限值使用带类型的 $mars 标记; Null 使用 JSON null,0 保留数值。Null 的业务原因需要结合标签状态、诊断与计算参数解释。 KS 是 0–100 百分制指标,缺失率和坏率是 0–1 比例,IV/PSI 是无量纲数值;币种、标签定义和观察 窗口未登记时标为 unknown。比较前核对拟合来源、参考、权重、箱范围和指标排序;JSON 不生成归因结论。

get_feature("income", limit=100) 返回关联表、每表省略行数和 unavailable 类别。 show_overview、show_summary、show_trend 也支持 columns、limit、sources,复用同一查询。 这些旧展示方法保持不传 limit 时的历史默认;新增 get_feature 与 AI 上下文有默认规模上限。

2. 导出 Excel 或 HTML

以下代码继续使用上一步定义的 report:

report.write_excel("risk_report.xlsx", engine="openpyxl")
report.write_html(
    "risk_report.html",
    report_name="Current-period risk review",
    max_plots=100,
    chart_embed_mode="auto",
)
HTML 模式 行为
auto 小报告内嵌图片,大报告生成同级资产目录并懒加载
inline 所有图片内嵌,适合必须单文件离线转发的报告
asset 强制使用相对路径图片目录,适合大报告归档

风险趋势图和 HTML Charts 需要评估阶段已经提供有效 time_col。group_col 不能替代日期范围。

MarsProfileReport.write_html() 始终生成无外部资源的单文件,包含 Metadata、Overview、 DQ、Stats 和 Comparisons 页面,不生成图表。 所有 Stable 报告导出都会在资源缺失、请求内容未生成或写入失败时抛出异常。 全局搜索与每张表的局部搜索按交集生效;清空其中一个仍保留另一个条件。 局部搜索只影响所属表,排序和页面切换保留条件。当前是页面导航,没有行级分页。

DQ/Stats 页面内部只展示当前指标,指标页签支持键盘方向键和 Home/End,并在外层切换后保留选择。 比例显示为保留两位小数的百分比,浮点统计显示两位小数,整数计数保留整数,小数权重不截断; PSI/IV/Lift/WOE 保持各自数值,AUC 使用自身数值格式,KS 为 0–100 点。 缺失显示 —,原始数值仍可从 tooltip 或 get_table() 查看。排序使用未舍入原值, 全局和局部搜索可检索英文、中文名及来源;元数据、日期、状态和类别不参与数值色阶。

Notebook、画像 HTML 与画像 Excel 均按指标语义决定单位。宽趋势的列名是业务分组, 即使包含 rate、ratio、auc 或 ks 也不改变该指标的单位。比例端点 0/1 仍显示为 0.00%/100.00%,整数计数不带小数,有小数的加权计数保留小数。

原生图在原说明框首行显示已登记中文名,保留英文 ID 和原指标。显式 group_col 时, 各面板首行显示本次分组全样本的有效 time_col 日期范围,包括未表现标签和缺失特征样本。 没有合法日期的组显示未知;旧报告缺少逐组范围时省略新增行,不使用全局日期代替。

report.plot_risk_trends(features="income", show_risk="both", include_total=False)
report.save_risk_trend_images(
    "charts", features="income", image_format="png", show_risk="both",
    include_total=False, dpi=300,
)
report.write_html("risk_report.html", max_plots=3, include_total=False)

分箱 HTML 的 include_detail 控制 Grouped Pivot 明细区域,include_charts 独立控制 原生趋势图。include_detail=False 会省去 Pivot 页面、导航、控件及其明细数据;图表仍可 读取原报告明细。两项均为 False 时可以导出纯摘要页面,无需 time_col;启用图表时仍需有效 时间信息。旧 #pivot / #pivot-section 链接会回退到有效页面。 这些开关不改变公共表、查询、保存内容或统计结果。

Charts 搜索支持已登记的中文名、英文 ID、目标及来源;重名中文名会命中所有对应特征, 全局与局部条件按交集生效。英文 feature 继续作为查询、证据和文件名的身份。 Notebook 与完整 HTML 的 Features 均按唯一英文特征计数;PSI 报警计数为 PSI > 0.25 的特征(任一标签触发),同一特征多标签触发只计一次。 未计算值不触发报警,报警数为零不能证明全部特征正常。

build/plot/save/HTML 原生图入口都接受仅限关键字参数 include_total=True。 该参数只控制是否显示 Total 面板。全局 IV/KS/AUC、样本数、Missing、RC 参考和底层明细仍来自完整统计。 关闭后没有任何面板时明确报错。普通业务组名称包含 Total 仍保留。

MarsBinningReport.write_excel() 无论源报告的 ks_method 是 raw 还是 binned,都复用同一 22 列原生模板,主展示统一为分箱 KS(0–100 点)。导出副本的 ks 按特征、目标和实际分组 取 max(detail.ks_bin),广播到同组全部箱行;ks_bin 保留原逐箱值。KS 与综合指标透视 读取导出的 ks,综合指标保留模板的 average 聚合。最终汇总使用对应 Total 的分箱 KS, 最终 KS 趋势保留首目标、实际分组与身份列。全部逐箱 KS 未定义的组在 Excel 中保持空值。

原生 Excel 只根据已计算的数据构建导出副本,不重新拟合或评估,也不改写源报告。 口径说明分别记录源报告 ks_method、导出 binned 口径、max(detail.ks_bin) 来源及单位; raw 计算诊断明确属于源报告。Python 公共查询、describe()、AI 上下文、保存/加载、HTML 和绘图继续遵循源报告语义,raw 模式数值特征的最终 ks 与类别特征的 binned 回退保持原样。 xlwings 可由 Excel 刷新透视和重算;openpyxl 只写结构/数据并标记打开时刷新,不执行公式或透视计算。 两种输出应使用不同文件名。snapshot_report(...).write_excel() 仍忠实静态导出保存的原报告 数值,没有模板透视交互;源报告为 raw 时,其中的 ks 仍按原报告 raw 语义保存。

两引擎的原生附表按实际导出字段设置数值格式,比例显示百分比,KS 保持 0–100 点, AUC、IV/PSI/Lift/WOE 保持数值;整数计数带千分位,小数权重与金额保留两位小数,金额不猜币种。 有内容的“最终汇总”作为默认打开页,保留原模板工作表及透视引用。 原生分箱、画像与静态快照 Excel 均将业务字符串按原文保存,包括 =1+1、00123 和日期形文字; 数值与真实日期保持各自类型,模板公式保持原有行为。

分箱报告的原始 Excel 入口保留透视模板;缓存可能仍是模板占位值,需要在原生 Excel 中 刷新透视表并保存。openpyxl.load_workbook(..., data_only=True) 不会执行这个刷新。 向人工用户交付本次当前数值时,使用已有静态快照入口:

from mars.reporting import snapshot_report, load_report

snapshot_report(report).write_excel("risk_static.xlsx")
report.save("risk.marsreport")
# 另一进程只持有保存文件时,也能直接静态导出。
restored = load_report("risk.marsreport")
restored.write_excel("risk_restored_static.xlsx")

静态工作簿逐公共表写出当前值,不依赖透视缓存;它不提供原模板的可刷新透视交互。 最短完整示例同时包含保存、加载和实际 Notebook 比较表渲染:

"""静态 Excel 交付与比较表 Notebook 展示的最短完整示例。"""

from pathlib import Path

import polars as pl
from openpyxl import load_workbook

from mars.analysis import profile_risk, profile_stats
from mars.reporting import load_report, snapshot_report

data = pl.DataFrame({
    "income": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
    "bad": [0, 0, None, 0, 1, 1, None, 1],
})
report = profile_risk(data, target="bad", n_bins=2).report

# 静态工作簿直接写出当前统计值,不需要 Excel 刷新透视缓存。
snapshot = snapshot_report(report)
snapshot.write_excel("risk_static.xlsx")
workbook = load_workbook("risk_static.xlsx", data_only=True)
assert workbook["000_summary"]["A1"].value == "feature"
workbook.close()

# 只有 marsreport 文件也能导出;可将此步骤放到另一 Python 进程。
archive = Path("risk.marsreport")
report.save(archive)
restored = load_report(archive)
restored.write_excel("risk_restored_static.xlsx")

current = pl.DataFrame({"category": ["甲", "新类别", None], "number": [1, 2, 3]})
benchmark = pl.DataFrame({"category": ["甲", "乙"], "number": ["one", "two"]})
comparison = profile_stats(
    current, benchmark_df=benchmark, metrics=["schema", "unseen"],
    categorical_features=["number"],
)

# Notebook 中可直接显示 Styler;这里实际渲染,确保文本状态保持原样。
schema = comparison.show_trend("schema")
unseen = comparison.show_trend("unseen", sort_by="feature", sort_ascending=True)
assert "incompatible_change" in schema.to_html()
assert "incompatible_dtype" in unseen.to_html()
comparison.write_html("comparison.html")

3. 单独复用趋势图

继续使用上一步定义的 report:

figures = report.build_risk_trend_figures(features=["income"])
fragment = report.render_risk_trends_html(
    features=["income"],
    image_format="svg",
    embed_mode="inline",
)

fragment.html 是可嵌入现有模板的 HTML 片段;资产模式同时返回已写入的图片路径。

中文名和分组日期按实际绘制时的 renderer 与 DPI 排布;取得 Figure 后修改 DPI,或在 PNG、SVG 之间反复导出,也会重新测量新增文字。英文身份、业务标题和统计沿用原有字体与 字号,$、反斜杠等业务文字按字面显示。

4. 构建评分卡

评分卡为 Experimental,暂时停止功能迭代;现有转换与部署能力继续保留。 必要的正确性、运行修复与上游适配继续处理,规则见稳定性。

Scoring:Experimental

评分映射、刻度参数和 SQL 输出仍可能调整。本文对应 0.0.28,发布后按 安装指南固定该版本;若 PyPI 尚未提供,使用 main 源码 并记录核验的提交。为 points_table 和生成 SQL 增加契约测试。

import polars as pl

from mars.feature import MarsNativeBinner
from mars.scoring import build_scorecard

X = pl.DataFrame(
    {
        "income": [2600, 3100, 3500, 3900, 4500, 5200, 6100, 7200],
        "utilization": [0.72, 0.64, 0.55, 0.48, 0.36, 0.28, 0.19, 0.11],
    }
)
y = pl.Series("target", [1, 1, 1, 0, 1, 0, 0, 0])

binner = MarsNativeBinner(method="quantile", n_bins=4).fit(
    X,
    y,
    features=["income", "utilization"],
)
scorecard = build_scorecard(
    binner,
    coefficients={"income": -0.35, "utilization": 0.70},
    intercept=-1.2,
    pdo=20,
    base_score=600,
    base_odds=50,
)

points_table = scorecard.points_table
sql = scorecard.generate_sql(
    features=["income", "utilization"],
    table_prefix="applications",
    score_name="credit_score",
)

评分卡要求分箱器已经使用 target 拟合并具备 WOE 映射。系数字典的特征必须与分箱器规则一致。

常见失败

  • HTML 图表为空:确认生成 report 时传入了有效 time_col。
  • 大报告单文件打开缓慢:使用 auto 或 asset,不要强制内嵌数百张图片。
  • 评分卡提示缺少映射:确认 binner 已拟合、特征名一致且包含 WOE 统计。

下一步

可携带的公共分析报告

状态:Stable。 画像与分箱报告实现 mars.reporting.Report 契约。外部建模 Agent、诊断归因 Agent 可以直接调用 describe()、search_features()、get_table()、query_page()、 get_feature()、to_ai_context() 和 save(),无需 MarsAgentSession、Notebook 或原始宽表。

完整可运行示例(包括新 Python 进程恢复、两种 Agent 消费方式及 HTML/Excel 导出):

python docs/snippets/portable_reports.py
"""完整报告跨进程使用示例;业务定义仅为模拟数据定义。"""

from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path
from tempfile import TemporaryDirectory

import pandas as pd
import polars as pl

from mars.analysis import profile_risk, profile_stats
from mars.reporting import load_report


def main() -> None:
    """生成、展示、导出、保存和恢复报告,并演示两种 Agent 消费方式。"""
    df = pl.DataFrame(
        {
            "income": [2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000],
            "salary": [2100, 2800, 4200, 4700, 5900, 7100, 8100, 8900],
            "y30": [0, 1, 0, 1, None, None, None, None],
            "y60": [None] * 8,
            "split": ["train"] * 4 + ["oot"] * 4,
        }
    )
    dictionary = {
        "income": {
            "display_name": "月收入",
            "description": "模拟申请的申报税后月收入",
            "data_source": "application",
            "unit": "CNY/month",
        },
        "salary": {
            "display_name": "月收入",
            "description": "模拟授权流水的入账月收入",
            "data_source": "bank",
            "unit": "CNY/month",
        },
        "other_project_feature": {"display_name": "其他项目字段"},
    }
    business_context = {
        "labels": {
            "y30": {
                "definition": "模拟首期还款违约",
                "positive_class": "违约",
                "negative_class": "履约",
                "performance_window": "放款后30天",
            },
            "y60": {"definition": "模拟第二期还款违约", "performance_window": "放款后60天"},
        },
        "sample": {
            "scope": "示例模拟申请",
            "filter": "全部8条模拟样本",
            "time_range": ["2026-01-01", "2026-02-28"],
        },
        "splits": {"train": "模拟开发样本", "oot": "模拟未来样本"},
        "currency": "CNY",
        "score_direction": "unknown",
    }
    rows = [{"feature": feature, **metadata} for feature, metadata in dictionary.items()]
    # DataFrame 必须含明确的 feature 标识列;两种后端使用同一套校验。
    pandas_dictionary = pd.DataFrame(rows)
    polars_dictionary = pl.DataFrame(rows)
    profile = profile_stats(
        df,
        features=["income", "salary"],
        metrics=["missing", "mean"],
        group_col="split",
        feature_metadata=pandas_dictionary,
        business_context=business_context,
    )
    polars_profile = profile_stats(
        df, features=["income", "salary"], metrics=["mean"], feature_metadata=polars_dictionary
    )
    assert polars_profile.get_feature("income")["metadata"]["display_name"] == "月收入"
    displayed = profile.show_overview(limit=2).data
    print(displayed.loc[:, ["feature", "display_name", "mean"]])
    run = profile_risk(
        df,
        target=["y30", "y60"],
        features=["income", "salary"],
        group_col="split",
        n_bins=2,
        feature_metadata=dictionary,
        business_context=business_context,
    )
    report = run.report
    print(report.show_summary(limit=4).data)
    candidates = report.search_features("月收入")
    assert {item["feature"] for item in candidates} == {"income", "salary"}
    identifier = report.search_features("授权流水", sources="bank")[0]["feature"]
    print(report.get_feature(identifier)["metadata"])
    print(report.get_table("calculation_status", filters={"target": "y60"}))
    persistent_id = report.report_id
    with TemporaryDirectory() as directory:
        root = Path(directory)
        report.write_html(str(root / "analysis.html"), include_charts=False)
        report.write_excel(str(root / "analysis.xlsx"))
        path = root / "analysis.marsreport"
        report.save(path)
        # 新 Python 进程只读取文件;没有原宽表、分箱器或会话。
        code = (
            "from mars.reporting import load_report; import sys; "
            "r=load_report(sys.argv[1]); "
            "assert r.report_id == sys.argv[2]; "
            "print(r.get_table('summary', features='income', limit=2))"
        )
        subprocess.run([sys.executable, "-c", code, str(path), persistent_id], check=True)
        del report, run, df
        restored = load_report(path)
        assert restored.report_id == persistent_id
        # 外部建模/诊断 Agent 直接使用公共契约,不需要 MarsAgentSession。
        catalog = restored.describe()
        print(list(catalog["tables"]))
        page = restored.query_page(
            "detail",
            features=identifier,
            filters={"y": "y30"},
            sort_by="bin_index",
            offset=0,
            limit=3,
        )
        print(page["reference"], page["total_rows"], page["next_offset"])
        context = restored.to_ai_context(
            features=identifier,
            max_chars=8000,
            queries={
                "summary": {"sort_by": "iv", "descending": True, "limit": 2},
                "detail": {
                    "filters": {"y": "y30"},
                    "columns": ["feature", "y", "bin_index", "count", "bad_rate"],
                    "limit": 3,
                },
                "calculation_status": {"filters": {"target": "y60"}, "limit": 3},
            },
        )
        assert len(context) <= 8000
        assert json.loads(context)["evidence"]
        # 内部 Agent 也是该契约的消费者;句柄与持久标识分别记录。
        if sys.version_info >= (3, 10):
            from mars.agent import MarsAgentSession, MarsRiskAgent

            original_session = MarsAgentSession()
            original_session.register_report(restored)
            del original_session
            session = MarsAgentSession()
            handle = session.register_report(restored)
            result = MarsRiskAgent().execute_tool(
                "get_report_table",
                {"report_id": handle, "table": "summary", "features": [identifier], "limit": 2},
                session=session,
            )
            assert result.success
            assert result.data["persistent_report_id"] == persistent_id
            print(handle, persistent_id)
        restored.write_html(str(root / "restored.html"))
        restored.write_excel(str(root / "restored.xlsx"))


if __name__ == "__main__":
    main()

特征元数据与业务上下文

profile_stats、MarsDataProfiler.generate_profile、profile_risk、MarsBinEvaluator.evaluate、 MarsStatsSelector.fit/fit_transform 和两个报告构造器都接受 feature_metadata、 business_context。内部 Agent 的 register_dataset 也可登记这些信息。

特征字典以原始英文字段名为稳定键,支持 display_name/description/data_source/unit 四个 可选字符串字段,缺失值允许 null。Pandas/Polars DataFrame 必须有 feature 列,其余列对应上述 字段;重复记录、非字符串值和未知字段报错。字典可覆盖整个项目,报告按实际分析特征裁剪。 重复中文名合法,检索返回全部候选及英文标识;features= 和 get_feature 仍只查询原始标识。

缺少显示名时,人类展示回退英文名,元数据中仍保留“未提供”的事实。中文名称按展示查询后的 小表附加,不重命名原始输入和统计表,也不把业务定义重复到每个日期或箱。来源旧参数在分析/ selector 中为“来源 → 特征列表”,在分箱报告构造器中为“特征 → 来源”。两者归一到元数据; 与新字典来源相同则合并,不同则报错。旧参数仍要求仅包含 active features;全项目字典请改用 feature_metadata。UNMAPPED 仅为旧统计表展示标记,不会作为已知业务来源写入元数据。

业务上下文是字符串键的 JSON 对象,支持字符串、布尔、数值、null、嵌套字典、列表和 ISO 可编码 的 date/datetime;不支持 tuple、集合、自定义对象或可执行 Python 对象。$mars 是保留键。 多标签定义使用 labels={target: {definition, positive_class, negative_class, performance_window}}; sample 描述范围/筛选/时间区间,splits 描述 train/val/test/oot,currency 和 score_direction 保存调用方解释。其他 JSON 业务字段也可保存。不提供的信息保持 unknown; 不根据字段名推测业务定义。describe().context_source 区分用户信息与实际计算参数, parameters 记录真正执行的权重、排序、分箱、基准和诊断;用户上下文不改变计算公式。 unit 是特征值单位,不是 KS/IV/PSI 的指标单位。

分页查询、证据与上下文预算

get_table 保留原生 DataFrame 类型,筛选、排序和分页后返回独立容器。支持原标识、 来源、列投影、eq/ne/lt/le/gt/ge/in/not_in/is_null/is_not_null 筛选、稳定排序、offset、limit。 日期/分组宽表范围通过 columns 选择,长明细通过 filters 选择。真实日期类型列支持 ISO-8601 字符串筛选,因此可以直接重放 JSON 证据查询;普通字符串列保持字符串类型。Pandas 索引仍保留,但不作为 查询列;需要筛选索引时先在自己的消费代码中处理。

query_page 使用相同参数,额外返回 total_rows/returned_rows/omitted_rows/truncated、 省略原因、next_offset 与 reference={report_id, table, query}。排序、投影和分页条件 可在保存恢复后重放。持久 report_id 是分析产物身份;会话登记返回的 report_id 参数实际是 会话句柄,Agent 工具另外提供 persistent_report_id/evidence_reference。外部报告如实记录 原 source,dataset_id 为 None;登记后修改原报告数据或元信息不影响已登记快照。

query_page() 默认 limit=10,显式页长只接受正整数或 None;0、负数、bool、浮点数 会抛出 ValueError。None 返回 offset 之后的全部剩余行,不再提供续页。 有续页时 next_offset 严格前进,结束时为 None。 get_table(limit=0)、get_feature(limit=0)、to_ai_context(limit=0) 及每表 queries 中的零行请求仍有效,用于取得 schema、匹配数及空证据。零行证据的 query.limit=0 保留,可通过 get_table(evidence["reference"], **evidence["query"]) 精确回放;需要继续取证时,使用省略记录 continue_at 中的正页长查询调用 query_page()。

to_ai_context(queries={table: get_table_options}, max_chars=16000) 支持不同表使用不同条件; 旧的 features/columns/filters/limit 等共同参数会适配到相同查询路径。摘要不能替代完整文件。 成员关系的 key 和 feature 可以指向同一列。原报告和保存后的快照都支持筛选、分页和生成 AI 证据, 统计行不会按成员重复展开。有限上下文只保留实际返回页涉及的成员和关系两端; 报告声明的 feature_scope 用于解释实际返回的汇总行,不附带未返回特征的字典。

未选择身份列时,evidence.identities 会保留既有身份字段,与 rows 一一对应;减少返回行数时,身份信息同步缩减。 宽趋势表只定义一次指标,日期/分组仍用原始列名标识,证据行与原表保持对应。 evidence.query 记录实际展示子集的查询。预算裁剪后,columns 和 limit 会同步缩减, 直接传回 get_table 即可重建展示的行和列。结果仍保留原筛选后的 total_rows 和省略说明。 非有限值筛选使用既有 $mars float 标记,标量与 in 列表均可 JSON 回放;畸形标签明确报错, 普通字符串不转换,NaN 相等和空判断沿用各后端现有规则。 预算按最终 JSON 的 Unicode 字符计数,包含查询和身份,不是 token 数。 超出预算时,接口依次裁剪完整时间列、行和说明块,并记录省略数量、原因及 describe / get_table 查询位置。 必要身份和引用仍超过预算时,接口会抛出 ValueError。上下文默认不包含未选择特征的项目字典。

单文件格式与恢复

.marsreport 是 ZIP,格式版本 1:manifest.json 保存持久身份、报告类型、全部表目录、 粒度、字段类型、指标定义、参数、上下文、元数据、状态/诊断及来源;tables/0000.parquet 等 文件保存每张完整统计表,清单把编号路径映射回公共表名。文件不使用 pickle,也不反序列化代码。 外部程序可以用支持 ZIP、JSON 和 Parquet 的工具读取,表顺序和列顺序会明确保留。

保存时逐表处理,不把整个报告转换为 Pandas 或大 JSON。父目录必须存在,默认拒绝覆盖已有文件。 设置 overwrite=True 时,先写入同目录的临时文件,再原子替换目标文件;失败会清理临时文件, 不改变已有完整文件。无效 ZIP、清单、缺失表、schema/顺序/行数不一致和非 1 版本明确报错; 当前没有历史版本迁移器。

清单记录各表后端及版本。跨后端版本若不能保留声明的 dtype,会明确拒绝恢复; 尚未承诺任意 Pandas/Polars 版本组合之间的 schema 迁移。格式仍可由标准 Parquet 工具读取。

恢复返回 ReportSnapshot,保留原报告类型、身份、目录语义和每张表的后端,无需原始宽表、 分箱器或会话,也不会重新计算。show_table 可查看中文名与英文标识; write_html/write_excel 导出全部公共表。原报告的 HTML/Excel 继续使用现有展示方式,并集中新增 FeatureMetadata/BusinessContext/ReportSemantics 信息。Excel 是人类展示产物, 需要无损复用统计值时使用 marsreport 文件。

Pandas 索引(包括 MultiIndex)、类别、日期和时区依赖 Arrow pandas schema 元数据保留; Polars 使用原生 Parquet 类型。清单记录规则并验证恢复 schema。表列必须是唯一字符串名, 无法编码的对象报错。Parquet 区分 null、浮点 NaN、±Infinity 和零;若原 Pandas 浮点列已经将 缺失折叠为 NaN,恢复不会伪造二者之间的区分。日期 JSON 使用 ISO-8601,date 与 datetime 依其实际 DataFrame 类型分别输出日期或完整时间戳。

状态与迁移

JSON 中的浮点非有限值统一为 {"$mars":"float","value":"nan"}、 {"$mars":"float","value":"inf"} 或 {"$mars":"float","value":"-inf"};null 表示缺失值, 零仍是数值零,普通字符串 "NaN"/"Infinity" 不转换。描述、AI 上下文、Agent 工具及文件清单 共用此规则。原消费者需停止把报告字符串标记或 Agent null 当作统一非有限数编码。

calculation_status 按目标、特征、分组和指标族保存计算状态及原因。未提供标签时为 not_computed, 标签存在但全部未表现时为 unobserved。只有一个箱、正常箱样本不足或坏率恒定时,mono 未定义, NaN/null 会保持缺失。旧代码若依赖 1.0 占位值筛选单调性,应改为先检查状态,再处理缺失。 IV、PSI 和原始/分箱 KS 公式保持现有口径;KS 为 0–100 点数,坏率为 0–1 比例, count 字段根据实际 weights_col 标明原始样本数或权重和。类别原始 KS 回退和失败仍见 ks_source_by_feature/raw_ks_diagnostics;分箱失败/跳过继续见已有 fit/diagnostics 信息。

画像的状态表只记录全量统计例外(样本不足、非数值字段跳过、未定义值及已有 PSI 失败诊断), 不生成每个趋势单元格的状态对象;缺少状态行不应代替核对实际值和诊断。 HTML/Excel 也集中导出该状态表;原报告 Excel 工作表名为 CalculationStatus。