analytics-toolkit 1.3.7.6__tar.gz → 1.3.8.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/PKG-INFO +18 -4
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/README.md +17 -3
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/api.py +42 -2
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/cuped.py +70 -11
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/metrics.py +4 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/parallel.py +41 -2
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/rows.py +200 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/cli.py +34 -1
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/__init__.py +19 -58
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/base.py +5 -1
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/clickhouse.py +3 -1
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/__init__.py +2 -2
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/lifecycle.py +33 -15
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/options.py +1 -24
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/__init__.py +1 -6
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/get_sql_connection.py +3 -26
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/__init__.py +0 -4
- analytics_toolkit-1.3.8.2/analytics_toolkit/sql/ddl/api.py +635 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/models.py +3 -6
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/__init__.py +6 -10
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/__init__.py +2 -2
- analytics_toolkit-1.3.8.2/analytics_toolkit/sql/dml/io/cancel_queries.py +211 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/execute_read.py +4 -4
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/execute_sql.py +4 -4
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/read_sql.py +8 -8
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/load_df.py +20 -33
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/models.py +0 -1
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/stage.py +7 -6
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/__init__.py +6 -19
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/_basic_ops.py +5 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/api.py +3 -4
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/ch_create_table_as.py +73 -69
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/create_table_from_sql.py +33 -20
- analytics_toolkit-1.3.8.2/analytics_toolkit/sql/dml/table/drop_tables.py +411 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/maintenance.py +13 -5
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/models.py +4 -23
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/partitions.py +34 -21
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/write_modes.py +11 -11
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/api.py +3 -15
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/finalize.py +0 -3
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/runtime/models.py +0 -1
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/tasks.py +21 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/PKG-INFO +18 -4
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/SOURCES.txt +5 -6
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/pyproject.toml +1 -1
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_metrics.py +161 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_parallel.py +172 -7
- analytics_toolkit-1.3.8.2/tests/test_rag_docs.py +264 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_architecture.py +82 -9
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_async.py +204 -69
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_backend_adapters.py +26 -14
- analytics_toolkit-1.3.8.2/tests/test_sql_cancel_queries.py +363 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_ch_create_table_as.py +48 -1
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_connection_config.py +53 -60
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_create_table_from_sql.py +117 -2
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_drop_many_partitions.py +39 -75
- analytics_toolkit-1.3.7.6/tests/test_sql_ch_drop_table.py → analytics_toolkit-1.3.8.2/tests/test_sql_drop_tables.py +96 -44
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_gp_create_partitions.py +101 -85
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_improvements.py +115 -67
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_load_table.py +26 -24
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_parallel.py +179 -48
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_retries.py +9 -2
- analytics_toolkit-1.3.7.6/analytics_toolkit/sql/ddl/api.py +0 -246
- analytics_toolkit-1.3.7.6/analytics_toolkit/sql/dml/io/gp_cancel.py +0 -83
- analytics_toolkit-1.3.7.6/analytics_toolkit/sql/dml/table/ch_drop_table.py +0 -214
- analytics_toolkit-1.3.7.6/analytics_toolkit/sql/dml/table/ch_full_table_move.py +0 -1096
- analytics_toolkit-1.3.7.6/tests/test_sql_ch_full_table_move.py +0 -463
- analytics_toolkit-1.3.7.6/tests/test_sql_gp_cancel.py +0 -223
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/LICENSE +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/bootstrap.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/constants.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/formatter.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/outliers.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/planning.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/ratio.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/split.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/stats.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/validation.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/dates/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/dates/dates.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/dates/functions.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/excel/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/excel/long_format.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/general/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/general/logging.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/general/read_file.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/dbapi.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/gp.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/registry.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/trino.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/utils.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/backend_adapters.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/wait.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/config.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/errors.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/protocols.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/capabilities.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/identifiers.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/types.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/builders.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/clickhouse.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/extract_ddl.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/identifiers.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/schema.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/models.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/load_sql_table.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/table_validation.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/attempt.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/estimate.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/stage.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/io/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/io/source.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/runtime/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/runtime/retry.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/schema.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/labels.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/operation_runner.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/plan_steps.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/plans.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/query_timing.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/metadata/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/metadata/show_tables.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/metadata/table_info.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/__init__.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/async_sql.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/parallel_sql.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/pipeline.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/dependency_links.txt +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/entry_points.txt +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/requires.txt +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/top_level.txt +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/setup.cfg +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_format.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_split.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_dates.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_excel_long_format.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_general_logging.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_general_read_file.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_execute_read.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_extract_ddl.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_show_tables.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_clickhouse.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_row_batches.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_schema.py +0 -0
- {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_source_batches.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: analytics-toolkit
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.8.2
|
|
4
4
|
Summary: Shared Python utilities for SQL, Excel, and date helpers.
|
|
5
5
|
Author: analytics_toolkit contributors
|
|
6
6
|
License: MIT License
|
|
@@ -75,7 +75,7 @@ Python toolkit for AB-test analysis, SQL workflows, Excel reports, and date help
|
|
|
75
75
|
|
|
76
76
|
[Documentation Overview](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/README.md)
|
|
77
77
|
|
|
78
|
-
**Version:** `1.3.
|
|
78
|
+
**Version:** `1.3.8.2`<br>
|
|
79
79
|
**Depends:** Python (`>=3.8,<3.15`)<br>
|
|
80
80
|
**Imports:** [clickhouse-connect](https://pypi.org/project/clickhouse-connect/) (`>=0.5.14,<1`), [lz4](https://pypi.org/project/lz4/) (`>=4.3.2,<5`), [numpy](https://pypi.org/project/numpy/) (`>=1.24.2,<2`), [openpyxl](https://pypi.org/project/openpyxl/) (`>=3.1.1,<4`), [orjson](https://pypi.org/project/orjson/) (`>=3.8.7,<4`), [pandas](https://pypi.org/project/pandas/) (`>=1.4.4,<3`), [psycopg2-binary](https://pypi.org/project/psycopg2-binary/) (`>=2.9.5,<3`), [python-dateutil](https://pypi.org/project/python-dateutil/) (`>=2.8.2,<3`), [pytz](https://pypi.org/project/pytz/) (`>=2022.7`), [requests](https://pypi.org/project/requests/) (`>=2.28.2,<3`), [scipy](https://pypi.org/project/scipy/) (`>=1.10.1,<2`), [sqlglot](https://pypi.org/project/sqlglot/) (`>=20,<31`), [sqlparse](https://pypi.org/project/sqlparse/) (`>=0.4.3,<1`), [tqdm](https://pypi.org/project/tqdm/) (`>=4.65.0,<5`), [trino](https://pypi.org/project/trino/) (`>=0.320,<1`), [zstandard](https://pypi.org/project/zstandard/) (`>=0.20.0,<1`)<br>
|
|
81
81
|
**Suggests:** [apache-airflow](https://pypi.org/project/apache-airflow/) (`>=2.4,<3`; optional extra `airflow`)<br>
|
|
@@ -130,8 +130,22 @@ rows = sql.transfer(
|
|
|
130
130
|
)
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
Aliases can point to the same backend type, so Greenplum-to-Greenplum transfers
|
|
134
|
+
work the same way.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
rows = sql.transfer(
|
|
138
|
+
from_db="gp_sales",
|
|
139
|
+
to_db="gp_finance",
|
|
140
|
+
from_sql="select user_id, order_id, amount from mart.sales_orders",
|
|
141
|
+
to_table="finance.sales_orders_copy",
|
|
142
|
+
write_mode="replace",
|
|
143
|
+
batch_size=50_000,
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- `sql.read`: run a query and return a dataframe.
|
|
148
|
+
- `sql.execute`: run DDL or DML without returning a dataframe.
|
|
135
149
|
- `sql.execute_read`: run setup SQL and return the final result as a dataframe.
|
|
136
150
|
- `sql.load_df`: load a pandas dataframe into a configured backend table.
|
|
137
151
|
- `sql.transfer`: move rows from a source query to a target table across backends.
|
|
@@ -6,7 +6,7 @@ Python toolkit for AB-test analysis, SQL workflows, Excel reports, and date help
|
|
|
6
6
|
|
|
7
7
|
[Documentation Overview](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/README.md)
|
|
8
8
|
|
|
9
|
-
**Version:** `1.3.
|
|
9
|
+
**Version:** `1.3.8.2`<br>
|
|
10
10
|
**Depends:** Python (`>=3.8,<3.15`)<br>
|
|
11
11
|
**Imports:** [clickhouse-connect](https://pypi.org/project/clickhouse-connect/) (`>=0.5.14,<1`), [lz4](https://pypi.org/project/lz4/) (`>=4.3.2,<5`), [numpy](https://pypi.org/project/numpy/) (`>=1.24.2,<2`), [openpyxl](https://pypi.org/project/openpyxl/) (`>=3.1.1,<4`), [orjson](https://pypi.org/project/orjson/) (`>=3.8.7,<4`), [pandas](https://pypi.org/project/pandas/) (`>=1.4.4,<3`), [psycopg2-binary](https://pypi.org/project/psycopg2-binary/) (`>=2.9.5,<3`), [python-dateutil](https://pypi.org/project/python-dateutil/) (`>=2.8.2,<3`), [pytz](https://pypi.org/project/pytz/) (`>=2022.7`), [requests](https://pypi.org/project/requests/) (`>=2.28.2,<3`), [scipy](https://pypi.org/project/scipy/) (`>=1.10.1,<2`), [sqlglot](https://pypi.org/project/sqlglot/) (`>=20,<31`), [sqlparse](https://pypi.org/project/sqlparse/) (`>=0.4.3,<1`), [tqdm](https://pypi.org/project/tqdm/) (`>=4.65.0,<5`), [trino](https://pypi.org/project/trino/) (`>=0.320,<1`), [zstandard](https://pypi.org/project/zstandard/) (`>=0.20.0,<1`)<br>
|
|
12
12
|
**Suggests:** [apache-airflow](https://pypi.org/project/apache-airflow/) (`>=2.4,<3`; optional extra `airflow`)<br>
|
|
@@ -61,8 +61,22 @@ rows = sql.transfer(
|
|
|
61
61
|
)
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
Aliases can point to the same backend type, so Greenplum-to-Greenplum transfers
|
|
65
|
+
work the same way.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
rows = sql.transfer(
|
|
69
|
+
from_db="gp_sales",
|
|
70
|
+
to_db="gp_finance",
|
|
71
|
+
from_sql="select user_id, order_id, amount from mart.sales_orders",
|
|
72
|
+
to_table="finance.sales_orders_copy",
|
|
73
|
+
write_mode="replace",
|
|
74
|
+
batch_size=50_000,
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `sql.read`: run a query and return a dataframe.
|
|
79
|
+
- `sql.execute`: run DDL or DML without returning a dataframe.
|
|
66
80
|
- `sql.execute_read`: run setup SQL and return the final result as a dataframe.
|
|
67
81
|
- `sql.load_df`: load a pandas dataframe into a configured backend table.
|
|
68
82
|
- `sql.transfer`: move rows from a source query to a target table across backends.
|
|
@@ -6,10 +6,15 @@ from analytics_toolkit.general import time_print
|
|
|
6
6
|
|
|
7
7
|
from .bootstrap import _apply_multiple_comparisons_adjustment
|
|
8
8
|
from .constants import DEFAULT_ALPHA, DEFAULT_POWER
|
|
9
|
-
from .cuped import _compute_cuped_statistics
|
|
9
|
+
from .cuped import _compute_cuped_statistics, _prepare_cuped_context
|
|
10
10
|
from .outliers import _build_outlier_context
|
|
11
11
|
from .ratio import _normalize_ratio_metrics
|
|
12
|
-
from .rows import
|
|
12
|
+
from .rows import (
|
|
13
|
+
_build_comparisons,
|
|
14
|
+
_build_metric_definitions,
|
|
15
|
+
_build_metric_row,
|
|
16
|
+
_prepare_metric_context,
|
|
17
|
+
)
|
|
13
18
|
from .stats import _compute_mde_from_standard_error, _safe_relative
|
|
14
19
|
from .validation import (
|
|
15
20
|
_validate_input_columns,
|
|
@@ -105,6 +110,15 @@ def compute_test_metrics(
|
|
|
105
110
|
ratio_specs = _normalize_ratio_metrics(df, ratio_metrics, reserved_columns={group, user_id})
|
|
106
111
|
comparisons = _build_comparisons(group_names, control, test_vs_test=test_vs_test)
|
|
107
112
|
metric_definitions = _build_metric_definitions(metric_columns, ratio_specs)
|
|
113
|
+
group_values = df[group].to_numpy()
|
|
114
|
+
group_masks = {group_name: group_values == group_name for group_name in group_names}
|
|
115
|
+
comparison_frames = {
|
|
116
|
+
(test_group, baseline_group): df.loc[
|
|
117
|
+
group_masks[test_group] | group_masks[baseline_group],
|
|
118
|
+
[user_id, group],
|
|
119
|
+
].copy()
|
|
120
|
+
for test_group, baseline_group in comparisons
|
|
121
|
+
}
|
|
108
122
|
time_print(
|
|
109
123
|
"compute_test_metrics: setup complete "
|
|
110
124
|
f"groups={len(group_names)} comparisons={len(comparisons)} "
|
|
@@ -129,6 +143,26 @@ def compute_test_metrics(
|
|
|
129
143
|
)
|
|
130
144
|
time_print("compute_test_metrics: outlier contexts complete")
|
|
131
145
|
|
|
146
|
+
prepared_metric_contexts: dict[str, dict[str, object]] = {}
|
|
147
|
+
prepared_cuped_contexts: dict[str, dict[str, object]] = {}
|
|
148
|
+
for metric_definition in metric_definitions:
|
|
149
|
+
metric_key = str(metric_definition["metric_key"])
|
|
150
|
+
outlier_context = metric_definition.get("_outlier_context")
|
|
151
|
+
prepared_metric_contexts[metric_key] = _prepare_metric_context(
|
|
152
|
+
df=df,
|
|
153
|
+
metric_definition=metric_definition,
|
|
154
|
+
outlier_context=outlier_context,
|
|
155
|
+
)
|
|
156
|
+
if pre_exp_metrics_df is not None:
|
|
157
|
+
prepared_cuped_contexts[metric_key] = _prepare_cuped_context(
|
|
158
|
+
df=df,
|
|
159
|
+
pre_exp_metrics_df=pre_exp_metrics_df,
|
|
160
|
+
user_id_column=user_id,
|
|
161
|
+
metric_definition=metric_definition,
|
|
162
|
+
outlier_context=outlier_context,
|
|
163
|
+
pre_outlier_context=metric_definition.get("_pre_outlier_context"),
|
|
164
|
+
)
|
|
165
|
+
|
|
132
166
|
rows: list[dict[str, object]] = []
|
|
133
167
|
for test_group, baseline_group in comparisons:
|
|
134
168
|
time_print(f"compute_test_metrics: comparison {test_group} vs {baseline_group}")
|
|
@@ -146,6 +180,8 @@ def compute_test_metrics(
|
|
|
146
180
|
mde_alpha=mde_alpha,
|
|
147
181
|
mde_power=mde_power,
|
|
148
182
|
outlier_context=outlier_context,
|
|
183
|
+
prepared_metric_context=prepared_metric_contexts.get(metric_name),
|
|
184
|
+
group_masks=group_masks,
|
|
149
185
|
)
|
|
150
186
|
row["_comparison_key"] = (test_group, baseline_group)
|
|
151
187
|
if pre_exp_metrics_df is not None:
|
|
@@ -160,6 +196,10 @@ def compute_test_metrics(
|
|
|
160
196
|
metric_definition=metric_definition,
|
|
161
197
|
outlier_context=outlier_context,
|
|
162
198
|
pre_outlier_context=metric_definition.get("_pre_outlier_context"),
|
|
199
|
+
prepared_cuped_context={
|
|
200
|
+
**prepared_cuped_contexts[metric_name],
|
|
201
|
+
"comparison_frame": comparison_frames[(test_group, baseline_group)],
|
|
202
|
+
},
|
|
163
203
|
)
|
|
164
204
|
row["mde_abs CUPED"] = _compute_mde_from_standard_error(
|
|
165
205
|
standard_error=row["s.e. CUPED"],
|
|
@@ -41,29 +41,68 @@ def _compute_cuped_p_value(
|
|
|
41
41
|
return p_value
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def
|
|
44
|
+
def _prepare_cuped_context(
|
|
45
45
|
df: pd.DataFrame,
|
|
46
46
|
pre_exp_metrics_df: pd.DataFrame,
|
|
47
|
-
group_column: str,
|
|
48
47
|
user_id_column: str,
|
|
49
|
-
baseline_group: str,
|
|
50
|
-
test_group: str,
|
|
51
48
|
metric_definition: dict[str, object],
|
|
52
49
|
outlier_context: dict[str, object] | None = None,
|
|
53
50
|
pre_outlier_context: dict[str, object] | None = None,
|
|
54
|
-
) ->
|
|
55
|
-
|
|
51
|
+
) -> dict[str, object]:
|
|
52
|
+
exp_values, exp_error = _build_metric_values_by_user(
|
|
56
53
|
df=df,
|
|
57
|
-
pre_exp_metrics_df=pre_exp_metrics_df,
|
|
58
54
|
user_id_column=user_id_column,
|
|
59
|
-
group_column=group_column,
|
|
60
|
-
baseline_group=baseline_group,
|
|
61
|
-
test_group=test_group,
|
|
62
55
|
metric_definition=metric_definition,
|
|
56
|
+
value_column="metric_exp",
|
|
63
57
|
outlier_context=outlier_context,
|
|
64
|
-
pre_outlier_context=pre_outlier_context,
|
|
65
58
|
)
|
|
59
|
+
pre_values, pre_error = _build_metric_values_by_user(
|
|
60
|
+
df=pre_exp_metrics_df,
|
|
61
|
+
user_id_column=user_id_column,
|
|
62
|
+
metric_definition=metric_definition,
|
|
63
|
+
value_column="metric_pre",
|
|
64
|
+
outlier_context=pre_outlier_context,
|
|
65
|
+
)
|
|
66
|
+
return {
|
|
67
|
+
"exp_values": exp_values.set_index(user_id_column) if exp_error is None else None,
|
|
68
|
+
"pre_values": pre_values.set_index(user_id_column) if pre_error is None else None,
|
|
69
|
+
"exp_error": exp_error,
|
|
70
|
+
"pre_error": pre_error,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _compute_cuped_statistics(
|
|
75
|
+
df: pd.DataFrame,
|
|
76
|
+
pre_exp_metrics_df: pd.DataFrame,
|
|
77
|
+
group_column: str,
|
|
78
|
+
user_id_column: str,
|
|
79
|
+
baseline_group: str,
|
|
80
|
+
test_group: str,
|
|
81
|
+
metric_definition: dict[str, object],
|
|
82
|
+
outlier_context: dict[str, object] | None = None,
|
|
83
|
+
pre_outlier_context: dict[str, object] | None = None,
|
|
84
|
+
prepared_cuped_context: dict[str, object] | None = None,
|
|
85
|
+
) -> tuple[float, float]:
|
|
66
86
|
metric_name = str(metric_definition["metric_key"])
|
|
87
|
+
if prepared_cuped_context is None:
|
|
88
|
+
cuped_frame, reason = _build_cuped_frame(
|
|
89
|
+
df=df,
|
|
90
|
+
pre_exp_metrics_df=pre_exp_metrics_df,
|
|
91
|
+
user_id_column=user_id_column,
|
|
92
|
+
group_column=group_column,
|
|
93
|
+
baseline_group=baseline_group,
|
|
94
|
+
test_group=test_group,
|
|
95
|
+
metric_definition=metric_definition,
|
|
96
|
+
outlier_context=outlier_context,
|
|
97
|
+
pre_outlier_context=pre_outlier_context,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
cuped_frame, reason = _build_cuped_frame_from_prepared_context(
|
|
101
|
+
comparison_frame=prepared_cuped_context["comparison_frame"],
|
|
102
|
+
user_id_column=user_id_column,
|
|
103
|
+
prepared_cuped_context=prepared_cuped_context,
|
|
104
|
+
)
|
|
105
|
+
|
|
67
106
|
if reason is not None:
|
|
68
107
|
warnings.warn(
|
|
69
108
|
(
|
|
@@ -93,6 +132,26 @@ def _compute_cuped_statistics(
|
|
|
93
132
|
return p_value, standard_error
|
|
94
133
|
|
|
95
134
|
|
|
135
|
+
def _build_cuped_frame_from_prepared_context(
|
|
136
|
+
comparison_frame: pd.DataFrame,
|
|
137
|
+
user_id_column: str,
|
|
138
|
+
prepared_cuped_context: dict[str, object],
|
|
139
|
+
) -> tuple[pd.DataFrame | None, str | None]:
|
|
140
|
+
exp_values = prepared_cuped_context.get("exp_values")
|
|
141
|
+
if exp_values is None:
|
|
142
|
+
return None, f"experiment metric values are unavailable: {prepared_cuped_context.get('exp_error')}"
|
|
143
|
+
pre_values = prepared_cuped_context.get("pre_values")
|
|
144
|
+
if pre_values is None:
|
|
145
|
+
return None, f"pre-experiment metric values are unavailable: {prepared_cuped_context.get('pre_error')}"
|
|
146
|
+
|
|
147
|
+
cuped_frame = comparison_frame.copy()
|
|
148
|
+
cuped_frame = cuped_frame.join(exp_values, on=user_id_column).join(pre_values, on=user_id_column)
|
|
149
|
+
cuped_frame = cuped_frame.dropna(subset=["metric_exp", "metric_pre"]).reset_index(drop=True)
|
|
150
|
+
if cuped_frame.empty:
|
|
151
|
+
return None, "no overlapping non-missing experiment/pre-experiment observations"
|
|
152
|
+
return cuped_frame, None
|
|
153
|
+
|
|
154
|
+
|
|
96
155
|
def _build_cuped_frame(
|
|
97
156
|
df: pd.DataFrame,
|
|
98
157
|
pre_exp_metrics_df: pd.DataFrame,
|
{analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/metrics.py
RENAMED
|
@@ -29,6 +29,7 @@ from .cuped import (
|
|
|
29
29
|
_compute_cuped_p_value_from_frame,
|
|
30
30
|
_compute_cuped_statistics,
|
|
31
31
|
_compute_cuped_statistics_from_frame,
|
|
32
|
+
_prepare_cuped_context,
|
|
32
33
|
)
|
|
33
34
|
from .outliers import (
|
|
34
35
|
_apply_outliers_to_agg_ratio_components,
|
|
@@ -59,6 +60,7 @@ from .rows import (
|
|
|
59
60
|
_build_metric_definitions,
|
|
60
61
|
_build_metric_row,
|
|
61
62
|
_build_ratio_metric_row,
|
|
63
|
+
_prepare_metric_context,
|
|
62
64
|
)
|
|
63
65
|
from .stats import (
|
|
64
66
|
_both_present,
|
|
@@ -110,6 +112,8 @@ __all__ = [
|
|
|
110
112
|
"_build_ratio_valid_mask",
|
|
111
113
|
"_build_ratio_valid_mask_from_arrays",
|
|
112
114
|
"_build_value_outlier_mask",
|
|
115
|
+
"_prepare_cuped_context",
|
|
116
|
+
"_prepare_metric_context",
|
|
113
117
|
"_compute_agg_ratio_diff_standard_error",
|
|
114
118
|
"_compute_agg_ratio_group_stats",
|
|
115
119
|
"_compute_agg_ratio_group_stats_arrays",
|
{analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/parallel.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import contextvars
|
|
4
|
+
import inspect
|
|
4
5
|
from collections.abc import Mapping
|
|
5
6
|
from contextlib import ExitStack
|
|
6
7
|
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
|
|
@@ -20,6 +21,9 @@ from .api import compute_test_metrics
|
|
|
20
21
|
async_sql = sql_facade.async_sql
|
|
21
22
|
|
|
22
23
|
_SQL_DATAFRAME_FIELDS = frozenset({"df", "pre_exp_df", "pre_exp_metrics_df"})
|
|
24
|
+
_COMPUTE_TEST_METRICS_FIELDS = frozenset(
|
|
25
|
+
inspect.signature(compute_test_metrics).parameters
|
|
26
|
+
)
|
|
23
27
|
_DEFAULT_HARD_CONCURRENCY_CAP = 10
|
|
24
28
|
_CONCURRENCY_STATE: contextvars.ContextVar["_ConcurrencyState | None"] = (
|
|
25
29
|
contextvars.ContextVar("analytics_toolkit_ab_parallel_concurrency", default=None)
|
|
@@ -127,9 +131,11 @@ def parallel_compute_metrics_from_sql(
|
|
|
127
131
|
soft_concurrency_cap: int | None = None,
|
|
128
132
|
hard_concurrency_cap: int = _DEFAULT_HARD_CONCURRENCY_CAP,
|
|
129
133
|
progress: bool = False,
|
|
134
|
+
**metric_defaults: Any,
|
|
130
135
|
) -> dict[str, pd.DataFrame | str]:
|
|
131
136
|
"""Load SQL-backed task dataframes, then run ``parallel_compute_metrics``."""
|
|
132
137
|
_validate_start_comment("start_comment", start_comment)
|
|
138
|
+
metric_defaults = _validate_metric_defaults(metric_defaults)
|
|
133
139
|
task_defs = _validate_sql_tasks(tasks)
|
|
134
140
|
_validate_concurrency(concurrency)
|
|
135
141
|
_validate_optional_soft_concurrency_cap(soft_concurrency_cap)
|
|
@@ -191,7 +197,8 @@ def parallel_compute_metrics_from_sql(
|
|
|
191
197
|
sql_failures[name] = df_result
|
|
192
198
|
continue
|
|
193
199
|
|
|
194
|
-
metric_kwargs = dict(
|
|
200
|
+
metric_kwargs = dict(metric_defaults)
|
|
201
|
+
metric_kwargs.update(kwargs)
|
|
195
202
|
metric_kwargs["df"] = df_result
|
|
196
203
|
if pre_exp_sql is not None:
|
|
197
204
|
pre_exp_df_result = sql_results[_sql_read_task_name(name, "pre_exp_sql")]
|
|
@@ -424,6 +431,38 @@ def _validate_sql_task_spec(
|
|
|
424
431
|
return name, kwargs, sql, pre_exp_sql, start_comment
|
|
425
432
|
|
|
426
433
|
|
|
434
|
+
def _validate_metric_defaults(metric_defaults: Mapping[str, Any]) -> dict[str, Any]:
|
|
435
|
+
if not metric_defaults:
|
|
436
|
+
return {}
|
|
437
|
+
|
|
438
|
+
dataframe_fields = sorted(_SQL_DATAFRAME_FIELDS.intersection(metric_defaults))
|
|
439
|
+
if dataframe_fields:
|
|
440
|
+
fields = ", ".join(dataframe_fields)
|
|
441
|
+
raise ValueError(
|
|
442
|
+
"parallel_compute_metrics_from_sql top-level metric defaults cannot "
|
|
443
|
+
f"define SQL-backed dataframe field(s): {fields}."
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
supported_fields = _COMPUTE_TEST_METRICS_FIELDS - {
|
|
447
|
+
"df",
|
|
448
|
+
"pre_exp_metrics_df",
|
|
449
|
+
}
|
|
450
|
+
unexpected_fields = sorted(set(metric_defaults) - supported_fields)
|
|
451
|
+
if unexpected_fields:
|
|
452
|
+
quoted_fields = ", ".join(repr(field) for field in unexpected_fields)
|
|
453
|
+
if len(unexpected_fields) == 1:
|
|
454
|
+
raise TypeError(
|
|
455
|
+
"parallel_compute_metrics_from_sql() got an unexpected keyword "
|
|
456
|
+
f"argument {quoted_fields}"
|
|
457
|
+
)
|
|
458
|
+
raise TypeError(
|
|
459
|
+
"parallel_compute_metrics_from_sql() got unexpected keyword "
|
|
460
|
+
f"arguments: {quoted_fields}"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
return dict(metric_defaults)
|
|
464
|
+
|
|
465
|
+
|
|
427
466
|
def _make_sql_read_task(
|
|
428
467
|
*,
|
|
429
468
|
name: str,
|
|
@@ -435,7 +474,7 @@ def _make_sql_read_task(
|
|
|
435
474
|
task = {
|
|
436
475
|
"name": _sql_read_task_name(name, field),
|
|
437
476
|
"type": "read",
|
|
438
|
-
"
|
|
477
|
+
"db_key": db,
|
|
439
478
|
"query": query,
|
|
440
479
|
}
|
|
441
480
|
if start_comment is not _MISSING:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from itertools import combinations
|
|
4
4
|
import math
|
|
5
5
|
|
|
6
|
+
import numpy as np
|
|
6
7
|
import pandas as pd
|
|
7
8
|
|
|
8
9
|
from .constants import DEFAULT_ALPHA, DEFAULT_POWER
|
|
@@ -71,6 +72,75 @@ def _build_metric_definitions(
|
|
|
71
72
|
return metric_definitions
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
def _prepare_metric_context(
|
|
76
|
+
df: pd.DataFrame,
|
|
77
|
+
metric_definition: dict[str, object],
|
|
78
|
+
outlier_context: dict[str, object] | None = None,
|
|
79
|
+
) -> dict[str, object]:
|
|
80
|
+
if outlier_context is None:
|
|
81
|
+
outlier_context = metric_definition.get("_outlier_context")
|
|
82
|
+
|
|
83
|
+
if metric_definition["kind"] == "mean":
|
|
84
|
+
metric_values = _get_numeric_metric_series(df, str(metric_definition["column"]))
|
|
85
|
+
metric_values, outlier_mask = _apply_outliers_to_values(
|
|
86
|
+
metric_values,
|
|
87
|
+
outlier_context,
|
|
88
|
+
)
|
|
89
|
+
return {
|
|
90
|
+
"kind": "mean",
|
|
91
|
+
"metric_key": str(metric_definition["metric_key"]),
|
|
92
|
+
"column": str(metric_definition["column"]),
|
|
93
|
+
"values": metric_values.to_numpy(dtype=float),
|
|
94
|
+
"outlier_mask": outlier_mask.to_numpy(dtype=bool),
|
|
95
|
+
"outlier_context": outlier_context,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ratio_spec = dict(metric_definition["ratio_spec"])
|
|
99
|
+
numerator = _get_numeric_metric_series(df, ratio_spec["numerator"])
|
|
100
|
+
denominator = _get_numeric_metric_series(df, ratio_spec["denominator"])
|
|
101
|
+
if ratio_spec["level"] == "user":
|
|
102
|
+
valid_mask = _build_ratio_valid_mask(
|
|
103
|
+
numerator=numerator,
|
|
104
|
+
denominator=denominator,
|
|
105
|
+
level=ratio_spec["level"],
|
|
106
|
+
)
|
|
107
|
+
values = pd.Series(np.nan, index=df.index, dtype=float)
|
|
108
|
+
values.loc[valid_mask] = numerator.loc[valid_mask] / denominator.loc[valid_mask]
|
|
109
|
+
values, outlier_mask = _apply_outliers_to_values(
|
|
110
|
+
values,
|
|
111
|
+
outlier_context,
|
|
112
|
+
)
|
|
113
|
+
return {
|
|
114
|
+
"kind": "ratio",
|
|
115
|
+
"metric_key": str(metric_definition["metric_key"]),
|
|
116
|
+
"ratio_spec": ratio_spec,
|
|
117
|
+
"values": values.to_numpy(dtype=float),
|
|
118
|
+
"outlier_mask": outlier_mask.to_numpy(dtype=bool),
|
|
119
|
+
"outlier_context": outlier_context,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
numerator, denominator, outlier_mask = _apply_outliers_to_agg_ratio_components(
|
|
123
|
+
numerator=numerator,
|
|
124
|
+
denominator=denominator,
|
|
125
|
+
outlier_context=outlier_context,
|
|
126
|
+
)
|
|
127
|
+
valid_mask = _build_ratio_valid_mask(
|
|
128
|
+
numerator=numerator,
|
|
129
|
+
denominator=denominator,
|
|
130
|
+
level=ratio_spec["level"],
|
|
131
|
+
)
|
|
132
|
+
return {
|
|
133
|
+
"kind": "ratio",
|
|
134
|
+
"metric_key": str(metric_definition["metric_key"]),
|
|
135
|
+
"ratio_spec": ratio_spec,
|
|
136
|
+
"numerator": numerator.to_numpy(dtype=float),
|
|
137
|
+
"denominator": denominator.to_numpy(dtype=float),
|
|
138
|
+
"valid_mask": valid_mask.to_numpy(dtype=bool),
|
|
139
|
+
"outlier_mask": outlier_mask.to_numpy(dtype=bool),
|
|
140
|
+
"outlier_context": outlier_context,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
74
144
|
def _build_metric_row(
|
|
75
145
|
df: pd.DataFrame,
|
|
76
146
|
group_column: str,
|
|
@@ -80,7 +150,22 @@ def _build_metric_row(
|
|
|
80
150
|
mde_alpha: float,
|
|
81
151
|
mde_power: float,
|
|
82
152
|
outlier_context: dict[str, object] | None = None,
|
|
153
|
+
prepared_metric_context: dict[str, object] | None = None,
|
|
154
|
+
group_masks: dict[str, np.ndarray] | None = None,
|
|
83
155
|
) -> dict[str, object]:
|
|
156
|
+
if prepared_metric_context is not None:
|
|
157
|
+
return _build_metric_row_from_prepared_context(
|
|
158
|
+
df=df,
|
|
159
|
+
group_column=group_column,
|
|
160
|
+
baseline_group=baseline_group,
|
|
161
|
+
test_group=test_group,
|
|
162
|
+
metric_definition=metric_definition,
|
|
163
|
+
prepared_metric_context=prepared_metric_context,
|
|
164
|
+
mde_alpha=mde_alpha,
|
|
165
|
+
mde_power=mde_power,
|
|
166
|
+
group_masks=group_masks,
|
|
167
|
+
)
|
|
168
|
+
|
|
84
169
|
if outlier_context is None:
|
|
85
170
|
outlier_context = metric_definition.get("_outlier_context")
|
|
86
171
|
if metric_definition["kind"] == "mean":
|
|
@@ -123,6 +208,121 @@ def _build_metric_row(
|
|
|
123
208
|
)
|
|
124
209
|
|
|
125
210
|
|
|
211
|
+
def _build_metric_row_from_prepared_context(
|
|
212
|
+
df: pd.DataFrame,
|
|
213
|
+
group_column: str,
|
|
214
|
+
baseline_group: str,
|
|
215
|
+
test_group: str,
|
|
216
|
+
metric_definition: dict[str, object],
|
|
217
|
+
prepared_metric_context: dict[str, object],
|
|
218
|
+
mde_alpha: float,
|
|
219
|
+
mde_power: float,
|
|
220
|
+
group_masks: dict[str, np.ndarray] | None = None,
|
|
221
|
+
) -> dict[str, object]:
|
|
222
|
+
if group_masks is None:
|
|
223
|
+
group_values = df[group_column].to_numpy()
|
|
224
|
+
group_masks = {
|
|
225
|
+
baseline_group: group_values == baseline_group,
|
|
226
|
+
test_group: group_values == test_group,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
baseline_mask = np.asarray(group_masks[baseline_group], dtype=bool)
|
|
230
|
+
test_mask = np.asarray(group_masks[test_group], dtype=bool)
|
|
231
|
+
|
|
232
|
+
if prepared_metric_context["kind"] == "mean":
|
|
233
|
+
metric_name = str(prepared_metric_context["metric_key"])
|
|
234
|
+
metric_values = np.asarray(prepared_metric_context["values"], dtype=float)
|
|
235
|
+
outlier_mask = np.asarray(prepared_metric_context["outlier_mask"], dtype=bool)
|
|
236
|
+
baseline_values = pd.Series(metric_values[baseline_mask]).dropna()
|
|
237
|
+
test_values = pd.Series(metric_values[test_mask]).dropna()
|
|
238
|
+
return _build_mean_metric_row(
|
|
239
|
+
metric_name=metric_name,
|
|
240
|
+
metric_key=metric_name,
|
|
241
|
+
baseline_values=baseline_values,
|
|
242
|
+
test_values=test_values,
|
|
243
|
+
mde_alpha=mde_alpha,
|
|
244
|
+
mde_power=mde_power,
|
|
245
|
+
outliers_cutoff=_get_outlier_cutoff(prepared_metric_context.get("outlier_context")),
|
|
246
|
+
outliers_n_control=int(outlier_mask[baseline_mask].sum()),
|
|
247
|
+
outliers_n_test=int(outlier_mask[test_mask].sum()),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
ratio_spec = dict(metric_definition["ratio_spec"])
|
|
251
|
+
metric_key = str(prepared_metric_context["metric_key"])
|
|
252
|
+
if ratio_spec["level"] == "user":
|
|
253
|
+
metric_values = np.asarray(prepared_metric_context["values"], dtype=float)
|
|
254
|
+
outlier_mask = np.asarray(prepared_metric_context["outlier_mask"], dtype=bool)
|
|
255
|
+
baseline_values = pd.Series(metric_values[baseline_mask]).dropna()
|
|
256
|
+
test_values = pd.Series(metric_values[test_mask]).dropna()
|
|
257
|
+
return _build_mean_metric_row(
|
|
258
|
+
metric_name=metric_key,
|
|
259
|
+
metric_key=metric_key,
|
|
260
|
+
baseline_values=baseline_values,
|
|
261
|
+
test_values=test_values,
|
|
262
|
+
mde_alpha=mde_alpha,
|
|
263
|
+
mde_power=mde_power,
|
|
264
|
+
outliers_cutoff=_get_outlier_cutoff(prepared_metric_context.get("outlier_context")),
|
|
265
|
+
outliers_n_control=int(outlier_mask[baseline_mask].sum()),
|
|
266
|
+
outliers_n_test=int(outlier_mask[test_mask].sum()),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
numerator = np.asarray(prepared_metric_context["numerator"], dtype=float)
|
|
270
|
+
denominator = np.asarray(prepared_metric_context["denominator"], dtype=float)
|
|
271
|
+
valid_mask = np.asarray(prepared_metric_context["valid_mask"], dtype=bool)
|
|
272
|
+
outlier_mask = np.asarray(prepared_metric_context["outlier_mask"], dtype=bool)
|
|
273
|
+
baseline_valid_mask = baseline_mask & valid_mask
|
|
274
|
+
test_valid_mask = test_mask & valid_mask
|
|
275
|
+
baseline_frame = pd.DataFrame(
|
|
276
|
+
{"numerator": numerator[baseline_valid_mask], "denominator": denominator[baseline_valid_mask]}
|
|
277
|
+
)
|
|
278
|
+
test_frame = pd.DataFrame(
|
|
279
|
+
{"numerator": numerator[test_valid_mask], "denominator": denominator[test_valid_mask]}
|
|
280
|
+
)
|
|
281
|
+
outliers_n_control = int(outlier_mask[baseline_mask].sum())
|
|
282
|
+
outliers_n_test = int(outlier_mask[test_mask].sum())
|
|
283
|
+
baseline_stats = _compute_agg_ratio_group_stats(baseline_frame)
|
|
284
|
+
test_stats = _compute_agg_ratio_group_stats(test_frame)
|
|
285
|
+
|
|
286
|
+
delta_abs = math.nan
|
|
287
|
+
if _both_present(test_stats["ratio"], baseline_stats["ratio"]):
|
|
288
|
+
delta_abs = test_stats["ratio"] - baseline_stats["ratio"]
|
|
289
|
+
|
|
290
|
+
baseline_variance = _compute_agg_ratio_variance(baseline_frame, baseline_stats["ratio"])
|
|
291
|
+
test_variance = _compute_agg_ratio_variance(test_frame, test_stats["ratio"])
|
|
292
|
+
se_diff = math.nan
|
|
293
|
+
if not math.isnan(baseline_variance) and not math.isnan(test_variance):
|
|
294
|
+
se_diff = math.sqrt(baseline_variance + test_variance)
|
|
295
|
+
p_value = _compute_normal_p_value(delta_abs=delta_abs, standard_error=se_diff)
|
|
296
|
+
mde_abs = _compute_mde_from_standard_error(
|
|
297
|
+
standard_error=se_diff,
|
|
298
|
+
alpha=mde_alpha,
|
|
299
|
+
power=mde_power,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
"metric_name": metric_key,
|
|
304
|
+
"n0": int(baseline_stats["n"]),
|
|
305
|
+
"n1": int(test_stats["n"]),
|
|
306
|
+
"outliers_cutoff": _get_outlier_cutoff(prepared_metric_context.get("outlier_context")),
|
|
307
|
+
"outliers_n_control": outliers_n_control,
|
|
308
|
+
"outliers_n_test": outliers_n_test,
|
|
309
|
+
"metric_control": baseline_stats["ratio"],
|
|
310
|
+
"metric_test": test_stats["ratio"],
|
|
311
|
+
"variance_control": baseline_variance,
|
|
312
|
+
"variance_test": test_variance,
|
|
313
|
+
"delta_abs": delta_abs,
|
|
314
|
+
"delta_relative": _safe_relative(delta_abs, baseline_stats["ratio"]),
|
|
315
|
+
"mde_abs": mde_abs,
|
|
316
|
+
"mde_relative": _safe_relative(mde_abs, baseline_stats["ratio"]),
|
|
317
|
+
"s.e.": se_diff,
|
|
318
|
+
"p-value": p_value,
|
|
319
|
+
"s.e. bootstrap": math.nan,
|
|
320
|
+
"bootstrap_adj_p": math.nan,
|
|
321
|
+
"_metric_key": metric_key,
|
|
322
|
+
"_test_stat": _compute_studentized_statistic(delta_abs, se_diff),
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
126
326
|
def _build_mean_metric_row(
|
|
127
327
|
metric_name: str,
|
|
128
328
|
metric_key: str,
|
|
@@ -3,8 +3,33 @@ from __future__ import annotations
|
|
|
3
3
|
import argparse
|
|
4
4
|
import sys
|
|
5
5
|
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
def _prefer_local_package_path() -> None:
|
|
10
|
+
package = sys.modules.get(__package__)
|
|
11
|
+
package_path = getattr(package, "__path__", None)
|
|
12
|
+
if package_path is None:
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
local_path = str(Path(__file__).resolve().parent)
|
|
16
|
+
existing_paths = [str(path) for path in package_path]
|
|
17
|
+
package.__path__ = [
|
|
18
|
+
local_path,
|
|
19
|
+
*[path for path in existing_paths if not _same_path(path, local_path)],
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _same_path(left: str, right: str) -> bool:
|
|
24
|
+
try:
|
|
25
|
+
return Path(left).resolve() == Path(right).resolve()
|
|
26
|
+
except OSError:
|
|
27
|
+
return left == right
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_prefer_local_package_path()
|
|
31
|
+
|
|
32
|
+
from .sql.core.capabilities import format_support_matrix
|
|
8
33
|
from .sql.connection import validate_connections
|
|
9
34
|
|
|
10
35
|
|
|
@@ -35,6 +60,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
35
60
|
|
|
36
61
|
support_parser = sql_subparsers.add_parser("support-matrix")
|
|
37
62
|
support_parser.set_defaults(handler=_handle_sql_support_matrix)
|
|
63
|
+
|
|
38
64
|
return parser
|
|
39
65
|
|
|
40
66
|
|
|
@@ -59,5 +85,12 @@ def _handle_sql_support_matrix(args: argparse.Namespace) -> int:
|
|
|
59
85
|
return 0
|
|
60
86
|
|
|
61
87
|
|
|
88
|
+
def _display_path(path: Path) -> str:
|
|
89
|
+
try:
|
|
90
|
+
return str(path.relative_to(Path.cwd()))
|
|
91
|
+
except ValueError:
|
|
92
|
+
return str(path)
|
|
93
|
+
|
|
94
|
+
|
|
62
95
|
if __name__ == "__main__":
|
|
63
96
|
raise SystemExit(main(sys.argv[1:]))
|