fast-exp-analytics 0.3.0__py3-none-any.whl

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.
@@ -0,0 +1,156 @@
1
+ from . import (
2
+ bayesian,
3
+ diagnostics,
4
+ effect_sizes,
5
+ multi_group,
6
+ multiple_testing,
7
+ power,
8
+ sequential,
9
+ stat_tests,
10
+ variance_reduction,
11
+ )
12
+ from ._extensions import EXTENDED_COLUMNS
13
+ from .ab import run_ab_test, style_table_ab
14
+ from .abc import (
15
+ run_abc_test,
16
+ run_abcd_test,
17
+ run_multigroup_test,
18
+ run_omnibus_test,
19
+ style_table_abc,
20
+ style_table_abcd,
21
+ style_table_multigroup,
22
+ )
23
+ from .bayesian import bayes_means, bayes_proportions
24
+ from .config import default_metrics_config, validate_metrics_config
25
+ from .datasets import make_synthetic_abc_dataset, make_synthetic_abcd_dataset
26
+ from .diagnostics import (
27
+ check_unit_overlap,
28
+ describe_groups,
29
+ normality_report,
30
+ recommend_test,
31
+ simulate_aa,
32
+ srm_check,
33
+ srm_check_df,
34
+ )
35
+ from .duration import (
36
+ build_experiment_level,
37
+ default_duration_metrics_config,
38
+ duration_plan_summary,
39
+ mde_table_for_experiment_duration,
40
+ mde_table_from_aggregated_df,
41
+ required_days_for_target_mde_table,
42
+ required_days_from_aggregated_df,
43
+ units_per_day_from_raw,
44
+ )
45
+ from .exporters import export_abc_results_to_excel, export_abcd_results_to_excel
46
+ from .exporters_ab import export_ab_results_to_excel
47
+ from .exporters_duration import (
48
+ build_duration_manager_text,
49
+ export_duration_results_to_excel,
50
+ )
51
+ from .llm import OpenAICompatConfig, build_llm_review
52
+ from .messaging import ChatSendConfig, send_chat_message
53
+ from .multi_group import (
54
+ anova_test,
55
+ chi2_homogeneity_test,
56
+ dunn_test,
57
+ dunnett_test,
58
+ games_howell_test,
59
+ kruskal_wallis_test,
60
+ tukey_hsd_test,
61
+ welch_anova_test,
62
+ )
63
+ from .multiple_testing import adjust_pvalues
64
+ from .reporting import build_abc_chat_message, build_abcd_chat_message, build_dashboard_url_abc
65
+ from .reporting_ab import build_ab_chat_message, build_dashboard_url_ab
66
+ from .sequential import (
67
+ always_valid_ci,
68
+ always_valid_p_value,
69
+ group_sequential_boundaries,
70
+ group_sequential_test,
71
+ msprt_sequence,
72
+ )
73
+ from .stat_tests import StatTestResult, available_tests
74
+ from .variance_reduction import (
75
+ add_cuped_column,
76
+ cuped_adjust,
77
+ post_stratified_difference,
78
+ winsorize_columns,
79
+ )
80
+
81
+ __all__ = [
82
+ "EXTENDED_COLUMNS",
83
+ "ChatSendConfig",
84
+ "OpenAICompatConfig",
85
+ "StatTestResult",
86
+ "add_cuped_column",
87
+ "adjust_pvalues",
88
+ "always_valid_ci",
89
+ "always_valid_p_value",
90
+ "anova_test",
91
+ "available_tests",
92
+ "bayes_means",
93
+ "bayes_proportions",
94
+ "bayesian",
95
+ "build_ab_chat_message",
96
+ "build_abc_chat_message",
97
+ "build_abcd_chat_message",
98
+ "build_dashboard_url_ab",
99
+ "build_dashboard_url_abc",
100
+ "build_duration_manager_text",
101
+ "build_experiment_level",
102
+ "build_llm_review",
103
+ "check_unit_overlap",
104
+ "chi2_homogeneity_test",
105
+ "cuped_adjust",
106
+ "default_duration_metrics_config",
107
+ "default_metrics_config",
108
+ "describe_groups",
109
+ "diagnostics",
110
+ "dunn_test",
111
+ "dunnett_test",
112
+ "duration_plan_summary",
113
+ "effect_sizes",
114
+ "export_ab_results_to_excel",
115
+ "export_abc_results_to_excel",
116
+ "export_abcd_results_to_excel",
117
+ "export_duration_results_to_excel",
118
+ "games_howell_test",
119
+ "group_sequential_boundaries",
120
+ "group_sequential_test",
121
+ "kruskal_wallis_test",
122
+ "make_synthetic_abc_dataset",
123
+ "make_synthetic_abcd_dataset",
124
+ "mde_table_for_experiment_duration",
125
+ "mde_table_from_aggregated_df",
126
+ "msprt_sequence",
127
+ "multi_group",
128
+ "multiple_testing",
129
+ "normality_report",
130
+ "post_stratified_difference",
131
+ "power",
132
+ "recommend_test",
133
+ "required_days_for_target_mde_table",
134
+ "required_days_from_aggregated_df",
135
+ "run_ab_test",
136
+ "run_abc_test",
137
+ "run_abcd_test",
138
+ "run_multigroup_test",
139
+ "run_omnibus_test",
140
+ "send_chat_message",
141
+ "sequential",
142
+ "simulate_aa",
143
+ "srm_check",
144
+ "srm_check_df",
145
+ "stat_tests",
146
+ "style_table_ab",
147
+ "style_table_abc",
148
+ "style_table_abcd",
149
+ "style_table_multigroup",
150
+ "tukey_hsd_test",
151
+ "units_per_day_from_raw",
152
+ "validate_metrics_config",
153
+ "variance_reduction",
154
+ "welch_anova_test",
155
+ "winsorize_columns",
156
+ ]
@@ -0,0 +1,289 @@
1
+ """Общая логика расширенных возможностей пайплайнов AB / ABC / ABCD.
2
+
3
+ Опциональные колонки конфига метрик:
4
+
5
+ * ``test`` — критерий вместо дефолтного для типа метрики (см. ``available_tests``);
6
+ * ``covariate`` — колонка с пред-экспериментальным значением для CUPED
7
+ (типы ``additive`` и ``average``);
8
+ * ``winsorize`` — верхний квантиль винзоризации, например ``0.99``
9
+ (типы ``additive`` и ``average``); порог считается по всем сравниваемым группам.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Callable
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+
19
+ from .confidence_intervals import metric_confidence_interval
20
+ from .effect_sizes import cliffs_delta, cohens_d, cohens_h, relative_lift_ci
21
+ from .stat_tests import (
22
+ ALLOWED_TESTS_BY_TYPE,
23
+ DEFAULT_TEST_BY_TYPE,
24
+ ESTIMAND_BY_TYPE,
25
+ JOINT_POSTHOC_TESTS,
26
+ available_tests,
27
+ ratio_group_variance,
28
+ run_metric_test,
29
+ )
30
+ from .variance_reduction import cuped_adjust
31
+
32
+ OPTIONAL_METRIC_COLUMNS = ("test", "covariate", "winsorize")
33
+
34
+ EXTENDED_COLUMNS = [
35
+ "test_name",
36
+ "statistic",
37
+ "effect_size",
38
+ "effect_size_name",
39
+ "rel_ci_lower",
40
+ "rel_ci_upper",
41
+ "delta_cuped",
42
+ "cuped_variance_reduction",
43
+ "winsorize_threshold",
44
+ ]
45
+
46
+ _VALUE_ADJUSTABLE_TYPES = ("additive", "average")
47
+
48
+
49
+ def _is_missing(value) -> bool:
50
+ if value is None:
51
+ return True
52
+ if isinstance(value, str):
53
+ return value.strip() == ""
54
+ try:
55
+ return bool(pd.isna(value))
56
+ except (TypeError, ValueError):
57
+ return False
58
+
59
+
60
+ def metric_options(metrics_df: pd.DataFrame, metric) -> dict:
61
+ """Прочитать опциональные настройки метрики (``test``, ``covariate``, ``winsorize``)."""
62
+ out = {"test": None, "covariate": None, "winsorize": None}
63
+ for key in OPTIONAL_METRIC_COLUMNS:
64
+ if key not in metrics_df.columns:
65
+ continue
66
+ value = metrics_df.loc[metric, key]
67
+ if _is_missing(value):
68
+ continue
69
+ if key == "test":
70
+ out[key] = str(value).strip().lower()
71
+ elif key == "covariate":
72
+ out[key] = str(value)
73
+ else:
74
+ out[key] = float(value)
75
+ return out
76
+
77
+
78
+ def validate_metric_options(metrics_df: pd.DataFrame, multi_group: bool = False) -> None:
79
+ """Проверить опциональные колонки конфига метрик; при ошибке — ``ValueError``."""
80
+ if not any(col in metrics_df.columns for col in OPTIONAL_METRIC_COLUMNS):
81
+ return
82
+ for metric in metrics_df.index:
83
+ metric_type = str(metrics_df.loc[metric, "type"]).lower()
84
+ opts = metric_options(metrics_df, metric)
85
+ test = opts["test"]
86
+ if test is not None:
87
+ if metric_type not in ALLOWED_TESTS_BY_TYPE:
88
+ raise ValueError(f"Metric {metric!r}: unsupported metric type {metric_type!r}")
89
+ allowed = available_tests(metric_type, multi_group=multi_group)
90
+ if test not in allowed:
91
+ hint = ""
92
+ if test in JOINT_POSTHOC_TESTS and not multi_group:
93
+ hint = " (post-hoc процедуры доступны только для 3+ групп)"
94
+ raise ValueError(
95
+ f"Metric {metric!r}: test {test!r} is not supported for type "
96
+ f"{metric_type!r}{hint}; allowed: {list(allowed)}"
97
+ )
98
+ for key in ("covariate", "winsorize"):
99
+ if opts[key] is not None and metric_type not in _VALUE_ADJUSTABLE_TYPES:
100
+ raise ValueError(
101
+ f"Metric {metric!r}: '{key}' is supported only for metric types "
102
+ f"{list(_VALUE_ADJUSTABLE_TYPES)}, got {metric_type!r}"
103
+ )
104
+ if opts["winsorize"] is not None and not 0 < opts["winsorize"] < 1:
105
+ raise ValueError(f"Metric {metric!r}: 'winsorize' must be a quantile in (0, 1)")
106
+ if opts["covariate"] is not None and test in JOINT_POSTHOC_TESTS:
107
+ raise ValueError(
108
+ f"Metric {metric!r}: 'covariate' (CUPED) cannot be combined with joint "
109
+ f"post-hoc test {test!r}"
110
+ )
111
+
112
+
113
+ def build_metric_frame(df: pd.DataFrame, num: str, den: str, covariate: str | None = None):
114
+ """Собрать фрейм метрики с колонками ``user_id, exp_group, num, den[, cov]``."""
115
+ if covariate is not None and covariate not in df.columns:
116
+ raise ValueError(f"Input data is missing covariate column: {covariate!r}")
117
+ data = {"user_id": df["user_id"], "exp_group": df["exp_group"], "num": df[num]}
118
+ data["den"] = df[den] if num != den else 1.0
119
+ if covariate is not None:
120
+ data["cov"] = pd.to_numeric(df[covariate], errors="coerce")
121
+ return pd.DataFrame(data)
122
+
123
+
124
+ def winsorize_metric_frame(df_metric: pd.DataFrame, metric_type: str, quantile: float, groups):
125
+ """Обрезать ``num`` сверху по квантилю, посчитанному по всем сравниваемым группам."""
126
+ df_metric = df_metric.copy()
127
+ num = pd.to_numeric(df_metric["num"], errors="coerce").replace([np.inf, -np.inf], np.nan)
128
+ in_scope = df_metric["exp_group"].isin(list(groups)) & num.notna()
129
+ if metric_type == "average":
130
+ in_scope &= pd.to_numeric(df_metric["den"], errors="coerce") > 0
131
+ reference = num[in_scope]
132
+ if reference.empty:
133
+ return df_metric, np.nan
134
+ threshold = float(np.quantile(reference.to_numpy(dtype=float), quantile))
135
+ df_metric["num"] = num.clip(upper=threshold)
136
+ return df_metric, threshold
137
+
138
+
139
+ def cuped_stat_frames(df_control: pd.DataFrame, df_pilot: pd.DataFrame):
140
+ """Вернуть копии фреймов с CUPED-скорректированной ``value`` и сводку корректировки."""
141
+ adj_c, adj_e, info = cuped_adjust(
142
+ df_control["value"].to_numpy(dtype=float),
143
+ df_control["cov"].to_numpy(dtype=float),
144
+ df_pilot["value"].to_numpy(dtype=float),
145
+ df_pilot["cov"].to_numpy(dtype=float),
146
+ )
147
+ stat_c = df_control.assign(value=adj_c)
148
+ stat_e = df_pilot.assign(value=adj_e)
149
+ info = dict(info)
150
+ info["delta_cuped"] = float(np.nanmean(adj_e) - np.nanmean(adj_c)) if len(adj_c) and len(adj_e) else np.nan
151
+ return stat_c, stat_e, info
152
+
153
+
154
+ def resolve_inference(
155
+ stat_control: pd.DataFrame,
156
+ stat_pilot: pd.DataFrame,
157
+ metric_type: str,
158
+ test: str | None,
159
+ ci_alpha: float,
160
+ n_resamples: int,
161
+ random_state,
162
+ default_p_value_fn: Callable,
163
+ need_statistic: bool = False,
164
+ override: dict | None = None,
165
+ ) -> dict:
166
+ """p-value, CI, имя критерия и статистика с учётом выбранного ``test``.
167
+
168
+ Без ``test`` (или с дефолтным для типа) поведение идентично исходному
169
+ пайплайну. Для альтернативного критерия CI берётся из него, только если
170
+ критерий оценивает ту же величину, что и дельта метрики; иначе остаётся
171
+ дефолтный CI типа метрики.
172
+ """
173
+ default_name = DEFAULT_TEST_BY_TYPE.get(metric_type)
174
+
175
+ def _default_ci():
176
+ return metric_confidence_interval(
177
+ stat_control, stat_pilot, metric_type, ci_alpha, n_resamples, random_state
178
+ )
179
+
180
+ if override is not None:
181
+ ci = (override.get("ci_lower", np.nan), override.get("ci_upper", np.nan))
182
+ if not (np.isfinite(ci[0]) and np.isfinite(ci[1])):
183
+ ci = _default_ci()
184
+ return {
185
+ "p_value": override.get("p_value", np.nan),
186
+ "ci_lower": ci[0],
187
+ "ci_upper": ci[1],
188
+ "test_name": override.get("test_name", ""),
189
+ "statistic": override.get("statistic", np.nan),
190
+ }
191
+
192
+ if test is None or test == default_name:
193
+ statistic = np.nan
194
+ if need_statistic and default_name is not None:
195
+ statistic = run_metric_test(
196
+ stat_control, stat_pilot, metric_type, None, ci_alpha, n_resamples, random_state
197
+ ).statistic
198
+ ci = _default_ci()
199
+ return {
200
+ "p_value": default_p_value_fn(stat_control, stat_pilot, metric_type),
201
+ "ci_lower": ci[0],
202
+ "ci_upper": ci[1],
203
+ "test_name": default_name or "",
204
+ "statistic": statistic,
205
+ }
206
+
207
+ res = run_metric_test(
208
+ stat_control, stat_pilot, metric_type, test, ci_alpha, n_resamples, random_state
209
+ )
210
+ ci = (res.ci_lower, res.ci_upper)
211
+ same_estimand = res.estimand == ESTIMAND_BY_TYPE.get(metric_type)
212
+ if not (same_estimand and np.isfinite(ci[0]) and np.isfinite(ci[1])):
213
+ ci = _default_ci()
214
+ p_value = float(res.p_value) if np.isfinite(res.p_value) else np.nan
215
+ return {
216
+ "p_value": p_value,
217
+ "ci_lower": ci[0],
218
+ "ci_upper": ci[1],
219
+ "test_name": test,
220
+ "statistic": res.statistic,
221
+ }
222
+
223
+
224
+ def _mean_and_var(values) -> tuple[float, float]:
225
+ x = np.asarray(values, dtype=float)
226
+ x = x[np.isfinite(x)]
227
+ if len(x) < 2:
228
+ return np.nan, np.nan
229
+ return float(x.mean()), float(x.var(ddof=1) / len(x))
230
+
231
+
232
+ def _share_mean_and_var(frame: pd.DataFrame) -> tuple[float, float]:
233
+ valid = (frame["den"] > 0) & frame["num"].notna()
234
+ n = int(valid.sum())
235
+ if n == 0:
236
+ return np.nan, np.nan
237
+ p = float((valid & (frame["num"] > 0)).sum() / n)
238
+ return p, p * (1 - p) / n
239
+
240
+
241
+ def extended_stats(
242
+ df_control: pd.DataFrame,
243
+ df_pilot: pd.DataFrame,
244
+ stat_control: pd.DataFrame,
245
+ stat_pilot: pd.DataFrame,
246
+ metric_type: str,
247
+ alpha: float,
248
+ ) -> dict:
249
+ """Размер эффекта и CI относительного изменения для расширенного вывода.
250
+
251
+ * ``additive`` / ``average`` / ``ratio`` — d Коэна (для ratio по
252
+ линеаризованным значениям);
253
+ * ``share`` — h Коэна;
254
+ * ``median`` — дельта Клиффа (ранг-бисериальная корреляция).
255
+
256
+ ``rel_ci_*`` — CI относительного изменения в долях (дельта-метод): для
257
+ ``additive`` / ``average`` — среднего на юнит, для ``share`` — доли, для
258
+ ``ratio`` — самой ratio-метрики.
259
+ """
260
+ out = {
261
+ "effect_size": np.nan,
262
+ "effect_size_name": "",
263
+ "rel_ci_lower": np.nan,
264
+ "rel_ci_upper": np.nan,
265
+ }
266
+ if metric_type in ("additive", "average"):
267
+ out["effect_size"] = cohens_d(df_control["value"], df_pilot["value"])
268
+ out["effect_size_name"] = "cohens_d"
269
+ m_c, v_c = _mean_and_var(stat_control["value"])
270
+ m_e, v_e = _mean_and_var(stat_pilot["value"])
271
+ elif metric_type == "ratio":
272
+ out["effect_size"] = cohens_d(df_control["value"], df_pilot["value"])
273
+ out["effect_size_name"] = "cohens_d_linearized"
274
+ m_c, v_c, _ = ratio_group_variance(df_control["num"], df_control["den"])
275
+ m_e, v_e, _ = ratio_group_variance(df_pilot["num"], df_pilot["den"])
276
+ elif metric_type == "share":
277
+ m_c, v_c = _share_mean_and_var(df_control)
278
+ m_e, v_e = _share_mean_and_var(df_pilot)
279
+ if np.isfinite(m_c) and np.isfinite(m_e):
280
+ out["effect_size"] = cohens_h(m_c, m_e)
281
+ out["effect_size_name"] = "cohens_h"
282
+ elif metric_type == "median":
283
+ out["effect_size"] = cliffs_delta(df_control["value"], df_pilot["value"])
284
+ out["effect_size_name"] = "cliffs_delta"
285
+ return out
286
+ else:
287
+ return out
288
+ out["rel_ci_lower"], out["rel_ci_upper"] = relative_lift_ci(m_c, v_c, m_e, v_e, alpha)
289
+ return out