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.
- fast_exp_analytics/__init__.py +156 -0
- fast_exp_analytics/_extensions.py +289 -0
- fast_exp_analytics/ab.py +806 -0
- fast_exp_analytics/abc.py +891 -0
- fast_exp_analytics/bayesian.py +91 -0
- fast_exp_analytics/confidence_intervals.py +192 -0
- fast_exp_analytics/config.py +49 -0
- fast_exp_analytics/datasets.py +97 -0
- fast_exp_analytics/diagnostics.py +389 -0
- fast_exp_analytics/duration.py +669 -0
- fast_exp_analytics/effect_sizes.py +111 -0
- fast_exp_analytics/exporters.py +180 -0
- fast_exp_analytics/exporters_ab.py +264 -0
- fast_exp_analytics/exporters_duration.py +375 -0
- fast_exp_analytics/llm.py +29 -0
- fast_exp_analytics/messaging.py +48 -0
- fast_exp_analytics/multi_group.py +337 -0
- fast_exp_analytics/multiple_testing.py +56 -0
- fast_exp_analytics/power.py +188 -0
- fast_exp_analytics/py.typed +0 -0
- fast_exp_analytics/reporting.py +370 -0
- fast_exp_analytics/reporting_ab.py +405 -0
- fast_exp_analytics/sequential.py +216 -0
- fast_exp_analytics/stat_tests.py +1005 -0
- fast_exp_analytics/variance_reduction.py +208 -0
- fast_exp_analytics-0.3.0.dist-info/METADATA +597 -0
- fast_exp_analytics-0.3.0.dist-info/RECORD +30 -0
- fast_exp_analytics-0.3.0.dist-info/WHEEL +5 -0
- fast_exp_analytics-0.3.0.dist-info/licenses/LICENSE +21 -0
- fast_exp_analytics-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -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
|