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.
Files changed (153) hide show
  1. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/PKG-INFO +18 -4
  2. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/README.md +17 -3
  3. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/api.py +42 -2
  4. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/cuped.py +70 -11
  5. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/metrics.py +4 -0
  6. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/parallel.py +41 -2
  7. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/rows.py +200 -0
  8. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/cli.py +34 -1
  9. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/__init__.py +19 -58
  10. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/base.py +5 -1
  11. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/clickhouse.py +3 -1
  12. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/__init__.py +2 -2
  13. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/lifecycle.py +33 -15
  14. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/options.py +1 -24
  15. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/__init__.py +1 -6
  16. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/get_sql_connection.py +3 -26
  17. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/__init__.py +0 -4
  18. analytics_toolkit-1.3.8.2/analytics_toolkit/sql/ddl/api.py +635 -0
  19. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/models.py +3 -6
  20. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/__init__.py +6 -10
  21. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/__init__.py +2 -2
  22. analytics_toolkit-1.3.8.2/analytics_toolkit/sql/dml/io/cancel_queries.py +211 -0
  23. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/execute_read.py +4 -4
  24. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/execute_sql.py +4 -4
  25. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/read_sql.py +8 -8
  26. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/load_df.py +20 -33
  27. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/models.py +0 -1
  28. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/stage.py +7 -6
  29. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/__init__.py +6 -19
  30. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/_basic_ops.py +5 -0
  31. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/api.py +3 -4
  32. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/ch_create_table_as.py +73 -69
  33. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/create_table_from_sql.py +33 -20
  34. analytics_toolkit-1.3.8.2/analytics_toolkit/sql/dml/table/drop_tables.py +411 -0
  35. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/maintenance.py +13 -5
  36. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/models.py +4 -23
  37. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/partitions.py +34 -21
  38. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/write_modes.py +11 -11
  39. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/api.py +3 -15
  40. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/finalize.py +0 -3
  41. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/runtime/models.py +0 -1
  42. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/tasks.py +21 -0
  43. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/PKG-INFO +18 -4
  44. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/SOURCES.txt +5 -6
  45. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/pyproject.toml +1 -1
  46. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_metrics.py +161 -0
  47. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_parallel.py +172 -7
  48. analytics_toolkit-1.3.8.2/tests/test_rag_docs.py +264 -0
  49. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_architecture.py +82 -9
  50. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_async.py +204 -69
  51. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_backend_adapters.py +26 -14
  52. analytics_toolkit-1.3.8.2/tests/test_sql_cancel_queries.py +363 -0
  53. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_ch_create_table_as.py +48 -1
  54. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_connection_config.py +53 -60
  55. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_create_table_from_sql.py +117 -2
  56. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_drop_many_partitions.py +39 -75
  57. 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
  58. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_gp_create_partitions.py +101 -85
  59. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_improvements.py +115 -67
  60. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_load_table.py +26 -24
  61. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_parallel.py +179 -48
  62. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_retries.py +9 -2
  63. analytics_toolkit-1.3.7.6/analytics_toolkit/sql/ddl/api.py +0 -246
  64. analytics_toolkit-1.3.7.6/analytics_toolkit/sql/dml/io/gp_cancel.py +0 -83
  65. analytics_toolkit-1.3.7.6/analytics_toolkit/sql/dml/table/ch_drop_table.py +0 -214
  66. analytics_toolkit-1.3.7.6/analytics_toolkit/sql/dml/table/ch_full_table_move.py +0 -1096
  67. analytics_toolkit-1.3.7.6/tests/test_sql_ch_full_table_move.py +0 -463
  68. analytics_toolkit-1.3.7.6/tests/test_sql_gp_cancel.py +0 -223
  69. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/LICENSE +0 -0
  70. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/__init__.py +0 -0
  71. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/__init__.py +0 -0
  72. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/bootstrap.py +0 -0
  73. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/constants.py +0 -0
  74. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/formatter.py +0 -0
  75. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/outliers.py +0 -0
  76. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/planning.py +0 -0
  77. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/ratio.py +0 -0
  78. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/split.py +0 -0
  79. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/stats.py +0 -0
  80. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/validation.py +0 -0
  81. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/dates/__init__.py +0 -0
  82. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/dates/dates.py +0 -0
  83. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/dates/functions.py +0 -0
  84. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/excel/__init__.py +0 -0
  85. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/excel/long_format.py +0 -0
  86. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/general/__init__.py +0 -0
  87. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/general/logging.py +0 -0
  88. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/general/read_file.py +0 -0
  89. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/__init__.py +0 -0
  90. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/dbapi.py +0 -0
  91. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/gp.py +0 -0
  92. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/registry.py +0 -0
  93. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/trino.py +0 -0
  94. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/utils.py +0 -0
  95. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/backend_adapters.py +0 -0
  96. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/wait.py +0 -0
  97. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/config.py +0 -0
  98. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/errors.py +0 -0
  99. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/protocols.py +0 -0
  100. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/__init__.py +0 -0
  101. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/capabilities.py +0 -0
  102. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/identifiers.py +0 -0
  103. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/types.py +0 -0
  104. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/builders.py +0 -0
  105. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/clickhouse.py +0 -0
  106. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/extract_ddl.py +0 -0
  107. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/identifiers.py +0 -0
  108. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/schema.py +0 -0
  109. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/models.py +0 -0
  110. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/__init__.py +0 -0
  111. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/load_sql_table.py +0 -0
  112. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/table_validation.py +0 -0
  113. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/__init__.py +0 -0
  114. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/__init__.py +0 -0
  115. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/attempt.py +0 -0
  116. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/estimate.py +0 -0
  117. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/stage.py +0 -0
  118. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/io/__init__.py +0 -0
  119. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/io/source.py +0 -0
  120. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/runtime/__init__.py +0 -0
  121. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/runtime/retry.py +0 -0
  122. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/schema.py +0 -0
  123. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/__init__.py +0 -0
  124. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/labels.py +0 -0
  125. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/operation_runner.py +0 -0
  126. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/plan_steps.py +0 -0
  127. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/plans.py +0 -0
  128. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/query_timing.py +0 -0
  129. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/metadata/__init__.py +0 -0
  130. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/metadata/show_tables.py +0 -0
  131. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/metadata/table_info.py +0 -0
  132. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/__init__.py +0 -0
  133. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/async_sql.py +0 -0
  134. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/parallel_sql.py +0 -0
  135. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/pipeline.py +0 -0
  136. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/dependency_links.txt +0 -0
  137. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/entry_points.txt +0 -0
  138. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/requires.txt +0 -0
  139. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/top_level.txt +0 -0
  140. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/setup.cfg +0 -0
  141. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_format.py +0 -0
  142. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_split.py +0 -0
  143. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_dates.py +0 -0
  144. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_excel_long_format.py +0 -0
  145. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_general_logging.py +0 -0
  146. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_general_read_file.py +0 -0
  147. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_execute_read.py +0 -0
  148. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_extract_ddl.py +0 -0
  149. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_show_tables.py +0 -0
  150. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_clickhouse.py +0 -0
  151. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_row_batches.py +0 -0
  152. {analytics_toolkit-1.3.7.6 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_schema.py +0 -0
  153. {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.7.6
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.7.6`<br>
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
- - `sql.read` / `sql.read_sql`: run a query and return a dataframe.
134
- - `sql.execute` / `sql.execute_sql`: run DDL or DML without returning a dataframe.
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.7.6`<br>
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
- - `sql.read` / `sql.read_sql`: run a query and return a dataframe.
65
- - `sql.execute` / `sql.execute_sql`: run DDL or DML without returning a dataframe.
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 _build_comparisons, _build_metric_definitions, _build_metric_row
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 _compute_cuped_statistics(
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
- ) -> tuple[float, float]:
55
- cuped_frame, reason = _build_cuped_frame(
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,
@@ -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",
@@ -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(kwargs)
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
- "connection_type": db,
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
- from .sql import format_support_matrix
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:]))