关于 Misata: synthetic data library
Misata 是面向 Python 数据工程和测试的声明式合成数据生成库。用户描述数据必须满足的约束,系统据此生成多表数据,并用 DuckDB 独立复核、生成可重跑的证明。
主要功能
- 01用 YAML 声明数据约束
- 02生成关联的多表数据
- 03从 SQL DDL 构建数据
- 04用 DuckDB 重验不变量
- 05生成数据证明与校验脚本
- 06支持流式及分区输出
适用场景
- 01为数据管道测试构造确定性样本
- 02验证 dbt 模型的预期结果
- 03生成满足外键和账目约束的夹具
- 04为团队分享附带可验证证明的数据
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
Misata
声明必须满足的条件。生成符合条件的数据。证明结果。
Misata 是一个面向 Python 的声明式合成数据生成器。你只需写下数据集必须满足的事实,从每月收入曲线、平衡账本,到绝不允许悬空的外键;引擎便会构建同时满足所有条件的多表数据。随后,DuckDB 会检查磁盘上的文件,并生成任何人都能重新运行的证明文件。
六十秒上手
pip install "misata[duckdb]"
# misata.yaml
seed: 7
tables:
customers:
rows: 2000
columns:
customer_id: {type: int, unique: true, min: 1, max: 2000}
name: {type: text, text_type: name}
signed_up: {type: date, start: "2024-01-01", end: "2024-12-31"}
orders:
rows: 40000
columns:
order_id: {type: int, unique: true, min: 1, max: 1000000}
customer_id: {type: foreign_key}
channel: {type: categorical, choices: [web, app, store]}
amount: {type: float, min: 5, max: 900}
ordered_at: {type: date, start: "2025-01-01", end: "2025-12-31"}
relationships:
- customers.customer_id -> orders.customer_id
outcome_curves:
- table: orders
column: amount
time_column: ordered_at
start_date: "2025-01-01"
curve_points:
- {month: 1, target_value: 180000}
- {month: 6, target_value: 240000}
- {month: 12, target_value: 310000}
group_shares:
- table: orders
measure: amount
group_column: channel
shares: {web: 0.5, app: 0.3, store: 0.2}
misata generate -c misata.yaml -o data --attest
python data/verify.py
Attestation: data
result: PASSED
files pinned: 2
invariants held: 69 of 69
ok 2 file(s) match their attested sha256
ok 69 of 69 invariant(s) hold
这 69 项在生成任何行之前,就已根据 YAML 推导得出:十二个月的收入总额(一月为 180,000.00,六月为 240,000.00,十二月为 310,000.00,中间月份按直线递增)、每个月内的渠道占比、每月计划行数、没有悬空的 customer_id,以及唯一键。verify.py 只需要标准库和 duckdb,且不与生成器共享任何代码。
上方 YAML 按月份和渠道生成并汇总的结果。每个柱形代表一个月的生成收入,按渠道划分;顶端标记为 YAML 声明的总额。该图由 readme_charts.py 根据本 README 中的 YAML 重新运行生成。
只要有一行偏差一分钱,它就会指出来:
FAIL orders.csv: sha256 301165f9dc4e does not match the attested 670f89195826
FAIL orders.amount.month:1.outcome_curve_period: expected 180000.00, observed 180000.01
FAIL orders.amount.outcome_curve_total: expected 2945000.00, observed 2945000.01
FAIL orders.amount.web.2025-01.group_share_period: expected 90000.00, observed 90000.01
FAIL orders.amount.web.group_share_total: expected 1472500.00, observed 1472500.01
FAIL 65 of 69 invariant(s) hold
为什么要声明,而不是生成
每个测试夹具都有预言机问题。要检查 dbt 模型、Spark 作业、 仪表板图块或数据 Agent,你需要输入数据,也需要一个独立于被测代码而得出的已知答案。复制生产数据能提供数据,却没有答案。手写夹具能提供答案,但会逐渐失效。随机生成器能提供数据,但你必须用正在测试的代码来计算答案。
Misata 扭转了这一做法。规范是真值,行数据由规范推导而来。“1 月收入是 180,000”在任何行数据产生之前就已成立,因此你的流水线要么返回 180,000.00,要么就是有缺陷。
flowchart LR
A["Declare<br/>curves, shares, ledgers,<br/>keys, lifecycles"] --> B{"Can they<br/>all hold?"}
B -- "no" --> R["Refused, with the<br/>arithmetic and a fix"]
B -- "yes" --> C["Build rows<br/>closed form where it can,<br/>by construction elsewhere"]
C --> D["Write<br/>CSV, Parquet, JSONL,<br/>DuckDB, Postgres"]
D --> E["Attest<br/>DuckDB re-runs every<br/>derived invariant"]
E --> F["Certificate<br/>in-toto Statement<br/>plus verify.py"]
以下三个特性使它不同于带选项的生成器:
- 声明会被遵守,或被拒绝,绝不会悄悄替换。 如果声明无法同时成立,生成过程就会停止并报错,指出每项声明、 展示计算过程并建议如何修改。
- 证书由未参与数据生成的代码进行检查。 Misata 算术中的错误会表现为不变量检查失败,而不是得到一个自洽的错误答案。
- 相同的规范、种子和版本会在 Linux、macOS 和 Windows 上生成相同的字节,因此夹具可以连同其指纹一起存入 git。
未来方向
Misata 的目标是:不再有人编写 Python 脚本来生成任何类型的数据集。你只需说明需要满足什么条件;引擎会计算出各行数据,拒绝无法成立的要求,并证明它生成的内容符合要求。
目前还未实现这一目标,但正一步步接近。每个版本都会增加可声明而非编程实现的功能。只有通过三项测试,才能添上一块砖:只需几行 YAML 就能声明;与其他声明矛盾时,会在生成前拒绝;生成后,还会由未参与生成的代码对各行数据进行重新检查。
每块砖都代表一项可通过声明而非代码实现的功能。内容取自 misata/registry.py,这是契约测试用来约束代码的清单,确保墙上不会出现引擎尚不支持的砖块。
一个事实,两种实现方式。 要让事实成立,脚本必须完成什么,以及你可以改为如何声明:
| 事实 | 在生成器脚本中 | 在 Misata 中 |
|---|---|---|
| 1 月收入为 180,000.00 | 抽取金额、求和、重新缩放、四舍五入,再反复调整最后一分钱 | {month: 1, target_value: 180000} |
| 每个月的收入中,web 占一半 | 在每个月和渠道内重新缩放,然后修复刚刚破坏的月度总额 | shares: {web: 0.5, app: 0.3, store: 0.2} |
| 每笔订单都有真实客户 | 从父表中抽取 ID,并确保两边保持同步 | customers.customer_id -> orders.customer_id |
| 一些流失客户此前曾处于活跃状态 | 按状态分支、按顺序写入时间戳、其余置空,再手动计数 | transitions: [[trialing, active], [active, churned]] |
| 1% 的转账构成洗钱模式,且模式比例固定 | 手动构建每个环路和扇形结构,再证明背景数据自身不存在环路 | rate: 0.01 和 shares: {cycle: 0.4, fan_in: 0.25, ...} |
| 试算平衡表的余额为零 | 为每笔分录记录借贷两方,并在最后进行核对 | 对日记账分录应用 balanced_ledger 约束 |
仍需编写代码的部分及其当前状态:
| 仍需手动编写脚本 | 当前状态 |
|---|---|
| Misata 无法反推的逻辑预期输出:窗口函数、自定义 SQL、业务规则 | misata solve 会运行你真实的 dbt、SQL 或 Python 流水线,使结果达到声明的数值及指定容差;超出该范围后,预期值需要由你编写 |
| 只能通过示例描述的相关性 | misata.mimic 会从真实数据行中拟合相关性,生成结果并非匿名数据 |
| 带有独特语气的自由文本 | 文本由语法生成,并与所在数据行一致;目前还无法声明读起来像人工撰写的文本 |
| 版本历史(SCD2)和库存变化轨迹 | 可生成满足平衡要求的数据,但尚不能逐步声明 |
| 文档、图像和音频 | 尚未开始 |
LIMITATIONS.md 列出了完整清单。当某项内容从此表中移除时,意味着取代它的砖块已经通过了全部三项测试。
证据
The Gauntlet:13 张表和 136 项 SQL 断言,由 DuckDB 执行。Faker 脚本只要事实涉及单列就能打平;凡是列、行或表之间必须保持一致的地方,它就会落败。源代码,可用 python -m benchmarks.gauntlet 重新运行。
梯度提升分类器尝试区分生成行与真实行;取五个随机种子的均值。在未查看任何真实行的情况下,声明式架构的表现优于使用真实订单训练的 SDV。在出租车行程数据上,SDV 胜出:盲猜车费的表现不佳。源代码,数字见 summary.json。
| 声明 | 测量结果 | 重现方式 |
|---|---|---|
| 按构造保证有效 | 在五种 DDL 架构上,键、CHECK、UNIQUE、NOT NULL 和自引用树均无违规。逐列生成的 Faker 脚本:每种架构有 15,014 至 50,416 项违规。使用有效数据训练的 SDV 多表模型:有 3,055 至 5,557 项违规,且拒绝处理五种架构中的两种 | python -m benchmarks.validity_bench |
| 符合性 | 三个测试套件共包含 294 项 SQL 断言,全部通过:The Gauntlet 136 项、The Warren 110 项、The Ledger 48 项 | python -m benchmarks.gauntlet |
| 可复现 | 在 Linux、macOS 和 Windows 上生成相同的指纹 | python -m benchmarks.golden |
| 规模 | 在 Apple M1 上以 0.78 GB 的峰值内存,在 36.3 秒内生成分布于 14 张表的 10,000,000 行数据,之后检查每个外键 | python benchmarks/bench_r3_volume.py --scale 10 --stream out/ |
可以声明的内容
每项声明都有可测试的保证、所属列列表、成本和拒绝条件。LANGUAGE.md 是参考文档。所有 25 种声明类型都会在行生成前进行可行性检查,并在生成行后再次检查;标有 ✓ 的类型也包含在 DuckDB 证书中。
| 声明 | 保证内容 | 证书 |
|---|---|---|
outcome_curves | 每个期间内某项指标的总量,精确到分 | ✓ |
rate_curves | 每个期间内标记行所占的比例,精确到行 | ✓ |
group_shares | 每个期间内各组在某项指标中的占比 | ✓ |
balanced_ledger | 每笔分录的借方等于贷方,且试算平衡为零 | ✓ |
waterfalls | 每个组成部分的期初值加变动等于期末值 | ✓ |
relationships、min_children、partition_by | 无孤儿记录、父记录至少有一个子记录、键不会跨越租户 | ✓ |
unique、inequality、col_range、choices、NOT NULL、DDL CHECK | 没有行违反规则 | ✓ |
lifecycles | 状态意味着完整的历史记录:应有的时间戳齐全且顺序正确,其余时间戳为空 | ✓ |
dag_edges、graph_motifs | 除预设案例外没有环;每种环、扇形和链的数量都精确 | ✓ |
汇总、stock_flows、scd2、bitemporal、when_then、lte_parent | 父项等于子项、库存保持平衡、各版本完整覆盖时间线 | |
outliers、typos、duplicates、missingness、late_arrivals | 脏数据数量精确,并附有答案 | |
retention、joint_distributions、time_grids、event_logs、processes | 队列曲线、联合边际分布、声明的时钟,以及与状态一致的日志 |
无论声明什么,外键都会通过构造方式解析(先构建父记录,再构建子记录);文本与所在行相符,金额呈偏态,时间戳具有每日节律,且少数客户会下大多数订单,因此即使不做任何声明,数据看起来也像真实业务数据。输入一句话即可选择 18 种故事领域之一,从 SaaS 和金融科技到医疗保健、物流和流媒体;每种领域都有自己的表、键和校准分布。
选择入口
| 你有 | 调用方式 | 备注 |
|---|---|---|
| 一句话 | misata.generate("A SaaS company with 1,000 customers", seed=42) | 最快上手;打印表,或运行 misata explain 查看生成内容 |
| YAML 或字典 | misata.generate_from_schema(schema) | 所有声明都必须成立,否则会拒绝 |
| SQL DDL | misata.from_ddl(ddl) 或 misata generate -c schema.sql | 支持 NOT NULL、UNIQUE、复合键、CHECK 范围和枚举、自引用 |
| 正在运行的数据库 | misata seed postgresql://... | 会检查数据库结构并在一个事务中插入数据;除非传入 --truncate,否则绝不清空表 |
| dbt 项目 | misata dbt-unit-test --select my_model | 从键相符的 manifest.json 生成测试数据 |
| Prisma 或 Django | misata prisma-seed、misata.from_django(app_labels=[...]) | |
| 超出内存容量的行数 | misata.generate_stream(schema) | 生成 (table, batch);按分区输出 Parquet,每个分区一个文件 |
| 输出必须达到指定数值的流水线 | misata solve -c solve.yaml | 将输入送入真实的 dbt 或 Python 流水线,达到声明的结果并满足指定容差 |
| 要分享的数据集 | misata publish data --hf you/name | 重新验证后,连同数据规范、证书和验证器一起上传 |
| 可供模仿的真实行 | misata.mimic(df) | 基于真实行拟合;并非匿名数据 |
每次都传入种子。同一种子会生成相同的数据。
十二个配方
每个配方都是一个文件夹,包含模式、命令,以及证明声明成立的检查。每次发布前,tools/stranger.py --clean 都会在全新的虚拟环境中,从刚构建的 wheel 运行每个页面上的所有命令,因此页面上的数字就是你实际会看到的结果。
| 配方 | 适用对象 | 验证结果 |
|---|---|---|
| 为开发数据库填充种子数据 | 应用开发者、编码 Agent | 根据三个 CREATE TABLE 语句生成 22 条不变量 |
| dbt 的已知答案测试 | 分析工程师 | 能捕捉月末最后一天错误的 dbt 测试 |
| 用户到来前准备好数据仓库 | 创始人、数据负责人 | 291,000 行分区 Parquet 数据,80 条不变量 |
| 演示租户 | 销售工程师、产品经理 | 已流失和健康的账户,以及少数巨鲸账户;逼真度 0.96 |
| 评测数据库 | AI 实验室、评测团队 | 在三个轮换使用的数据库上验证文本转 SQL 的答案 |
| Agent 沙箱 | Agent 构建者 | 沙箱与评分器逐行一致 |
| 负载测试 | 平台工程师 | 热点键和每日曲线;重放前检查键 |
| 欺诈与 AML | 风险与 ML 团队 | 无环背景数据,且每个关系环都符合声明的数量 |
| 可对账的分类账 | 财务工程、审计 | 两个财年、52 条不变量,试算平衡为零 |
| 带回家的练习 | 招聘团队、课程作者 | 每位候选人都有不同的答案密钥,且均经过验证 |
| 不含 PII 的环境 | 受监管团队 | 根据生产 DDL 生成暂存数据,绝不使用生产行 |
| 文档示例数据 | DX、API 和 SDK 团队 | 嵌套 JSONL,ID 可跨文件解析 |
状态蕴含其历史
只需声明一次状态机,每一行的时间戳都会根据其状态生成。摘自演示租户:
stateDiagram-v2
direction LR
[*] --> trialing
trialing --> active
trialing --> churned
active --> past_due
active --> churned
past_due --> churned
lifecycles:
- name: subscription
table: subscriptions
state_column: status
start_column: started_at
initial: trialing
states:
- {name: trialing}
- {name: active, timestamp: activated_at}
- {name: past_due, timestamp: past_due_at}
- {name: churned, timestamp: churned_at, terminal: true}
transitions:
- [trialing, active]
- [trialing, churned]
- [active, past_due]
- [active, churned]
- [past_due, churned]
weights: {trialing: 0.07, active: 0.6, past_due: 0.07, churned: 0.26}
1,200 个订阅中,恰有 720 个处于活跃状态、312 个已流失、84 个逾期,另有 84 个仍在试用期:权重表示数量,按最大余数法分配,而非概率。每一条逾期记录都有激活日期和逾期日期,顺序如此,且都晚于开始日期;活跃记录没有流失日期。证书会逐行检查这两点。一个状态若有多条可达路径,其记录会分布到每条路径上:312 个已流失订阅中,156 个在试用期间离开,78 个在活跃后离开,78 个在逾期后离开。
演示租户的 1,200 条订阅,按已设置的时间戳分组。六条路径,没有一条记录不符合路径。由 readme_charts.py 根据配方自身的 YAML 绘制。
当它拒绝时
$ misata explain bad.yaml
Generation would refuse: 1 conflict(s).
1 declaration cannot be satisfied together:
1. [orders.channel] group_shares on amount
declared shares sum to 1.500 (web=0.6, app=0.6, store=0.3); a share of a whole cannot exceed 1.0
→ Suggestion: scale the shares so they sum to 1.0, or drop a group. Misata will not renormalise them for you, because the result would be a specification you did not write
Nothing was generated. Change the declarations each conflict names; its remedy says how.
misata explain 会打印计划生成的行、每项声明将写入的内容、证书将包含多少条不变量,以及所有冲突,且不会生成任何内容。每条命令都支持 --json,并打印一个封装对象;退出代码 3 表示这些声明无法同时成立,且没有写入任何内容。
与编码 Agent 协作
Agent 设计架构,Misata 负责计算,DuckDB 负责检查。Agent 无法为自己的输出认证;这种方式能让它得到一个并非由它自己编写的答案。
claude mcp add misata -- uvx --from "misata[mcp]" misata-mcp # Claude Code
misata mcp install --client all # Cursor, Windsurf, Claude Desktop
共有 15 个工具:generate_from_schema、generate_dataset、explain_schema、validate_yaml、inspect_schema、preview_story、list_domains、validate_domain、seed_database、create_sandbox、query_sandbox、solve_outcome、attest_dataset、audit_dataset 和 check_realism。
AGENTS.md 会告诉 Agent 哪个入口适合哪项任务,以及证据支持哪些说法。
它不是什么
除非你提出要求,否则 Misata 不会模仿任何内容。它不会从你的数据中学习联合分布。如果你需要特定私有表的统计孪生数据,请使用明确声明 epsilon 值的差分隐私合成器。mimic 会拟合真实行,因此并非匿名。
对于可声明的内容(总和、比率、占比、汇总、分类账、键、生命周期),它是精确的;对于任意 SQL,则并非如此。即使模型依赖窗口函数或 Misata 无法反演的业务逻辑,生成的数据仍然连贯且键有效,而预期输出由你编写。misata solve 通过将输入送入你的真实流水线,驱动其达到声明结果,并满足指定容差,从而弥补部分差距;证书会说明该容差。
先验设定以英语为主,金额偏向美国惯例,且比率会四舍五入到整数行数。LIMITATIONS.md 列出了完整限制;若与其他文档有争议,以此为准。
| 命令式 | 推断式 | 声明式 | |
|---|---|---|---|
| 你提供 | 生成值的代码 | 用于拟合的真实数据 | 必须成立的事实 |
| 保证 | 事后不作保证 | 大致相似 | 事实精确成立,否则拒绝 |
| 外键 | 由你负责 | 单表内没问题,跨表则不可靠 | 构造时保证 |
| 需要真实数据 | 否 | 是 | 否 |
| 示例 | Faker、Mockaroo、LLM 种子脚本 | SDV、学习型合成器 | Misata |
引用
如果 Misata 对你的工作有所帮助,请引用该软件。GitHub 的 引用此仓库按钮会读取 CITATION.cff 中的相同元数据。
@software{rasin_misata,
title = {Misata: declarative synthetic data},
author = {Rasin, Muhammed},
url = {https://github.com/rasinmuhammed/misata}
}
参与贡献
git clone https://github.com/rasinmuhammed/misata && cd misata
uv sync --all-extras && uv run pytest -q
python tools/stranger.py --clean # every documented command, from the built wheel
只有能够声明并验证的功能才值得纳入。benchmarks/ 中的一致性测试套件就是约定:先编写断言,看着它失败,然后让它通过。采用 MIT 许可证。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。