From ae746ad4a5e846753a107e662b63e34abfd97387 Mon Sep 17 00:00:00 2001 From: Dmatryus Date: Fri, 18 Apr 2025 18:19:49 +0300 Subject: [PATCH 01/17] Base executors in the executors family scheme. --- schemes/executors_famaly.dot | 152 +++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 schemes/executors_famaly.dot diff --git a/schemes/executors_famaly.dot b/schemes/executors_famaly.dot new file mode 100644 index 00000000..93b2774e --- /dev/null +++ b/schemes/executors_famaly.dot @@ -0,0 +1,152 @@ +digraph Architecture{ + rankdir=LR + node[style="filled"] + compound=true + + subgraph atributes{ + node[shape=note, fillcolor=darkseagreen1] + id + role + n_iterations + searching_role + params + } + + subgraph functions{ + node[shape=box, fillcolor=skyblue2] + execute + calc + check_rule + set_params + } + + subgraph objects{ + node[shape=box3d, fillcolor=fuchsia] + if_executor + else_executor + reporter + stopping_criterion + } + + subgraph collections{ + node[shape=folder, fillcolor=lightgoldenrod1] + executors + } + + subgraph cluster_abstract_executors{ + graph[label="Abstract Executors", style="dashed"] + + subgraph cluster_executor{ + graph[label="Executor", style="solid"] + id + execute + } + + subgraph cluster_calculator{ + graph[label="Calculator", style="solid"] + calc + } + calc -> execute [ + arrowhead=curve + ltail=cluster_calculator + lhead=cluster_executor + ] + + subgraph cluster_if_executor{ + graph[label="IfExecutor", style="solid"] + check_rule + if_executor + else_executor + } + check_rule -> execute [ + arrowhead=curve + ltail=cluster_if_executor + lhead=cluster_executor + ] + execute -> {if_executor, else_executor} [ + style=dashed + arrowhead=box + ltail=cluster_executor + ] + } + + subgraph cluster_experiments{ + graph[label="Experiment Types", style="dashed"] + + subgraph cluster_experiment{ + graph[label="Experiment", style="solid"] + executors + set_params + } + executors -> execute [ + arrowhead=curve + ltail=cluster_experiment + lhead=cluster_executor + ] + + subgraph cluster_on_role_experiment{ + graph[label="OnRoleExperiment", style="solid"] + role + } + role -> executors [ + arrowhead=curve + ltail=cluster_on_role_experiment + lhead=cluster_experiment + ] + + subgraph cluster_experiment_with_reporter{ + graph[label="ExperimentWithReporter", style="solid"] + reporter + } + reporter -> executors [ + arrowhead=curve + ltail=cluster_experiment_with_reporter + lhead=cluster_experiment + ] + + subgraph cluster_cycled_experiment{ + graph[label="CycledExperiment", style="solid"] + n_iterations + } + n_iterations -> reporter [ + arrowhead=curve + ltail=cluster_cycled_experiment + lhead=cluster_experiment_with_reporter + ] + + subgraph cluster_group_experiment{ + graph[label="GroupExperiment", style="solid"] + searching_role + } + searching_role -> reporter [ + arrowhead=curve + ltail=cluster_group_experiment + lhead=cluster_experiment_with_reporter + ] + + subgraph cluster_params_experiment{ + graph[label="ParamsExperiment", style="solid"] + params + } + params -> reporter [ + arrowhead=curve + ltail=cluster_params_experiment + lhead=cluster_experiment_with_reporter + ] + + subgraph cluster_if_params_experiment{ + graph[label="IfParamsExperiment", style="solid"] + stopping_criterion + } + stopping_criterion -> params [ + arrowhead=curve + ltail=cluster_if_params_experiment + lhead=cluster_params_experiment + ] + check_rule -> stopping_criterion [ + style=dashed + arrowhead=box + ltail=cluster_if_executor + ] + } +} \ No newline at end of file From 9bb13e4846ef64c6c121707ca6393861e5e3449f Mon Sep 17 00:00:00 2001 From: Dmatryus Date: Mon, 21 Apr 2025 05:37:16 +0300 Subject: [PATCH 02/17] Add analyzers in executors_famaly.dot --- schemes/executors_famaly.dot | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/schemes/executors_famaly.dot b/schemes/executors_famaly.dot index 93b2774e..8ebbcce0 100644 --- a/schemes/executors_famaly.dot +++ b/schemes/executors_famaly.dot @@ -26,6 +26,12 @@ digraph Architecture{ else_executor reporter stopping_criterion + // Analyzers---------------- + analyzer[label="Analyzer"] + one_aa_analyzer[label="OneAAAnalyzer"] + aa_score_analyzer[label="AAScoreAnalyzer"] + ab_analyzer[label="ABAnalyzer"] + matching_analyzer[label="MatchingAnalyzer"] } subgraph collections{ @@ -149,4 +155,33 @@ digraph Architecture{ ltail=cluster_if_executor ] } + + subgraph cluster_analyzers{ + graph[label="Analyzers Ierarchy", style="dashed"] + analyzer + subgraph cluster_aa_analyzers{ + graph[label="AA", style="dashed"] + one_aa_analyzer + aa_score_analyzer + } + subgraph cluster_ab_analyzers{ + graph[label="AB", style="dashed"] + ab_analyzer + } + subgraph cluster_matching_analyzers{ + graph[label="Matching", style="dashed"] + matching_analyzer + } + analyzer -> execute [ + arrowhead=curve + lhead=cluster_executor + ] + + { + one_aa_analyzer, + aa_score_analyzer, + ab_analyzer, + matching_analyzer + } -> analyzer [arrowhead=curve] + } } \ No newline at end of file From 36f141b66900be122a02c62caacd41ea1647f82b Mon Sep 17 00:00:00 2001 From: Dmatryus Date: Mon, 21 Apr 2025 09:06:10 +0300 Subject: [PATCH 03/17] Start abstract comporators --- schemes/executors_famaly.dot | 50 +++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/schemes/executors_famaly.dot b/schemes/executors_famaly.dot index 8ebbcce0..e3595e13 100644 --- a/schemes/executors_famaly.dot +++ b/schemes/executors_famaly.dot @@ -10,6 +10,11 @@ digraph Architecture{ n_iterations searching_role params + compare_by + groping_reole, + target_roles, + baseline_role + reliability } subgraph functions{ @@ -18,6 +23,7 @@ digraph Architecture{ calc check_rule set_params + _split_data_to_buckets } subgraph objects{ @@ -27,11 +33,11 @@ digraph Architecture{ reporter stopping_criterion // Analyzers---------------- - analyzer[label="Analyzer"] - one_aa_analyzer[label="OneAAAnalyzer"] - aa_score_analyzer[label="AAScoreAnalyzer"] - ab_analyzer[label="ABAnalyzer"] - matching_analyzer[label="MatchingAnalyzer"] + analyzer[label="Analyzer", fillcolor=mintcream] + one_aa_analyzer[label="OneAAAnalyzer", fillcolor=mintcream] + aa_score_analyzer[label="AAScoreAnalyzer", fillcolor=mintcream] + ab_analyzer[label="ABAnalyzer", fillcolor=mintcream] + matching_analyzer[label="MatchingAnalyzer", fillcolor=mintcream] } subgraph collections{ @@ -157,7 +163,7 @@ digraph Architecture{ } subgraph cluster_analyzers{ - graph[label="Analyzers Ierarchy", style="dashed"] + graph[label="Analyzers", style="dashed"] analyzer subgraph cluster_aa_analyzers{ graph[label="AA", style="dashed"] @@ -184,4 +190,36 @@ digraph Architecture{ matching_analyzer } -> analyzer [arrowhead=curve] } + + subgraph cluster_comparators{ + graph[label="Comparators", style="dashed"] + + subgraph cluster_abstract_comporators{ + graph[label="Abstract", style="dotted"] + + subgraph cluster_comporator{ + graph[label="Comparator", style="solid"] + _split_data_to_buckets + compare_by + groping_reole + target_roles + baseline_role + } + _split_data_to_buckets -> calc [ + arrowhead=curve + ltail=cluster_comporator + lhead=cluster_calculator + ] + + subgraph cluster_stat_hyp_testing{ + graph[label="StatHypothesisTesting", style="solid"] + reliability + } + reliability -> compare_by [ + arrowhead=curve + ltail=cluster_stat_hyp_testing + lhead=cluster_comporator + ] + } + } } \ No newline at end of file From 5b26c6f17b8e200ce772c79a18e4f3891a9cc154 Mon Sep 17 00:00:00 2001 From: Dmatryus Date: Mon, 21 Apr 2025 09:17:18 +0300 Subject: [PATCH 04/17] Abstract comporators are finished --- schemes/executors_famaly.dot | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/schemes/executors_famaly.dot b/schemes/executors_famaly.dot index e3595e13..bfa43884 100644 --- a/schemes/executors_famaly.dot +++ b/schemes/executors_famaly.dot @@ -15,6 +15,8 @@ digraph Architecture{ target_roles, baseline_role reliability + significance, + power } subgraph functions{ @@ -220,6 +222,17 @@ digraph Architecture{ ltail=cluster_stat_hyp_testing lhead=cluster_comporator ] + + subgraph cluster_power_testing{ + graph[label="PowerTesting", style="solid"] + significance + power + } + power -> compare_by [ + arrowhead=curve + ltail=cluster_power_testing + lhead=cluster_comporator + ] } } } \ No newline at end of file From ce4081ea43a14d97166745c298e4ddc7d12f7de0 Mon Sep 17 00:00:00 2001 From: Dmatryus Date: Mon, 21 Apr 2025 09:43:49 +0300 Subject: [PATCH 05/17] Comporators are finished --- schemes/executors_famaly.dot | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/schemes/executors_famaly.dot b/schemes/executors_famaly.dot index bfa43884..605e4036 100644 --- a/schemes/executors_famaly.dot +++ b/schemes/executors_famaly.dot @@ -40,6 +40,16 @@ digraph Architecture{ aa_score_analyzer[label="AAScoreAnalyzer", fillcolor=mintcream] ab_analyzer[label="ABAnalyzer", fillcolor=mintcream] matching_analyzer[label="MatchingAnalyzer", fillcolor=mintcream] + // Comporators------------- + group_diff[label="GroupDifference", fillcolor=mintcream] + group_sizes[label="GroupSizes", fillcolor=mintcream] + psi[label="PSI", fillcolor=mintcream] + maha_dist[label="MahalanobisDistance", fillcolor=mintcream] + ttest[label="TTest", fillcolor=mintcream] + kstest[label="KSTest", fillcolor=mintcream] + utest[label="UTest", fillcolor=mintcream] + chi2test[label="Chi2Test", fillcolor=mintcream] + mdebysize[label="MDEBySize", fillcolor=mintcream] } subgraph collections{ @@ -234,5 +244,33 @@ digraph Architecture{ lhead=cluster_comporator ] } + + { + group_diff, + group_sizes + psi + maha_dist + } -> compare_by [ + arrowhead=curve + lhead=cluster_comporator + ] + + subgraph cluster_hyp_testing{ + graph[label="Hypothesis testing", style=dotted] + { + ttest + kstest + utest + chi2test + } -> reliability [ + arrowhead=curve + lhead=cluster_stat_hyp_testing + ] + } + + mdebysize -> power [ + arrowhead=curve + lhead=cluster_power_testing + ] } } \ No newline at end of file From 56b7e08b266e71d68d93f25324c3acd871d838a1 Mon Sep 17 00:00:00 2001 From: Dmatryus Date: Wed, 23 Apr 2025 15:20:27 +0300 Subject: [PATCH 06/17] Add Data, Encoders and Extensions --- schemes/executors_famaly.dot | 236 +++++++++++++++++++++++++++++++++-- 1 file changed, 223 insertions(+), 13 deletions(-) diff --git a/schemes/executors_famaly.dot b/schemes/executors_famaly.dot index 605e4036..23383fcc 100644 --- a/schemes/executors_famaly.dot +++ b/schemes/executors_famaly.dot @@ -11,12 +11,19 @@ digraph Architecture{ searching_role params compare_by - groping_reole, - target_roles, + groping_reole + target_roles_comporator[label="target_roles"] baseline_role reliability - significance, + significance power + target_roles_encoder[label="target_roles"] + method + alpha + alpha_q[label="alpha"] + iteration_size + equal_variance + random_state_q[label="random_state"] } subgraph functions{ @@ -26,14 +33,25 @@ digraph Architecture{ check_rule set_params _split_data_to_buckets + fit + predict + score + calc_ext[label="calc"] + _calc_pandas + fit_ext[label="fit"] + predict_ext[label="predict"] + test_function + reliability_ext[label="reliability"] + matrix_preparation } subgraph objects{ - node[shape=box3d, fillcolor=fuchsia] + node[shape=box3d, fillcolor=orchid2] if_executor else_executor reporter stopping_criterion + dummy_encoder[label="DummyEncoder", fillcolor=mintcream] // Analyzers---------------- analyzer[label="Analyzer", fillcolor=mintcream] one_aa_analyzer[label="OneAAAnalyzer", fillcolor=mintcream] @@ -50,22 +68,41 @@ digraph Architecture{ utest[label="UTest", fillcolor=mintcream] chi2test[label="Chi2Test", fillcolor=mintcream] mdebysize[label="MDEBySize", fillcolor=mintcream] + // Dataset----------------- + backend + data_pandas[label="data: pd.DataFrame"] + abc_role[label="ABCRole", fillcolor=mintcream] + // Extensions-------------- + compare_extension[label="CompareExtension", fillcolor=mintcream] + dummy_encoder_ext[label="DummyEncoderExtension", fillcolor=mintcream] + faiss_ext[label="FaissExtension", fillcolor=mintcream] + cholesky_ext[label="CholeskyExtension", fillcolor=mintcream] + inverse_ext[label="InverseExtension", fillcolor=mintcream] + ttest_ext[label="TTestExtension", fillcolor=mintcream] + kstest_ext[label="KSTestExtension", fillcolor=mintcream] + utest_ext[label="UtestExtension", fillcolor=mintcream] + norm_cdf_ext[label="NormCDFExtension", fillcolor=mintcream] } subgraph collections{ node[shape=folder, fillcolor=lightgoldenrod1] executors + roles + data_manipulations_functios[label="Data Manipulation Functions", fillcolor=lightcoral] + calculation_functions[label="Calculation Functions", fillcolor=lightcoral] + pandas_dataset_backend_functions[label="Pandas Dataset Backend Functions", fillcolor=lightcoral] + navigation_backend_calling_functions[label="Navigation Backend Calling Functions", fillcolor=lightcoral] + calculation_functions_via_backend[label="Calculation Functions via Backend", fillcolor=lightcoral] } subgraph cluster_abstract_executors{ graph[label="Abstract Executors", style="dashed"] - subgraph cluster_executor{ - graph[label="Executor", style="solid"] - id - execute + subgraph cluster_executor{ + graph[label="Executor", style="solid"] + id + execute } - subgraph cluster_calculator{ graph[label="Calculator", style="solid"] calc @@ -75,13 +112,19 @@ digraph Architecture{ ltail=cluster_calculator lhead=cluster_executor ] - subgraph cluster_if_executor{ graph[label="IfExecutor", style="solid"] check_rule if_executor else_executor } + subgraph cluster_ml_executor{ + graph[label="MLExecutor", style="solid"] + fit + predict + score + } + check_rule -> execute [ arrowhead=curve ltail=cluster_if_executor @@ -92,6 +135,11 @@ digraph Architecture{ arrowhead=box ltail=cluster_executor ] + fit -> calc [ + arrowhead=curve + ltail=cluster_ml_executor + lhead=cluster_calculator + ] } subgraph cluster_experiments{ @@ -107,6 +155,10 @@ digraph Architecture{ ltail=cluster_experiment lhead=cluster_executor ] + executors -> execute [ + arrowhead=box + lhead=cluster_executor + ] subgraph cluster_on_role_experiment{ graph[label="OnRoleExperiment", style="solid"] @@ -167,10 +219,10 @@ digraph Architecture{ ltail=cluster_if_params_experiment lhead=cluster_params_experiment ] - check_rule -> stopping_criterion [ + stopping_criterion -> check_rule [ style=dashed arrowhead=box - ltail=cluster_if_executor + lhead=cluster_if_executor ] } @@ -214,7 +266,7 @@ digraph Architecture{ _split_data_to_buckets compare_by groping_reole - target_roles + target_roles_comporator baseline_role } _split_data_to_buckets -> calc [ @@ -273,4 +325,162 @@ digraph Architecture{ lhead=cluster_power_testing ] } + + subgraph cluster_data{ + graph[label="Data", style=dashed] + subgraph cluster_dataset_backend_navigation{ + graph[label="DatasetBackendNavigation", style=solid] + data_manipulations_functios + } + subgraph cluster_dataset_backend_calc{ + graph[label="DatasetBackendCalc", style=solid] + calculation_functions + } + subgraph cluster_pandas_backend{ + graph[label="PandasBackend", style=solid] + pandas_dataset_backend_functions + data_pandas + } + subgraph cluster_dataset_base{ + graph[label="DatasetBase", style=solid] + backend + roles + navigation_backend_calling_functions + } + subgraph cluster_dataset{ + graph[label="Dataset", style=solid] + calculation_functions_via_backend + } + + calculation_functions -> data_manipulations_functios [ + arrowhead=curve, + ltail=cluster_dataset_backend_calc + lhead=cluster_dataset_backend_navigation + ] + pandas_dataset_backend_functions -> calculation_functions [ + arrowhead=curve, + ltail=cluster_pandas_backend + lhead=cluster_dataset_backend_calc + ] + backend -> calculation_functions [ + arrowhead=box, + lhead=cluster_dataset_backend_calc + ] + calculation_functions_via_backend -> navigation_backend_calling_functions [ + arrowhead=curve, + ltail=cluster_dataset + lhead=cluster_dataset_base + ] + roles -> abc_role[arrowhead=box] + } + + subgraph cluster_encoders{ + graph[label="Encoders", style=dashed] + subgraph cluster_encoder{ + graph[label="Encoder", style=solid] + target_roles_encoder + } + target_roles_encoder -> calc [ + arrowhead=curve, + ltail=cluster_encoder, + lhead=cluster_calculator + ] + dummy_encoder -> target_roles_encoder [ + arrowhead=curve, + lhead=cluster_encoder + ] + } + + subgraph cluster_extensions{ + graph[label="Extensions", style=dashed] + subgraph cluster_abstract_extensions{ + graph[label="Abstract", style=dotted] + + subgraph cluster_extension{ + graph[label="Extension", style=solid] + calc_ext + _calc_pandas + } + subgraph cluster_ml_extension{ + graph[label="MLExtension", style=solid] + fit_ext + predict_ext + } + + compare_extension -> calc_ext [ + arrowhead=curve + lhead=cluster_extension + ] + fit_ext -> calc_ext [ + arrowhead=curve + lhead=cluster_extension + ltail=cluster_ml_extension + ] + } + + subgraph cluster_stat_test{ + graph[label="StatTest", style=solid] + test_function + reliability_ext + } + subgraph cluster_chi2test_ext{ + graph[label="Chi2Test", style=solid] + matrix_preparation + } + subgraph cluster_multitest{ + graph[label="MultiTest", style=solid] + compare_extension + method + alpha + } + subgraph cluster_multitest_q{ + graph[label="MultiTestQuantile", style=solid] + alpha_q + iteration_size + equal_variance + random_state_q + } + + { + dummy_encoder_ext + inverse_ext + cholesky_ext + } -> calc_ext [ + arrowhead=curve + lhead=cluster_extension + ] + faiss_ext -> fit_ext [ + arrowhead=curve + lhead=cluster_ml_extension + ] + test_function -> compare_extension [ + arrowhead=curve + ltail=cluster_stat_test + ] + { + ttest_ext + kstest_ext + utest_ext + norm_cdf_ext + } -> test_function [ + arrowhead=curve + lhead=cluster_stat_test + ] + matrix_preparation -> test_function [ + arrowhead=curve + ltail=cluster_chi2test_ext + lhead=cluster_stat_test + ] + method -> calc_ext [ + arrowhead=curve + ltail=cluster_multitest + lhead=cluster_extension + ] + iteration_size -> calc_ext [ + arrowhead=curve + ltail=cluster_multitest_q + lhead=cluster_extension + ] + } + } \ No newline at end of file From d9dae9b778c0c2ea1c2f08b0af705b7cef10b500 Mon Sep 17 00:00:00 2001 From: Dmatryus Date: Wed, 7 May 2025 08:27:43 +0300 Subject: [PATCH 07/17] Add forks --- schemes/executors_famaly.dot | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/schemes/executors_famaly.dot b/schemes/executors_famaly.dot index 23383fcc..b3279ec6 100644 --- a/schemes/executors_famaly.dot +++ b/schemes/executors_famaly.dot @@ -24,6 +24,7 @@ digraph Architecture{ iteration_size equal_variance random_state_q[label="random_state"] + sample_size } subgraph functions{ @@ -483,4 +484,18 @@ digraph Architecture{ ] } + subgraph cluster_forks{ + graph[label="Forks", style=dashed] + + subgraph cluster_if_aa_executor{ + graph[label="IfAAExecutor", style=solid] + sample_size + } + } + sample_size -> if_executor [ + arrowhead=curve + ltail=cluster_if_aa_executor + lhead=cluster_if_executor + ] + } \ No newline at end of file From e0d28d7dc5d0a9e8841e47342fd17ae032aac68a Mon Sep 17 00:00:00 2001 From: Dmatryus Date: Fri, 13 Jun 2025 19:25:49 +0300 Subject: [PATCH 08/17] Micro refactor and new schemes: * ml * operators * reporters --- hypex/reporters/aa.py | 5 +- hypex/reporters/matching.py | 3 +- schemes/executors_famaly.dot | 200 +++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 5 deletions(-) diff --git a/hypex/reporters/aa.py b/hypex/reporters/aa.py index 9c63fa70..585338e4 100644 --- a/hypex/reporters/aa.py +++ b/hypex/reporters/aa.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib from typing import Any, ClassVar from ..comparators import Chi2Test, GroupDifference, GroupSizes, KSTest, TTest @@ -29,10 +30,8 @@ def convert_flat_dataset(data: dict) -> Dataset: @staticmethod def get_splitter_id(data: ExperimentData): for c in [AASplitter, AASplitterWithStratification]: - try: + with contextlib.suppress(NotFoundInExperimentDataError): return data.get_one_id(c, ExperimentDataEnum.additional_fields) - except NotFoundInExperimentDataError: - pass # The splitting was done by another class def extract_group_difference(self, data: ExperimentData) -> dict[str, Any]: group_difference_ids = data.get_ids(GroupDifference)[GroupDifference.__name__][ diff --git a/hypex/reporters/matching.py b/hypex/reporters/matching.py index c7274e9c..337f909b 100644 --- a/hypex/reporters/matching.py +++ b/hypex/reporters/matching.py @@ -79,8 +79,7 @@ def report(self, data: ExperimentData): self.front = False dict_report = super().report(data) self.front = front_buffer - result = self.convert_flat_dataset(dict_report) - return result + return self.convert_flat_dataset(dict_report) class MatchingDatasetReporter(DatasetReporter): diff --git a/schemes/executors_famaly.dot b/schemes/executors_famaly.dot index b3279ec6..03aa97a3 100644 --- a/schemes/executors_famaly.dot +++ b/schemes/executors_famaly.dot @@ -25,6 +25,12 @@ digraph Architecture{ equal_variance random_state_q[label="random_state"] sample_size + n_neighbors + two_sides + test_pairs + faiss_mode + matching_metric[label="metric"] + one_aa_dict_reporter_tests[label="tests"] } subgraph functions{ @@ -44,6 +50,17 @@ digraph Architecture{ test_function reliability_ext[label="reliability"] matrix_preparation + report + extract_from_one_row_dataset + convert_to_dataset + extract_tests + convert_flat_dataset + get_splitter_id + extract_group_difference + extract_group_sizes + extract_analyzer_data_aa[label="extract_analyzer_data"] + extract_data_from_analysis_tables_aa[label="extract_data_from_analysis_tables"] + } subgraph objects{ @@ -69,6 +86,9 @@ digraph Architecture{ utest[label="UTest", fillcolor=mintcream] chi2test[label="Chi2Test", fillcolor=mintcream] mdebysize[label="MDEBySize", fillcolor=mintcream] + group_operator[label="GroupOperator", fillcolor=mintcream] + smd[label="SMD", fillcolor=mintcream] + bias[label="Bias", fillcolor=mintcream] // Dataset----------------- backend data_pandas[label="data: pd.DataFrame"] @@ -83,6 +103,19 @@ digraph Architecture{ kstest_ext[label="KSTestExtension", fillcolor=mintcream] utest_ext[label="UtestExtension", fillcolor=mintcream] norm_cdf_ext[label="NormCDFExtension", fillcolor=mintcream] + // Reporters-------------- + on_dict_reporter[label="OnDictReporter", fillcolor=mintcream] + aa_dataset_reporter[label="AADatasetReporter", fillcolor=mintcream] + aa_passed_reporter[label="AAPassedReporter", fillcolor=mintcream] + aa_best_split_reporter[label="AABestSplitReporter", fillcolor=mintcream] + ab_dict_rporter[label="ABDictReporter", fillcolor=mintcream] + ab_dataset_reporter[label="ABDatasetReporter", fillcolor=mintcream] + homo_dict_reporter[label="HomoDictReporter", fillcolor=mintcream] + homo_dataset_reporter[label="HomoDatasetReporter", fillcolor=mintcream] + matching_dict_reporter[label="MatchingDictReporter", fillcolor=mintcream] + matching_quality_dict_reporter[label="MatchingQualityDictReporter", fillcolor=mintcream] + matching_quality_dataset_reporter[label="MatchingQualityDatasetReporter", fillcolor=mintcream] + matching_dataset_reporter[label="MatchingDatasetReporter", fillcolor=mintcream] } subgraph collections{ @@ -498,4 +531,171 @@ digraph Architecture{ lhead=cluster_if_executor ] + subgraph cluster_ml{ + graph[label="ML", style=dashed] + + subgraph cluster_faiss{ + graph[label="Faiss", style=solid] + n_neighbors + two_sides + test_pairs + faiss_mode + } + } + faiss_ext -> faiss_mode[ + arrowhead=box + lhead=cluster_faiss + ] + faiss_mode -> fit[ + arrowhead=curve, + ltail=cluster_faiss + lhead=cluster_ml_executor + ] + + subgraph cluster_operators{ + graph[label="Operators", style=dashed] + + subgraph cluster_matching_metrics{ + graph[label="MatchingMetrics", style=solid] + matching_metric + } + + {smd, bias} -> group_operator [arrowhead=curve] + matching_metric -> group_operator [ + arrowhead=curve, + ltail=cluster_matching_metrics + ] + + group_operator -> calc [ + arrowhead=curve, + lhead=cluster_calculator + ] + } + + subgraph cluster_reporters{ + graph[label="Reporters", style=dashed] + + subgraph cluster_reporter{ + graph[label="Reporter", style=solid] + report + } + + subgraph cluster_dict_reporter{ + graph[label="DictReporter", style=solid] + extract_from_one_row_dataset + } + on_dict_reporter + + subgraph cluster_dataset_reporter{ + graph[label="DatasetReporter", style=solid] + convert_to_dataset + } + + subgraph cluster_test_dict_reporter{ + graph[label="TestDictReporter", style=solid] + extract_tests + } + + subgraph cluster_one_aa_dict_reporter{ + graph[label="OneAADictReporter", style=solid] + one_aa_dict_reporter_tests + convert_flat_dataset + get_splitter_id + extract_group_difference + extract_group_sizes + extract_analyzer_data_aa + extract_data_from_analysis_tables_aa + } + + aa_dataset_reporter + aa_passed_reporter + aa_best_split_reporter + ab_dataset_reporter + + matching_dict_reporter + matching_quality_dict_reporter + matching_quality_dataset_reporter + matching_dataset_reporter + + extract_from_one_row_dataset -> report [ + arrowhead=curve, + ltail=cluster_dict_reporter, + lhead=cluster_reporter + ] + + + on_dict_reporter -> report [ + arrowhead=curve, + lhead=cluster_reporter + ] + + convert_to_dataset -> on_dict_reporter [ + arrowhead=curve, + ltail=cluster_dataset_reporter, + ] + + extract_tests -> extract_from_one_row_dataset [ + arrowhead=curve, + ltail=cluster_test_dict_reporter, + lhead=cluster_dict_reporter + ] + + one_aa_dict_reporter_tests -> extract_tests [ + arrowhead=curve, + ltail=cluster_one_aa_dict_reporter, + lhead=cluster_test_dict_reporter + ] + + aa_dataset_reporter -> one_aa_dict_reporter_tests [ + arrowhead=curve, + lhead=cluster_one_aa_dict_reporter + ] + + aa_passed_reporter -> report [ + arrowhead=curve, + lhead=cluster_reporter + ] + + aa_best_split_reporter -> report [ + arrowhead=curve, + lhead=cluster_reporter + ] + + ab_dict_rporter -> one_aa_dict_reporter_tests [ + arrowhead=curve, + ltail=cluster_ab_dict_reporter + lhead=cluster_one_aa_dict_reporter + ] + + ab_dataset_reporter -> ab_dict_rporter [arrowhead=curve] + + homo_dict_reporter -> one_aa_dict_reporter_tests [ + arrowhead=curve, + lhead=cluster_one_aa_dict_reporter + ] + + homo_dataset_reporter -> convert_to_dataset [ + arrowhead=curve, + lhead=cluster_dataset_reporter + ] + homo_dict_reporter -> homo_dataset_reporter [arrowhead=box] + + matching_dict_reporter -> extract_from_one_row_dataset [ + arrowhead=curve, + lhead=cluster_dict_reporter + ] + + matching_quality_dict_reporter -> extract_tests [ + arrowhead=curve, + lhead=cluster_test_dict_reporter + ] + + matching_quality_dataset_reporter -> matching_quality_dict_reporter [arrowhead=curve] + + matching_dataset_reporter -> convert_to_dataset [ + arrowhead=curve, + lhead=cluster_dataset_reporter + ] + } + } \ No newline at end of file From b1c8ab5dfdabcbbc88faed3c0991f299b50606ee Mon Sep 17 00:00:00 2001 From: dmbulychev Date: Thu, 4 Sep 2025 06:58:29 +0300 Subject: [PATCH 09/17] New architecture sceme --- schemes/architecture.dot | 280 --------- schemes/architecture.svg | 841 --------------------------- schemes/class inheritance scheme.svg | 4 - schemes/class_path.dot | 170 ------ schemes/class_path.svg | 644 -------------------- schemes/executors_famaly.dot | 701 ---------------------- schemes/hierarchy.dot | 819 ++++++++++++++++++++++++++ 7 files changed, 819 insertions(+), 2640 deletions(-) delete mode 100644 schemes/architecture.dot delete mode 100644 schemes/architecture.svg delete mode 100644 schemes/class inheritance scheme.svg delete mode 100644 schemes/class_path.dot delete mode 100644 schemes/class_path.svg delete mode 100644 schemes/executors_famaly.dot create mode 100644 schemes/hierarchy.dot diff --git a/schemes/architecture.dot b/schemes/architecture.dot deleted file mode 100644 index 2a6fe7c2..00000000 --- a/schemes/architecture.dot +++ /dev/null @@ -1,280 +0,0 @@ -digraph Architecture{ - compound=true - node[style=filled] - - subgraph modules{ - node[shape=box3d, fillcolor=violet] - - executor_legend[label="Executor"] - // ------- - experiment_executor_list[label="List[Executor]", shape=folder] - experiment_legend[label="Experiment"] - experiment_executor_0_in_list[label="Executor 0"] - experiment_executor_1_in_list[label="Executor 1"] - experiment_executor_dotted_in_list[label="..."] - experiment_executor_n_in_list[label="Executor n"] - // ------- - multiexperiment_experiment[label="Experiment"] - multiexperiment_analyzer[label="Analyzer"] - multiexperiment[label="MultiExperiment"] - multiexperiment_execute_experiment[label="Experiment"] - multiexperiment_execute_analyzer[label="Analyzer"] - // ------- - spliter_executor[label="Spliter"] - // ------- - stat_executor[label="Stat"] - // ------- - analyzer_executor[label="Analyzer"] - // ------- - transformer_executor[label="Transformer"] - // ------- - operator_executor[label="Operator"] - // ------- - report_experiment[label="Experiment"] - } - - subgraph methods{ - node[shape=box, fillcolor=cornflowerblue] - executor_execute_legend[label="execute"] - // ------- - experiment_execute_legend[label="execute"] - experiment_execute_0[label="execute 0"] - experiment_execute_1[label="execute 1"] - experiment_execute_dotted[label="..."] - experiment_execute_n[label="execute n"] - // ------- - multiexperiment_execute[label="execute"] - multiexperiment_executor_execute[label="execute"] - multiexperiment_analyzer_execute[label="execute"] - // ------- - spliter_execute[label="execute"] - // ------- - stat_execute[label="execute"] - // ------- - analyzer_execute[label="execute"] - // ------- - transformer_execute[label="execute"] - // ------- - operator_execute[label="execute"] - // ------- - report_execute[label="execute"] - } - - subgraph data{ - node[shape=note, fillcolor=lightgreen] - - executor_legend_data[label="Dataset"] - // ------- - experiment_legend_data[label="Dataset"] - experiment_data_0[label="ExperimentData 0"] - experiment_data_1[label="ExperimentData 1"] - experiment_data_n[label="ExperimentData n-1"] - experiment_data_final[label="ExperimentData n"] - // ------- - multiexperiment_data[label="Dataset"] - multiexperiment_executor_experiment_data[label="ExperimentData i"] - multiexperiment_executor_result_data[label="ExperimentData"] - // ------- - spliter_data[label="Dataset"] - spliter_out_data[shape=record label="Dataset | group column"] - // ------- - stat_data[shape=record label="Dataset | stat target column"] - stat_experiment_data[label="ExperimentData"] - // ------- - analyzer_in_data[label="ExperimentData"] - analyzer_out_data[label="ExperimentData"] - // ------- - transformer_in_data[label="Dataset"] - transformer_out_data[label="Dataset"] - // ------- - operator_in_data[shape=record label="Dataset | x1 | x2"] - operator_out_data[label="ExperimentData"] - // ------- - report_in_data[label="ExperimentData"] - report_out_data[label="Report artifact"] - } - - subgraph attributes{ - node[shape=record, fillcolor=lightpink] - experiment_executor_list_attribute[label="List[Executor]"] - report_backend[label="Backend"] - } - - subgraph cluster_executors{ - graph[label="Executors"] - - subgraph cluster_executor{ - graph[style=dashed, label="Executor"] - - executor_legend_data -> executor_execute_legend - executor_legend -> executor_execute_legend [arrowhead=none] - } - - subgraph cluster_spliter{ - graph[style=dashed, label="Spliter"] - - spliter_executor -> spliter_execute [arrowhead=none] - spliter_execute -> spliter_out_data - spliter_data -> spliter_execute - } - - executor_legend -> spliter_executor [ - style="dotted" - arrowhead=curve - ltail=cluster_executor - lhead=cluster_spliter - ] - - subgraph cluster_stats{ - graph[style=dashed, label="Stats"] - - stat_executor -> stat_execute [arrowhead=none] - stat_data -> stat_execute - stat_execute -> stat_experiment_data - } - - executor_legend -> stat_executor [ - style="dotted" - arrowhead=curve - ltail=cluster_executor - lhead=cluster_stats - ] - - subgraph cluster_analyser{ - graph[style=dashed, label="Analyser"] - - analyzer_executor -> analyzer_execute [arrowhead=none] - analyzer_in_data -> analyzer_execute - analyzer_execute -> analyzer_out_data - } - - executor_legend -> analyzer_executor [ - style="dotted" - arrowhead=curve - ltail=cluster_executor - lhead=cluster_analyser - ] - - subgraph cluster_transformer{ - graph[style=dashed, label="Transformer"] - - transformer_executor -> transformer_execute [arrowhead=none] - transformer_in_data -> transformer_execute - transformer_execute -> transformer_out_data - } - - executor_legend -> transformer_executor [ - style="dotted" - arrowhead=curve - ltail=cluster_executor - lhead=cluster_transformer - ] - - subgraph cluster_operator{ - graph[style=dashed, label="Operator"] - - operator_executor -> operator_execute [arrowhead=none] - operator_in_data -> operator_execute - operator_execute -> operator_out_data - } - - executor_legend -> operator_executor [ - style="dotted" - arrowhead=curve - ltail=cluster_executor - lhead=cluster_operator - ] - - } - - subgraph cluster_experiment{ - graph[style=dashed, label="Experiment"] - experiment_executor_list -> experiment_executor_list_attribute - experiment_executor_list_attribute -> experiment_legend [arrowhead=none] - experiment_legend -> experiment_execute_legend [arrowhead=none] - experiment_legend_data -> experiment_execute_legend - experiment_legend_data -> experiment_execute_0 [style=dotted] - - subgraph cluster_experiment_executor{ - graph[style=dotted, label="execute"] - experiment_executor_0_in_list -> experiment_execute_0 [arrowhead=none] - experiment_execute_0 -> experiment_data_0 -> experiment_execute_1 - experiment_executor_1_in_list -> experiment_execute_1 [arrowhead=none] - experiment_execute_1 -> experiment_data_1 -> experiment_execute_dotted - experiment_executor_dotted_in_list -> experiment_execute_dotted [arrowhead=none] - experiment_execute_dotted -> experiment_data_n -> experiment_execute_n - experiment_executor_n_in_list -> experiment_execute_n [arrowhead=none] - experiment_execute_n -> experiment_data_final - } - - experiment_execute_legend -> experiment_execute_0 [ - arrowhead=none, - lhead=cluster_experiment_executor - ] - } - - executor_legend -> experiment_legend [ - style="dotted" - arrowhead=curve - ltail=cluster_executor - lhead=cluster_experiment - ] - - subgraph cluster_multiexperiment{ - graph[style=dashed, label="MultiExperiment"] - {multiexperiment_experiment multiexperiment_analyzer} -> multiexperiment [arrowhead=none] - multiexperiment -> multiexperiment_execute [arrowhead=none] - multiexperiment_data -> multiexperiment_execute - - subgraph cluster_multiexperiment_execute{ - graph[style=dotted, label="execute"] - - multiexperiment_execute_experiment -> multiexperiment_executor_execute [arrowhead=none] - multiexperiment_execute_analyzer -> multiexperiment_analyzer_execute [arrowhead=none] - multiexperiment_executor_execute -> multiexperiment_executor_experiment_data - multiexperiment_executor_experiment_data -> multiexperiment_analyzer_execute - - multiexperiment_analyzer_execute -> multiexperiment_executor_result_data - - multiexperiment_executor_result_data -> multiexperiment_executor_execute [ - style=dashed - label="n times" - ] - } - - multiexperiment_execute -> multiexperiment_executor_execute [ - arrowhead=none - lhead=cluster_multiexperiment_execute - ] - } - - analyzer_executor -> multiexperiment_analyzer [ - style="dotted" - arrowhead=curve - ltail=cluster_analyser - lhead=cluster_multiexperiment - ] - - experiment_legend -> multiexperiment [ - style="dotted" - arrowhead=curve - ltail=cluster_experiment - lhead=cluster_multiexperiment - ] - - subgraph cluster_report{ - graph[style=dashed, label="Report"] - - report_backend -> report_experiment [arrowhead=none] - report_experiment -> report_execute [arrowhead=none] - report_in_data -> report_execute - report_execute -> report_out_data - } - - experiment_legend -> report_experiment [ - style="dotted" - arrowhead=curve - ltail=cluster_experiment - lhead=cluster_report - ] -} \ No newline at end of file diff --git a/schemes/architecture.svg b/schemes/architecture.svg deleted file mode 100644 index c4bc5ed8..00000000 --- a/schemes/architecture.svg +++ /dev/null @@ -1,841 +0,0 @@ - - - - - - -Architecture - - -cluster_executors - -Executors - - -cluster_spliter - -Spliter - - -cluster_executor - -Executor - - -cluster_stats - -Stats - - -cluster_analyser - -Analyser - - -cluster_operator - -Operator - - -cluster_transformer - -Transformer - - -cluster_report - -Report - - -cluster_experiment - -Experiment - - -cluster_experiment_executor - -execute - - -cluster_multiexperiment - -MultiExperiment - - -cluster_multiexperiment_execute - -execute - - - -executor_legend - - - - -Executor - - - -experiment_legend - - - - -Experiment - - - -executor_legend->experiment_legend - - - - - - -spliter_executor - - - - -Spliter - - - -executor_legend->spliter_executor - - - - - - -stat_executor - - - - -Stat - - - -executor_legend->stat_executor - - - - - - -analyzer_executor - - - - -Analyzer - - - -executor_legend->analyzer_executor - - - - - - -transformer_executor - - - - -Transformer - - - -executor_legend->transformer_executor - - - - - - -operator_executor - - - - -Operator - - - -executor_legend->operator_executor - - - - - - -executor_execute_legend - -execute - - - -executor_legend->executor_execute_legend - - - - -experiment_executor_list - -List[Executor] - - - -experiment_executor_list_attribute - -List[Executor] - - - -experiment_executor_list->experiment_executor_list_attribute - - - - - -multiexperiment - - - - -MultiExperiment - - - -experiment_legend->multiexperiment - - - - - - -report_experiment - - - - -Experiment - - - -experiment_legend->report_experiment - - - - - - -experiment_execute_legend - -execute - - - -experiment_legend->experiment_execute_legend - - - - -experiment_executor_0_in_list - - - - -Executor 0 - - - -experiment_execute_0 - -execute 0 - - - -experiment_executor_0_in_list->experiment_execute_0 - - - - -experiment_executor_1_in_list - - - - -Executor 1 - - - -experiment_execute_1 - -execute 1 - - - -experiment_executor_1_in_list->experiment_execute_1 - - - - -experiment_executor_dotted_in_list - - - - -... - - - -experiment_execute_dotted - -... - - - -experiment_executor_dotted_in_list->experiment_execute_dotted - - - - -experiment_executor_n_in_list - - - - -Executor n - - - -experiment_execute_n - -execute n - - - -experiment_executor_n_in_list->experiment_execute_n - - - - -multiexperiment_experiment - - - - -Experiment - - - -multiexperiment_experiment->multiexperiment - - - - -multiexperiment_analyzer - - - - -Analyzer - - - -multiexperiment_analyzer->multiexperiment - - - - -multiexperiment_execute - -execute - - - -multiexperiment->multiexperiment_execute - - - - -multiexperiment_execute_experiment - - - - -Experiment - - - -multiexperiment_executor_execute - -execute - - - -multiexperiment_execute_experiment->multiexperiment_executor_execute - - - - -multiexperiment_execute_analyzer - - - - -Analyzer - - - -multiexperiment_analyzer_execute - -execute - - - -multiexperiment_execute_analyzer->multiexperiment_analyzer_execute - - - - -spliter_execute - -execute - - - -spliter_executor->spliter_execute - - - - -stat_execute - -execute - - - -stat_executor->stat_execute - - - - -analyzer_executor->multiexperiment_analyzer - - - - - - -analyzer_execute - -execute - - - -analyzer_executor->analyzer_execute - - - - -transformer_execute - -execute - - - -transformer_executor->transformer_execute - - - - -operator_execute - -execute - - - -operator_executor->operator_execute - - - - -report_execute - -execute - - - -report_experiment->report_execute - - - - -experiment_execute_legend->experiment_execute_0 - - - - -experiment_data_0 - - - -ExperimentData 0 - - - -experiment_execute_0->experiment_data_0 - - - - - -experiment_data_1 - - - -ExperimentData 1 - - - -experiment_execute_1->experiment_data_1 - - - - - -experiment_data_n - - - -ExperimentData n-1 - - - -experiment_execute_dotted->experiment_data_n - - - - - -experiment_data_final - - - -ExperimentData n - - - -experiment_execute_n->experiment_data_final - - - - - -multiexperiment_execute->multiexperiment_executor_execute - - - - -multiexperiment_executor_experiment_data - - - -ExperimentData i - - - -multiexperiment_executor_execute->multiexperiment_executor_experiment_data - - - - - -multiexperiment_executor_result_data - - - -ExperimentData - - - -multiexperiment_analyzer_execute->multiexperiment_executor_result_data - - - - - -spliter_out_data - -Dataset - -group column - - - -spliter_execute->spliter_out_data - - - - - -stat_experiment_data - - - -ExperimentData - - - -stat_execute->stat_experiment_data - - - - - -analyzer_out_data - - - -ExperimentData - - - -analyzer_execute->analyzer_out_data - - - - - -transformer_out_data - - - -Dataset - - - -transformer_execute->transformer_out_data - - - - - -operator_out_data - - - -ExperimentData - - - -operator_execute->operator_out_data - - - - - -report_out_data - - - -Report artifact - - - -report_execute->report_out_data - - - - - -executor_legend_data - - - -Dataset - - - -executor_legend_data->executor_execute_legend - - - - - -experiment_legend_data - - - -Dataset - - - -experiment_legend_data->experiment_execute_legend - - - - - -experiment_legend_data->experiment_execute_0 - - - - - -experiment_data_0->experiment_execute_1 - - - - - -experiment_data_1->experiment_execute_dotted - - - - - -experiment_data_n->experiment_execute_n - - - - - -multiexperiment_data - - - -Dataset - - - -multiexperiment_data->multiexperiment_execute - - - - - -multiexperiment_executor_experiment_data->multiexperiment_analyzer_execute - - - - - -multiexperiment_executor_result_data->multiexperiment_executor_execute - - -n times - - - -spliter_data - - - -Dataset - - - -spliter_data->spliter_execute - - - - - -stat_data - -Dataset - -stat target column - - - -stat_data->stat_execute - - - - - -analyzer_in_data - - - -ExperimentData - - - -analyzer_in_data->analyzer_execute - - - - - -transformer_in_data - - - -Dataset - - - -transformer_in_data->transformer_execute - - - - - -operator_in_data - -Dataset - -x1 - -x2 - - - -operator_in_data->operator_execute - - - - - -report_in_data - - - -ExperimentData - - - -report_in_data->report_execute - - - - - -experiment_executor_list_attribute->experiment_legend - - - - -report_backend - -Backend - - - -report_backend->report_experiment - - - - diff --git a/schemes/class inheritance scheme.svg b/schemes/class inheritance scheme.svg deleted file mode 100644 index 64f1d45d..00000000 --- a/schemes/class inheritance scheme.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
Excecutor
AASplitter
Calculator
ABC
DatasetBase
ExperimentData
Dataset
DatasetBackendNavigation
Experiment
IfExecutor
MatchingAnalyzer
ABAnalyzer
OneAAStatAnalyzer
AAScoreAnalyzer
ExperimentShell
AATest
Matchint
HomegeneityTest
ABTest
ExperimentWIthReporter
OnRoleExperiment
IfAAExecutor
Transformer
Shuffle
GroupOperator
MLExcecutor
Encoder
Comparator
MahalanobisDistance
StatHypothesisTesting
GroupDifference
GroupSizes
PSI
Chi2Test
UTest
KSTest
TTest
TestPower
MDEBySize
DummyEncoder
ParamsExperiment
CycledExperiment
GroupExperiment
IfParamsExperiment
Extension
CompareExtension
MLExtension
DummyEncoderExtension
FiasExtension
CholeskyExtension
InverseExtension
StatTest
Chi2TestExtension
UTestExtension
KSTestExtension
TTestExtension
NormCDF
MultiTest
MultiTestQuantile
FaissNearestNeighbors
Bias
MatchingMetrics
SMD
AASplitterWithStratificattion
Reporter
DictReporter
OnDictReporter
DatasetReporter
OneAADictReporter
AADatasetReporter
AAParsedReporter
AABestSplitReporter
ABDictReporter
ABDatasetReporter
HomoDictReporter
HomoDatasetReporter
MatchingDictReporter
MatchingDatasetReporter
CategoryAggregator
OutliersFilter
CorrFilter
NanFIlter
ConstFilter
CVFilter
NaFiller
Ouput
AAOutput
ABOutput
HomoOutput
MatchingOutput
\ No newline at end of file diff --git a/schemes/class_path.dot b/schemes/class_path.dot deleted file mode 100644 index ff27c479..00000000 --- a/schemes/class_path.dot +++ /dev/null @@ -1,170 +0,0 @@ -digraph ClassPath{ - compound=true - - subgraph abstracts{ - node [shape=egg style=filled fillcolor=silver] - abstract_class[label="abstract"] - abstract_object[label="abstract object"] - - Executor - AbstractHypothesis - } - - subgraph modules{ - node [shape=box style=filled fillcolor=cornflowerblue] - module - factory_legend[label="Factory"] - created_object[label="created object"] - inner_object[label="inner object"] - implementation - - - Dataset - Factory - Experiment - Pipeline - Hypothesis - - // -------------------------------------- - Matcher - ReportMatcher [label="Report"] - TransformerMatchingValidation [label="Transformer"] - MatherMatchingValidation [label="Matcher"] - ReportMatchingValidation [label="Report"] - // -------------------------------------- - SpliterAAOne[label="Spliter"] - ReportAAOneSplit [label="Report"] - ReportAABest [label="Report"] - - } - - subgraph pipelines{ - node [shape=box3d, style=filled, fillcolor=violet] - pipeline - complex_object[label="complex object"] - - // -------------------------------------- - SelectorsMatching[label="Selectors"] - StatsMatcher[label="Stats"] - MetricsMatcher[label="Metrics"] - ValidationMatching[label="Validation"] - AnalyzerMatching[label="Analyzer"] - MetricMatchingValidation [label="Metric"] - StatsMatchingValidation [label="Stats"] - // -------------------------------------- - StatsAA[label="Stats"] - SpliterAAPipeline[label="Spliter"] - AnalyzerAASplit[label="Analyzer"] - MetricsAASplit[label="Metrics"] - StatsAASplit[label="Stats"] - AnalyzerAA[label="Analyzer"] - // -------------------------------------- - SelectorAB[label="Selector"] - MetricsAB[label="Metrics"] - StatsAB[label="Stats"] - AnalyzerAB[label="Analyzer"] - ReportAB[label="Report"] - } - - subgraph cluster_legend{ - graph[label="legend", style=filled, fillcolor=whitesmoke] - - subgraph cluster_nodes{ - graph[label="nodes", style=dashed] - abstract_class - module - pipeline - } - - subgraph cluster_edges{ - graph[label="edges", style=dashed] - - factory_legend -> created_object [arrowhead=crow, label="create"] - inner_object -> complex_object [arrowhead=box, label="in"] - abstract_object -> implementation [arrowhead=curve, label="implement"] - } - - - - } - -// ----------------------------------------------------------------------------------- -// ----------------------------------------------------------------------------------- - - - Executor -> Pipeline [arrowhead=curved] - AbstractHypothesis -> Hypothesis [arrowhead=curved] - - Hypothesis -> Factory [arrowhead=box] - Factory -> Experiment [arrowhead=crow label="object"] - Factory -> Pipeline [arrowhead=crow label="object"] - - subgraph cluster_experiment{ - graph[label="experiment structure" style=dotted] - Pipeline -> Experiment [arrowhead=box] - Dataset -> Experiment [arrowhead=box] - } - -// ----------------------------------------------------------------------------------- - - subgraph cluster_matcher{ - graph[label="Matcher process", style=dashed, style=filled, fillcolor=mistyrose] - - SelectorsMatching -> Matcher - Matcher -> MetricsMatcher - Matcher -> StatsMatcher - Matcher -> ValidationMatching - { - MetricsMatcher - StatsMatcher - ValidationMatching - } -> ReportMatcher - - subgraph cluster_matching_validation{ - graph[label="Validation process" style=dotted] - TransformerMatchingValidation -> MatherMatchingValidation - MatherMatchingValidation -> - {MetricMatchingValidation StatsMatchingValidation} -> ReportMatchingValidation - ReportMatchingValidation -> TransformerMatchingValidation [label="n times"] - } - TransformerMatchingValidation -> ValidationMatching [ltail=cluster_matching_validation, arrowhead=box] - ReportMatcher -> AnalyzerMatching - } - - Experiment -> SelectorsMatching [lhead=cluster_matcher] - -// ----------------------------------------------------------------------------------- - - subgraph cluster_AA{ - graph[label="AA process", style=dashed, style=filled, fillcolor=lavender] - - subgraph cluster_split{ - graph[label="Split process" style=dotted] - SpliterAAOne -> StatsAA -> ReportAAOneSplit - ReportAAOneSplit -> SpliterAAOne [label="n times"] - } - - SpliterAAOne -> SpliterAAPipeline [arrowhead=box ltail=cluster_split] - SpliterAAPipeline -> AnalyzerAASplit - AnalyzerAASplit -> {MetricsAASplit StatsAASplit} - {MetricsAASplit StatsAASplit} -> ReportAABest - ReportAABest -> AnalyzerAA - } - - Experiment -> SpliterAAPipeline [lhead=cluster_AA] - -// ----------------------------------------------------------------------------------- - - subgraph cluster_AB{ - graph[label="AB process", style=dashed, style=filled, fillcolor=honeydew] - - SelectorAB -> MetricsAB - SelectorAB -> StatsAB - {MetricsAB StatsAB} -> AnalyzerAB - AnalyzerAB -> ReportAB - - } - - Experiment -> SelectorAB [lhead=cluster_AB] - -} \ No newline at end of file diff --git a/schemes/class_path.svg b/schemes/class_path.svg deleted file mode 100644 index 31d8e0bc..00000000 --- a/schemes/class_path.svg +++ /dev/null @@ -1,644 +0,0 @@ - - - - - - -ClassPath - - -cluster_AA - -AA process - - -cluster_split - -Split process - - -cluster_legend - -legend - - -cluster_nodes - -nodes - - -cluster_edges - -edges - - -cluster_matcher - -Matcher process - - -cluster_matching_validation - -Validation process - - -cluster_AB - -AB process - - -cluster_experiment - -experiment structure - - - -abstract_class - -abstract - - - -abstract_object - -abstract object - - - -implementation - -implementation - - - -abstract_object->implementation - - - -implement - - - -Executor - -Executor - - - -Pipeline - -Pipeline - - - -Executor->Pipeline - - - - - - -AbstractHypothesis - -AbstractHypothesis - - - -Hypothesis - -Hypothesis - - - -AbstractHypothesis->Hypothesis - - - - - - -module - -module - - - -factory_legend - -Factory - - - -created_object - -created object - - - -factory_legend->created_object - - -create - - - -inner_object - -inner object - - - -complex_object - - - - -complex object - - - -inner_object->complex_object - - - -in - - - -Dataset - -Dataset - - - -Experiment - -Experiment - - - -Dataset->Experiment - - - - - - -Factory - -Factory - - - -Factory->Experiment - - -object - - - -Factory->Pipeline - - -object - - - -SelectorsMatching - - - - -Selectors - - - -Experiment->SelectorsMatching - - - - - -SpliterAAPipeline - - - - -Spliter - - - -Experiment->SpliterAAPipeline - - - - - -SelectorAB - - - - -Selector - - - -Experiment->SelectorAB - - - - - -Pipeline->Experiment - - - - - - -Hypothesis->Factory - - - - - - -Matcher - -Matcher - - - -StatsMatcher - - - - -Stats - - - -Matcher->StatsMatcher - - - - - -MetricsMatcher - - - - -Metrics - - - -Matcher->MetricsMatcher - - - - - -ValidationMatching - - - - -Validation - - - -Matcher->ValidationMatching - - - - - -ReportMatcher - -Report - - - -AnalyzerMatching - - - - -Analyzer - - - -ReportMatcher->AnalyzerMatching - - - - - -TransformerMatchingValidation - -Transformer - - - -MatherMatchingValidation - -Matcher - - - -TransformerMatchingValidation->MatherMatchingValidation - - - - - -TransformerMatchingValidation->ValidationMatching - - - - - - -MetricMatchingValidation - - - - -Metric - - - -MatherMatchingValidation->MetricMatchingValidation - - - - - -StatsMatchingValidation - - - - -Stats - - - -MatherMatchingValidation->StatsMatchingValidation - - - - - -ReportMatchingValidation - -Report - - - -ReportMatchingValidation->TransformerMatchingValidation - - -n times - - - -SpliterAAOne - -Spliter - - - -StatsAA - - - - -Stats - - - -SpliterAAOne->StatsAA - - - - - -SpliterAAOne->SpliterAAPipeline - - - - - - -ReportAAOneSplit - -Report - - - -ReportAAOneSplit->SpliterAAOne - - -n times - - - -ReportAABest - -Report - - - -AnalyzerAA - - - - -Analyzer - - - -ReportAABest->AnalyzerAA - - - - - -pipeline - - - - -pipeline - - - -SelectorsMatching->Matcher - - - - - -StatsMatcher->ReportMatcher - - - - - -MetricsMatcher->ReportMatcher - - - - - -ValidationMatching->ReportMatcher - - - - - -MetricMatchingValidation->ReportMatchingValidation - - - - - -StatsMatchingValidation->ReportMatchingValidation - - - - - -StatsAA->ReportAAOneSplit - - - - - -AnalyzerAASplit - - - - -Analyzer - - - -SpliterAAPipeline->AnalyzerAASplit - - - - - -MetricsAASplit - - - - -Metrics - - - -AnalyzerAASplit->MetricsAASplit - - - - - -StatsAASplit - - - - -Stats - - - -AnalyzerAASplit->StatsAASplit - - - - - -MetricsAASplit->ReportAABest - - - - - -StatsAASplit->ReportAABest - - - - - -MetricsAB - - - - -Metrics - - - -SelectorAB->MetricsAB - - - - - -StatsAB - - - - -Stats - - - -SelectorAB->StatsAB - - - - - -AnalyzerAB - - - - -Analyzer - - - -MetricsAB->AnalyzerAB - - - - - -StatsAB->AnalyzerAB - - - - - -ReportAB - - - - -Report - - - -AnalyzerAB->ReportAB - - - - - diff --git a/schemes/executors_famaly.dot b/schemes/executors_famaly.dot deleted file mode 100644 index 03aa97a3..00000000 --- a/schemes/executors_famaly.dot +++ /dev/null @@ -1,701 +0,0 @@ -digraph Architecture{ - rankdir=LR - node[style="filled"] - compound=true - - subgraph atributes{ - node[shape=note, fillcolor=darkseagreen1] - id - role - n_iterations - searching_role - params - compare_by - groping_reole - target_roles_comporator[label="target_roles"] - baseline_role - reliability - significance - power - target_roles_encoder[label="target_roles"] - method - alpha - alpha_q[label="alpha"] - iteration_size - equal_variance - random_state_q[label="random_state"] - sample_size - n_neighbors - two_sides - test_pairs - faiss_mode - matching_metric[label="metric"] - one_aa_dict_reporter_tests[label="tests"] - } - - subgraph functions{ - node[shape=box, fillcolor=skyblue2] - execute - calc - check_rule - set_params - _split_data_to_buckets - fit - predict - score - calc_ext[label="calc"] - _calc_pandas - fit_ext[label="fit"] - predict_ext[label="predict"] - test_function - reliability_ext[label="reliability"] - matrix_preparation - report - extract_from_one_row_dataset - convert_to_dataset - extract_tests - convert_flat_dataset - get_splitter_id - extract_group_difference - extract_group_sizes - extract_analyzer_data_aa[label="extract_analyzer_data"] - extract_data_from_analysis_tables_aa[label="extract_data_from_analysis_tables"] - - } - - subgraph objects{ - node[shape=box3d, fillcolor=orchid2] - if_executor - else_executor - reporter - stopping_criterion - dummy_encoder[label="DummyEncoder", fillcolor=mintcream] - // Analyzers---------------- - analyzer[label="Analyzer", fillcolor=mintcream] - one_aa_analyzer[label="OneAAAnalyzer", fillcolor=mintcream] - aa_score_analyzer[label="AAScoreAnalyzer", fillcolor=mintcream] - ab_analyzer[label="ABAnalyzer", fillcolor=mintcream] - matching_analyzer[label="MatchingAnalyzer", fillcolor=mintcream] - // Comporators------------- - group_diff[label="GroupDifference", fillcolor=mintcream] - group_sizes[label="GroupSizes", fillcolor=mintcream] - psi[label="PSI", fillcolor=mintcream] - maha_dist[label="MahalanobisDistance", fillcolor=mintcream] - ttest[label="TTest", fillcolor=mintcream] - kstest[label="KSTest", fillcolor=mintcream] - utest[label="UTest", fillcolor=mintcream] - chi2test[label="Chi2Test", fillcolor=mintcream] - mdebysize[label="MDEBySize", fillcolor=mintcream] - group_operator[label="GroupOperator", fillcolor=mintcream] - smd[label="SMD", fillcolor=mintcream] - bias[label="Bias", fillcolor=mintcream] - // Dataset----------------- - backend - data_pandas[label="data: pd.DataFrame"] - abc_role[label="ABCRole", fillcolor=mintcream] - // Extensions-------------- - compare_extension[label="CompareExtension", fillcolor=mintcream] - dummy_encoder_ext[label="DummyEncoderExtension", fillcolor=mintcream] - faiss_ext[label="FaissExtension", fillcolor=mintcream] - cholesky_ext[label="CholeskyExtension", fillcolor=mintcream] - inverse_ext[label="InverseExtension", fillcolor=mintcream] - ttest_ext[label="TTestExtension", fillcolor=mintcream] - kstest_ext[label="KSTestExtension", fillcolor=mintcream] - utest_ext[label="UtestExtension", fillcolor=mintcream] - norm_cdf_ext[label="NormCDFExtension", fillcolor=mintcream] - // Reporters-------------- - on_dict_reporter[label="OnDictReporter", fillcolor=mintcream] - aa_dataset_reporter[label="AADatasetReporter", fillcolor=mintcream] - aa_passed_reporter[label="AAPassedReporter", fillcolor=mintcream] - aa_best_split_reporter[label="AABestSplitReporter", fillcolor=mintcream] - ab_dict_rporter[label="ABDictReporter", fillcolor=mintcream] - ab_dataset_reporter[label="ABDatasetReporter", fillcolor=mintcream] - homo_dict_reporter[label="HomoDictReporter", fillcolor=mintcream] - homo_dataset_reporter[label="HomoDatasetReporter", fillcolor=mintcream] - matching_dict_reporter[label="MatchingDictReporter", fillcolor=mintcream] - matching_quality_dict_reporter[label="MatchingQualityDictReporter", fillcolor=mintcream] - matching_quality_dataset_reporter[label="MatchingQualityDatasetReporter", fillcolor=mintcream] - matching_dataset_reporter[label="MatchingDatasetReporter", fillcolor=mintcream] - } - - subgraph collections{ - node[shape=folder, fillcolor=lightgoldenrod1] - executors - roles - data_manipulations_functios[label="Data Manipulation Functions", fillcolor=lightcoral] - calculation_functions[label="Calculation Functions", fillcolor=lightcoral] - pandas_dataset_backend_functions[label="Pandas Dataset Backend Functions", fillcolor=lightcoral] - navigation_backend_calling_functions[label="Navigation Backend Calling Functions", fillcolor=lightcoral] - calculation_functions_via_backend[label="Calculation Functions via Backend", fillcolor=lightcoral] - } - - subgraph cluster_abstract_executors{ - graph[label="Abstract Executors", style="dashed"] - - subgraph cluster_executor{ - graph[label="Executor", style="solid"] - id - execute - } - subgraph cluster_calculator{ - graph[label="Calculator", style="solid"] - calc - } - calc -> execute [ - arrowhead=curve - ltail=cluster_calculator - lhead=cluster_executor - ] - subgraph cluster_if_executor{ - graph[label="IfExecutor", style="solid"] - check_rule - if_executor - else_executor - } - subgraph cluster_ml_executor{ - graph[label="MLExecutor", style="solid"] - fit - predict - score - } - - check_rule -> execute [ - arrowhead=curve - ltail=cluster_if_executor - lhead=cluster_executor - ] - execute -> {if_executor, else_executor} [ - style=dashed - arrowhead=box - ltail=cluster_executor - ] - fit -> calc [ - arrowhead=curve - ltail=cluster_ml_executor - lhead=cluster_calculator - ] - } - - subgraph cluster_experiments{ - graph[label="Experiment Types", style="dashed"] - - subgraph cluster_experiment{ - graph[label="Experiment", style="solid"] - executors - set_params - } - executors -> execute [ - arrowhead=curve - ltail=cluster_experiment - lhead=cluster_executor - ] - executors -> execute [ - arrowhead=box - lhead=cluster_executor - ] - - subgraph cluster_on_role_experiment{ - graph[label="OnRoleExperiment", style="solid"] - role - } - role -> executors [ - arrowhead=curve - ltail=cluster_on_role_experiment - lhead=cluster_experiment - ] - - subgraph cluster_experiment_with_reporter{ - graph[label="ExperimentWithReporter", style="solid"] - reporter - } - reporter -> executors [ - arrowhead=curve - ltail=cluster_experiment_with_reporter - lhead=cluster_experiment - ] - - subgraph cluster_cycled_experiment{ - graph[label="CycledExperiment", style="solid"] - n_iterations - } - n_iterations -> reporter [ - arrowhead=curve - ltail=cluster_cycled_experiment - lhead=cluster_experiment_with_reporter - ] - - subgraph cluster_group_experiment{ - graph[label="GroupExperiment", style="solid"] - searching_role - } - searching_role -> reporter [ - arrowhead=curve - ltail=cluster_group_experiment - lhead=cluster_experiment_with_reporter - ] - - subgraph cluster_params_experiment{ - graph[label="ParamsExperiment", style="solid"] - params - } - params -> reporter [ - arrowhead=curve - ltail=cluster_params_experiment - lhead=cluster_experiment_with_reporter - ] - - subgraph cluster_if_params_experiment{ - graph[label="IfParamsExperiment", style="solid"] - stopping_criterion - } - stopping_criterion -> params [ - arrowhead=curve - ltail=cluster_if_params_experiment - lhead=cluster_params_experiment - ] - stopping_criterion -> check_rule [ - style=dashed - arrowhead=box - lhead=cluster_if_executor - ] - } - - subgraph cluster_analyzers{ - graph[label="Analyzers", style="dashed"] - analyzer - subgraph cluster_aa_analyzers{ - graph[label="AA", style="dashed"] - one_aa_analyzer - aa_score_analyzer - } - subgraph cluster_ab_analyzers{ - graph[label="AB", style="dashed"] - ab_analyzer - } - subgraph cluster_matching_analyzers{ - graph[label="Matching", style="dashed"] - matching_analyzer - } - analyzer -> execute [ - arrowhead=curve - lhead=cluster_executor - ] - - { - one_aa_analyzer, - aa_score_analyzer, - ab_analyzer, - matching_analyzer - } -> analyzer [arrowhead=curve] - } - - subgraph cluster_comparators{ - graph[label="Comparators", style="dashed"] - - subgraph cluster_abstract_comporators{ - graph[label="Abstract", style="dotted"] - - subgraph cluster_comporator{ - graph[label="Comparator", style="solid"] - _split_data_to_buckets - compare_by - groping_reole - target_roles_comporator - baseline_role - } - _split_data_to_buckets -> calc [ - arrowhead=curve - ltail=cluster_comporator - lhead=cluster_calculator - ] - - subgraph cluster_stat_hyp_testing{ - graph[label="StatHypothesisTesting", style="solid"] - reliability - } - reliability -> compare_by [ - arrowhead=curve - ltail=cluster_stat_hyp_testing - lhead=cluster_comporator - ] - - subgraph cluster_power_testing{ - graph[label="PowerTesting", style="solid"] - significance - power - } - power -> compare_by [ - arrowhead=curve - ltail=cluster_power_testing - lhead=cluster_comporator - ] - } - - { - group_diff, - group_sizes - psi - maha_dist - } -> compare_by [ - arrowhead=curve - lhead=cluster_comporator - ] - - subgraph cluster_hyp_testing{ - graph[label="Hypothesis testing", style=dotted] - { - ttest - kstest - utest - chi2test - } -> reliability [ - arrowhead=curve - lhead=cluster_stat_hyp_testing - ] - } - - mdebysize -> power [ - arrowhead=curve - lhead=cluster_power_testing - ] - } - - subgraph cluster_data{ - graph[label="Data", style=dashed] - subgraph cluster_dataset_backend_navigation{ - graph[label="DatasetBackendNavigation", style=solid] - data_manipulations_functios - } - subgraph cluster_dataset_backend_calc{ - graph[label="DatasetBackendCalc", style=solid] - calculation_functions - } - subgraph cluster_pandas_backend{ - graph[label="PandasBackend", style=solid] - pandas_dataset_backend_functions - data_pandas - } - subgraph cluster_dataset_base{ - graph[label="DatasetBase", style=solid] - backend - roles - navigation_backend_calling_functions - } - subgraph cluster_dataset{ - graph[label="Dataset", style=solid] - calculation_functions_via_backend - } - - calculation_functions -> data_manipulations_functios [ - arrowhead=curve, - ltail=cluster_dataset_backend_calc - lhead=cluster_dataset_backend_navigation - ] - pandas_dataset_backend_functions -> calculation_functions [ - arrowhead=curve, - ltail=cluster_pandas_backend - lhead=cluster_dataset_backend_calc - ] - backend -> calculation_functions [ - arrowhead=box, - lhead=cluster_dataset_backend_calc - ] - calculation_functions_via_backend -> navigation_backend_calling_functions [ - arrowhead=curve, - ltail=cluster_dataset - lhead=cluster_dataset_base - ] - roles -> abc_role[arrowhead=box] - } - - subgraph cluster_encoders{ - graph[label="Encoders", style=dashed] - subgraph cluster_encoder{ - graph[label="Encoder", style=solid] - target_roles_encoder - } - target_roles_encoder -> calc [ - arrowhead=curve, - ltail=cluster_encoder, - lhead=cluster_calculator - ] - dummy_encoder -> target_roles_encoder [ - arrowhead=curve, - lhead=cluster_encoder - ] - } - - subgraph cluster_extensions{ - graph[label="Extensions", style=dashed] - subgraph cluster_abstract_extensions{ - graph[label="Abstract", style=dotted] - - subgraph cluster_extension{ - graph[label="Extension", style=solid] - calc_ext - _calc_pandas - } - subgraph cluster_ml_extension{ - graph[label="MLExtension", style=solid] - fit_ext - predict_ext - } - - compare_extension -> calc_ext [ - arrowhead=curve - lhead=cluster_extension - ] - fit_ext -> calc_ext [ - arrowhead=curve - lhead=cluster_extension - ltail=cluster_ml_extension - ] - } - - subgraph cluster_stat_test{ - graph[label="StatTest", style=solid] - test_function - reliability_ext - } - subgraph cluster_chi2test_ext{ - graph[label="Chi2Test", style=solid] - matrix_preparation - } - subgraph cluster_multitest{ - graph[label="MultiTest", style=solid] - compare_extension - method - alpha - } - subgraph cluster_multitest_q{ - graph[label="MultiTestQuantile", style=solid] - alpha_q - iteration_size - equal_variance - random_state_q - } - - { - dummy_encoder_ext - inverse_ext - cholesky_ext - } -> calc_ext [ - arrowhead=curve - lhead=cluster_extension - ] - faiss_ext -> fit_ext [ - arrowhead=curve - lhead=cluster_ml_extension - ] - test_function -> compare_extension [ - arrowhead=curve - ltail=cluster_stat_test - ] - { - ttest_ext - kstest_ext - utest_ext - norm_cdf_ext - } -> test_function [ - arrowhead=curve - lhead=cluster_stat_test - ] - matrix_preparation -> test_function [ - arrowhead=curve - ltail=cluster_chi2test_ext - lhead=cluster_stat_test - ] - method -> calc_ext [ - arrowhead=curve - ltail=cluster_multitest - lhead=cluster_extension - ] - iteration_size -> calc_ext [ - arrowhead=curve - ltail=cluster_multitest_q - lhead=cluster_extension - ] - } - - subgraph cluster_forks{ - graph[label="Forks", style=dashed] - - subgraph cluster_if_aa_executor{ - graph[label="IfAAExecutor", style=solid] - sample_size - } - } - sample_size -> if_executor [ - arrowhead=curve - ltail=cluster_if_aa_executor - lhead=cluster_if_executor - ] - - subgraph cluster_ml{ - graph[label="ML", style=dashed] - - subgraph cluster_faiss{ - graph[label="Faiss", style=solid] - n_neighbors - two_sides - test_pairs - faiss_mode - } - } - faiss_ext -> faiss_mode[ - arrowhead=box - lhead=cluster_faiss - ] - faiss_mode -> fit[ - arrowhead=curve, - ltail=cluster_faiss - lhead=cluster_ml_executor - ] - - subgraph cluster_operators{ - graph[label="Operators", style=dashed] - - subgraph cluster_matching_metrics{ - graph[label="MatchingMetrics", style=solid] - matching_metric - } - - {smd, bias} -> group_operator [arrowhead=curve] - matching_metric -> group_operator [ - arrowhead=curve, - ltail=cluster_matching_metrics - ] - - group_operator -> calc [ - arrowhead=curve, - lhead=cluster_calculator - ] - } - - subgraph cluster_reporters{ - graph[label="Reporters", style=dashed] - - subgraph cluster_reporter{ - graph[label="Reporter", style=solid] - report - } - - subgraph cluster_dict_reporter{ - graph[label="DictReporter", style=solid] - extract_from_one_row_dataset - } - on_dict_reporter - - subgraph cluster_dataset_reporter{ - graph[label="DatasetReporter", style=solid] - convert_to_dataset - } - - subgraph cluster_test_dict_reporter{ - graph[label="TestDictReporter", style=solid] - extract_tests - } - - subgraph cluster_one_aa_dict_reporter{ - graph[label="OneAADictReporter", style=solid] - one_aa_dict_reporter_tests - convert_flat_dataset - get_splitter_id - extract_group_difference - extract_group_sizes - extract_analyzer_data_aa - extract_data_from_analysis_tables_aa - } - - aa_dataset_reporter - aa_passed_reporter - aa_best_split_reporter - ab_dataset_reporter - - matching_dict_reporter - matching_quality_dict_reporter - matching_quality_dataset_reporter - matching_dataset_reporter - - extract_from_one_row_dataset -> report [ - arrowhead=curve, - ltail=cluster_dict_reporter, - lhead=cluster_reporter - ] - - - on_dict_reporter -> report [ - arrowhead=curve, - lhead=cluster_reporter - ] - - convert_to_dataset -> on_dict_reporter [ - arrowhead=curve, - ltail=cluster_dataset_reporter, - ] - - extract_tests -> extract_from_one_row_dataset [ - arrowhead=curve, - ltail=cluster_test_dict_reporter, - lhead=cluster_dict_reporter - ] - - one_aa_dict_reporter_tests -> extract_tests [ - arrowhead=curve, - ltail=cluster_one_aa_dict_reporter, - lhead=cluster_test_dict_reporter - ] - - aa_dataset_reporter -> one_aa_dict_reporter_tests [ - arrowhead=curve, - lhead=cluster_one_aa_dict_reporter - ] - - aa_passed_reporter -> report [ - arrowhead=curve, - lhead=cluster_reporter - ] - - aa_best_split_reporter -> report [ - arrowhead=curve, - lhead=cluster_reporter - ] - - ab_dict_rporter -> one_aa_dict_reporter_tests [ - arrowhead=curve, - ltail=cluster_ab_dict_reporter - lhead=cluster_one_aa_dict_reporter - ] - - ab_dataset_reporter -> ab_dict_rporter [arrowhead=curve] - - homo_dict_reporter -> one_aa_dict_reporter_tests [ - arrowhead=curve, - lhead=cluster_one_aa_dict_reporter - ] - - homo_dataset_reporter -> convert_to_dataset [ - arrowhead=curve, - lhead=cluster_dataset_reporter - ] - homo_dict_reporter -> homo_dataset_reporter [arrowhead=box] - - matching_dict_reporter -> extract_from_one_row_dataset [ - arrowhead=curve, - lhead=cluster_dict_reporter - ] - - matching_quality_dict_reporter -> extract_tests [ - arrowhead=curve, - lhead=cluster_test_dict_reporter - ] - - matching_quality_dataset_reporter -> matching_quality_dict_reporter [arrowhead=curve] - - matching_dataset_reporter -> convert_to_dataset [ - arrowhead=curve, - lhead=cluster_dataset_reporter - ] - } - -} \ No newline at end of file diff --git a/schemes/hierarchy.dot b/schemes/hierarchy.dot new file mode 100644 index 00000000..59fec65a --- /dev/null +++ b/schemes/hierarchy.dot @@ -0,0 +1,819 @@ +digraph HypExInheritance { + rankdir=TB + node[shape=record, style="filled,rounded", fontsize=10] + edge[fontsize=9] + compound=true + + // Color scheme + // Base classes - light blue + // Abstract intermediate - light yellow + // Concrete implementations - light green + // Special/Complex - light pink + + // ========== EXECUTOR HIERARCHY ========== + subgraph cluster_executor { + label="Executor Hierarchy" + style="dashed,rounded" + bgcolor="#f0f0f0" + + // ========== BASE EXECUTOR CLASSES ========== + subgraph cluster_base_executors { + label="Core Executor Classes" + style="filled,rounded" + bgcolor="#e8f4f8" + + // Base Executor + Executor [ + fillcolor=lightblue + label="{ Executor | + + id: str\l + + key: Any\l + + params_hash: str\l | + + execute(data): ExperimentData\l + + set_params(params): void\l + + _generate_id(): void\l + }" + ] + + // Calculator branch + Calculator [ + fillcolor=lightyellow + label="{ «abstract» Calculator | | + + calc(data, **kwargs): Any\l + + _inner_function(data, **kwargs): Any\l + }" + ] + + // IfExecutor branch + IfExecutor [ + fillcolor=lightyellow + label="{ IfExecutor | + + if_executor: Executor\l + + else_executor: Executor\l | + + check_rule(data): bool\l + Conditional execution\l + }" + ] + + IfAAExecutor [ + fillcolor=lightgreen + label="{ IfAAExecutor | + + sample_size: float\l | + Checks AA test pass\l + }" + ] + + // MLExecutor branch + MLExecutor [ + fillcolor=lightyellow + label="{ «abstract» MLExecutor | + + grouping_role: ABCRole\l + + target_role: ABCRole\l | + + fit(X, Y): MLExecutor\l + + predict(X): Dataset\l + + score(X, Y): float\l + }" + ] + } + + // ========== COMPARATORS ========== + subgraph cluster_comparators { + label="Comparators" + style="filled,rounded" + bgcolor="#f8e8e8" + + Comparator [ + fillcolor=lightyellow + label="{ «abstract» Comparator | + + compare_by: str\l + + grouping_role: ABCRole\l + + target_roles: ABCRole\l + + baseline_role: ABCRole\l | + + _inner_function(data, test_data): dict\l + }" + ] + + // Basic comparators + subgraph cluster_basic_comparators { + label="Basic Metrics" + style="dashed" + + GroupDifference [ + fillcolor=lightgreen + label="{ GroupDifference | | + Calculates:\l + • control/test mean\l + • difference\l + • difference %\l + }" + ] + + GroupSizes [ + fillcolor=lightgreen + label="{ GroupSizes | | + Calculates:\l + • control/test size\l + • size percentages\l + }" + ] + + PSI [ + fillcolor=lightgreen + label="{ PSI | | + Population\l + Stability Index\l + }" + ] + + MahalanobisDistance [ + fillcolor=lightgreen + label="{ MahalanobisDistance | | + Calculates Mahalanobis\l + distance between groups\l + }" + ] + } + + // Statistical tests + subgraph cluster_stat_tests { + label="Statistical Tests" + style="dashed" + + StatHypothesisTesting [ + fillcolor=lightyellow + label="{ «abstract» StatHypothesisTesting | + + reliability: float\l | + Statistical tests base\l + }" + ] + + TTest [fillcolor=lightgreen label="{ TTest | | Student's t-test }"] + KSTest [fillcolor=lightgreen label="{ KSTest | | Kolmogorov-Smirnov test }"] + UTest [fillcolor=lightgreen label="{ UTest | | Mann-Whitney U test }"] + Chi2Test [fillcolor=lightgreen label="{ Chi2Test | + matrix_preparation(): void\l | Chi-squared test }"] + } + + // Power testing + subgraph cluster_power_testing { + label="Power Analysis" + style="dashed" + + PowerTesting [ + fillcolor=lightyellow + label="{ «abstract» PowerTesting | + + significance: float = 0.95\l + + power: float = 0.8\l | + Power analysis base\l + }" + ] + + MDEBySize [ + fillcolor=lightgreen + label="{ MDEBySize | | + Minimum Detectable Effect\l + by sample size\l + }" + ] + } + } + + // ========== SPLITTERS ========== + subgraph cluster_splitters { + label="Splitters" + style="filled,rounded" + bgcolor="#e8f8e8" + + Splitter [ + fillcolor=lightyellow + label="{ «abstract» Splitter | + + control_size: float\l + + random_state: int\l + + sample_size: float\l + + constant_key: bool\l + + save_groups: bool\l | + + init_from_hash(hash): void\l + Splits data into groups\l + }" + ] + + AASplitter [ + fillcolor=lightgreen + label="{ AASplitter | | + A/A test splitter\l + Uses AdditionalTreatmentRole\l + }" + ] + + AASplitterWithStratification [ + fillcolor=lightgreen + label="{ AASplitterWithStratification | | + + Uses StratificationRole\l + for grouped splitting\l + }" + ] + } + + // ========== TRANSFORMERS & ENCODERS ========== + subgraph cluster_transformers { + label="Transformers & Encoders" + style="filled,rounded" + bgcolor="#f8f8e8" + + Transformer [ + fillcolor=lightyellow + label="{ «abstract» Transformer | + + _is_transformer = True\l | + Transforms Dataset\l + }" + ] + + Shuffle [ + fillcolor=lightgreen + label="{ Shuffle | + + random_state: int\l | + Randomly shuffles data\l + }" + ] + + NaFiller [ + fillcolor=lightgreen + label="{ NaFiller | + + method: str\l + + value: Any\l | + Fills missing values\l + }" + ] + + CategoryAggregator [ + fillcolor=lightgreen + label="{ CategoryAggregator | + + threshold: int\l + + new_group_name: str\l | + Aggregates rare categories\l + }" + ] + + // Filters + ConstFilter [ + fillcolor=lightgreen + label="{ ConstFilter | + + threshold: float\l | + Filters constant columns\l + }" + ] + + CorrFilter [ + fillcolor=lightgreen + label="{ CorrFilter | + + threshold: float\l | + Filters correlated columns\l + }" + ] + + CVFilter [ + fillcolor=lightgreen + label="{ CVFilter | + + lower_bound: float\l + + upper_bound: float\l | + Coefficient of variation filter\l + }" + ] + + NanFilter [ + fillcolor=lightgreen + label="{ NanFilter | + + threshold: float\l | + Filters columns with NaNs\l + }" + ] + + OutliersFilter [ + fillcolor=lightgreen + label="{ OutliersFilter | + + method: str\l + + threshold: float\l | + Filters outliers\l + }" + ] + + Encoder [ + fillcolor=lightyellow + label="{ «abstract» Encoder | + + target_roles: Sequence[str]\l | + Encodes categorical data\l + }" + ] + + DummyEncoder [ + fillcolor=lightgreen + label="{ DummyEncoder | | + One-hot encoding\l + }" + ] + } + + // ========== GROUP OPERATORS ========== + subgraph cluster_operators { + label="Group Operators" + style="filled,rounded" + bgcolor="#e8e8f8" + + GroupOperator [ + fillcolor=lightyellow + label="{ «abstract» GroupOperator | + + grouping_role: ABCRole\l + + target_roles: list[ABCRole]\l | + Operations on groups\l + }" + ] + + SMD [ + fillcolor=lightgreen + label="{ SMD | | + Standardized\l + Mean Difference\l + }" + ] + + Bias [ + fillcolor=lightgreen + label="{ Bias | | + + calc_coefficients(X, Y): list\l + Bias estimation\l + }" + ] + + MatchingMetrics [ + fillcolor=lightgreen + label="{ MatchingMetrics | + + metric: str\l | + ATT, ATC, ATE metrics\l + }" + ] + } + + // ========== ANALYZERS ========== + subgraph cluster_analyzers { + label="Analyzers" + style="filled,rounded" + bgcolor="#f0e8f8" + + Analyzer [ + fillcolor=lightyellow + label="{ «abstract» Analyzer | | + Analyzes experiment results\l + }" + ] + + OneAAStatAnalyzer [ + fillcolor=lightgreen + label="{ OneAAStatAnalyzer | | + Analyzes single\l + A/A test statistics\l + }" + ] + + AAScoreAnalyzer [ + fillcolor=lightgreen + label="{ AAScoreAnalyzer | + + threshold: float\l + + alpha: float\l | + Scores A/A tests\l + Finds best split\l + }" + ] + + ABAnalyzer [ + fillcolor=lightgreen + label="{ ABAnalyzer | + + multitest_method: str\l + + alpha: float\l + + equal_variance: bool\l + + quantiles: float\l + + iteration_size: int\l | + A/B test analyzer\l + Multiple testing correction\l + }" + ] + + MatchingAnalyzer [ + fillcolor=lightgreen + label="{ MatchingAnalyzer | | + Analyzes matching\l + quality and metrics\l + }" + ] + } + + // ========== ML EXECUTORS ========== + subgraph cluster_ml_executors { + label="ML Executors" + style="filled,rounded" + bgcolor="#f8e8f8" + + FaissNearestNeighbors [ + fillcolor=lightgreen + label="{ FaissNearestNeighbors | + + n_neighbors: int\l + + two_sides: bool\l + + test_pairs: bool\l + + faiss_mode: str\l | + Finds nearest neighbors\l + Uses FAISS library\l + }" + ] + } + } + + // ========== EXPERIMENT HIERARCHY ========== + subgraph cluster_experiment { + label="Experiment Hierarchy" + style="dashed,rounded" + bgcolor="#f5f5f0" + + Experiment [ + fillcolor=lightblue + label="{ Experiment | + + executors: list[Executor]\l + + key: str\l | + + execute(data): ExperimentData\l + + set_params(params): void\l + Chains executors\l + }" + ] + + OnRoleExperiment [ + fillcolor=lightgreen + label="{ OnRoleExperiment | + + role: ABCRole\l | + Executes on specific role\l + }" + ] + + ExperimentWithReporter [ + fillcolor=lightyellow + label="{ ExperimentWithReporter | + + reporter: Reporter\l | + + one_iteration(data): Dataset\l + Adds reporting capability\l + }" + ] + + CycledExperiment [ + fillcolor=lightgreen + label="{ CycledExperiment | + + n_iterations: int\l | + Runs n iterations\l + }" + ] + + GroupExperiment [ + fillcolor=lightgreen + label="{ GroupExperiment | + + searching_role: ABCRole\l | + Runs on groups\l + }" + ] + + ParamsExperiment [ + fillcolor=lightgreen + label="{ ParamsExperiment | + + params: dict\l | + Parameterized runs\l + }" + ] + + IfParamsExperiment [ + fillcolor=lightgreen + label="{ IfParamsExperiment | + + stopping_criterion: Executor\l | + Conditional params\l + }" + ] + } + + // ========== REPORTER HIERARCHY ========== + subgraph cluster_reporter { + label="Reporter Hierarchy" + style="dashed,rounded" + bgcolor="#f0f5f0" + + Reporter [ + fillcolor=lightblue + label="{ «abstract» Reporter | | + + report(data): Any\l + }" + ] + + DictReporter [ + fillcolor=lightyellow + label="{ «abstract» DictReporter | + + front: bool\l | + Returns dict results\l + }" + ] + + OnDictReporter [ + fillcolor=lightyellow + label="{ «abstract» OnDictReporter | + + dict_reporter: DictReporter\l | + Wraps dict reporter\l + }" + ] + + DatasetReporter [ + fillcolor=lightgreen + label="{ DatasetReporter | | + Converts dict to Dataset\l + }" + ] + + TestDictReporter [ + fillcolor=lightyellow + label="{ «abstract» TestDictReporter | + + tests: list\l | + Extracts test results\l + }" + ] + + OneAADictReporter [ + fillcolor=lightgreen + label="{ OneAADictReporter | | + Reports A/A test results\l + }" + ] + + ABDictReporter [ + fillcolor=lightgreen + label="{ ABDictReporter | | + Reports A/B test results\l + }" + ] + + ABDatasetReporter [ + fillcolor=lightgreen + label="{ ABDatasetReporter | | + A/B results as Dataset\l + }" + ] + + HomoDictReporter [ + fillcolor=lightgreen + label="{ HomoDictReporter | | + Homogeneity test results\l + }" + ] + + HomoDatasetReporter [ + fillcolor=lightgreen + label="{ HomoDatasetReporter | | + Homogeneity as Dataset\l + }" + ] + + AAPassedReporter [ + fillcolor=lightgreen + label="{ AAPassedReporter | | + Reports passed A/A tests\l + }" + ] + + AABestSplitReporter [ + fillcolor=lightgreen + label="{ AABestSplitReporter | | + Reports best split\l + }" + ] + + MatchingDictReporter [ + fillcolor=lightgreen + label="{ MatchingDictReporter | + + searching_class: type\l | + Matching results dict\l + }" + ] + + MatchingQualityDictReporter [ + fillcolor=lightgreen + label="{ MatchingQualityDictReporter | | + Matching quality dict\l + }" + ] + + MatchingDatasetReporter [ + fillcolor=lightgreen + label="{ MatchingDatasetReporter | | + Matching as Dataset\l + }" + ] + + MatchingQualityDatasetReporter [ + fillcolor=lightgreen + label="{ MatchingQualityDatasetReporter | | + Quality as Dataset\l + }" + ] + } + + // ========== UI/OUTPUT HIERARCHY ========== + subgraph cluster_ui { + label="UI/Output Classes" + style="dashed,rounded" + bgcolor="#f5f0f5" + + Output [ + fillcolor=lightpink + label="{ Output | + + resume: Dataset\l + + resume_reporter: Reporter\l + + additional_reporters: dict\l | + + extract(data): void\l + Formats experiment output\l + }" + ] + + AAOutput [ + fillcolor=lightpink + label="{ AAOutput | + + best_split: Dataset\l + + experiments: Dataset\l + + aa_score: Dataset\l + + best_split_statistic: Dataset\l | + A/A test output\l + }" + ] + + HomoOutput [ + fillcolor=lightpink + label="{ HomoOutput | | + Homogeneity test output\l + }" + ] + + MatchingOutput [ + fillcolor=lightpink + label="{ MatchingOutput | + + matching_results: Dataset\l + + quality_metrics: Dataset\l | + Matching analysis output\l + }" + ] + + ExperimentShell [ + fillcolor=lightpink + label="{ ExperimentShell | + + experiment: Experiment\l + + output: Output\l | + UI wrapper for experiments\l + }" + ] + + AATest [ + fillcolor=lightpink + label="{ AATest | | + A/A test shell\l + with configuration\l + }" + ] + + Matching [ + fillcolor=lightpink + label="{ Matching | + + distance: str\l + + metric: str\l + + bias_estimation: bool\l | + Matching analysis shell\l + }" + ] + } + + // ========== INHERITANCE RELATIONSHIPS ========== + // Solid arrows for inheritance + Executor -> Calculator [arrowhead=empty] + Executor -> IfExecutor [arrowhead=empty] + Executor -> Analyzer [arrowhead=empty] + + Calculator -> Comparator [arrowhead=empty] + Calculator -> Splitter [arrowhead=empty] + Calculator -> Transformer [arrowhead=empty] + Calculator -> Encoder [arrowhead=empty] + Calculator -> GroupOperator [arrowhead=empty] + Calculator -> MLExecutor [arrowhead=empty] + + Comparator -> GroupDifference [arrowhead=empty] + Comparator -> GroupSizes [arrowhead=empty] + Comparator -> PSI [arrowhead=empty] + Comparator -> MahalanobisDistance [arrowhead=empty] + Comparator -> StatHypothesisTesting [arrowhead=empty] + Comparator -> PowerTesting [arrowhead=empty] + + PowerTesting -> MDEBySize [arrowhead=empty] + + StatHypothesisTesting -> TTest [arrowhead=empty] + StatHypothesisTesting -> KSTest [arrowhead=empty] + StatHypothesisTesting -> UTest [arrowhead=empty] + StatHypothesisTesting -> Chi2Test [arrowhead=empty] + + Splitter -> AASplitter [arrowhead=empty] + AASplitter -> AASplitterWithStratification [arrowhead=empty] + + Transformer -> Shuffle [arrowhead=empty] + Transformer -> NaFiller [arrowhead=empty] + Transformer -> CategoryAggregator [arrowhead=empty] + Transformer -> ConstFilter [arrowhead=empty] + Transformer -> CorrFilter [arrowhead=empty] + Transformer -> CVFilter [arrowhead=empty] + Transformer -> NanFilter [arrowhead=empty] + Transformer -> OutliersFilter [arrowhead=empty] + + Encoder -> DummyEncoder [arrowhead=empty] + + GroupOperator -> SMD [arrowhead=empty] + GroupOperator -> Bias [arrowhead=empty] + GroupOperator -> MatchingMetrics [arrowhead=empty] + + IfExecutor -> IfAAExecutor [arrowhead=empty] + + MLExecutor -> FaissNearestNeighbors [arrowhead=empty] + + Analyzer -> OneAAStatAnalyzer [arrowhead=empty] + Analyzer -> AAScoreAnalyzer [arrowhead=empty] + Analyzer -> ABAnalyzer [arrowhead=empty] + Analyzer -> MatchingAnalyzer [arrowhead=empty] + + Experiment -> OnRoleExperiment [arrowhead=empty] + Experiment -> ExperimentWithReporter [arrowhead=empty] + + ExperimentWithReporter -> CycledExperiment [arrowhead=empty] + ExperimentWithReporter -> ParamsExperiment [arrowhead=empty] + ExperimentWithReporter -> GroupExperiment [arrowhead=empty] + ParamsExperiment -> IfParamsExperiment [arrowhead=empty] + + Reporter -> DictReporter [arrowhead=empty] + Reporter -> OnDictReporter [arrowhead=empty] + Reporter -> AAPassedReporter [arrowhead=empty] + Reporter -> AABestSplitReporter [arrowhead=empty] + + OnDictReporter -> DatasetReporter [arrowhead=empty] + + DictReporter -> TestDictReporter [arrowhead=empty] + DictReporter -> MatchingDictReporter [arrowhead=empty] + + TestDictReporter -> OneAADictReporter [arrowhead=empty] + TestDictReporter -> MatchingQualityDictReporter [arrowhead=empty] + + OneAADictReporter -> ABDictReporter [arrowhead=empty] + OneAADictReporter -> HomoDictReporter [arrowhead=empty] + + ABDictReporter -> ABDatasetReporter [arrowhead=empty] + HomoDictReporter -> HomoDatasetReporter [arrowhead=empty] + MatchingQualityDictReporter -> MatchingQualityDatasetReporter [arrowhead=empty] + + DatasetReporter -> MatchingDatasetReporter [arrowhead=empty] + + Output -> AAOutput [arrowhead=empty] + Output -> HomoOutput [arrowhead=empty] + Output -> MatchingOutput [arrowhead=empty] + + ExperimentShell -> AATest [arrowhead=empty] + ExperimentShell -> Matching [arrowhead=empty] + + // ========== COMPOSITION/USAGE RELATIONSHIPS ========== + // Dashed arrows for composition/usage + Experiment -> Executor [style=dashed, label="uses list of", arrowhead=open] + ExperimentWithReporter -> Reporter [style=dashed, label="has", arrowhead=open] + DatasetReporter -> DictReporter [style=dashed, label="wraps", arrowhead=open] + OnDictReporter -> DictReporter [style=dashed, label="wraps", arrowhead=open] + Output -> Reporter [style=dashed, label="uses", arrowhead=open] + ExperimentShell -> Experiment [style=dashed, label="wraps", arrowhead=open] + ExperimentShell -> Output [style=dashed, label="has", arrowhead=open] + IfExecutor -> Executor [style=dashed, label="contains 2", arrowhead=open] + IfParamsExperiment -> Executor [style=dashed, label="has stopping\ncriterion", arrowhead=open] + MatchingDatasetReporter -> MatchingDictReporter [style=dashed, label="uses", arrowhead=open] + FaissNearestNeighbors -> MahalanobisDistance [style=dashed, label="may use", arrowhead=open] + AAScoreAnalyzer -> AASplitter [style=dashed, label="rebuilds", arrowhead=open] + + // Legend + subgraph cluster_legend { + label="Legend" + style="filled,rounded" + bgcolor="#fafafa" + + // Arrow types examples + node[shape=box, style=filled, fillcolor=white] + inherit_from [label="Parent"] + inherit_to [label="Child"] + inherit_from -> inherit_to [arrowhead=empty, label="Inheritance"] + + compose_from [label="Container"] + compose_to [label="Component"] + compose_from -> compose_to [style=dashed, arrowhead=open, label="Composition/Uses"] + + // Color coding examples + node[shape=record, style="filled,rounded"] + base_example [fillcolor=lightblue, label="{ Base Class | Foundation }"] + abstract_example [fillcolor=lightyellow, label="{ «abstract» Class | Template }"] + concrete_example [fillcolor=lightgreen, label="{ Concrete Class | Implementation }"] + ui_example [fillcolor=lightpink, label="{ UI/Shell Class | User Interface }"] + } +} \ No newline at end of file From 394e04c42102e6a61d92a05c3249848f1576cf5a Mon Sep 17 00:00:00 2001 From: dmbulychev Date: Thu, 4 Sep 2025 07:08:16 +0300 Subject: [PATCH 10/17] Update scheme --- schemes/hierarchy.dot | 139 +++++++++++++++++++++++++++++------------- 1 file changed, 98 insertions(+), 41 deletions(-) diff --git a/schemes/hierarchy.dot b/schemes/hierarchy.dot index 59fec65a..4c92dff4 100644 --- a/schemes/hierarchy.dot +++ b/schemes/hierarchy.dot @@ -183,22 +183,16 @@ digraph HypExInheritance { style="filled,rounded" bgcolor="#e8f8e8" - Splitter [ - fillcolor=lightyellow - label="{ «abstract» Splitter | + // Note: No abstract Splitter class in actual code + AASplitter [ + fillcolor=lightgreen + label="{ AASplitter | + control_size: float\l + random_state: int\l + sample_size: float\l + constant_key: bool\l + save_groups: bool\l | + init_from_hash(hash): void\l - Splits data into groups\l - }" - ] - - AASplitter [ - fillcolor=lightgreen - label="{ AASplitter | | A/A test splitter\l Uses AdditionalTreatmentRole\l }" @@ -239,7 +233,7 @@ digraph HypExInheritance { fillcolor=lightgreen label="{ NaFiller | + method: str\l - + value: Any\l | + + values: Any\l | Fills missing values\l }" ] @@ -265,7 +259,8 @@ digraph HypExInheritance { CorrFilter [ fillcolor=lightgreen label="{ CorrFilter | - + threshold: float\l | + + threshold: float\l + + method: str\l | Filters correlated columns\l }" ] @@ -290,8 +285,8 @@ digraph HypExInheritance { OutliersFilter [ fillcolor=lightgreen label="{ OutliersFilter | - + method: str\l - + threshold: float\l | + + lower_percentile: float\l + + upper_percentile: float\l | Filters outliers\l }" ] @@ -339,6 +334,7 @@ digraph HypExInheritance { fillcolor=lightgreen label="{ Bias | | + calc_coefficients(X, Y): list\l + + calc_bias(X, X_matched, coef): list\l Bias estimation\l }" ] @@ -346,7 +342,8 @@ digraph HypExInheritance { MatchingMetrics [ fillcolor=lightgreen label="{ MatchingMetrics | - + metric: str\l | + + metric: str (atc/att/ate)\l | + Calculates treatment effects:\l ATT, ATC, ATE metrics\l }" ] @@ -358,18 +355,13 @@ digraph HypExInheritance { style="filled,rounded" bgcolor="#f0e8f8" - Analyzer [ - fillcolor=lightyellow - label="{ «abstract» Analyzer | | - Analyzes experiment results\l - }" - ] - + // Note: Analyzers inherit directly from Executor OneAAStatAnalyzer [ fillcolor=lightgreen label="{ OneAAStatAnalyzer | | Analyzes single\l A/A test statistics\l + Inherits from Executor\l }" ] @@ -380,19 +372,21 @@ digraph HypExInheritance { + alpha: float\l | Scores A/A tests\l Finds best split\l + Inherits from Executor\l }" ] ABAnalyzer [ fillcolor=lightgreen label="{ ABAnalyzer | - + multitest_method: str\l + + multitest_method: ABNTestMethodsEnum\l + alpha: float\l + equal_variance: bool\l + quantiles: float\l + iteration_size: int\l | A/B test analyzer\l Multiple testing correction\l + Inherits from Executor\l }" ] @@ -401,6 +395,7 @@ digraph HypExInheritance { label="{ MatchingAnalyzer | | Analyzes matching\l quality and metrics\l + Inherits from Executor\l }" ] } @@ -435,6 +430,7 @@ digraph HypExInheritance { fillcolor=lightblue label="{ Experiment | + executors: list[Executor]\l + + transformer: bool\l + key: str\l | + execute(data): ExperimentData\l + set_params(params): void\l @@ -445,7 +441,7 @@ digraph HypExInheritance { OnRoleExperiment [ fillcolor=lightgreen label="{ OnRoleExperiment | - + role: ABCRole\l | + + role: list[ABCRole]\l | Executes on specific role\l }" ] @@ -478,7 +474,8 @@ digraph HypExInheritance { ParamsExperiment [ fillcolor=lightgreen label="{ ParamsExperiment | - + params: dict\l | + + params: dict\l + + flat_params: list\l | Parameterized runs\l }" ] @@ -486,8 +483,9 @@ digraph HypExInheritance { IfParamsExperiment [ fillcolor=lightgreen label="{ IfParamsExperiment | - + stopping_criterion: Executor\l | + + stopping_criterion: IfExecutor\l | Conditional params\l + with early stopping\l }" ] } @@ -543,6 +541,13 @@ digraph HypExInheritance { }" ] + AADatasetReporter [ + fillcolor=lightgreen + label="{ AADatasetReporter | | + A/A results as Dataset\l + }" + ] + ABDictReporter [ fillcolor=lightgreen label="{ ABDictReporter | | @@ -575,6 +580,7 @@ digraph HypExInheritance { fillcolor=lightgreen label="{ AAPassedReporter | | Reports passed A/A tests\l + Detects which tests passed\l }" ] @@ -582,6 +588,7 @@ digraph HypExInheritance { fillcolor=lightgreen label="{ AABestSplitReporter | | Reports best split\l + Adds split column to data\l }" ] @@ -643,9 +650,20 @@ digraph HypExInheritance { }" ] + ABOutput [ + fillcolor=lightpink + label="{ ABOutput | + + multitest: Dataset | str\l + + sizes: Dataset\l | + A/B test output with\l + multiple testing correction\l + }" + ] + HomoOutput [ fillcolor=lightpink - label="{ HomoOutput | | + label="{ HomoOutput | + + resume: Dataset\l | Homogeneity test output\l }" ] @@ -653,8 +671,9 @@ digraph HypExInheritance { MatchingOutput [ fillcolor=lightpink label="{ MatchingOutput | - + matching_results: Dataset\l - + quality_metrics: Dataset\l | + + full_data: Dataset\l + + quality_results: Dataset\l + + indexes: Dataset\l | Matching analysis output\l }" ] @@ -664,24 +683,50 @@ digraph HypExInheritance { label="{ ExperimentShell | + experiment: Experiment\l + output: Output\l | + + execute(data): Output\l UI wrapper for experiments\l }" ] AATest [ fillcolor=lightpink - label="{ AATest | | + label="{ AATest | + + precision_mode: bool\l + + control_size: float\l + + stratification: bool\l + + n_iterations: int\l + + sample_size: float\l | A/A test shell\l with configuration\l }" ] + ABTest [ + fillcolor=lightpink + label="{ ABTest | + + additional_tests: list[str]\l + + multitest_method: str\l | + A/B test shell with\l + configurable tests\l + }" + ] + + HomogeneityTest [ + fillcolor=lightpink + label="{ HomogeneityTest | | + Homogeneity test shell\l + Uses predefined experiment\l + }" + ] + Matching [ fillcolor=lightpink label="{ Matching | + + group_match: bool\l + distance: str\l + metric: str\l - + bias_estimation: bool\l | + + bias_estimation: bool\l + + quality_tests: list\l | Matching analysis shell\l }" ] @@ -691,10 +736,13 @@ digraph HypExInheritance { // Solid arrows for inheritance Executor -> Calculator [arrowhead=empty] Executor -> IfExecutor [arrowhead=empty] - Executor -> Analyzer [arrowhead=empty] + Executor -> OneAAStatAnalyzer [arrowhead=empty] + Executor -> AAScoreAnalyzer [arrowhead=empty] + Executor -> ABAnalyzer [arrowhead=empty] + Executor -> MatchingAnalyzer [arrowhead=empty] Calculator -> Comparator [arrowhead=empty] - Calculator -> Splitter [arrowhead=empty] + Calculator -> AASplitter [arrowhead=empty] Calculator -> Transformer [arrowhead=empty] Calculator -> Encoder [arrowhead=empty] Calculator -> GroupOperator [arrowhead=empty] @@ -714,7 +762,6 @@ digraph HypExInheritance { StatHypothesisTesting -> UTest [arrowhead=empty] StatHypothesisTesting -> Chi2Test [arrowhead=empty] - Splitter -> AASplitter [arrowhead=empty] AASplitter -> AASplitterWithStratification [arrowhead=empty] Transformer -> Shuffle [arrowhead=empty] @@ -736,11 +783,6 @@ digraph HypExInheritance { MLExecutor -> FaissNearestNeighbors [arrowhead=empty] - Analyzer -> OneAAStatAnalyzer [arrowhead=empty] - Analyzer -> AAScoreAnalyzer [arrowhead=empty] - Analyzer -> ABAnalyzer [arrowhead=empty] - Analyzer -> MatchingAnalyzer [arrowhead=empty] - Experiment -> OnRoleExperiment [arrowhead=empty] Experiment -> ExperimentWithReporter [arrowhead=empty] @@ -763,6 +805,7 @@ digraph HypExInheritance { TestDictReporter -> MatchingQualityDictReporter [arrowhead=empty] OneAADictReporter -> ABDictReporter [arrowhead=empty] + OneAADictReporter -> AADatasetReporter [arrowhead=empty] OneAADictReporter -> HomoDictReporter [arrowhead=empty] ABDictReporter -> ABDatasetReporter [arrowhead=empty] @@ -772,10 +815,13 @@ digraph HypExInheritance { DatasetReporter -> MatchingDatasetReporter [arrowhead=empty] Output -> AAOutput [arrowhead=empty] + Output -> ABOutput [arrowhead=empty] Output -> HomoOutput [arrowhead=empty] Output -> MatchingOutput [arrowhead=empty] ExperimentShell -> AATest [arrowhead=empty] + ExperimentShell -> ABTest [arrowhead=empty] + ExperimentShell -> HomogeneityTest [arrowhead=empty] ExperimentShell -> Matching [arrowhead=empty] // ========== COMPOSITION/USAGE RELATIONSHIPS ========== @@ -788,10 +834,21 @@ digraph HypExInheritance { ExperimentShell -> Experiment [style=dashed, label="wraps", arrowhead=open] ExperimentShell -> Output [style=dashed, label="has", arrowhead=open] IfExecutor -> Executor [style=dashed, label="contains 2", arrowhead=open] - IfParamsExperiment -> Executor [style=dashed, label="has stopping\ncriterion", arrowhead=open] + IfParamsExperiment -> IfExecutor [style=dashed, label="has stopping\ncriterion", arrowhead=open] MatchingDatasetReporter -> MatchingDictReporter [style=dashed, label="uses", arrowhead=open] FaissNearestNeighbors -> MahalanobisDistance [style=dashed, label="may use", arrowhead=open] - AAScoreAnalyzer -> AASplitter [style=dashed, label="rebuilds", arrowhead=open] + AAScoreAnalyzer -> AASplitter [style=dashed, label="rebuilds from id", arrowhead=open] + + // Main user classes usage + ABTest -> ABAnalyzer [style=dashed, label="uses", arrowhead=open] + ABTest -> OnRoleExperiment [style=dashed, label="creates", arrowhead=open] + HomogeneityTest -> OneAAStatAnalyzer [style=dashed, label="uses", arrowhead=open] + AATest -> ParamsExperiment [style=dashed, label="creates", arrowhead=open] + AATest -> IfParamsExperiment [style=dashed, label="may create", arrowhead=open] + AATest -> AAScoreAnalyzer [style=dashed, label="uses", arrowhead=open] + Matching -> FaissNearestNeighbors [style=dashed, label="uses", arrowhead=open] + Matching -> MatchingMetrics [style=dashed, label="uses", arrowhead=open] + Matching -> MatchingAnalyzer [style=dashed, label="uses", arrowhead=open] // Legend subgraph cluster_legend { From 5e04cdc82eef6b4c434bbefcbce9bfd68583670c Mon Sep 17 00:00:00 2001 From: dmbulychev Date: Sat, 6 Sep 2025 20:52:03 +0300 Subject: [PATCH 11/17] =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=BE=D1=82=20=D0=BD=D0=B0=D1=87=D0=B0=D0=BB?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BE=20=D1=8D=D0=BA=D1=81=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schemes/anatomy.md | 1963 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1963 insertions(+) create mode 100644 schemes/anatomy.md diff --git a/schemes/anatomy.md b/schemes/anatomy.md new file mode 100644 index 00000000..74485b14 --- /dev/null +++ b/schemes/anatomy.md @@ -0,0 +1,1963 @@ +# Архитектура HypEx: Подробное руководство + +## 1. Введение и философия + +### Общая концепция библиотеки + +HypEx (Hypothesis Experiments) — это библиотека для проведения статистических экспериментов, построенная на принципах модульности, расширяемости и многоуровневой абстракции. Основная идея заключается в создании гибкой системы, которая позволяет как быстро запускать стандартные эксперименты (A/B тесты, A/A тесты, matching), так и конструировать сложные кастомные пайплайны обработки данных. + +Библиотека решает ключевую проблему: разрыв между потребностями бизнес-пользователей, которым нужны готовые решения, и потребностями исследователей данных, которым требуется гибкость и возможность кастомизации. + +### Принцип многоуровневой абстракции + +HypEx реализует 8 уровней абстракции, от простого использования готовых решений до модификации ядра библиотеки: + +1. **Уровень платформы** — использование через UI без написания кода +2. **Уровень конструктора** — создание сценариев через визуальный интерфейс +3. **Уровень шаблонов** — запуск предконфигурированных сценариев +4. **Уровень оболочек (Shell)** — использование готовых экспериментов в несколько строк кода +5. **Уровень композиции** — создание экспериментов из готовых блоков +6. **Уровень расширения** — создание новых блоков через наследование +7. **Уровень модификации** — глубокие доработки базовых механик +8. **Уровень ядра** — изменение фундаментального поведения + +Эта философия пронизывает всю архитектуру: каждый слой системы предоставляет свой уровень абстракции, позволяя пользователям работать на комфортном для них уровне сложности. + +### Основные архитектурные принципы + +**1. Композиция над наследованием** +- Эксперименты строятся путем композиции Executor'ов +- Каждый Executor выполняет одну конкретную задачу +- Сложное поведение достигается через комбинацию простых компонентов + +**2. Единый поток данных** +- Все данные проходят через ExperimentData +- Каждый Executor может читать и модифицировать данные +- Результаты сохраняются в структурированном виде + +**3. Разделение ответственности** +- Executor'ы выполняют вычисления +- Experiment'ы управляют последовательностью выполнения +- Reporter'ы форматируют результаты +- Shell'ы предоставляют удобный интерфейс + +**4. Расширяемость через полиморфизм** +- Абстрактные базовые классы определяют контракты +- Конкретные реализации следуют единому интерфейсу +- Новая функциональность добавляется через создание новых классов + +**5. Immutability где возможно** +- ExperimentData копируется при необходимости +- Transformers работают с копиями данных +- Состояние изменяется явно и контролируемо + +## 2. Обзор архитектуры + +### Три основных слоя + +Архитектура HypEx построена на трех основных слоях, каждый из которых имеет четкую зону ответственности: + +#### Слой Shell (Пользовательский интерфейс) +Самый верхний слой, предоставляющий готовые к использованию решения: +- **ExperimentShell** — базовый класс для всех оболочек +- **AATest, ABTest, HomogeneityTest, Matching** — предконфигурированные эксперименты +- **Output классы** — форматирование и представление результатов + +Этот слой скрывает всю сложность и позволяет запускать эксперименты в 2-3 строки кода: +```python +test = ABTest(multitest_method="bonferroni") +results = test.execute(data) +``` + +#### Слой Experiments (Оркестрация) +Средний слой, отвечающий за управление потоком выполнения: +- **Experiment** — базовый класс для композиции Executor'ов +- **Специализированные эксперименты** — OnRoleExperiment, GroupExperiment, ParamsExperiment +- **Управление итерациями** — CycledExperiment, IfParamsExperiment + +Этот слой определяет, КАК и В КАКОМ ПОРЯДКЕ выполняются вычисления. + +#### Слой Executors/Reporters (Исполнение и отчетность) +Нижний слой, где происходят фактические вычисления: +- **Executors** — выполняют конкретные операции (тесты, преобразования, анализ) +- **Reporters** — извлекают и форматируют результаты из ExperimentData + +Этот слой определяет, ЧТО именно вычисляется и КАК представляются результаты. + +### Взаимодействие между слоями + +``` +Пользователь + ↓ +[Shell Layer] + - Принимает простые параметры + - Создает сложную конфигурацию + - Возвращает отформатированные результаты + ↓ +[Experiments Layer] + - Управляет последовательностью + - Координирует выполнение + - Применяет стратегии (группы, итерации) + ↓ +[Executors/Reporters Layer] + - Выполняют атомарные операции + - Модифицируют ExperimentData + - Форматируют результаты +``` + +### Поток данных через систему + +1. **Инициализация**: Dataset оборачивается в ExperimentData +2. **Выполнение**: Каждый Executor в цепочке: + - Читает необходимые данные из ExperimentData + - Выполняет свою операцию + - Записывает результаты обратно в ExperimentData +3. **Отчетность**: Reporter извлекает результаты и форматирует их +4. **Вывод**: Output классы представляют результаты пользователю + +Ключевая особенность — ExperimentData служит общей шиной данных, через которую компоненты обмениваются информацией, не зная о существовании друг друга. + +## 3. Структуры данных: Dataset и ExperimentData + +### Dataset: Универсальный контейнер данных + +Dataset — это основная структура для хранения табличных данных в HypEx. Он обеспечивает: + +#### Архитектура Dataset + +**Компоненты:** +- **Backend** — адаптер для работы с конкретной реализацией (PandasBackend) +- **Roles** — словарь, сопоставляющий колонки с их семантическими ролями +- **Методы** — богатый API для манипуляции данными + +**Ключевые особенности:** +- Инкапсулирует pandas DataFrame, но может работать с другими backend'ами +- Каждая колонка имеет роль (Role), определяющую её семантику +- Поддерживает цепочки операций (fluent interface) +- Immutable по умолчанию (операции возвращают новый Dataset) + +#### Система ролей (ABCRole) + +Роли определяют семантическое значение колонок: + +``` +ABCRole (abstract) +├── InfoRole — информационные поля +├── TargetRole — целевые переменные +├── FeatureRole — признаки +├── TreatmentRole — индикатор группы эксперимента +├── GroupingRole — поле для группировки +├── StratificationRole — поле для стратификации +├── PreTargetRole — baseline значения +├── StatisticRole — статистические метрики +├── FilterRole — фильтрующие поля +├── TempRole — временные роли +│ ├── TempTargetRole +│ ├── TempTreatmentRole +│ └── TempGroupingRole +└── AdditionalRole — дополнительные поля + ├── AdditionalTargetRole + ├── AdditionalTreatmentRole + ├── AdditionalGroupingRole + └── AdditionalMatchingRole +``` + +Роли позволяют: +- Автоматически определять, какие колонки использовать для анализа +- Валидировать корректность данных +- Применять правильные преобразования + +#### Основные операции Dataset + +```python +# Создание +ds = Dataset(data=df, roles={'outcome': TargetRole(), 'group': TreatmentRole()}) + +# Фильтрация и выборка +ds_filtered = ds[ds['value'] > 100] +ds_subset = ds[['col1', 'col2']] + +# Группировка и агрегация +grouped = ds.groupby('category') +aggregated = ds.agg(['mean', 'std']) + +# Преобразования +ds_transformed = ds.apply(lambda x: x * 2, role={'result': StatisticRole()}) + +# Слияние +ds_merged = ds1.merge(ds2, on='id') +``` + +### ExperimentData: Контекст эксперимента + +ExperimentData — это расширенный контейнер, который хранит не только исходные данные, но и все промежуточные результаты, метаданные и состояние эксперимента. + +#### Структура ExperimentData + +```python +class ExperimentData: + # Основные данные + _data: Dataset # Исходный датасет + + # Дополнительные поля (новые колонки, созданные Executor'ами) + additional_fields: Dataset + + # Переменные (скалярные значения, метрики) + variables: dict[str, dict[str, Any]] + + # Группы (разбиение данных на подгруппы) + groups: dict[str, dict[str, Dataset]] + + # Таблицы анализа (результаты тестов и анализов) + analysis_tables: dict[str, Dataset] +``` + +#### Пространства имен (ExperimentDataEnum) + +ExperimentData организует данные в четыре пространства имен: + +1. **additional_fields** — новые признаки и колонки + - Результаты encoding'а + - Вычисленные features + - Matched индексы + +2. **variables** — скалярные значения и словари + - Параметры моделей + - Вычисленные константы + - Метрики качества + +3. **groups** — сгруппированные данные + - Разбиение на control/test + - Подгруппы для анализа + - Результаты стратификации + +4. **analysis_tables** — результаты анализов + - Результаты статистических тестов + - Таблицы сравнений + - Агрегированные метрики + +#### Взаимодействие с Executors + +Каждый Executor работает с ExperimentData по следующему паттерну: + +```python +def execute(self, data: ExperimentData) -> ExperimentData: + # 1. Извлечение необходимых данных + input_data = data.ds # или data.additional_fields, data.groups и т.д. + + # 2. Выполнение операции + result = self._inner_function(input_data) + + # 3. Сохранение результата в нужное пространство имен + return data.set_value( + space=ExperimentDataEnum.analysis_tables, + executor_id=self.id, + value=result + ) +``` + +#### Система идентификаторов + +Каждый Executor имеет уникальный ID, построенный по схеме: +``` +ClassName╤ParamsHash╤Key +``` + +Где: +- `ClassName` — имя класса Executor'а +- `ParamsHash` — хеш параметров +- `Key` — дополнительный ключ (например, имя колонки) + +Это позволяет: +- Избегать повторных вычислений +- Находить результаты конкретных Executor'ов +- Строить зависимости между компонентами +- Восстанавливать состояние Executor + +### Поток преобразования данных + +``` +Dataset → ExperimentData → [Executor1] → ExperimentData' → [Executor2] → ExperimentData'' + ↓ ↓ + Модификация Модификация + additional_fields analysis_tables +``` + +Каждый Executor может: +- Читать из любого пространства имен +- Писать в одно или несколько пространств +- Использовать результаты предыдущих Executor'ов +- Не влиять на исходные данные (immutability) + +## 4. Ядро системы: Executor Framework + +### Концепция Executor + +Executor — это фундаментальный строительный блок HypEx. Каждый Executor представляет собой атомарную операцию, которая: +- Принимает ExperimentData на вход +- Выполняет одну конкретную задачу +- Возвращает модифицированный ExperimentData + +Это позволяет строить сложные пайплайны из простых, переиспользуемых компонентов. + +### Базовый класс Executor + +```python +class Executor(ABC): + def __init__(self, key: Any = ""): + self._id: str = "" # Уникальный идентификатор + self._params_hash = "" # Хеш параметров + self.key: Any = key # Дополнительный ключ + self._generate_id() + + @abstractmethod + def execute(self, data: ExperimentData) -> ExperimentData: + """Основной метод выполнения""" + raise AbstractMethodError + + def set_params(self, params: dict) -> None: + """Динамическая установка параметров""" + # Позволяет изменять параметры после создания + + def _generate_id(self): + """Генерация уникального ID""" + # ClassName╤ParamsHash╤Key +``` + +**Ключевые особенности:** +- **Единый интерфейс** — все Executor'ы реализуют метод `execute` +- **Самоидентификация** — каждый Executor знает свой уникальный ID +- **Параметризация** — поддержка динамического изменения параметров +- **Композируемость** — Executor'ы можно комбинировать в цепочки + +### Три ветви наследования + +#### 1. Calculator — вычислительная ветвь + +Calculator добавляет возможность выполнять вычисления вне контекста ExperimentData: + +```python +class Calculator(Executor, ABC): + @classmethod + def calc(cls, data: Dataset, **kwargs): + """Статический метод для вычислений""" + return cls._inner_function(data, **kwargs) + + @staticmethod + @abstractmethod + def _inner_function(data: Dataset, **kwargs) -> Any: + """Реализация вычисления""" + pass +``` + +**Назначение:** Разделение логики вычисления от логики работы с ExperimentData. Это позволяет: +- Использовать вычисления отдельно от эксперимента +- Тестировать логику изолированно +- Переиспользовать код в разных контекстах + +**Основные подклассы Calculator:** + +``` +Calculator +├── Comparator — сравнение групп и статистические тесты +│ ├── GroupDifference, GroupSizes, PSI +│ ├── StatHypothesisTesting (TTest, KSTest, UTest, Chi2Test) +│ └── PowerTesting (MDEBySize) +├── Transformer — преобразование данных +│ ├── Filters (ConstFilter, CorrFilter, CVFilter, NanFilter, OutliersFilter) +│ ├── NaFiller — заполнение пропусков +│ ├── CategoryAggregator — агрегация категорий +│ └── Shuffle — перемешивание данных +├── Encoder — кодирование категориальных переменных +│ └── DummyEncoder — one-hot encoding +├── GroupOperator — операции над группами +│ ├── SMD — Standardized Mean Difference +│ ├── Bias — оценка смещения +│ └── MatchingMetrics — метрики matching'а +├── MLExecutor — машинное обучение +│ └── FaissNearestNeighbors — поиск ближайших соседей +└── Splitter — разделение на группы + ├── AASplitter — базовое разделение для A/A теста + └── AASplitterWithStratification — со стратификацией +``` + +#### 2. IfExecutor — условное выполнение + +IfExecutor реализует паттерн условного выполнения: + +```python +class IfExecutor(Executor, ABC): + def __init__(self, + if_executor: Executor | None = None, + else_executor: Executor | None = None): + self.if_executor = if_executor + self.else_executor = else_executor + + @abstractmethod + def check_rule(self, data) -> bool: + """Проверка условия""" + pass + + def execute(self, data: ExperimentData) -> ExperimentData: + if self.check_rule(data): + return self.if_executor.execute(data) if self.if_executor else data + else: + return self.else_executor.execute(data) if self.else_executor else data +``` + +**Назначение:** Ветвление логики выполнения на основе условий. + +**Пример использования — IfAAExecutor:** +```python +class IfAAExecutor(IfExecutor): + def check_rule(self, data) -> bool: + # Проверяет, прошел ли A/A тест + score_table = data.analysis_tables[...] + feature_pass = sum([...]) # Подсчет прошедших тестов + return feature_pass >= 1 +``` + +#### 3. Прямые наследники — Analyzers + +Analyzers наследуются напрямую от Executor и выполняют сложный анализ результатов: + +```python +# Прямые наследники Executor +├── OneAAStatAnalyzer — анализ статистики одного A/A теста +├── AAScoreAnalyzer — оценка качества A/A тестов и выбор лучшего +├── ABAnalyzer — анализ A/B теста с коррекцией множественного тестирования +└── MatchingAnalyzer — анализ качества matching'а +``` + +**Особенности Analyzers:** +- Работают с результатами других Executor'ов +- Агрегируют и анализируют множественные результаты +- Принимают сложные решения на основе статистики + +### Паттерн "Цепочка ответственности" + +Executor'ы образуют цепочку, где каждый: +1. Получает ExperimentData от предыдущего +2. Выполняет свою операцию +3. Передает обогащенный ExperimentData следующему + +```python +# Пример цепочки для A/B теста +chain = [ + GroupSizes(), # Подсчет размеров групп + GroupDifference(), # Вычисление разницы между группами + TTest(), # T-тест + KSTest(), # KS-тест + ABAnalyzer() # Анализ и коррекция множественного тестирования +] + +# Выполнение цепочки +data = ExperimentData(dataset) +for executor in chain: + data = executor.execute(data) +``` + +### Система идентификации и хеширования + +Каждый Executor имеет уникальный ID вида: `ClassName╤ParamsHash╤Key` + +**Генерация ParamsHash:** +```python +def _generate_params_hash(self): + # Для AASplitter + hash_parts = [] + if self.control_size != 0.5: + hash_parts.append(f"cs {self.control_size}") + if self.random_state is not None: + hash_parts.append(f"rs {self.random_state}") + self._params_hash = "|".join(hash_parts) +``` + +**Восстановление из ID:** +```python +@classmethod +def build_from_id(cls, executor_id: str): + """Восстанавливает Executor из его ID""" + splitted_id = executor_id.split(ID_SPLIT_SYMBOL) + result = cls() + result.init_from_hash(splitted_id[1]) # ParamsHash + return result +``` + +Это позволяет: +- **Кеширование** — не выполнять повторно одинаковые операции +- **Трассировка** — понимать, какой Executor создал какой результат +- **Воспроизводимость** — восстанавливать состояние из ID + +### Жизненный цикл Executor + +1. **Создание:** + ```python + executor = TTest(reliability=0.05) + # Генерируется ID: "TTest╤╤" + ``` + +2. **Конфигурация (опционально):** + ```python + executor.set_params({"reliability": 0.01}) + # ID обновляется: "TTest╤rel 0.01╤" + ``` + +3. **Выполнение:** + ```python + result = executor.execute(experiment_data) + # Результат сохраняется в ExperimentData под ID executor'а + ``` + +4. **Поиск результатов:** + ```python + # Найти все результаты TTest + ids = experiment_data.get_ids(TTest) + # Получить конкретный результат + result = experiment_data.analysis_tables[ids[0]] + ``` + +### Принципы проектирования Executor'ов + +1. **Единая ответственность** — один Executor = одна задача +2. **Независимость** — не должны знать о других Executor'ах +3. **Идемпотентность** — повторное выполнение дает тот же результат +4. **Самодостаточность** — содержат всю логику для своей задачи +5. **Testability** — легко тестировать изолированно + +### Расширение через наследование + +Создание нового Executor: + +```python +class MyCustomTest(StatHypothesisTesting): + @classmethod + def _inner_function(cls, data: Dataset, test_data: Dataset) -> Dataset: + # Реализация кастомного теста + statistic = calculate_my_statistic(data, test_data) + p_value = calculate_p_value(statistic) + + return Dataset.from_dict({ + "statistic": statistic, + "p-value": p_value, + "pass": p_value < cls.reliability + }) + + def execute(self, data: ExperimentData) -> ExperimentData: + # Использует базовую логику StatHypothesisTesting + return super().execute(data) +``` + +Таким образом, Executor Framework обеспечивает: +- **Модульность** — каждая операция инкапсулирована +- **Переиспользуемость** — Executor'ы можно комбинировать по-разному +- **Расширяемость** — легко добавлять новую функциональность +- **Тестируемость** — каждый компонент можно тестировать отдельно + +## 5. Слой вычислений: Comparators, Transformers, Operators + +Слой вычислений содержит конкретные реализации Calculator'ов, каждая из которых специализируется на определенном типе операций. Все они следуют паттерну разделения вычислительной логики от работы с ExperimentData. + +### Comparators: Сравнение и тестирование + +Comparators отвечают за сравнение групп и проведение статистических тестов. Они имеют сложную иерархию и богатую функциональность. + +#### Архитектура Comparator + +```python +class Comparator(Calculator, ABC): + def __init__(self, + compare_by: Literal["groups", "columns", "columns_in_groups", "cross"], + grouping_role: ABCRole = GroupingRole(), + target_roles: ABCRole | list[ABCRole] = TargetRole(), + baseline_role: ABCRole = PreTargetRole()): + self.compare_by = compare_by + self.grouping_role = grouping_role + self.target_roles = target_roles + self.baseline_role = baseline_role +``` + +**Режимы сравнения (compare_by):** + +#### Режим "groups" — сравнение между группами +Самый распространенный режим для A/B тестов. Сравнивает одну и ту же метрику между разными группами (control vs test). + +```python +# Данные: +# | user_id | group | revenue | retention | +# |---------|---------|---------|-----------| +# | 1 | control | 100 | 1 | +# | 2 | test | 150 | 1 | +# | 3 | control | 80 | 0 | +# | 4 | test | 120 | 1 | + +comparator = TTest( + compare_by="groups", + grouping_role=TreatmentRole(), # группировка по "group" + target_roles=TargetRole() # анализ "revenue" и "retention" +) + +# Результат: +# Для revenue: сравнение control_revenue vs test_revenue +# Для retention: сравнение control_retention vs test_retention +``` + +#### Режим "columns" — сравнение колонок между собой +Сравнивает разные метрики внутри всего датасета. Полезно для анализа корреляций или изменений между pre/post периодами. + +```python +# Данные: +# | user_id | pre_revenue | post_revenue | pre_clicks | post_clicks | +# |---------|-------------|--------------|------------|-------------| +# | 1 | 100 | 150 | 10 | 15 | +# | 2 | 80 | 90 | 8 | 12 | + +comparator = GroupDifference( + compare_by="columns", + baseline_role=PreTargetRole(), # baseline: "pre_revenue", "pre_clicks" + target_roles=TargetRole() # сравнить с: "post_revenue", "post_clicks" +) + +# Результат: +# Сравнение pre_revenue vs post_revenue (для всех пользователей) +# Сравнение pre_clicks vs post_clicks (для всех пользователей) +``` + +#### Режим "columns_in_groups" — сравнение колонок внутри каждой группы +Комбинация первых двух режимов. Сравнивает разные метрики, но отдельно для каждой группы. + +```python +# Данные: +# | user_id | group | before_treatment | after_treatment | +# |---------|---------|------------------|-----------------| +# | 1 | control | 100 | 105 | +# | 2 | test | 100 | 150 | +# | 3 | control | 80 | 82 | +# | 4 | test | 80 | 120 | + +comparator = TTest( + compare_by="columns_in_groups", + grouping_role=TreatmentRole(), # группировка по "group" + baseline_role=PreTargetRole(), # baseline: "before_treatment" + target_roles=TargetRole() # сравнить с: "after_treatment" +) + +# Результат: +# Для группы control: сравнение before_treatment vs after_treatment +# Для группы test: сравнение before_treatment vs after_treatment +# Позволяет оценить эффект внутри каждой группы отдельно +``` + +#### Режим "cross" — перекрестное сравнение +Самый сложный режим. Берет baseline из одной группы (обычно control) и сравнивает с метриками из других групп. Используется для difference-in-differences анализа. + +```python +# Данные: +# | user_id | group | metric_A | metric_B | +# |---------|---------|----------|----------| +# | 1 | control | 100 | 50 | +# | 2 | test1 | 120 | 60 | +# | 3 | control | 90 | 45 | +# | 4 | test2 | 130 | 70 | + +comparator = TTest( + compare_by="cross", + grouping_role=TreatmentRole(), # группировка по "group" + baseline_role=PreTargetRole(), # baseline из control: "metric_A" + target_roles=TargetRole() # сравнить с метриками из test групп +) + +# Результат: +# control.metric_A vs test1.metric_A +# control.metric_A vs test1.metric_B +# control.metric_A vs test2.metric_A +# control.metric_A vs test2.metric_B + +# Это позволяет оценить, насколько изменения в test группах +# отличаются от baseline в control группе +``` + +**Практическое применение режима "cross":** + +Режим "cross" особенно полезен для: +1. **Difference-in-Differences (DiD)** — оценка причинно-следственной связи +2. **Synthetic control** — когда control группа служит базой для сравнения +3. **Multiple treatment arms** — когда есть несколько вариантов воздействия + +```python +# Пример DiD анализа +# До внедрения фичи: +# control: revenue = 100, retention = 0.5 +# test: revenue = 100, retention = 0.5 + +# После внедрения фичи (только в test): +# control: revenue = 110, retention = 0.52 (естественный рост) +# test: revenue = 140, retention = 0.65 (рост + эффект фичи) + +# С режимом "cross" можно оценить: +# (test_after - test_before) - (control_after - control_before) +# = (140 - 100) - (110 - 100) = 30 - истинный эффект фичи +``` + +#### Базовые метрики (GroupDifference, GroupSizes) + +**GroupDifference** вычисляет разницу между группами: +```python +class GroupDifference(Comparator): + def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: + control_mean = data.mean() + test_mean = test_data.mean() + return { + "control mean": control_mean, + "test mean": test_mean, + "difference": test_mean - control_mean, + "difference %": (test_mean / control_mean - 1) * 100 + } +``` + +**GroupSizes** подсчитывает размеры групп: +```python +class GroupSizes(Comparator): + def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: + size_a = len(data) + size_b = len(test_data) + return { + "control size": size_a, + "test size": size_b, + "control size %": (size_a / (size_a + size_b)) * 100, + "test size %": (size_b / (size_a + size_b)) * 100 + } +``` + +#### Статистические тесты (StatHypothesisTesting) + +Абстрактный класс StatHypothesisTesting добавляет концепцию статистической значимости: + +```python +class StatHypothesisTesting(Comparator, ABC): + def __init__(self, reliability: float = 0.05, **kwargs): + self.reliability = reliability # Уровень значимости + super().__init__(**kwargs) +``` + +**Конкретные реализации:** + +1. **TTest** — t-тест Стьюдента для нормальных распределений +2. **KSTest** — тест Колмогорова-Смирнова для любых распределений +3. **UTest** — U-тест Манна-Уитни для ненормальных распределений +4. **Chi2Test** — хи-квадрат тест для категориальных переменных + +Каждый тест возвращает стандартизированный результат: +```python +{ + "p-value": 0.042, + "statistic": 2.15, + "pass": True # p-value < reliability +} +``` + +#### Специализированные метрики + +**PSI (Population Stability Index)** — измеряет стабильность распределения: +```python +class PSI(Comparator): + def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: + # Разбиение на бакеты и вычисление PSI + psi = sum((y - x) * np.log(y / x) for x, y in zip(data_psi, test_data_psi)) + return {"PSI": psi} +``` + +**MahalanobisDistance** — вычисляет расстояние Махаланобиса: +```python +class MahalanobisDistance(Calculator): + def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: + # Вычисление ковариационной матрицы + cov = (data.cov() + test_data.cov()) / 2 + # Преобразование Холецкого и вычисление расстояния + cholesky = CholeskyExtension().calc(cov) + mahalanobis_transform = InverseExtension().calc(cholesky) + y_control = data.dot(mahalanobis_transform.transpose()) + y_test = test_data.dot(mahalanobis_transform.transpose()) + return {"control": y_control, "test": y_test} +``` + +### Transformers: Преобразование данных + +Transformers изменяют сам Dataset (в отличие от других Calculator'ов, которые только вычисляют результаты). Они работают с копией данных для обеспечения immutability. + +#### Базовый класс Transformer + +```python +class Transformer(Calculator, ABC): + @property + def _is_transformer(self) -> bool: + return True # Маркер для Experiment + + def execute(self, data: ExperimentData) -> ExperimentData: + # Создает копию и модифицирует данные + return data.copy(data=self.calc(data=data.ds)) +``` + +#### Фильтры данных + +Фильтры изменяют роли колонок или удаляют строки/колонки: + +**ConstFilter** — фильтрует константные колонки: +```python +class ConstFilter(Transformer): + def __init__(self, threshold: float = 0.95): + self.threshold = threshold + + def _inner_function(data: Dataset, target_cols, threshold) -> Dataset: + for column in target_cols: + value_counts = data[column].value_counts(normalize=True) + if value_counts.iloc[0] > threshold: + data.roles[column] = InfoRole() # Понижение роли + return data +``` + +**CorrFilter** — удаляет коррелированные признаки: +```python +class CorrFilter(Transformer): + def _inner_function(data: Dataset, threshold: float = 0.8) -> Dataset: + corr_matrix = data.corr() + for col1, col2 in high_corr_pairs: + # Удаляем признак с меньшей вариативностью + if data[col1].cv() < data[col2].cv(): + data.roles[col1] = InfoRole() + return data +``` + +**OutliersFilter** — фильтрует выбросы по процентилям: +```python +class OutliersFilter(Transformer): + def __init__(self, lower_percentile: float = 0.05, upper_percentile: float = 0.95): + self.lower_percentile = lower_percentile + self.upper_percentile = upper_percentile + + def _inner_function(data: Dataset, target_cols, lower, upper) -> Dataset: + mask = (data[target_cols] < data[target_cols].quantile(lower)) | \ + (data[target_cols] > data[target_cols].quantile(upper)) + return data.drop(data[mask].index) +``` + +#### Обработка данных + +**NaFiller** — заполнение пропущенных значений: +```python +class NaFiller(Transformer): + def __init__(self, method: Literal["ffill", "bfill"] = None, values=None): + self.method = method + self.values = values + + def _inner_function(data: Dataset, target_cols, method, values) -> Dataset: + for column in target_cols: + data[column] = data[column].fillna(values=values, method=method) + return data +``` + +**CategoryAggregator** — объединение редких категорий: +```python +class CategoryAggregator(Transformer): + def __init__(self, threshold: int = 15, new_group_name: str = "Other"): + self.threshold = threshold + self.new_group_name = new_group_name + + def _inner_function(data: Dataset, target_cols, threshold, new_name) -> Dataset: + for column in target_cols: + value_counts = data[column].value_counts() + rare_values = value_counts[value_counts < threshold].index + data[column] = data[column].replace(rare_values, new_name) + return data +``` + +**Shuffle** — перемешивание данных: +```python +class Shuffle(Transformer): + def __init__(self, random_state: int = None): + self.random_state = random_state + + def _inner_function(data: Dataset, random_state) -> Dataset: + return data.sample(frac=1, random_state=random_state) +``` + +### Encoders: Кодирование категориальных переменных + +Encoders преобразуют категориальные переменные в числовые. Результат сохраняется в additional_fields. + +```python +class Encoder(Calculator): + def execute(self, data: ExperimentData) -> ExperimentData: + target_cols = data.ds.search_columns(roles=FeatureRole(), + search_types=[str]) + encoded = self.calc(data=data.ds, target_cols=target_cols) + # Сохраняем в additional_fields + return data.set_value( + space=ExperimentDataEnum.additional_fields, + executor_id=self._ids_to_names(encoded.columns), + value=encoded + ) +``` + +**DummyEncoder** — one-hot encoding: +```python +class DummyEncoder(Encoder): + def _inner_function(data: Dataset, target_cols) -> Dataset: + # Использует pandas.get_dummies + dummies_df = pd.get_dummies(data[target_cols], drop_first=True) + # Устанавливает роли для новых колонок + roles = {col: data.roles[col.split('_')[0]] for col in dummies_df.columns} + return Dataset(data=dummies_df, roles=roles) +``` + +### GroupOperators: Операции над группами + +GroupOperators выполняют специализированные операции над группами данных, часто используемые в matching и causal inference. + +#### SMD (Standardized Mean Difference) + +Стандартизированная разница средних — метрика баланса ковариат: +```python +class SMD(GroupOperator): + def _inner_function(cls, data: Dataset, test_data: Dataset) -> float: + return (data.mean() - test_data.mean()) / data.std() +``` + +#### Bias — оценка смещения + +Оценивает смещение при matching: +```python +class Bias(GroupOperator): + @staticmethod + def calc_coefficients(X: Dataset, Y: Dataset) -> list[float]: + # Линейная регрессия для оценки коэффициентов + return np.linalg.lstsq(X.values, Y.values, rcond=-1)[0] + + @staticmethod + def calc_bias(X: Dataset, X_matched: Dataset, coefficients: list[float]) -> list[float]: + # Вычисление смещения на основе разницы признаков + return [(j - i).dot(coefficients) for i, j in zip(X.values, X_matched.values)] +``` + +#### MatchingMetrics — метрики для matching + +Вычисляет различные метрики treatment effect: +```python +class MatchingMetrics(GroupOperator): + def __init__(self, metric: Literal["atc", "att", "ate"] = "ate"): + self.metric = metric # Average Treatment on Controls/Treated/Everyone + + def _inner_function(cls, data, test_data, target_fields, metric, bias) -> dict: + # Вычисление Individual Treatment Effect + itt = test_data[target_fields[0]] - test_data[target_fields[1]] + itc = data[target_fields[1]] - data[target_fields[0]] + + # Коррекция на смещение + if bias: + itt -= bias["test"] + itc -= bias["control"] + + # Вычисление метрик с учетом весов + att = itt.mean() + atc = itc.mean() + ate = (att * len(test_data) + atc * len(data)) / (len(test_data) + len(data)) + + # Вычисление стандартных ошибок и p-values + return { + "ATT": [att, se_att, p_val_att, ci_lower_att, ci_upper_att], + "ATC": [atc, se_atc, p_val_atc, ci_lower_atc, ci_upper_atc], + "ATE": [ate, se_ate, p_val_ate, ci_lower_ate, ci_upper_ate] + } +``` + +### MLExecutors: Машинное обучение + +MLExecutors интегрируют алгоритмы машинного обучения в pipeline экспериментов. + +#### Базовый класс MLExecutor + +```python +class MLExecutor(Calculator, ABC): + @abstractmethod + def fit(self, X: Dataset, Y: Dataset = None) -> MLExecutor: + """Обучение модели""" + pass + + @abstractmethod + def predict(self, X: Dataset) -> Dataset: + """Предсказание""" + pass + + def score(self, X: Dataset, Y: Dataset) -> float: + """Оценка качества""" + pass +``` + +#### FaissNearestNeighbors — поиск ближайших соседей + +Использует библиотеку FAISS для эффективного поиска ближайших соседей: +```python +class FaissNearestNeighbors(MLExecutor): + def __init__(self, + n_neighbors: int = 1, + two_sides: bool = False, # Искать пары в обе стороны + test_pairs: bool = False, # Пары для test группы + faiss_mode: Literal["base", "fast", "auto"] = "auto"): + self.n_neighbors = n_neighbors + self.faiss_mode = faiss_mode + + def fit(self, X: Dataset) -> MLExecutor: + # Создание индекса FAISS + self.index = faiss.IndexFlatL2(X.shape[1]) + if len(X) > 1_000_000 and self.faiss_mode in ["auto", "fast"]: + # Используем приближенный поиск для больших данных + self.index = faiss.IndexIVFFlat(self.index, 1, 1000) + self.index.train(X.values) + self.index.add(X.values) + return self + + def predict(self, X: Dataset) -> Dataset: + # Поиск ближайших соседей + distances, indices = self.index.search(X.values, self.n_neighbors) + return Dataset.from_dict({"indices": indices}) +``` + +### Splitters: Разделение на группы + +Splitters отвечают за разделение данных на группы для A/A и A/B тестов. + +#### AASplitter — базовое разделение + +```python +class AASplitter(Calculator): + def __init__(self, + control_size: float = 0.5, + random_state: int = None, + sample_size: float = None): # Доля от общих данных + self.control_size = control_size + self.random_state = random_state + self.sample_size = sample_size + + def _inner_function(data: Dataset, control_size, random_state, sample_size) -> list[str]: + # Сэмплирование если нужно + if sample_size: + data = data.sample(frac=sample_size, random_state=random_state) + + # Разделение на control/test + n_control = int(len(data) * control_size) + indices = data.sample(frac=1, random_state=random_state).index + + split = pd.Series("test", index=data.index) + split[indices[:n_control]] = "control" + + return split.tolist() +``` + +#### AASplitterWithStratification — стратифицированное разделение + +```python +class AASplitterWithStratification(AASplitter): + def _inner_function(data: Dataset, control_size, random_state, grouping_fields) -> Dataset: + if not grouping_fields: + return super()._inner_function(data, control_size, random_state) + + # Разделение внутри каждой страты + result = [] + for group, group_data in data.groupby(grouping_fields): + split = super()._inner_function(group_data, control_size, random_state) + result.extend(split) + + return Dataset.from_dict({"split": result}, roles={"split": TreatmentRole()}) +``` + +### Принципы проектирования Calculator'ов + +1. **Разделение вычисления и контекста:** + - `_inner_function` — чистая логика вычисления + - `execute` — работа с ExperimentData + - `calc` — статический интерфейс для использования вне экспериментов + +2. **Унифицированный результат:** + - Всегда возвращают Dataset или dict + - Результаты имеют стандартную структуру + - Роли определяют семантику результатов + +3. **Конфигурируемость:** + - Параметры передаются через конструктор + - Поддержка `set_params` для динамического изменения + - Параметры влияют на ID для кеширования + +4. **Поиск подходящих данных:** + - Используют роли для поиска нужных колонок + - Могут фильтровать по типам данных + - Работают с temporary roles при необходимости + +### Взаимодействие компонентов + +```python +# Пример pipeline для matching +pipeline = [ + # 1. Подготовка данных + OutliersFilter(lower_percentile=0.05, upper_percentile=0.95), + NaFiller(method="ffill"), + DummyEncoder(), + + # 2. Вычисление расстояний + MahalanobisDistance(grouping_role=TreatmentRole()), + + # 3. Поиск пар + FaissNearestNeighbors(n_neighbors=1), + + # 4. Оценка качества + Bias(grouping_role=TreatmentRole()), + MatchingMetrics(metric="ate"), + + # 5. Статистические тесты + TTest(compare_by="groups"), + KSTest(compare_by="groups") +] +``` + +Каждый компонент: +- Независим и может быть заменен +- Использует результаты предыдущих через ExperimentData +- Добавляет свои результаты для последующих + +Это обеспечивает максимальную гибкость при построении экспериментов. + +## 6. Слой экспериментов: Experiment Framework + +Слой экспериментов управляет композицией и оркестрацией Executor'ов. Он определяет, КАК и В КАКОМ ПОРЯДКЕ выполняются операции, предоставляя различные стратегии выполнения. + +### Базовый класс Experiment + +Experiment — это контейнер для цепочки Executor'ов с логикой управления их выполнением: + +```python +class Experiment(Executor): + def __init__(self, + executors: Sequence[Executor], + transformer: bool = None, + key: Any = ""): + self.executors = executors + self.transformer = transformer or self._detect_transformer() + super().__init__(key) + + def _detect_transformer(self) -> bool: + """Автоматически определяет, содержит ли цепочка Transformer'ы""" + return all(executor._is_transformer for executor in self.executors) + + def execute(self, data: ExperimentData) -> ExperimentData: + """Последовательное выполнение всех Executor'ов""" + experiment_data = deepcopy(data) if self.transformer else data + for executor in self.executors: + executor.key = self.key + experiment_data = executor.execute(experiment_data) + return experiment_data +``` + +**Ключевые особенности:** +- **Композиция** — объединяет множество Executor'ов +- **Порядок важен** — Executor'ы выполняются последовательно +- **Управление состоянием** — копирует данные если есть Transformer'ы +- **Наследование от Executor** — Experiment сам является Executor'ом (паттерн Composite) + +### OnRoleExperiment — выполнение для каждой роли + +OnRoleExperiment применяет цепочку Executor'ов к каждой колонке с определенной ролью: + +```python +class OnRoleExperiment(Experiment): + def __init__(self, + executors: list[Executor], + role: ABCRole | Sequence[ABCRole]): + self.role = [role] if isinstance(role, ABCRole) else list(role) + super().__init__(executors) + + def execute(self, data: ExperimentData) -> ExperimentData: + # Находим все колонки с нужной ролью + for field in data.ds.search_columns(self.role): + # Устанавливаем временную роль для текущей колонки + data.ds.tmp_roles = {field: TempTargetRole()} + # Выполняем всю цепочку для этой колонки + data = super().execute(data) + # Очищаем временную роль + data.ds.tmp_roles = {} + return data +``` + +**Пример использования:** +```python +# Применить тесты ко всем target метрикам +experiment = OnRoleExperiment( + executors=[ + GroupDifference(grouping_role=TreatmentRole()), + TTest(grouping_role=TreatmentRole()), + KSTest(grouping_role=TreatmentRole()) + ], + role=TargetRole() # Будет применено к каждой колонке с TargetRole +) + +# Если есть колонки: revenue (TargetRole), retention (TargetRole), clicks (FeatureRole) +# То тесты будут применены к revenue и retention, но не к clicks +``` + +### ExperimentWithReporter — эксперименты с отчетностью + +Добавляет возможность генерации отчетов после выполнения: + +```python +class ExperimentWithReporter(Experiment): + def __init__(self, + executors: Sequence[Executor], + reporter: Reporter): + super().__init__(executors) + self.reporter = reporter + + def one_iteration(self, + data: ExperimentData, + key: str = "") -> Dataset: + """Одна итерация эксперимента с отчетом""" + t_data = ExperimentData(data.ds) + self.key = key + t_data = super().execute(t_data) + return self.reporter.report(t_data) +``` + +Это базовый класс для специализированных экспериментов, которые нуждаются в форматированном выводе результатов. + +### CycledExperiment — многократное выполнение + +Выполняет эксперимент заданное количество раз (например, для оценки стабильности): + +```python +class CycledExperiment(ExperimentWithReporter): + def __init__(self, + executors: list[Executor], + reporter: DatasetReporter, + n_iterations: int): + super().__init__(executors, reporter) + self.n_iterations = n_iterations + + def execute(self, data: ExperimentData) -> ExperimentData: + results = [] + for i in tqdm(range(self.n_iterations)): + # Каждая итерация начинается с чистых данных + result = self.one_iteration(data, str(i)) + results.append(result) + + # Объединяем результаты всех итераций + final_result = results[0].append(results[1:]) + return self._set_value(data, final_result) +``` + +**Применение:** +- Оценка вариативности метрик +- Bootstrap анализ +- Проверка устойчивости результатов + +### GroupExperiment — выполнение по группам + +Применяет эксперимент отдельно к каждой группе данных: + +```python +class GroupExperiment(ExperimentWithReporter): + def __init__(self, + executors: Sequence[Executor], + reporter: Reporter, + searching_role: ABCRole = GroupingRole()): + self.searching_role = searching_role + super().__init__(executors, reporter) + + def execute(self, data: ExperimentData) -> ExperimentData: + group_field = data.ds.search_columns(self.searching_role) + results = [] + + # Применяем эксперимент к каждой группе отдельно + for group, group_data in data.ds.groupby(group_field): + result = self.one_iteration( + ExperimentData(group_data), + str(group[0]) + ) + results.append(result) + + # Объединяем результаты с сохранением индексов групп + return self._set_result(data, results, reset_index=False) +``` + +**Применение:** +- Анализ по сегментам +- Гетерогенные эффекты +- Подгрупповой анализ + +### ParamsExperiment — параметрический поиск + +Выполняет эксперимент с различными комбинациями параметров: + +```python +class ParamsExperiment(ExperimentWithReporter): + def __init__(self, + executors: Sequence[Executor], + reporter: DatasetReporter, + params: dict[type, dict[str, Sequence[Any]]]): + super().__init__(executors, reporter) + self._params = params + self._flat_params = [] # Все комбинации параметров + + def _update_flat_params(self): + """Генерирует все комбинации параметров""" + # Пример params: + # { + # AASplitter: { + # "random_state": [0, 1, 2], + # "control_size": [0.4, 0.5, 0.6] + # }, + # TTest: { + # "reliability": [0.01, 0.05, 0.1] + # } + # } + # Создаст 3 * 3 * 3 = 27 комбинаций + + param_combinations = itertools.product(*[ + itertools.product(*[ + itertools.product([param], values) + for param, values in class_params.items() + ]) + for class_params in self._params.values() + ]) + + self._flat_params = [ + {class_: dict(params) for class_, params in combination} + for combination in param_combinations + ] + + def execute(self, data: ExperimentData) -> ExperimentData: + self._update_flat_params() + results = [] + + for flat_param in tqdm(self._flat_params): + t_data = ExperimentData(data.ds) + + # Устанавливаем параметры для каждого Executor + for executor in self.executors: + executor.set_params(flat_param) + t_data = executor.execute(t_data) + + # Собираем отчет для этой комбинации + report = self.reporter.report(t_data) + results.append(report) + + return self._set_result(data, results) +``` + +**Применение:** +- Grid search для оптимальных параметров +- Sensitivity analysis +- A/A тестирование с разными random_state + +**Пример использования:** +```python +# A/A тест с 2000 различными разбиениями +params_exp = ParamsExperiment( + executors=[ + AASplitter(), # Будет параметризован + GroupSizes(grouping_role=AdditionalTreatmentRole()), + TTest(grouping_role=AdditionalTreatmentRole()) + ], + params={ + AASplitter: { + "random_state": range(2000), # 2000 разных разбиений + "control_size": [0.5] + } + }, + reporter=AADictReporter() +) +``` + +### IfParamsExperiment — параметрический поиск с ранней остановкой + +Расширение ParamsExperiment с возможностью остановки при выполнении условия: + +```python +class IfParamsExperiment(ParamsExperiment): + def __init__(self, + executors: Sequence[Executor], + reporter: DatasetReporter, + params: dict[type, dict[str, Sequence[Any]]], + stopping_criterion: IfExecutor): + super().__init__(executors, reporter, params) + self.stopping_criterion = stopping_criterion + + def execute(self, data: ExperimentData) -> ExperimentData: + self._update_flat_params() + + for flat_param in tqdm(self._flat_params): + t_data = ExperimentData(data.ds) + + # Выполняем эксперимент + for executor in self.executors: + executor.set_params(flat_param) + t_data = executor.execute(t_data) + + # Проверяем условие остановки + if_result = self.stopping_criterion.execute(t_data) + if_executor_id = if_result.get_one_id( + self.stopping_criterion.__class__, + ExperimentDataEnum.variables + ) + + if if_result.variables[if_executor_id]["response"]: + # Условие выполнено - останавливаемся + return self._set_result(data, [self.reporter.report(t_data)]) + + # Условие не выполнено ни для одной комбинации + return data +``` + +**Применение:** +- Поиск первого "хорошего" разбиения для A/A теста +- Early stopping в оптимизации +- Адаптивные эксперименты + +### Композиция экспериментов + +Эксперименты можно вкладывать друг в друга, создавая сложные pipeline'ы: + +```python +# Сложный эксперимент для A/A тестирования +aa_experiment = Experiment([ + # 1. Параметрический поиск лучшего разбиения + ParamsExperiment( + executors=[ + AASplitter(), + Experiment([ # Вложенный эксперимент + GroupSizes(), + OnRoleExperiment( # Применить тесты ко всем targets + executors=[TTest(), KSTest()], + role=TargetRole() + ) + ]) + ], + params={ + AASplitter: {"random_state": range(100)} + }, + reporter=OneAADictReporter() + ), + + # 2. Анализ результатов + AAScoreAnalyzer(alpha=0.05), + + # 3. Условное выполнение на основе результатов + IfAAExecutor( + if_executor=Experiment([...]), # Если тест прошел + else_executor=Experiment([...]) # Если тест не прошел + ) +]) +``` + +### Жизненный цикл Experiment + +1. **Создание и конфигурация:** +```python +experiment = Experiment( + executors=[executor1, executor2, executor3] +) +``` + +2. **Детекция типа:** +```python +# Автоматически определяет, нужно ли копировать данные +if experiment.transformer: # True если есть Transformers + # Будет работать с копией данных +``` + +3. **Выполнение:** +```python +result = experiment.execute(experiment_data) +# Каждый executor выполняется последовательно +# Результаты накапливаются в ExperimentData +``` + +4. **Доступ к результатам:** +```python +# Experiment может сам сохранить агрегированный результат +experiment_id = experiment.id +aggregated_result = result.analysis_tables[experiment_id] + +# Или можно получить результаты отдельных Executor'ов +executor_ids = experiment.get_executor_ids([TTest, KSTest]) +``` + +### Паттерны использования + +#### 1. Pipeline паттерн — последовательная обработка + +Самый базовый паттерн, где операции выполняются строго последовательно, и каждая использует результаты предыдущей. + +```python +# Классический pipeline для A/B теста +ab_pipeline = Experiment([ + # Этап 1: Подготовка данных + NaFiller(method="ffill"), # Заполнение пропусков + OutliersFilter(lower_percentile=0.05), # Удаление выбросов + DummyEncoder(), # Кодирование категорий + + # Этап 2: Основные метрики + GroupSizes(grouping_role=TreatmentRole()), + GroupDifference(grouping_role=TreatmentRole()), + + # Этап 3: Статистические тесты + TTest(grouping_role=TreatmentRole()), + KSTest(grouping_role=TreatmentRole()), + + # Этап 4: Анализ результатов + ABAnalyzer(multitest_method="bonferroni") +]) + +# Можно создавать переиспользуемые подпайплайны +data_preparation = Experiment([ + NaFiller(method="ffill"), + OutliersFilter(lower_percentile=0.05, upper_percentile=0.95), + ConstFilter(threshold=0.95), + CorrFilter(threshold=0.8), + DummyEncoder() +]) + +statistical_tests = Experiment([ + TTest(grouping_role=TreatmentRole()), + KSTest(grouping_role=TreatmentRole()), + Chi2Test(grouping_role=TreatmentRole()) +]) + +# Композиция подпайплайнов +full_pipeline = Experiment([ + data_preparation, # Experiment как Executor + statistical_tests, # Experiment как Executor + ABAnalyzer() +]) +``` + +**Преимущества:** +- Простота понимания и отладки +- Легко модифицировать отдельные этапы +- Возможность переиспользования подпайплайнов + +#### 2. Branching паттерн — условное ветвление + +Выбор пути выполнения на основе характеристик данных или промежуточных результатов. + +```python +# Адаптивный выбор теста на основе характеристик данных +class DataCharacterizer(Executor): + def execute(self, data: ExperimentData) -> ExperimentData: + # Анализируем характеристики данных + is_normal = self._check_normality(data.ds) + sample_size = len(data.ds) + has_categories = self._has_categorical(data.ds) + + # Сохраняем характеристики + return data.set_value( + ExperimentDataEnum.variables, + self.id, + { + "is_normal": is_normal, + "sample_size": sample_size, + "has_categories": has_categories + } + ) + +class AdaptiveTestSelector(IfExecutor): + def check_rule(self, data: ExperimentData) -> bool: + chars = data.variables[data.get_one_id(DataCharacterizer)] + # Выбираем подходящий тест + return chars["is_normal"] and chars["sample_size"] > 30 + +adaptive_testing = Experiment([ + DataCharacterizer(), + AdaptiveTestSelector( + # Для нормальных данных с большой выборкой + if_executor=Experiment([ + TTest(grouping_role=TreatmentRole()), + PowerTesting(significance=0.95) + ]), + # Для ненормальных или малых выборок + else_executor=Experiment([ + UTest(grouping_role=TreatmentRole()), + KSTest(grouping_role=TreatmentRole()) + ]) + ) +]) + +# Многоуровневое ветвление для matching +matching_pipeline = Experiment([ + # Проверка качества данных + DataQualityChecker(), + + IfExecutor( + condition=lambda d: d.quality_score > 0.9, + if_executor=Experiment([ + # Высокое качество - используем точный matching + MahalanobisDistance(), + FaissNearestNeighbors(n_neighbors=1, faiss_mode="base") + ]), + else_executor=IfExecutor( + condition=lambda d: d.quality_score > 0.5, + if_executor=Experiment([ + # Среднее качество - используем приближенный matching + FaissNearestNeighbors(n_neighbors=3, faiss_mode="fast") + ]), + else_executor=Experiment([ + # Низкое качество - используем propensity score + PropensityScoreMatching() + ]) + ) + ) +]) +``` + +**Преимущества:** +- Адаптация к характеристикам данных +- Оптимизация производительности +- Обработка edge cases + +#### 3. Fan-out/Fan-in паттерн — параллельное применение + +Применение разных анализов к одним данным с последующей агрегацией результатов. + +```python +# Fan-out: применяем разные тесты ко всем метрикам +fanout_tests = OnRoleExperiment( + executors=[ + # Ветка 1: Параметрические тесты + Experiment([ + TTest(grouping_role=TreatmentRole()), + GroupDifference(grouping_role=TreatmentRole()) + ]), + + # Ветка 2: Непараметрические тесты + Experiment([ + KSTest(grouping_role=TreatmentRole()), + UTest(grouping_role=TreatmentRole()) + ]), + + # Ветка 3: Анализ мощности + Experiment([ + PowerTesting(significance=0.95, power=0.8), + MDEBySize() + ]) + ], + role=TargetRole() # Применить ко всем target метрикам +) + +# Fan-in: агрегация результатов разных тестов +class TestAggregator(Executor): + def execute(self, data: ExperimentData) -> ExperimentData: + # Собираем результаты всех тестов + ttest_results = data.analysis_tables[data.get_one_id(TTest)] + kstest_results = data.analysis_tables[data.get_one_id(KSTest)] + utest_results = data.analysis_tables[data.get_one_id(UTest)] + + # Агрегируем (например, голосованием) + aggregated = self._aggregate_by_voting([ + ttest_results, kstest_results, utest_results + ]) + + return data.set_value( + ExperimentDataEnum.analysis_tables, + self.id, + aggregated + ) + +# Полный fan-out/fan-in pipeline +ensemble_testing = Experiment([ + fanout_tests, # Fan-out + TestAggregator() # Fan-in +]) + +# Параллельный анализ по сегментам +segment_analysis = Experiment([ + # Fan-out по разным сегментам + GroupExperiment( + executors=[ + GroupDifference(), + TTest() + ], + searching_role=SegmentRole(), # age_group, region, etc. + reporter=SegmentReporter() + ), + + # Fan-in: сводная таблица по всем сегментам + SegmentAggregator() +]) +``` + +**Преимущества:** +- Параллельная обработка независимых анализов +- Ансамблирование результатов для надежности +- Comprehensive анализ с разных углов + +#### 4. Grid Search паттерн — поиск оптимальных параметров + +Систематический перебор комбинаций параметров для поиска оптимальных. + +```python +# Поиск оптимального разбиения для A/A теста +aa_grid_search = ParamsExperiment( + executors=[ + AASplitter(), # Будет параметризован + Experiment([ + GroupSizes(grouping_role=AdditionalTreatmentRole()), + OnRoleExperiment( + executors=[ + TTest(grouping_role=AdditionalTreatmentRole()), + KSTest(grouping_role=AdditionalTreatmentRole()) + ], + role=TargetRole() + ) + ]), + OneAAStatAnalyzer() + ], + params={ + AASplitter: { + "random_state": range(1000), # 1000 разных seed'ов + "control_size": [0.3, 0.5, 0.7], # 3 варианта размера + "sample_size": [0.8, 1.0] # С сэмплированием и без + } + }, + reporter=AADictReporter() +) +# Итого: 1000 * 3 * 2 = 6000 комбинаций + +# Grid search с ранней остановкой +optimized_search = IfParamsExperiment( + executors=[...], + params={ + Splitter: {"random_state": range(10000)}, + Filter: {"threshold": np.linspace(0.01, 0.1, 10)} + }, + stopping_criterion=IfAAExecutor( + # Останавливаемся, когда нашли хорошее разбиение + condition=lambda d: d.aa_score > 0.95 + ), + reporter=OptimalParamsReporter() +) + +# Вложенный grid search для hyperparameter tuning +hyperparameter_tuning = ParamsExperiment( + executors=[ + # Внешний уровень: параметры предобработки + ParamsExperiment( + executors=[ + OutliersFilter(), + NaFiller() + ], + params={ + OutliersFilter: { + "lower_percentile": [0.01, 0.05], + "upper_percentile": [0.95, 0.99] + }, + NaFiller: { + "method": ["ffill", "bfill", "mean"] + } + }, + reporter=PreprocessingReporter() + ), + + # Внутренний уровень: параметры модели + ParamsExperiment( + executors=[ + FaissNearestNeighbors() + ], + params={ + FaissNearestNeighbors: { + "n_neighbors": [1, 3, 5, 7], + "faiss_mode": ["base", "fast"] + } + }, + reporter=ModelReporter() + ) + ], + params={}, # Внешний уровень без дополнительных параметров + reporter=HyperparameterReporter() +) +``` + +**Преимущества:** +- Систематический поиск оптимума +- Возможность ранней остановки +- Вложенная оптимизация для сложных pipeline'ов + +#### 5. Hierarchical паттерн — иерархическая обработка + +Многоуровневая обработка с агрегацией на разных уровнях. + +```python +# Иерархический анализ: пользователь -> сегмент -> общий +hierarchical_analysis = Experiment([ + # Уровень 1: Анализ на уровне пользователей + OnRoleExperiment( + executors=[ + UserLevelMetrics(), + UserLevelTests() + ], + role=UserRole() + ), + + # Уровень 2: Агрегация по сегментам + GroupExperiment( + executors=[ + SegmentAggregator(), + SegmentLevelTests() + ], + searching_role=SegmentRole(), + reporter=SegmentReporter() + ), + + # Уровень 3: Общая агрегация + Experiment([ + GlobalAggregator(), + GlobalSignificanceTest(), + MultipleTestingCorrection() + ]) +]) + +# Каскадный анализ с фильтрацией на каждом уровне +cascade_filtering = Experiment([ + # Этап 1: Грубая фильтрация + ConstFilter(threshold=0.99), + + # Этап 2: Фильтрация по корреляции (только для оставшихся) + CorrFilter(threshold=0.9), + + # Этап 3: Тонкая фильтрация по CV (только для некоррелированных) + CVFilter(lower_bound=0.01, upper_bound=10), + + # Этап 4: Финальная фильтрация outliers + OutliersFilter(lower_percentile=0.01, upper_percentile=0.99) +]) +``` + +**Преимущества:** +- Эффективная обработка больших данных +- Иерархическая агрегация результатов +- Последовательное уточнение анализа + +#### 6. Retry паттерн — повторные попытки с разными стратегиями + +Попытки выполнить анализ разными способами при неудаче. + +```python +class RetryExecutor(Executor): + def __init__(self, strategies: list[Executor], max_retries: int = 3): + self.strategies = strategies + self.max_retries = max_retries + + def execute(self, data: ExperimentData) -> ExperimentData: + for i, strategy in enumerate(self.strategies[:self.max_retries]): + try: + result = strategy.execute(data) + if self._is_valid_result(result): + return result + except Exception as e: + if i == len(self.strategies) - 1: + raise e + continue + return data + +# Использование retry паттерна +robust_matching = Experiment([ + RetryExecutor( + strategies=[ + # Стратегия 1: Точный matching + Experiment([ + MahalanobisDistance(), + FaissNearestNeighbors(n_neighbors=1, faiss_mode="base") + ]), + + # Стратегия 2: Приближенный matching + Experiment([ + FaissNearestNeighbors(n_neighbors=5, faiss_mode="fast") + ]), + + # Стратегия 3: Fallback на случайное сопоставление + RandomMatching() + ] + ), + MatchingQualityAssessment() +]) +``` + +**Преимущества:** +- Устойчивость к ошибкам +- Graceful degradation +- Адаптивность к качеству данных + +#### 7. Template Method паттерн — шаблонные pipeline'ы + +Создание переиспользуемых шаблонов экспериментов с точками расширения. + +```python +class StandardABTestTemplate(Experiment): + """Шаблон стандартного A/B теста""" + + def __init__(self, + custom_preprocessing: list[Executor] = None, + custom_tests: list[Executor] = None, + multitest_method: str = "bonferroni"): + + # Базовая предобработка + base_preprocessing = [ + NaFiller(method="ffill"), + OutliersFilter() + ] + + # Добавляем кастомную предобработку + preprocessing = base_preprocessing + (custom_preprocessing or []) + + # Базовые тесты + base_tests = [ + GroupSizes(grouping_role=TreatmentRole()), + GroupDifference(grouping_role=TreatmentRole()), + TTest(grouping_role=TreatmentRole()) + ] + + # Добавляем кастомные тесты + tests = base_tests + (custom_tests or []) + + # Собираем полный pipeline + executors = preprocessing + tests + [ + ABAnalyzer(multitest_method=multitest_method) + ] + + super().__init__(executors) + +# Использование шаблона +custom_ab_test = StandardABTestTemplate( + custom_preprocessing=[ + CategoryAggregator(threshold=10), + DummyEncoder() + ], + custom_tests=[ + KSTest(grouping_role=TreatmentRole()), + PSI(grouping_role=TreatmentRole()) + ], + multitest_method="fdr_bh" +) +``` + +**Преимущества:** +- Стандартизация процессов +- Легкая кастомизация +- Соблюдение best practices + +Эти паттерны можно комбинировать для создания сложных аналитических pipeline'ов, сохраняя при этом читаемость и поддерживаемость кода. + +### Принципы проектирования Experiments + +1. **Композируемость** — Experiments можно вкладывать друг в друга +2. **Переиспользуемость** — Общие pipeline'ы можно выделить в отдельные Experiments +3. **Конфигурируемость** — Параметры можно менять без изменения структуры +4. **Прозрачность** — Каждый шаг сохраняет свои результаты в ExperimentData +5. **Расширяемость** — Легко создавать новые типы Experiments через наследование + +Experiment Framework обеспечивает мощную и гибкую систему для построения сложных аналитических pipeline'ов, сохраняя при этом простоту использования и понимания. \ No newline at end of file From 79ded14bfdce2fc8b45c0146d54dcc5648f95a06 Mon Sep 17 00:00:00 2001 From: dmbulychev Date: Tue, 16 Sep 2025 18:30:35 +0300 Subject: [PATCH 12/17] Add Reporters --- schemes/anatomy.md | 1048 +++++++++++++++++++++++++++----------------- 1 file changed, 638 insertions(+), 410 deletions(-) diff --git a/schemes/anatomy.md b/schemes/anatomy.md index 74485b14..d3de1b8a 100644 --- a/schemes/anatomy.md +++ b/schemes/anatomy.md @@ -628,76 +628,43 @@ comparator = GroupDifference( ```python # Данные: -# | user_id | group | before_treatment | after_treatment | -# |---------|---------|------------------|-----------------| -# | 1 | control | 100 | 105 | -# | 2 | test | 100 | 150 | -# | 3 | control | 80 | 82 | -# | 4 | test | 80 | 120 | +# | user_id | group | pre_revenue | post_revenue | +# |---------|---------|-------------|--------------| +# | 1 | control | 100 | 110 | +# | 2 | test | 100 | 150 | -comparator = TTest( +comparator = GroupDifference( compare_by="columns_in_groups", - grouping_role=TreatmentRole(), # группировка по "group" - baseline_role=PreTargetRole(), # baseline: "before_treatment" - target_roles=TargetRole() # сравнить с: "after_treatment" + grouping_role=TreatmentRole(), # группировка по "group" + baseline_role=PreTargetRole(), # baseline: "pre_revenue" + target_roles=TargetRole() # сравнить с: "post_revenue" ) # Результат: -# Для группы control: сравнение before_treatment vs after_treatment -# Для группы test: сравнение before_treatment vs after_treatment -# Позволяет оценить эффект внутри каждой группы отдельно +# control: сравнение pre_revenue vs post_revenue +# test: сравнение pre_revenue vs post_revenue ``` #### Режим "cross" — перекрестное сравнение -Самый сложный режим. Берет baseline из одной группы (обычно control) и сравнивает с метриками из других групп. Используется для difference-in-differences анализа. +Самый сложный режим. Сравнивает изменения между колонками в разных группах. Используется для difference-in-differences анализа. ```python -# Данные: -# | user_id | group | metric_A | metric_B | -# |---------|---------|----------|----------| -# | 1 | control | 100 | 50 | -# | 2 | test1 | 120 | 60 | -# | 3 | control | 90 | 45 | -# | 4 | test2 | 130 | 70 | - -comparator = TTest( +comparator = GroupDifference( compare_by="cross", - grouping_role=TreatmentRole(), # группировка по "group" - baseline_role=PreTargetRole(), # baseline из control: "metric_A" - target_roles=TargetRole() # сравнить с метриками из test групп + grouping_role=TreatmentRole(), + baseline_role=PreTargetRole(), + target_roles=TargetRole() ) -# Результат: -# control.metric_A vs test1.metric_A -# control.metric_A vs test1.metric_B -# control.metric_A vs test2.metric_A -# control.metric_A vs test2.metric_B - -# Это позволяет оценить, насколько изменения в test группах -# отличаются от baseline в control группе +# Сравнивает: +# (test_post - test_pre) vs (control_post - control_pre) ``` -**Практическое применение режима "cross":** - -Режим "cross" особенно полезен для: -1. **Difference-in-Differences (DiD)** — оценка причинно-следственной связи -2. **Synthetic control** — когда control группа служит базой для сравнения -3. **Multiple treatment arms** — когда есть несколько вариантов воздействия - -```python -# Пример DiD анализа -# До внедрения фичи: -# control: revenue = 100, retention = 0.5 -# test: revenue = 100, retention = 0.5 - -# После внедрения фичи (только в test): -# control: revenue = 110, retention = 0.52 (естественный рост) -# test: revenue = 140, retention = 0.65 (рост + эффект фичи) - -# С режимом "cross" можно оценить: -# (test_after - test_before) - (control_after - control_before) -# = (140 - 100) - (110 - 100) = 30 - истинный эффект фичи -``` +**Применение режимов:** +- **"groups"** — классические A/B тесты, сравнение метрик между группами +- **"columns"** — анализ изменений во времени, pre/post анализ +- **"columns_in_groups"** — гетерогенные эффекты, анализ по сегментам +- **"cross"** — каузальная инференция, DiD анализ #### Базовые метрики (GroupDifference, GroupSizes) @@ -821,69 +788,64 @@ class ConstFilter(Transformer): class CorrFilter(Transformer): def _inner_function(data: Dataset, threshold: float = 0.8) -> Dataset: corr_matrix = data.corr() + # Находим пары с высокой корреляцией for col1, col2 in high_corr_pairs: - # Удаляем признак с меньшей вариативностью - if data[col1].cv() < data[col2].cv(): - data.roles[col1] = InfoRole() + if abs(corr_matrix[col1][col2]) > threshold: + data.roles[col2] = InfoRole() # Понижаем роль второго return data ``` -**OutliersFilter** — фильтрует выбросы по процентилям: +**OutliersFilter** — удаляет выбросы: ```python class OutliersFilter(Transformer): - def __init__(self, lower_percentile: float = 0.05, upper_percentile: float = 0.95): + def __init__(self, + lower_percentile: float = 0.05, + upper_percentile: float = 0.95): self.lower_percentile = lower_percentile self.upper_percentile = upper_percentile def _inner_function(data: Dataset, target_cols, lower, upper) -> Dataset: - mask = (data[target_cols] < data[target_cols].quantile(lower)) | \ - (data[target_cols] > data[target_cols].quantile(upper)) - return data.drop(data[mask].index) + for col in target_cols: + q_low = data[col].quantile(lower) + q_high = data[col].quantile(upper) + data = data[(data[col] >= q_low) & (data[col] <= q_high)] + return data ``` -#### Обработка данных +#### Обработка пропусков и категорий **NaFiller** — заполнение пропущенных значений: ```python class NaFiller(Transformer): - def __init__(self, method: Literal["ffill", "bfill"] = None, values=None): - self.method = method - self.values = values + def __init__(self, method: str = "ffill"): + self.method = method # 'ffill', 'bfill', 'mean', 'median', etc. - def _inner_function(data: Dataset, target_cols, method, values) -> Dataset: - for column in target_cols: - data[column] = data[column].fillna(values=values, method=method) + def _inner_function(data: Dataset, target_cols, method) -> Dataset: + if method in ['ffill', 'bfill']: + return data.fillna(method=method) + elif method == 'mean': + for col in target_cols: + data[col].fillna(data[col].mean(), inplace=True) return data ``` -**CategoryAggregator** — объединение редких категорий: +**CategoryAggregator** — агрегация редких категорий: ```python class CategoryAggregator(Transformer): - def __init__(self, threshold: int = 15, new_group_name: str = "Other"): - self.threshold = threshold - self.new_group_name = new_group_name + def __init__(self, min_frequency: float = 0.01): + self.min_frequency = min_frequency - def _inner_function(data: Dataset, target_cols, threshold, new_name) -> Dataset: - for column in target_cols: - value_counts = data[column].value_counts() - rare_values = value_counts[value_counts < threshold].index - data[column] = data[column].replace(rare_values, new_name) + def _inner_function(data: Dataset, target_cols, min_freq) -> Dataset: + for col in target_cols: + value_counts = data[col].value_counts(normalize=True) + rare_categories = value_counts[value_counts < min_freq].index + data[col] = data[col].replace(rare_categories, 'Other') return data ``` -**Shuffle** — перемешивание данных: -```python -class Shuffle(Transformer): - def __init__(self, random_state: int = None): - self.random_state = random_state - - def _inner_function(data: Dataset, random_state) -> Dataset: - return data.sample(frac=1, random_state=random_state) -``` - -### Encoders: Кодирование категориальных переменных +### Encoders: Кодирование признаков -Encoders преобразуют категориальные переменные в числовые. Результат сохраняется в additional_fields. +Encoders преобразуют категориальные признаки в числовые. Результат сохраняется в additional_fields. ```python class Encoder(Calculator): @@ -914,117 +876,45 @@ class DummyEncoder(Encoder): GroupOperators выполняют специализированные операции над группами данных, часто используемые в matching и causal inference. -#### SMD (Standardized Mean Difference) - -Стандартизированная разница средних — метрика баланса ковариат: +**SMD (Standardized Mean Difference)** — стандартизированная разница средних: ```python class SMD(GroupOperator): - def _inner_function(cls, data: Dataset, test_data: Dataset) -> float: - return (data.mean() - test_data.mean()) / data.std() + def _inner_function(data: Dataset, test_data: Dataset) -> float: + mean_diff = data.mean() - test_data.mean() + pooled_std = np.sqrt((data.var() + test_data.var()) / 2) + return mean_diff / pooled_std ``` -#### Bias — оценка смещения - -Оценивает смещение при matching: +**Bias** — оценка смещения для matching: ```python class Bias(GroupOperator): - @staticmethod - def calc_coefficients(X: Dataset, Y: Dataset) -> list[float]: - # Линейная регрессия для оценки коэффициентов - return np.linalg.lstsq(X.values, Y.values, rcond=-1)[0] - - @staticmethod - def calc_bias(X: Dataset, X_matched: Dataset, coefficients: list[float]) -> list[float]: - # Вычисление смещения на основе разницы признаков - return [(j - i).dot(coefficients) for i, j in zip(X.values, X_matched.values)] -``` - -#### MatchingMetrics — метрики для matching - -Вычисляет различные метрики treatment effect: -```python -class MatchingMetrics(GroupOperator): - def __init__(self, metric: Literal["atc", "att", "ate"] = "ate"): - self.metric = metric # Average Treatment on Controls/Treated/Everyone - - def _inner_function(cls, data, test_data, target_fields, metric, bias) -> dict: - # Вычисление Individual Treatment Effect - itt = test_data[target_fields[0]] - test_data[target_fields[1]] - itc = data[target_fields[1]] - data[target_fields[0]] - - # Коррекция на смещение - if bias: - itt -= bias["test"] - itc -= bias["control"] - - # Вычисление метрик с учетом весов - att = itt.mean() - atc = itc.mean() - ate = (att * len(test_data) + atc * len(data)) / (len(test_data) + len(data)) - - # Вычисление стандартных ошибок и p-values + def _inner_function(data: Dataset, matched_data: Dataset) -> dict: + # Оценка качества matching через смещение + bias = (matched_data.mean() - data.mean()) / data.std() return { - "ATT": [att, se_att, p_val_att, ci_lower_att, ci_upper_att], - "ATC": [atc, se_atc, p_val_atc, ci_lower_atc, ci_upper_atc], - "ATE": [ate, se_ate, p_val_ate, ci_lower_ate, ci_upper_ate] + "bias": bias, + "bias_reduced": abs(bias) < 0.1 # Порог 10% } ``` -### MLExecutors: Машинное обучение - -MLExecutors интегрируют алгоритмы машинного обучения в pipeline экспериментов. - -#### Базовый класс MLExecutor - +**MatchingMetrics** — метрики качества matching: ```python -class MLExecutor(Calculator, ABC): - @abstractmethod - def fit(self, X: Dataset, Y: Dataset = None) -> MLExecutor: - """Обучение модели""" - pass - - @abstractmethod - def predict(self, X: Dataset) -> Dataset: - """Предсказание""" - pass - - def score(self, X: Dataset, Y: Dataset) -> float: - """Оценка качества""" - pass -``` - -#### FaissNearestNeighbors — поиск ближайших соседей - -Использует библиотеку FAISS для эффективного поиска ближайших соседей: -```python -class FaissNearestNeighbors(MLExecutor): - def __init__(self, - n_neighbors: int = 1, - two_sides: bool = False, # Искать пары в обе стороны - test_pairs: bool = False, # Пары для test группы - faiss_mode: Literal["base", "fast", "auto"] = "auto"): - self.n_neighbors = n_neighbors - self.faiss_mode = faiss_mode - - def fit(self, X: Dataset) -> MLExecutor: - # Создание индекса FAISS - self.index = faiss.IndexFlatL2(X.shape[1]) - if len(X) > 1_000_000 and self.faiss_mode in ["auto", "fast"]: - # Используем приближенный поиск для больших данных - self.index = faiss.IndexIVFFlat(self.index, 1, 1000) - self.index.train(X.values) - self.index.add(X.values) - return self +class MatchingMetrics(GroupOperator): + def __init__(self, metric: str = "ate"): + self.metric = metric # 'ate', 'att', 'atc' - def predict(self, X: Dataset) -> Dataset: - # Поиск ближайших соседей - distances, indices = self.index.search(X.values, self.n_neighbors) - return Dataset.from_dict({"indices": indices}) + def _inner_function(data: Dataset, matched_indices: Dataset) -> dict: + if self.metric == "ate": # Average Treatment Effect + effect = matched_treatment.mean() - matched_control.mean() + elif self.metric == "att": # Average Treatment on Treated + effect = treated.mean() - matched_control_for_treated.mean() + # ... другие метрики + return {"effect": effect, "se": standard_error} ``` -### Splitters: Разделение на группы +### Splitters: Разделение данных -Splitters отвечают за разделение данных на группы для A/A и A/B тестов. +Splitters разделяют данные на группы для экспериментов. #### AASplitter — базовое разделение @@ -1125,7 +1015,175 @@ pipeline = [ Это обеспечивает максимальную гибкость при построении экспериментов. -## 6. Слой экспериментов: Experiment Framework +## 6. Analyzer'ы — комплексный анализ результатов + +Analyzer'ы представляют собой специальный класс Executor'ов, которые выполняют высокоуровневый анализ результатов экспериментов. В отличие от простых вычислительных блоков (Calculator'ов), Analyzer'ы работают с результатами множества других Executor'ов, агрегируют их и принимают комплексные решения. + +### Архитектура Analyzer'ов + +Analyzer'ы наследуются напрямую от Executor, минуя Calculator, так как их задача — не вычисления над сырыми данными, а анализ уже полученных результатов. Типичный Analyzer: + +1. **Извлекает результаты** предыдущих Executor'ов из ExperimentData +2. **Анализирует и агрегирует** эти результаты согласно своей логике +3. **Принимает решения** на основе комплексных критериев +4. **Сохраняет итоговый анализ** в analysis_tables + +### OneAAStatAnalyzer — анализ одного A/A теста + +**Назначение:** Анализирует результаты статистических тестов для одного разбиения A/A теста. + +**Функциональность:** +- Извлекает результаты всех примененных тестов (TTest, KSTest, Chi2Test) +- Для каждой метрики подсчитывает количество прошедших тестов +- Оценивает общее качество разбиения по pass rate +- Классифицирует качество: excellent (>95%), good (>80%), acceptable (>60%), poor (<60%) + +**Входные данные:** Результаты статистических тестов в analysis_tables + +**Выходные данные:** +- Статистика по каждой метрике (passed/total tests, pass_rate, status) +- Общая оценка качества разбиения (overall pass_rate, quality level) + +### AAScoreAnalyzer — выбор лучшего разбиения + +**Назначение:** Анализирует результаты множественных A/A тестов и выбирает оптимальное разбиение. + +**Функциональность:** +- Извлекает результаты всех итераций OneAAStatAnalyzer +- Вычисляет score для каждого разбиения согласно критерию: + - `max_pass_rate` — максимизация доли прошедших тестов + - `min_bias` — минимизация смещения между группами + - `balanced` — комбинация pass_rate и баланса групп +- Ранжирует разбиения по качеству +- Восстанавливает параметры лучшего Splitter'а +- Применяет лучшее разбиение к данным + +**Входные данные:** Результаты множественных OneAAStatAnalyzer + +**Выходные данные:** +- ID и параметры лучшего разбиения +- Статистика по всем разбиениям (scores, mean, std) +- Колонка с лучшим разбиением в additional_fields + +### ABAnalyzer — анализ A/B теста + +**Назначение:** Выполняет финальный анализ A/B теста с учетом множественного тестирования. + +**Функциональность:** +- Проверяет достаточность размера выборки +- Извлекает все p-values из проведенных тестов +- Применяет коррекцию множественного тестирования: + - Bonferroni — консервативная коррекция + - Holm — последовательная коррекция + - FDR — контроль False Discovery Rate +- Оценивает размер эффекта и его confidence interval +- Разделяет статистическую и практическую значимость +- Принимает решение по каждой метрике: + - `ship` — значимый и практичный эффект + - `monitor` — значимый, но малый эффект + - `investigate` — большой, но незначимый эффект + - `no_effect` — нет эффекта +- Формирует общие рекомендации по эксперименту + +**Входные данные:** +- Результаты статистических тестов +- Размеры групп из GroupSizes +- Эффекты из GroupDifference + +**Выходные данные:** +- Детальный анализ по каждой метрике +- Скорректированные p-values +- Общее решение и рекомендации +- Оценка рисков и качества выборки + +### MatchingAnalyzer — оценка качества matching + +**Назначение:** Комплексная оценка качества matching'а между группами. + +**Функциональность:** +- Извлекает matched индексы из FaissNearestNeighbors +- Создает matched датасеты для control и treatment +- Оценивает баланс ковариат через SMD (Standardized Mean Difference) +- Вычисляет метрики качества: + - SMD — стандартизированная разница средних + - KS статистика — различие распределений + - Variance ratio — соотношение дисперсий +- Оценивает treatment effect после matching +- Выполняет диагностику проблем: + - Проверка размера matched выборки + - Идентификация несбалансированных ковариат + - Оценка common support region +- Генерирует рекомендации по улучшению + +**Входные данные:** +- Matched индексы из additional_fields +- Исходные данные control и treatment групп + +**Выходные данные:** +- Количество и доля matched пар +- Баланс по каждой ковариате +- Общие метрики качества matching +- Treatment effect с доверительными интервалами +- Диагностика и рекомендации + +### Паттерны использования Analyzer'ов + +#### 1. Последовательный анализ +Analyzer'ы размещаются в конце pipeline'а для анализа накопленных результатов: +```python +experiment = Experiment([ + DataPreparation(), + StatisticalTests(), + ABAnalyzer(multitest_method="fdr") +]) +``` + +#### 2. Иерархический анализ +Один Analyzer использует результаты другого: +```python +experiment = Experiment([ + ParamsExperiment(...), + OneAAStatAnalyzer(), # Анализ каждого теста + AAScoreAnalyzer() # Выбор лучшего +]) +``` + +#### 3. Условный анализ +Выбор Analyzer'а на основе характеристик данных: +```python +IfExecutor( + condition=lambda d: d.n_metrics > 10, + if_executor=ABAnalyzer(multitest_method="fdr"), + else_executor=ABAnalyzer(multitest_method=None) +) +``` + +### Создание кастомных Analyzer'ов + +Для создания своего Analyzer'а необходимо: + +1. **Наследоваться от Executor** (не от Calculator) +2. **Реализовать метод execute** с логикой: + - Извлечение нужных результатов через `data.get_ids()` + - Анализ и агрегация результатов + - Сохранение через `data.set_value()` +3. **Следовать конвенциям:** + - Результаты сохранять в analysis_tables + - Использовать Dataset для структурированных результатов + - Предоставлять summary и recommendations + +### Преимущества Analyzer'ов + +1. **Высокоуровневая абстракция** — скрывают сложность анализа за простым интерфейсом +2. **Переиспользуемость** — стандартные паттерны анализа для типовых задач +3. **Композируемость** — можно комбинировать разные виды анализа +4. **Расширяемость** — легко добавлять новые типы анализа +5. **Воспроизводимость** — стандартизированные методы обеспечивают консистентность +6. **Decision-ready** — превращают сырые результаты в actionable insights + +Analyzer'ы являются ключевым компонентом для превращения множества технических результатов вычислений в понятные бизнес-решения и рекомендации. + +## 7. Слой экспериментов: Experiment Framework Слой экспериментов управляет композицией и оркестрацией Executor'ов. Он определяет, КАК и В КАКОМ ПОРЯДКЕ выполняются операции, предоставляя различные стратегии выполнения. @@ -1228,52 +1286,57 @@ class ExperimentWithReporter(Experiment): ### CycledExperiment — многократное выполнение -Выполняет эксперимент заданное количество раз (например, для оценки стабильности): +Выполняет эксперимент заданное количество раз: ```python class CycledExperiment(ExperimentWithReporter): def __init__(self, - executors: list[Executor], + executors: Sequence[Executor], reporter: DatasetReporter, - n_iterations: int): + n_iterations: int = 10): super().__init__(executors, reporter) self.n_iterations = n_iterations def execute(self, data: ExperimentData) -> ExperimentData: results = [] - for i in tqdm(range(self.n_iterations)): + for i in range(self.n_iterations): # Каждая итерация начинается с чистых данных - result = self.one_iteration(data, str(i)) - results.append(result) + iteration_result = self.one_iteration(data, key=str(i)) + results.append(iteration_result) # Объединяем результаты всех итераций - final_result = results[0].append(results[1:]) - return self._set_value(data, final_result) + combined_results = Dataset.concat(results) + + return data.set_value( + space=ExperimentDataEnum.analysis_tables, + executor_id=self.id, + value=combined_results + ) ``` **Применение:** -- Оценка вариативности метрик - Bootstrap анализ -- Проверка устойчивости результатов +- Оценка стабильности результатов +- Monte Carlo симуляции ### GroupExperiment — выполнение по группам -Применяет эксперимент отдельно к каждой группе данных: +Применяет эксперимент к каждой группе данных отдельно: ```python class GroupExperiment(ExperimentWithReporter): def __init__(self, executors: Sequence[Executor], - reporter: Reporter, + reporter: DatasetReporter, searching_role: ABCRole = GroupingRole()): - self.searching_role = searching_role super().__init__(executors, reporter) + self.searching_role = searching_role def execute(self, data: ExperimentData) -> ExperimentData: - group_field = data.ds.search_columns(self.searching_role) - results = [] + # Находим поле для группировки + group_field = data.ds.search_columns([self.searching_role])[0] - # Применяем эксперимент к каждой группе отдельно + results = [] for group, group_data in data.ds.groupby(group_field): result = self.one_iteration( ExperimentData(group_data), @@ -1326,57 +1389,36 @@ class ParamsExperiment(ExperimentWithReporter): for class_params in self._params.values() ]) - self._flat_params = [ - {class_: dict(params) for class_, params in combination} - for combination in param_combinations - ] + self._flat_params = list(param_combinations) def execute(self, data: ExperimentData) -> ExperimentData: self._update_flat_params() - results = [] - for flat_param in tqdm(self._flat_params): + results = [] + for i, flat_param in enumerate(tqdm(self._flat_params)): t_data = ExperimentData(data.ds) - # Устанавливаем параметры для каждого Executor + # Применяем параметры к соответствующим Executor'ам for executor in self.executors: - executor.set_params(flat_param) + params_for_executor = self._extract_params_for_executor( + executor, flat_param + ) + if params_for_executor: + executor.set_params(params_for_executor) + t_data = executor.execute(t_data) - # Собираем отчет для этой комбинации - report = self.reporter.report(t_data) - results.append(report) + # Сохраняем результат итерации + iteration_result = self.reporter.report(t_data) + iteration_result["params"] = flat_param + results.append(iteration_result) return self._set_result(data, results) ``` -**Применение:** -- Grid search для оптимальных параметров -- Sensitivity analysis -- A/A тестирование с разными random_state - -**Пример использования:** -```python -# A/A тест с 2000 различными разбиениями -params_exp = ParamsExperiment( - executors=[ - AASplitter(), # Будет параметризован - GroupSizes(grouping_role=AdditionalTreatmentRole()), - TTest(grouping_role=AdditionalTreatmentRole()) - ], - params={ - AASplitter: { - "random_state": range(2000), # 2000 разных разбиений - "control_size": [0.5] - } - }, - reporter=AADictReporter() -) -``` - -### IfParamsExperiment — параметрический поиск с ранней остановкой +### IfParamsExperiment — параметрический поиск с условием остановки -Расширение ParamsExperiment с возможностью остановки при выполнении условия: +Добавляет возможность ранней остановки при достижении условия: ```python class IfParamsExperiment(ParamsExperiment): @@ -1547,142 +1589,78 @@ full_pipeline = Experiment([ Выбор пути выполнения на основе характеристик данных или промежуточных результатов. ```python -# Адаптивный выбор теста на основе характеристик данных -class DataCharacterizer(Executor): - def execute(self, data: ExperimentData) -> ExperimentData: - # Анализируем характеристики данных - is_normal = self._check_normality(data.ds) - sample_size = len(data.ds) - has_categories = self._has_categorical(data.ds) - - # Сохраняем характеристики - return data.set_value( - ExperimentDataEnum.variables, - self.id, - { - "is_normal": is_normal, - "sample_size": sample_size, - "has_categories": has_categories - } - ) - -class AdaptiveTestSelector(IfExecutor): - def check_rule(self, data: ExperimentData) -> bool: - chars = data.variables[data.get_one_id(DataCharacterizer)] - # Выбираем подходящий тест - return chars["is_normal"] and chars["sample_size"] > 30 - +# Адаптивный выбор теста adaptive_testing = Experiment([ - DataCharacterizer(), - AdaptiveTestSelector( - # Для нормальных данных с большой выборкой - if_executor=Experiment([ - TTest(grouping_role=TreatmentRole()), - PowerTesting(significance=0.95) - ]), - # Для ненормальных или малых выборок - else_executor=Experiment([ - UTest(grouping_role=TreatmentRole()), - KSTest(grouping_role=TreatmentRole()) - ]) - ) + # Подготовка + DataPreparation(), + + # Проверка нормальности + NormalityTest(), + + # Выбор теста на основе результата + IfExecutor( + condition=lambda d: d.normality_test_passed, + if_executor=TTest(), # Параметрический тест + else_executor=UTest() # Непараметрический тест + ), + + # Дальнейший анализ + ResultAnalyzer() ]) -# Многоуровневое ветвление для matching -matching_pipeline = Experiment([ - # Проверка качества данных - DataQualityChecker(), +# Каскадное ветвление +cascading_analysis = Experiment([ + InitialTest(), IfExecutor( - condition=lambda d: d.quality_score > 0.9, + condition=lambda d: d.p_value < 0.05, if_executor=Experiment([ - # Высокое качество - используем точный matching - MahalanobisDistance(), - FaissNearestNeighbors(n_neighbors=1, faiss_mode="base") + # Значимый эффект - глубокий анализ + EffectSizeCalculator(), + SegmentAnalysis(), + HeterogeneityTest() ]), - else_executor=IfExecutor( - condition=lambda d: d.quality_score > 0.5, - if_executor=Experiment([ - # Среднее качество - используем приближенный matching - FaissNearestNeighbors(n_neighbors=3, faiss_mode="fast") - ]), - else_executor=Experiment([ - # Низкое качество - используем propensity score - PropensityScoreMatching() - ]) - ) + else_executor=Experiment([ + # Незначимый эффект - проверка power + PowerAnalysis(), + SampleSizeRecommendation() + ]) ) ]) ``` **Преимущества:** -- Адаптация к характеристикам данных -- Оптимизация производительности -- Обработка edge cases +- Адаптивность к данным +- Оптимизация вычислений +- Автоматический выбор методов -#### 3. Fan-out/Fan-in паттерн — параллельное применение +#### 3. Fan-out/Fan-in паттерн — параллельные анализы -Применение разных анализов к одним данным с последующей агрегацией результатов. +Применение разных анализов к одним данным с последующей агрегацией. ```python -# Fan-out: применяем разные тесты ко всем метрикам -fanout_tests = OnRoleExperiment( - executors=[ - # Ветка 1: Параметрические тесты - Experiment([ - TTest(grouping_role=TreatmentRole()), - GroupDifference(grouping_role=TreatmentRole()) - ]), - - # Ветка 2: Непараметрические тесты - Experiment([ - KSTest(grouping_role=TreatmentRole()), - UTest(grouping_role=TreatmentRole()) - ]), - - # Ветка 3: Анализ мощности - Experiment([ - PowerTesting(significance=0.95, power=0.8), - MDEBySize() - ]) - ], - role=TargetRole() # Применить ко всем target метрикам -) - -# Fan-in: агрегация результатов разных тестов -class TestAggregator(Executor): - def execute(self, data: ExperimentData) -> ExperimentData: - # Собираем результаты всех тестов - ttest_results = data.analysis_tables[data.get_one_id(TTest)] - kstest_results = data.analysis_tables[data.get_one_id(KSTest)] - utest_results = data.analysis_tables[data.get_one_id(UTest)] - - # Агрегируем (например, голосованием) - aggregated = self._aggregate_by_voting([ - ttest_results, kstest_results, utest_results - ]) - - return data.set_value( - ExperimentDataEnum.analysis_tables, - self.id, - aggregated - ) - -# Полный fan-out/fan-in pipeline -ensemble_testing = Experiment([ - fanout_tests, # Fan-out - TestAggregator() # Fan-in -]) - -# Параллельный анализ по сегментам -segment_analysis = Experiment([ - # Fan-out по разным сегментам +# Fan-out: множественные анализы +multi_analysis = Experiment([ + # Подготовка + DataPreparation(), + + # Fan-out: разные виды анализа + OnRoleExperiment( + executors=[ + TTest(), + KSTest(), + Chi2Test() + ], + role=TargetRole() + ), + + # Fan-out: анализ по сегментам GroupExperiment( executors=[ - GroupDifference(), - TTest() + SegmentStatistics(), + SegmentTests() ], - searching_role=SegmentRole(), # age_group, region, etc. + searching_role=SegmentRole(), reporter=SegmentReporter() ), @@ -1892,72 +1870,322 @@ robust_matching = Experiment([ - Graceful degradation - Адаптивность к качеству данных -#### 7. Template Method паттерн — шаблонные pipeline'ы +#### 7. Observer паттерн — мониторинг выполнения -Создание переиспользуемых шаблонов экспериментов с точками расширения. +Добавление логирования и мониторинга в процесс выполнения. ```python -class StandardABTestTemplate(Experiment): - """Шаблон стандартного A/B теста""" +class ObservableExperiment(Experiment): + def __init__(self, executors: list[Executor], observers: list[Observer] = None): + super().__init__(executors) + self.observers = observers or [] - def __init__(self, - custom_preprocessing: list[Executor] = None, - custom_tests: list[Executor] = None, - multitest_method: str = "bonferroni"): - - # Базовая предобработка - base_preprocessing = [ - NaFiller(method="ffill"), - OutliersFilter() - ] - - # Добавляем кастомную предобработку - preprocessing = base_preprocessing + (custom_preprocessing or []) + def execute(self, data: ExperimentData) -> ExperimentData: + for i, executor in enumerate(self.executors): + # Уведомляем о начале + for observer in self.observers: + observer.on_executor_start(executor, data) + + # Выполнение + start_time = time.time() + data = executor.execute(data) + execution_time = time.time() - start_time + + # Уведомляем о завершении + for observer in self.observers: + observer.on_executor_complete(executor, data, execution_time) - # Базовые тесты - base_tests = [ - GroupSizes(grouping_role=TreatmentRole()), - GroupDifference(grouping_role=TreatmentRole()), - TTest(grouping_role=TreatmentRole()) - ] + return data + +# Использование +experiment = ObservableExperiment( + executors=[...], + observers=[ + LoggingObserver(), + MetricsCollector(), + ProgressBar(), + AlertingObserver(threshold=60) # Алерт если executor > 60 сек + ] +) +``` + +**Преимущества:** +- Прозрачность выполнения +- Сбор метрик и диагностика +- Возможность прерывания при проблемах + +### Лучшие практики при проектировании экспериментов + +1. **Модульность** — разбивайте сложные эксперименты на логические блоки +2. **Переиспользование** — создавайте библиотеку стандартных подэкспериментов +3. **Валидация** — добавляйте проверки между этапами +4. **Документирование** — используйте говорящие имена и комментарии +5. **Тестирование** — тестируйте эксперименты на небольших данных +6. **Версионирование** — сохраняйте версии успешных экспериментов +7. **Мониторинг** — логируйте ключевые метрики выполнения + +Слой экспериментов предоставляет мощные абстракции для построения сложных аналитических pipeline'ов, сохраняя при этом простоту и читаемость кода. + +## 8. Система Reporter'ов + +Reporter'ы отвечают за извлечение, форматирование и представление результатов экспериментов. Они служат мостом между внутренним представлением данных в ExperimentData и форматом, удобным для пользователя или последующей обработки. + +### Архитектура Reporter'ов + +Reporter'ы работают по принципу "извлечь и отформатировать": + +1. **Извлекают результаты** из различных пространств имен ExperimentData +2. **Агрегируют и структурируют** информацию согласно своей логике +3. **Форматируют вывод** в нужном виде (dict, Dataset, HTML, PDF и т.д.) +4. **Сохраняют семантику** — каждый Reporter знает, какие результаты и как интерпретировать + +### Базовый класс Reporter + +**Reporter** — абстрактный базовый класс для всех репортеров: + +**Контракт:** +- Метод `report(data: ExperimentData)` — единая точка входа +- Возвращает Any — конкретный формат определяется наследником +- Не модифицирует ExperimentData — только читает данные +- Детерминированность — одинаковые данные дают одинаковый отчет + +### DictReporter — универсальная основа + +DictReporter является фундаментальным классом для большинства Reporter'ов в HypEx. Его философия — предоставить универсальный промежуточный формат (словарь), который легко преобразовать в любой другой. + +#### Концепция DictReporter + +**Основная идея:** Все результаты экспериментов можно представить как плоский словарь с уникальными ключами. + +**Формат ключей:** +```python +# Технический формат (front=False) +"TTest╤╤revenue" → {"p-value": 0.042, "statistic": 2.15} + +# User-friendly формат (front=True) +"revenue_ttest_pvalue" → 0.042 +"revenue_ttest_statistic" → 2.15 +``` + +**Преимущества плоской структуры:** +- Отсутствие вложенности упрощает обработку +- Уникальные ключи предотвращают конфликты +- Легко конвертировать в табличный формат +- Простая сериализация (JSON, pickle) + +#### Методы извлечения в DictReporter + +DictReporter предоставляет набор методов для извлечения разных типов результатов: + +- **`extract_from_one_row_dataset()`** — для скалярных результатов +- **`_extract_from_comparators()`** — для результатов сравнений +- **`_get_struct_dict()`** — создание структурированного словаря +- **`_convert_dataset_to_dict()`** — конвертация Dataset в dict + +Каждый наследник DictReporter переопределяет метод `report()`, комбинируя эти методы извлечения для создания нужного словаря. + +### OnDictReporter — универсальный форматтер + +OnDictReporter — это паттерн Decorator для DictReporter'ов. Он позволяет преобразовать базовый словарный формат в любой другой без изменения логики извлечения. + +#### Архитектура форматирования + +``` +ExperimentData → DictReporter → dict → OnDictReporter → Любой формат + ↑ ↓ + (извлечение) (форматирование) +``` + +**Ключевая идея:** Разделение извлечения данных и их представления. + +#### Стандартные форматтеры + +**DatasetReporter** — табличное представление: +```python +class DatasetReporter(OnDictReporter): + def report(self, data: ExperimentData): + # Получаем базовый dict + dict_report = self.dict_reporter.report(data) + # Преобразуем в структурированную таблицу + return self.convert_flat_dataset(dict_report) +``` + +**Потенциальные форматтеры (roadmap):** + +**HTMLReporter** — интерактивные HTML отчеты: +- Таблицы с сортировкой и фильтрацией +- Графики и визуализации +- Collapsible секции для детализации +- Экспорт в различные форматы + +**PDFReporter** — профессиональные PDF отчеты: +- Форматированные таблицы и графики +- Executive summary на первой странице +- Детальные приложения с методологией +- Брендирование и стилизация + +**MarkdownReporter** — отчеты для документации: +- Структурированный markdown +- Таблицы в GFM формате +- Встроенные графики как base64 +- Готов для вставки в wiki/confluence + +**JSONReporter** — машиночитаемый формат: +- Полная сериализация результатов +- Метаданные об эксперименте +- Версионирование схемы +- Поддержка streaming + +**ExcelReporter** — multi-sheet Excel файлы: +- Summary на первом листе +- Детальные результаты по листам +- Условное форматирование +- Встроенные формулы и графики + +#### Создание кастомного форматтера + +```python +class CustomFormatter(OnDictReporter): + def __init__(self, dict_reporter: DictReporter, format_options: dict): + super().__init__(dict_reporter) + self.format_options = format_options + + def report(self, data: ExperimentData): + # Получаем базовый словарь + base_dict = self.dict_reporter.report(data) - # Добавляем кастомные тесты - tests = base_tests + (custom_tests or []) + # Применяем кастомное форматирование + formatted = self.apply_formatting(base_dict) - # Собираем полный pipeline - executors = preprocessing + tests + [ - ABAnalyzer(multitest_method=multitest_method) - ] + # Добавляем метаданные + formatted['metadata'] = self.extract_metadata(data) + # Возвращаем в нужном формате + return self.render(formatted) +``` + +### Использование Reporter'ов в Experiment + +Reporter'ы интегрированы в систему экспериментов через класс ExperimentWithReporter. + +#### ExperimentWithReporter + +Этот класс добавляет автоматическую генерацию отчетов к экспериментам: + +```python +class ExperimentWithReporter(Experiment): + def __init__(self, executors: list, reporter: Reporter): super().__init__(executors) + self.reporter = reporter + + def one_iteration(self, data: ExperimentData, key: str = ""): + # Выполняем эксперимент + result_data = super().execute(data) + # Автоматически генерируем отчет + return self.reporter.report(result_data) +``` -# Использование шаблона -custom_ab_test = StandardABTestTemplate( - custom_preprocessing=[ - CategoryAggregator(threshold=10), - DummyEncoder() - ], - custom_tests=[ - KSTest(grouping_role=TreatmentRole()), - PSI(grouping_role=TreatmentRole()) - ], - multitest_method="fdr_bh" +#### Специализированные эксперименты с Reporter'ами + +**ParamsExperiment** — всегда требует Reporter: +- После каждой комбинации параметров генерирует отчет +- Агрегирует отчеты всех итераций +- Позволяет сравнить результаты разных параметров + +**GroupExperiment** — Reporter для каждой группы: +- Генерирует отчет для каждой группы отдельно +- Объединяет в общую таблицу с индексом по группам + +**CycledExperiment** — Reporter для каждого цикла: +- Отчет после каждой итерации +- Статистика по всем итерациям + +#### Паттерны использования + +**Inline reporter в эксперименте:** +```python +experiment = ParamsExperiment( + executors=[...], + params={...}, + reporter=DatasetReporter(ABDictReporter()) ) ``` -**Преимущества:** -- Стандартизация процессов -- Легкая кастомизация -- Соблюдение best practices +**Композиция репортеров:** +```python +base_reporter = TestDictReporter() +formatted_reporter = DatasetReporter(base_reporter) + +experiment = ExperimentWithReporter( + executors=[...], + reporter=formatted_reporter +) +``` + +**Множественные отчеты:** +```python +data = experiment.execute(initial_data) + +# Разные форматы из одних данных +summary = SummaryDictReporter().report(data) +detailed = DetailedDatasetReporter().report(data) +visual = VisualizationReporter().report(data) +``` + +### Иерархия конкретных Reporter'ов + +HypEx предоставляет набор готовых Reporter'ов для типовых задач: + +#### Для статистических тестов +- **TestDictReporter** — базовый класс для тестовых репортеров +- **OneAADictReporter** — отчет по одному A/A тесту +- **AADatasetReporter** — табличный отчет по A/A тестам +- **ABDictReporter** — отчет по A/B тесту +- **ABDatasetReporter** — табличный отчет по A/B тесту +- **HomoDictReporter** — отчет по тесту гомогенности +- **HomoDatasetReporter** — табличный отчет по гомогенности + +#### Для matching +- **MatchingDictReporter** — базовый отчет по matching +- **MatchingDatasetReporter** — табличный отчет по matching +- **MatchingQualityDictReporter** — отчет по качеству matching +- **MatchingQualityDatasetReporter** — табличный отчет по качеству + +#### Специальные репортеры +- **AAPassedReporter** — определение прошедших A/A тестов +- **AABestSplitReporter** — отчет о лучшем разбиении + +Каждый из этих Reporter'ов знает, какие именно результаты извлекать из ExperimentData и как их правильно интерпретировать. + +### Принципы проектирования Reporter'ов + +1. **Separation of Concerns:** + - Извлечение (что достать) отделено от форматирования (как показать) + - DictReporter отвечает за извлечение + - OnDictReporter отвечает за форматирование + +2. **Composability:** + - Reporter'ы можно комбинировать + - Один базовый reporter, множество форматов вывода + +3. **Reusability:** + - Один Reporter может использоваться в разных экспериментах + - Форматтеры переиспользуются для разных типов данных + +4. **Extensibility:** + - Легко добавить новый формат вывода + - Не нужно менять логику извлечения -Эти паттерны можно комбинировать для создания сложных аналитических pipeline'ов, сохраняя при этом читаемость и поддерживаемость кода. +5. **Testability:** + - Reporter'ы тестируются независимо от экспериментов + - Форматтеры тестируются отдельно от извлечения -### Принципы проектирования Experiments +### Преимущества архитектуры Reporter'ов -1. **Композируемость** — Experiments можно вкладывать друг в друга -2. **Переиспользуемость** — Общие pipeline'ы можно выделить в отдельные Experiments -3. **Конфигурируемость** — Параметры можно менять без изменения структуры -4. **Прозрачность** — Каждый шаг сохраняет свои результаты в ExperimentData -5. **Расширяемость** — Легко создавать новые типы Experiments через наследование +1. **Гибкость представления** — один эксперимент, множество форматов вывода +2. **Консистентность** — стандартизированные способы извлечения результатов +3. **Масштабируемость** — легко добавлять новые форматы без изменения core +4. **Maintainability** — изменения в форматировании не влияют на логику +5. **User Experience** — пользователь получает результаты в удобном виде -Experiment Framework обеспечивает мощную и гибкую систему для построения сложных аналитических pipeline'ов, сохраняя при этом простоту использования и понимания. \ No newline at end of file +Reporter'ы обеспечивают элегантное решение проблемы представления результатов, позволяя HypEx адаптироваться под различные use cases и требования к отчетности. \ No newline at end of file From 82cd9df6b26f22c0ee1b2062f24c4014f5eca2b8 Mon Sep 17 00:00:00 2001 From: dmbulychev Date: Sun, 21 Sep 2025 11:46:11 +0300 Subject: [PATCH 13/17] Add Extensions --- schemes/anatomy.md | 2298 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 1990 insertions(+), 308 deletions(-) diff --git a/schemes/anatomy.md b/schemes/anatomy.md index d3de1b8a..a1be5273 100644 --- a/schemes/anatomy.md +++ b/schemes/anatomy.md @@ -4,9 +4,13 @@ ### Общая концепция библиотеки -HypEx (Hypothesis Experiments) — это библиотека для проведения статистических экспериментов, построенная на принципах модульности, расширяемости и многоуровневой абстракции. Основная идея заключается в создании гибкой системы, которая позволяет как быстро запускать стандартные эксперименты (A/B тесты, A/A тесты, matching), так и конструировать сложные кастомные пайплайны обработки данных. +HypEx (Hypothesis Experiments) — это библиотека для проведения статистических экспериментов, построенная на принципах +модульности, расширяемости и многоуровневой абстракции. Основная идея заключается в создании гибкой системы, которая +позволяет как быстро запускать стандартные эксперименты (A/B тесты, A/A тесты, matching), так и конструировать сложные +кастомные пайплайны обработки данных. -Библиотека решает ключевую проблему: разрыв между потребностями бизнес-пользователей, которым нужны готовые решения, и потребностями исследователей данных, которым требуется гибкость и возможность кастомизации. +Библиотека решает ключевую проблему: разрыв между потребностями бизнес-пользователей, которым нужны готовые решения, и +потребностями исследователей данных, которым требуется гибкость и возможность кастомизации. ### Принцип многоуровневой абстракции @@ -21,32 +25,38 @@ HypEx реализует 8 уровней абстракции, от прост 7. **Уровень модификации** — глубокие доработки базовых механик 8. **Уровень ядра** — изменение фундаментального поведения -Эта философия пронизывает всю архитектуру: каждый слой системы предоставляет свой уровень абстракции, позволяя пользователям работать на комфортном для них уровне сложности. +Эта философия пронизывает всю архитектуру: каждый слой системы предоставляет свой уровень абстракции, позволяя +пользователям работать на комфортном для них уровне сложности. ### Основные архитектурные принципы **1. Композиция над наследованием** + - Эксперименты строятся путем композиции Executor'ов - Каждый Executor выполняет одну конкретную задачу - Сложное поведение достигается через комбинацию простых компонентов **2. Единый поток данных** + - Все данные проходят через ExperimentData - Каждый Executor может читать и модифицировать данные - Результаты сохраняются в структурированном виде **3. Разделение ответственности** + - Executor'ы выполняют вычисления - Experiment'ы управляют последовательностью выполнения - Reporter'ы форматируют результаты - Shell'ы предоставляют удобный интерфейс **4. Расширяемость через полиморфизм** + - Абстрактные базовые классы определяют контракты - Конкретные реализации следуют единому интерфейсу - Новая функциональность добавляется через создание новых классов **5. Immutability где возможно** + - ExperimentData копируется при необходимости - Transformers работают с копиями данных - Состояние изменяется явно и контролируемо @@ -58,19 +68,24 @@ HypEx реализует 8 уровней абстракции, от прост Архитектура HypEx построена на трех основных слоях, каждый из которых имеет четкую зону ответственности: #### Слой Shell (Пользовательский интерфейс) + Самый верхний слой, предоставляющий готовые к использованию решения: + - **ExperimentShell** — базовый класс для всех оболочек - **AATest, ABTest, HomogeneityTest, Matching** — предконфигурированные эксперименты - **Output классы** — форматирование и представление результатов Этот слой скрывает всю сложность и позволяет запускать эксперименты в 2-3 строки кода: + ```python test = ABTest(multitest_method="bonferroni") results = test.execute(data) ``` #### Слой Experiments (Оркестрация) + Средний слой, отвечающий за управление потоком выполнения: + - **Experiment** — базовый класс для композиции Executor'ов - **Специализированные эксперименты** — OnRoleExperiment, GroupExperiment, ParamsExperiment - **Управление итерациями** — CycledExperiment, IfParamsExperiment @@ -78,7 +93,9 @@ results = test.execute(data) Этот слой определяет, КАК и В КАКОМ ПОРЯДКЕ выполняются вычисления. #### Слой Executors/Reporters (Исполнение и отчетность) + Нижний слой, где происходят фактические вычисления: + - **Executors** — выполняют конкретные операции (тесты, преобразования, анализ) - **Reporters** — извлекают и форматируют результаты из ExperimentData @@ -109,13 +126,14 @@ results = test.execute(data) 1. **Инициализация**: Dataset оборачивается в ExperimentData 2. **Выполнение**: Каждый Executor в цепочке: - - Читает необходимые данные из ExperimentData - - Выполняет свою операцию - - Записывает результаты обратно в ExperimentData + - Читает необходимые данные из ExperimentData + - Выполняет свою операцию + - Записывает результаты обратно в ExperimentData 3. **Отчетность**: Reporter извлекает результаты и форматирует их 4. **Вывод**: Output классы представляют результаты пользователю -Ключевая особенность — ExperimentData служит общей шиной данных, через которую компоненты обмениваются информацией, не зная о существовании друг друга. +Ключевая особенность — ExperimentData служит общей шиной данных, через которую компоненты обмениваются информацией, не +зная о существовании друг друга. ## 3. Структуры данных: Dataset и ExperimentData @@ -126,11 +144,13 @@ Dataset — это основная структура для хранения #### Архитектура Dataset **Компоненты:** + - **Backend** — адаптер для работы с конкретной реализацией (PandasBackend) - **Roles** — словарь, сопоставляющий колонки с их семантическими ролями - **Методы** — богатый API для манипуляции данными **Ключевые особенности:** + - Инкапсулирует pandas DataFrame, но может работать с другими backend'ами - Каждая колонка имеет роль (Role), определяющую её семантику - Поддерживает цепочки операций (fluent interface) @@ -163,6 +183,7 @@ ABCRole (abstract) ``` Роли позволяют: + - Автоматически определять, какие колонки использовать для анализа - Валидировать корректность данных - Применять правильные преобразования @@ -190,7 +211,8 @@ ds_merged = ds1.merge(ds2, on='id') ### ExperimentData: Контекст эксперимента -ExperimentData — это расширенный контейнер, который хранит не только исходные данные, но и все промежуточные результаты, метаданные и состояние эксперимента. +ExperimentData — это расширенный контейнер, который хранит не только исходные данные, но и все промежуточные результаты, +метаданные и состояние эксперимента. #### Структура ExperimentData @@ -198,16 +220,16 @@ ExperimentData — это расширенный контейнер, котор class ExperimentData: # Основные данные _data: Dataset # Исходный датасет - + # Дополнительные поля (новые колонки, созданные Executor'ами) additional_fields: Dataset - + # Переменные (скалярные значения, метрики) variables: dict[str, dict[str, Any]] - + # Группы (разбиение данных на подгруппы) groups: dict[str, dict[str, Dataset]] - + # Таблицы анализа (результаты тестов и анализов) analysis_tables: dict[str, Dataset] ``` @@ -217,24 +239,24 @@ class ExperimentData: ExperimentData организует данные в четыре пространства имен: 1. **additional_fields** — новые признаки и колонки - - Результаты encoding'а - - Вычисленные features - - Matched индексы + - Результаты encoding'а + - Вычисленные features + - Matched индексы 2. **variables** — скалярные значения и словари - - Параметры моделей - - Вычисленные константы - - Метрики качества + - Параметры моделей + - Вычисленные константы + - Метрики качества 3. **groups** — сгруппированные данные - - Разбиение на control/test - - Подгруппы для анализа - - Результаты стратификации + - Разбиение на control/test + - Подгруппы для анализа + - Результаты стратификации 4. **analysis_tables** — результаты анализов - - Результаты статистических тестов - - Таблицы сравнений - - Агрегированные метрики + - Результаты статистических тестов + - Таблицы сравнений + - Агрегированные метрики #### Взаимодействие с Executors @@ -244,10 +266,10 @@ ExperimentData организует данные в четыре простра def execute(self, data: ExperimentData) -> ExperimentData: # 1. Извлечение необходимых данных input_data = data.ds # или data.additional_fields, data.groups и т.д. - + # 2. Выполнение операции result = self._inner_function(input_data) - + # 3. Сохранение результата в нужное пространство имен return data.set_value( space=ExperimentDataEnum.analysis_tables, @@ -259,16 +281,19 @@ def execute(self, data: ExperimentData) -> ExperimentData: #### Система идентификаторов Каждый Executor имеет уникальный ID, построенный по схеме: + ``` ClassName╤ParamsHash╤Key ``` Где: + - `ClassName` — имя класса Executor'а - `ParamsHash` — хеш параметров - `Key` — дополнительный ключ (например, имя колонки) Это позволяет: + - Избегать повторных вычислений - Находить результаты конкретных Executor'ов - Строить зависимости между компонентами @@ -284,6 +309,7 @@ Dataset → ExperimentData → [Executor1] → ExperimentData' → [Executor2] ``` Каждый Executor может: + - Читать из любого пространства имен - Писать в одно или несколько пространств - Использовать результаты предыдущих Executor'ов @@ -294,6 +320,7 @@ Dataset → ExperimentData → [Executor1] → ExperimentData' → [Executor2] ### Концепция Executor Executor — это фундаментальный строительный блок HypEx. Каждый Executor представляет собой атомарную операцию, которая: + - Принимает ExperimentData на вход - Выполняет одну конкретную задачу - Возвращает модифицированный ExperimentData @@ -305,26 +332,27 @@ Executor — это фундаментальный строительный бл ```python class Executor(ABC): def __init__(self, key: Any = ""): - self._id: str = "" # Уникальный идентификатор - self._params_hash = "" # Хеш параметров - self.key: Any = key # Дополнительный ключ + self._id: str = "" # Уникальный идентификатор + self._params_hash = "" # Хеш параметров + self.key: Any = key # Дополнительный ключ self._generate_id() - + @abstractmethod def execute(self, data: ExperimentData) -> ExperimentData: """Основной метод выполнения""" raise AbstractMethodError - + def set_params(self, params: dict) -> None: """Динамическая установка параметров""" # Позволяет изменять параметры после создания - + def _generate_id(self): """Генерация уникального ID""" # ClassName╤ParamsHash╤Key ``` **Ключевые особенности:** + - **Единый интерфейс** — все Executor'ы реализуют метод `execute` - **Самоидентификация** — каждый Executor знает свой уникальный ID - **Параметризация** — поддержка динамического изменения параметров @@ -342,7 +370,7 @@ class Calculator(Executor, ABC): def calc(cls, data: Dataset, **kwargs): """Статический метод для вычислений""" return cls._inner_function(data, **kwargs) - + @staticmethod @abstractmethod def _inner_function(data: Dataset, **kwargs) -> Any: @@ -351,6 +379,7 @@ class Calculator(Executor, ABC): ``` **Назначение:** Разделение логики вычисления от логики работы с ExperimentData. Это позволяет: + - Использовать вычисления отдельно от эксперимента - Тестировать логику изолированно - Переиспользовать код в разных контекстах @@ -387,17 +416,17 @@ IfExecutor реализует паттерн условного выполнен ```python class IfExecutor(Executor, ABC): - def __init__(self, + def __init__(self, if_executor: Executor | None = None, else_executor: Executor | None = None): self.if_executor = if_executor self.else_executor = else_executor - + @abstractmethod def check_rule(self, data) -> bool: """Проверка условия""" pass - + def execute(self, data: ExperimentData) -> ExperimentData: if self.check_rule(data): return self.if_executor.execute(data) if self.if_executor else data @@ -408,6 +437,7 @@ class IfExecutor(Executor, ABC): **Назначение:** Ветвление логики выполнения на основе условий. **Пример использования — IfAAExecutor:** + ```python class IfAAExecutor(IfExecutor): def check_rule(self, data) -> bool: @@ -423,13 +453,33 @@ Analyzers наследуются напрямую от Executor и выполн ```python # Прямые наследники Executor -├── OneAAStatAnalyzer — анализ статистики одного A/A теста -├── AAScoreAnalyzer — оценка качества A/A тестов и выбор лучшего -├── ABAnalyzer — анализ A/B теста с коррекцией множественного тестирования -└── MatchingAnalyzer — анализ качества matching'а +├── OneAAStatAnalyzer — анализ +статистики +одного +A / A +теста +├── AAScoreAnalyzer — оценка +качества +A / A +тестов +и +выбор +лучшего +├── ABAnalyzer — анализ +A / B +теста +с +коррекцией +множественного +тестирования +└── MatchingAnalyzer — анализ +качества +matching +'а ``` **Особенности Analyzers:** + - Работают с результатами других Executor'ов - Агрегируют и анализируют множественные результаты - Принимают сложные решения на основе статистики @@ -437,6 +487,7 @@ Analyzers наследуются напрямую от Executor и выполн ### Паттерн "Цепочка ответственности" Executor'ы образуют цепочку, где каждый: + 1. Получает ExperimentData от предыдущего 2. Выполняет свою операцию 3. Передает обогащенный ExperimentData следующему @@ -444,11 +495,11 @@ Executor'ы образуют цепочку, где каждый: ```python # Пример цепочки для A/B теста chain = [ - GroupSizes(), # Подсчет размеров групп - GroupDifference(), # Вычисление разницы между группами - TTest(), # T-тест - KSTest(), # KS-тест - ABAnalyzer() # Анализ и коррекция множественного тестирования + GroupSizes(), # Подсчет размеров групп + GroupDifference(), # Вычисление разницы между группами + TTest(), # T-тест + KSTest(), # KS-тест + ABAnalyzer() # Анализ и коррекция множественного тестирования ] # Выполнение цепочки @@ -462,6 +513,7 @@ for executor in chain: Каждый Executor имеет уникальный ID вида: `ClassName╤ParamsHash╤Key` **Генерация ParamsHash:** + ```python def _generate_params_hash(self): # Для AASplitter @@ -474,6 +526,7 @@ def _generate_params_hash(self): ``` **Восстановление из ID:** + ```python @classmethod def build_from_id(cls, executor_id: str): @@ -485,6 +538,7 @@ def build_from_id(cls, executor_id: str): ``` Это позволяет: + - **Кеширование** — не выполнять повторно одинаковые операции - **Трассировка** — понимать, какой Executor создал какой результат - **Воспроизводимость** — восстанавливать состояние из ID @@ -536,19 +590,20 @@ class MyCustomTest(StatHypothesisTesting): # Реализация кастомного теста statistic = calculate_my_statistic(data, test_data) p_value = calculate_p_value(statistic) - + return Dataset.from_dict({ "statistic": statistic, "p-value": p_value, "pass": p_value < cls.reliability }) - + def execute(self, data: ExperimentData) -> ExperimentData: # Использует базовую логику StatHypothesisTesting return super().execute(data) ``` Таким образом, Executor Framework обеспечивает: + - **Модульность** — каждая операция инкапсулирована - **Переиспользуемость** — Executor'ы можно комбинировать по-разному - **Расширяемость** — легко добавлять новую функциональность @@ -556,11 +611,13 @@ class MyCustomTest(StatHypothesisTesting): ## 5. Слой вычислений: Comparators, Transformers, Operators -Слой вычислений содержит конкретные реализации Calculator'ов, каждая из которых специализируется на определенном типе операций. Все они следуют паттерну разделения вычислительной логики от работы с ExperimentData. +Слой вычислений содержит конкретные реализации Calculator'ов, каждая из которых специализируется на определенном типе +операций. Все они следуют паттерну разделения вычислительной логики от работы с ExperimentData. ### Comparators: Сравнение и тестирование -Comparators отвечают за сравнение групп и проведение статистических тестов. Они имеют сложную иерархию и богатую функциональность. +Comparators отвечают за сравнение групп и проведение статистических тестов. Они имеют сложную иерархию и богатую +функциональность. #### Архитектура Comparator @@ -580,6 +637,7 @@ class Comparator(Calculator, ABC): **Режимы сравнения (compare_by):** #### Режим "groups" — сравнение между группами + Самый распространенный режим для A/B тестов. Сравнивает одну и ту же метрику между разными группами (control vs test). ```python @@ -594,7 +652,7 @@ class Comparator(Calculator, ABC): comparator = TTest( compare_by="groups", grouping_role=TreatmentRole(), # группировка по "group" - target_roles=TargetRole() # анализ "revenue" и "retention" + target_roles=TargetRole() # анализ "revenue" и "retention" ) # Результат: @@ -603,6 +661,7 @@ comparator = TTest( ``` #### Режим "columns" — сравнение колонок между собой + Сравнивает разные метрики внутри всего датасета. Полезно для анализа корреляций или изменений между pre/post периодами. ```python @@ -614,8 +673,8 @@ comparator = TTest( comparator = GroupDifference( compare_by="columns", - baseline_role=PreTargetRole(), # baseline: "pre_revenue", "pre_clicks" - target_roles=TargetRole() # сравнить с: "post_revenue", "post_clicks" + baseline_role=PreTargetRole(), # baseline: "pre_revenue", "pre_clicks" + target_roles=TargetRole() # сравнить с: "post_revenue", "post_clicks" ) # Результат: @@ -624,6 +683,7 @@ comparator = GroupDifference( ``` #### Режим "columns_in_groups" — сравнение колонок внутри каждой группы + Комбинация первых двух режимов. Сравнивает разные метрики, но отдельно для каждой группы. ```python @@ -635,9 +695,9 @@ comparator = GroupDifference( comparator = GroupDifference( compare_by="columns_in_groups", - grouping_role=TreatmentRole(), # группировка по "group" - baseline_role=PreTargetRole(), # baseline: "pre_revenue" - target_roles=TargetRole() # сравнить с: "post_revenue" + grouping_role=TreatmentRole(), # группировка по "group" + baseline_role=PreTargetRole(), # baseline: "pre_revenue" + target_roles=TargetRole() # сравнить с: "post_revenue" ) # Результат: @@ -646,7 +706,9 @@ comparator = GroupDifference( ``` #### Режим "cross" — перекрестное сравнение -Самый сложный режим. Сравнивает изменения между колонками в разных группах. Используется для difference-in-differences анализа. + +Самый сложный режим. Сравнивает изменения между колонками в разных группах. Используется для difference-in-differences +анализа. ```python comparator = GroupDifference( @@ -661,6 +723,7 @@ comparator = GroupDifference( ``` **Применение режимов:** + - **"groups"** — классические A/B тесты, сравнение метрик между группами - **"columns"** — анализ изменений во времени, pre/post анализ - **"columns_in_groups"** — гетерогенные эффекты, анализ по сегментам @@ -669,6 +732,7 @@ comparator = GroupDifference( #### Базовые метрики (GroupDifference, GroupSizes) **GroupDifference** вычисляет разницу между группами: + ```python class GroupDifference(Comparator): def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: @@ -683,6 +747,7 @@ class GroupDifference(Comparator): ``` **GroupSizes** подсчитывает размеры групп: + ```python class GroupSizes(Comparator): def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: @@ -715,6 +780,7 @@ class StatHypothesisTesting(Comparator, ABC): 4. **Chi2Test** — хи-квадрат тест для категориальных переменных Каждый тест возвращает стандартизированный результат: + ```python { "p-value": 0.042, @@ -726,6 +792,7 @@ class StatHypothesisTesting(Comparator, ABC): #### Специализированные метрики **PSI (Population Stability Index)** — измеряет стабильность распределения: + ```python class PSI(Comparator): def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: @@ -735,6 +802,7 @@ class PSI(Comparator): ``` **MahalanobisDistance** — вычисляет расстояние Махаланобиса: + ```python class MahalanobisDistance(Calculator): def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: @@ -750,7 +818,8 @@ class MahalanobisDistance(Calculator): ### Transformers: Преобразование данных -Transformers изменяют сам Dataset (в отличие от других Calculator'ов, которые только вычисляют результаты). Они работают с копией данных для обеспечения immutability. +Transformers изменяют сам Dataset (в отличие от других Calculator'ов, которые только вычисляют результаты). Они работают +с копией данных для обеспечения immutability. #### Базовый класс Transformer @@ -759,7 +828,7 @@ class Transformer(Calculator, ABC): @property def _is_transformer(self) -> bool: return True # Маркер для Experiment - + def execute(self, data: ExperimentData) -> ExperimentData: # Создает копию и модифицирует данные return data.copy(data=self.calc(data=data.ds)) @@ -770,11 +839,12 @@ class Transformer(Calculator, ABC): Фильтры изменяют роли колонок или удаляют строки/колонки: **ConstFilter** — фильтрует константные колонки: + ```python class ConstFilter(Transformer): def __init__(self, threshold: float = 0.95): self.threshold = threshold - + def _inner_function(data: Dataset, target_cols, threshold) -> Dataset: for column in target_cols: value_counts = data[column].value_counts(normalize=True) @@ -784,6 +854,7 @@ class ConstFilter(Transformer): ``` **CorrFilter** — удаляет коррелированные признаки: + ```python class CorrFilter(Transformer): def _inner_function(data: Dataset, threshold: float = 0.8) -> Dataset: @@ -796,14 +867,15 @@ class CorrFilter(Transformer): ``` **OutliersFilter** — удаляет выбросы: + ```python class OutliersFilter(Transformer): - def __init__(self, + def __init__(self, lower_percentile: float = 0.05, upper_percentile: float = 0.95): self.lower_percentile = lower_percentile self.upper_percentile = upper_percentile - + def _inner_function(data: Dataset, target_cols, lower, upper) -> Dataset: for col in target_cols: q_low = data[col].quantile(lower) @@ -815,11 +887,12 @@ class OutliersFilter(Transformer): #### Обработка пропусков и категорий **NaFiller** — заполнение пропущенных значений: + ```python class NaFiller(Transformer): def __init__(self, method: str = "ffill"): self.method = method # 'ffill', 'bfill', 'mean', 'median', etc. - + def _inner_function(data: Dataset, target_cols, method) -> Dataset: if method in ['ffill', 'bfill']: return data.fillna(method=method) @@ -830,11 +903,12 @@ class NaFiller(Transformer): ``` **CategoryAggregator** — агрегация редких категорий: + ```python class CategoryAggregator(Transformer): def __init__(self, min_frequency: float = 0.01): self.min_frequency = min_frequency - + def _inner_function(data: Dataset, target_cols, min_freq) -> Dataset: for col in target_cols: value_counts = data[col].value_counts(normalize=True) @@ -850,8 +924,8 @@ Encoders преобразуют категориальные признаки в ```python class Encoder(Calculator): def execute(self, data: ExperimentData) -> ExperimentData: - target_cols = data.ds.search_columns(roles=FeatureRole(), - search_types=[str]) + target_cols = data.ds.search_columns(roles=FeatureRole(), + search_types=[str]) encoded = self.calc(data=data.ds, target_cols=target_cols) # Сохраняем в additional_fields return data.set_value( @@ -862,6 +936,7 @@ class Encoder(Calculator): ``` **DummyEncoder** — one-hot encoding: + ```python class DummyEncoder(Encoder): def _inner_function(data: Dataset, target_cols) -> Dataset: @@ -874,9 +949,11 @@ class DummyEncoder(Encoder): ### GroupOperators: Операции над группами -GroupOperators выполняют специализированные операции над группами данных, часто используемые в matching и causal inference. +GroupOperators выполняют специализированные операции над группами данных, часто используемые в matching и causal +inference. **SMD (Standardized Mean Difference)** — стандартизированная разница средних: + ```python class SMD(GroupOperator): def _inner_function(data: Dataset, test_data: Dataset) -> float: @@ -886,6 +963,7 @@ class SMD(GroupOperator): ``` **Bias** — оценка смещения для matching: + ```python class Bias(GroupOperator): def _inner_function(data: Dataset, matched_data: Dataset) -> dict: @@ -898,11 +976,12 @@ class Bias(GroupOperator): ``` **MatchingMetrics** — метрики качества matching: + ```python class MatchingMetrics(GroupOperator): def __init__(self, metric: str = "ate"): self.metric = metric # 'ate', 'att', 'atc' - + def _inner_function(data: Dataset, matched_indices: Dataset) -> dict: if self.metric == "ate": # Average Treatment Effect effect = matched_treatment.mean() - matched_control.mean() @@ -927,19 +1006,19 @@ class AASplitter(Calculator): self.control_size = control_size self.random_state = random_state self.sample_size = sample_size - + def _inner_function(data: Dataset, control_size, random_state, sample_size) -> list[str]: # Сэмплирование если нужно if sample_size: data = data.sample(frac=sample_size, random_state=random_state) - + # Разделение на control/test n_control = int(len(data) * control_size) indices = data.sample(frac=1, random_state=random_state).index - + split = pd.Series("test", index=data.index) split[indices[:n_control]] = "control" - + return split.tolist() ``` @@ -950,37 +1029,37 @@ class AASplitterWithStratification(AASplitter): def _inner_function(data: Dataset, control_size, random_state, grouping_fields) -> Dataset: if not grouping_fields: return super()._inner_function(data, control_size, random_state) - + # Разделение внутри каждой страты result = [] for group, group_data in data.groupby(grouping_fields): split = super()._inner_function(group_data, control_size, random_state) result.extend(split) - + return Dataset.from_dict({"split": result}, roles={"split": TreatmentRole()}) ``` ### Принципы проектирования Calculator'ов 1. **Разделение вычисления и контекста:** - - `_inner_function` — чистая логика вычисления - - `execute` — работа с ExperimentData - - `calc` — статический интерфейс для использования вне экспериментов + - `_inner_function` — чистая логика вычисления + - `execute` — работа с ExperimentData + - `calc` — статический интерфейс для использования вне экспериментов 2. **Унифицированный результат:** - - Всегда возвращают Dataset или dict - - Результаты имеют стандартную структуру - - Роли определяют семантику результатов + - Всегда возвращают Dataset или dict + - Результаты имеют стандартную структуру + - Роли определяют семантику результатов 3. **Конфигурируемость:** - - Параметры передаются через конструктор - - Поддержка `set_params` для динамического изменения - - Параметры влияют на ID для кеширования + - Параметры передаются через конструктор + - Поддержка `set_params` для динамического изменения + - Параметры влияют на ID для кеширования 4. **Поиск подходящих данных:** - - Используют роли для поиска нужных колонок - - Могут фильтровать по типам данных - - Работают с temporary roles при необходимости + - Используют роли для поиска нужных колонок + - Могут фильтровать по типам данных + - Работают с temporary roles при необходимости ### Взаимодействие компонентов @@ -991,17 +1070,17 @@ pipeline = [ OutliersFilter(lower_percentile=0.05, upper_percentile=0.95), NaFiller(method="ffill"), DummyEncoder(), - + # 2. Вычисление расстояний MahalanobisDistance(grouping_role=TreatmentRole()), - + # 3. Поиск пар FaissNearestNeighbors(n_neighbors=1), - + # 4. Оценка качества Bias(grouping_role=TreatmentRole()), MatchingMetrics(metric="ate"), - + # 5. Статистические тесты TTest(compare_by="groups"), KSTest(compare_by="groups") @@ -1009,183 +1088,636 @@ pipeline = [ ``` Каждый компонент: + - Независим и может быть заменен - Использует результаты предыдущих через ExperimentData - Добавляет свои результаты для последующих Это обеспечивает максимальную гибкость при построении экспериментов. -## 6. Analyzer'ы — комплексный анализ результатов +## 6. Extension Framework -Analyzer'ы представляют собой специальный класс Executor'ов, которые выполняют высокоуровневый анализ результатов экспериментов. В отличие от простых вычислительных блоков (Calculator'ов), Analyzer'ы работают с результатами множества других Executor'ов, агрегируют их и принимают комплексные решения. +### Концепция Extension'ов -### Архитектура Analyzer'ов - -Analyzer'ы наследуются напрямую от Executor, минуя Calculator, так как их задача — не вычисления над сырыми данными, а анализ уже полученных результатов. Типичный Analyzer: - -1. **Извлекает результаты** предыдущих Executor'ов из ExperimentData -2. **Анализирует и агрегирует** эти результаты согласно своей логике -3. **Принимает решения** на основе комплексных критериев -4. **Сохраняет итоговый анализ** в analysis_tables +Extension'ы в HypEx представляют собой систему для инкапсуляции backend-специфичных вычислений, которая обеспечивает работу Calculator'ов с различными типами данных (pandas, Spark, и другими) через единый интерфейс. -### OneAAStatAnalyzer — анализ одного A/A теста +#### Ключевое архитектурное разделение -**Назначение:** Анализирует результаты статистических тестов для одного разбиения A/A теста. +**Backend-агностичность vs Backend-специфичность** — это основной принцип разделения ответственности в HypEx: -**Функциональность:** -- Извлекает результаты всех примененных тестов (TTest, KSTest, Chi2Test) -- Для каждой метрики подсчитывает количество прошедших тестов -- Оценивает общее качество разбиения по pass rate -- Классифицирует качество: excellent (>95%), good (>80%), acceptable (>60%), poor (<60%) +**Calculator'ы (Backend-агностичные):** +- Работают исключительно через Dataset API +- Не зависят от конкретной реализации backend'а +- Делегируют backend-специфичные вычисления Extension'ам +- Фокусируются на бизнес-логике анализа -**Входные данные:** Результаты статистических тестов в analysis_tables +**Extension'ы (Backend-специфичные):** +- Инкапсулируют детали работы с конкретными технологиями +- Предоставляют оптимизированные реализации для разных backend'ов +- Изолируют внешние зависимости (numpy, scipy, sklearn и т.д.) +- Автоматически выбирают правильную реализацию для текущего backend'а -**Выходные данные:** -- Статистика по каждой метрике (passed/total tests, pass_rate, status) -- Общая оценка качества разбиения (overall pass_rate, quality level) +### Архитектура Extension'ов -### AAScoreAnalyzer — выбор лучшего разбиения +#### Базовый класс Extension -**Назначение:** Анализирует результаты множественных A/A тестов и выбирает оптимальное разбиение. +```python +class Extension(ABC): + """Базовый класс для всех Extension'ов в HypEx""" + + def __init__(self): + # Маппинг типов backend'ов на соответствующие методы реализации + self.BACKEND_MAPPING = { + PandasDataset: self._calc_pandas, + # SparkDataset: self._calc_spark, # Для будущих backend'ов + # DaskDataset: self._calc_dask, + } -**Функциональность:** -- Извлекает результаты всех итераций OneAAStatAnalyzer -- Вычисляет score для каждого разбиения согласно критерию: - - `max_pass_rate` — максимизация доли прошедших тестов - - `min_bias` — минимизация смещения между группами - - `balanced` — комбинация pass_rate и баланса групп -- Ранжирует разбиения по качеству -- Восстанавливает параметры лучшего Splitter'а -- Применяет лучшее разбиение к данным + @abstractmethod + def _calc_pandas(self, data: Dataset, **kwargs): + """Backend-специфичная реализация для pandas""" + raise AbstractMethodError -**Входные данные:** Результаты множественных OneAAStatAnalyzer + def calc(self, data: Dataset, **kwargs): + """Основной метод - автоматически выбирает правильную реализацию""" + backend_type = type(data.backend) + implementation = self.BACKEND_MAPPING[backend_type] + return implementation(data=data, **kwargs) -**Выходные данные:** -- ID и параметры лучшего разбиения -- Статистика по всем разбиениям (scores, mean, std) -- Колонка с лучшим разбиением в additional_fields + @staticmethod + def result_to_dataset(result: Any, roles: ABCRole | dict[str, ABCRole]) -> Dataset: + """Утилитарный метод для преобразования результата обратно в Dataset""" + return DatasetAdapter.to_dataset(result, roles=roles) +``` -### ABAnalyzer — анализ A/B теста +#### Принцип автоматического выбора backend'а -**Назначение:** Выполняет финальный анализ A/B теста с учетом множественного тестирования. +Extension'ы автоматически определяют тип backend'а Dataset'а и вызывают соответствующую реализацию: -**Функциональность:** -- Проверяет достаточность размера выборки -- Извлекает все p-values из проведенных тестов -- Применяет коррекцию множественного тестирования: - - Bonferroni — консервативная коррекция - - Holm — последовательная коррекция - - FDR — контроль False Discovery Rate -- Оценивает размер эффекта и его confidence interval -- Разделяет статистическую и практическую значимость -- Принимает решение по каждой метрике: - - `ship` — значимый и практичный эффект - - `monitor` — значимый, но малый эффект - - `investigate` — большой, но незначимый эффект - - `no_effect` — нет эффекта -- Формирует общие рекомендации по эксперименту +```python +# Пример: Extension автоматически выбирает pandas реализацию +dataset = Dataset(data=pandas_dataframe, roles=roles) +extension = CholeskyExtension() -**Входные данные:** -- Результаты статистических тестов -- Размеры групп из GroupSizes -- Эффекты из GroupDifference +# calc() автоматически вызовет _calc_pandas() +result = extension.calc(dataset) +``` -**Выходные данные:** -- Детальный анализ по каждой метрике -- Скорректированные p-values -- Общее решение и рекомендации -- Оценка рисков и качества выборки +### Типы Extension'ов -### MatchingAnalyzer — оценка качества matching +#### 1. Базовые математические Extension'ы -**Назначение:** Комплексная оценка качества matching'а между группами. +Инкапсулируют фундаментальные математические операции: -**Функциональность:** -- Извлекает matched индексы из FaissNearestNeighbors -- Создает matched датасеты для control и treatment -- Оценивает баланс ковариат через SMD (Standardized Mean Difference) -- Вычисляет метрики качества: - - SMD — стандартизированная разница средних - - KS статистика — различие распределений - - Variance ratio — соотношение дисперсий -- Оценивает treatment effect после matching -- Выполняет диагностику проблем: - - Проверка размера matched выборки - - Идентификация несбалансированных ковариат - - Оценка common support region -- Генерирует рекомендации по улучшению +```python +class CholeskyExtension(Extension): + """Extension для разложения Холецкого""" + + def _calc_pandas(self, data: Dataset, epsilon: float = 1e-3, **kwargs): + # Получаем numpy массив из pandas backend'а + cov = data.data.to_numpy() + + # Добавляем регуляризацию для численной стабильности + cov = cov + np.eye(cov.shape[0]) * epsilon + + # Выполняем разложение Холецкого + cholesky_result = np.linalg.cholesky(cov) + + # Преобразуем результат обратно в Dataset + return self.result_to_dataset( + pd.DataFrame(cholesky_result, columns=data.columns), + {column: FeatureRole() for column in data.columns} + ) -**Входные данные:** -- Matched индексы из additional_fields -- Исходные данные control и treatment групп +class InverseExtension(Extension): + """Extension для обращения матрицы""" + + def _calc_pandas(self, data: Dataset, **kwargs): + inverse_matrix = np.linalg.inv(data.data.to_numpy()) + + return self.result_to_dataset( + pd.DataFrame(inverse_matrix, columns=data.columns), + {column: FeatureRole() for column in data.columns} + ) +``` -**Выходные данные:** -- Количество и доля matched пар -- Баланс по каждой ковариате -- Общие метрики качества matching -- Treatment effect с доверительными интервалами -- Диагностика и рекомендации +#### 2. Encoder Extension'ы -### Паттерны использования Analyzer'ов +Обеспечивают предобработку данных: -#### 1. Последовательный анализ -Analyzer'ы размещаются в конце pipeline'а для анализа накопленных результатов: ```python -experiment = Experiment([ - DataPreparation(), - StatisticalTests(), - ABAnalyzer(multitest_method="fdr") -]) +class DummyEncoderExtension(Extension): + """Extension для one-hot encoding""" + + @staticmethod + def _calc_pandas(data: Dataset, target_cols: str | None = None, **kwargs): + # Создаем dummy переменные + dummies_df = pd.get_dummies( + data=data[target_cols].data, + drop_first=True + ) + + # Создаем роли для новых колонок на основе исходных + roles = { + col: data.roles[col[:col.rfind("_")]] + for col in dummies_df.columns + } + + # Обновляем тип данных для boolean колонок + for role in roles.values(): + role.data_type = bool + + return DatasetAdapter.to_dataset(dummies_df, roles=roles) ``` -#### 2. Иерархический анализ -Один Analyzer использует результаты другого: +#### 3. Статистические Extension'ы + +Интегрируют внешние статистические библиотеки: + ```python -experiment = Experiment([ - ParamsExperiment(...), - OneAAStatAnalyzer(), # Анализ каждого теста - AAScoreAnalyzer() # Выбор лучшего -]) +class MultiTest(Extension): + """Extension для коррекции множественного тестирования из statsmodels""" + + def __init__(self, method: ABNTestMethodsEnum, alpha: float = 0.05): + self.method = method + self.alpha = alpha + super().__init__() + + def _calc_pandas(self, data: Dataset, **kwargs): + # Извлекаем p-values из Dataset + p_values = data.data.values.flatten() + + # Применяем коррекцию из statsmodels + from statsmodels.stats.multitest import multipletests + corrected_results = multipletests( + p_values, + method=self.method.value, + alpha=self.alpha, + **kwargs + ) + + # Формируем результат в структурированном виде + result_data = { + "field": [i.split(ID_SPLIT_SYMBOL)[2] for i in data.index], + "test": [i.split(ID_SPLIT_SYMBOL)[0] for i in data.index], + "old p-value": p_values, + "new p-value": corrected_results[1], + "correction": [ + j / i if j != 0 else 0.0 + for i, j in zip(corrected_results[1], p_values) + ], + "rejected": corrected_results[0], + } + + return DatasetAdapter.to_dataset(result_data, StatisticRole()) ``` -#### 3. Условный анализ -Выбор Analyzer'а на основе характеристик данных: +#### 4. Специализированные Extension'ы + +**CompareExtension** для сравнения двух Dataset'ов: + ```python -IfExecutor( - condition=lambda d: d.n_metrics > 10, - if_executor=ABAnalyzer(multitest_method="fdr"), - else_executor=ABAnalyzer(multitest_method=None) -) +class CompareExtension(Extension, ABC): + """Базовый класс для Extension'ов, сравнивающих два Dataset'а""" + + def calc(self, data: Dataset, other: Dataset | None = None, **kwargs): + return super().calc(data=data, other=other, **kwargs) ``` -### Создание кастомных Analyzer'ов +**MLExtension** для машинного обучения: -Для создания своего Analyzer'а необходимо: +```python +class MLExtension(Extension): + """Базовый класс для ML Extension'ов с поддержкой fit/predict""" + + def _calc_pandas(self, data: Dataset, test_data: Dataset | None = None, + mode: Literal["auto", "fit", "predict"] | None = None, **kwargs): + + if mode in ["auto", "fit"]: + return self.fit(data, test_data, **kwargs) + return self.predict(data) -1. **Наследоваться от Executor** (не от Calculator) -2. **Реализовать метод execute** с логикой: - - Извлечение нужных результатов через `data.get_ids()` - - Анализ и агрегация результатов - - Сохранение через `data.set_value()` -3. **Следовать конвенциям:** - - Результаты сохранять в analysis_tables - - Использовать Dataset для структурированных результатов - - Предоставлять summary и recommendations + @abstractmethod + def fit(self, X, Y=None, **kwargs): + """Обучение модели""" + raise NotImplementedError -### Преимущества Analyzer'ов + @abstractmethod + def predict(self, X, **kwargs): + """Предсказание""" + raise NotImplementedError +``` -1. **Высокоуровневая абстракция** — скрывают сложность анализа за простым интерфейсом -2. **Переиспользуемость** — стандартные паттерны анализа для типовых задач -3. **Композируемость** — можно комбинировать разные виды анализа -4. **Расширяемость** — легко добавлять новые типы анализа -5. **Воспроизводимость** — стандартизированные методы обеспечивают консистентность -6. **Decision-ready** — превращают сырые результаты в actionable insights +### Интеграция Extension'ов с Calculator'ами -Analyzer'ы являются ключевым компонентом для превращения множества технических результатов вычислений в понятные бизнес-решения и рекомендации. +#### Правильный паттерн использования -## 7. Слой экспериментов: Experiment Framework +Calculator'ы используют Extension'ы через делегирование для backend-специфичных операций: -Слой экспериментов управляет композицией и оркестрацией Executor'ов. Он определяет, КАК и В КАКОМ ПОРЯДКЕ выполняются операции, предоставляя различные стратегии выполнения. +```python +# Концептуальный пример правильного использования Extension'ов в Calculator'е +class MahalanobisDistance(Calculator): + """Calculator для вычисления расстояния Махаланобиса""" + + def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: + # Вычисление ковариационной матрицы (backend-агностично) + control_cov = data.cov() + test_cov = test_data.cov() + pooled_cov = (control_cov + test_cov) / 2 + + # Делегируем backend-специфичные операции Extension'ам + cholesky_ext = CholeskyExtension() + cholesky_result = cholesky_ext.calc(pooled_cov) + + inverse_ext = InverseExtension() + mahalanobis_transform = inverse_ext.calc(cholesky_result) + + # Применяем преобразование (backend-агностично через Dataset API) + y_control = data.dot(mahalanobis_transform.transpose()) + y_test = test_data.dot(mahalanobis_transform.transpose()) + + return {"control": y_control, "test": y_test} +``` + +#### Архитектурные преимущества + +**1. Автоматическая адаптация к backend'у:** +- Extension автоматически выбирает правильную реализацию +- Calculator остается backend-агностичным +- Добавление нового backend'а требует только реализации новых методов в Extension'ах + +**2. Изоляция зависимостей:** +- Внешние библиотеки (numpy, scipy, sklearn) изолированы в Extension'ах +- Calculator'ы не имеют прямых зависимостей на внешние библиотеки +- Упрощается управление зависимостями и тестирование + +**3. Переиспользование логики:** +- Extension'ы можно использовать в разных Calculator'ах +- Единая реализация сложных операций для всей системы + +### Жизненный цикл Extension'ов + +#### 1. Инициализация и выбор backend'а + +```python +# При вызове Extension'а происходит автоматический выбор реализации +extension = CholeskyExtension() + +# Extension анализирует тип backend'а Dataset'а +pandas_dataset = Dataset(data=pd.DataFrame(...), roles=...) +result = extension.calc(pandas_dataset) # Вызовет _calc_pandas() + +# Для другого backend'а автоматически выберется другая реализация +# spark_dataset = Dataset(data=spark_df, roles=...) +# result = extension.calc(spark_dataset) # Вызовет _calc_spark() +``` + +#### 2. Обработка результатов + +Extension'ы стандартизируют возвращаемые значения через `result_to_dataset()`: + +```python +def _calc_pandas(self, data: Dataset, **kwargs): + # Выполняем backend-специфичные вычисления + raw_result = np.some_calculation(data.data.to_numpy()) + + # Стандартизируем результат - всегда возвращаем Dataset + return self.result_to_dataset( + pd.DataFrame(raw_result, columns=data.columns), + {col: FeatureRole() for col in data.columns} + ) +``` + +### Создание кастомных Extension'ов + +#### Шаблон для создания Extension'а + +```python +class CustomExtension(Extension): + """Шаблон для создания кастомного Extension'а""" + + def __init__(self, param1: float = 1.0, param2: str = "default"): + """Инициализация с параметрами Extension'а""" + self.param1 = param1 + self.param2 = param2 + super().__init__() + + def _calc_pandas(self, data: Dataset, **kwargs): + """Реализация для pandas backend'а""" + try: + # Проверяем доступность необходимых библиотек + import required_library + + # Извлекаем данные из Dataset + numpy_data = data.data.to_numpy() + + # Выполняем backend-специфичные вычисления + result = required_library.some_function( + numpy_data, + param1=self.param1, + param2=self.param2, + **kwargs + ) + + # Преобразуем результат обратно в Dataset + if isinstance(result, np.ndarray): + result_df = pd.DataFrame(result, columns=data.columns) + roles = {col: FeatureRole() for col in data.columns} + else: + # Обработка других типов результатов + result_df = pd.DataFrame({"result": [result]}) + roles = {"result": StatisticRole()} + + return self.result_to_dataset(result_df, roles) + + except ImportError: + raise RuntimeError( + "CustomExtension требует библиотеку 'required_library'. " + "Установите: pip install required_library" + ) + except Exception as e: + raise RuntimeError(f"Ошибка в CustomExtension: {e}") + + # Для поддержки других backend'ов добавьте соответствующие методы: + # def _calc_spark(self, data: Dataset, **kwargs): + # """Реализация для Spark backend'а""" + # pass +``` + +### Архитектурные принципы Extension'ов + +#### 1. Принцип единого интерфейса + +Все Extension'ы предоставляют метод `calc()` с одинаковой сигнатурой: + +```python +# Единый интерфейс для всех Extension'ов +result = extension.calc(data=dataset, **optional_params) +``` + +#### 2. Принцип изоляции зависимостей + +Extension'ы изолируют внешние зависимости от основной логики: + +```python +# Правильно: зависимости изолированы в Extension +class ScipyStatsExtension(Extension): + def _calc_pandas(self, data: Dataset, **kwargs): + from scipy.stats import ttest_ind # Изолированный import + # ... использование scipy + +# Неправильно: прямое использование в Calculator +class BadCalculator(Calculator): + def _inner_function(data: Dataset, **kwargs): + from scipy.stats import ttest_ind # Нарушение архитектуры! + # ... это нарушает backend-агностичность +``` + +#### 3. Принцип автоматического выбора реализации + +Extension'ы автоматически выбирают оптимальную реализацию: + +```python +class AdaptiveExtension(Extension): + def __init__(self): + super().__init__() + # Расширяем mapping при добавлении новых backend'ов + if hasattr(self, '_calc_spark'): + self.BACKEND_MAPPING[SparkDataset] = self._calc_spark + if hasattr(self, '_calc_dask'): + self.BACKEND_MAPPING[DaskDataset] = self._calc_dask +``` + +#### 4. Принцип graceful degradation + +Extension'ы обеспечивают работу даже при частичной недоступности функций: + +```python +class RobustExtension(Extension): + def _calc_pandas(self, data: Dataset, **kwargs): + try: + # Пытаемся использовать оптимизированную реализацию + import scipy.linalg + return self._fast_implementation(data, **kwargs) + except ImportError: + # Fallback на базовую реализацию + return self._basic_implementation(data, **kwargs) +``` + +### Преимущества Extension Framework + +#### 1. Архитектурная чистота + +- **Четкое разделение ответственности:** Calculator'ы для логики, Extension'ы для реализации +- **Backend-агностичность:** Бизнес-логика не зависит от технических деталей +- **Изоляция зависимостей:** Внешние библиотеки не "протекают" в основной код + +#### 2. Производительность и масштабируемость + +- **Автоматические оптимизации:** Система автоматически выбирает лучшую реализацию +- **Поддержка разных backend'ов:** pandas, Spark, Dask без изменения бизнес-логики +- **Ленивые вычисления:** Некоторые Extension'ы могут использовать ленивые вычисления + +#### 3. Гибкость и расширяемость + +- **Простое добавление backend'ов:** Новые backend'ы добавляются через Extension'ы +- **Модульность:** Extension'ы можно переиспользовать и комбинировать +- **Эволюция технологий:** Поддержка новых библиотек через Extension'ы + +Extension Framework является ключевым архитектурным решением HypEx, которое обеспечивает баланс между простотой использования и техническими возможностями. Он позволяет Calculator'ам оставаться backend-агностичными, при этом используя мощь специализированных библиотек для каждого типа данных. + +## 7. Analyzer'ы — комплексный анализ результатов + +Analyzer'ы представляют собой специальный класс Executor'ов, которые выполняют высокоуровневый анализ результатов +экспериментов. В отличие от простых вычислительных блоков (Calculator'ов), Analyzer'ы работают с результатами множества +других Executor'ов, агрегируют их и принимают комплексные решения. + +### Архитектура Analyzer'ов + +Analyzer'ы наследуются напрямую от Executor, минуя Calculator, так как их задача — не вычисления над сырыми данными, а +анализ уже полученных результатов. Типичный Analyzer: + +1. **Извлекает результаты** предыдущих Executor'ов из ExperimentData +2. **Анализирует и агрегирует** эти результаты согласно своей логике +3. **Принимает решения** на основе комплексных критериев +4. **Сохраняет итоговый анализ** в analysis_tables + +### OneAAStatAnalyzer — анализ одного A/A теста + +**Назначение:** Анализирует результаты статистических тестов для одного разбиения A/A теста. + +**Функциональность:** + +- Извлекает результаты всех примененных тестов (TTest, KSTest, Chi2Test) +- Для каждой метрики подсчитывает количество прошедших тестов +- Оценивает общее качество разбиения по pass rate +- Классифицирует качество: excellent (>95%), good (>80%), acceptable (>60%), poor (<60%) + +**Входные данные:** Результаты статистических тестов в analysis_tables + +**Выходные данные:** + +- Статистика по каждой метрике (passed/total tests, pass_rate, status) +- Общая оценка качества разбиения (overall pass_rate, quality level) + +### AAScoreAnalyzer — выбор лучшего разбиения + +**Назначение:** Анализирует результаты множественных A/A тестов и выбирает оптимальное разбиение. + +**Функциональность:** + +- Извлекает результаты всех итераций OneAAStatAnalyzer +- Вычисляет score для каждого разбиения согласно критерию: + - `max_pass_rate` — максимизация доли прошедших тестов + - `min_bias` — минимизация смещения между группами + - `balanced` — комбинация pass_rate и баланса групп +- Ранжирует разбиения по качеству +- Восстанавливает параметры лучшего Splitter'а +- Применяет лучшее разбиение к данным + +**Входные данные:** Результаты множественных OneAAStatAnalyzer + +**Выходные данные:** + +- ID и параметры лучшего разбиения +- Статистика по всем разбиениям (scores, mean, std) +- Колонка с лучшим разбиением в additional_fields + +### ABAnalyzer — анализ A/B теста + +**Назначение:** Выполняет финальный анализ A/B теста с учетом множественного тестирования. + +**Функциональность:** + +- Проверяет достаточность размера выборки +- Извлекает все p-values из проведенных тестов +- Применяет коррекцию множественного тестирования: + - Bonferroni — консервативная коррекция + - Holm — последовательная коррекция + - FDR — контроль False Discovery Rate +- Оценивает размер эффекта и его confidence interval +- Разделяет статистическую и практическую значимость +- Принимает решение по каждой метрике: + - `ship` — значимый и практичный эффект + - `monitor` — значимый, но малый эффект + - `investigate` — большой, но незначимый эффект + - `no_effect` — нет эффекта +- Формирует общие рекомендации по эксперименту + +**Входные данные:** + +- Результаты статистических тестов +- Размеры групп из GroupSizes +- Эффекты из GroupDifference + +**Выходные данные:** + +- Детальный анализ по каждой метрике +- Скорректированные p-values +- Общее решение и рекомендации +- Оценка рисков и качества выборки + +### MatchingAnalyzer — оценка качества matching + +**Назначение:** Комплексная оценка качества matching'а между группами. + +**Функциональность:** + +- Извлекает matched индексы из FaissNearestNeighbors +- Создает matched датасеты для control и treatment +- Оценивает баланс ковариат через SMD (Standardized Mean Difference) +- Вычисляет метрики качества: + - SMD — стандартизированная разница средних + - KS статистика — различие распределений + - Variance ratio — соотношение дисперсий +- Оценивает treatment effect после matching +- Выполняет диагностику проблем: + - Проверка размера matched выборки + - Идентификация несбалансированных ковариат + - Оценка common support region +- Генерирует рекомендации по улучшению + +**Входные данные:** + +- Matched индексы из additional_fields +- Исходные данные control и treatment групп + +**Выходные данные:** + +- Количество и доля matched пар +- Баланс по каждой ковариате +- Общие метрики качества matching +- Treatment effect с доверительными интервалами +- Диагностика и рекомендации + +### Паттерны использования Analyzer'ов + +#### 1. Последовательный анализ + +Analyzer'ы размещаются в конце pipeline'а для анализа накопленных результатов: + +```python +experiment = Experiment([ + DataPreparation(), + StatisticalTests(), + ABAnalyzer(multitest_method="fdr") +]) +``` + +#### 2. Иерархический анализ + +Один Analyzer использует результаты другого: + +```python +experiment = Experiment([ + ParamsExperiment(...), + OneAAStatAnalyzer(), # Анализ каждого теста + AAScoreAnalyzer() # Выбор лучшего +]) +``` + +#### 3. Условный анализ + +Выбор Analyzer'а на основе характеристик данных: + +```python +IfExecutor( + condition=lambda d: d.n_metrics > 10, + if_executor=ABAnalyzer(multitest_method="fdr"), + else_executor=ABAnalyzer(multitest_method=None) +) +``` + +### Создание кастомных Analyzer'ов + +Для создания своего Analyzer'а необходимо: + +1. **Наследоваться от Executor** (не от Calculator) +2. **Реализовать метод execute** с логикой: + - Извлечение нужных результатов через `data.get_ids()` + - Анализ и агрегация результатов + - Сохранение через `data.set_value()` +3. **Следовать конвенциям:** + - Результаты сохранять в analysis_tables + - Использовать Dataset для структурированных результатов + - Предоставлять summary и recommendations + +### Преимущества Analyzer'ов + +1. **Высокоуровневая абстракция** — скрывают сложность анализа за простым интерфейсом +2. **Переиспользуемость** — стандартные паттерны анализа для типовых задач +3. **Композируемость** — можно комбинировать разные виды анализа +4. **Расширяемость** — легко добавлять новые типы анализа +5. **Воспроизводимость** — стандартизированные методы обеспечивают консистентность +6. **Decision-ready** — превращают сырые результаты в actionable insights + +Analyzer'ы являются ключевым компонентом для превращения множества технических результатов вычислений в понятные +бизнес-решения и рекомендации. + +## 8. Слой экспериментов: Experiment Framework + +Слой экспериментов управляет композицией и оркестрацией Executor'ов. Он определяет, КАК и В КАКОМ ПОРЯДКЕ выполняются +операции, предоставляя различные стратегии выполнения. ### Базовый класс Experiment @@ -1200,11 +1732,11 @@ class Experiment(Executor): self.executors = executors self.transformer = transformer or self._detect_transformer() super().__init__(key) - + def _detect_transformer(self) -> bool: """Автоматически определяет, содержит ли цепочка Transformer'ы""" return all(executor._is_transformer for executor in self.executors) - + def execute(self, data: ExperimentData) -> ExperimentData: """Последовательное выполнение всех Executor'ов""" experiment_data = deepcopy(data) if self.transformer else data @@ -1215,6 +1747,7 @@ class Experiment(Executor): ``` **Ключевые особенности:** + - **Композиция** — объединяет множество Executor'ов - **Порядок важен** — Executor'ы выполняются последовательно - **Управление состоянием** — копирует данные если есть Transformer'ы @@ -1231,7 +1764,7 @@ class OnRoleExperiment(Experiment): role: ABCRole | Sequence[ABCRole]): self.role = [role] if isinstance(role, ABCRole) else list(role) super().__init__(executors) - + def execute(self, data: ExperimentData) -> ExperimentData: # Находим все колонки с нужной ролью for field in data.ds.search_columns(self.role): @@ -1245,6 +1778,7 @@ class OnRoleExperiment(Experiment): ``` **Пример использования:** + ```python # Применить тесты ко всем target метрикам experiment = OnRoleExperiment( @@ -1271,10 +1805,10 @@ class ExperimentWithReporter(Experiment): reporter: Reporter): super().__init__(executors) self.reporter = reporter - - def one_iteration(self, - data: ExperimentData, - key: str = "") -> Dataset: + + def one_iteration(self, + data: ExperimentData, + key: str = "") -> Dataset: """Одна итерация эксперимента с отчетом""" t_data = ExperimentData(data.ds) self.key = key @@ -1296,17 +1830,17 @@ class CycledExperiment(ExperimentWithReporter): n_iterations: int = 10): super().__init__(executors, reporter) self.n_iterations = n_iterations - + def execute(self, data: ExperimentData) -> ExperimentData: results = [] for i in range(self.n_iterations): # Каждая итерация начинается с чистых данных iteration_result = self.one_iteration(data, key=str(i)) results.append(iteration_result) - + # Объединяем результаты всех итераций combined_results = Dataset.concat(results) - + return data.set_value( space=ExperimentDataEnum.analysis_tables, executor_id=self.id, @@ -1315,6 +1849,7 @@ class CycledExperiment(ExperimentWithReporter): ``` **Применение:** + - Bootstrap анализ - Оценка стабильности результатов - Monte Carlo симуляции @@ -1331,24 +1866,25 @@ class GroupExperiment(ExperimentWithReporter): searching_role: ABCRole = GroupingRole()): super().__init__(executors, reporter) self.searching_role = searching_role - + def execute(self, data: ExperimentData) -> ExperimentData: # Находим поле для группировки group_field = data.ds.search_columns([self.searching_role])[0] - + results = [] for group, group_data in data.ds.groupby(group_field): result = self.one_iteration( - ExperimentData(group_data), + ExperimentData(group_data), str(group[0]) ) results.append(result) - + # Объединяем результаты с сохранением индексов групп return self._set_result(data, results, reset_index=False) ``` **Применение:** + - Анализ по сегментам - Гетерогенные эффекты - Подгрупповой анализ @@ -1366,7 +1902,7 @@ class ParamsExperiment(ExperimentWithReporter): super().__init__(executors, reporter) self._params = params self._flat_params = [] # Все комбинации параметров - + def _update_flat_params(self): """Генерирует все комбинации параметров""" # Пример params: @@ -1380,7 +1916,7 @@ class ParamsExperiment(ExperimentWithReporter): # } # } # Создаст 3 * 3 * 3 = 27 комбинаций - + param_combinations = itertools.product(*[ itertools.product(*[ itertools.product([param], values) @@ -1388,16 +1924,16 @@ class ParamsExperiment(ExperimentWithReporter): ]) for class_params in self._params.values() ]) - + self._flat_params = list(param_combinations) - + def execute(self, data: ExperimentData) -> ExperimentData: self._update_flat_params() - + results = [] for i, flat_param in enumerate(tqdm(self._flat_params)): t_data = ExperimentData(data.ds) - + # Применяем параметры к соответствующим Executor'ам for executor in self.executors: params_for_executor = self._extract_params_for_executor( @@ -1405,14 +1941,14 @@ class ParamsExperiment(ExperimentWithReporter): ) if params_for_executor: executor.set_params(params_for_executor) - + t_data = executor.execute(t_data) - + # Сохраняем результат итерации iteration_result = self.reporter.report(t_data) iteration_result["params"] = flat_param results.append(iteration_result) - + return self._set_result(data, results) ``` @@ -1429,34 +1965,35 @@ class IfParamsExperiment(ParamsExperiment): stopping_criterion: IfExecutor): super().__init__(executors, reporter, params) self.stopping_criterion = stopping_criterion - + def execute(self, data: ExperimentData) -> ExperimentData: self._update_flat_params() - + for flat_param in tqdm(self._flat_params): t_data = ExperimentData(data.ds) - + # Выполняем эксперимент for executor in self.executors: executor.set_params(flat_param) t_data = executor.execute(t_data) - + # Проверяем условие остановки if_result = self.stopping_criterion.execute(t_data) if_executor_id = if_result.get_one_id( - self.stopping_criterion.__class__, + self.stopping_criterion.__class__, ExperimentDataEnum.variables ) - + if if_result.variables[if_executor_id]["response"]: # Условие выполнено - останавливаемся return self._set_result(data, [self.reporter.report(t_data)]) - + # Условие не выполнено ни для одной комбинации return data ``` **Применение:** + - Поиск первого "хорошего" разбиения для A/A теста - Early stopping в оптимизации - Адаптивные эксперименты @@ -1485,10 +2022,10 @@ aa_experiment = Experiment([ }, reporter=OneAADictReporter() ), - + # 2. Анализ результатов AAScoreAnalyzer(alpha=0.05), - + # 3. Условное выполнение на основе результатов IfAAExecutor( if_executor=Experiment([...]), # Если тест прошел @@ -1500,6 +2037,7 @@ aa_experiment = Experiment([ ### Жизненный цикл Experiment 1. **Создание и конфигурация:** + ```python experiment = Experiment( executors=[executor1, executor2, executor3] @@ -1507,13 +2045,15 @@ experiment = Experiment( ``` 2. **Детекция типа:** + ```python # Автоматически определяет, нужно ли копировать данные if experiment.transformer: # True если есть Transformers - # Будет работать с копией данных +# Будет работать с копией данных ``` 3. **Выполнение:** + ```python result = experiment.execute(experiment_data) # Каждый executor выполняется последовательно @@ -1521,6 +2061,7 @@ result = experiment.execute(experiment_data) ``` 4. **Доступ к результатам:** + ```python # Experiment может сам сохранить агрегированный результат experiment_id = experiment.id @@ -1540,18 +2081,18 @@ executor_ids = experiment.get_executor_ids([TTest, KSTest]) # Классический pipeline для A/B теста ab_pipeline = Experiment([ # Этап 1: Подготовка данных - NaFiller(method="ffill"), # Заполнение пропусков - OutliersFilter(lower_percentile=0.05), # Удаление выбросов - DummyEncoder(), # Кодирование категорий - + NaFiller(method="ffill"), # Заполнение пропусков + OutliersFilter(lower_percentile=0.05), # Удаление выбросов + DummyEncoder(), # Кодирование категорий + # Этап 2: Основные метрики GroupSizes(grouping_role=TreatmentRole()), GroupDifference(grouping_role=TreatmentRole()), - + # Этап 3: Статистические тесты TTest(grouping_role=TreatmentRole()), KSTest(grouping_role=TreatmentRole()), - + # Этап 4: Анализ результатов ABAnalyzer(multitest_method="bonferroni") ]) @@ -1573,13 +2114,14 @@ statistical_tests = Experiment([ # Композиция подпайплайнов full_pipeline = Experiment([ - data_preparation, # Experiment как Executor + data_preparation, # Experiment как Executor statistical_tests, # Experiment как Executor ABAnalyzer() ]) ``` **Преимущества:** + - Простота понимания и отладки - Легко модифицировать отдельные этапы - Возможность переиспользования подпайплайнов @@ -1593,17 +2135,17 @@ full_pipeline = Experiment([ adaptive_testing = Experiment([ # Подготовка DataPreparation(), - + # Проверка нормальности NormalityTest(), - + # Выбор теста на основе результата IfExecutor( condition=lambda d: d.normality_test_passed, - if_executor=TTest(), # Параметрический тест - else_executor=UTest() # Непараметрический тест + if_executor=TTest(), # Параметрический тест + else_executor=UTest() # Непараметрический тест ), - + # Дальнейший анализ ResultAnalyzer() ]) @@ -1611,7 +2153,7 @@ adaptive_testing = Experiment([ # Каскадное ветвление cascading_analysis = Experiment([ InitialTest(), - + IfExecutor( condition=lambda d: d.p_value < 0.05, if_executor=Experiment([ @@ -1630,6 +2172,7 @@ cascading_analysis = Experiment([ ``` **Преимущества:** + - Адаптивность к данным - Оптимизация вычислений - Автоматический выбор методов @@ -1643,7 +2186,7 @@ cascading_analysis = Experiment([ multi_analysis = Experiment([ # Подготовка DataPreparation(), - + # Fan-out: разные виды анализа OnRoleExperiment( executors=[ @@ -1653,7 +2196,7 @@ multi_analysis = Experiment([ ], role=TargetRole() ), - + # Fan-out: анализ по сегментам GroupExperiment( executors=[ @@ -1663,13 +2206,14 @@ multi_analysis = Experiment([ searching_role=SegmentRole(), reporter=SegmentReporter() ), - + # Fan-in: сводная таблица по всем сегментам SegmentAggregator() ]) ``` **Преимущества:** + - Параллельная обработка независимых анализов - Ансамблирование результатов для надежности - Comprehensive анализ с разных углов @@ -1697,9 +2241,9 @@ aa_grid_search = ParamsExperiment( ], params={ AASplitter: { - "random_state": range(1000), # 1000 разных seed'ов + "random_state": range(1000), # 1000 разных seed'ов "control_size": [0.3, 0.5, 0.7], # 3 варианта размера - "sample_size": [0.8, 1.0] # С сэмплированием и без + "sample_size": [0.8, 1.0] # С сэмплированием и без } }, reporter=AADictReporter() @@ -1740,7 +2284,7 @@ hyperparameter_tuning = ParamsExperiment( }, reporter=PreprocessingReporter() ), - + # Внутренний уровень: параметры модели ParamsExperiment( executors=[ @@ -1761,6 +2305,7 @@ hyperparameter_tuning = ParamsExperiment( ``` **Преимущества:** + - Систематический поиск оптимума - Возможность ранней остановки - Вложенная оптимизация для сложных pipeline'ов @@ -1780,7 +2325,7 @@ hierarchical_analysis = Experiment([ ], role=UserRole() ), - + # Уровень 2: Агрегация по сегментам GroupExperiment( executors=[ @@ -1790,7 +2335,7 @@ hierarchical_analysis = Experiment([ searching_role=SegmentRole(), reporter=SegmentReporter() ), - + # Уровень 3: Общая агрегация Experiment([ GlobalAggregator(), @@ -1803,19 +2348,20 @@ hierarchical_analysis = Experiment([ cascade_filtering = Experiment([ # Этап 1: Грубая фильтрация ConstFilter(threshold=0.99), - + # Этап 2: Фильтрация по корреляции (только для оставшихся) CorrFilter(threshold=0.9), - + # Этап 3: Тонкая фильтрация по CV (только для некоррелированных) CVFilter(lower_bound=0.01, upper_bound=10), - + # Этап 4: Финальная фильтрация outliers OutliersFilter(lower_percentile=0.01, upper_percentile=0.99) ]) ``` **Преимущества:** + - Эффективная обработка больших данных - Иерархическая агрегация результатов - Последовательное уточнение анализа @@ -1829,7 +2375,7 @@ class RetryExecutor(Executor): def __init__(self, strategies: list[Executor], max_retries: int = 3): self.strategies = strategies self.max_retries = max_retries - + def execute(self, data: ExperimentData) -> ExperimentData: for i, strategy in enumerate(self.strategies[:self.max_retries]): try: @@ -1842,6 +2388,7 @@ class RetryExecutor(Executor): continue return data + # Использование retry паттерна robust_matching = Experiment([ RetryExecutor( @@ -1851,12 +2398,12 @@ robust_matching = Experiment([ MahalanobisDistance(), FaissNearestNeighbors(n_neighbors=1, faiss_mode="base") ]), - + # Стратегия 2: Приближенный matching Experiment([ FaissNearestNeighbors(n_neighbors=5, faiss_mode="fast") ]), - + # Стратегия 3: Fallback на случайное сопоставление RandomMatching() ] @@ -1866,6 +2413,7 @@ robust_matching = Experiment([ ``` **Преимущества:** + - Устойчивость к ошибкам - Graceful degradation - Адаптивность к качеству данных @@ -1879,24 +2427,25 @@ class ObservableExperiment(Experiment): def __init__(self, executors: list[Executor], observers: list[Observer] = None): super().__init__(executors) self.observers = observers or [] - + def execute(self, data: ExperimentData) -> ExperimentData: for i, executor in enumerate(self.executors): # Уведомляем о начале for observer in self.observers: observer.on_executor_start(executor, data) - + # Выполнение start_time = time.time() data = executor.execute(data) execution_time = time.time() - start_time - + # Уведомляем о завершении for observer in self.observers: observer.on_executor_complete(executor, data, execution_time) - + return data + # Использование experiment = ObservableExperiment( executors=[...], @@ -1910,6 +2459,7 @@ experiment = ObservableExperiment( ``` **Преимущества:** + - Прозрачность выполнения - Сбор метрик и диагностика - Возможность прерывания при проблемах @@ -1924,11 +2474,13 @@ experiment = ObservableExperiment( 6. **Версионирование** — сохраняйте версии успешных экспериментов 7. **Мониторинг** — логируйте ключевые метрики выполнения -Слой экспериментов предоставляет мощные абстракции для построения сложных аналитических pipeline'ов, сохраняя при этом простоту и читаемость кода. +Слой экспериментов предоставляет мощные абстракции для построения сложных аналитических pipeline'ов, сохраняя при этом +простоту и читаемость кода. -## 8. Система Reporter'ов +## 9. Система Reporter'ов -Reporter'ы отвечают за извлечение, форматирование и представление результатов экспериментов. Они служат мостом между внутренним представлением данных в ExperimentData и форматом, удобным для пользователя или последующей обработки. +Reporter'ы отвечают за извлечение, форматирование и представление результатов экспериментов. Они служат мостом между +внутренним представлением данных в ExperimentData и форматом, удобным для пользователя или последующей обработки. ### Архитектура Reporter'ов @@ -1944,6 +2496,7 @@ Reporter'ы работают по принципу "извлечь и отфор **Reporter** — абстрактный базовый класс для всех репортеров: **Контракт:** + - Метод `report(data: ExperimentData)` — единая точка входа - Возвращает Any — конкретный формат определяется наследником - Не модифицирует ExperimentData — только читает данные @@ -1951,13 +2504,15 @@ Reporter'ы работают по принципу "извлечь и отфор ### DictReporter — универсальная основа -DictReporter является фундаментальным классом для большинства Reporter'ов в HypEx. Его философия — предоставить универсальный промежуточный формат (словарь), который легко преобразовать в любой другой. +DictReporter является фундаментальным классом для большинства Reporter'ов в HypEx. Его философия — предоставить +универсальный промежуточный формат (словарь), который легко преобразовать в любой другой. #### Концепция DictReporter **Основная идея:** Все результаты экспериментов можно представить как плоский словарь с уникальными ключами. **Формат ключей:** + ```python # Технический формат (front=False) "TTest╤╤revenue" → {"p-value": 0.042, "statistic": 2.15} @@ -1968,6 +2523,7 @@ DictReporter является фундаментальным классом дл ``` **Преимущества плоской структуры:** + - Отсутствие вложенности упрощает обработку - Уникальные ключи предотвращают конфликты - Легко конвертировать в табличный формат @@ -1982,11 +2538,13 @@ DictReporter предоставляет набор методов для изв - **`_get_struct_dict()`** — создание структурированного словаря - **`_convert_dataset_to_dict()`** — конвертация Dataset в dict -Каждый наследник DictReporter переопределяет метод `report()`, комбинируя эти методы извлечения для создания нужного словаря. +Каждый наследник DictReporter переопределяет метод `report()`, комбинируя эти методы извлечения для создания нужного +словаря. ### OnDictReporter — универсальный форматтер -OnDictReporter — это паттерн Decorator для DictReporter'ов. Он позволяет преобразовать базовый словарный формат в любой другой без изменения логики извлечения. +OnDictReporter — это паттерн Decorator для DictReporter'ов. Он позволяет преобразовать базовый словарный формат в любой +другой без изменения логики извлечения. #### Архитектура форматирования @@ -2001,6 +2559,7 @@ ExperimentData → DictReporter → dict → OnDictReporter → Любой фо #### Стандартные форматтеры **DatasetReporter** — табличное представление: + ```python class DatasetReporter(OnDictReporter): def report(self, data: ExperimentData): @@ -2013,30 +2572,35 @@ class DatasetReporter(OnDictReporter): **Потенциальные форматтеры (roadmap):** **HTMLReporter** — интерактивные HTML отчеты: + - Таблицы с сортировкой и фильтрацией - Графики и визуализации - Collapsible секции для детализации - Экспорт в различные форматы **PDFReporter** — профессиональные PDF отчеты: + - Форматированные таблицы и графики - Executive summary на первой странице - Детальные приложения с методологией - Брендирование и стилизация **MarkdownReporter** — отчеты для документации: + - Структурированный markdown - Таблицы в GFM формате - Встроенные графики как base64 - Готов для вставки в wiki/confluence **JSONReporter** — машиночитаемый формат: + - Полная сериализация результатов - Метаданные об эксперименте - Версионирование схемы - Поддержка streaming **ExcelReporter** — multi-sheet Excel файлы: + - Summary на первом листе - Детальные результаты по листам - Условное форматирование @@ -2049,17 +2613,17 @@ class CustomFormatter(OnDictReporter): def __init__(self, dict_reporter: DictReporter, format_options: dict): super().__init__(dict_reporter) self.format_options = format_options - + def report(self, data: ExperimentData): # Получаем базовый словарь base_dict = self.dict_reporter.report(data) - + # Применяем кастомное форматирование formatted = self.apply_formatting(base_dict) - + # Добавляем метаданные formatted['metadata'] = self.extract_metadata(data) - + # Возвращаем в нужном формате return self.render(formatted) ``` @@ -2077,7 +2641,7 @@ class ExperimentWithReporter(Experiment): def __init__(self, executors: list, reporter: Reporter): super().__init__(executors) self.reporter = reporter - + def one_iteration(self, data: ExperimentData, key: str = ""): # Выполняем эксперимент result_data = super().execute(data) @@ -2088,21 +2652,25 @@ class ExperimentWithReporter(Experiment): #### Специализированные эксперименты с Reporter'ами **ParamsExperiment** — всегда требует Reporter: + - После каждой комбинации параметров генерирует отчет - Агрегирует отчеты всех итераций - Позволяет сравнить результаты разных параметров **GroupExperiment** — Reporter для каждой группы: + - Генерирует отчет для каждой группы отдельно - Объединяет в общую таблицу с индексом по группам **CycledExperiment** — Reporter для каждого цикла: + - Отчет после каждой итерации - Статистика по всем итерациям #### Паттерны использования **Inline reporter в эксперименте:** + ```python experiment = ParamsExperiment( executors=[...], @@ -2112,6 +2680,7 @@ experiment = ParamsExperiment( ``` **Композиция репортеров:** + ```python base_reporter = TestDictReporter() formatted_reporter = DatasetReporter(base_reporter) @@ -2123,6 +2692,7 @@ experiment = ExperimentWithReporter( ``` **Множественные отчеты:** + ```python data = experiment.execute(initial_data) @@ -2137,6 +2707,7 @@ visual = VisualizationReporter().report(data) HypEx предоставляет набор готовых Reporter'ов для типовых задач: #### Для статистических тестов + - **TestDictReporter** — базовый класс для тестовых репортеров - **OneAADictReporter** — отчет по одному A/A тесту - **AADatasetReporter** — табличный отчет по A/A тестам @@ -2146,39 +2717,42 @@ HypEx предоставляет набор готовых Reporter'ов для - **HomoDatasetReporter** — табличный отчет по гомогенности #### Для matching + - **MatchingDictReporter** — базовый отчет по matching - **MatchingDatasetReporter** — табличный отчет по matching - **MatchingQualityDictReporter** — отчет по качеству matching - **MatchingQualityDatasetReporter** — табличный отчет по качеству #### Специальные репортеры + - **AAPassedReporter** — определение прошедших A/A тестов - **AABestSplitReporter** — отчет о лучшем разбиении -Каждый из этих Reporter'ов знает, какие именно результаты извлекать из ExperimentData и как их правильно интерпретировать. +Каждый из этих Reporter'ов знает, какие именно результаты извлекать из ExperimentData и как их правильно +интерпретировать. ### Принципы проектирования Reporter'ов 1. **Separation of Concerns:** - - Извлечение (что достать) отделено от форматирования (как показать) - - DictReporter отвечает за извлечение - - OnDictReporter отвечает за форматирование + - Извлечение (что достать) отделено от форматирования (как показать) + - DictReporter отвечает за извлечение + - OnDictReporter отвечает за форматирование 2. **Composability:** - - Reporter'ы можно комбинировать - - Один базовый reporter, множество форматов вывода + - Reporter'ы можно комбинировать + - Один базовый reporter, множество форматов вывода 3. **Reusability:** - - Один Reporter может использоваться в разных экспериментах - - Форматтеры переиспользуются для разных типов данных + - Один Reporter может использоваться в разных экспериментах + - Форматтеры переиспользуются для разных типов данных 4. **Extensibility:** - - Легко добавить новый формат вывода - - Не нужно менять логику извлечения + - Легко добавить новый формат вывода + - Не нужно менять логику извлечения 5. **Testability:** - - Reporter'ы тестируются независимо от экспериментов - - Форматтеры тестируются отдельно от извлечения + - Reporter'ы тестируются независимо от экспериментов + - Форматтеры тестируются отдельно от извлечения ### Преимущества архитектуры Reporter'ов @@ -2188,4 +2762,1112 @@ HypEx предоставляет набор готовых Reporter'ов для 4. **Maintainability** — изменения в форматировании не влияют на логику 5. **User Experience** — пользователь получает результаты в удобном виде -Reporter'ы обеспечивают элегантное решение проблемы представления результатов, позволяя HypEx адаптироваться под различные use cases и требования к отчетности. \ No newline at end of file +Reporter'ы обеспечивают элегантное решение проблемы представления результатов, позволяя HypEx адаптироваться под +различные use cases и требования к отчетности. + +## 10. Shell слой — готовые решения + +### Концепция Shell слоя + +Shell слой представляет собой вершину архитектуры HypEx, реализуя уровень 4 абстракции — "использование готовых +экспериментов". Этот слой воплощает философию библиотеки о предоставлении простых решений для сложных задач, скрывая всю +внутреннюю сложность за интуитивно понятным интерфейсом. + +**Ключевая идея Shell слоя:** Превратить запуск статистического эксперимента из многоэтапного процесса конфигурирования +в простой вызов метода с минимальным набором параметров. + +### Двухкомпонентная архитектура Shell слоя + +Shell слой построен на принципе разделения ответственности между двумя типами компонентов: + +#### ExperimentShell — конструктор экспериментов + +ExperimentShell является расширяемым конструктором типовых экспериментов. Его задачи: + +- **Инкапсуляция сложности**: Скрывает детали композиции Executor'ов, их последовательность и параметры +- **Интеллектуальная сборка**: Динамически строит конфигурацию эксперимента на основе входных параметров +- **Встроенные лучшие практики**: Автоматически применяет проверенные паттерны для каждого типа эксперимента +- **Валидация и безопасность**: Обеспечивает корректность входных данных и параметров + +```python +class ExperimentShell: + def __init__(self, experiment: Experiment, output: Output): + self.experiment = experiment # Композиция Executor'ов + self.output = output # Форматтер результатов + + def execute(self, data: Dataset) -> Output: + # Единый интерфейс для всех экспериментов + experiment_data = ExperimentData(data) + result_data = self.experiment.execute(experiment_data) + self.output.extract(result_data) + return self.output +``` + +#### Output — система форматированного представления + +Output классы реализуют кураторский подход к представлению результатов: + +- **Селективность**: Из всех возможных результатов выбирают только ключевые для принятия решений +- **Контекстуализация**: Представляют результаты с учетом специфики конкретного типа эксперимента +- **Удобство восприятия**: Форматируют данные для максимального удобства бизнес-пользователей +- **Семантическая организация**: Группируют результаты по смыслу через типизированные атрибуты +- **Доступ к детальным данным**: Сохраняют доступ к полным результатам ExperimentData для углубленного анализа + +### Готовые эксперименты + +#### AATest — автоматизированное A/A тестирование + +**Назначение:** Оценка качества разбиения на группы через многократное A/A тестирование с выбором оптимального варианта. + +**Архитектура эксперимента:** + +```python +# Упрощенная схема AATest +AATest = ExperimentShell( + experiment=ParamsExperiment([ + AASplitter(control_size=param, random_state=param), + OnRoleExperiment([ + GroupSizes(), GroupDifference(), TTest(), KSTest(), Chi2Test() + ]), + OneAAStatAnalyzer(), + IfAAExecutor(stop_condition) # Досрочная остановка при хорошем результате + ]), + output=AAOutput() +) +``` + +**Ключевые возможности:** + +- **precision_mode**: Режим повышенной точности с большим числом итераций +- **control_size**: Размер контрольной группы (по умолчанию 50/50) +- **stratification**: Стратифицированное разбиение для улучшения баланса +- **n_iterations**: Количество попыток разбиения +- **sample_size**: Размер выборки для ускорения на больших данных + +**Интеллектуальное поведение:** + +- Автоматический выбор лучшего разбиения по pass rate статистических тестов +- Досрочная остановка при достижении отличного качества (>95% pass rate) +- Адаптивная стратегия: стратификация включается автоматически при дисбалансе групп + +#### ABTest — A/B тестирование с коррекцией + +**Назначение:** Статистическое сравнение контрольной и тестовой групп с корректной обработкой множественного +тестирования. + +**Архитектура эксперимента:** + +```python +# Упрощенная схема ABTest +ABTest = ExperimentShell( + experiment=Experiment([ + OnRoleExperiment([ + GroupSizes(), GroupDifference(), TTest(), KSTest(), Chi2Test(), + # + дополнительные тесты по параметру additional_tests + ]), + ABAnalyzer(multitest_method=param) + ]), + output=ABOutput() +) +``` + +**Ключевые возможности:** + +- **additional_tests**: Дополнительные статистические тесты ("utest", "psi", "chi2") +- **multitest_method**: Метод коррекции множественного тестирования ("bonferroni", "holm", "fdr_bh") +- **reliability**: Уровень значимости (по умолчанию 0.05) + +**Интеллектуальное поведение:** + +- Автоматическое применение коррекции множественного тестирования +- Адаптивный выбор тестов в зависимости от типов данных +- Интерпретация результатов с учетом поправок на множественность + +#### HomogeneityTest — проверка однородности групп + +**Назначение:** Валидация корректности разбиения на группы через проверку их статистической однородности. + +**Архитектура эксперимента:** + +```python +# Схема HomogeneityTest +HomogeneityTest = ExperimentShell( + experiment=Experiment([ + OnRoleExperiment([ + GroupSizes(), GroupDifference(), TTest(), KSTest(), Chi2Test() + ]), + OneAAStatAnalyzer() + ]), + output=HomoOutput() +) +``` + +**Назначение и применение:** + +- Проверка качества randomization в экспериментах +- Валидация исторических разбиений +- Диагностика проблем с балансом групп +- Pre-flight проверка перед запуском A/B теста + +#### Matching — анализ сопоставления + +**Назначение:** Полный цикл matching анализа от поиска пар до оценки качества сопоставления. + +**Архитектура эксперимента:** + +```python +# Упрощенная схема Matching +Matching = ExperimentShell( + experiment=Experiment([ + # Подготовка данных + OutliersFilter(), NaFiller(), DummyEncoder(), + # Вычисление расстояний + MahalanobisDistance() if distance == "mahalanobis", + # Поиск пар + FaissNearestNeighbors(n_neighbors=1), + # Оценка качества + Bias(), MatchingMetrics(metric=param), + TTest(), KSTest(), # если quality_tests включены + MatchingAnalyzer() + ]), + output=MatchingOutput() +) +``` + +**Ключевые возможности:** + +- **group_match**: Сопоставление внутри групп или между группами +- **distance**: Метрика расстояния ("mahalanobis", "euclidean", "cosine") +- **metric**: Метрика качества matching ("ate", "att", "atc") +- **bias_estimation**: Оценка bias после сопоставления +- **quality_tests**: Дополнительные статистические тесты качества + +### Система Output'ов + +Каждый тип эксперимента имеет специализированный Output класс, оптимизированный для представления результатов +конкретного анализа: + +#### AAOutput — результаты A/A тестирования + +```python +class AAOutput: + best_split: Dataset # Лучшее разбиение с метриками качества + experiments: Dataset # Статистика по всем итерациям + aa_score: Dataset # Итоговый скор качества разбиения + best_split_statistic: Dataset # Детальная статистика лучшего разбиения +``` + +**Кураторский подход:** Пользователь получает готовое к использованию разбиение и полную диагностику его качества. + +#### ABOutput — результаты A/B тестирования + +```python +class ABOutput: + resume: Dataset # Основные результаты тестов + multitest: Dataset | str # Результаты с коррекцией множественного тестирования + sizes: Dataset # Размеры групп +``` + +**Кураторский подход:** Акцент на итоговых решениях с учетом коррекции и интерпретации значимости. + +#### HomoOutput — результаты проверки однородности + +```python +class HomoOutput: + resume: Dataset # Сводка по однородности всех метрик +``` + +**Кураторский подход:** Компактное представление с фокусом на общей оценке качества разбиения. + +#### MatchingOutput — результаты matching анализа + +```python +class MatchingOutput: + full_data: Dataset # Полный датасет с результатами matching + quality_results: Dataset # Метрики качества сопоставления + indexes: Dataset # Индексы сопоставленных объектов +``` + +**Кураторский подход:** Разделение на итоговые данные, диагностику качества и техническую информацию. + +### Доступ к детальным результатам + +Важная особенность системы Output'ов — сохранение доступа к полным результатам эксперимента для случаев, когда требуется +более глубокий анализ: + +```python +# Стандартное использование — кураторские результаты +ab_results = ab_test.execute(dataset) +summary = ab_results.resume # Основные выводы + +# Доступ к детальным результатам при необходимости +full_experiment_data = ab_results.experiment_data # Полный ExperimentData +raw_test_results = ab_results.additional_reporters # Результаты всех Reporter'ов +detailed_diagnostics = ab_results.get_detailed_analysis() # Расширенная диагностика +``` + +**Принципы доступа к сырым данным:** + +- **Прозрачность**: Любой результат нижних уровней остается доступным +- **Постепенная детализация**: От summary к details по мере необходимости +- **Техническая диагностика**: Доступ к промежуточным вычислениям для debugging +- **Интеграция с нижними уровнями**: Возможность извлечь данные для композиции с кастомными Executor'ами + +### Расширяемость и конфигурирование + +#### Принципы расширяемости Shell'ов + +Shell'ы спроектированы как расширяемые конструкторы, которые адаптируются под различные варианты типовых экспериментов: + +**1. Параметрическая расширяемость** + +```python +# Базовое использование +ab_test = ABTest() + +# Расширенная конфигурация +ab_test = ABTest( + additional_tests=["utest", "psi"], # Дополнительные тесты + multitest_method="fdr_bh", # Альтернативная коррекция + reliability=0.01 # Повышенная строгость +) +``` + +**2. Адаптивное поведение** + +- AATest автоматически включает стратификацию при дисбалансе +- Matching выбирает оптимальный алгоритм в зависимости от размера данных +- ABTest применяет разные стратегии коррекции в зависимости от количества метрик + +**3. Композиционная расширяемость** + +```python +# Кастомизация через наследование для новых типовых паттернов +class CustomABTest(ABTest): + def __init__(self, **kwargs): + super().__init__( + additional_tests=["bootstrap", "permutation"], + multitest_method="custom_fdr", + **kwargs + ) +``` + +#### Расширяемость Output'ов + +Output'ы поддерживают гибкую настройку представления результатов: + +**1. Дополнительные Reporter'ы** + +```python +# Добавление кастомных форматтеров +output = AAOutput() +output.additional_reporters = { + 'detailed_diagnostics': CustomDiagnosticsReporter(), + 'export_format': ExcelReporter() +} +``` + +**2. Конфигурация основных Reporter'ов** + +- Настройка форматирования чисел (precision, научная нотация) +- Выбор языка для интерпретаций (русский/английский) +- Настройка уровней детализации (краткий/полный отчет) + +### Практические примеры использования + +#### Минимальный код для типовых сценариев + +```python +# A/A тест для валидации разбиения +aa_test = AATest() +aa_results = aa_test.execute(dataset) +print(f"Качество разбиения: {aa_results.aa_score}") + +# A/B тест с консервативными настройками +ab_test = ABTest(multitest_method="bonferroni") +ab_results = ab_test.execute(dataset) +print(f"Значимые различия: {ab_results.resume}") + +# Проверка однородности групп +homo_test = HomogeneityTest() +homo_results = homo_test.execute(dataset) +print(f"Группы однородны: {homo_results.resume}") + +# Matching анализ с расширенной диагностикой +matching = Matching( + bias_estimation=True, + quality_tests=["ttest", "kstest"] +) +matching_results = matching.execute(dataset) +print(f"Качество matching: {matching_results.quality_results}") +``` + +#### Конфигурация под специфические требования + +```python +# Высокоточный A/A тест для критических экспериментов +aa_precision = AATest( + precision_mode=True, # Увеличенное число итераций + n_iterations=1000, # Явное задание количества попыток + stratification=True, # Принудительная стратификация + control_size=0.3 # Нестандартное соотношение групп +) + +# A/B тест с множественными гипотезами и строгой коррекцией +ab_comprehensive = ABTest( + additional_tests=["utest", "bootstrap", "psi"], + multitest_method="holm", + reliability=0.01 +) + +# Matching с кастомной метрикой расстояния +matching_custom = Matching( + distance="cosine", # Альтернативная метрика + metric="att", # Average Treatment Effect on Treated + group_match=True # Matching внутри групп +) +``` + +### Философия типовых решений + +#### Принцип "90% в 2 строчки" + +Shell слой реализует ключевой принцип библиотеки HypEx: 90% практических задач должны решаться в 2 строчки кода. Каждый +Shell создавался на основе анализа реальных индустриальных потребностей: + +- **AATest** — стандартная процедура валидации разбиений в product экспериментах +- **ABTest** — основной инструмент для измерения эффекта изменений +- **HomogeneityTest** — обязательная проверка для observational studies +- **Matching** — стандартный подход для работы с non-randomized данными + +```python +# 90% задач A/B тестирования решается так: +ab_test = ABTest() +results = ab_test.execute(dataset) +``` + +#### Встроенные лучшие практики + +Каждый Shell инкапсулирует проверенные методологические подходы: + +**Статистическая корректность:** + +- Автоматическая коррекция множественного тестирования в ABTest +- Стратификация для улучшения баланса в AATest +- Проверка assumptions для параметрических тестов +- Robust оценки при наличии выбросов + +**Production готовность:** + +- Валидация входных данных и параметров +- Graceful обработка edge cases (малые выборки, отсутствующие данные) +- Информативные сообщения об ошибках +- Детерминированность результатов при фиксированном random_state + +**Интерпретируемость:** + +- Автоматическая генерация текстовых выводов +- Предупреждения о потенциальных проблемах (низкая мощность, нарушение assumptions) +- Рекомендации по улучшению качества анализа + +#### Границы применимости и расширения + +**Когда использовать Shell'ы:** + +- Стандартные экспериментальные сценарии +- Необходимость быстрого получения надежных результатов +- Работа пользователей без глубокой экспертизы в статистике +- Production системы с требованиями к стабильности + +**Когда переходить на уровень композиции:** + +- Нестандартные экспериментальные дизайны +- Специфические статистические методы, не покрытые библиотекой +- Сложные multi-stage эксперименты +- Исследовательские задачи с неопределенной методологией + +**Создание новых Shell'ов:** +Новые Shell'ы оправданы при появлении устойчивых индустриальных паттернов: + +- Новый тип эксперимента становится стандартной практикой +- Определенная комбинация методов регулярно воспроизводится +- Есть возможность инкапсулировать лучшие практики для новой области + +Shell слой HypEx воплощает идеал современного статистического софтвера: максимальная простота использования при +сохранении методологической строгости и гибкости настройки под конкретные потребности. + +## 11. Практические примеры и сценарии + +### Философия практических примеров + +> **Важное примечание:** Код в данном разделе написан в демонстрационных целях для иллюстрации архитектурных принципов +> HypEx. Примеры не взяты из реальных проектов и не претендуют на переиспользуемость без дополнительной доработки. При +> создании production решений рекомендуется использовать официальную документацию API и лучшие практики разработки. + +Данный раздел демонстрирует, как архитектура HypEx работает в реальных сценариях. Каждый пример показывает применение +определенного уровня абстракции и объясняет, почему именно этот подход оптимален для конкретной задачи. + +**Принцип эволюции решений:** Мы покажем, как одна и та же бизнес-потребность может решаться на разных уровнях сложности +в зависимости от специфических требований. Это демонстрирует ключевое преимущество архитектуры HypEx — возможность +начать с простого решения и постепенно углубляться по мере необходимости. + +**Связь с уровнями абстракции:** + +- **Уровень 4 (Shell)** — стандартные сценарии, 90% задач +- **Уровень 5 (Композиция)** — нестандартные комбинации стандартных блоков +- **Уровень 6 (Расширение)** — создание новой функциональности + +### Сценарий 1: Стандартный A/B тест (Уровень 4 — Shell) + +**Бизнес-задача:** Продуктовая команда тестирует новый дизайн кнопки на конверсию в покупку. Нужно статистически +корректно сравнить контрольную и тестовую группы. + +**Характеристики задачи:** + +- Типовый A/B тест с одной метрикой +- Стандартные требования к статистической значимости +- Нужен быстрый и надежный результат +- Команда не имеет глубокой экспертизы в статистике + +**Решение — использование ABTest Shell:** + +```python +from hypex import ABTest +from hypex.dataset import Dataset, TargetRole, TreatmentRole + +# Подготовка данных +dataset = Dataset( + data=experiment_data, + roles={ + 'user_group': TreatmentRole(), # 'control' или 'test' + 'conversion': TargetRole() # 0 или 1 + } +) + +# Запуск A/B теста +ab_test = ABTest() +results = ab_test.execute(dataset) + +# Получение результатов +print("=== РЕЗУЛЬТАТЫ A/B ТЕСТА ===") +print(f"Основные выводы:\n{results.resume}") +print(f"Размеры групп:\n{results.sizes}") +``` + +**Что происходит под капотом:** + +1. ABTest автоматически применяет набор статистических тестов (t-test, KS-test) +2. ABAnalyzer анализирует результаты и формирует выводы +3. ABOutput форматирует результаты в удобном для бизнеса виде + +**Результат:** + +``` +Группа control: 1000 пользователей, конверсия 5.2% +Группа test: 1000 пользователей, конверсия 6.8% +Статистическая значимость: p-value = 0.032 (значимо) +Рекомендация: Внедрять изменение +``` + +**Почему этот уровень подходит:** + +- Задача полностью покрывается стандартной функциональностью +- Встроенные лучшие практики (коррекция, валидация) +- Минимальный код, максимальная надежность +- Готовые к презентации результаты + +### Сценарий 2: A/B тест с дополнительными требованиями (Уровень 5 — Композиция) + +**Бизнес-задача:** Та же команда хочет провести более углубленный анализ — добавить PSI (Population Stability Index) для +проверки стабильности распределения пользователей между группами и использовать менее консервативную коррекцию +множественного тестирования. + +**Характеристики задачи:** + +- Базовый A/B тест + дополнительные методы проверки качества +- Нестандартная комбинация стандартных блоков +- Требуется кастомизация параметров анализа +- Команда готова работать с более сложным API + +**Решение — композиция Executor'ов:** + +```python +from hypex.experiments.base import Experiment, OnRoleExperiment +from hypex.comparators import TTest, KSTest, PSI +from hypex.operators import GroupSizes, GroupDifference +from hypex.analyzers.ab import ABAnalyzer +from hypex.dataset import TargetRole + +# Создание кастомного эксперимента +custom_ab_experiment = Experiment([ + # Стандартная часть + OnRoleExperiment([ + GroupSizes(grouping_role=TreatmentRole()), + GroupDifference(grouping_role=TreatmentRole()), + TTest(grouping_role=TreatmentRole()), + KSTest(grouping_role=TreatmentRole()), + + # Дополнительный анализ стабильности + PSI(grouping_role=TreatmentRole()), # Стабильность распределения + + ], role=TargetRole()), + + # Анализ результатов с менее консервативной коррекцией + ABAnalyzer(multitest_method="fdr_bh") # False Discovery Rate вместо Bonferroni +]) + +# Выполнение +experiment_data = ExperimentData(dataset) +results = custom_ab_experiment.execute(experiment_data) + +# Извлечение результатов через Reporter +from hypex.reporters.ab import ABDictReporter + +reporter = ABDictReporter() +formatted_results = reporter.report(results) + +print("=== РАСШИРЕННЫЙ A/B АНАЛИЗ ===") +print(f"T-test: {formatted_results['ttest']}") +print(f"KS-test: {formatted_results['kstest']}") +print(f"PSI (стабильность групп): {formatted_results['psi']}") +print(f"FDR-скорректированные результаты: {formatted_results['multitest']}") +``` + +**Что изменилось:** + +- Перешли от Shell к прямой композиции Executor'ов +- Добавили PSI для проверки качества разбиения на группы +- Изменили метод коррекции множественного тестирования на менее консервативный +- Используем Reporter для извлечения результатов + +**Результат:** + +``` +T-test: p-value = 0.032, effect = 1.6% +KS-test: p-value = 0.045, distribution differs +PSI = 0.08 (группы сбалансированы, PSI < 0.1) +FDR-скорректированные p-values: [0.038, 0.048] +Рекомендация: Эффект значим, группы качественно сбалансированы +``` + +**Почему потребовался переход на уровень композиции:** + +- Нужен PSI анализ, который не включен в стандартный ABTest +- Требуется другой метод коррекции множественного тестирования +- Кастомизация набора выполняемых тестов + +### Сценарий 3: Кастомный анализ для специфической метрики (Уровень 6 — Расширение) + +**Бизнес-задача:** Продуктовая команда анализирует влияние изменений на конверсию в покупку, которая рассчитывается как +отношение (ratio): покупки / уникальные пользователи. Стандартные тесты не учитывают специфику ratio метрик. + +**Характеристики задачи:** + +- Специфический тип метрики (ratio: события/пользователи) +- Нужен специализированный статистический тест с delta method +- Стандартные Calculator'ы не покрывают потребность +- Команда имеет статистическую экспертизу + +**Решение — создание кастомного Calculator'а:** + +```python +from hypex.executors.calculator import Comparator +from hypex.dataset import Dataset +import numpy as np +from scipy import stats + + +class RatioTest(Comparator): + """ + Статистический тест для сравнения ratio метрик между группами. + Использует delta method для корректной оценки стандартной ошибки. + """ + + def __init__(self, alpha: float = 0.05, **kwargs): + super().__init__(**kwargs) + self.alpha = alpha + + def _inner_function(self, control_data: Dataset, test_data: Dataset) -> dict: + """ + Выполняет тест для ratio метрик. + + Ожидает колонки: + - events: количество событий (покупок) + - users: количество уникальных пользователей + """ + + # Извлечение данных + control_events = control_data['events'].sum() + control_users = control_data['users'].sum() + test_events = test_data['events'].sum() + test_users = test_data['users'].sum() + + # Расчет ratio метрик + control_ratio = control_events / control_users if control_users > 0 else 0 + test_ratio = test_events / test_users if test_users > 0 else 0 + + # Delta method для стандартной ошибки ratio + control_se = self._delta_method_se(control_events, control_users) + test_se = self._delta_method_se(test_events, test_users) + + # Pooled standard error для разности + pooled_se = np.sqrt(control_se ** 2 + test_se ** 2) + + # Z-test для разности ratio + if pooled_se > 0: + z_stat = (test_ratio - control_ratio) / pooled_se + p_value = 2 * (1 - stats.norm.cdf(abs(z_stat))) + else: + z_stat = 0 + p_value = 1.0 + + # Доверительный интервал для разности + diff = test_ratio - control_ratio + ci_margin = stats.norm.ppf(1 - self.alpha / 2) * pooled_se + ci_lower = diff - ci_margin + ci_upper = diff + ci_margin + + return { + "control_ratio": control_ratio, + "test_ratio": test_ratio, + "ratio_difference": diff, + "relative_lift": (diff / control_ratio * 100) if control_ratio > 0 else 0, + "statistic": z_stat, + "p-value": p_value, + "pass": p_value < self.alpha, + "confidence_interval": [ci_lower, ci_upper], + "interpretation": self._interpret_results(p_value, diff, control_ratio) + } + + def _delta_method_se(self, events: float, users: float) -> float: + """Вычисляет стандартную ошибку ratio через delta method""" + if users <= 0: + return 0 + + ratio = events / users + # Delta method: SE(X/Y) ≈ sqrt(Var(X)/Y² + X²*Var(Y)/Y⁴ - 2*X*Cov(X,Y)/Y³) + # Для Poisson процесса: Var(events) = events, Cov(events, users) ≈ 0 + variance = (events / users ** 2) + (ratio ** 2 * users / users ** 2) # Упрощенная формула + return np.sqrt(variance / users) # Нормализация на размер выборки + + def _interpret_results(self, p_value: float, diff: float, control_ratio: float) -> str: + """Генерирует текстовую интерпретацию результатов""" + if p_value >= self.alpha: + return "Нет статистически значимой разницы в конверсии" + + if diff > 0: + lift_pct = (diff / control_ratio * 100) if control_ratio > 0 else 0 + return f"Конверсия выше в тестовой группе на {lift_pct:.1f}% (улучшение)" + else: + drop_pct = abs(diff / control_ratio * 100) if control_ratio > 0 else 0 + return f"Конверсия ниже в тестовой группе на {drop_pct:.1f}% (ухудшение)" + + +# Создание эксперимента с кастомным тестом +ratio_experiment = Experiment([ + OnRoleExperiment([ + GroupSizes(grouping_role=TreatmentRole()), + + # Кастомный анализ ratio метрик + RatioTest( + grouping_role=TreatmentRole(), + alpha=0.05 + ), + + # Дополнительные стандартные тесты для validation + TTest(grouping_role=TreatmentRole()), # Для проверки consistency + + ], role=TargetRole()), +]) + +# Подготовка данных для ratio анализа +ratio_dataset = Dataset( + data=experiment_data, + roles={ + 'user_group': TreatmentRole(), + 'conversion_events': TargetRole(), # Количество покупок + 'unique_users': TargetRole(), # Количество уникальных пользователей + 'revenue_per_user': TargetRole() # Дополнительная метрика для t-test + } +) + +# Выполнение анализа +experiment_data = ExperimentData(ratio_dataset) +results = ratio_experiment.execute(experiment_data) +``` + +**Результат кастомного анализа:** + +``` +=== RATIO METRICS АНАЛИЗ === +Control группа: 1247 покупок / 4520 пользователей = 27.6% конверсия +Test группа: 1456 покупок / 4580 пользователей = 31.8% конверсия +Difference: +4.2 п.п. (relative lift: +15.2%) +Z-statistic: 3.24, p-value = 0.001 (высоко значимо) +95% CI для разности: [1.7%, 6.7%] +Интерпретация: Конверсия выше в тестовой группе на 15.2% (улучшение) + +=== VALIDATION === +T-test на revenue per user: p-value = 0.018 (согласуется с ratio тестом) +``` + +**Почему потребовался уровень расширения:** + +- Ratio метрики требуют специального подхода (delta method для SE) +- Стандартные тесты дают некорректные результаты для отношений +- Нужна доменная экспертиза для правильной статистической модели +- Требуется кастомная интерпретация результатов (relative lift) + +**Преимущества архитектурного подхода:** + +- Новый Calculator легко интегрируется в существующие Experiment'ы +- Следует всем конвенциям библиотеки (единый интерфейс, ExperimentData) +- Можно комбинировать с стандартными компонентами для validation +- Переиспользуется в других экспериментах с ratio метриками + +### Сценарий 4: Сложный многоэтапный matching анализ (Комбинация уровней) + +**Бизнес-задача:** Аналитическая команда исследует эффект маркетинговой кампании на retention пользователей. Данные +observational (без рандомизации), поэтому нужен sophisticated matching анализ с проверкой качества и sensitivity +анализом. + +**Характеристики задачи:** + +- Многоэтапный процесс: подготовка → matching → валидация → анализ +- Комбинация готовых решений и кастомных компонентов +- Требования к quality assurance на каждом этапе +- Нужна детальная диагностика и отчетность + +**Решение — комбинация Shell + Композиция + Расширение:** + +```python +# Этап 1: Быстрая оценка с помощью Shell (базовый matching) +initial_matching = Matching( + distance="mahalanobis", + bias_estimation=True, + quality_tests=["ttest", "kstest"] +) + +initial_results = initial_matching.execute(dataset) +print(f"Базовое качество matching: {initial_results.quality_results}") + +# Этап 2: Если качество неудовлетворительное - детальная настройка +if initial_results.quality_results['overall_quality'] < 0.8: + + # Кастомный preprocessor для улучшения matching + class AdvancedPreprocessor(Transformer): + """Продвинутая предобработка для улучшения quality matching'а""" + + def _inner_function(self, data: Dataset) -> Dataset: + # Логарифмирование скошенных переменных + log_features = ['income', 'session_duration'] + for feature in log_features: + data[f'{feature}_log'] = np.log1p(data[feature]) + + # Создание interaction features + data['age_income_interaction'] = data['age'] * data['income'] + + # Binning категориальных переменных с малыми группами + rare_categories = data['device_type'].value_counts() + data['device_type_grouped'] = data['device_type'].map( + lambda x: x if rare_categories[x] > 50 else 'other' + ) + + return data + + + # Кастомный качественный анализ + class SensitivityAnalyzer(Executor): + """Sensitivity анализ для оценки робастности результатов""" + + def execute(self, data: ExperimentData) -> ExperimentData: + matched_data = data.additional_fields['matched_pairs'] + + # Анализ с разными distance thresholds + thresholds = [0.1, 0.2, 0.3, 0.4, 0.5] + sensitivity_results = [] + + for threshold in thresholds: + # Фильтруем пары по качеству matching + high_quality_pairs = matched_data[ + matched_data['match_distance'] <= threshold + ] + + # Вычисляем эффект на filtered данных + effect = self._calculate_treatment_effect(high_quality_pairs) + + sensitivity_results.append({ + 'threshold': threshold, + 'n_pairs': len(high_quality_pairs), + 'treatment_effect': effect, + 'coverage': len(high_quality_pairs) / len(matched_data) + }) + + # Сохраняем результаты sensitivity анализа + sensitivity_dataset = Dataset(data=pd.DataFrame(sensitivity_results)) + return data.set_value( + space=ExperimentDataEnum.analysis_tables, + executor_id=self.id, + value=sensitivity_dataset + ) + + + # Композиция продвинутого matching pipeline + advanced_matching = Experiment([ + # Этап 1: Продвинутая предобработка + AdvancedPreprocessor(), + OutliersFilter(method="isolation_forest"), + NaFiller(method="knn"), + + # Этап 2: Matching с несколькими алгоритмами + MahalanobisDistance(grouping_role=TreatmentRole()), + FaissNearestNeighbors(n_neighbors=3), # Больше кандидатов + + # Этап 3: Выбор лучших пар + MatchingQualityFilter(min_quality_score=0.8), + + # Этап 4: Анализ качества + Bias(grouping_role=TreatmentRole()), + SMD(grouping_role=TreatmentRole()), # Standardized Mean Difference + + # Этап 5: Основной анализ + OnRoleExperiment([ + TTest(grouping_role=TreatmentRole()), + KSTest(grouping_role=TreatmentRole()), + ], role=TargetRole()), + + # Этап 6: Sensitivity анализ + SensitivityAnalyzer(), + + # Этап 7: Итоговый анализ + MatchingAnalyzer() + ]) + + # Выполнение продвинутого анализа + final_results = advanced_matching.execute(ExperimentData(dataset)) + + +# Этап 3: Детальная отчетность с кастомным Reporter +class ComprehensiveMatchingReporter(DictReporter): + """Подробный отчет по всем этапам matching анализа""" + + def report(self, data: ExperimentData) -> dict: + base_results = super().report(data) + + # Добавляем sensitivity анализ + sensitivity_data = data.analysis_tables.get('SensitivityAnalyzer╤╤') + if sensitivity_data is not None: + base_results['sensitivity_analysis'] = { + 'stability_assessment': self._assess_stability(sensitivity_data), + 'robust_effect_estimate': self._robust_estimate(sensitivity_data), + 'recommended_threshold': self._recommend_threshold(sensitivity_data) + } + + # Добавляем качественную оценку + base_results['quality_assessment'] = self._comprehensive_quality_check(data) + + return base_results + + +comprehensive_reporter = ComprehensiveMatchingReporter() +final_report = comprehensive_reporter.report(final_results) +``` + +**Результат многоэтапного анализа:** + +``` +=== COMPREHENSIVE MATCHING ANALYSIS === + +Preprocessing Quality: + - Outliers removed: 234 observations (2.1%) + - Missing values imputed: 12 features + - Feature engineering: 8 new features created + +Matching Results: + - Matched pairs: 3,847 из 4,120 (93.4% coverage) + - Average match distance: 0.18 (good quality) + - Bias reduction: 85% (excellent) + +Treatment Effect: + - Retention increase: +8.4% (95% CI: [5.2%, 11.6%]) + - P-value: < 0.001 (highly significant) + - Effect size: Cohen's d = 0.32 (medium effect) + +Sensitivity Analysis: + - Effect stable across thresholds 0.1-0.4 + - Robust estimate: +8.1% ± 1.2% + - Recommended threshold: 0.25 (best coverage/quality trade-off) + +Quality Assessment: EXCELLENT + - All balance checks passed + - Covariate overlap sufficient + - Results robust to specification choices +``` + +**Архитектурные преимущества комбинированного подхода:** + +1. **Эффективность разработки:** Начали с Shell для быстрой оценки +2. **Модульность:** Каждый этап можно тестировать и отлаживать независимо +3. **Переиспользование:** Кастомные компоненты можно использовать в других проектах +4. **Прозрачность:** Полная видимость всех этапов анализа +5. **Научная строгость:** Sensitivity анализ и множественные проверки качества + +### Эволюция решения: От простого к сложному + +Рассмотрим, как одна и та же задача решается на разных уровнях сложности: + +**Задача:** Измерить влияние нового алгоритма рекомендаций на выручку. + +#### Этап 1: Первая итерация (Shell) + +```python +# Быстрый A/B тест для первичной оценки +ab_test = ABTest() +initial_results = ab_test.execute(dataset) +# Результат: "Есть положительный эффект +12%, p=0.02" +``` + +#### Этап 2: Углубленный анализ (Композиция) + +```python +# Stakeholder'ы просят добавить анализ подгрупп +segmented_experiment = Experiment([ + # Общий анализ + OnRoleExperiment([TTest(), KSTest()], role=TargetRole()), + + # Анализ по сегментам пользователей + GroupExperiment([ + OnRoleExperiment([TTest()], role=TargetRole()) + ], grouping_role=UserSegmentRole()), + + ABAnalyzer(multitest_method="fdr_bh") # Коррекция для multiple segments +]) +``` + +#### Этап 3: Продвинутая статистика (Расширение) + +```python +# Экономисты просят добавить causal inference методы +class DoublyRobustEstimator(Calculator): + """Doubly robust estimation для более точной causal inference""" + # ... реализация + + +advanced_experiment = Experiment([ + # Стандартные тесты + OnRoleExperiment([TTest(), KSTest()], role=TargetRole()), + + # Продвинутые causal methods + DoublyRobustEstimator(), + InstrumentalVariablesAnalysis(), + + # Sensitivity анализ + ConfoundingSensitivityTest(), +]) +``` + +**Ключевой инсайт:** Архитектура HypEx позволяет начать с простого решения и органично наращивать сложность без +переписывания кода. Каждый уровень строится на предыдущем, добавляя необходимую функциональность. + +### Паттерны выбора подхода + +#### Матрица принятия решений + +| Критерий | Shell (Уровень 4) | Композиция (Уровень 5) | Расширение (Уровень 6) | +|-------------------------------|------------------------------|--------------------------|------------------------| +| **Стандартность задачи** | Типовая (A/B, A/A, Matching) | Нестандартная комбинация | Уникальная методология | +| **Экспертиза команды** | Базовая | Средняя | Высокая | +| **Временные рамки** | Срочно (часы) | Умеренно (дни) | Не критично (недели) | +| **Требования к кастомизации** | Минимальные | Средние | Максимальные | +| **Частота использования** | Разовая или регулярная | Регулярная | Проектная | + +#### Типичные сигналы для перехода на следующий уровень + +**Shell → Композиция:** + +- "Нужно добавить еще один тест" +- "Хотим изменить порядок выполнения" +- "Требуется нестандартная комбинация методов" +- "Shell делает не совсем то, что нужно" + +**Композиция → Расширение:** + +- "Нужного Executor'а нет в библиотеке" +- "Требуется интеграция с внешней библиотекой" +- "Нужна доменно-специфическая логика" +- "Стандартные методы не подходят для наших данных" + +#### Антипаттерны и их решения + +**Антипаттерн 1: Преждевременная оптимизация** + +```python +# Плохо: сразу создавать сложный custom Executor +class ComplexCustomAnalyzer(Executor): + + +# 200 строк сложной логики для простой задачи + +# Хорошо: начать с Shell и усложнять по необходимости +ab_test = ABTest() # Сначала проверить, что базовое решение не подходит +``` + +**Антипаттерн 2: Игнорирование стандартных решений** + +```python +# Плохо: переизобретать велосипед +custom_experiment = Experiment([ + ManualGroupSplit(), # Вместо стандартного AASplitter + ManualTTest(), # Вместо стандартного TTest + ManualReporting() # Вместо стандартных Reporter'ов +]) + +# Хорошо: использовать стандартные блоки где возможно +experiment = Experiment([ + AASplitter(), # Стандартный, надежный, протестированный + OnRoleExperiment([TTest()], role=TargetRole()), + CustomSpecificAnalyzer() # Только то, что действительно уникально +]) +``` + +**Антипаттерн 3: Монолитные кастомные решения** + +```python +# Плохо: один большой Executor делает все +class MonolithicAnalyzer(Executor): + def execute(self, data): +# Предобработка + анализ + отчетность в одном классе +# Трудно тестировать, переиспользовать, поддерживать + + +# Хорошо: разбить на композируемые части +experiment = Experiment([ + CustomPreprocessor(), # Одна ответственность + StandardAnalysis(), # Переиспользуемый компонент + CustomReporter() # Отдельный форматтер +]) +``` + +### Выводы и рекомендации + +**Принцип прогрессивного усложнения:** +Всегда начинайте с самого простого решения, которое решает задачу. HypEx позволяет органично развивать решение по мере +роста требований. + +**Правило 90-9-1:** + +- 90% задач решаются Shell'ами (уровень 4) +- 9% требуют композиции стандартных блоков (уровень 5) +- 1% нуждается в создании новой функциональности (уровень 6) + +**Критерии качественного решения:** + +1. **Простота:** Используйте минимально необходимый уровень сложности +2. **Переиспользование:** Предпочитайте стандартные компоненты кастомным +3. **Модульность:** Разбивайте сложную логику на композируемые части +4. **Тестируемость:** Каждый компонент должен быть независимо тестируемым +5. **Документированность:** Кастомные компоненты требуют подробного описания + +Архитектура HypEx спроектирована так, чтобы естественным образом направлять разработчиков к качественным решениям, +предоставляя правильные абстракции для каждого уровня сложности. + From 54eb8a1ea0465e26664bb90bba56aa95d17ec76d Mon Sep 17 00:00:00 2001 From: dmbulychev Date: Sun, 21 Sep 2025 12:33:36 +0300 Subject: [PATCH 14/17] Rework part 11 --- schemes/anatomy.md | 2762 ++++++++++++++------------------------------ 1 file changed, 891 insertions(+), 1871 deletions(-) diff --git a/schemes/anatomy.md b/schemes/anatomy.md index a1be5273..1ffb5ca0 100644 --- a/schemes/anatomy.md +++ b/schemes/anatomy.md @@ -214,106 +214,53 @@ ds_merged = ds1.merge(ds2, on='id') ExperimentData — это расширенный контейнер, который хранит не только исходные данные, но и все промежуточные результаты, метаданные и состояние эксперимента. -#### Структура ExperimentData +#### Архитектура ExperimentData -```python -class ExperimentData: - # Основные данные - _data: Dataset # Исходный датасет - - # Дополнительные поля (новые колонки, созданные Executor'ами) - additional_fields: Dataset - - # Переменные (скалярные значения, метрики) - variables: dict[str, dict[str, Any]] - - # Группы (разбиение данных на подгруппы) - groups: dict[str, dict[str, Dataset]] - - # Таблицы анализа (результаты тестов и анализов) - analysis_tables: dict[str, Dataset] -``` - -#### Пространства имен (ExperimentDataEnum) - -ExperimentData организует данные в четыре пространства имен: - -1. **additional_fields** — новые признаки и колонки - - Результаты encoding'а - - Вычисленные features - - Matched индексы - -2. **variables** — скалярные значения и словари - - Параметры моделей - - Вычисленные константы - - Метрики качества - -3. **groups** — сгруппированные данные - - Разбиение на control/test - - Подгруппы для анализа - - Результаты стратификации - -4. **analysis_tables** — результаты анализов - - Результаты статистических тестов - - Таблицы сравнений - - Агрегированные метрики - -#### Взаимодействие с Executors - -Каждый Executor работает с ExperimentData по следующему паттерну: +**Пространства имен:** ```python -def execute(self, data: ExperimentData) -> ExperimentData: - # 1. Извлечение необходимых данных - input_data = data.ds # или data.additional_fields, data.groups и т.д. - - # 2. Выполнение операции - result = self._inner_function(input_data) - - # 3. Сохранение результата в нужное пространство имен - return data.set_value( - space=ExperimentDataEnum.analysis_tables, - executor_id=self.id, - value=result - ) +class ExperimentDataEnum: + analysis_tables = "analysis_tables" # Результаты Calculator'ов + variables = "variables" # Переменные и настройки + additional_fields = "additional_fields" # Дополнительные колонки данных ``` -#### Система идентификаторов - -Каждый Executor имеет уникальный ID, построенный по схеме: +**Компоненты:** -``` -ClassName╤ParamsHash╤Key -``` +- **ds: Dataset** — основные данные эксперимента +- **analysis_tables: dict** — результаты анализов по ID Executor'ов +- **variables: dict** — переменные и метаданные +- **additional_fields: dict** — дополнительные поля данных -Где: +#### Основные операции ExperimentData -- `ClassName` — имя класса Executor'а -- `ParamsHash` — хеш параметров -- `Key` — дополнительный ключ (например, имя колонки) +```python +# Создание +experiment_data = ExperimentData(dataset) -Это позволяет: +# Сохранение результата Executor'а +experiment_data.set_value( + space=ExperimentDataEnum.analysis_tables, + executor_id="TTest╤reliability 0.05╤", + value=test_results +) -- Избегать повторных вычислений -- Находить результаты конкретных Executor'ов -- Строить зависимости между компонентами -- Восстанавливать состояние Executor +# Получение результата по ID +result = experiment_data.analysis_tables["TTest╤reliability 0.05╤"] -### Поток преобразования данных +# Поиск ID'шников по типу Executor'а +ids = experiment_data.get_ids(TTest) -``` -Dataset → ExperimentData → [Executor1] → ExperimentData' → [Executor2] → ExperimentData'' - ↓ ↓ - Модификация Модификация - additional_fields analysis_tables +# Добавление дополнительных полей +experiment_data.add_fields({'predicted_score': prediction_column}) ``` -Каждый Executor может: +### Принципы работы с данными -- Читать из любого пространства имен -- Писать в одно или несколько пространств -- Использовать результаты предыдущих Executor'ов -- Не влиять на исходные данные (immutability) +1. **Immutability**: ExperimentData копируется при необходимости изменения основного Dataset +2. **Namespace separation**: Разные типы результатов хранятся в разных пространствах имен +3. **ID-based access**: Результаты индексируются по уникальным ID Executor'ов +4. **Rich metadata**: Поддержка метаданных и переменных для каждого эксперимента ## 4. Ядро системы: Executor Framework @@ -378,171 +325,65 @@ class Calculator(Executor, ABC): pass ``` -**Назначение:** Разделение логики вычисления от логики работы с ExperimentData. Это позволяет: - -- Использовать вычисления отдельно от эксперимента -- Тестировать логику изолированно -- Переиспользовать код в разных контекстах - -**Основные подклассы Calculator:** - -``` -Calculator -├── Comparator — сравнение групп и статистические тесты -│ ├── GroupDifference, GroupSizes, PSI -│ ├── StatHypothesisTesting (TTest, KSTest, UTest, Chi2Test) -│ └── PowerTesting (MDEBySize) -├── Transformer — преобразование данных -│ ├── Filters (ConstFilter, CorrFilter, CVFilter, NanFilter, OutliersFilter) -│ ├── NaFiller — заполнение пропусков -│ ├── CategoryAggregator — агрегация категорий -│ └── Shuffle — перемешивание данных -├── Encoder — кодирование категориальных переменных -│ └── DummyEncoder — one-hot encoding -├── GroupOperator — операции над группами -│ ├── SMD — Standardized Mean Difference -│ ├── Bias — оценка смещения -│ └── MatchingMetrics — метрики matching'а -├── MLExecutor — машинное обучение -│ └── FaissNearestNeighbors — поиск ближайших соседей -└── Splitter — разделение на группы - ├── AASplitter — базовое разделение для A/A теста - └── AASplitterWithStratification — со стратификацией -``` - -#### 2. IfExecutor — условное выполнение +**Назначение:** Разделение логики вычисления от логики работы с ExperimentData. -IfExecutor реализует паттерн условного выполнения: +#### 2. IfExecutor — условная ветвь ```python -class IfExecutor(Executor, ABC): - def __init__(self, - if_executor: Executor | None = None, - else_executor: Executor | None = None): +class IfExecutor(Executor): + def __init__(self, if_executor: Executor, else_executor: Executor = None): self.if_executor = if_executor self.else_executor = else_executor - @abstractmethod - def check_rule(self, data) -> bool: - """Проверка условия""" + def check_rule(self, data: ExperimentData) -> bool: + """Логика принятия решения""" pass def execute(self, data: ExperimentData) -> ExperimentData: if self.check_rule(data): - return self.if_executor.execute(data) if self.if_executor else data - else: - return self.else_executor.execute(data) if self.else_executor else data + return self.if_executor.execute(data) + elif self.else_executor: + return self.else_executor.execute(data) + return data ``` -**Назначение:** Ветвление логики выполнения на основе условий. +**Назначение:** Условное выполнение на основе состояния данных. -**Пример использования — IfAAExecutor:** +#### 3. MLExecutor — машинное обучение ветвь ```python -class IfAAExecutor(IfExecutor): - def check_rule(self, data) -> bool: - # Проверяет, прошел ли A/A тест - score_table = data.analysis_tables[...] - feature_pass = sum([...]) # Подсчет прошедших тестов - return feature_pass >= 1 -``` - -#### 3. Прямые наследники — Analyzers +class MLExecutor(Executor, ABC): + def fit(self, X: Dataset, Y: Dataset) -> 'MLExecutor': + """Обучение модели""" + pass -Analyzers наследуются напрямую от Executor и выполняют сложный анализ результатов: + def predict(self, X: Dataset) -> Dataset: + """Предсказания""" + pass -```python -# Прямые наследники Executor -├── OneAAStatAnalyzer — анализ -статистики -одного -A / A -теста -├── AAScoreAnalyzer — оценка -качества -A / A -тестов -и -выбор -лучшего -├── ABAnalyzer — анализ -A / B -теста -с -коррекцией -множественного -тестирования -└── MatchingAnalyzer — анализ -качества -matching -'а + def score(self, X: Dataset, Y: Dataset) -> float: + """Метрика качества""" + pass ``` -**Особенности Analyzers:** - -- Работают с результатами других Executor'ов -- Агрегируют и анализируют множественные результаты -- Принимают сложные решения на основе статистики - -### Паттерн "Цепочка ответственности" +**Назначение:** Стандартный интерфейс для ML операций. -Executor'ы образуют цепочку, где каждый: +### Система ID -1. Получает ExperimentData от предыдущего -2. Выполняет свою операцию -3. Передает обогащенный ExperimentData следующему +Каждый Executor имеет уникальный ID, состоящий из: -```python -# Пример цепочки для A/B теста -chain = [ - GroupSizes(), # Подсчет размеров групп - GroupDifference(), # Вычисление разницы между группами - TTest(), # T-тест - KSTest(), # KS-тест - ABAnalyzer() # Анализ и коррекция множественного тестирования -] - -# Выполнение цепочки -data = ExperimentData(dataset) -for executor in chain: - data = executor.execute(data) ``` - -### Система идентификации и хеширования - -Каждый Executor имеет уникальный ID вида: `ClassName╤ParamsHash╤Key` - -**Генерация ParamsHash:** - -```python -def _generate_params_hash(self): - # Для AASplitter - hash_parts = [] - if self.control_size != 0.5: - hash_parts.append(f"cs {self.control_size}") - if self.random_state is not None: - hash_parts.append(f"rs {self.random_state}") - self._params_hash = "|".join(hash_parts) +ClassName╤ParamsHash╤Key ``` -**Восстановление из ID:** +**Примеры:** ```python -@classmethod -def build_from_id(cls, executor_id: str): - """Восстанавливает Executor из его ID""" - splitted_id = executor_id.split(ID_SPLIT_SYMBOL) - result = cls() - result.init_from_hash(splitted_id[1]) # ParamsHash - return result +TTest() → "TTest╤╤" +TTest(reliability=0.01) → "TTest╤rel 0.01╤" +TTest(key="revenue") → "TTest╤╤revenue" ``` -Это позволяет: - -- **Кеширование** — не выполнять повторно одинаковые операции -- **Трассировка** — понимать, какой Executor создал какой результат -- **Воспроизводимость** — восстанавливать состояние из ID - ### Жизненный цикл Executor 1. **Создание:** @@ -609,493 +450,7 @@ class MyCustomTest(StatHypothesisTesting): - **Расширяемость** — легко добавлять новую функциональность - **Тестируемость** — каждый компонент можно тестировать отдельно -## 5. Слой вычислений: Comparators, Transformers, Operators - -Слой вычислений содержит конкретные реализации Calculator'ов, каждая из которых специализируется на определенном типе -операций. Все они следуют паттерну разделения вычислительной логики от работы с ExperimentData. - -### Comparators: Сравнение и тестирование - -Comparators отвечают за сравнение групп и проведение статистических тестов. Они имеют сложную иерархию и богатую -функциональность. - -#### Архитектура Comparator - -```python -class Comparator(Calculator, ABC): - def __init__(self, - compare_by: Literal["groups", "columns", "columns_in_groups", "cross"], - grouping_role: ABCRole = GroupingRole(), - target_roles: ABCRole | list[ABCRole] = TargetRole(), - baseline_role: ABCRole = PreTargetRole()): - self.compare_by = compare_by - self.grouping_role = grouping_role - self.target_roles = target_roles - self.baseline_role = baseline_role -``` - -**Режимы сравнения (compare_by):** - -#### Режим "groups" — сравнение между группами - -Самый распространенный режим для A/B тестов. Сравнивает одну и ту же метрику между разными группами (control vs test). - -```python -# Данные: -# | user_id | group | revenue | retention | -# |---------|---------|---------|-----------| -# | 1 | control | 100 | 1 | -# | 2 | test | 150 | 1 | -# | 3 | control | 80 | 0 | -# | 4 | test | 120 | 1 | - -comparator = TTest( - compare_by="groups", - grouping_role=TreatmentRole(), # группировка по "group" - target_roles=TargetRole() # анализ "revenue" и "retention" -) - -# Результат: -# Для revenue: сравнение control_revenue vs test_revenue -# Для retention: сравнение control_retention vs test_retention -``` - -#### Режим "columns" — сравнение колонок между собой - -Сравнивает разные метрики внутри всего датасета. Полезно для анализа корреляций или изменений между pre/post периодами. - -```python -# Данные: -# | user_id | pre_revenue | post_revenue | pre_clicks | post_clicks | -# |---------|-------------|--------------|------------|-------------| -# | 1 | 100 | 150 | 10 | 15 | -# | 2 | 80 | 90 | 8 | 12 | - -comparator = GroupDifference( - compare_by="columns", - baseline_role=PreTargetRole(), # baseline: "pre_revenue", "pre_clicks" - target_roles=TargetRole() # сравнить с: "post_revenue", "post_clicks" -) - -# Результат: -# Сравнение pre_revenue vs post_revenue (для всех пользователей) -# Сравнение pre_clicks vs post_clicks (для всех пользователей) -``` - -#### Режим "columns_in_groups" — сравнение колонок внутри каждой группы - -Комбинация первых двух режимов. Сравнивает разные метрики, но отдельно для каждой группы. - -```python -# Данные: -# | user_id | group | pre_revenue | post_revenue | -# |---------|---------|-------------|--------------| -# | 1 | control | 100 | 110 | -# | 2 | test | 100 | 150 | - -comparator = GroupDifference( - compare_by="columns_in_groups", - grouping_role=TreatmentRole(), # группировка по "group" - baseline_role=PreTargetRole(), # baseline: "pre_revenue" - target_roles=TargetRole() # сравнить с: "post_revenue" -) - -# Результат: -# control: сравнение pre_revenue vs post_revenue -# test: сравнение pre_revenue vs post_revenue -``` - -#### Режим "cross" — перекрестное сравнение - -Самый сложный режим. Сравнивает изменения между колонками в разных группах. Используется для difference-in-differences -анализа. - -```python -comparator = GroupDifference( - compare_by="cross", - grouping_role=TreatmentRole(), - baseline_role=PreTargetRole(), - target_roles=TargetRole() -) - -# Сравнивает: -# (test_post - test_pre) vs (control_post - control_pre) -``` - -**Применение режимов:** - -- **"groups"** — классические A/B тесты, сравнение метрик между группами -- **"columns"** — анализ изменений во времени, pre/post анализ -- **"columns_in_groups"** — гетерогенные эффекты, анализ по сегментам -- **"cross"** — каузальная инференция, DiD анализ - -#### Базовые метрики (GroupDifference, GroupSizes) - -**GroupDifference** вычисляет разницу между группами: - -```python -class GroupDifference(Comparator): - def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: - control_mean = data.mean() - test_mean = test_data.mean() - return { - "control mean": control_mean, - "test mean": test_mean, - "difference": test_mean - control_mean, - "difference %": (test_mean / control_mean - 1) * 100 - } -``` - -**GroupSizes** подсчитывает размеры групп: - -```python -class GroupSizes(Comparator): - def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: - size_a = len(data) - size_b = len(test_data) - return { - "control size": size_a, - "test size": size_b, - "control size %": (size_a / (size_a + size_b)) * 100, - "test size %": (size_b / (size_a + size_b)) * 100 - } -``` - -#### Статистические тесты (StatHypothesisTesting) - -Абстрактный класс StatHypothesisTesting добавляет концепцию статистической значимости: - -```python -class StatHypothesisTesting(Comparator, ABC): - def __init__(self, reliability: float = 0.05, **kwargs): - self.reliability = reliability # Уровень значимости - super().__init__(**kwargs) -``` - -**Конкретные реализации:** - -1. **TTest** — t-тест Стьюдента для нормальных распределений -2. **KSTest** — тест Колмогорова-Смирнова для любых распределений -3. **UTest** — U-тест Манна-Уитни для ненормальных распределений -4. **Chi2Test** — хи-квадрат тест для категориальных переменных - -Каждый тест возвращает стандартизированный результат: - -```python -{ - "p-value": 0.042, - "statistic": 2.15, - "pass": True # p-value < reliability -} -``` - -#### Специализированные метрики - -**PSI (Population Stability Index)** — измеряет стабильность распределения: - -```python -class PSI(Comparator): - def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: - # Разбиение на бакеты и вычисление PSI - psi = sum((y - x) * np.log(y / x) for x, y in zip(data_psi, test_data_psi)) - return {"PSI": psi} -``` - -**MahalanobisDistance** — вычисляет расстояние Махаланобиса: - -```python -class MahalanobisDistance(Calculator): - def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: - # Вычисление ковариационной матрицы - cov = (data.cov() + test_data.cov()) / 2 - # Преобразование Холецкого и вычисление расстояния - cholesky = CholeskyExtension().calc(cov) - mahalanobis_transform = InverseExtension().calc(cholesky) - y_control = data.dot(mahalanobis_transform.transpose()) - y_test = test_data.dot(mahalanobis_transform.transpose()) - return {"control": y_control, "test": y_test} -``` - -### Transformers: Преобразование данных - -Transformers изменяют сам Dataset (в отличие от других Calculator'ов, которые только вычисляют результаты). Они работают -с копией данных для обеспечения immutability. - -#### Базовый класс Transformer - -```python -class Transformer(Calculator, ABC): - @property - def _is_transformer(self) -> bool: - return True # Маркер для Experiment - - def execute(self, data: ExperimentData) -> ExperimentData: - # Создает копию и модифицирует данные - return data.copy(data=self.calc(data=data.ds)) -``` - -#### Фильтры данных - -Фильтры изменяют роли колонок или удаляют строки/колонки: - -**ConstFilter** — фильтрует константные колонки: - -```python -class ConstFilter(Transformer): - def __init__(self, threshold: float = 0.95): - self.threshold = threshold - - def _inner_function(data: Dataset, target_cols, threshold) -> Dataset: - for column in target_cols: - value_counts = data[column].value_counts(normalize=True) - if value_counts.iloc[0] > threshold: - data.roles[column] = InfoRole() # Понижение роли - return data -``` - -**CorrFilter** — удаляет коррелированные признаки: - -```python -class CorrFilter(Transformer): - def _inner_function(data: Dataset, threshold: float = 0.8) -> Dataset: - corr_matrix = data.corr() - # Находим пары с высокой корреляцией - for col1, col2 in high_corr_pairs: - if abs(corr_matrix[col1][col2]) > threshold: - data.roles[col2] = InfoRole() # Понижаем роль второго - return data -``` - -**OutliersFilter** — удаляет выбросы: - -```python -class OutliersFilter(Transformer): - def __init__(self, - lower_percentile: float = 0.05, - upper_percentile: float = 0.95): - self.lower_percentile = lower_percentile - self.upper_percentile = upper_percentile - - def _inner_function(data: Dataset, target_cols, lower, upper) -> Dataset: - for col in target_cols: - q_low = data[col].quantile(lower) - q_high = data[col].quantile(upper) - data = data[(data[col] >= q_low) & (data[col] <= q_high)] - return data -``` - -#### Обработка пропусков и категорий - -**NaFiller** — заполнение пропущенных значений: - -```python -class NaFiller(Transformer): - def __init__(self, method: str = "ffill"): - self.method = method # 'ffill', 'bfill', 'mean', 'median', etc. - - def _inner_function(data: Dataset, target_cols, method) -> Dataset: - if method in ['ffill', 'bfill']: - return data.fillna(method=method) - elif method == 'mean': - for col in target_cols: - data[col].fillna(data[col].mean(), inplace=True) - return data -``` - -**CategoryAggregator** — агрегация редких категорий: - -```python -class CategoryAggregator(Transformer): - def __init__(self, min_frequency: float = 0.01): - self.min_frequency = min_frequency - - def _inner_function(data: Dataset, target_cols, min_freq) -> Dataset: - for col in target_cols: - value_counts = data[col].value_counts(normalize=True) - rare_categories = value_counts[value_counts < min_freq].index - data[col] = data[col].replace(rare_categories, 'Other') - return data -``` - -### Encoders: Кодирование признаков - -Encoders преобразуют категориальные признаки в числовые. Результат сохраняется в additional_fields. - -```python -class Encoder(Calculator): - def execute(self, data: ExperimentData) -> ExperimentData: - target_cols = data.ds.search_columns(roles=FeatureRole(), - search_types=[str]) - encoded = self.calc(data=data.ds, target_cols=target_cols) - # Сохраняем в additional_fields - return data.set_value( - space=ExperimentDataEnum.additional_fields, - executor_id=self._ids_to_names(encoded.columns), - value=encoded - ) -``` - -**DummyEncoder** — one-hot encoding: - -```python -class DummyEncoder(Encoder): - def _inner_function(data: Dataset, target_cols) -> Dataset: - # Использует pandas.get_dummies - dummies_df = pd.get_dummies(data[target_cols], drop_first=True) - # Устанавливает роли для новых колонок - roles = {col: data.roles[col.split('_')[0]] for col in dummies_df.columns} - return Dataset(data=dummies_df, roles=roles) -``` - -### GroupOperators: Операции над группами - -GroupOperators выполняют специализированные операции над группами данных, часто используемые в matching и causal -inference. - -**SMD (Standardized Mean Difference)** — стандартизированная разница средних: - -```python -class SMD(GroupOperator): - def _inner_function(data: Dataset, test_data: Dataset) -> float: - mean_diff = data.mean() - test_data.mean() - pooled_std = np.sqrt((data.var() + test_data.var()) / 2) - return mean_diff / pooled_std -``` - -**Bias** — оценка смещения для matching: - -```python -class Bias(GroupOperator): - def _inner_function(data: Dataset, matched_data: Dataset) -> dict: - # Оценка качества matching через смещение - bias = (matched_data.mean() - data.mean()) / data.std() - return { - "bias": bias, - "bias_reduced": abs(bias) < 0.1 # Порог 10% - } -``` - -**MatchingMetrics** — метрики качества matching: - -```python -class MatchingMetrics(GroupOperator): - def __init__(self, metric: str = "ate"): - self.metric = metric # 'ate', 'att', 'atc' - - def _inner_function(data: Dataset, matched_indices: Dataset) -> dict: - if self.metric == "ate": # Average Treatment Effect - effect = matched_treatment.mean() - matched_control.mean() - elif self.metric == "att": # Average Treatment on Treated - effect = treated.mean() - matched_control_for_treated.mean() - # ... другие метрики - return {"effect": effect, "se": standard_error} -``` - -### Splitters: Разделение данных - -Splitters разделяют данные на группы для экспериментов. - -#### AASplitter — базовое разделение - -```python -class AASplitter(Calculator): - def __init__(self, - control_size: float = 0.5, - random_state: int = None, - sample_size: float = None): # Доля от общих данных - self.control_size = control_size - self.random_state = random_state - self.sample_size = sample_size - - def _inner_function(data: Dataset, control_size, random_state, sample_size) -> list[str]: - # Сэмплирование если нужно - if sample_size: - data = data.sample(frac=sample_size, random_state=random_state) - - # Разделение на control/test - n_control = int(len(data) * control_size) - indices = data.sample(frac=1, random_state=random_state).index - - split = pd.Series("test", index=data.index) - split[indices[:n_control]] = "control" - - return split.tolist() -``` - -#### AASplitterWithStratification — стратифицированное разделение - -```python -class AASplitterWithStratification(AASplitter): - def _inner_function(data: Dataset, control_size, random_state, grouping_fields) -> Dataset: - if not grouping_fields: - return super()._inner_function(data, control_size, random_state) - - # Разделение внутри каждой страты - result = [] - for group, group_data in data.groupby(grouping_fields): - split = super()._inner_function(group_data, control_size, random_state) - result.extend(split) - - return Dataset.from_dict({"split": result}, roles={"split": TreatmentRole()}) -``` - -### Принципы проектирования Calculator'ов - -1. **Разделение вычисления и контекста:** - - `_inner_function` — чистая логика вычисления - - `execute` — работа с ExperimentData - - `calc` — статический интерфейс для использования вне экспериментов - -2. **Унифицированный результат:** - - Всегда возвращают Dataset или dict - - Результаты имеют стандартную структуру - - Роли определяют семантику результатов - -3. **Конфигурируемость:** - - Параметры передаются через конструктор - - Поддержка `set_params` для динамического изменения - - Параметры влияют на ID для кеширования - -4. **Поиск подходящих данных:** - - Используют роли для поиска нужных колонок - - Могут фильтровать по типам данных - - Работают с temporary roles при необходимости - -### Взаимодействие компонентов - -```python -# Пример pipeline для matching -pipeline = [ - # 1. Подготовка данных - OutliersFilter(lower_percentile=0.05, upper_percentile=0.95), - NaFiller(method="ffill"), - DummyEncoder(), - - # 2. Вычисление расстояний - MahalanobisDistance(grouping_role=TreatmentRole()), - - # 3. Поиск пар - FaissNearestNeighbors(n_neighbors=1), - - # 4. Оценка качества - Bias(grouping_role=TreatmentRole()), - MatchingMetrics(metric="ate"), - - # 5. Статистические тесты - TTest(compare_by="groups"), - KSTest(compare_by="groups") -] -``` - -Каждый компонент: - -- Независим и может быть заменен -- Использует результаты предыдущих через ExperimentData -- Добавляет свои результаты для последующих - -Это обеспечивает максимальную гибкость при построении экспериментов. - -## 6. Extension Framework +## 5. Extension Framework ### Концепция Extension'ов @@ -1115,245 +470,98 @@ Extension'ы в HypEx представляют собой систему для - Инкапсулируют детали работы с конкретными технологиями - Предоставляют оптимизированные реализации для разных backend'ов - Изолируют внешние зависимости (numpy, scipy, sklearn и т.д.) -- Автоматически выбирают правильную реализацию для текущего backend'а ### Архитектура Extension'ов -#### Базовый класс Extension - ```python class Extension(ABC): - """Базовый класс для всех Extension'ов в HypEx""" - - def __init__(self): - # Маппинг типов backend'ов на соответствующие методы реализации - self.BACKEND_MAPPING = { - PandasDataset: self._calc_pandas, - # SparkDataset: self._calc_spark, # Для будущих backend'ов - # DaskDataset: self._calc_dask, - } + def calc(self, data: Dataset, **kwargs) -> Dataset: + """Единая точка входа""" + backend_type = data.backend.__class__.__name__ + + if backend_type == "PandasBackend": + return self._calc_pandas(data, **kwargs) + elif backend_type == "SparkBackend": + return self._calc_spark(data, **kwargs) + else: + raise NotImplementedError(f"Backend {backend_type} not supported") @abstractmethod - def _calc_pandas(self, data: Dataset, **kwargs): - """Backend-специфичная реализация для pandas""" - raise AbstractMethodError - - def calc(self, data: Dataset, **kwargs): - """Основной метод - автоматически выбирает правильную реализацию""" - backend_type = type(data.backend) - implementation = self.BACKEND_MAPPING[backend_type] - return implementation(data=data, **kwargs) - - @staticmethod - def result_to_dataset(result: Any, roles: ABCRole | dict[str, ABCRole]) -> Dataset: - """Утилитарный метод для преобразования результата обратно в Dataset""" - return DatasetAdapter.to_dataset(result, roles=roles) -``` - -#### Принцип автоматического выбора backend'а - -Extension'ы автоматически определяют тип backend'а Dataset'а и вызывают соответствующую реализацию: - -```python -# Пример: Extension автоматически выбирает pandas реализацию -dataset = Dataset(data=pandas_dataframe, roles=roles) -extension = CholeskyExtension() + def _calc_pandas(self, data: Dataset, **kwargs) -> Dataset: + """Реализация для pandas""" + pass -# calc() автоматически вызовет _calc_pandas() -result = extension.calc(dataset) + def _calc_spark(self, data: Dataset, **kwargs) -> Dataset: + """Реализация для Spark (опционально)""" + raise NotImplementedError("Spark backend not implemented") ``` -### Типы Extension'ов +### Примеры Extension'ов -#### 1. Базовые математические Extension'ы - -Инкапсулируют фундаментальные математические операции: +#### StatisticalExtension — статистические вычисления ```python -class CholeskyExtension(Extension): - """Extension для разложения Холецкого""" - - def _calc_pandas(self, data: Dataset, epsilon: float = 1e-3, **kwargs): - # Получаем numpy массив из pandas backend'а - cov = data.data.to_numpy() +class TTestExtension(Extension): + def _calc_pandas(self, data: Dataset, grouping_col: str, target_col: str) -> Dataset: + """Pandas реализация t-теста""" + from scipy import stats - # Добавляем регуляризацию для численной стабильности - cov = cov + np.eye(cov.shape[0]) * epsilon + groups = data.df.groupby(grouping_col)[target_col] + group1, group2 = [group.values for name, group in groups] - # Выполняем разложение Холецкого - cholesky_result = np.linalg.cholesky(cov) + statistic, p_value = stats.ttest_ind(group1, group2) - # Преобразуем результат обратно в Dataset - return self.result_to_dataset( - pd.DataFrame(cholesky_result, columns=data.columns), - {column: FeatureRole() for column in data.columns} - ) + return Dataset.from_dict({ + "statistic": statistic, + "p-value": p_value + }) -class InverseExtension(Extension): - """Extension для обращения матрицы""" - - def _calc_pandas(self, data: Dataset, **kwargs): - inverse_matrix = np.linalg.inv(data.data.to_numpy()) - - return self.result_to_dataset( - pd.DataFrame(inverse_matrix, columns=data.columns), - {column: FeatureRole() for column in data.columns} - ) + def _calc_spark(self, data: Dataset, grouping_col: str, target_col: str) -> Dataset: + """Spark реализация через SQL""" + # Реализация через Spark SQL для больших данных + spark_df = data.backend.df + # ... Spark-специфичная логика ``` -#### 2. Encoder Extension'ы - -Обеспечивают предобработку данных: +#### MLExtension — машинное обучение ```python -class DummyEncoderExtension(Extension): - """Extension для one-hot encoding""" - - @staticmethod - def _calc_pandas(data: Dataset, target_cols: str | None = None, **kwargs): - # Создаем dummy переменные - dummies_df = pd.get_dummies( - data=data[target_cols].data, - drop_first=True - ) +class CholeskyExtension(Extension): + def _calc_pandas(self, data: Dataset, features: list) -> Dataset: + """Разложение Холецкого для pandas""" + import numpy as np - # Создаем роли для новых колонок на основе исходных - roles = { - col: data.roles[col[:col.rfind("_")]] - for col in dummies_df.columns - } + matrix = data.df[features].values + cholesky = np.linalg.cholesky(matrix) - # Обновляем тип данных для boolean колонок - for role in roles.values(): - role.data_type = bool - - return DatasetAdapter.to_dataset(dummies_df, roles=roles) -``` - -#### 3. Статистические Extension'ы - -Интегрируют внешние статистические библиотеки: - -```python -class MultiTest(Extension): - """Extension для коррекции множественного тестирования из statsmodels""" - - def __init__(self, method: ABNTestMethodsEnum, alpha: float = 0.05): - self.method = method - self.alpha = alpha - super().__init__() - - def _calc_pandas(self, data: Dataset, **kwargs): - # Извлекаем p-values из Dataset - p_values = data.data.values.flatten() - - # Применяем коррекцию из statsmodels - from statsmodels.stats.multitest import multipletests - corrected_results = multipletests( - p_values, - method=self.method.value, - alpha=self.alpha, - **kwargs + return Dataset( + data=pd.DataFrame(cholesky, columns=features), + roles={col: FeatureRole() for col in features} ) - - # Формируем результат в структурированном виде - result_data = { - "field": [i.split(ID_SPLIT_SYMBOL)[2] for i in data.index], - "test": [i.split(ID_SPLIT_SYMBOL)[0] for i in data.index], - "old p-value": p_values, - "new p-value": corrected_results[1], - "correction": [ - j / i if j != 0 else 0.0 - for i, j in zip(corrected_results[1], p_values) - ], - "rejected": corrected_results[0], - } - - return DatasetAdapter.to_dataset(result_data, StatisticRole()) -``` - -#### 4. Специализированные Extension'ы - -**CompareExtension** для сравнения двух Dataset'ов: -```python -class CompareExtension(Extension, ABC): - """Базовый класс для Extension'ов, сравнивающих два Dataset'а""" - - def calc(self, data: Dataset, other: Dataset | None = None, **kwargs): - return super().calc(data=data, other=other, **kwargs) -``` - -**MLExtension** для машинного обучения: - -```python -class MLExtension(Extension): - """Базовый класс для ML Extension'ов с поддержкой fit/predict""" - - def _calc_pandas(self, data: Dataset, test_data: Dataset | None = None, - mode: Literal["auto", "fit", "predict"] | None = None, **kwargs): - - if mode in ["auto", "fit"]: - return self.fit(data, test_data, **kwargs) - return self.predict(data) - - @abstractmethod - def fit(self, X, Y=None, **kwargs): - """Обучение модели""" - raise NotImplementedError - - @abstractmethod - def predict(self, X, **kwargs): - """Предсказание""" - raise NotImplementedError -``` - -### Интеграция Extension'ов с Calculator'ами - -#### Правильный паттерн использования - -Calculator'ы используют Extension'ы через делегирование для backend-специфичных операций: - -```python -# Концептуальный пример правильного использования Extension'ов в Calculator'е -class MahalanobisDistance(Calculator): - """Calculator для вычисления расстояния Махаланобиса""" - - def _inner_function(cls, data: Dataset, test_data: Dataset) -> dict: - # Вычисление ковариационной матрицы (backend-агностично) - control_cov = data.cov() - test_cov = test_data.cov() - pooled_cov = (control_cov + test_cov) / 2 - - # Делегируем backend-специфичные операции Extension'ам - cholesky_ext = CholeskyExtension() - cholesky_result = cholesky_ext.calc(pooled_cov) - - inverse_ext = InverseExtension() - mahalanobis_transform = inverse_ext.calc(cholesky_result) - - # Применяем преобразование (backend-агностично через Dataset API) - y_control = data.dot(mahalanobis_transform.transpose()) - y_test = test_data.dot(mahalanobis_transform.transpose()) - - return {"control": y_control, "test": y_test} + def _calc_spark(self, data: Dataset, features: list) -> Dataset: + """Spark ML реализация""" + from pyspark.ml.linalg import Vectors, Matrices + # ... Spark ML логика ``` -#### Архитектурные преимущества +### Интеграция с Calculator'ами -**1. Автоматическая адаптация к backend'у:** -- Extension автоматически выбирает правильную реализацию -- Calculator остается backend-агностичным -- Добавление нового backend'а требует только реализации новых методов в Extension'ах +Calculator'ы используют Extension'ы как делегаты: -**2. Изоляция зависимостей:** -- Внешние библиотеки (numpy, scipy, sklearn) изолированы в Extension'ах -- Calculator'ы не имеют прямых зависимостей на внешние библиотеки -- Упрощается управление зависимостями и тестирование +```python +class TTest(Comparator): + extension = TTestExtension() # Класс-атрибут -**3. Переиспользование логики:** -- Extension'ы можно использовать в разных Calculator'ах -- Единая реализация сложных операций для всей системы + @classmethod + def _inner_function(cls, data: Dataset, test_data: Dataset) -> Dataset: + # Делегируем вычисления Extension'у + return cls.extension.calc( + data=data, + grouping_col=cls._get_grouping_column(data), + target_col=cls._get_target_column(test_data) + ) +``` ### Жизненный цикл Extension'ов @@ -1372,159 +580,222 @@ result = extension.calc(pandas_dataset) # Вызовет _calc_pandas() # result = extension.calc(spark_dataset) # Вызовет _calc_spark() ``` -#### 2. Обработка результатов - -Extension'ы стандартизируют возвращаемые значения через `result_to_dataset()`: +#### 2. Изоляция зависимостей ```python -def _calc_pandas(self, data: Dataset, **kwargs): - # Выполняем backend-специфичные вычисления - raw_result = np.some_calculation(data.data.to_numpy()) - - # Стандартизируем результат - всегда возвращаем Dataset - return self.result_to_dataset( - pd.DataFrame(raw_result, columns=data.columns), - {col: FeatureRole() for col in data.columns} - ) +class ScipyExtension(Extension): + def _calc_pandas(self, data: Dataset, **kwargs) -> Dataset: + try: + from scipy import stats # Импорт только при использовании + # ... логика с scipy + except ImportError: + raise ImportError("scipy required for this operation") ``` -### Создание кастомных Extension'ов +### Преимущества Extension Framework + +#### 1. Архитектурная чистота + +- **Четкое разделение ответственности:** Calculator'ы для логики, Extension'ы для реализации +- **Backend-агностичность:** Бизнес-логика не зависит от технических деталей +- **Изоляция зависимостей:** Внешние библиотеки не "протекают" в основной код + +#### 2. Производительность и масштабируемость + +- **Автоматические оптимизации:** Система автоматически выбирает лучшую реализацию +- **Поддержка разных backend'ов:** pandas, Spark, Dask без изменения бизнес-логики +- **Ленивые вычисления:** Некоторые Extension'ы могут использовать ленивые вычисления + +#### 3. Гибкость и расширяемость + +- **Простое добавление backend'ов:** Новые backend'ы добавляются через Extension'ы +- **Модульность:** Extension'ы можно переиспользовать и комбинировать +- **Эволюция технологий:** Поддержка новых библиотек через Extension'ы + +Extension Framework является ключевым архитектурным решением HypEx, которое обеспечивает баланс между простотой использования и техническими возможностями. Он позволяет Calculator'ам оставаться backend-агностичными, при этом используя мощь специализированных библиотек для каждого типа данных. + +## 6. Слой вычислений: Comparators, Transformers, Operators -#### Шаблон для создания Extension'а +Слой вычислений содержит конкретные реализации Calculator'ов, каждая из которых специализируется на определенном типе +операций. Все они следуют паттерну разделения вычислительной логики от работы с ExperimentData. + +### Comparators: Сравнение и тестирование + +Comparators отвечают за сравнение групп и проведение статистических тестов. Они имеют сложную иерархию и богатую +функциональность. + +#### Иерархия Comparators ```python -class CustomExtension(Extension): - """Шаблон для создания кастомного Extension'а""" - - def __init__(self, param1: float = 1.0, param2: str = "default"): - """Инициализация с параметрами Extension'а""" - self.param1 = param1 - self.param2 = param2 - super().__init__() - - def _calc_pandas(self, data: Dataset, **kwargs): - """Реализация для pandas backend'а""" - try: - # Проверяем доступность необходимых библиотек - import required_library - - # Извлекаем данные из Dataset - numpy_data = data.data.to_numpy() - - # Выполняем backend-специфичные вычисления - result = required_library.some_function( - numpy_data, - param1=self.param1, - param2=self.param2, - **kwargs - ) - - # Преобразуем результат обратно в Dataset - if isinstance(result, np.ndarray): - result_df = pd.DataFrame(result, columns=data.columns) - roles = {col: FeatureRole() for col in data.columns} - else: - # Обработка других типов результатов - result_df = pd.DataFrame({"result": [result]}) - roles = {"result": StatisticRole()} - - return self.result_to_dataset(result_df, roles) - - except ImportError: - raise RuntimeError( - "CustomExtension требует библиотеку 'required_library'. " - "Установите: pip install required_library" - ) - except Exception as e: - raise RuntimeError(f"Ошибка в CustomExtension: {e}") - - # Для поддержки других backend'ов добавьте соответствующие методы: - # def _calc_spark(self, data: Dataset, **kwargs): - # """Реализация для Spark backend'а""" - # pass +Comparator (abstract) +├── StatHypothesisTesting — статистические тесты +│ ├── TTest — t-тест для сравнения средних +│ ├── KSTest — тест Колмогорова-Смирнова для распределений +│ ├── Chi2Test — хи-квадрат для категориальных переменных +│ └── UTest — тест Манна-Уитни (непараметрический) +├── Difference — вычисление разностей +│ ├── GroupDifference — разности между группами +│ └── RelativeDifference — относительные изменения +└── Correlation — корреляционный анализ + ├── PearsonCorrelation — корреляция Пирсона + └── SpearmanCorrelation — ранговая корреляция ``` -### Архитектурные принципы Extension'ов +#### Система ролей в Comparators + +Comparators автоматически определяют, какие колонки сравнивать, на основе ролей: + +- **TreatmentRole / GroupingRole**: Колонки для разбиения на группы +- **TargetRole**: Метрики для сравнения +- **FeatureRole**: Дополнительные признаки для анализа -#### 1. Принцип единого интерфейса +#### Примеры Comparators -Все Extension'ы предоставляют метод `calc()` с одинаковой сигнатурой: +**TTest** — сравнение средних значений: ```python -# Единый интерфейс для всех Extension'ов -result = extension.calc(data=dataset, **optional_params) +class TTest(StatHypothesisTesting): + def __init__(self, reliability: float = 0.05): + self.reliability = reliability + + @classmethod + def _inner_function(cls, data: Dataset, test_data: Dataset) -> Dataset: + # Использует TTestExtension для backend-специфичных вычислений + return cls.extension.calc(data, test_data, reliability=cls.reliability) +``` + +**KSTest** — сравнение распределений: + +```python +class KSTest(StatHypothesisTesting): + @classmethod + def _inner_function(cls, data: Dataset, test_data: Dataset) -> Dataset: + # Тест на различие распределений между группами + return cls.extension.calc(data, test_data, test_type="two_sample") ``` -#### 2. Принцип изоляции зависимостей +### Transformers: Преобразование данных + +Transformers изменяют данные, возвращая модифицированную копию Dataset. -Extension'ы изолируют внешние зависимости от основной логики: +#### Архитектура Transformers ```python -# Правильно: зависимости изолированы в Extension -class ScipyStatsExtension(Extension): - def _calc_pandas(self, data: Dataset, **kwargs): - from scipy.stats import ttest_ind # Изолированный import - # ... использование scipy - -# Неправильно: прямое использование в Calculator -class BadCalculator(Calculator): - def _inner_function(data: Dataset, **kwargs): - from scipy.stats import ttest_ind # Нарушение архитектуры! - # ... это нарушает backend-агностичность +class Transformer(Calculator): + def execute(self, data: ExperimentData) -> ExperimentData: + # Копируем данные для безопасного изменения + new_data = ExperimentData(self.calc(data.ds)) + # Переносим метаданные + new_data.copy_metadata_from(data) + return new_data + + def calc(self, data: Dataset) -> Dataset: + target_cols = self._get_target_columns(data) + return self._inner_function(data, target_cols, **self.params) ``` -#### 3. Принцип автоматического выбора реализации +#### Примеры Transformers -Extension'ы автоматически выбирают оптимальную реализацию: +**NaFiller** — заполнение пропусков: ```python -class AdaptiveExtension(Extension): - def __init__(self): - super().__init__() - # Расширяем mapping при добавлении новых backend'ов - if hasattr(self, '_calc_spark'): - self.BACKEND_MAPPING[SparkDataset] = self._calc_spark - if hasattr(self, '_calc_dask'): - self.BACKEND_MAPPING[DaskDataset] = self._calc_dask +class NaFiller(Transformer): + def __init__(self, method: str = "mean"): + self.method = method + + def _inner_function(data: Dataset, target_cols, method) -> Dataset: + if method in ['ffill', 'bfill']: + return data.fillna(method=method) + elif method == 'mean': + for col in target_cols: + data[col].fillna(data[col].mean(), inplace=True) + return data +``` + +**CategoryAggregator** — агрегация редких категорий: + +```python +class CategoryAggregator(Transformer): + def __init__(self, min_frequency: float = 0.01): + self.min_frequency = min_frequency + + def _inner_function(data: Dataset, target_cols, min_freq) -> Dataset: + for col in target_cols: + value_counts = data[col].value_counts(normalize=True) + rare_categories = value_counts[value_counts < min_freq].index + data[col] = data[col].replace(rare_categories, 'Other') + return data ``` -#### 4. Принцип graceful degradation +### Encoders: Кодирование признаков -Extension'ы обеспечивают работу даже при частичной недоступности функций: +Encoders преобразуют категориальные признаки в числовые. Результат сохраняется в additional_fields. ```python -class RobustExtension(Extension): - def _calc_pandas(self, data: Dataset, **kwargs): - try: - # Пытаемся использовать оптимизированную реализацию - import scipy.linalg - return self._fast_implementation(data, **kwargs) - except ImportError: - # Fallback на базовую реализацию - return self._basic_implementation(data, **kwargs) +class Encoder(Calculator): + def execute(self, data: ExperimentData) -> ExperimentData: + target_cols = data.ds.search_columns(roles=FeatureRole(), + search_types=[str]) + encoded = self.calc(data=data.ds, target_cols=target_cols) + # Сохраняем в additional_fields + return data.set_value( + space=ExperimentDataEnum.additional_fields, + executor_id=self._ids_to_names(encoded.columns), + value=encoded + ) ``` -### Преимущества Extension Framework +**DummyEncoder** — one-hot encoding: -#### 1. Архитектурная чистота +```python +class DummyEncoder(Encoder): + def _inner_function(data: Dataset, target_cols) -> Dataset: + # Использует pandas.get_dummies + dummies_df = pd.get_dummies(data[target_cols], drop_first=True) + # Устанавливает роли для новых колонок + roles = {col: data.roles[col.split('_')[0]] for col in dummies_df.columns} + return Dataset(data=dummies_df, roles=roles) +``` -- **Четкое разделение ответственности:** Calculator'ы для логики, Extension'ы для реализации -- **Backend-агностичность:** Бизнес-логика не зависит от технических деталей -- **Изоляция зависимостей:** Внешние библиотеки не "протекают" в основной код +### GroupOperators: Операции над группами -#### 2. Производительность и масштабируемость +GroupOperators выполняют специализированные операции над группами данных, часто используемые в matching и causal +inference. -- **Автоматические оптимизации:** Система автоматически выбирает лучшую реализацию -- **Поддержка разных backend'ов:** pandas, Spark, Dask без изменения бизнес-логики -- **Ленивые вычисления:** Некоторые Extension'ы могут использовать ленивые вычисления +#### Примеры GroupOperators -#### 3. Гибкость и расширяемость +**SMD (Standardized Mean Difference)** — стандартизированная разность средних: -- **Простое добавление backend'ов:** Новые backend'ы добавляются через Extension'ы -- **Модульность:** Extension'ы можно переиспользовать и комбинировать -- **Эволюция технологий:** Поддержка новых библиотек через Extension'ы +```python +class SMD(GroupOperator): + def _inner_function(data: Dataset, control_data: Dataset, treatment_data: Dataset) -> Dataset: + # Вычисляет Cohen's d для каждого признака + results = [] + for col in data.columns: + mean_diff = treatment_data[col].mean() - control_data[col].mean() + pooled_std = np.sqrt((treatment_data[col].var() + control_data[col].var()) / 2) + smd = mean_diff / pooled_std + results.append({"feature": col, "smd": smd}) + + return Dataset.from_records(results) +``` -Extension Framework является ключевым архитектурным решением HypEx, которое обеспечивает баланс между простотой использования и техническими возможностями. Он позволяет Calculator'ам оставаться backend-агностичными, при этом используя мощь специализированных библиотек для каждого типа данных. +**Bias** — оценка смещения после matching: + +```python +class Bias(GroupOperator): + def _inner_function(data: Dataset, matched_indices) -> Dataset: + # Оценивает качество балансировки после matching + # Возвращает метрики bias по каждому признаку +``` + +### Принципы проектирования слоя вычислений + +1. **Специализация по типам операций** — каждый тип Calculator'а решает свой класс задач +2. **Унификация интерфейсов** — общие паттерны для работы с ролями и данными +3. **Расширяемость** — легко добавлять новые операции через наследование +4. **Backend-агностичность** — вычисления делегируются Extension'ам +5. **Composability** — Calculator'ы можно комбинировать в complex pipeline'ы ## 7. Analyzer'ы — комплексный анализ результатов @@ -1721,166 +992,94 @@ Analyzer'ы являются ключевым компонентом для пр ### Базовый класс Experiment -Experiment — это контейнер для цепочки Executor'ов с логикой управления их выполнением: - ```python class Experiment(Executor): - def __init__(self, - executors: Sequence[Executor], - transformer: bool = None, - key: Any = ""): + def __init__(self, executors: Sequence[Executor]): self.executors = executors - self.transformer = transformer or self._detect_transformer() - super().__init__(key) - - def _detect_transformer(self) -> bool: - """Автоматически определяет, содержит ли цепочка Transformer'ы""" - return all(executor._is_transformer for executor in self.executors) + super().__init__() def execute(self, data: ExperimentData) -> ExperimentData: - """Последовательное выполнение всех Executor'ов""" - experiment_data = deepcopy(data) if self.transformer else data + result_data = data for executor in self.executors: - executor.key = self.key - experiment_data = executor.execute(experiment_data) - return experiment_data -``` - -**Ключевые особенности:** - -- **Композиция** — объединяет множество Executor'ов -- **Порядок важен** — Executor'ы выполняются последовательно -- **Управление состоянием** — копирует данные если есть Transformer'ы -- **Наследование от Executor** — Experiment сам является Executor'ом (паттерн Composite) - -### OnRoleExperiment — выполнение для каждой роли - -OnRoleExperiment применяет цепочку Executor'ов к каждой колонке с определенной ролью: + result_data = executor.execute(result_data) + return result_data -```python -class OnRoleExperiment(Experiment): - def __init__(self, - executors: list[Executor], - role: ABCRole | Sequence[ABCRole]): - self.role = [role] if isinstance(role, ABCRole) else list(role) - super().__init__(executors) - - def execute(self, data: ExperimentData) -> ExperimentData: - # Находим все колонки с нужной ролью - for field in data.ds.search_columns(self.role): - # Устанавливаем временную роль для текущей колонки - data.ds.tmp_roles = {field: TempTargetRole()} - # Выполняем всю цепочку для этой колонки - data = super().execute(data) - # Очищаем временную роль - data.ds.tmp_roles = {} - return data + @property + def transformer(self) -> bool: + """Определяет, есть ли среди Executor'ов Transformer'ы""" + return any(isinstance(ex, Transformer) for ex in self.executors) ``` -**Пример использования:** +**Ключевые особенности:** -```python -# Применить тесты ко всем target метрикам -experiment = OnRoleExperiment( - executors=[ - GroupDifference(grouping_role=TreatmentRole()), - TTest(grouping_role=TreatmentRole()), - KSTest(grouping_role=TreatmentRole()) - ], - role=TargetRole() # Будет применено к каждой колонке с TargetRole -) +- **Композиция** — Experiment сам является Executor'ом и может включать другие Experiment'ы +- **Автоопределение копирования** — автоматически определяет, нужно ли копировать данные +- **ID генерация** — создает составной ID из ID своих Executor'ов -# Если есть колонки: revenue (TargetRole), retention (TargetRole), clicks (FeatureRole) -# То тесты будут применены к revenue и retention, но не к clicks -``` +### Специализированные эксперименты -### ExperimentWithReporter — эксперименты с отчетностью +#### OnRoleExperiment — применение по ролям -Добавляет возможность генерации отчетов после выполнения: +Применяет набор Executor'ов ко всем колонкам с определенной ролью: ```python -class ExperimentWithReporter(Experiment): - def __init__(self, - executors: Sequence[Executor], - reporter: Reporter): +class OnRoleExperiment(Experiment): + def __init__(self, executors: Sequence[Executor], role: ABCRole): + self.role = role super().__init__(executors) - self.reporter = reporter - - def one_iteration(self, - data: ExperimentData, - key: str = "") -> Dataset: - """Одна итерация эксперимента с отчетом""" - t_data = ExperimentData(data.ds) - self.key = key - t_data = super().execute(t_data) - return self.reporter.report(t_data) -``` - -Это базовый класс для специализированных экспериментов, которые нуждаются в форматированном выводе результатов. - -### CycledExperiment — многократное выполнение - -Выполняет эксперимент заданное количество раз: - -```python -class CycledExperiment(ExperimentWithReporter): - def __init__(self, - executors: Sequence[Executor], - reporter: DatasetReporter, - n_iterations: int = 10): - super().__init__(executors, reporter) - self.n_iterations = n_iterations def execute(self, data: ExperimentData) -> ExperimentData: - results = [] - for i in range(self.n_iterations): - # Каждая итерация начинается с чистых данных - iteration_result = self.one_iteration(data, key=str(i)) - results.append(iteration_result) - - # Объединяем результаты всех итераций - combined_results = Dataset.concat(results) - - return data.set_value( - space=ExperimentDataEnum.analysis_tables, - executor_id=self.id, - value=combined_results - ) + target_columns = data.ds.search_columns(roles=self.role) + + result_data = data + for column in target_columns: + for executor in self.executors: + # Применяем executor к каждой колонке с данной ролью + executor.set_params({"target_column": column}) + result_data = executor.execute(result_data) + + return result_data ``` **Применение:** -- Bootstrap анализ -- Оценка стабильности результатов -- Monte Carlo симуляции +- Статистические тесты ко всем метрикам +- Применение фильтров ко всем признакам +- Агрегация по всем группирующим колонкам -### GroupExperiment — выполнение по группам +#### GroupExperiment — обработка по группам -Применяет эксперимент к каждой группе данных отдельно: +Разбивает данные по группам и применяет Executor'ы к каждой группе: ```python class GroupExperiment(ExperimentWithReporter): def __init__(self, executors: Sequence[Executor], reporter: DatasetReporter, - searching_role: ABCRole = GroupingRole()): - super().__init__(executors, reporter) + searching_role: ABCRole): self.searching_role = searching_role + super().__init__(executors, reporter) def execute(self, data: ExperimentData) -> ExperimentData: - # Находим поле для группировки - group_field = data.ds.search_columns([self.searching_role])[0] - - results = [] - for group, group_data in data.ds.groupby(group_field): - result = self.one_iteration( - ExperimentData(group_data), - str(group[0]) - ) - results.append(result) - - # Объединяем результаты с сохранением индексов групп - return self._set_result(data, results, reset_index=False) + grouping_columns = data.ds.search_columns(roles=self.searching_role) + + all_results = [] + for group_col in grouping_columns: + for group_value in data.ds[group_col].unique(): + # Фильтруем данные для текущей группы + group_data = data.ds[data.ds[group_col] == group_value] + t_data = ExperimentData(group_data) + + # Применяем все Executor'ы к группе + for executor in self.executors: + t_data = executor.execute(t_data) + + # Сохраняем результат группы + group_result = self.reporter.report(t_data) + group_result[group_col] = group_value + all_results.append(group_result) + + return self._set_result(data, all_results, reset_index=False) ``` **Применение:** @@ -1889,7 +1088,7 @@ class GroupExperiment(ExperimentWithReporter): - Гетерогенные эффекты - Подгрупповой анализ -### ParamsExperiment — параметрический поиск +#### ParamsExperiment — параметрический поиск Выполняет эксперимент с различными комбинациями параметров: @@ -1947,12 +1146,13 @@ class ParamsExperiment(ExperimentWithReporter): # Сохраняем результат итерации iteration_result = self.reporter.report(t_data) iteration_result["params"] = flat_param + iteration_result["iteration"] = i results.append(iteration_result) return self._set_result(data, results) ``` -### IfParamsExperiment — параметрический поиск с условием остановки +#### IfParamsExperiment — параметрический поиск с условием остановки Добавляет возможность ранней остановки при достижении условия: @@ -1969,12 +1169,16 @@ class IfParamsExperiment(ParamsExperiment): def execute(self, data: ExperimentData) -> ExperimentData: self._update_flat_params() - for flat_param in tqdm(self._flat_params): + for i, flat_param in enumerate(tqdm(self._flat_params)): t_data = ExperimentData(data.ds) # Выполняем эксперимент for executor in self.executors: - executor.set_params(flat_param) + params_for_executor = self._extract_params_for_executor( + executor, flat_param + ) + if params_for_executor: + executor.set_params(params_for_executor) t_data = executor.execute(t_data) # Проверяем условие остановки @@ -1986,7 +1190,10 @@ class IfParamsExperiment(ParamsExperiment): if if_result.variables[if_executor_id]["response"]: # Условие выполнено - останавливаемся - return self._set_result(data, [self.reporter.report(t_data)]) + final_result = self.reporter.report(t_data) + final_result["params"] = flat_param + final_result["iteration"] = i + return self._set_result(data, [final_result]) # Условие не выполнено ни для одной комбинации return data @@ -2229,7 +1436,7 @@ aa_grid_search = ParamsExperiment( AASplitter(), # Будет параметризован Experiment([ GroupSizes(grouping_role=AdditionalTreatmentRole()), - OnRoleExperiment( + OnRoleExperiment( # Применить тесты ко всем targets executors=[ TTest(grouping_role=AdditionalTreatmentRole()), KSTest(grouping_role=AdditionalTreatmentRole()) @@ -2263,45 +1470,6 @@ optimized_search = IfParamsExperiment( ), reporter=OptimalParamsReporter() ) - -# Вложенный grid search для hyperparameter tuning -hyperparameter_tuning = ParamsExperiment( - executors=[ - # Внешний уровень: параметры предобработки - ParamsExperiment( - executors=[ - OutliersFilter(), - NaFiller() - ], - params={ - OutliersFilter: { - "lower_percentile": [0.01, 0.05], - "upper_percentile": [0.95, 0.99] - }, - NaFiller: { - "method": ["ffill", "bfill", "mean"] - } - }, - reporter=PreprocessingReporter() - ), - - # Внутренний уровень: параметры модели - ParamsExperiment( - executors=[ - FaissNearestNeighbors() - ], - params={ - FaissNearestNeighbors: { - "n_neighbors": [1, 3, 5, 7], - "faiss_mode": ["base", "fast"] - } - }, - reporter=ModelReporter() - ) - ], - params={}, # Внешний уровень без дополнительных параметров - reporter=HyperparameterReporter() -) ``` **Преимущества:** @@ -2315,167 +1483,117 @@ hyperparameter_tuning = ParamsExperiment( Многоуровневая обработка с агрегацией на разных уровнях. ```python -# Иерархический анализ: пользователь -> сегмент -> общий +# Иерархический анализ по регионам и городам hierarchical_analysis = Experiment([ - # Уровень 1: Анализ на уровне пользователей - OnRoleExperiment( - executors=[ - UserLevelMetrics(), - UserLevelTests() - ], - role=UserRole() - ), - - # Уровень 2: Агрегация по сегментам + # Уровень 1: Анализ по регионам GroupExperiment( executors=[ - SegmentAggregator(), - SegmentLevelTests() + # Базовая статистика по региону + GroupSizes(), GroupDifference(), + TTest(), KSTest(), + + # Уровень 2: Анализ городов внутри региона + GroupExperiment( + executors=[ + GroupSizes(), GroupDifference(), + TTest() # Упрощенный анализ для городов + ], + searching_role=CityRole(), + reporter=CityReporter() + ) ], - searching_role=SegmentRole(), - reporter=SegmentReporter() + searching_role=RegionRole(), + reporter=RegionReporter() ), - # Уровень 3: Общая агрегация - Experiment([ - GlobalAggregator(), - GlobalSignificanceTest(), - MultipleTestingCorrection() - ]) -]) - -# Каскадный анализ с фильтрацией на каждом уровне -cascade_filtering = Experiment([ - # Этап 1: Грубая фильтрация - ConstFilter(threshold=0.99), - - # Этап 2: Фильтрация по корреляции (только для оставшихся) - CorrFilter(threshold=0.9), - - # Этап 3: Тонкая фильтрация по CV (только для некоррелированных) - CVFilter(lower_bound=0.01, upper_bound=10), - - # Этап 4: Финальная фильтрация outliers - OutliersFilter(lower_percentile=0.01, upper_percentile=0.99) + # Агрегация по всем уровням + HierarchicalAggregator() ]) -``` - -**Преимущества:** - -- Эффективная обработка больших данных -- Иерархическая агрегация результатов -- Последовательное уточнение анализа - -#### 6. Retry паттерн — повторные попытки с разными стратегиями - -Попытки выполнить анализ разными способами при неудаче. -```python -class RetryExecutor(Executor): - def __init__(self, strategies: list[Executor], max_retries: int = 3): - self.strategies = strategies - self.max_retries = max_retries - - def execute(self, data: ExperimentData) -> ExperimentData: - for i, strategy in enumerate(self.strategies[:self.max_retries]): - try: - result = strategy.execute(data) - if self._is_valid_result(result): - return result - except Exception as e: - if i == len(self.strategies) - 1: - raise e - continue - return data - - -# Использование retry паттерна -robust_matching = Experiment([ - RetryExecutor( - strategies=[ - # Стратегия 1: Точный matching - Experiment([ - MahalanobisDistance(), - FaissNearestNeighbors(n_neighbors=1, faiss_mode="base") - ]), - - # Стратегия 2: Приближенный matching - Experiment([ - FaissNearestNeighbors(n_neighbors=5, faiss_mode="fast") - ]), - - # Стратегия 3: Fallback на случайное сопоставление - RandomMatching() - ] +# Многоуровневая предобработка +hierarchical_preprocessing = Experiment([ + # Глобальная предобработка + GlobalOutliersFilter(), + + # Предобработка по группам + GroupExperiment( + executors=[ + LocalOutliersFilter(), # Локальное удаление выбросов + GroupSpecificNormalization() # Нормализация по группе + ], + searching_role=TreatmentRole(), + reporter=PreprocessingReporter() ), - MatchingQualityAssessment() + + # Финальная агрегация + FinalNormalization() ]) ``` **Преимущества:** -- Устойчивость к ошибкам -- Graceful degradation -- Адаптивность к качеству данных - -#### 7. Observer паттерн — мониторинг выполнения - -Добавление логирования и мониторинга в процесс выполнения. - -```python -class ObservableExperiment(Experiment): - def __init__(self, executors: list[Executor], observers: list[Observer] = None): - super().__init__(executors) - self.observers = observers or [] +- Естественное моделирование иерархических структур +- Агрегация на разных уровнях детализации +- Гибкость в обработке сложных данных - def execute(self, data: ExperimentData) -> ExperimentData: - for i, executor in enumerate(self.executors): - # Уведомляем о начале - for observer in self.observers: - observer.on_executor_start(executor, data) +### Композиция и переиспользуемость - # Выполнение - start_time = time.time() - data = executor.execute(data) - execution_time = time.time() - start_time +Эксперименты спроектированы для максимального переиспользования: - # Уведомляем о завершении - for observer in self.observers: - observer.on_executor_complete(executor, data, execution_time) +```python +# Стандартные блоки +data_cleaning = Experiment([ + NaFiller(method="ffill"), + OutliersFilter(lower_percentile=0.05, upper_percentile=0.95) +]) - return data +feature_engineering = Experiment([ + CategoryAggregator(min_frequency=0.01), + DummyEncoder(), + ConstFilter(threshold=0.95) +]) +basic_testing = Experiment([ + GroupSizes(), + GroupDifference(), + TTest(), + KSTest() +]) -# Использование -experiment = ObservableExperiment( - executors=[...], - observers=[ - LoggingObserver(), - MetricsCollector(), - ProgressBar(), - AlertingObserver(threshold=60) # Алерт если executor > 60 сек - ] -) -``` +# Композиция для разных случаев +simple_ab_test = Experiment([ + data_cleaning, + basic_testing, + ABAnalyzer() +]) -**Преимущества:** +advanced_ab_test = Experiment([ + data_cleaning, + feature_engineering, + basic_testing, + Chi2Test(), # Дополнительный тест + UTest(), # Непараметрический тест + ABAnalyzer(multitest_method="fdr_bh") +]) -- Прозрачность выполнения -- Сбор метрик и диагностика -- Возможность прерывания при проблемах +matching_analysis = Experiment([ + data_cleaning, + feature_engineering, + # Специфичные для matching компоненты + MahalanobisDistance(), + FaissNearestNeighbors(), + MatchingMetrics(), + MatchingAnalyzer() +]) +``` -### Лучшие практики при проектировании экспериментов +Каждый компонент: -1. **Модульность** — разбивайте сложные эксперименты на логические блоки -2. **Переиспользование** — создавайте библиотеку стандартных подэкспериментов -3. **Валидация** — добавляйте проверки между этапами -4. **Документирование** — используйте говорящие имена и комментарии -5. **Тестирование** — тестируйте эксперименты на небольших данных -6. **Версионирование** — сохраняйте версии успешных экспериментов -7. **Мониторинг** — логируйте ключевые метрики выполнения +- Независим и может быть заменен +- Использует результаты предыдущих через ExperimentData +- Добавляет свои результаты для последующих -Слой экспериментов предоставляет мощные абстракции для построения сложных аналитических pipeline'ов, сохраняя при этом -простоту и читаемость кода. +Это обеспечивает максимальную гибкость при построении экспериментов. ## 9. Система Reporter'ов @@ -2556,181 +1674,74 @@ ExperimentData → DictReporter → dict → OnDictReporter → Любой фо **Ключевая идея:** Разделение извлечения данных и их представления. -#### Стандартные форматтеры +#### Примеры OnDictReporter -**DatasetReporter** — табличное представление: +**DatasetReporter** — конвертация в Dataset: ```python class DatasetReporter(OnDictReporter): - def report(self, data: ExperimentData): - # Получаем базовый dict - dict_report = self.dict_reporter.report(data) - # Преобразуем в структурированную таблицу - return self.convert_flat_dataset(dict_report) -``` - -**Потенциальные форматтеры (roadmap):** - -**HTMLReporter** — интерактивные HTML отчеты: - -- Таблицы с сортировкой и фильтрацией -- Графики и визуализации -- Collapsible секции для детализации -- Экспорт в различные форматы - -**PDFReporter** — профессиональные PDF отчеты: - -- Форматированные таблицы и графики -- Executive summary на первой странице -- Детальные приложения с методологией -- Брендирование и стилизация - -**MarkdownReporter** — отчеты для документации: - -- Структурированный markdown -- Таблицы в GFM формате -- Встроенные графики как base64 -- Готов для вставки в wiki/confluence - -**JSONReporter** — машиночитаемый формат: - -- Полная сериализация результатов -- Метаданные об эксперименте -- Версионирование схемы -- Поддержка streaming + def __init__(self, dict_reporter: DictReporter): + self.dict_reporter = dict_reporter -**ExcelReporter** — multi-sheet Excel файлы: - -- Summary на первом листе -- Детальные результаты по листам -- Условное форматирование -- Встроенные формулы и графики - -#### Создание кастомного форматтера - -```python -class CustomFormatter(OnDictReporter): - def __init__(self, dict_reporter: DictReporter, format_options: dict): - super().__init__(dict_reporter) - self.format_options = format_options - - def report(self, data: ExperimentData): - # Получаем базовый словарь - base_dict = self.dict_reporter.report(data) - - # Применяем кастомное форматирование - formatted = self.apply_formatting(base_dict) - - # Добавляем метаданные - formatted['metadata'] = self.extract_metadata(data) - - # Возвращаем в нужном формате - return self.render(formatted) + def report(self, data: ExperimentData) -> Dataset: + result_dict = self.dict_reporter.report(data) + return Dataset.from_dict(result_dict) ``` -### Использование Reporter'ов в Experiment - -Reporter'ы интегрированы в систему экспериментов через класс ExperimentWithReporter. - -#### ExperimentWithReporter - -Этот класс добавляет автоматическую генерацию отчетов к экспериментам: +**HTMLReporter** — генерация HTML отчетов: ```python -class ExperimentWithReporter(Experiment): - def __init__(self, executors: list, reporter: Reporter): - super().__init__(executors) - self.reporter = reporter - - def one_iteration(self, data: ExperimentData, key: str = ""): - # Выполняем эксперимент - result_data = super().execute(data) - # Автоматически генерируем отчет - return self.reporter.report(result_data) +class HTMLReporter(OnDictReporter): + def report(self, data: ExperimentData) -> str: + result_dict = self.dict_reporter.report(data) + return self._dict_to_html(result_dict) ``` -#### Специализированные эксперименты с Reporter'ами - -**ParamsExperiment** — всегда требует Reporter: +### Специализированные Reporter'ы -- После каждой комбинации параметров генерирует отчет -- Агрегирует отчеты всех итераций -- Позволяет сравнить результаты разных параметров +#### ABDictReporter — A/B тестирование -**GroupExperiment** — Reporter для каждой группы: - -- Генерирует отчет для каждой группы отдельно -- Объединяет в общую таблицу с индексом по группам +```python +class ABDictReporter(TestDictReporter): + def report(self, data: ExperimentData) -> dict: + result = {} -**CycledExperiment** — Reporter для каждого цикла: + # Извлекаем результаты тестов + result.update(self._extract_from_comparators(data)) -- Отчет после каждой итерации -- Статистика по всем итерациям + # Извлекаем размеры групп + result.update(self._extract_from_executors(data, [GroupSizes])) -#### Паттерны использования + # Извлекаем эффекты + result.update(self._extract_from_executors(data, [GroupDifference])) -**Inline reporter в эксперименте:** + # Результаты ABAnalyzer (если есть) + ab_analysis = self._extract_from_executors(data, [ABAnalyzer]) + if ab_analysis: + result.update(ab_analysis) -```python -experiment = ParamsExperiment( - executors=[...], - params={...}, - reporter=DatasetReporter(ABDictReporter()) -) + return result ``` -**Композиция репортеров:** +#### MatchingDictReporter — Matching анализ ```python -base_reporter = TestDictReporter() -formatted_reporter = DatasetReporter(base_reporter) +class MatchingDictReporter(DictReporter): + def report(self, data: ExperimentData) -> dict: + result = {} -experiment = ExperimentWithReporter( - executors=[...], - reporter=formatted_reporter -) -``` + # Результаты matching + result.update(self._extract_matching_results(data)) -**Множественные отчеты:** + # Метрики качества + result.update(self._extract_quality_metrics(data)) -```python -data = experiment.execute(initial_data) + # Bias оценки + result.update(self._extract_bias_analysis(data)) -# Разные форматы из одних данных -summary = SummaryDictReporter().report(data) -detailed = DetailedDatasetReporter().report(data) -visual = VisualizationReporter().report(data) + return result ``` -### Иерархия конкретных Reporter'ов - -HypEx предоставляет набор готовых Reporter'ов для типовых задач: - -#### Для статистических тестов - -- **TestDictReporter** — базовый класс для тестовых репортеров -- **OneAADictReporter** — отчет по одному A/A тесту -- **AADatasetReporter** — табличный отчет по A/A тестам -- **ABDictReporter** — отчет по A/B тесту -- **ABDatasetReporter** — табличный отчет по A/B тесту -- **HomoDictReporter** — отчет по тесту гомогенности -- **HomoDatasetReporter** — табличный отчет по гомогенности - -#### Для matching - -- **MatchingDictReporter** — базовый отчет по matching -- **MatchingDatasetReporter** — табличный отчет по matching -- **MatchingQualityDictReporter** — отчет по качеству matching -- **MatchingQualityDatasetReporter** — табличный отчет по качеству - -#### Специальные репортеры - -- **AAPassedReporter** — определение прошедших A/A тестов -- **AABestSplitReporter** — отчет о лучшем разбиении - -Каждый из этих Reporter'ов знает, какие именно результаты извлекать из ExperimentData и как их правильно -интерпретировать. - ### Принципы проектирования Reporter'ов 1. **Separation of Concerns:** @@ -3132,8 +2143,9 @@ matching_custom = Matching( #### Принцип "90% в 2 строчки" -Shell слой реализует ключевой принцип библиотеки HypEx: 90% практических задач должны решаться в 2 строчки кода. Каждый -Shell создавался на основе анализа реальных индустриальных потребностей: +Shell слой реализует ключевой принцип библиотеки HypEx: 90% практических задач должны решаться в 2 строчки кода. + +Каждый Shell создавался на основе анализа реальных индустриальных потребностей: - **AATest** — стандартная процедура валидации разбиений в product экспериментах - **ABTest** — основной инструмент для измерения эффекта изменений @@ -3284,393 +2296,328 @@ print(f"Размеры групп:\n{results.sizes}") **Характеристики задачи:** -- Базовый A/B тест + дополнительные методы проверки качества -- Нестандартная комбинация стандартных блоков -- Требуется кастомизация параметров анализа -- Команда готова работать с более сложным API +- Нестандартные дополнительные тесты +- Специфические настройки коррекции +- Сохранение стандартной основы A/B теста -**Решение — композиция Executor'ов:** +**Решение — композиция стандартных компонентов:** ```python -from hypex.experiments.base import Experiment, OnRoleExperiment -from hypex.comparators import TTest, KSTest, PSI -from hypex.operators import GroupSizes, GroupDifference -from hypex.analyzers.ab import ABAnalyzer -from hypex.dataset import TargetRole +from hypex.experiments import Experiment, OnRoleExperiment +from hypex.executors import PSITest, TTest, KSTest, GroupSizes, GroupDifference +from hypex.analyzers import ABAnalyzer +from hypex.reporters import ABDictReporter -# Создание кастомного эксперимента -custom_ab_experiment = Experiment([ - # Стандартная часть +# Построение кастомного A/B теста +enhanced_ab_test = Experiment([ + # Базовые метрики (стандартно) + GroupSizes(grouping_role=TreatmentRole()), + GroupDifference(grouping_role=TreatmentRole()), + + # Применяем расширенный набор тестов ко всем метрикам OnRoleExperiment([ - GroupSizes(grouping_role=TreatmentRole()), - GroupDifference(grouping_role=TreatmentRole()), TTest(grouping_role=TreatmentRole()), KSTest(grouping_role=TreatmentRole()), - - # Дополнительный анализ стабильности - PSI(grouping_role=TreatmentRole()), # Стабильность распределения - + PSITest(grouping_role=TreatmentRole()) # Дополнительный тест ], role=TargetRole()), - - # Анализ результатов с менее консервативной коррекцией - ABAnalyzer(multitest_method="fdr_bh") # False Discovery Rate вместо Bonferroni + + # Анализ с кастомными настройками + ABAnalyzer( + multitest_method="fdr_bh", # Менее консервативная коррекция + effect_size_threshold=0.02 # Кастомный порог практической значимости + ) ]) # Выполнение experiment_data = ExperimentData(dataset) -results = custom_ab_experiment.execute(experiment_data) - -# Извлечение результатов через Reporter -from hypex.reporters.ab import ABDictReporter +results = enhanced_ab_test.execute(experiment_data) +# Извлечение результатов reporter = ABDictReporter() formatted_results = reporter.report(results) - -print("=== РАСШИРЕННЫЙ A/B АНАЛИЗ ===") -print(f"T-test: {formatted_results['ttest']}") -print(f"KS-test: {formatted_results['kstest']}") -print(f"PSI (стабильность групп): {formatted_results['psi']}") -print(f"FDR-скорректированные результаты: {formatted_results['multitest']}") -``` - -**Что изменилось:** - -- Перешли от Shell к прямой композиции Executor'ов -- Добавили PSI для проверки качества разбиения на группы -- Изменили метод коррекции множественного тестирования на менее консервативный -- Используем Reporter для извлечения результатов - -**Результат:** - -``` -T-test: p-value = 0.032, effect = 1.6% -KS-test: p-value = 0.045, distribution differs -PSI = 0.08 (группы сбалансированы, PSI < 0.1) -FDR-скорректированные p-values: [0.038, 0.048] -Рекомендация: Эффект значим, группы качественно сбалансированы ``` -**Почему потребовался переход на уровень композиции:** +**Архитектурные преимущества:** -- Нужен PSI анализ, который не включен в стандартный ABTest -- Требуется другой метод коррекции множественного тестирования -- Кастомизация набора выполняемых тестов +- **Переиспользование**: Используем стандартные компоненты +- **Гибкость**: Добавляем только нужную функциональность +- **Консистентность**: Сохраняется логика стандартного A/B теста +- **Прозрачность**: Видны все этапы анализа -### Сценарий 3: Кастомный анализ для специфической метрики (Уровень 6 — Расширение) +### Сценарий 3: Создание кастомного Executor (Уровень 6 — Расширение) -**Бизнес-задача:** Продуктовая команда анализирует влияние изменений на конверсию в покупку, которая рассчитывается как -отношение (ratio): покупки / уникальные пользователи. Стандартные тесты не учитывают специфику ratio метрик. +**Бизнес-задача:** Data Science команда нуждается в специфическом статистическом тесте — Permutation Test — который +не входит в стандартный набор HypEx. **Характеристики задачи:** -- Специфический тип метрики (ratio: события/пользователи) -- Нужен специализированный статистический тест с delta method -- Стандартные Calculator'ы не покрывают потребность -- Команда имеет статистическую экспертизу +- Требуется новая функциональность +- Нужна интеграция с существующей архитектурой +- Планируется переиспользование в разных экспериментах -**Решение — создание кастомного Calculator'а:** +**Решение — создание кастомного Executor:** ```python -from hypex.executors.calculator import Comparator +from hypex.executors.base import StatHypothesisTesting from hypex.dataset import Dataset import numpy as np -from scipy import stats +from typing import Optional - -class RatioTest(Comparator): +class PermutationTest(StatHypothesisTesting): """ - Статистический тест для сравнения ratio метрик между группами. - Использует delta method для корректной оценки стандартной ошибки. + Permutation Test для сравнения групп без предположений о распределении. """ - - def __init__(self, alpha: float = 0.05, **kwargs): - super().__init__(**kwargs) - self.alpha = alpha - - def _inner_function(self, control_data: Dataset, test_data: Dataset) -> dict: + + def __init__(self, + n_permutations: int = 10000, + reliability: float = 0.05, + key: str = ""): + self.n_permutations = n_permutations + super().__init__(reliability=reliability, key=key) + + @classmethod + def _inner_function(cls, + data: Dataset, + test_data: Dataset, + n_permutations: int = 10000, + reliability: float = 0.05) -> Dataset: """ - Выполняет тест для ratio метрик. - - Ожидает колонки: - - events: количество событий (покупок) - - users: количество уникальных пользователей + Реализация permutation test. """ - - # Извлечение данных - control_events = control_data['events'].sum() - control_users = control_data['users'].sum() - test_events = test_data['events'].sum() - test_users = test_data['users'].sum() - - # Расчет ratio метрик - control_ratio = control_events / control_users if control_users > 0 else 0 - test_ratio = test_events / test_users if test_users > 0 else 0 - - # Delta method для стандартной ошибки ratio - control_se = self._delta_method_se(control_events, control_users) - test_se = self._delta_method_se(test_events, test_users) - - # Pooled standard error для разности - pooled_se = np.sqrt(control_se ** 2 + test_se ** 2) - - # Z-test для разности ratio - if pooled_se > 0: - z_stat = (test_ratio - control_ratio) / pooled_se - p_value = 2 * (1 - stats.norm.cdf(abs(z_stat))) - else: - z_stat = 0 - p_value = 1.0 - - # Доверительный интервал для разности - diff = test_ratio - control_ratio - ci_margin = stats.norm.ppf(1 - self.alpha / 2) * pooled_se - ci_lower = diff - ci_margin - ci_upper = diff + ci_margin - - return { - "control_ratio": control_ratio, - "test_ratio": test_ratio, - "ratio_difference": diff, - "relative_lift": (diff / control_ratio * 100) if control_ratio > 0 else 0, - "statistic": z_stat, + # Извлекаем данные для двух групп + group_col = cls._get_grouping_column(data) + target_col = cls._get_target_column(test_data) + + groups = data.df[group_col].unique() + if len(groups) != 2: + raise ValueError("Permutation test поддерживает только 2 группы") + + group1_data = test_data.df[data.df[group_col] == groups[0]][target_col] + group2_data = test_data.df[data.df[group_col] == groups[1]][target_col] + + # Наблюдаемая разность средних + observed_diff = group2_data.mean() - group1_data.mean() + + # Объединяем данные для перестановок + combined_data = np.concatenate([group1_data, group2_data]) + n1, n2 = len(group1_data), len(group2_data) + + # Выполняем перестановки + permutation_diffs = [] + np.random.seed(42) # Для воспроизводимости + + for _ in range(n_permutations): + # Случайно перемешиваем данные + np.random.shuffle(combined_data) + perm_group1 = combined_data[:n1] + perm_group2 = combined_data[n1:] + + # Вычисляем разность для перестановки + perm_diff = perm_group2.mean() - perm_group1.mean() + permutation_diffs.append(perm_diff) + + # Вычисляем p-value (двусторонний тест) + permutation_diffs = np.array(permutation_diffs) + p_value = np.mean(np.abs(permutation_diffs) >= np.abs(observed_diff)) + + # Формируем результат + return Dataset.from_dict({ + "statistic": observed_diff, "p-value": p_value, - "pass": p_value < self.alpha, - "confidence_interval": [ci_lower, ci_upper], - "interpretation": self._interpret_results(p_value, diff, control_ratio) - } - - def _delta_method_se(self, events: float, users: float) -> float: - """Вычисляет стандартную ошибку ratio через delta method""" - if users <= 0: - return 0 - - ratio = events / users - # Delta method: SE(X/Y) ≈ sqrt(Var(X)/Y² + X²*Var(Y)/Y⁴ - 2*X*Cov(X,Y)/Y³) - # Для Poisson процесса: Var(events) = events, Cov(events, users) ≈ 0 - variance = (events / users ** 2) + (ratio ** 2 * users / users ** 2) # Упрощенная формула - return np.sqrt(variance / users) # Нормализация на размер выборки - - def _interpret_results(self, p_value: float, diff: float, control_ratio: float) -> str: - """Генерирует текстовую интерпретацию результатов""" - if p_value >= self.alpha: - return "Нет статистически значимой разницы в конверсии" - - if diff > 0: - lift_pct = (diff / control_ratio * 100) if control_ratio > 0 else 0 - return f"Конверсия выше в тестовой группе на {lift_pct:.1f}% (улучшение)" - else: - drop_pct = abs(diff / control_ratio * 100) if control_ratio > 0 else 0 - return f"Конверсия ниже в тестовой группе на {drop_pct:.1f}% (ухудшение)" - + "n_permutations": n_permutations, + "pass": p_value < reliability + }) -# Создание эксперимента с кастомным тестом -ratio_experiment = Experiment([ +# Использование в композиции +advanced_ab_experiment = Experiment([ + GroupSizes(), + GroupDifference(), + OnRoleExperiment([ - GroupSizes(grouping_role=TreatmentRole()), - - # Кастомный анализ ratio метрик - RatioTest( - grouping_role=TreatmentRole(), - alpha=0.05 - ), - - # Дополнительные стандартные тесты для validation - TTest(grouping_role=TreatmentRole()), # Для проверки consistency - + TTest(), # Стандартный параметрический тест + KSTest(), # Стандартный непараметрический тест + PermutationTest( # Наш кастомный тест + n_permutations=20000, + reliability=0.01 + ) ], role=TargetRole()), + + ABAnalyzer(multitest_method="holm") ]) - -# Подготовка данных для ratio анализа -ratio_dataset = Dataset( - data=experiment_data, - roles={ - 'user_group': TreatmentRole(), - 'conversion_events': TargetRole(), # Количество покупок - 'unique_users': TargetRole(), # Количество уникальных пользователей - 'revenue_per_user': TargetRole() # Дополнительная метрика для t-test - } -) - -# Выполнение анализа -experiment_data = ExperimentData(ratio_dataset) -results = ratio_experiment.execute(experiment_data) -``` - -**Результат кастомного анализа:** - -``` -=== RATIO METRICS АНАЛИЗ === -Control группа: 1247 покупок / 4520 пользователей = 27.6% конверсия -Test группа: 1456 покупок / 4580 пользователей = 31.8% конверсия -Difference: +4.2 п.п. (relative lift: +15.2%) -Z-statistic: 3.24, p-value = 0.001 (высоко значимо) -95% CI для разности: [1.7%, 6.7%] -Интерпретация: Конверсия выше в тестовой группе на 15.2% (улучшение) - -=== VALIDATION === -T-test на revenue per user: p-value = 0.018 (согласуется с ratio тестом) ``` -**Почему потребовался уровень расширения:** - -- Ratio метрики требуют специального подхода (delta method для SE) -- Стандартные тесты дают некорректные результаты для отношений -- Нужна доменная экспертиза для правильной статистической модели -- Требуется кастомная интерпретация результатов (relative lift) +**Архитектурные преимущества:** -**Преимущества архитектурного подхода:** +- **Интеграция**: Кастомный Executor работает как стандартный +- **Переиспользуемость**: Может использоваться в любых экспериментах +- **Тестируемость**: Легко тестировать изолированно +- **Расширяемость**: Базовый класс предоставляет всю инфраструктуру -- Новый Calculator легко интегрируется в существующие Experiment'ы -- Следует всем конвенциям библиотеки (единый интерфейс, ExperimentData) -- Можно комбинировать с стандартными компонентами для validation -- Переиспользуется в других экспериментах с ratio метриками +### Сценарий 4: Комплексный многоэтапный анализ -### Сценарий 4: Сложный многоэтапный matching анализ (Комбинация уровней) - -**Бизнес-задача:** Аналитическая команда исследует эффект маркетинговой кампании на retention пользователей. Данные -observational (без рандомизации), поэтому нужен sophisticated matching анализ с проверкой качества и sensitivity -анализом. +**Бизнес-задача:** Исследовательская команда изучает эффект нового алгоритма рекомендаций на удержание пользователей. +Требуется полный цикл: от A/A валидации данных до matching анализа с sensitivity проверками. **Характеристики задачи:** -- Многоэтапный процесс: подготовка → matching → валидация → анализ -- Комбинация готовых решений и кастомных компонентов -- Требования к quality assurance на каждом этапе -- Нужна детальная диагностика и отчетность +- Многоэтапный процесс анализа +- Комбинация разных типов экспериментов +- Сложная логика принятия решений +- Потребность в детальной диагностике -**Решение — комбинация Shell + Композиция + Расширение:** +**Решение — комбинация всех уровней архитектуры:** ```python -# Этап 1: Быстрая оценка с помощью Shell (базовый matching) -initial_matching = Matching( - distance="mahalanobis", - bias_estimation=True, - quality_tests=["ttest", "kstest"] -) +# Этап 1: Валидация качества данных (Shell уровень) +aa_validation = AATest(precision_mode=True, n_iterations=500) +aa_results = aa_validation.execute(dataset) -initial_results = initial_matching.execute(dataset) -print(f"Базовое качество matching: {initial_results.quality_results}") +if aa_results.aa_score.quality_level != "excellent": + print("Предупреждение: качество разбиения недостаточно высокое") + # Дополнительная диагностика... + +# Этап 2: Предварительный A/B тест (Композиция) +preliminary_ab = Experiment([ + GroupSizes(), GroupDifference(), + OnRoleExperiment([TTest(), KSTest(), PermutationTest()], role=TargetRole()), + ABAnalyzer(multitest_method="fdr_bh") +]) -# Этап 2: Если качество неудовлетворительное - детальная настройка -if initial_results.quality_results['overall_quality'] < 0.8: +preliminary_results = preliminary_ab.execute(ExperimentData(dataset)) - # Кастомный preprocessor для улучшения matching - class AdvancedPreprocessor(Transformer): - """Продвинутая предобработка для улучшения quality matching'а""" +# Этап 3: Решение о дальнейшем анализе +ab_analyzer_id = preliminary_results.get_ids(ABAnalyzer)[0] +ab_summary = preliminary_results.analysis_tables[ab_analyzer_id] - def _inner_function(self, data: Dataset) -> Dataset: - # Логарифмирование скошенных переменных - log_features = ['income', 'session_duration'] - for feature in log_features: - data[f'{feature}_log'] = np.log1p(data[feature]) +if ab_summary["overall_decision"] in ["ship", "monitor"]: + print("Обнаружен эффект. Переходим к matching анализу для causal inference.") + + # Этап 4: Matching анализ с кастомными компонентами + matching_analysis = Experiment([ + # Подготовка данных + OutliersFilter(lower_percentile=0.05, upper_percentile=0.95), + NaFiller(method="ffill"), + DummyEncoder(), - # Создание interaction features - data['age_income_interaction'] = data['age'] * data['income'] + # Вычисление расстояний + MahalanobisDistance(grouping_role=TreatmentRole()), - # Binning категориальных переменных с малыми группами - rare_categories = data['device_type'].value_counts() - data['device_type_grouped'] = data['device_type'].map( - lambda x: x if rare_categories[x] > 50 else 'other' - ) + # Поиск пар + FaissNearestNeighbors(n_neighbors=1), - return data + # Оценка качества + Bias(grouping_role=TreatmentRole()), + MatchingMetrics(metric="ate"), + # Статистические тесты + TTest(compare_by="groups"), + KSTest(compare_by="groups"), - # Кастомный качественный анализ + # Анализ результатов + MatchingAnalyzer() + ]) + + matching_results = matching_analysis.execute(ExperimentData(dataset)) + + # Этап 5: Sensitivity анализ (Расширение) class SensitivityAnalyzer(Executor): - """Sensitivity анализ для оценки робастности результатов""" - + """Анализ чувствительности результатов к различным параметрам matching.""" + def execute(self, data: ExperimentData) -> ExperimentData: - matched_data = data.additional_fields['matched_pairs'] - - # Анализ с разными distance thresholds - thresholds = [0.1, 0.2, 0.3, 0.4, 0.5] + # Тестируем разные пороги matching sensitivity_results = [] - - for threshold in thresholds: - # Фильтруем пары по качеству matching - high_quality_pairs = matched_data[ - matched_data['match_distance'] <= threshold - ] - - # Вычисляем эффект на filtered данных - effect = self._calculate_treatment_effect(high_quality_pairs) - + + for threshold in [0.1, 0.2, 0.3, 0.4, 0.5]: + # Повторяем matching с разными порогами + threshold_experiment = Experiment([ + FaissNearestNeighbors(threshold=threshold), + MatchingMetrics(metric="ate"), + TTest(compare_by="groups") + ]) + + threshold_data = ExperimentData(data.ds) + threshold_result = threshold_experiment.execute(threshold_data) + + # Извлекаем ключевые метрики + metrics_id = threshold_result.get_ids(MatchingMetrics)[0] + ate_estimate = threshold_result.analysis_tables[metrics_id]["ate"] + sensitivity_results.append({ - 'threshold': threshold, - 'n_pairs': len(high_quality_pairs), - 'treatment_effect': effect, - 'coverage': len(high_quality_pairs) / len(matched_data) + "threshold": threshold, + "ate_estimate": ate_estimate, + "n_matched": len(threshold_result.additional_fields.get("matched_indices", [])) }) - + # Сохраняем результаты sensitivity анализа - sensitivity_dataset = Dataset(data=pd.DataFrame(sensitivity_results)) + sensitivity_dataset = Dataset.from_records(sensitivity_results) return data.set_value( space=ExperimentDataEnum.analysis_tables, executor_id=self.id, value=sensitivity_dataset ) - - - # Композиция продвинутого matching pipeline - advanced_matching = Experiment([ - # Этап 1: Продвинутая предобработка - AdvancedPreprocessor(), - OutliersFilter(method="isolation_forest"), - NaFiller(method="knn"), - - # Этап 2: Matching с несколькими алгоритмами - MahalanobisDistance(grouping_role=TreatmentRole()), - FaissNearestNeighbors(n_neighbors=3), # Больше кандидатов - - # Этап 3: Выбор лучших пар - MatchingQualityFilter(min_quality_score=0.8), - - # Этап 4: Анализ качества - Bias(grouping_role=TreatmentRole()), - SMD(grouping_role=TreatmentRole()), # Standardized Mean Difference - - # Этап 5: Основной анализ - OnRoleExperiment([ - TTest(grouping_role=TreatmentRole()), - KSTest(grouping_role=TreatmentRole()), - ], role=TargetRole()), - - # Этап 6: Sensitivity анализ - SensitivityAnalyzer(), - - # Этап 7: Итоговый анализ - MatchingAnalyzer() - ]) - - # Выполнение продвинутого анализа - final_results = advanced_matching.execute(ExperimentData(dataset)) - - -# Этап 3: Детальная отчетность с кастомным Reporter -class ComprehensiveMatchingReporter(DictReporter): - """Подробный отчет по всем этапам matching анализа""" - - def report(self, data: ExperimentData) -> dict: - base_results = super().report(data) - - # Добавляем sensitivity анализ - sensitivity_data = data.analysis_tables.get('SensitivityAnalyzer╤╤') - if sensitivity_data is not None: + + # Запуск sensitivity анализа + sensitivity_analyzer = SensitivityAnalyzer() + final_results = sensitivity_analyzer.execute(matching_results) + + # Этап 6: Comprehensive отчетность + class ComprehensiveMatchingReporter(DictReporter): + """Comprehensive reporter для всего анализа.""" + + def report(self, data: ExperimentData) -> dict: + base_results = super().report(data) + + # Добавляем результаты sensitivity анализа + sensitivity_id = data.get_ids(SensitivityAnalyzer)[0] + sensitivity_data = data.analysis_tables[sensitivity_id] + base_results['sensitivity_analysis'] = { 'stability_assessment': self._assess_stability(sensitivity_data), 'robust_effect_estimate': self._robust_estimate(sensitivity_data), 'recommended_threshold': self._recommend_threshold(sensitivity_data) } - # Добавляем качественную оценку - base_results['quality_assessment'] = self._comprehensive_quality_check(data) + # Добавляем качественную оценку + base_results['quality_assessment'] = self._comprehensive_quality_check(data) - return base_results + return base_results + + def _assess_stability(self, data: Dataset) -> str: + # Логика оценки стабильности результатов + estimates = data.df["ate_estimate"].values + cv = np.std(estimates) / np.mean(estimates) # Coefficient of variation + + if cv < 0.1: + return "highly_stable" + elif cv < 0.2: + return "stable" + else: + return "unstable" + + def _robust_estimate(self, data: Dataset) -> float: + # Робастная оценка (медиана) + return data.df["ate_estimate"].median() + + def _recommend_threshold(self, data: Dataset) -> float: + # Рекомендуемый порог (баланс качества и покрытия) + data_df = data.df + # Простая эвристика: максимизируем произведение покрытия и обратной дисперсии + coverage = data_df["n_matched"] / data_df["n_matched"].max() + stability = 1 / (data_df["ate_estimate"].rolling(3).std().fillna(1)) + score = coverage * stability + + return data_df.loc[score.idxmax(), "threshold"] + + def _comprehensive_quality_check(self, data: ExperimentData) -> str: + # Комплексная оценка качества всего анализа + # ... логика проверки всех компонентов + return "EXCELLENT" # Упрощено + comprehensive_reporter = ComprehensiveMatchingReporter() + final_report = comprehensive_reporter.report(final_results) -comprehensive_reporter = ComprehensiveMatchingReporter() -final_report = comprehensive_reporter.report(final_results) +else: + print("Эффект не обнаружен или слишком слаб для matching анализа.") ``` **Результат многоэтапного анализа:** @@ -3769,105 +2716,178 @@ advanced_experiment = Experiment([ **Ключевой инсайт:** Архитектура HypEx позволяет начать с простого решения и органично наращивать сложность без переписывания кода. Каждый уровень строится на предыдущем, добавляя необходимую функциональность. -### Паттерны выбора подхода +### Руководство по выбору подходящего уровня -#### Матрица принятия решений +#### Принципы принятия решений -| Критерий | Shell (Уровень 4) | Композиция (Уровень 5) | Расширение (Уровень 6) | -|-------------------------------|------------------------------|--------------------------|------------------------| -| **Стандартность задачи** | Типовая (A/B, A/A, Matching) | Нестандартная комбинация | Уникальная методология | -| **Экспертиза команды** | Базовая | Средняя | Высокая | -| **Временные рамки** | Срочно (часы) | Умеренно (дни) | Не критично (недели) | -| **Требования к кастомизации** | Минимальные | Средние | Максимальные | -| **Частота использования** | Разовая или регулярная | Регулярная | Проектная | +**Используйте Shell (Уровень 4), когда:** -#### Типичные сигналы для перехода на следующий уровень +- Задача полностью покрывается стандартным экспериментом +- Команда не имеет глубокой экспертизы в статистике +- Нужен быстрый и надежный результат +- Требуется production-ready решение -**Shell → Композиция:** +**Переходите на Композицию (Уровень 5), когда:** -- "Нужно добавить еще один тест" -- "Хотим изменить порядок выполнения" -- "Требуется нестандартная комбинация методов" -- "Shell делает не совсем то, что нужно" +- Нужны дополнительные тесты или метрики +- Требуется нестандартная последовательность операций +- Необходимо объединить несколько типовых анализов +- Хотите больше контроля над процессом -**Композиция → Расширение:** +**Создавайте Расширения (Уровень 6), когда:** -- "Нужного Executor'а нет в библиотеке" -- "Требуется интеграция с внешней библиотекой" -- "Нужна доменно-специфическая логика" -- "Стандартные методы не подходят для наших данных" +- Требуется функциональность, отсутствующая в библиотеке +- У команды есть экспертиза для создания кастомных методов +- Планируется многократное переиспользование +- Нужна интеграция с внешними библиотеками -#### Антипаттерны и их решения +#### Критерии качественного решения -**Антипаттерн 1: Преждевременная оптимизация** +HypEx позволяет органично развивать решение по мере роста требований. -```python -# Плохо: сразу создавать сложный custom Executor -class ComplexCustomAnalyzer(Executor): +**Правило 90-9-1:** +- 90% задач решаются Shell'ами (уровень 4) +- 9% требуют композиции стандартных блоков (уровень 5) +- 1% нуждается в создании новой функциональности (уровень 6) -# 200 строк сложной логики для простой задачи +**Критерии качественного решения:** -# Хорошо: начать с Shell и усложнять по необходимости -ab_test = ABTest() # Сначала проверить, что базовое решение не подходит -``` +1. **Простота:** Используйте минимально необходимый уровень сложности +2. **Переиспользование:** Предпочитайте стандартные компоненты кастомным +3. **Модульность:** Разбивайте сложную логику на композируемые части +4. **Тестируемость:** Каждый компонент должен быть независимо тестируемым +5. **Документированность:** Кастомные компоненты требуют подробного описания -**Антипаттерн 2: Игнорирование стандартных решений** +Архитектура HypEx спроектирована так, чтобы естественным образом направлять разработчиков к качественным решениям, +предоставляя правильные абстракции для каждого уровня сложности. -```python -# Плохо: переизобретать велосипед -custom_experiment = Experiment([ - ManualGroupSplit(), # Вместо стандартного AASplitter - ManualTTest(), # Вместо стандартного TTest - ManualReporting() # Вместо стандартных Reporter'ов -]) +## Заключение -# Хорошо: использовать стандартные блоки где возможно -experiment = Experiment([ - AASplitter(), # Стандартный, надежный, протестированный - OnRoleExperiment([TTest()], role=TargetRole()), - CustomSpecificAnalyzer() # Только то, что действительно уникально -]) +### Ключевые архитектурные достижения + +Архитектура HypEx успешно решает фундаментальную проблему разрыва между простотой использования и гибкостью статистических инструментов. Ключевые достижения: + +**1. Многоуровневая абстракция как архитектурный принцип** + +Система 8 уровней абстракции позволяет пользователям работать на комфортном уровне сложности, при этом сохраняя возможность перехода на более детальные уровни по мере роста потребностей. Это обеспечивает: +- Низкий порог входа для начинающих +- Неограниченную гибкость для экспертов +- Естественную эволюцию решений + +**2. Композиция как основа расширяемости** + +Принцип "композиция над наследованием" реализован последовательно на всех уровнях: +- Executor'ы как атомарные операции +- Experiment'ы как оркестраторы +- Shell'ы как готовые решения +- Каждый компонент может быть заменен или дополнен + +**3. Единый поток данных через ExperimentData** + +ExperimentData служит универсальной шиной данных, обеспечивая: +- Прозрачность всех промежуточных результатов +- Возможность отладки на любом этапе +- Простую интеграцию новых компонентов +- Воспроизводимость экспериментов + +### Принципы, выдержавшие проверку практикой + +**Разделение ответственности** + +Четкое разделение между вычислениями (Calculator'ы), оркестрацией (Experiment'ы), анализом (Analyzer'ы) и представлением (Reporter'ы) обеспечивает: +- Простоту тестирования +- Легкость модификации +- Возможность независимого развития компонентов + +**Backend-агностичность через Extension Framework** + +Разделение бизнес-логики и технических деталей реализации позволяет: +- Использовать оптимальные инструменты для каждого типа данных +- Добавлять новые backend'ы без изменения core логики +- Изолировать внешние зависимости + +**Кураторский подход к результатам** + +Система Output'ов и Reporter'ов превращает технические результаты в business-ready решения: +- 90% пользователей получают готовые выводы +- 10% экспертов имеют доступ к детальным данным +- Консистентная интерпретация результатов + +### Архитектурные паттерны HypEx + +**Паттерн "Эволюционного усложнения"** + +Возможность начать с простого Shell'а и постепенно добавлять сложность: +``` +Shell → Композиция → Расширение → Модификация ``` -**Антипаттерн 3: Монолитные кастомные решения** +**Паттерн "Декомпозиции сложности"** -```python -# Плохо: один большой Executor делает все -class MonolithicAnalyzer(Executor): - def execute(self, data): -# Предобработка + анализ + отчетность в одном классе -# Трудно тестировать, переиспользовать, поддерживать +Разбиение сложных операций на цепочки простых: +``` +Сложный анализ = Preprocessing + Tests + Analysis + Reporting +``` +**Паттерн "Полиморфной замещаемости"** -# Хорошо: разбить на композируемые части -experiment = Experiment([ - CustomPreprocessor(), # Одна ответственность - StandardAnalysis(), # Переиспользуемый компонент - CustomReporter() # Отдельный форматтер -]) +Любой компонент может быть заменен на альтернативную реализацию: +``` +TTest ← → PermutationTest ← → BootstrapTest ``` -### Выводы и рекомендации +### Практические выводы для разработчиков -**Принцип прогрессивного усложнения:** -Всегда начинайте с самого простого решения, которое решает задачу. HypEx позволяет органично развивать решение по мере -роста требований. +**Выбор правильного уровня абстракции** -**Правило 90-9-1:** +- Начинайте с Shell'ов для стандартных задач +- Переходите к композиции при нестандартных требованиях +- Создавайте расширения только при необходимости многократного использования -- 90% задач решаются Shell'ами (уровень 4) -- 9% требуют композиции стандартных блоков (уровень 5) -- 1% нуждается в создании новой функциональности (уровень 6) +**Принципы качественного кода в HypEx** -**Критерии качественного решения:** +1. **Следуйте правилу 90-9-1**: большинство задач должно решаться стандартными инструментами +2. **Предпочитайте композицию наследованию**: комбинируйте готовые блоки +3. **Тестируйте на уровне компонентов**: каждый Executor должен быть протестирован изолированно +4. **Документируйте кастомные решения**: нестандартные компоненты требуют подробного описания -1. **Простота:** Используйте минимально необходимый уровень сложности -2. **Переиспользование:** Предпочитайте стандартные компоненты кастомным -3. **Модульность:** Разбивайте сложную логику на композируемые части -4. **Тестируемость:** Каждый компонент должен быть независимо тестируемым -5. **Документированность:** Кастомные компоненты требуют подробного описания +**Архитектурные anti-patterns** -Архитектура HypEx спроектирована так, чтобы естественным образом направлять разработчиков к качественным решениям, -предоставляя правильные абстракции для каждого уровня сложности. +- Создание monolithic Executor'ов, выполняющих множество задач +- Прямые зависимости между Executor'ами +- Обход системы ролей при работе с данными +- Игнорирование Extension Framework при работе с внешними библиотеками + +### Направления развития архитектуры + +**Масштабируемость** + +- Поддержка распределенных вычислений через новые backend'ы +- Оптимизация для больших данных +- Асинхронное выполнение Executor'ов + +**Интеграция** + +- Расширение Extension Framework для новых ML библиотек +- Интеграция с workflow системами (Airflow, Prefect) +- API для интеграции с внешними системами + +**Пользовательский опыт** + +- Интеллектуальные рекомендации по выбору методов +- Автоматическая диагностика проблем в данных +- Интерактивные визуализации результатов + +### Философия архитектурных решений + +HypEx демонстрирует, что хорошая архитектура должна быть: + +**Приспособляемой** — легко адаптироваться к изменяющимся требованиям +**Интуитивной** — естественно направлять пользователей к правильным решениям +**Прозрачной** — позволять понимать и контролировать происходящие процессы +**Расширяемой** — предоставлять четкие точки для добавления новой функциональности + +Архитектура HypEx служит примером того, как сложная предметная область (статистический анализ) может быть организована в интуитивно понятную и мощную систему через правильное применение принципов объектно-ориентированного проектирования и многоуровневой абстракции. +Успех HypEx в решении задач статистического анализа демонстрирует универсальность этих архитектурных принципов и их применимость к другим сложным предметным областям. From 5001a252ae72e61aa93f6d3ec6d74f920c0ced7e Mon Sep 17 00:00:00 2001 From: dmbulychev Date: Sun, 21 Sep 2025 13:21:48 +0300 Subject: [PATCH 15/17] Add toc --- schemes/anatomy.md | 159 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/schemes/anatomy.md b/schemes/anatomy.md index 1ffb5ca0..f4054a29 100644 --- a/schemes/anatomy.md +++ b/schemes/anatomy.md @@ -1,5 +1,164 @@ # Архитектура HypEx: Подробное руководство +## 📑 Оглавление + +### 🎯 Быстрая навигация +- **Для новичков:** [1. Введение и философия](#введение-и-философия) → [10. Shell слой — готовые решения](#shell-слой-готовые-решения) +- **Для разработчиков:** [4. Ядро системы: Executor Framework](#ядро-системы-executor-framework) → [8. Слой экспериментов: Experiment Framework](#слой-экспериментов-experiment-framework) +- **Для архитекторов:** [2. Обзор архитектуры](#обзор-архитектуры) → [Заключение](#заключение) + +### 📋 Подробное содержание + +- [🎯 1. Введение и философия](#введение-и-философия) + - [Общая концепция библиотеки](#общая-концепция-библиотеки) + - [Принцип многоуровневой абстракции](#принцип-многоуровневой-абстракции) + - [Основные архитектурные принципы](#основные-архитектурные-принципы) +- [🏗️ 2. Обзор архитектуры](#обзор-архитектуры) + - [Три основных слоя](#три-основных-слоя) + - [Слой Shell (Пользовательский интерфейс)](#слой-shell-пользовательский-интерфейс) + - [Слой Experiments (Оркестрация)](#слой-experiments-оркестрация) + - [Слой Executors/Reporters (Исполнение и отчетность)](#слой-executorsreporters-исполнение-и-отчетность) + - [Взаимодействие между слоями](#взаимодействие-между-слоями) + - [Поток данных через систему](#поток-данных-через-систему) +- [📊 3. Структуры данных: Dataset и ExperimentData](#структуры-данных-dataset-и-experimentdata) + - [Dataset: Универсальный контейнер данных](#dataset-универсальный-контейнер-данных) + - [Архитектура Dataset](#архитектура-dataset) + - [Система ролей (ABCRole)](#система-ролей-abcrole) + - [Основные операции Dataset](#основные-операции-dataset) + - [ExperimentData: Контекст эксперимента](#experimentdata-контекст-эксперимента) + - [Архитектура ExperimentData](#архитектура-experimentdata) + - [Основные операции ExperimentData](#основные-операции-experimentdata) + - [Принципы работы с данными](#принципы-работы-с-данными) +- [⚙️ 4. Ядро системы: Executor Framework](#ядро-системы-executor-framework) + - [Концепция Executor](#концепция-executor) + - [Базовый класс Executor](#базовый-класс-executor) + - [Три ветви наследования](#три-ветви-наследования) + - [1. Calculator — вычислительная ветвь](#calculator-вычислительная-ветвь) + - [2. IfExecutor — условная ветвь](#ifexecutor-условная-ветвь) + - [3. MLExecutor — машинное обучение ветвь](#mlexecutor-машинное-обучение-ветвь) + - [Система ID](#система-id) + - [Жизненный цикл Executor](#жизненный-цикл-executor) + - [Принципы проектирования Executor'ов](#принципы-проектирования-executorов) + - [Расширение через наследование](#расширение-через-наследование) +- [5. Extension Framework](#extension-framework) + - [Концепция Extension'ов](#концепция-extensionов) + - [Ключевое архитектурное разделение](#ключевое-архитектурное-разделение) + - [Архитектура Extension'ов](#архитектура-extensionов) + - [Примеры Extension'ов](#примеры-extensionов) + - [StatisticalExtension — статистические вычисления](#statisticalextension-статистические-вычисления) + - [MLExtension — машинное обучение](#mlextension-машинное-обучение) + - [Интеграция с Calculator'ами](#интеграция-с-calculatorами) + - [Жизненный цикл Extension'ов](#жизненный-цикл-extensionов) + - [1. Инициализация и выбор backend'а](#инициализация-и-выбор-backendа) + - [2. Изоляция зависимостей](#изоляция-зависимостей) + - [Преимущества Extension Framework](#преимущества-extension-framework) + - [1. Архитектурная чистота](#архитектурная-чистота) + - [2. Производительность и масштабируемость](#производительность-и-масштабируемость) + - [3. Гибкость и расширяемость](#гибкость-и-расширяемость) +- [6. Слой вычислений: Comparators, Transformers, Operators](#слой-вычислений-comparators-transformers-operators) + - [Comparators: Сравнение и тестирование](#comparators-сравнение-и-тестирование) + - [Иерархия Comparators](#иерархия-comparators) + - [Система ролей в Comparators](#система-ролей-в-comparators) + - [Примеры Comparators](#примеры-comparators) + - [Transformers: Преобразование данных](#transformers-преобразование-данных) + - [Архитектура Transformers](#архитектура-transformers) + - [Примеры Transformers](#примеры-transformers) + - [Encoders: Кодирование признаков](#encoders-кодирование-признаков) + - [GroupOperators: Операции над группами](#groupoperators-операции-над-группами) + - [Примеры GroupOperators](#примеры-groupoperators) + - [Принципы проектирования слоя вычислений](#принципы-проектирования-слоя-вычислений) +- [7. Analyzer'ы — комплексный анализ результатов](#analyzerы-комплексный-анализ-результатов) + - [Архитектура Analyzer'ов](#архитектура-analyzerов) + - [OneAAStatAnalyzer — анализ одного A/A теста](#oneaastatanalyzer-анализ-одного-aa-теста) + - [AAScoreAnalyzer — выбор лучшего разбиения](#aascoreanalyzer-выбор-лучшего-разбиения) + - [ABAnalyzer — анализ A/B теста](#abanalyzer-анализ-ab-теста) + - [MatchingAnalyzer — оценка качества matching](#matchinganalyzer-оценка-качества-matching) + - [Паттерны использования Analyzer'ов](#паттерны-использования-analyzerов) + - [1. Последовательный анализ](#последовательный-анализ) + - [2. Иерархический анализ](#иерархический-анализ) + - [3. Условный анализ](#условный-анализ) + - [Создание кастомных Analyzer'ов](#создание-кастомных-analyzerов) + - [Преимущества Analyzer'ов](#преимущества-analyzerов) +- [🔬 8. Слой экспериментов: Experiment Framework](#слой-экспериментов-experiment-framework) + - [Базовый класс Experiment](#базовый-класс-experiment) + - [Специализированные эксперименты](#специализированные-эксперименты) + - [OnRoleExperiment — применение по ролям](#onroleexperiment-применение-по-ролям) + - [GroupExperiment — обработка по группам](#groupexperiment-обработка-по-группам) + - [ParamsExperiment — параметрический поиск](#paramsexperiment-параметрический-поиск) + - [IfParamsExperiment — параметрический поиск с условием остановки](#ifparamsexperiment-параметрический-поиск-с-условием-остановки) + - [Композиция экспериментов](#композиция-экспериментов) + - [Жизненный цикл Experiment](#жизненный-цикл-experiment) + - [Паттерны использования](#паттерны-использования) + - [1. Pipeline паттерн — последовательная обработка](#pipeline-паттерн-последовательная-обработка) + - [2. Branching паттерн — условное ветвление](#branching-паттерн-условное-ветвление) + - [3. Fan-out/Fan-in паттерн — параллельные анализы](#fan-outfan-in-паттерн-параллельные-анализы) + - [4. Grid Search паттерн — поиск оптимальных параметров](#grid-search-паттерн-поиск-оптимальных-параметров) + - [5. Hierarchical паттерн — иерархическая обработка](#hierarchical-паттерн-иерархическая-обработка) + - [Композиция и переиспользуемость](#композиция-и-переиспользуемость) +- [📋 9. Система Reporter'ов](#система-reporterов) + - [Архитектура Reporter'ов](#архитектура-reporterов) + - [Базовый класс Reporter](#базовый-класс-reporter) + - [DictReporter — универсальная основа](#dictreporter-универсальная-основа) + - [Концепция DictReporter](#концепция-dictreporter) + - [Методы извлечения в DictReporter](#методы-извлечения-в-dictreporter) + - [OnDictReporter — универсальный форматтер](#ondictreporter-универсальный-форматтер) + - [Архитектура форматирования](#архитектура-форматирования) + - [Примеры OnDictReporter](#примеры-ondictreporter) + - [Специализированные Reporter'ы](#специализированные-reporterы) + - [ABDictReporter — A/B тестирование](#abdictreporter-ab-тестирование) + - [MatchingDictReporter — Matching анализ](#matchingdictreporter-matching-анализ) + - [Принципы проектирования Reporter'ов](#принципы-проектирования-reporterов) + - [Преимущества архитектуры Reporter'ов](#преимущества-архитектуры-reporterов) +- [🚀 10. Shell слой — готовые решения](#shell-слой-готовые-решения) + - [Концепция Shell слоя](#концепция-shell-слоя) + - [Двухкомпонентная архитектура Shell слоя](#двухкомпонентная-архитектура-shell-слоя) + - [ExperimentShell — конструктор экспериментов](#experimentshell-конструктор-экспериментов) + - [Output — система форматированного представления](#output-система-форматированного-представления) + - [Готовые эксперименты](#готовые-эксперименты) + - [AATest — автоматизированное A/A тестирование](#aatest-автоматизированное-aa-тестирование) + - [ABTest — A/B тестирование с коррекцией](#abtest-ab-тестирование-с-коррекцией) + - [HomogeneityTest — проверка однородности групп](#homogeneitytest-проверка-однородности-групп) + - [Matching — анализ сопоставления](#matching-анализ-сопоставления) + - [Система Output'ов](#система-outputов) + - [AAOutput — результаты A/A тестирования](#aaoutput-результаты-aa-тестирования) + - [ABOutput — результаты A/B тестирования](#aboutput-результаты-ab-тестирования) + - [HomoOutput — результаты проверки однородности](#homooutput-результаты-проверки-однородности) + - [MatchingOutput — результаты matching анализа](#matchingoutput-результаты-matching-анализа) + - [Доступ к детальным результатам](#доступ-к-детальным-результатам) + - [Расширяемость и конфигурирование](#расширяемость-и-конфигурирование) + - [Принципы расширяемости Shell'ов](#принципы-расширяемости-shellов) + - [Расширяемость Output'ов](#расширяемость-outputов) + - [Практические примеры использования](#практические-примеры-использования) + - [Минимальный код для типовых сценариев](#минимальный-код-для-типовых-сценариев) + - [Конфигурация под специфические требования](#конфигурация-под-специфические-требования) + - [Философия типовых решений](#философия-типовых-решений) + - [Принцип "90% в 2 строчки"](#принцип-90-в-2-строчки) + - [Встроенные лучшие практики](#встроенные-лучшие-практики) + - [Границы применимости и расширения](#границы-применимости-и-расширения) +- [11. Практические примеры и сценарии](#практические-примеры-и-сценарии) + - [Философия практических примеров](#философия-практических-примеров) + - [Сценарий 1: Стандартный A/B тест (Уровень 4 — Shell)](#сценарий-1-стандартный-ab-тест-уровень-4-shell) + - [Сценарий 2: A/B тест с дополнительными требованиями (Уровень 5 — Композиция)](#сценарий-2-ab-тест-с-дополнительными-требованиями-уровень-5-композиция) + - [Сценарий 3: Создание кастомного Executor (Уровень 6 — Расширение)](#сценарий-3-создание-кастомного-executor-уровень-6-расширение) + - [Сценарий 4: Комплексный многоэтапный анализ](#сценарий-4-комплексный-многоэтапный-анализ) + - [Эволюция решения: От простого к сложному](#эволюция-решения-от-простого-к-сложному) + - [Этап 1: Первая итерация (Shell)](#этап-1-первая-итерация-shell) + - [Этап 2: Углубленный анализ (Композиция)](#этап-2-углубленный-анализ-композиция) + - [Этап 3: Продвинутая статистика (Расширение)](#этап-3-продвинутая-статистика-расширение) + - [Руководство по выбору подходящего уровня](#руководство-по-выбору-подходящего-уровня) + - [Принципы принятия решений](#принципы-принятия-решений) + - [Критерии качественного решения](#критерии-качественного-решения) +- [🎉 Заключение](#заключение) + - [Ключевые архитектурные достижения](#ключевые-архитектурные-достижения) + - [Принципы, выдержавшие проверку практикой](#принципы-выдержавшие-проверку-практикой) + - [Архитектурные паттерны HypEx](#архитектурные-паттерны-hypex) + - [Практические выводы для разработчиков](#практические-выводы-для-разработчиков) + - [Направления развития архитектуры](#направления-развития-архитектуры) + - [Философия архитектурных решений](#философия-архитектурных-решений) + +--- + + ## 1. Введение и философия ### Общая концепция библиотеки From 05fb12afc7ad8379221aabf4ae832040b1b588e9 Mon Sep 17 00:00:00 2001 From: dmbulychev Date: Sun, 21 Sep 2025 14:18:20 +0300 Subject: [PATCH 16/17] Add part of presentation --- .gitignore | 1 - schemes/hypex_architecture_presentation.html | 1486 ++++++++++++++++++ 2 files changed, 1486 insertions(+), 1 deletion(-) create mode 100644 schemes/hypex_architecture_presentation.html diff --git a/.gitignore b/.gitignore index 0cbc3b52..0c1c5948 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ *.csv *.png *.pickle -*.html # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/schemes/hypex_architecture_presentation.html b/schemes/hypex_architecture_presentation.html new file mode 100644 index 00000000..392d3e8d --- /dev/null +++ b/schemes/hypex_architecture_presentation.html @@ -0,0 +1,1486 @@ + + + + + + Архитектура HypEx + + + +
+
+ 1 / 67 +
+ + +
+

Архитектура HypEx

+

Модульная библиотека для статистического анализа и A/B тестирования

+
+
Executor Pattern
+
Type-Safe Data
+
Composable Experiments
+
+
+ + +
+

Что такое HypEx

+
    +
  • Библиотека для статистического анализа экспериментов
  • +
  • Модульная архитектура на основе Executor pattern
  • +
  • Типобезопасность через систему ролей данных
  • +
  • Композиция вместо наследования
  • +
  • Переиспользование компонентов
  • +
+
+ + +
+

Проблемы которые решает HypEx

+
    +
  • Сложность настройки статистических тестов
  • +
  • Дублирование кода при анализе экспериментов
  • +
  • Ошибки типов в данных и параметрах
  • +
  • Несовместимость различных подходов к анализу
  • +
  • Сложность масштабирования аналитических пайплайнов
  • +
+
+ + +
+

Домены применения

+
+
+

A/B Testing

+

Сравнение групп пользователей, статистические тесты, анализ эффекта

+
+
+

A/A Testing

+

Проверка качества разбиения, валидация экспериментальной системы

+
+
+

Matching

+

Поиск похожих объектов, анализ парных наблюдений

+
+
+

Homogeneity

+

Тестирование однородности групп, проверка предпосылок

+
+
+
+ + +
+

Обзор архитектуры

+
+
Dataset
+
+
Executor
+
+
Experiment
+
+
Results
+
+

Данные проходят через цепочку исполнителей, объединенных в эксперименты

+
+ + +
+

Философия: Модульность

+
    +
  • Мелкие компоненты с четкой ответственностью
  • +
  • Единый интерфейс для всех исполнителей
  • +
  • Простота тестирования отдельных компонентов
  • +
  • Легкость замены и расширения
  • +
+
+class Executor(ABC): + def execute(self, data: ExperimentData) -> ExperimentData: + # Единый интерфейс для всех компонентов + pass +
+
+ + +
+

Композиция вместо наследования

+
    +
  • Гибкость в построении пайплайнов
  • +
  • Переиспользование готовых компонентов
  • +
  • Избежание глубоких иерархий наследования
  • +
  • Динамическая настройка поведения
  • +
+
+experiment = Experiment([ + AASplitter(), + TTest(compare_by="groups"), + AAScoreAnalyzer() +]) +
+
+ + +
+

Типобезопасность через роли

+
    +
  • ABCRole система для описания назначения данных
  • +
  • Автоматическая валидация совместимости
  • +
  • Предотвращение ошибок на этапе выполнения
  • +
  • Самодокументирующийся код
  • +
+
+roles = { + 'user_id': TreatmentRole(), + 'revenue': TargetRole(), + 'age': FeatureRole() +} +
+
+ + +
+

Единообразие интерфейсов

+
    +
  • Один метод execute() для всех компонентов
  • +
  • Стандартные паттерны конфигурации
  • +
  • Предсказуемое поведение компонентов
  • +
  • Низкий порог входа для новых разработчиков
  • +
+
+ + +
+

Принципы расширяемости

+
    +
  • Extension точки для интеграции библиотек
  • +
  • Pluggable архитектура для новых алгоритмов
  • +
  • Минимальные зависимости в базовом функционале
  • +
  • Обратная совместимость API
  • +
+
+ + +
+

Уровень 1: Пользовательский интерфейс

+
+

Пользователь: Бизнес пользователь

+

Сегмент: Платформа

+

Идея: Работа без знания статистики и кода

+

Использование: Выбор и запуск готовых сценариев

+
+

Простой интерфейс для выбора типа анализа и получения результатов

+
+ + +
+

Уровень 2: Конструктор сценариев

+
+

Пользователь: Финансист/Аналитик

+

Сегмент: Платформа

+

Идея: Создание сценариев без программирования

+

Использование: Графический конструктор пайплайнов

+
+

Drag & drop интерфейс для составления аналитических сценариев

+
+ + +
+

Уровень 3: Настраиваемый шаблон

+
+

Пользователь: Бизнес пользователь с доступом к данным

+

Сегмент: Dev пакет

+

Идея: Готовый код с настраиваемыми параметрами

+

Использование: Запуск шаблона в лабораторной зоне

+
+
+# Настраиваемые параметры +CONTROL_SIZE = 0.5 +RANDOM_STATE = 42 +
+
+ + +
+

Уровень 4: Оболочка эксперимента

+
+

Пользователь: Исследователь данных

+

Сегмент: Библиотека HypEx

+

Идея: Готовые эксперименты в несколько строк

+

Использование: ExperimentShell классы

+
+
+ab_test = ABTest(additional_tests=["t-test"]) +results = ab_test.execute(data) +
+
+ + +
+

Уровень 5: Создание эксперимента

+
+

Пользователь: Разработчик, знакомый с Executor

+

Сегмент: Библиотека HypEx

+

Идея: Композиция из готовых блоков

+

Использование: Создание Experiment из Executor'ов

+
+
+experiment = Experiment([ + TTest(compare_by="groups"), + ABAnalyzer(multitest_method="holm") +]) +
+
+ + +
+

Уровень 6: Создание Executor

+
+

Пользователь: Разработчик, знакомый с типовыми блоками

+

Сегмент: Библиотека HypEx

+

Идея: Наследование от готовых классов

+

Использование: Создание кастомных Executor'ов

+
+
+class CustomTest(StatHypothesisTesting): + def _inner_function(self, data, test_data): + return my_statistical_test(data, test_data) +
+
+ + +
+

Уровень 7: Модификация библиотеки

+
+

Пользователь: Разработчик, знакомый с архитектурой HypEx

+

Сегмент: Библиотека HypEx

+

Идея: Глубокие доработки базовых механик

+

Использование: Модификация Extension, Backend

+
+
+ + +
+

Уровень 8: Ядро архитектуры

+
+

Пользователь: Архитектор HypEx

+

Сегмент: Библиотека HypEx

+

Идея: Изменение фундаментального поведения

+

Использование: Новое поколение архитектуры

+
+
+ + +
+

Executor Pattern: Концепция

+
    +
  • Единый интерфейс для всех операций
  • +
  • Инкапсуляция бизнес-логики
  • +
  • Стандартизация входных и выходных данных
  • +
  • Композиция сложных операций из простых
  • +
+
+
execute(ExperimentData) → ExperimentData
+
+
+ + +
+

Жизненный цикл Executor

+
+
Создание
+
+
Конфигурация
+
+
Выполнение
+
+
Результат
+
+
+executor = TTest(compare_by="groups") +executor.set_params({"reliability": 0.01}) +result = executor.execute(experiment_data) +
+
+ + +
+

Типы Executor'ов

+
+
+

Calculator

+

Вычислительные операции без изменения данных

+
+
+

Transformer

+

Преобразование исходных данных

+
+
+

Comparator

+

Сравнение групп и проведение тестов

+
+
+

MLExecutor

+

Машинное обучение и предсказания

+
+
+
+ + +
+

Композиция Executor'ов

+
    +
  • Последовательное выполнение в Experiment
  • +
  • Передача данных между компонентами
  • +
  • Накопление результатов в ExperimentData
  • +
  • Условное выполнение через IfExecutor
  • +
+
+
Exec1
+
+
Exec2
+
+
Exec3
+
+
+ + +
+

Пайплайны из Executor'ов

+
+AA_METRICS = Experiment([ + GroupSizes(grouping_role=AdditionalTreatmentRole()), + OnRoleExperiment([ + GroupDifference(compare_by="groups"), + TTest(compare_by="groups"), + KSTest(compare_by="groups") + ], role=TargetRole()), + OneAAStatAnalyzer() +]) +
+

Готовые пайплайны для типовых задач анализа

+
+ + +
+

Конфигурирование Executor'ов

+
    +
  • set_params() для настройки параметров
  • +
  • Типизированные параметры по классам
  • +
  • Динамическая настройка во время выполнения
  • +
  • Валидация параметров на уровне класса
  • +
+
+experiment.set_params({ + TTest: {"calc_kwargs": {"equal_var": False}}, + AASplitter: {"control_size": 0.6} +}) +
+
+ + +
+

Dataset vs ExperimentData

+
+
+

Dataset

+

Данные: Табличные данные с ролями

+

Назначение: Хранение исходных данных

+

Операции: Фильтрация, группировка, агрегация

+
+
+

ExperimentData

+

Данные: Dataset + результаты анализа

+

Назначение: Контейнер для эксперимента

+

Операции: Накопление результатов

+
+
+
+ + +
+

ABCRole Hierarchy

+
+
ABCRole

+
TargetRole
+
TreatmentRole
+
FeatureRole

+
AdditionalTargetRole
+
TempTargetRole
+
+

Иерархия ролей определяет поведение компонентов с данными

+
+ + +
+

Влияние ролей на поведение

+
    +
  • Автоматический поиск релевантных колонок
  • +
  • Валидация совместимости операций
  • +
  • Типизированные вычисления по типам данных
  • +
  • Документирование назначения данных
  • +
+
+target_fields = data.ds.search_columns( + TargetRole(), search_types=[int, float] +) +
+
+ + +
+

Временные роли (tmp_roles)

+
    +
  • Динамическое переназначение ролей
  • +
  • OnRoleExperiment использует tmp_roles
  • +
  • Автоматическая очистка после выполнения
  • +
  • Изоляция изменений от основных ролей
  • +
+
+data.ds.tmp_roles = {field: TempTargetRole()} +result = experiment.execute(data) +data.ds.tmp_roles = {} # Очистка +
+
+ + +
+

Backend система

+
+
+

PandasBackend

+

Основная реализация на pandas DataFrame

+
+
+

Абстракция

+

DatasetBackendCalc, DatasetBackendNavigation

+
+
+

Расширяемость

+

Возможность добавления других backend'ов

+
+
+

Единый интерфейс для различных хранилищ данных

+
+ + +
+

Experiment как контейнер

+
    +
  • Последовательность Executor'ов
  • +
  • Управление состоянием между шагами
  • +
  • Автоматическое копирование данных при необходимости
  • +
  • Трекинг результатов выполнения
  • +
+
+experiment = Experiment( + executors=[splitter, comparator, analyzer], + transformer=False # Без копирования данных +) +
+
+ + +
+

ParamsExperiment

+
    +
  • Перебор параметров для Executor'ов
  • +
  • Автоматическое создание комбинаций
  • +
  • Параллельное выполнение вариантов
  • +
  • Агрегация результатов через Reporter
  • +
+
+ParamsExperiment( + executors=[ONE_AA_TEST], + params={ + AASplitter: {"random_state": range(2000)}, + Comparator: {"space": [SpaceEnum.additional]} + } +) +
+
+ + +
+

GroupExperiment

+
    +
  • Выполнение по группам данных
  • +
  • Автоматическое разбиение по ролям
  • +
  • Независимая обработка каждой группы
  • +
  • Объединение результатов в единый отчет
  • +
+
+GroupExperiment( + executors=[matching_pipeline], + searching_role=GroupingRole(), + reporter=MatchingDatasetReporter() +) +
+
+ + +
+

IfParamsExperiment

+
    +
  • Условное выполнение экспериментов
  • +
  • Остановка по критерию качества
  • +
  • Оптимизация времени выполнения
  • +
  • IfExecutor как критерий остановки
  • +
+
+IfParamsExperiment( + executors=[aa_test], + stopping_criterion=IfAAExecutor(sample_size=0.8), + params=parameter_grid +) +
+
+ + +
+

Вложенность и иерархия

+
+
ParamsExperiment
+
+
Experiment
+
+
Executor
+
+
    +
  • Эксперименты могут содержать другие эксперименты
  • +
  • Рекурсивная обработка вложенной структуры
  • +
  • Изоляция контекстов выполнения
  • +
+
+ + +
+

Иерархия композиции

+
+AA_TEST = Experiment([ + ParamsExperiment([ + Experiment([ # ONE_AA_TEST + AASplitter(), + Experiment([ # AA_METRICS + GroupSizes(), + OnRoleExperiment([TTest(), KSTest()]) + ]) + ]) + ]), + AAScoreAnalyzer() +]) +
+
+ + +
+

Comparators

+
    +
  • Сравнение групп по различным критериям
  • +
  • Гибкие режимы сравнения (groups, columns, cross)
  • +
  • Статистические тесты и метрики
  • +
  • Автоматическая валидация данных
  • +
+
+
GroupDifference
+
TTest
+
Chi2Test
+
GroupSizes
+
+
+ + +
+

Transformers

+
    +
  • Предобработка данных перед анализом
  • +
  • Фильтрация и очистка данных
  • +
  • Кодирование категориальных переменных
  • +
  • Заполнение пропусков и нормализация
  • +
+
+
NaFiller
+
OutliersFilter
+
DummyEncoder
+
CategoryAggregator
+
+
+ + +
+

Analyzers

+
    +
  • Агрегация результатов тестов
  • +
  • Вычисление метрик качества
  • +
  • Принятие решений по критериям
  • +
  • Подготовка финальных отчетов
  • +
+
+
ABAnalyzer
+
AAScoreAnalyzer
+
MatchingAnalyzer
+
OneAAStatAnalyzer
+
+
+ + +
+

Reporters

+
    +
  • Форматирование результатов для вывода
  • +
  • Различные форматы (dict, Dataset)
  • +
  • Настраиваемая структура отчетов
  • +
  • Интеграция с ParamsExperiment
  • +
+
+reporter = DatasetReporter( + OneAADictReporter(front=False) +) +
+
+ + +
+

Splitters

+
    +
  • Разбиение данных на группы
  • +
  • Контроль качества разбиения
  • +
  • Стратификация по признакам
  • +
  • Воспроизводимость через random_state
  • +
+
+
AASplitter
+
AASplitterWithStratification
+
+
+ + +
+

Extensions

+
    +
  • Интеграция с внешними библиотеками
  • +
  • Унифицированный интерфейс для разных backend'ов
  • +
  • Изоляция зависимостей от основного кода
  • +
  • Простота добавления новых алгоритмов
  • +
+
+
scipy_stats
+
statsmodels
+
faiss
+
scipy_linalg
+
+
+ + +
+

Роль компонентов в архитектуре

+
+
Transformers
+
+
Splitters
+
+
Comparators
+
+
Analyzers
+
+
Reporters
+
+

Каждый тип компонента выполняет специфическую роль в пайплайне

+
+ + +
+

ExperimentShell как основа

+
    +
  • Базовый класс для готовых решений
  • +
  • Стандартизация интерфейса экспериментов
  • +
  • Интеграция Experiment + Output
  • +
  • Единообразная настройка параметров
  • +
+
+class AATest(ExperimentShell): + def __init__(self, precision_mode=False): + super().__init__( + experiment=AA_TEST, + output=AAOutput() + ) +
+
+ + +
+

AATest

+
    +
  • Проверка качества разбиения на группы
  • +
  • Множественные сплиты с разными random_state
  • +
  • Статистические тесты однородности
  • +
  • Автоматический выбор лучшего разбиения
  • +
+
+aa_test = AATest( + precision_mode=True, + stratification=True, + control_size=0.5 +) +
+
+ + +
+

ABTest

+
    +
  • Сравнение экспериментальных групп
  • +
  • Множественные тесты (t-test, chi2, u-test)
  • +
  • Коррекция на множественное тестирование
  • +
  • Настраиваемые статистические критерии
  • +
+
+ab_test = ABTest( + additional_tests=["t-test", "chi2-test"], + multitest_method="bonferroni" +) +
+
+ + +
+

HomogeneityTest

+
    +
  • Проверка однородности между группами
  • +
  • Валидация предпосылок для тестирования
  • +
  • Множественные тесты по всем переменным
  • +
  • Диагностика проблем в данных
  • +
+
+homo_test = HomogeneityTest() +results = homo_test.execute(data) +
+
+ + +
+

Matching

+
    +
  • Поиск похожих объектов в группах
  • +
  • Различные метрики расстояния
  • +
  • Оценка качества сопоставления
  • +
  • Интеграция с Faiss для быстрого поиска
  • +
+
+matching = Matching( + distance="mahalanobis", + metric="ate", + quality_tests=["t-test", "ks-test"] +) +
+
+ + +
+

Кастомизация готовых решений

+
    +
  • Настройка параметров через конструктор
  • +
  • Модификация эксперимента после создания
  • +
  • Замена компонентов на кастомные
  • +
  • Расширение функциональности наследованием
  • +
+
+class CustomAATest(AATest): + def __init__(self): + super().__init__() + self.experiment.set_params({ + TTest: {"reliability": 0.01} + }) +
+
+ + +
+

Output классы

+
    +
  • Структурированный вывод результатов
  • +
  • Автоматическое извлечение данных
  • +
  • Форматирование для пользователя
  • +
  • Интеграция с Reporter системой
  • +
+
+
AAOutput
+
ABOutput
+
MatchingOutput
+
HomoOutput
+
+
+ + +
+

Reporter интеграция

+
+
ExperimentData
+
+
Reporter
+
+
Output
+
+
    +
  • Автоматическое форматирование результатов
  • +
  • Множественные Reporter'ы для разных нужд
  • +
  • Кастомизация формата вывода
  • +
+
+ + +
+

Связь Output с экспериментами

+
+class ABOutput(Output): + def __init__(self): + super().__init__( + resume_reporter=ABDatasetReporter(), + additional_reporters={ + 'statistics': StatsReporter() + } + ) +
+

Output автоматически извлекает и форматирует результаты эксперимента

+
+ + +
+

Система извлечения результатов

+
    +
  • get_ids() для поиска результатов по типу
  • +
  • Автоматическая навигация по ExperimentData
  • +
  • Извлечение из разных пространств данных
  • +
  • Форматирование в удобный вид
  • +
+
+ids = data.get_ids( + TTest, ExperimentDataEnum.analysis_tables +) +
+
+ + +
+

Общая схема архитектуры

+
+
+
+
Dataset
+
ExperimentData
+
+
+
Executor
+
Experiment
+
ExperimentShell
+
+
+
Reporter
+
Output
+
+
+
+
+ + +
+

Схема взаимодействий

+
+
Raw Data
+
+
Dataset
+
+
Executors
+
+
Results
+
+
Reports
+
+
+ + +
+

Диаграмма наследования

+
+
+
Executor (ABC)

+
+
Calculator
+
IfExecutor
+
Comparator
+
MLExecutor
+
+
+
+
+ + +
+

Поток данных в архитектуре

+
+
pandas.DataFrame
+
+
Dataset + Roles
+
+
ExperimentData
+
+
Formatted Output
+
+
+ + +
+

Композиционная схема

+
+
+
ParamsExperiment

+
содержит
+
Experiment

+
содержит
+
+
Exec1
+
Exec2
+
Exec3
+
+
+
+
+ + +
+

Точки расширения

+
    +
  • Новые Executor'ы через наследование
  • +
  • Extensions для интеграции библиотек
  • +
  • Backend'ы для различных хранилищ
  • +
  • Reporter'ы для новых форматов
  • +
  • Роли данных для специфических задач
  • +
+
+ + +
+

Extensions архитектура

+
+
HypEx Core
+
+
Extension Interface
+
+
External Library
+
+
    +
  • Унифицированный интерфейс calc()
  • +
  • Изоляция зависимостей от основного кода
  • +
  • Простота добавления новых алгоритмов
  • +
+
+ + +
+

Интеграции с библиотеками

+
+
+

scipy.stats

+

Статистические тесты: t-test, KS-test, chi2, Mann-Whitney

+
+
+

statsmodels

+

Коррекция множественных сравнений, продвинутая статистика

+
+
+

faiss

+

Быстрый поиск ближайших соседей для matching

+
+
+

scipy.linalg

+

Линейная алгебра: разложения, расстояния

+
+
+
+ + +
+

Добавление новых Extension

+
+class MyExtension(Extension): + def _calc_pandas(self, data: Dataset, **kwargs): + # Интеграция с внешней библиотекой + result = external_library.process(data.data) + return self.result_to_dataset(result, roles) +
+

Простое добавление новых алгоритмов через Extension интерфейс

+
+ + +
+

ExperimentData как хранилище

+
    +
  • Центральное хранилище состояния эксперимента
  • +
  • Накопление результатов между шагами
  • +
  • Изоляция данных разных экспериментов
  • +
  • Автоматическое управление жизненным циклом
  • +
+
+
ds: Dataset
+
additional_fields
+
analysis_tables
+
variables
+
groups
+
+
+ + +
+

Пространства данных

+
+
+

ds (Dataset)

+

Основные исходные данные

+
+
+

additional_fields

+

Дополнительные вычисленные колонки

+
+
+

analysis_tables

+

Результаты статистических тестов

+
+
+

variables

+

Скалярные результаты и метрики

+
+
+

groups

+

Предвычисленные группировки данных

+
+
+
+ + +
+

ID система

+
    +
  • Уникальные идентификаторы для результатов
  • +
  • Автоматическая генерация на основе параметров
  • +
  • Поиск результатов по типу Executor'а
  • +
  • Навигация по пространствам данных
  • +
+
+# Формат ID: ClassName⊔params_hash⊔key +"TTest⊔reliability_0.05⊔target_revenue" +
+
+ + +
+

Управление состоянием

+
+
set_value()
+
+
ExperimentData
+
+
get_ids()
+
+
    +
  • Централизованное управление результатами
  • +
  • Типизированные пространства данных
  • +
  • Автоматическая навигация по результатам
  • +
+
+ + +
+

Преимущества: Переиспользование

+
    +
  • Модульные компоненты для разных задач
  • +
  • Стандартизированные интерфейсы
  • +
  • Готовые пайплайны для типовых анализов
  • +
  • Композиция сложного из простого
  • +
+
+# Один Executor в разных экспериментах +t_test = TTest(compare_by="groups") +ab_experiment.add_executor(t_test) +aa_experiment.add_executor(t_test) +
+
+ + +
+

Тестируемость и читаемость

+
    +
  • Изолированное тестирование компонентов
  • +
  • Мокирование зависимостей
  • +
  • Декларативное описание пайплайнов
  • +
  • Самодокументирующийся код через роли
  • +
+
+def test_t_test(): + executor = TTest(compare_by="groups") + result = executor.execute(test_data) + assert result.analysis_tables[...]["pass"] +
+
+ + + +
+ + + + \ No newline at end of file From 46c1e1f17277a1be1c60d2a79da0bdd686b8ac86 Mon Sep 17 00:00:00 2001 From: dmbulychev Date: Sun, 21 Sep 2025 14:44:03 +0300 Subject: [PATCH 17/17] Add part of presentation --- schemes/anatomy_presentation.html | 912 +++++++++++ schemes/hypex_architecture_presentation.html | 1486 ------------------ 2 files changed, 912 insertions(+), 1486 deletions(-) create mode 100644 schemes/anatomy_presentation.html delete mode 100644 schemes/hypex_architecture_presentation.html diff --git a/schemes/anatomy_presentation.html b/schemes/anatomy_presentation.html new file mode 100644 index 00000000..e7ff5528 --- /dev/null +++ b/schemes/anatomy_presentation.html @@ -0,0 +1,912 @@ + + + + + + Архитектура HypEx - Часть 1 + + + +
+
1 / 18
+ +
+ +
+

Архитектура HypEx

+

Модульная библиотека для статистического анализа и A/B тестирования

+
+
+

Executor Pattern

+

Композиционная архитектура

+
+
+
+

Типобезопасность

+

Система ролей данных

+
+
+
+

Готовые решения

+

AATest, ABTest, Matching

+
+
+
+ + +
+

Что такое HypEx?

+
+
+

Библиотека анализа

+

Статистический анализ и экспериментирование с акцентом на типобезопасность и композицию

+
+
+

Модульная архитектура

+

Основана на паттерне Executor с возможностью создания сложных пайплайнов

+
+
+

Готовые решения

+

Предустановленные эксперименты для A/A тестов, A/B тестов и matching анализа

+
+
+

Расширяемость

+

8 уровней абстракции для пользователей разного уровня экспертизы

+
+
+
+ + +
+

Проблемы которые решает HypEx

+
    +
  • Разрозненность инструментов: Единая экосистема для всех видов статистического анализа
  • +
  • Ошибки в экспериментах: Типобезопасная система ролей предотвращает неправильное использование данных
  • +
  • Сложность настройки: Готовые решения для типовых задач из коробки
  • +
  • Отсутствие композиции: Модульная архитектура позволяет создавать сложные пайплайны
  • +
  • Низкая переиспользуемость: Executor pattern обеспечивает многократное использование компонентов
  • +
  • Отсутствие стандартизации: Единообразные интерфейсы для всех типов анализа
  • +
+
+ + +
+

Домены применения

+
+
+

A/B тестирование

+

Сравнение двух групп с множественными статистическими тестами и коррекцией

+
+
+

A/A тестирование

+

Проверка корректности разделения и отсутствия ложных срабатываний

+
+
+

Matching анализ

+

Поиск похожих объектов и оценка качества сопоставления

+
+
+

Homogeneity тестирование

+

Проверка однородности групп по множественным характеристикам

+
+
+
+ + +
+

Философия архитектуры

+

Модульность

+
+
+

Executor

+

Базовый строительный блок

+
+
+
+
+

Executor

+

Другой компонент

+
+
=
+
+

Experiment

+

Сложный пайплайн

+
+
+
    +
  • Каждый компонент выполняет одну функцию хорошо
  • +
  • Компоненты легко комбинируются в сложные пайплайны
  • +
  • Возможность повторного использования в разных контекстах
  • +
+
+ + +
+

Композиция вместо наследования

+
+
+

❌ Наследование

+
+class CustomTest(BaseTest): + def run(self): + # Жесткая связанность + # Сложно тестировать + # Трудно изменять +
+
+
+

✅ Композиция

+
+Experiment([ + Splitter(), + TTest(), + Analyzer() +]) +# Гибкость +# Тестируемость +
+
+
+
    +
  • Слабая связанность: Компоненты независимы друг от друга
  • +
  • Легкое тестирование: Каждый компонент тестируется изолированно
  • +
  • Простая замена: Компоненты легко заменяются без изменения архитектуры
  • +
+
+ + +
+

Типобезопасность через роли

+
+
+

TargetRole

+

Целевая переменная для анализа

+
+
+

TreatmentRole

+

Группирующая переменная (контроль/тест)

+
+
+

FeatureRole

+

Признаки для анализа

+
+
+

GroupingRole

+

Дополнительное группирование

+
+
+
+

Преимущества:

+
    +
  • Компилятор проверяет корректность использования данных
  • +
  • Предотвращение ошибок на этапе разработки
  • +
  • Самодокументируемый код - роли объясняют назначение колонок
  • +
+
+
+ + +
+

Единообразие интерфейсов

+
+# Единый интерфейс для всех компонентов +class Executor: + def execute(self, data: ExperimentData) -> ExperimentData: + pass + +# Использование в любом контексте +experiment = Experiment([ + AASplitter(), # Разделение данных + TTest(), # Статистический тест + ABAnalyzer() # Анализ результатов +]) +
+
    +
  • Единый метод execute(): Все компоненты имеют одинаковый интерфейс
  • +
  • Простота изучения: Освоив один компонент, легко использовать остальные
  • +
  • Композиционность: Любые компоненты можно комбинировать
  • +
  • Предсказуемость: Поведение компонентов следует единым правилам
  • +
+
+ + +
+

8 уровней абстракции

+

+ HypEx предоставляет разные уровни взаимодействия + для пользователей с различным уровнем экспертизы +

+
+
+

Бизнес-пользователи

+

UI платформы, готовые сценарии

+
+
+
+

Аналитики и исследователи

+

Конструкторы, шаблоны, оболочки

+
+
+
+

Разработчики

+

Код, наследование, модификация ядра

+
+
+
+ + +
+

Уровни 1-2: Платформа

+
+
Уровень 1. Пользовательский интерфейс
+
👤 Бизнес-пользователь
+
Запуск готовых сценариев через веб-интерфейс без написания кода. Выбор сценария и получение результатов анализа.
+
+
+
Уровень 2. Конструктор сценариев
+
👤 Финансист/Аналитик
+
Создание сценариев в графическом конструкторе. Знание статистики, понимание бизнес-процессов, но без программирования.
+
+
+

Преимущества платформенных уровней:

+

• Доступность для нетехнических специалистов
+ • Быстрое получение результатов
+ • Стандартизированные процессы анализа

+
+
+ + +
+

Уровни 3-4: Dev пакет и библиотека

+
+
Уровень 3. Настраиваемый шаблонный код
+
👤 Бизнес-пользователь с доступом к лабораторной зоне
+
Запуск заранее запрограммированных сценариев с настраиваемыми параметрами. Минимальные изменения в коде.
+
+
+
Уровень 4. Оболочка эксперимента HypEx
+
👤 Исследователь данных
+
Использование готовых экспериментальных оболочек (AATest, ABTest). Настройка и запуск в несколько строк кода.
+
+
+# Пример уровня 4 +aa_test = AATest(stratification=True) +results = aa_test.execute(dataset) +
+
+ + +
+

Уровни 5-6: Создание экспериментов

+
+
Уровень 5. Создание эксперимента в коде
+
👤 Разработчик, знакомый с базовыми блоками
+
Создание кастомных экспериментов из готовых Executor'ов. Композиция базовых блоков в новые пайплайны.
+
+
+
Уровень 6. Создание Executor наследованием
+
👤 Разработчик, знакомый с типовыми блоками
+
Создание кастомных Executor'ов наследованием от типовых блоков. Расширение функциональности существующих компонентов.
+
+
+# Пример уровня 5 +custom_experiment = Experiment([ + AASplitter(), TTest(), CustomAnalyzer() +]) +
+
+ + +
+

Уровни 7-8: Модификация и ядро

+
+
Уровень 7. Модификация библиотеки
+
👤 Опытный разработчик HypEx
+
Глубокие доработки базовых механик библиотеки. Изменение поведения существующих компонентов и добавление новых возможностей.
+
+
+
Уровень 8. Ядро архитектуры
+
👤 Архитектор HypEx
+
Изменение фундаментального поведения библиотеки. Создание нового поколения архитектуры, изменение базовых принципов.
+
+
+

⚠️ Высокие уровни требуют:

+

• Глубокого понимания архитектуры HypEx
+ • Опыта разработки библиотек
+ • Ответственности за обратную совместимость

+
+
+ + +
+

Преимущества многоуровневой архитектуры

+
+
+

Доступность

+

Каждый пользователь находит подходящий уровень сложности

+
+
+

Масштабируемость обучения

+

Постепенное углубление знаний от простого к сложному

+
+
+

Эффективность

+

Не нужно изучать сложные концепции для простых задач

+
+
+

Гибкость применения

+

От готовых решений до полной кастомизации

+
+
+
+

Путь пользователя в HypEx

+
+
UI
+
+
Шаблоны
+
+
Код
+
+
Ядро
+
+
+
+ + +
+

Сегментация пользователей

+
+
+

Платформа

+
+

Бизнес-пользователи

+

• Менеджеры продуктов
• Маркетологи
• Аналитики без технических навыков

+

Нужны результаты, не код

+
+
+
+

Dev пакет

+
+

Исследователи данных

+

• Data Scientists
• Финансовые аналитики
• Исследователи

+

Знают статистику, умеют программировать

+
+
+
+
+

Библиотека

+
+
+

Разработчики

+

• Python разработчики
• ML инженеры
• Архитекторы решений

+
+
+

Контрибьюторы

+

• Core разработчики
• Архитекторы библиотеки
• Экспертные пользователи

+
+
+
+
+ + +
+

Примеры использования уровней

+
+
+

Уровень 1: UI

+
+1. Выбрать "A/B тест" +2. Загрузить данные +3. Указать колонки +4. Нажать "Запустить" +5. Получить отчет +
+
+
+

Уровень 4: Оболочка

+
+ab_test = ABTest( + additional_tests=["t-test"], + multitest_method="holm" +) +result = ab_test.execute(data) +
+
+
+
+
+

Уровень 5: Композиция

+
+Experiment([ + GroupSizes(), + TTest(), + ABAnalyzer() +]) +
+
+
+

Уровень 6: Наследование

+
+class CustomTTest(TTest): + def calc(self, data, **kwargs): + # Custom logic + return super().calc(data, **kwargs) +
+
+
+
+ + +
+

Переходы между уровнями

+
+
+
+

Платформа

+

Готовые сценарии

+
+
+
+

Нужна кастомизация

+
+
+

Dev пакет

+

Шаблоны с параметрами

+
+
+
+
+

Оболочки

+

AATest, ABTest

+
+
+
+

Нужна гибкость

+
+
+

Композиция

+

Experiment([...])

+
+
+
+
+

Готовые блоки

+

TTest, Splitter

+
+
+
+

Нужна новая логика

+
+
+

Наследование

+

class Custom(Base)

+
+
+
+

+ Плавный переход между уровнями по мере роста экспертизы +

+
+ + +
+

Заключение: Основы архитектуры

+
+
+

Философия

+

Модульность, композиция, типобезопасность через роли

+
+
+

Многоуровневость

+

8 уровней от UI до ядра для разных пользователей

+
+
+

Решаемые проблемы

+

Стандартизация, переиспользование, предотвращение ошибок

+
+
+

Домены применения

+

A/B, A/A тесты, matching, homogeneity анализ

+
+
+
+

Далее: Executor Pattern

+

+ В следующей части разберем ядро архитектуры - паттерн Executor, + систему типов данных и композицию экспериментов +

+
+
+
+ + + + + + \ No newline at end of file diff --git a/schemes/hypex_architecture_presentation.html b/schemes/hypex_architecture_presentation.html deleted file mode 100644 index 392d3e8d..00000000 --- a/schemes/hypex_architecture_presentation.html +++ /dev/null @@ -1,1486 +0,0 @@ - - - - - - Архитектура HypEx - - - -
-
- 1 / 67 -
- - -
-

Архитектура HypEx

-

Модульная библиотека для статистического анализа и A/B тестирования

-
-
Executor Pattern
-
Type-Safe Data
-
Composable Experiments
-
-
- - -
-

Что такое HypEx

-
    -
  • Библиотека для статистического анализа экспериментов
  • -
  • Модульная архитектура на основе Executor pattern
  • -
  • Типобезопасность через систему ролей данных
  • -
  • Композиция вместо наследования
  • -
  • Переиспользование компонентов
  • -
-
- - -
-

Проблемы которые решает HypEx

-
    -
  • Сложность настройки статистических тестов
  • -
  • Дублирование кода при анализе экспериментов
  • -
  • Ошибки типов в данных и параметрах
  • -
  • Несовместимость различных подходов к анализу
  • -
  • Сложность масштабирования аналитических пайплайнов
  • -
-
- - -
-

Домены применения

-
-
-

A/B Testing

-

Сравнение групп пользователей, статистические тесты, анализ эффекта

-
-
-

A/A Testing

-

Проверка качества разбиения, валидация экспериментальной системы

-
-
-

Matching

-

Поиск похожих объектов, анализ парных наблюдений

-
-
-

Homogeneity

-

Тестирование однородности групп, проверка предпосылок

-
-
-
- - -
-

Обзор архитектуры

-
-
Dataset
-
-
Executor
-
-
Experiment
-
-
Results
-
-

Данные проходят через цепочку исполнителей, объединенных в эксперименты

-
- - -
-

Философия: Модульность

-
    -
  • Мелкие компоненты с четкой ответственностью
  • -
  • Единый интерфейс для всех исполнителей
  • -
  • Простота тестирования отдельных компонентов
  • -
  • Легкость замены и расширения
  • -
-
-class Executor(ABC): - def execute(self, data: ExperimentData) -> ExperimentData: - # Единый интерфейс для всех компонентов - pass -
-
- - -
-

Композиция вместо наследования

-
    -
  • Гибкость в построении пайплайнов
  • -
  • Переиспользование готовых компонентов
  • -
  • Избежание глубоких иерархий наследования
  • -
  • Динамическая настройка поведения
  • -
-
-experiment = Experiment([ - AASplitter(), - TTest(compare_by="groups"), - AAScoreAnalyzer() -]) -
-
- - -
-

Типобезопасность через роли

-
    -
  • ABCRole система для описания назначения данных
  • -
  • Автоматическая валидация совместимости
  • -
  • Предотвращение ошибок на этапе выполнения
  • -
  • Самодокументирующийся код
  • -
-
-roles = { - 'user_id': TreatmentRole(), - 'revenue': TargetRole(), - 'age': FeatureRole() -} -
-
- - -
-

Единообразие интерфейсов

-
    -
  • Один метод execute() для всех компонентов
  • -
  • Стандартные паттерны конфигурации
  • -
  • Предсказуемое поведение компонентов
  • -
  • Низкий порог входа для новых разработчиков
  • -
-
- - -
-

Принципы расширяемости

-
    -
  • Extension точки для интеграции библиотек
  • -
  • Pluggable архитектура для новых алгоритмов
  • -
  • Минимальные зависимости в базовом функционале
  • -
  • Обратная совместимость API
  • -
-
- - -
-

Уровень 1: Пользовательский интерфейс

-
-

Пользователь: Бизнес пользователь

-

Сегмент: Платформа

-

Идея: Работа без знания статистики и кода

-

Использование: Выбор и запуск готовых сценариев

-
-

Простой интерфейс для выбора типа анализа и получения результатов

-
- - -
-

Уровень 2: Конструктор сценариев

-
-

Пользователь: Финансист/Аналитик

-

Сегмент: Платформа

-

Идея: Создание сценариев без программирования

-

Использование: Графический конструктор пайплайнов

-
-

Drag & drop интерфейс для составления аналитических сценариев

-
- - -
-

Уровень 3: Настраиваемый шаблон

-
-

Пользователь: Бизнес пользователь с доступом к данным

-

Сегмент: Dev пакет

-

Идея: Готовый код с настраиваемыми параметрами

-

Использование: Запуск шаблона в лабораторной зоне

-
-
-# Настраиваемые параметры -CONTROL_SIZE = 0.5 -RANDOM_STATE = 42 -
-
- - -
-

Уровень 4: Оболочка эксперимента

-
-

Пользователь: Исследователь данных

-

Сегмент: Библиотека HypEx

-

Идея: Готовые эксперименты в несколько строк

-

Использование: ExperimentShell классы

-
-
-ab_test = ABTest(additional_tests=["t-test"]) -results = ab_test.execute(data) -
-
- - -
-

Уровень 5: Создание эксперимента

-
-

Пользователь: Разработчик, знакомый с Executor

-

Сегмент: Библиотека HypEx

-

Идея: Композиция из готовых блоков

-

Использование: Создание Experiment из Executor'ов

-
-
-experiment = Experiment([ - TTest(compare_by="groups"), - ABAnalyzer(multitest_method="holm") -]) -
-
- - -
-

Уровень 6: Создание Executor

-
-

Пользователь: Разработчик, знакомый с типовыми блоками

-

Сегмент: Библиотека HypEx

-

Идея: Наследование от готовых классов

-

Использование: Создание кастомных Executor'ов

-
-
-class CustomTest(StatHypothesisTesting): - def _inner_function(self, data, test_data): - return my_statistical_test(data, test_data) -
-
- - -
-

Уровень 7: Модификация библиотеки

-
-

Пользователь: Разработчик, знакомый с архитектурой HypEx

-

Сегмент: Библиотека HypEx

-

Идея: Глубокие доработки базовых механик

-

Использование: Модификация Extension, Backend

-
-
- - -
-

Уровень 8: Ядро архитектуры

-
-

Пользователь: Архитектор HypEx

-

Сегмент: Библиотека HypEx

-

Идея: Изменение фундаментального поведения

-

Использование: Новое поколение архитектуры

-
-
- - -
-

Executor Pattern: Концепция

-
    -
  • Единый интерфейс для всех операций
  • -
  • Инкапсуляция бизнес-логики
  • -
  • Стандартизация входных и выходных данных
  • -
  • Композиция сложных операций из простых
  • -
-
-
execute(ExperimentData) → ExperimentData
-
-
- - -
-

Жизненный цикл Executor

-
-
Создание
-
-
Конфигурация
-
-
Выполнение
-
-
Результат
-
-
-executor = TTest(compare_by="groups") -executor.set_params({"reliability": 0.01}) -result = executor.execute(experiment_data) -
-
- - -
-

Типы Executor'ов

-
-
-

Calculator

-

Вычислительные операции без изменения данных

-
-
-

Transformer

-

Преобразование исходных данных

-
-
-

Comparator

-

Сравнение групп и проведение тестов

-
-
-

MLExecutor

-

Машинное обучение и предсказания

-
-
-
- - -
-

Композиция Executor'ов

-
    -
  • Последовательное выполнение в Experiment
  • -
  • Передача данных между компонентами
  • -
  • Накопление результатов в ExperimentData
  • -
  • Условное выполнение через IfExecutor
  • -
-
-
Exec1
-
-
Exec2
-
-
Exec3
-
-
- - -
-

Пайплайны из Executor'ов

-
-AA_METRICS = Experiment([ - GroupSizes(grouping_role=AdditionalTreatmentRole()), - OnRoleExperiment([ - GroupDifference(compare_by="groups"), - TTest(compare_by="groups"), - KSTest(compare_by="groups") - ], role=TargetRole()), - OneAAStatAnalyzer() -]) -
-

Готовые пайплайны для типовых задач анализа

-
- - -
-

Конфигурирование Executor'ов

-
    -
  • set_params() для настройки параметров
  • -
  • Типизированные параметры по классам
  • -
  • Динамическая настройка во время выполнения
  • -
  • Валидация параметров на уровне класса
  • -
-
-experiment.set_params({ - TTest: {"calc_kwargs": {"equal_var": False}}, - AASplitter: {"control_size": 0.6} -}) -
-
- - -
-

Dataset vs ExperimentData

-
-
-

Dataset

-

Данные: Табличные данные с ролями

-

Назначение: Хранение исходных данных

-

Операции: Фильтрация, группировка, агрегация

-
-
-

ExperimentData

-

Данные: Dataset + результаты анализа

-

Назначение: Контейнер для эксперимента

-

Операции: Накопление результатов

-
-
-
- - -
-

ABCRole Hierarchy

-
-
ABCRole

-
TargetRole
-
TreatmentRole
-
FeatureRole

-
AdditionalTargetRole
-
TempTargetRole
-
-

Иерархия ролей определяет поведение компонентов с данными

-
- - -
-

Влияние ролей на поведение

-
    -
  • Автоматический поиск релевантных колонок
  • -
  • Валидация совместимости операций
  • -
  • Типизированные вычисления по типам данных
  • -
  • Документирование назначения данных
  • -
-
-target_fields = data.ds.search_columns( - TargetRole(), search_types=[int, float] -) -
-
- - -
-

Временные роли (tmp_roles)

-
    -
  • Динамическое переназначение ролей
  • -
  • OnRoleExperiment использует tmp_roles
  • -
  • Автоматическая очистка после выполнения
  • -
  • Изоляция изменений от основных ролей
  • -
-
-data.ds.tmp_roles = {field: TempTargetRole()} -result = experiment.execute(data) -data.ds.tmp_roles = {} # Очистка -
-
- - -
-

Backend система

-
-
-

PandasBackend

-

Основная реализация на pandas DataFrame

-
-
-

Абстракция

-

DatasetBackendCalc, DatasetBackendNavigation

-
-
-

Расширяемость

-

Возможность добавления других backend'ов

-
-
-

Единый интерфейс для различных хранилищ данных

-
- - -
-

Experiment как контейнер

-
    -
  • Последовательность Executor'ов
  • -
  • Управление состоянием между шагами
  • -
  • Автоматическое копирование данных при необходимости
  • -
  • Трекинг результатов выполнения
  • -
-
-experiment = Experiment( - executors=[splitter, comparator, analyzer], - transformer=False # Без копирования данных -) -
-
- - -
-

ParamsExperiment

-
    -
  • Перебор параметров для Executor'ов
  • -
  • Автоматическое создание комбинаций
  • -
  • Параллельное выполнение вариантов
  • -
  • Агрегация результатов через Reporter
  • -
-
-ParamsExperiment( - executors=[ONE_AA_TEST], - params={ - AASplitter: {"random_state": range(2000)}, - Comparator: {"space": [SpaceEnum.additional]} - } -) -
-
- - -
-

GroupExperiment

-
    -
  • Выполнение по группам данных
  • -
  • Автоматическое разбиение по ролям
  • -
  • Независимая обработка каждой группы
  • -
  • Объединение результатов в единый отчет
  • -
-
-GroupExperiment( - executors=[matching_pipeline], - searching_role=GroupingRole(), - reporter=MatchingDatasetReporter() -) -
-
- - -
-

IfParamsExperiment

-
    -
  • Условное выполнение экспериментов
  • -
  • Остановка по критерию качества
  • -
  • Оптимизация времени выполнения
  • -
  • IfExecutor как критерий остановки
  • -
-
-IfParamsExperiment( - executors=[aa_test], - stopping_criterion=IfAAExecutor(sample_size=0.8), - params=parameter_grid -) -
-
- - -
-

Вложенность и иерархия

-
-
ParamsExperiment
-
-
Experiment
-
-
Executor
-
-
    -
  • Эксперименты могут содержать другие эксперименты
  • -
  • Рекурсивная обработка вложенной структуры
  • -
  • Изоляция контекстов выполнения
  • -
-
- - -
-

Иерархия композиции

-
-AA_TEST = Experiment([ - ParamsExperiment([ - Experiment([ # ONE_AA_TEST - AASplitter(), - Experiment([ # AA_METRICS - GroupSizes(), - OnRoleExperiment([TTest(), KSTest()]) - ]) - ]) - ]), - AAScoreAnalyzer() -]) -
-
- - -
-

Comparators

-
    -
  • Сравнение групп по различным критериям
  • -
  • Гибкие режимы сравнения (groups, columns, cross)
  • -
  • Статистические тесты и метрики
  • -
  • Автоматическая валидация данных
  • -
-
-
GroupDifference
-
TTest
-
Chi2Test
-
GroupSizes
-
-
- - -
-

Transformers

-
    -
  • Предобработка данных перед анализом
  • -
  • Фильтрация и очистка данных
  • -
  • Кодирование категориальных переменных
  • -
  • Заполнение пропусков и нормализация
  • -
-
-
NaFiller
-
OutliersFilter
-
DummyEncoder
-
CategoryAggregator
-
-
- - -
-

Analyzers

-
    -
  • Агрегация результатов тестов
  • -
  • Вычисление метрик качества
  • -
  • Принятие решений по критериям
  • -
  • Подготовка финальных отчетов
  • -
-
-
ABAnalyzer
-
AAScoreAnalyzer
-
MatchingAnalyzer
-
OneAAStatAnalyzer
-
-
- - -
-

Reporters

-
    -
  • Форматирование результатов для вывода
  • -
  • Различные форматы (dict, Dataset)
  • -
  • Настраиваемая структура отчетов
  • -
  • Интеграция с ParamsExperiment
  • -
-
-reporter = DatasetReporter( - OneAADictReporter(front=False) -) -
-
- - -
-

Splitters

-
    -
  • Разбиение данных на группы
  • -
  • Контроль качества разбиения
  • -
  • Стратификация по признакам
  • -
  • Воспроизводимость через random_state
  • -
-
-
AASplitter
-
AASplitterWithStratification
-
-
- - -
-

Extensions

-
    -
  • Интеграция с внешними библиотеками
  • -
  • Унифицированный интерфейс для разных backend'ов
  • -
  • Изоляция зависимостей от основного кода
  • -
  • Простота добавления новых алгоритмов
  • -
-
-
scipy_stats
-
statsmodels
-
faiss
-
scipy_linalg
-
-
- - -
-

Роль компонентов в архитектуре

-
-
Transformers
-
-
Splitters
-
-
Comparators
-
-
Analyzers
-
-
Reporters
-
-

Каждый тип компонента выполняет специфическую роль в пайплайне

-
- - -
-

ExperimentShell как основа

-
    -
  • Базовый класс для готовых решений
  • -
  • Стандартизация интерфейса экспериментов
  • -
  • Интеграция Experiment + Output
  • -
  • Единообразная настройка параметров
  • -
-
-class AATest(ExperimentShell): - def __init__(self, precision_mode=False): - super().__init__( - experiment=AA_TEST, - output=AAOutput() - ) -
-
- - -
-

AATest

-
    -
  • Проверка качества разбиения на группы
  • -
  • Множественные сплиты с разными random_state
  • -
  • Статистические тесты однородности
  • -
  • Автоматический выбор лучшего разбиения
  • -
-
-aa_test = AATest( - precision_mode=True, - stratification=True, - control_size=0.5 -) -
-
- - -
-

ABTest

-
    -
  • Сравнение экспериментальных групп
  • -
  • Множественные тесты (t-test, chi2, u-test)
  • -
  • Коррекция на множественное тестирование
  • -
  • Настраиваемые статистические критерии
  • -
-
-ab_test = ABTest( - additional_tests=["t-test", "chi2-test"], - multitest_method="bonferroni" -) -
-
- - -
-

HomogeneityTest

-
    -
  • Проверка однородности между группами
  • -
  • Валидация предпосылок для тестирования
  • -
  • Множественные тесты по всем переменным
  • -
  • Диагностика проблем в данных
  • -
-
-homo_test = HomogeneityTest() -results = homo_test.execute(data) -
-
- - -
-

Matching

-
    -
  • Поиск похожих объектов в группах
  • -
  • Различные метрики расстояния
  • -
  • Оценка качества сопоставления
  • -
  • Интеграция с Faiss для быстрого поиска
  • -
-
-matching = Matching( - distance="mahalanobis", - metric="ate", - quality_tests=["t-test", "ks-test"] -) -
-
- - -
-

Кастомизация готовых решений

-
    -
  • Настройка параметров через конструктор
  • -
  • Модификация эксперимента после создания
  • -
  • Замена компонентов на кастомные
  • -
  • Расширение функциональности наследованием
  • -
-
-class CustomAATest(AATest): - def __init__(self): - super().__init__() - self.experiment.set_params({ - TTest: {"reliability": 0.01} - }) -
-
- - -
-

Output классы

-
    -
  • Структурированный вывод результатов
  • -
  • Автоматическое извлечение данных
  • -
  • Форматирование для пользователя
  • -
  • Интеграция с Reporter системой
  • -
-
-
AAOutput
-
ABOutput
-
MatchingOutput
-
HomoOutput
-
-
- - -
-

Reporter интеграция

-
-
ExperimentData
-
-
Reporter
-
-
Output
-
-
    -
  • Автоматическое форматирование результатов
  • -
  • Множественные Reporter'ы для разных нужд
  • -
  • Кастомизация формата вывода
  • -
-
- - -
-

Связь Output с экспериментами

-
-class ABOutput(Output): - def __init__(self): - super().__init__( - resume_reporter=ABDatasetReporter(), - additional_reporters={ - 'statistics': StatsReporter() - } - ) -
-

Output автоматически извлекает и форматирует результаты эксперимента

-
- - -
-

Система извлечения результатов

-
    -
  • get_ids() для поиска результатов по типу
  • -
  • Автоматическая навигация по ExperimentData
  • -
  • Извлечение из разных пространств данных
  • -
  • Форматирование в удобный вид
  • -
-
-ids = data.get_ids( - TTest, ExperimentDataEnum.analysis_tables -) -
-
- - -
-

Общая схема архитектуры

-
-
-
-
Dataset
-
ExperimentData
-
-
-
Executor
-
Experiment
-
ExperimentShell
-
-
-
Reporter
-
Output
-
-
-
-
- - -
-

Схема взаимодействий

-
-
Raw Data
-
-
Dataset
-
-
Executors
-
-
Results
-
-
Reports
-
-
- - -
-

Диаграмма наследования

-
-
-
Executor (ABC)

-
-
Calculator
-
IfExecutor
-
Comparator
-
MLExecutor
-
-
-
-
- - -
-

Поток данных в архитектуре

-
-
pandas.DataFrame
-
-
Dataset + Roles
-
-
ExperimentData
-
-
Formatted Output
-
-
- - -
-

Композиционная схема

-
-
-
ParamsExperiment

-
содержит
-
Experiment

-
содержит
-
-
Exec1
-
Exec2
-
Exec3
-
-
-
-
- - -
-

Точки расширения

-
    -
  • Новые Executor'ы через наследование
  • -
  • Extensions для интеграции библиотек
  • -
  • Backend'ы для различных хранилищ
  • -
  • Reporter'ы для новых форматов
  • -
  • Роли данных для специфических задач
  • -
-
- - -
-

Extensions архитектура

-
-
HypEx Core
-
-
Extension Interface
-
-
External Library
-
-
    -
  • Унифицированный интерфейс calc()
  • -
  • Изоляция зависимостей от основного кода
  • -
  • Простота добавления новых алгоритмов
  • -
-
- - -
-

Интеграции с библиотеками

-
-
-

scipy.stats

-

Статистические тесты: t-test, KS-test, chi2, Mann-Whitney

-
-
-

statsmodels

-

Коррекция множественных сравнений, продвинутая статистика

-
-
-

faiss

-

Быстрый поиск ближайших соседей для matching

-
-
-

scipy.linalg

-

Линейная алгебра: разложения, расстояния

-
-
-
- - -
-

Добавление новых Extension

-
-class MyExtension(Extension): - def _calc_pandas(self, data: Dataset, **kwargs): - # Интеграция с внешней библиотекой - result = external_library.process(data.data) - return self.result_to_dataset(result, roles) -
-

Простое добавление новых алгоритмов через Extension интерфейс

-
- - -
-

ExperimentData как хранилище

-
    -
  • Центральное хранилище состояния эксперимента
  • -
  • Накопление результатов между шагами
  • -
  • Изоляция данных разных экспериментов
  • -
  • Автоматическое управление жизненным циклом
  • -
-
-
ds: Dataset
-
additional_fields
-
analysis_tables
-
variables
-
groups
-
-
- - -
-

Пространства данных

-
-
-

ds (Dataset)

-

Основные исходные данные

-
-
-

additional_fields

-

Дополнительные вычисленные колонки

-
-
-

analysis_tables

-

Результаты статистических тестов

-
-
-

variables

-

Скалярные результаты и метрики

-
-
-

groups

-

Предвычисленные группировки данных

-
-
-
- - -
-

ID система

-
    -
  • Уникальные идентификаторы для результатов
  • -
  • Автоматическая генерация на основе параметров
  • -
  • Поиск результатов по типу Executor'а
  • -
  • Навигация по пространствам данных
  • -
-
-# Формат ID: ClassName⊔params_hash⊔key -"TTest⊔reliability_0.05⊔target_revenue" -
-
- - -
-

Управление состоянием

-
-
set_value()
-
-
ExperimentData
-
-
get_ids()
-
-
    -
  • Централизованное управление результатами
  • -
  • Типизированные пространства данных
  • -
  • Автоматическая навигация по результатам
  • -
-
- - -
-

Преимущества: Переиспользование

-
    -
  • Модульные компоненты для разных задач
  • -
  • Стандартизированные интерфейсы
  • -
  • Готовые пайплайны для типовых анализов
  • -
  • Композиция сложного из простого
  • -
-
-# Один Executor в разных экспериментах -t_test = TTest(compare_by="groups") -ab_experiment.add_executor(t_test) -aa_experiment.add_executor(t_test) -
-
- - -
-

Тестируемость и читаемость

-
    -
  • Изолированное тестирование компонентов
  • -
  • Мокирование зависимостей
  • -
  • Декларативное описание пайплайнов
  • -
  • Самодокументирующийся код через роли
  • -
-
-def test_t_test(): - executor = TTest(compare_by="groups") - result = executor.execute(test_data) - assert result.analysis_tables[...]["pass"] -
-
- - - -
- - - - \ No newline at end of file