tea-tasting

repository·main·Indexed 43 days ago

https://github.com/e10v/tea-tasting

A Python package for the statistical analysis of A/B tests. It supports various statistical tests including t-tests, z-tests, and bootstrap, as well as ratio metrics via the Delta method and variance reduction techniques like CUPED/CUPAC. The library allows for the definition of both aggregated and granular custom metrics and supports multiple data backends including pandas, Polars, and Ibis.

Tokens
22.3K
Snippets
69
Records
86
Agent score
56%

What's inside tea-tasting

  1. Supported data backends for tea-tasting

    main

    tea-tasting supports a wide range of data backends through two primary interfaces:

    1. Ibis: This allows tea-tasting to work with any backend supported by the Ibis DataFrame API, such as BigQuery, ClickHouse, PostgreSQL, Snowflake, and Trino. Using Ibis is highly efficient for statistical tests (like t-tests or z-tests) because tea-tasting can retrieve only aggregated statistics (mean, variance, etc.) directly from the database instead of downloading all granular data.

    2. Narwhals: tea-tasting accepts dataframes supported by Narwhals, which provides a compatibility layer for libraries including:

      • cuDF
      • Daft
      • Dask
      • DuckDB
      • Modin
      • pandas
      • Polars
      • PyArrow
      • PySpark
  2. How aggregated and granular metrics work together in an Experiment

    main

    A single tea_tasting.Experiment can contain both MetricBaseAggregated and MetricBaseGranular metrics.

    When experiment.analyze(data) is called, tea-tasting optimizes the data retrieval by performing two distinct queries:

    1. A query for aggregated statistics required by all MetricBaseAggregated metrics.
    2. A query for detailed data containing the specific columns required by all MetricBaseGranular metrics.

    This allows you to analyze proportions (aggregated) and non-parametric tests (granular) in a single pass.

    experiment = tt.Experiment(
        prop_users_with_orders=Proportion("has_order"), # Aggregated
        mean_users_with_orders=tt.Mean("has_order"),    # Aggregated
        mwu_orders=MannWhitneyU("orders"),               # Granular
        mwu_revenue=MannWhitneyU("revenue"),             # Granular
    )
    result = experiment.analyze(data)
  3. Supported input data formats

    main

    tea-tasting can process data in several formats. It is compatible with:

    • PyArrow Table: The default return type for make_users_data.
    • Pandas DataFrame: Requires the pandas package.
    • Polars DataFrame: Requires the polars package.
    • Ibis Table: An optional API for various data backends (BigQuery, Snowflake, etc.).
    • Narwhals-compatible DataFrames: Includes cuDF, Daft, Dask, DuckDB, Modin, pandas, Polars, PyArrow, and PySpark.

    Data Requirements:

    • Data must be grouped by randomization units (e.g., individual users).
    • A column must indicate the variant (defaults to "variant").
    • All columns required for metric calculations must be present.
  4. How multiple testing correction works in tea-tasting

    main

    The multiple hypothesis testing problem occurs when an A/B test involves multiple success metrics or multiple treatment variants, increasing the risk of false positives.

    tea-tasting provides two main categories of correction methods:

    1. False Discovery Rate (FDR) controlling procedures: Controls the expected proportion of false discoveries among all rejections.

      • Benjamini-Hochberg: Default; assumes non-negative correlation between hypotheses.
      • Benjamini-Yekutieli: Use by setting arbitrary_dependence=True; assumes arbitrary dependence.
    2. Family-wise Error Rate (FWER) controlling procedures: Controls the probability of making at least one type I error.

      • Hochberg's step-up: Default; assumes non-negative correlation.
      • Holm's step-down: Use by setting arbitrary_dependence=True; assumes arbitrary dependence.

    When using FWER with arbitrary dependence, it is recommended to use the method="bonferroni" parameter.

  5. How A/A and A/B simulations work

    main

    In tea-tasting, simulations split data into control and treatment groups.

    • A/A tests: A simulation where the treatment data is not modified. These are used to identify potential issues (like estimating Type I error rates) before running actual A/B tests.
    • A/B tests (Treatment simulations): A simulation where the treatment data is modified using a treatment function. These are used for power analysis, especially when specific uplift distributions are required or analytical formulas are unavailable.
  6. Analyze experiments with more than two variants

    main

    When an experiment has more than two variants, tea-tasting performs pairwise comparisons using two-sample statistical tests.

    Controlling Comparisons

    • Specified Control: If you pass a control ID to analyze(..., control=ID), that variant is compared against every other variant.
    • Automatic Control: If control=None (default), tea-tasting compares all possible pairs. In each pair, the variant with the lowest ID is treated as the control.

    To enable pairwise comparisons for all variants, set all_variants=True in the analyze method.

    Accessing Results

    The result of an analysis with multiple variants is a mapping where keys are tuples representing the pair (control, treatment). You can access a specific pair's results using this tuple as a key.

    >>> # Compare all variants pairwise
    >>> results = experiment.analyze(data_three_variants, all_variants=True)
    
    >>> # Access results for a specific pair (e.g., variant 0 vs variant 1)
    >>> pair_results = results[0, 1]
  7. Reduce variance with CUPED/CUPAC

    main

    You can reduce variance in your A/B test results using CUPED or CUPAC within the Mean and RatioOfMeans metric classes. This requires providing pre-experimental data as covariates.

    1. Prepare Data: Ensure your dataset contains covariate columns (e.g., sessions_covariate, orders_covariate, revenue_covariate). If using tt.make_users_data, set covariates=True.
    2. Configure Metrics:
      • For Mean: Use the covariate parameter to specify the column name.
      • For RatioOfMeans: Use numer_covariate and denom_covariate to specify covariates for the numerator and denominator respectively.
    >>> data_cuped = tt.make_users_data(rng=42, covariates=True)
    >>> experiment_cuped = tt.Experiment(
    ...     sessions_per_user=tt.Mean("sessions", "sessions_covariate"),
    ...     orders_per_session=tt.RatioOfMeans(
    ...         numer="orders",
    ...         denom="sessions",
    ...         numer_covariate="orders_covariate",
    ...         denom_covariate="sessions_covariate",
    ...     ),
    ...     orders_per_user=tt.Mean("orders", "orders_covariate"),
    ...     revenue_per_user=tt.Mean("revenue", "revenue_covariate"),
    ... )
    >>> result_cuped = experiment_cuped.analyze(data_cuped)
  8. Analyze experiments using Ibis tables

    main

    You can perform experiment analysis by passing an Ibis Table to the tt.Experiment.analyze() method. tea-tasting will automatically generate the necessary SQL queries to fetch required statistics (like means, variances, and covariances) from your backend.

    Requirements for your data table:

    • Data must be grouped by randomization units (e.g., individual users).
    • A column must indicate the variant (e.g., 'A', 'B').
    • All necessary columns for metric calculations must be present.
    import ibis
    import tea_tasting as tt
    
    # Assuming 'con' is an Ibis connection and 'data' is an Ibis Table
    experiment = tt.Experiment(
        sessions_per_user=tt.Mean("sessions"),
        orders_per_session=tt.RatioOfMeans("orders", "sessions"),
        orders_per_user=tt.Mean("orders"),
        revenue_per_user=tt.Mean("revenue"),
    )
    
    result = experiment.analyze(data)
    print(result)
  9. Run tea-tasting examples in a local environment

    main

    To run the example notebooks locally, you need to clone the repository, set up a virtual environment with the required dependencies (marimo, tea-tasting, polars, and ibis-framework[duckdb]), and launch the marimo notebook server. The examples are provided in the marimo notebook format.

    # 1. Clone the repository
    git clone https://github.com/e10v/tea-tasting.git && cd tea-tasting
    
    # 2. Install dependencies using uv
    uv venv && uv pip install marimo tea-tasting polars 'ibis-framework[duckdb]'
    
    # 3. Launch the notebook server
    uv run marimo edit examples
  10. Run simulations in parallel

    main

    To speed up simulations, use the map_ parameter to provide an alternative mapping function (such as those from concurrent.futures or multiprocessing).

    Implementation Details

    • map_ parameter: Accepts a mapping function like executor.map.
    • batch_size parameter: Useful when running in parallel to group tasks.

    Platform & Environment Constraints

    • WASM/Online Playgrounds: Parallel execution using multiprocessing is not supported in marimo online playgrounds (WASM-based) because they run entirely in the browser.
    • Windows vs. POSIX:
      • On POSIX systems, you can use the "fork" multiprocessing start method via mp.get_context("fork"), which allows worker processes to inherit functions defined in the current session.
      • On Windows, you must use the "spawn" method. To ensure compatibility, define your treatment functions and data generators at the top level of a Python module and wrap your execution in an if __name__ == "__main__": block.

    Alternative Parallelism Options

    • concurrent.futures.ThreadPoolExecutor: Avoids multiprocessing import issues as threads run in the same process (speedup depends on GIL release).
    • joblib: Good for interactive workflows.
    • Distributed Engines: Use Dask or Ray for simulations spanning multiple machines.
    >>> import concurrent.futures
    >>> import multiprocessing as mp
    >>> import tqdm
    
    >>> ctx = mp.get_context("fork")
    >>> with concurrent.futures.ProcessPoolExecutor(mp_context=ctx) as executor:
    ...     results_parallel = experiment.simulate(
    ...         data,
    ...         100,
    ...         rng=42,
    ...         treat=treat,
    ...         batch_size=10,
    ...         map_=executor.map,
    ...         progress=tqdm.tqdm,
    ...     )
  11. Run tea-tasting examples locally

    main

    The repository contains examples in marimo notebook format. To run them locally:

    1. Clone the repository.
    2. Create a virtual environment and install dependencies (marimo, tea-tasting, polars, and ibis-framework[duckdb]).
    3. Launch the marimo editor pointing to the examples directory.
    git clone https://github.com/e10v/tea-tasting.git && cd tea-tasting
    
    uv venv && uv pip install marimo tea-tasting polars 'ibis-framework[duckdb]'
    
    uv run marimo edit examples
  12. Install tea-tasting with Ibis and Polars

    main

    To use tea-tasting with Ibis (using the DuckDB backend) and Polars, install them using uv:

    uv add 'ibis-framework[duckdb]' polars