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.
- scmgen-0.10.0/PKG-INFO +155 -0
- scmgen-0.10.0/README.md +138 -0
- scmgen-0.10.0/pyproject.toml +33 -0
- scmgen-0.10.0/setup.cfg +4 -0
- scmgen-0.10.0/src/scmgen/__init__.py +39 -0
- scmgen-0.10.0/src/scmgen/betafn.py +158 -0
- scmgen-0.10.0/src/scmgen/config.py +768 -0
- scmgen-0.10.0/src/scmgen/diagnostics.py +45 -0
- scmgen-0.10.0/src/scmgen/generate.py +608 -0
- scmgen-0.10.0/src/scmgen/graph.py +700 -0
- scmgen-0.10.0/src/scmgen/io.py +474 -0
- scmgen-0.10.0/src/scmgen/mechanisms.py +419 -0
- scmgen-0.10.0/src/scmgen/outcomes.py +259 -0
- scmgen-0.10.0/src/scmgen/pathologies.py +1108 -0
- scmgen-0.10.0/src/scmgen/reference.py +57 -0
- scmgen-0.10.0/src/scmgen/scenarios/__init__.py +38 -0
- scmgen-0.10.0/src/scmgen/scenarios/ctl-binary-rd.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/ctl-const-neg.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/ctl-const-pos.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/ctl-linear-cate.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/ctl-null.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/obs-cluster.yaml +83 -0
- scmgen-0.10.0/src/scmgen/scenarios/obs-confounding.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/obs-hidden-conf.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/obs-noise-conf.yaml +84 -0
- scmgen-0.10.0/src/scmgen/scenarios/obs-overlap.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/obs-targeted.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-bad-controls.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-beta-clean.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-beta-het-seg.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-beta-hetero-seg.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-beta-indep.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-beta-zoi-both.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-beta-zoi.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-clean-disc.yaml +79 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-clean.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-cluster-assign.yaml +83 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-cluster.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-covariate-shift.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-drift-seg.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-drift-tau.yaml +95 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-drift.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-dup-rows.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-fewplacebo-nl.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-fewplacebo.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-blocks-nl.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-blocks.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-clones-nl.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-clones.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-deep.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-dense.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-layered.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-sf-disc.yaml +79 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-sf-nl.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-sf.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-graph-sparse.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-het-seg-disc.yaml +78 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-het-seg.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-hidden-modifier.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-indep.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-mediation-binary.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-mediation-customer.yaml +186 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-mediation-partial.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-missing-mar.yaml +83 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-missing-seg.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-missing-sentinel.yaml +83 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-missing-ty.yaml +86 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-money-het.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-money-null.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-nl-clean.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-nl-het-seg.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-nl-noise-y-seg.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-nl-zeroinfl.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-noise-y-seg.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-noisey-beta.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-noisey-bin.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-noisey-diff-y.yaml +85 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-noisey-diff.yaml +85 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-noisey-ziln.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-noncompliance.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-outliers.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-quantized.yaml +84 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-select-seg-t.yaml +83 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-select-seg.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-t-noise.yaml +82 -0
- scmgen-0.10.0/src/scmgen/scenarios/rct-zeroinfl-smalltau.yaml +76 -0
- scmgen-0.10.0/src/scmgen/scm.py +577 -0
- scmgen-0.10.0/src/scmgen/segments.py +144 -0
- scmgen-0.10.0/src/scmgen/truth.py +1042 -0
- scmgen-0.10.0/src/scmgen.egg-info/PKG-INFO +155 -0
- scmgen-0.10.0/src/scmgen.egg-info/SOURCES.txt +112 -0
- scmgen-0.10.0/src/scmgen.egg-info/dependency_links.txt +1 -0
- scmgen-0.10.0/src/scmgen.egg-info/requires.txt +10 -0
- scmgen-0.10.0/src/scmgen.egg-info/top_level.txt +1 -0
- scmgen-0.10.0/tests/test_acceptance.py +108 -0
- scmgen-0.10.0/tests/test_airgapped.py +43 -0
- scmgen-0.10.0/tests/test_audit_2026_09_19.py +576 -0
- scmgen-0.10.0/tests/test_audit_2026_09_19_1.py +214 -0
- scmgen-0.10.0/tests/test_audit_2026_09_19_2.py +299 -0
- scmgen-0.10.0/tests/test_audit_2026_09_20.py +223 -0
- scmgen-0.10.0/tests/test_discrete_v040.py +271 -0
- scmgen-0.10.0/tests/test_golden.py +68 -0
- scmgen-0.10.0/tests/test_graph_contract.py +54 -0
- scmgen-0.10.0/tests/test_graph_structure.py +123 -0
- scmgen-0.10.0/tests/test_graph_v030.py +212 -0
- scmgen-0.10.0/tests/test_pathologies.py +165 -0
- scmgen-0.10.0/tests/test_pathologies_ext.py +248 -0
- scmgen-0.10.0/tests/test_requests_v020.py +136 -0
- scmgen-0.10.0/tests/test_scmgen.py +246 -0
- scmgen-0.10.0/tests/test_v0100.py +352 -0
- scmgen-0.10.0/tests/test_v060.py +610 -0
- scmgen-0.10.0/tests/test_v070.py +684 -0
- scmgen-0.10.0/tests/test_v080.py +497 -0
- 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`.
|
scmgen-0.10.0/README.md
ADDED
|
@@ -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"]
|
scmgen-0.10.0/setup.cfg
ADDED
|
@@ -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)
|