scmgen 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. scmgen-0.10.0/PKG-INFO +155 -0
  2. scmgen-0.10.0/README.md +138 -0
  3. scmgen-0.10.0/pyproject.toml +33 -0
  4. scmgen-0.10.0/setup.cfg +4 -0
  5. scmgen-0.10.0/src/scmgen/__init__.py +39 -0
  6. scmgen-0.10.0/src/scmgen/betafn.py +158 -0
  7. scmgen-0.10.0/src/scmgen/config.py +768 -0
  8. scmgen-0.10.0/src/scmgen/diagnostics.py +45 -0
  9. scmgen-0.10.0/src/scmgen/generate.py +608 -0
  10. scmgen-0.10.0/src/scmgen/graph.py +700 -0
  11. scmgen-0.10.0/src/scmgen/io.py +474 -0
  12. scmgen-0.10.0/src/scmgen/mechanisms.py +419 -0
  13. scmgen-0.10.0/src/scmgen/outcomes.py +259 -0
  14. scmgen-0.10.0/src/scmgen/pathologies.py +1108 -0
  15. scmgen-0.10.0/src/scmgen/reference.py +57 -0
  16. scmgen-0.10.0/src/scmgen/scenarios/__init__.py +38 -0
  17. scmgen-0.10.0/src/scmgen/scenarios/ctl-binary-rd.yaml +76 -0
  18. scmgen-0.10.0/src/scmgen/scenarios/ctl-const-neg.yaml +76 -0
  19. scmgen-0.10.0/src/scmgen/scenarios/ctl-const-pos.yaml +76 -0
  20. scmgen-0.10.0/src/scmgen/scenarios/ctl-linear-cate.yaml +76 -0
  21. scmgen-0.10.0/src/scmgen/scenarios/ctl-null.yaml +76 -0
  22. scmgen-0.10.0/src/scmgen/scenarios/obs-cluster.yaml +83 -0
  23. scmgen-0.10.0/src/scmgen/scenarios/obs-confounding.yaml +76 -0
  24. scmgen-0.10.0/src/scmgen/scenarios/obs-hidden-conf.yaml +82 -0
  25. scmgen-0.10.0/src/scmgen/scenarios/obs-noise-conf.yaml +84 -0
  26. scmgen-0.10.0/src/scmgen/scenarios/obs-overlap.yaml +82 -0
  27. scmgen-0.10.0/src/scmgen/scenarios/obs-targeted.yaml +82 -0
  28. scmgen-0.10.0/src/scmgen/scenarios/rct-bad-controls.yaml +82 -0
  29. scmgen-0.10.0/src/scmgen/scenarios/rct-beta-clean.yaml +76 -0
  30. scmgen-0.10.0/src/scmgen/scenarios/rct-beta-het-seg.yaml +76 -0
  31. scmgen-0.10.0/src/scmgen/scenarios/rct-beta-hetero-seg.yaml +82 -0
  32. scmgen-0.10.0/src/scmgen/scenarios/rct-beta-indep.yaml +76 -0
  33. scmgen-0.10.0/src/scmgen/scenarios/rct-beta-zoi-both.yaml +76 -0
  34. scmgen-0.10.0/src/scmgen/scenarios/rct-beta-zoi.yaml +76 -0
  35. scmgen-0.10.0/src/scmgen/scenarios/rct-clean-disc.yaml +79 -0
  36. scmgen-0.10.0/src/scmgen/scenarios/rct-clean.yaml +76 -0
  37. scmgen-0.10.0/src/scmgen/scenarios/rct-cluster-assign.yaml +83 -0
  38. scmgen-0.10.0/src/scmgen/scenarios/rct-cluster.yaml +82 -0
  39. scmgen-0.10.0/src/scmgen/scenarios/rct-covariate-shift.yaml +82 -0
  40. scmgen-0.10.0/src/scmgen/scenarios/rct-drift-seg.yaml +82 -0
  41. scmgen-0.10.0/src/scmgen/scenarios/rct-drift-tau.yaml +95 -0
  42. scmgen-0.10.0/src/scmgen/scenarios/rct-drift.yaml +82 -0
  43. scmgen-0.10.0/src/scmgen/scenarios/rct-dup-rows.yaml +82 -0
  44. scmgen-0.10.0/src/scmgen/scenarios/rct-fewplacebo-nl.yaml +76 -0
  45. scmgen-0.10.0/src/scmgen/scenarios/rct-fewplacebo.yaml +76 -0
  46. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-blocks-nl.yaml +76 -0
  47. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-blocks.yaml +76 -0
  48. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-clones-nl.yaml +76 -0
  49. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-clones.yaml +76 -0
  50. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-deep.yaml +76 -0
  51. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-dense.yaml +76 -0
  52. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-layered.yaml +76 -0
  53. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-sf-disc.yaml +79 -0
  54. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-sf-nl.yaml +76 -0
  55. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-sf.yaml +76 -0
  56. scmgen-0.10.0/src/scmgen/scenarios/rct-graph-sparse.yaml +76 -0
  57. scmgen-0.10.0/src/scmgen/scenarios/rct-het-seg-disc.yaml +78 -0
  58. scmgen-0.10.0/src/scmgen/scenarios/rct-het-seg.yaml +82 -0
  59. scmgen-0.10.0/src/scmgen/scenarios/rct-hidden-modifier.yaml +82 -0
  60. scmgen-0.10.0/src/scmgen/scenarios/rct-indep.yaml +76 -0
  61. scmgen-0.10.0/src/scmgen/scenarios/rct-mediation-binary.yaml +76 -0
  62. scmgen-0.10.0/src/scmgen/scenarios/rct-mediation-customer.yaml +186 -0
  63. scmgen-0.10.0/src/scmgen/scenarios/rct-mediation-partial.yaml +76 -0
  64. scmgen-0.10.0/src/scmgen/scenarios/rct-missing-mar.yaml +83 -0
  65. scmgen-0.10.0/src/scmgen/scenarios/rct-missing-seg.yaml +82 -0
  66. scmgen-0.10.0/src/scmgen/scenarios/rct-missing-sentinel.yaml +83 -0
  67. scmgen-0.10.0/src/scmgen/scenarios/rct-missing-ty.yaml +86 -0
  68. scmgen-0.10.0/src/scmgen/scenarios/rct-money-het.yaml +76 -0
  69. scmgen-0.10.0/src/scmgen/scenarios/rct-money-null.yaml +82 -0
  70. scmgen-0.10.0/src/scmgen/scenarios/rct-nl-clean.yaml +76 -0
  71. scmgen-0.10.0/src/scmgen/scenarios/rct-nl-het-seg.yaml +82 -0
  72. scmgen-0.10.0/src/scmgen/scenarios/rct-nl-noise-y-seg.yaml +82 -0
  73. scmgen-0.10.0/src/scmgen/scenarios/rct-nl-zeroinfl.yaml +76 -0
  74. scmgen-0.10.0/src/scmgen/scenarios/rct-noise-y-seg.yaml +82 -0
  75. scmgen-0.10.0/src/scmgen/scenarios/rct-noisey-beta.yaml +82 -0
  76. scmgen-0.10.0/src/scmgen/scenarios/rct-noisey-bin.yaml +82 -0
  77. scmgen-0.10.0/src/scmgen/scenarios/rct-noisey-diff-y.yaml +85 -0
  78. scmgen-0.10.0/src/scmgen/scenarios/rct-noisey-diff.yaml +85 -0
  79. scmgen-0.10.0/src/scmgen/scenarios/rct-noisey-ziln.yaml +82 -0
  80. scmgen-0.10.0/src/scmgen/scenarios/rct-noncompliance.yaml +82 -0
  81. scmgen-0.10.0/src/scmgen/scenarios/rct-outliers.yaml +82 -0
  82. scmgen-0.10.0/src/scmgen/scenarios/rct-quantized.yaml +84 -0
  83. scmgen-0.10.0/src/scmgen/scenarios/rct-select-seg-t.yaml +83 -0
  84. scmgen-0.10.0/src/scmgen/scenarios/rct-select-seg.yaml +82 -0
  85. scmgen-0.10.0/src/scmgen/scenarios/rct-t-noise.yaml +82 -0
  86. scmgen-0.10.0/src/scmgen/scenarios/rct-zeroinfl-smalltau.yaml +76 -0
  87. scmgen-0.10.0/src/scmgen/scm.py +577 -0
  88. scmgen-0.10.0/src/scmgen/segments.py +144 -0
  89. scmgen-0.10.0/src/scmgen/truth.py +1042 -0
  90. scmgen-0.10.0/src/scmgen.egg-info/PKG-INFO +155 -0
  91. scmgen-0.10.0/src/scmgen.egg-info/SOURCES.txt +112 -0
  92. scmgen-0.10.0/src/scmgen.egg-info/dependency_links.txt +1 -0
  93. scmgen-0.10.0/src/scmgen.egg-info/requires.txt +10 -0
  94. scmgen-0.10.0/src/scmgen.egg-info/top_level.txt +1 -0
  95. scmgen-0.10.0/tests/test_acceptance.py +108 -0
  96. scmgen-0.10.0/tests/test_airgapped.py +43 -0
  97. scmgen-0.10.0/tests/test_audit_2026_09_19.py +576 -0
  98. scmgen-0.10.0/tests/test_audit_2026_09_19_1.py +214 -0
  99. scmgen-0.10.0/tests/test_audit_2026_09_19_2.py +299 -0
  100. scmgen-0.10.0/tests/test_audit_2026_09_20.py +223 -0
  101. scmgen-0.10.0/tests/test_discrete_v040.py +271 -0
  102. scmgen-0.10.0/tests/test_golden.py +68 -0
  103. scmgen-0.10.0/tests/test_graph_contract.py +54 -0
  104. scmgen-0.10.0/tests/test_graph_structure.py +123 -0
  105. scmgen-0.10.0/tests/test_graph_v030.py +212 -0
  106. scmgen-0.10.0/tests/test_pathologies.py +165 -0
  107. scmgen-0.10.0/tests/test_pathologies_ext.py +248 -0
  108. scmgen-0.10.0/tests/test_requests_v020.py +136 -0
  109. scmgen-0.10.0/tests/test_scmgen.py +246 -0
  110. scmgen-0.10.0/tests/test_v0100.py +352 -0
  111. scmgen-0.10.0/tests/test_v060.py +610 -0
  112. scmgen-0.10.0/tests/test_v070.py +684 -0
  113. scmgen-0.10.0/tests/test_v080.py +497 -0
  114. scmgen-0.10.0/tests/test_v090.py +342 -0
scmgen-0.10.0/PKG-INFO ADDED
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: scmgen
3
+ Version: 0.10.0
4
+ Summary: SCM-генератор синтетических данных для uplift/ITE/CATE-бенчмарков: явный DAG, полный ground truth, оси патологий данных
5
+ Author-email: DK <dk.promsoft@gmail.com>
6
+ Project-URL: Homepage, https://github.com/Causal-Fin-AI/scmgen
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: numpy>=2
10
+ Requires-Dist: pandas>=2
11
+ Requires-Dist: pyyaml>=6
12
+ Provides-Extra: io
13
+ Requires-Dist: pyarrow>=15; extra == "io"
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8; extra == "dev"
16
+ Requires-Dist: pyarrow>=15; extra == "dev"
17
+
18
+ # scmgen — SCM-генератор синтетических данных для uplift/ITE-бенчмарков
19
+
20
+ Генератор порождает:
21
+
22
+ * **SCM с явным DAG над переменными** (ковариаты X, воздействие T, отклик Y) — структура графа и роли узлов
23
+ являются параметрами экспериментального дизайна;
24
+ * выборку из него любого размера, вложенную по n;
25
+ * **полный ground truth** — потенциальные исходы, ITE / CATE / ATE и их дисперсии, граф, разметку сегментов и
26
+ применённых патологий данных.
27
+
28
+ Режимы назначения воздействия: **rct** (e(x) = const, дефолт) и **obs** (e(x) зависит от конфаундеров).
29
+ Семейства исхода: gaussian (с природным масштабом «размера клиента»), binary, zero-inflated lognormal, beta
30
+ (доля с массами в 0 и 1). Модель отделена от выборки: `sample_scm(config, model_seed)` строит объект `SCM`
31
+ (граф, коэффициенты, константы калибровки), `scm.sample(n, sample_seed)` — выборку любого n из той же задачи,
32
+ `generate(config, n, seed)` — обёртка с одним seed. Воздействие бинарное.
33
+
34
+ Публичная копия репозитория — https://github.com/Causal-Fin-AI/scmgen. Документация — папка `docs/`
35
+ (`docs/README.md` — оглавление): порождающая модель, контракт полей истины, формат датасета,
36
+ воспроизводимость и эталоны, параметры конфига, реестр патологий, библиотека сценариев; история версий —
37
+ `CHANGELOG.md`.
38
+
39
+ ## Установка
40
+
41
+ Python ≥ 3.12. Зависимости ядра — `numpy`, `pandas`, `pyyaml`; parquet-сериализация требует `pyarrow`
42
+ (extra `io`).
43
+
44
+ ```bash
45
+ pip install scmgen
46
+ # с поддержкой parquet
47
+ pip install "scmgen[io]"
48
+ # для разработки
49
+ git clone https://github.com/Causal-Fin-AI/scmgen.git && cd scmgen
50
+ pip install -e ".[dev]" && OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 pytest
51
+ ```
52
+
53
+ ## Быстрый старт
54
+
55
+ ```python
56
+ from scmgen import scenarios, generate, generate_pair, generate_split, save_dataset, load_dataset, replay
57
+
58
+ scenarios.list_scenarios() # 70 сценариев библиотеки (docs/scenarios.md)
59
+ cfg = scenarios.load("rct-noise-y-seg") # ScenarioConfig; можно править поля
60
+ ds = generate(cfg, n=100_000, seed=0) # Dataset (model_seed = sample_seed = pathology_seed = 0)
61
+
62
+ ds.x, ds.t, ds.y # наблюдаемые данные (X — DataFrame)
63
+ ds.y0, ds.y1, ds.ite, ds.tau, ds.e # ground truth per row: потенциальные исходы, ite = y1 - y0,
64
+ ds.tau_cate, ds.cvte, ds.vte_w, ds.segments # oracle tau на латентах; CATE по W: E[tau|W], Var[tau|W], Var[ite|W]; сегменты
65
+ ds.graph.adjacency, ds.graph.roles # DAG (T -> Y всегда; в RCT без X -> T) и роли узлов
66
+ ds.dag() # post-intervention DAG по аргументам осей (z, compliance, u_tau_i, x3_obs, ...)
67
+ ds.pathology_log # что и куда применено: номер P-n, фаза, active, affected_columns
68
+ ds.ground_truth()["targets"], ds.design() # контракт полей истины (conditioning, population); дизайн, cate_dgp
69
+ ds.recipe() # рецепт: модель + выборка + применённые оси (replay воспроизводит бит в бит)
70
+
71
+ clean, broken = generate_pair(cfg, n=100_000, seed=0) # парные выборки: с патологиями и без
72
+ clean, dirty, test = generate_split(cfg, n_train=100_000, n_test=20_000, seed=0) # + чистый test той же SCM (test_dgp="none")
73
+ clean, dirty, test = generate_split(cfg, 100_000, 20_000, 0, test_dgp="replay") # test с теми же DGP-осями (drift: period ≡ 1)
74
+ save_dataset(ds, "out/rct-noise-y-seg-0") # out/CURRENT + out/v-<utc>-<rand>/{manifest,observed,ground_truth,meta,scm}
75
+ observed, gt, meta = load_dataset("out/rct-noise-y-seg-0", verify="rows") # "hashes" — ещё и SHA-256 файлов
76
+ ds2 = replay("out/rct-noise-y-seg-0") # тот же датасет по рецепту (в reference environment — бит в бит)
77
+ ```
78
+
79
+ Модель отдельно от выборки — learning curves по n на одной задаче и общие случайные числа:
80
+
81
+ ```python
82
+ from scmgen import sample_scm, apply_pathologies, load_scm
83
+
84
+ scm = sample_scm(cfg, model_seed=0) # граф, коэффициенты, константы калибровки (n_calib = 200 000)
85
+ small = scm.sample(1_000, sample_seed=7) # чистые выборки одной SCM: small — первые 1000 строк big
86
+ big = scm.sample(50_000, sample_seed=7) # (бит в бит по X, T, Y и всем полям истины)
87
+ dirty = apply_pathologies(big, pathology_seed=3)
88
+ scm.tau(x), scm.mu0(x), scm.e_assign(x) # уравнения на произвольных X
89
+ scm2 = load_scm("out/rct-noise-y-seg-0") # scm.json -> та же модель; .sample + apply_pathologies = датасет бит в бит
90
+ ```
91
+
92
+ Свой сценарий — через конфиг в коде или YAML:
93
+
94
+ ```python
95
+ from scmgen import ScenarioConfig, GraphConfig, MediatorConfig, PathologyConfig
96
+ cfg = ScenarioConfig(
97
+ name="my-obs",
98
+ graph=GraphConfig(n_confounders=5, n_modifiers=3, n_predictors=5, n_noise=5,
99
+ xx_density=0.1, xx_structure="scale_free", sf_gamma=2.2),
100
+ pathologies=[PathologyConfig(kind="noise_y", strength=0.8, target="segments", segment_share=0.25)],
101
+ )
102
+ cfg.treatment.mode = "obs"
103
+ cfg.to_yaml("my-obs.yaml"); cfg = ScenarioConfig.from_yaml("my-obs.yaml")
104
+ # медиация как у заказчика: без прямого T -> Y, бинарный медиатор с неоднородными стадиями
105
+ med = ScenarioConfig(name="my-mediation", graph=GraphConfig(n_predictors=4, n_med_confounders=2, n_upstream=3, n_side=1,
106
+ n_mediators=1, direct_effect=False, xx_density=0.5),
107
+ mediator=MediatorConfig(family="binary", base_rate=0.08, effect=0.55, effect_amplitude=0.4,
108
+ theta=1.2, theta_amplitude=0.4))
109
+ med.tau.amplitude = 0.0
110
+ ```
111
+
112
+ ## Что умеет генератор
113
+
114
+ - **Граф** (`docs/model.md` §1): роли узлов — конфаундеры, модификаторы, предикторы, шум, медиаторы,
115
+ инструменты, коллайдеры, конфаундеры и предикторы медиатора, узлы `upstream` / `side` без прямых рёбер к
116
+ T / M / Y; прямое ребро T → Y опционально; структуры X–X `er` | `scale_free` | `blocks` | `layered` |
117
+ `copying` и их композиции; дискретные признаки (бинарные и категориальные, one-hot по запросу); ручной
118
+ X–X-блок и ручные роли по позициям (любой граф ролевой модели задаётся точно); граф отражает исполняемые
119
+ уравнения, post-intervention DAG по применённым осям; контракт графа для внешних читателей (G-6).
120
+ - **Объект SCM** (`docs/model.md` §2): истина не зависит от n — константы калибровки на 200 000 строк; три
121
+ потока seed (модель, выборка, патологии); вложенные выборки бит в бит; неизменяемый SCM; арифметика без
122
+ BLAS; test-популяция той же SCM с двумя задачами оценки (`test_dgp = none | replay`).
123
+ - **Механизмы и исходы** (`docs/model.md` §3): линейные и нелинейные уравнения, CATE отдельно от μ₀
124
+ (амплитуда, sparsity, форма, сдвиг), SNR вплоть до «крошечного эффекта», четыре семейства исхода, связь
125
+ потенциальных исходов `shared | independent`, природный масштаб исхода для денежных откликов, медиаторы
126
+ гауссовы или бинарные («продажа»: неоднородные эффекты T → M и M → Y, §3.6).
127
+ - **Оси патологий** (`docs/model.md` §4, `docs/pathology-catalog.md`): 19 осей в трёх фазах (DGP → отбор →
128
+ измерение), сила 0..1, локализация глобально или по сегментам, свой поток rng у каждой оси, `strength = 0`
129
+ — no-op; ground truth остаётся чистым, DGP-оси пересчитывают его на том же шуме.
130
+ - **Истина** (`docs/truth-contract.md`): `tau_cate`, `cvte`, `vte_w`, поля non-compliance и отбора
131
+ (`tau_itt*`, `*_selected`), поля бинарных медиаторов (`p_m0`, `p_m1`, `tau_m`, `theta`), блок `targets` с условными наборами и способом интегрирования каждого поля,
132
+ таблица осей, меняющих функцию CATE(W).
133
+ - **Выход** (`docs/dataset-format.md`): версионная атомарная публикация (`CURRENT` + версии), manifest с
134
+ хешами файлов и отпечатком окружения, `meta.json` с конфигом, `targets`, `design`, `recipe`,
135
+ `pathology_log`; `scm.json` — model artifact; `replay` по рецепту.
136
+ - **Воспроизводимость** (`docs/reproducibility.md`): битовая в reference environment (включая
137
+ reference-equivalent режим для AVX512), численная (rtol 1e-9) в любом окружении; два уровня эталона по всей
138
+ библиотеке; инструменты локализации расхождений между машинами.
139
+ - **Библиотека** (`docs/scenarios.md`): 70 именованных сценариев — база и патологии, расширенный пакет,
140
+ структуры графа, дискретные признаки, контрольные аналитические, кластеры и дубли, beta и связь исходов,
141
+ денежный исход с выбросами, медиация как у заказчика (в том числе граф примера заказчика целиком).
142
+
143
+ ## Проверки
144
+
145
+ 957 тестов (`pytest`, около 13 минут в reference environment: каждый уникальный конфиг калибрует SCM на
146
+ 200 000 строк) и два уровня эталона (`tests/golden/`, `scripts/scmgen_golden.py`) — подробно в
147
+ `docs/reproducibility.md`. Изменение поведения генератора = новая версия + новый эталон + запись в
148
+ `CHANGELOG.md`.
149
+
150
+ ## Происхождение
151
+
152
+ Пакет разработан в рамках НИР «Оценка индивидуальных эффектов на финансовых данных» (2026) как синтетический
153
+ слой бенчмарка ITE/uplift-методов и выделен из внутреннего репозитория НИР `uplift_methods` (см.
154
+ `CHANGELOG.md`). Обзоры литературы и результаты бенчмарка остаются в исходном репозитории; каталог патологий
155
+ с источниками — `docs/pathology-catalog.md`.
@@ -0,0 +1,138 @@
1
+ # scmgen — SCM-генератор синтетических данных для uplift/ITE-бенчмарков
2
+
3
+ Генератор порождает:
4
+
5
+ * **SCM с явным DAG над переменными** (ковариаты X, воздействие T, отклик Y) — структура графа и роли узлов
6
+ являются параметрами экспериментального дизайна;
7
+ * выборку из него любого размера, вложенную по n;
8
+ * **полный ground truth** — потенциальные исходы, ITE / CATE / ATE и их дисперсии, граф, разметку сегментов и
9
+ применённых патологий данных.
10
+
11
+ Режимы назначения воздействия: **rct** (e(x) = const, дефолт) и **obs** (e(x) зависит от конфаундеров).
12
+ Семейства исхода: gaussian (с природным масштабом «размера клиента»), binary, zero-inflated lognormal, beta
13
+ (доля с массами в 0 и 1). Модель отделена от выборки: `sample_scm(config, model_seed)` строит объект `SCM`
14
+ (граф, коэффициенты, константы калибровки), `scm.sample(n, sample_seed)` — выборку любого n из той же задачи,
15
+ `generate(config, n, seed)` — обёртка с одним seed. Воздействие бинарное.
16
+
17
+ Публичная копия репозитория — https://github.com/Causal-Fin-AI/scmgen. Документация — папка `docs/`
18
+ (`docs/README.md` — оглавление): порождающая модель, контракт полей истины, формат датасета,
19
+ воспроизводимость и эталоны, параметры конфига, реестр патологий, библиотека сценариев; история версий —
20
+ `CHANGELOG.md`.
21
+
22
+ ## Установка
23
+
24
+ Python ≥ 3.12. Зависимости ядра — `numpy`, `pandas`, `pyyaml`; parquet-сериализация требует `pyarrow`
25
+ (extra `io`).
26
+
27
+ ```bash
28
+ pip install scmgen
29
+ # с поддержкой parquet
30
+ pip install "scmgen[io]"
31
+ # для разработки
32
+ git clone https://github.com/Causal-Fin-AI/scmgen.git && cd scmgen
33
+ pip install -e ".[dev]" && OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 pytest
34
+ ```
35
+
36
+ ## Быстрый старт
37
+
38
+ ```python
39
+ from scmgen import scenarios, generate, generate_pair, generate_split, save_dataset, load_dataset, replay
40
+
41
+ scenarios.list_scenarios() # 70 сценариев библиотеки (docs/scenarios.md)
42
+ cfg = scenarios.load("rct-noise-y-seg") # ScenarioConfig; можно править поля
43
+ ds = generate(cfg, n=100_000, seed=0) # Dataset (model_seed = sample_seed = pathology_seed = 0)
44
+
45
+ ds.x, ds.t, ds.y # наблюдаемые данные (X — DataFrame)
46
+ ds.y0, ds.y1, ds.ite, ds.tau, ds.e # ground truth per row: потенциальные исходы, ite = y1 - y0,
47
+ ds.tau_cate, ds.cvte, ds.vte_w, ds.segments # oracle tau на латентах; CATE по W: E[tau|W], Var[tau|W], Var[ite|W]; сегменты
48
+ ds.graph.adjacency, ds.graph.roles # DAG (T -> Y всегда; в RCT без X -> T) и роли узлов
49
+ ds.dag() # post-intervention DAG по аргументам осей (z, compliance, u_tau_i, x3_obs, ...)
50
+ ds.pathology_log # что и куда применено: номер P-n, фаза, active, affected_columns
51
+ ds.ground_truth()["targets"], ds.design() # контракт полей истины (conditioning, population); дизайн, cate_dgp
52
+ ds.recipe() # рецепт: модель + выборка + применённые оси (replay воспроизводит бит в бит)
53
+
54
+ clean, broken = generate_pair(cfg, n=100_000, seed=0) # парные выборки: с патологиями и без
55
+ clean, dirty, test = generate_split(cfg, n_train=100_000, n_test=20_000, seed=0) # + чистый test той же SCM (test_dgp="none")
56
+ clean, dirty, test = generate_split(cfg, 100_000, 20_000, 0, test_dgp="replay") # test с теми же DGP-осями (drift: period ≡ 1)
57
+ save_dataset(ds, "out/rct-noise-y-seg-0") # out/CURRENT + out/v-<utc>-<rand>/{manifest,observed,ground_truth,meta,scm}
58
+ observed, gt, meta = load_dataset("out/rct-noise-y-seg-0", verify="rows") # "hashes" — ещё и SHA-256 файлов
59
+ ds2 = replay("out/rct-noise-y-seg-0") # тот же датасет по рецепту (в reference environment — бит в бит)
60
+ ```
61
+
62
+ Модель отдельно от выборки — learning curves по n на одной задаче и общие случайные числа:
63
+
64
+ ```python
65
+ from scmgen import sample_scm, apply_pathologies, load_scm
66
+
67
+ scm = sample_scm(cfg, model_seed=0) # граф, коэффициенты, константы калибровки (n_calib = 200 000)
68
+ small = scm.sample(1_000, sample_seed=7) # чистые выборки одной SCM: small — первые 1000 строк big
69
+ big = scm.sample(50_000, sample_seed=7) # (бит в бит по X, T, Y и всем полям истины)
70
+ dirty = apply_pathologies(big, pathology_seed=3)
71
+ scm.tau(x), scm.mu0(x), scm.e_assign(x) # уравнения на произвольных X
72
+ scm2 = load_scm("out/rct-noise-y-seg-0") # scm.json -> та же модель; .sample + apply_pathologies = датасет бит в бит
73
+ ```
74
+
75
+ Свой сценарий — через конфиг в коде или YAML:
76
+
77
+ ```python
78
+ from scmgen import ScenarioConfig, GraphConfig, MediatorConfig, PathologyConfig
79
+ cfg = ScenarioConfig(
80
+ name="my-obs",
81
+ graph=GraphConfig(n_confounders=5, n_modifiers=3, n_predictors=5, n_noise=5,
82
+ xx_density=0.1, xx_structure="scale_free", sf_gamma=2.2),
83
+ pathologies=[PathologyConfig(kind="noise_y", strength=0.8, target="segments", segment_share=0.25)],
84
+ )
85
+ cfg.treatment.mode = "obs"
86
+ cfg.to_yaml("my-obs.yaml"); cfg = ScenarioConfig.from_yaml("my-obs.yaml")
87
+ # медиация как у заказчика: без прямого T -> Y, бинарный медиатор с неоднородными стадиями
88
+ med = ScenarioConfig(name="my-mediation", graph=GraphConfig(n_predictors=4, n_med_confounders=2, n_upstream=3, n_side=1,
89
+ n_mediators=1, direct_effect=False, xx_density=0.5),
90
+ mediator=MediatorConfig(family="binary", base_rate=0.08, effect=0.55, effect_amplitude=0.4,
91
+ theta=1.2, theta_amplitude=0.4))
92
+ med.tau.amplitude = 0.0
93
+ ```
94
+
95
+ ## Что умеет генератор
96
+
97
+ - **Граф** (`docs/model.md` §1): роли узлов — конфаундеры, модификаторы, предикторы, шум, медиаторы,
98
+ инструменты, коллайдеры, конфаундеры и предикторы медиатора, узлы `upstream` / `side` без прямых рёбер к
99
+ T / M / Y; прямое ребро T → Y опционально; структуры X–X `er` | `scale_free` | `blocks` | `layered` |
100
+ `copying` и их композиции; дискретные признаки (бинарные и категориальные, one-hot по запросу); ручной
101
+ X–X-блок и ручные роли по позициям (любой граф ролевой модели задаётся точно); граф отражает исполняемые
102
+ уравнения, post-intervention DAG по применённым осям; контракт графа для внешних читателей (G-6).
103
+ - **Объект SCM** (`docs/model.md` §2): истина не зависит от n — константы калибровки на 200 000 строк; три
104
+ потока seed (модель, выборка, патологии); вложенные выборки бит в бит; неизменяемый SCM; арифметика без
105
+ BLAS; test-популяция той же SCM с двумя задачами оценки (`test_dgp = none | replay`).
106
+ - **Механизмы и исходы** (`docs/model.md` §3): линейные и нелинейные уравнения, CATE отдельно от μ₀
107
+ (амплитуда, sparsity, форма, сдвиг), SNR вплоть до «крошечного эффекта», четыре семейства исхода, связь
108
+ потенциальных исходов `shared | independent`, природный масштаб исхода для денежных откликов, медиаторы
109
+ гауссовы или бинарные («продажа»: неоднородные эффекты T → M и M → Y, §3.6).
110
+ - **Оси патологий** (`docs/model.md` §4, `docs/pathology-catalog.md`): 19 осей в трёх фазах (DGP → отбор →
111
+ измерение), сила 0..1, локализация глобально или по сегментам, свой поток rng у каждой оси, `strength = 0`
112
+ — no-op; ground truth остаётся чистым, DGP-оси пересчитывают его на том же шуме.
113
+ - **Истина** (`docs/truth-contract.md`): `tau_cate`, `cvte`, `vte_w`, поля non-compliance и отбора
114
+ (`tau_itt*`, `*_selected`), поля бинарных медиаторов (`p_m0`, `p_m1`, `tau_m`, `theta`), блок `targets` с условными наборами и способом интегрирования каждого поля,
115
+ таблица осей, меняющих функцию CATE(W).
116
+ - **Выход** (`docs/dataset-format.md`): версионная атомарная публикация (`CURRENT` + версии), manifest с
117
+ хешами файлов и отпечатком окружения, `meta.json` с конфигом, `targets`, `design`, `recipe`,
118
+ `pathology_log`; `scm.json` — model artifact; `replay` по рецепту.
119
+ - **Воспроизводимость** (`docs/reproducibility.md`): битовая в reference environment (включая
120
+ reference-equivalent режим для AVX512), численная (rtol 1e-9) в любом окружении; два уровня эталона по всей
121
+ библиотеке; инструменты локализации расхождений между машинами.
122
+ - **Библиотека** (`docs/scenarios.md`): 70 именованных сценариев — база и патологии, расширенный пакет,
123
+ структуры графа, дискретные признаки, контрольные аналитические, кластеры и дубли, beta и связь исходов,
124
+ денежный исход с выбросами, медиация как у заказчика (в том числе граф примера заказчика целиком).
125
+
126
+ ## Проверки
127
+
128
+ 957 тестов (`pytest`, около 13 минут в reference environment: каждый уникальный конфиг калибрует SCM на
129
+ 200 000 строк) и два уровня эталона (`tests/golden/`, `scripts/scmgen_golden.py`) — подробно в
130
+ `docs/reproducibility.md`. Изменение поведения генератора = новая версия + новый эталон + запись в
131
+ `CHANGELOG.md`.
132
+
133
+ ## Происхождение
134
+
135
+ Пакет разработан в рамках НИР «Оценка индивидуальных эффектов на финансовых данных» (2026) как синтетический
136
+ слой бенчмарка ITE/uplift-методов и выделен из внутреннего репозитория НИР `uplift_methods` (см.
137
+ `CHANGELOG.md`). Обзоры литературы и результаты бенчмарка остаются в исходном репозитории; каталог патологий
138
+ с источниками — `docs/pathology-catalog.md`.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "scmgen"
7
+ version = "0.10.0"
8
+ description = "SCM-генератор синтетических данных для uplift/ITE/CATE-бенчмарков: явный DAG, полный ground truth, оси патологий данных"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ authors = [{ name = "DK", email = "dk.promsoft@gmail.com" }]
12
+ dependencies = [
13
+ "numpy>=2",
14
+ "pandas>=2",
15
+ "pyyaml>=6",
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/Causal-Fin-AI/scmgen"
20
+
21
+ [project.optional-dependencies]
22
+ io = ["pyarrow>=15"] # save_dataset / load_dataset (parquet)
23
+ dev = ["pytest>=8", "pyarrow>=15"]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ "scmgen.scenarios" = ["*.yaml"]
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["tests"]
33
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,39 @@
1
+ """SCM-генератор синтетических данных для uplift/CATE-бенчмарка.
2
+
3
+ Спецификация: `docs/model.md`; контракт полей истины и фазы патологий (0.5.0) —
4
+ см. CHANGELOG 0.5.0; объект SCM, уровни seed, provenance, оси 0.6.0 — `docs/model.md`;
5
+ контракт истины 0.7.0, рецепт/replay, версионная публикация — `docs/truth-contract.md`;
6
+ реестр патологий — `docs/pathology-catalog.md`.
7
+ """
8
+
9
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
10
+
11
+ from . import scenarios
12
+ from .config import (PATHOLOGY_NUMBERS, GraphConfig, MechanismConfig, MediatorConfig, OutcomeConfig,
13
+ PathologyConfig, ScenarioConfig, SegmentConfig, TauConfig, TreatmentConfig, pathology_number,
14
+ pathology_phase)
15
+ from .diagnostics import data_properties
16
+ from .generate import Dataset, generate, generate_pair, generate_split
17
+ from .graph import SampledGraph, graph_properties, sample_graph
18
+ from .io import load_dataset, load_scm, prune_versions, replay, save_dataset
19
+ from .pathologies import apply_pathologies
20
+ from .reference import pehe, segment_dim, t_learner_ridge
21
+ from .scm import SCM, sample_scm
22
+ from .segments import build_segments
23
+ from .truth import refresh_truth, truth_targets
24
+
25
+ try:
26
+ __version__ = _pkg_version("scmgen")
27
+ except PackageNotFoundError: # запуск из дерева исходников без установки
28
+ __version__ = "0.10.0"
29
+
30
+ __all__ = [
31
+ "ScenarioConfig", "GraphConfig", "MechanismConfig", "TauConfig",
32
+ "TreatmentConfig", "OutcomeConfig", "SegmentConfig", "PathologyConfig", "MediatorConfig",
33
+ "Dataset", "generate", "generate_pair", "generate_split", "apply_pathologies",
34
+ "SCM", "sample_scm", "load_scm", "replay", "prune_versions", "PATHOLOGY_NUMBERS", "pathology_number",
35
+ "build_segments", "SampledGraph", "sample_graph", "graph_properties",
36
+ "save_dataset", "load_dataset", "t_learner_ridge", "segment_dim", "pehe",
37
+ "data_properties", "pathology_phase", "refresh_truth", "truth_targets",
38
+ "scenarios", "__version__",
39
+ ]
@@ -0,0 +1,158 @@
1
+ """Бета-распределение на numpy (0.8.0, см. CHANGELOG 0.8.0): ln Γ, регуляризованная неполная
2
+ бета-функция I_x(a, b) и обратная к ней по x. scipy в зависимости не входит (как и для Φ⁻¹ в truth.py).
3
+
4
+ Все функции поэлементны в строгом смысле: результат элемента зависит только от его аргументов. Итерации
5
+ (цепная дробь, уточнение квантиля) идут до сходимости каждого элемента отдельно, сошедшиеся элементы
6
+ замораживаются — поэтому вложенные выборки (`scm.sample(n₁)` — префикс `scm.sample(n₂)`) совпадают бит в бит.
7
+ Область: a, b ∈ [1e-3, 1e4], x, p ∈ [0, 1]; точность закреплена тестами (tests/test_v080.py).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+
14
+ _LANCZOS_G = 7.0
15
+ _LANCZOS = (0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313,
16
+ -176.61502916214059, 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6,
17
+ 1.5056327351493116e-7)
18
+ _HALF_LOG_2PI = 0.5 * np.log(2.0 * np.pi)
19
+ _CF_EPS = 1e-16
20
+ _CF_TINY = 1e-300
21
+ _CF_MAXIT = 2000
22
+ _INV_MAXIT = 200
23
+
24
+
25
+ def gammaln(x) -> np.ndarray:
26
+ """ln Γ(x), x > 0 (Ланцош g = 7, 9 членов; x < 0.5 — через Γ(x + 1) = x Γ(x))."""
27
+ x = np.asarray(x, dtype=float)
28
+ small = x < 0.5
29
+ z = np.where(small, x + 1.0, x) - 1.0
30
+ acc = np.full(x.shape, _LANCZOS[0])
31
+ for i in range(1, 9):
32
+ acc = acc + _LANCZOS[i] / (z + i)
33
+ t = z + _LANCZOS_G + 0.5
34
+ out = _HALF_LOG_2PI + (z + 0.5) * np.log(t) - t + np.log(acc)
35
+ return np.where(small, out - np.log(np.where(small, x, 1.0)), out)
36
+
37
+
38
+ def betaln(a, b) -> np.ndarray:
39
+ """ln B(a, b)."""
40
+ return gammaln(a) + gammaln(b) - gammaln(np.add(a, b))
41
+
42
+
43
+ def _betacf(a: np.ndarray, b: np.ndarray, x: np.ndarray) -> np.ndarray:
44
+ """Цепная дробь неполной бета-функции (модифицированный метод Ленца); одномерные массивы равной длины.
45
+ Каждый элемент итерируется до |Δ − 1| < eps и замораживается."""
46
+ out = np.empty(x.size)
47
+ idx = np.arange(x.size)
48
+ qab, qap, qam = a + b, a + 1.0, a - 1.0
49
+ c = np.ones(x.size)
50
+ d = 1.0 - qab * x / qap
51
+ d = 1.0 / np.where(np.abs(d) < _CF_TINY, _CF_TINY, d)
52
+ h = d.copy()
53
+ for m in range(1, _CF_MAXIT + 1):
54
+ m2 = 2.0 * m
55
+ aa = m * (b - m) * x / ((qam + m2) * (a + m2))
56
+ d = 1.0 + aa * d
57
+ d = 1.0 / np.where(np.abs(d) < _CF_TINY, _CF_TINY, d)
58
+ c = 1.0 + aa / c
59
+ c = np.where(np.abs(c) < _CF_TINY, _CF_TINY, c)
60
+ h = h * d * c
61
+ aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
62
+ d = 1.0 + aa * d
63
+ d = 1.0 / np.where(np.abs(d) < _CF_TINY, _CF_TINY, d)
64
+ c = 1.0 + aa / c
65
+ c = np.where(np.abs(c) < _CF_TINY, _CF_TINY, c)
66
+ delta = d * c
67
+ h = h * delta
68
+ done = np.abs(delta - 1.0) < _CF_EPS
69
+ if done.any():
70
+ out[idx[done]] = h[done]
71
+ keep = ~done
72
+ if not keep.any():
73
+ return out
74
+ idx, a, b, x, c, d, h = idx[keep], a[keep], b[keep], x[keep], c[keep], d[keep], h[keep]
75
+ qab, qap, qam = qab[keep], qap[keep], qam[keep]
76
+ out[idx] = h # не сошлось за _CF_MAXIT: вне заявленной области
77
+ return out
78
+
79
+
80
+ def betainc(a, b, x, lnb=None) -> np.ndarray:
81
+ """Регуляризованная неполная бета-функция I_x(a, b) = P(Beta(a, b) ≤ x); x вне [0, 1] обрезается.
82
+ lnb — заранее посчитанный ln B(a, b) (той же формы после broadcast), иначе считается здесь."""
83
+ a, b, x = np.broadcast_arrays(np.asarray(a, dtype=float), np.asarray(b, dtype=float),
84
+ np.asarray(x, dtype=float))
85
+ shape = x.shape
86
+ lnb = betaln(a, b) if lnb is None else np.broadcast_to(np.asarray(lnb, dtype=float), shape)
87
+ a, b, x, lnb = a.ravel(), b.ravel(), np.clip(x.ravel(), 0.0, 1.0), lnb.ravel()
88
+ out = np.where(x >= 1.0, 1.0, 0.0)
89
+ inner = np.flatnonzero((x > 0.0) & (x < 1.0))
90
+ if inner.size:
91
+ a, b, x, lnb = a[inner], b[inner], x[inner], lnb[inner]
92
+ front = np.exp(a * np.log(x) + b * np.log1p(-x) - lnb)
93
+ direct = x < (a + 1.0) / (a + b + 2.0)
94
+ aa, bb, xx = np.where(direct, a, b), np.where(direct, b, a), np.where(direct, x, 1.0 - x)
95
+ val = front * _betacf(aa, bb, xx) / aa
96
+ out[inner] = np.where(direct, val, 1.0 - val)
97
+ return out.reshape(shape)
98
+
99
+
100
+ def _initial_guess(a, b, p):
101
+ """Начальное приближение квантиля (Numerical Recipes, invbetai)."""
102
+ with np.errstate(divide="ignore", invalid="ignore", over="ignore"):
103
+ pp = np.where(p < 0.5, p, 1.0 - p)
104
+ t = np.sqrt(-2.0 * np.log(pp))
105
+ z = (2.30753 + t * 0.27061) / (1.0 + t * (0.99229 + t * 0.04481)) - t
106
+ z = np.where(p < 0.5, -z, z)
107
+ al = (z * z - 3.0) / 6.0
108
+ ia, ib = 1.0 / (2.0 * a - 1.0), 1.0 / (2.0 * b - 1.0)
109
+ hh = 2.0 / (ia + ib)
110
+ w = z * np.sqrt(al + hh) / hh - (ib - ia) * (al + 5.0 / 6.0 - 2.0 / (3.0 * hh))
111
+ big = a / (a + b * np.exp(2.0 * w))
112
+ lna, lnbb = np.log(a / (a + b)), np.log(b / (a + b))
113
+ ta, tb = np.exp(a * lna) / a, np.exp(b * lnbb) / b
114
+ ww = ta + tb
115
+ small = np.where(p < ta / ww, np.power(a * ww * p, 1.0 / a), 1.0 - np.power(b * ww * (1.0 - p), 1.0 / b))
116
+ x = np.where((a >= 1.0) & (b >= 1.0), big, small)
117
+ return np.where(np.isfinite(x), x, 0.5)
118
+
119
+
120
+ def betaincinv(a, b, p) -> np.ndarray:
121
+ """Квантиль бета-распределения: x с I_x(a, b) = p. Шаги Галлея с вилкой [lo, hi] (вне вилки — бисекция),
122
+ каждый элемент — до сходимости шага и замораживается."""
123
+ a, b, p = np.broadcast_arrays(np.asarray(a, dtype=float), np.asarray(b, dtype=float),
124
+ np.asarray(p, dtype=float))
125
+ shape = p.shape
126
+ a, b, p = a.ravel(), b.ravel(), p.ravel()
127
+ out = np.where(p >= 1.0, 1.0, 0.0)
128
+ idx = np.flatnonzero((p > 0.0) & (p < 1.0))
129
+ if not idx.size:
130
+ return out.reshape(shape)
131
+ a, b, p = a[idx], b[idx], p[idx]
132
+ lnb = betaln(a, b)
133
+ a1, b1 = a - 1.0, b - 1.0
134
+ lo, hi = np.zeros(p.size), np.ones(p.size)
135
+ x = np.clip(_initial_guess(a, b, p), 1e-300, 1.0 - 1e-16)
136
+ for _ in range(_INV_MAXIT):
137
+ err = betainc(a, b, x, lnb) - p
138
+ lo = np.where(err < 0.0, x, lo)
139
+ hi = np.where(err > 0.0, x, hi)
140
+ with np.errstate(divide="ignore", invalid="ignore", over="ignore"):
141
+ dens = np.exp(a1 * np.log(x) + b1 * np.log1p(-x) - lnb)
142
+ u = err / dens
143
+ step = u / (1.0 - 0.5 * np.clip(u * (a1 / x - b1 / (1.0 - x)), -1.0, 1.0))
144
+ new = x - step
145
+ bad = ~np.isfinite(new) | (new <= lo) | (new >= hi) | ~np.isfinite(dens) | (dens <= 0.0)
146
+ new = np.where(bad, 0.5 * (lo + hi), new)
147
+ done = (err == 0.0) | (np.abs(new - x) <= 4e-16 * x) | (hi - lo <= 4e-16 * hi)
148
+ x = np.where(err == 0.0, x, new)
149
+ if done.any():
150
+ out[idx[done]] = x[done]
151
+ keep = ~done
152
+ if not keep.any():
153
+ break
154
+ idx, a, b, p, lnb, a1, b1 = idx[keep], a[keep], b[keep], p[keep], lnb[keep], a1[keep], b1[keep]
155
+ lo, hi, x = lo[keep], hi[keep], x[keep]
156
+ else:
157
+ out[idx] = x
158
+ return out.reshape(shape)