Observed since 20 Oct, 17:28 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
ab-lab
<img src="theme/icon-cluster.png" width=200 height=200 align="right"> # ab_lab [](https://pepy.tech/project/cluster-experiments) []( https://pypi.org/project/ab-lab/) [](https://github.com/david26694/ab-lab/actions) [](https://app.codecov.io/gh/david26694/ab-lab/)  [](https://pypi.python.org/pypi
A Python library for end-to-end A/B testing workflows, featuring:
import numpy as np
import pandas as pd
from ab_lab import PowerAnalysis, NormalPowerAnalysis
# Create sample data
N = 1_000
df = pd.DataFrame({
"target": np.random.normal(0, 1, size=N),
"date": pd.to_datetime(
np.random.randint(
pd.Timestamp("2024-01-01").value,
pd.Timestamp("2024-01-31").value,
size=N,
)
),
})
# Simulation-based power analysis with CUPED
config = {
"analysis": "ols",
"perturbator": "constant",
"splitter": "non_clustered",
"n_simulations": 50,
}
pw = PowerAnalysis.from_dict(config)
power = pw.power_analysis(df, average_effect=0.1)
# Normal approximation (faster)
npw = NormalPowerAnalysis.from_dict({
"analysis": "ols",
"splitter": "non_clustered",
"n_simulations": 5,
"time_col": "date",
})
power_normal = npw.power_analysis(df, average_effect=0.1)
power_line_normal = npw.power_line(df, average_effects=[0.1, 0.2, 0.3])
# MDE calculation
mde = npw.mde(df, power=0.8)
# MDE line with length
mde_timeline = npw.mde_time_line(
df,
powers=[0.8],
experiment_length=[7, 14, 21]
)
print(power, power_line_normal, power_normal, mde, mde_timeline)
import numpy as np
import pandas as pd
from ab_lab import AnalysisPlan, SimpleMetric, Variant, Dimension
N = 1_000
experiment_data = pd.DataFrame({
"order_value": np.random.normal(100, 10, size=N),
"delivery_time": np.random.normal(10, 1, size=N),
"experiment_group": np.random.choice(["control", "treatment"], size=N),
"city": np.random.choice(["NYC", "LA"], size=N),
"customer_id": np.random.randint(1, 100, size=N),
"customer_age": np.random.randint(20, 60, size=N),
})
# Define metrics
aov = SimpleMetric(alias="AOV", name="order_value")
delivery_time = SimpleMetric(alias="Delivery Time", name="delivery_time")
# Define variants and dimensions
variants = [
Variant("control", is_control=True),
Variant("treatment", is_control=False),
]
city_dimension = Dimension(name="city", values=["NYC", "LA"])
# Create analysis plan
plan = AnalysisPlan.from_metrics(
metrics=[aov, delivery_time],
variants=variants,
variant_col="experiment_group",
dimensions=[city_dimension],
analysis_type="clustered_ols",
analysis_config={
"cluster_cols": ["customer_id"],
},
)
# Run analysis
results = plan.analyze(experiment_data)
print(results.to_dataframe())
import numpy as np
import pandas as pd
from ab_lab import (
AnalysisPlan,
SimpleMetric,
Variant,
Dimension,
TargetAggregation,
HypothesisTest
)
N = 1000
experiment_data = pd.DataFrame({
"order_value": np.random.normal(100, 10, size=N),
"delivery_time": np.random.normal(10, 1, size=N),
"experiment_group": np.random.choice(["control", "treatment"], size=N),
"city": np.random.choice(["NYC", "LA"], size=N),
"customer_id": np.random.randint(1, 100, size=N),
"customer_age": np.random.randint(20, 60, size=N),
})
pre_experiment_data = pd.DataFrame({
"order_value": np.random.normal(100, 10, size=N),
"customer_id": np.random.randint(1, 100, size=N),
})
# Define test
cupac_model = TargetAggregation(
agg_col="customer_id",
target_col="order_value"
)
hypothesis_test = HypothesisTest(
metric=SimpleMetric(alias="AOV", name="order_value"),
analysis_type="clustered_ols",
analysis_config={
"cluster_cols": ["customer_id"],
"covariates": ["customer_age", "estimate_order_value"],
},
cupac_config={
"cupac_model": cupac_model,
"target_col": "order_value",
},
)
# Create analysis plan
plan = AnalysisPlan(
tests=[hypothesis_test],
variants=[
Variant("control", is_control=True),
Variant("treatment", is_control=False),
],
variant_col="experiment_group",
)
# Run analysis
results = plan.analyze(experiment_data, pre_experiment_data)
print(results.to_dataframe())
You can install this package via pip.
pip install ab-lab
For detailed documentation and examples, visit our documentation site.
The library offers the following classes:
PowerAnalysis: to run power analysis on any experiment design, using simulationPowerAnalysisWithPreExperimentData: to run power analysis on a clustered/switchback design, but adding pre-experiment df during split and perturbation (especially useful for Synthetic Control)NormalPowerAnalysis: to run power analysis on any experiment design using the central limit theorem for the distribution of the estimator. It can be used to compute the minimum detectable effect (MDE) for a given power level.ConstantPerturbator: to artificially perturb treated group with constant perturbationsBinaryPerturbator: to artificially perturb treated group for binary outcomesRelativePositivePerturbator: to artificially perturb treated group with relative positive perturbationsRelativeMixedPerturbator: to artificially perturb treated group with relative perturbations for positive and negative targetsNormalPerturbator: to artificially perturb treated group with normal distribution perturbationsBetaRelativePositivePerturbator: to artificially perturb treated group with relative positive beta distribution perturbationsBetaRelativePerturbator: to artificially perturb treated group with relative beta distribution perturbations in a specified support intervalSegmentedBetaRelativePerturbator: to artificially perturb treated group with relative beta distribution perturbations in a specified support interval, but using clustersClusteredSplitter: to split data based on clustersFixedSizeClusteredSplitter: to split data based on clusters with a fixed size (example: only 1 treatment cluster and the rest in control)BalancedClusteredSplitter: to split data based on clusters in a balanced wayNonClusteredSplitter: Regular data splitting, no clustersStratifiedClusteredSplitter: to split based on clusters and strata, balancing the number of clusters in each stratusRepeatedSampler: for backtests where we have access to counterfactuals, does not split the data, just duplicates the data for all groupsSwitchbackSplitter: to split data based on clusters and dates, for switchback experimentsBalancedSwitchbackSplitter: to split data based on clusters and dates, for switchback experiments, balancing treatment and control among all clustersStratifiedSwitchbackSplitter: to split data based on clusters and dates, for switchback experiments, balancing the number of clusters in each stratusEmptyWashover: no washover done at all.ConstantWashover: accepts a timedelta parameter and removes the data when we switch from A to B for the timedelta interval.GeeExperimentAnalysis: to run GEE analysis on the results of a clustered designMLMExperimentAnalysis: to run Mixed Linear Model analysis on the results of a clustered designTTestClusteredAnalysis: to run a t-test on aggregated data for clustersPairedTTestClusteredAnalysis: to run a paired t-test on aggregated data for clustersClusteredOLSAnalysis: to run OLS analysis on the results of a clustered designOLSAnalysis: to run OLS analysis for non-clustered dataTargetAggregation: to add pre-experimental data of the outcome to reduce varianceSyntheticControlAnalysis: to run synthetic control analysisMetric: abstract class to define a metric to be used in the analysisSimpleMetric: to create a metric defined at the same level of the data used for the analysisRatioMetric: to create a metric defined at a lower level than the data used for the analysisVariant: to define a variant of the experimentDimension: to define a dimension to slice the results of the experimentHypothesisTest: to define a Hypothesis Test with a metric, analysis method, optional analysis configuration, and optional dimensionsAnalysisPlan: to define a plan of analysis with a list of Hypothesis Tests for a dataset and the experiment variants. The analyze() method runs the analysis and returns the resultsAnalysisResults: to store the results of the analysisPowerConfig: to conveniently configure PowerAnalysis classConfidenceInterval: to store the data representation of a confidence intervalInferenceResults: to store the structure of complete statistical analysis resultsDownload counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
pipEnvironment{
"extras": "",
"marker": null,
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pip>=22.2.2 | runtime |
statsmodelsEnvironment{
"extras": "",
"marker": null,
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | statsmodels>=0.13.2 | runtime |
pandasEnvironment{
"extras": "",
"marker": null,
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pandas>=1.2.0 | runtime |
scikit-learnEnvironment{
"extras": "",
"marker": null,
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | scikit-learn>=1.0.0 | runtime |
tqdmEnvironment{
"extras": "",
"marker": null,
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | tqdm>=4.0.0 | runtime |
numpyEnvironment{
"extras": "",
"marker": null,
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | numpy>=1.20.0 | runtime |
pytestEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest<9.0.0,>=5.4.3; extra == "dev" | runtime |
blackEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | black<25.0.0,>=22.12.0; extra == "dev" | runtime |
ruffEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | ruff<1.0.0,>=0.7.4; extra == "dev" | runtime |
mktestdocsEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | mktestdocs<1.0.0,>=0.2.2; extra == "dev" | runtime |
pytest-covEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-cov<7.0.0,>=2.10.1; extra == "dev" | runtime |
pytest-sugarEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-sugar<2.0.0,>=0.9.4; extra == "dev" | runtime |
pytest-slow-lastEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-slow-last<1.0.0,>=0.1.3; extra == "dev" | runtime |
coverageEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | coverage<8.0.0,>=7.6.7; extra == "dev" | runtime |
pytest-reportlogEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-reportlog<1.0.0,>=0.4.0; extra == "dev" | runtime |
pytest-duration-insightsEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-duration-insights<1.0.0,>=0.1.2; extra == "dev" | runtime |
pytest-clarityEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-clarity<2.0.0,>=1.0.1; extra == "dev" | runtime |
pytest-xdistEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-xdist<4.0.0,>=3.6.1; extra == "dev" | runtime |
pre-commitEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pre-commit<5.0.0,>=2.6.0; extra == "dev" | runtime |
ipykernelEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | ipykernel<7.0.0,>=6.15.1; extra == "dev" | runtime |
twineEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | twine<6.0.0,>=5.1.1; extra == "dev" | runtime |
buildEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | build<2.0.0.0,>=1.2.2.post1; extra == "dev" | runtime |
toxEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | tox<5.0.0,>=4.23.2; extra == "dev" | runtime |
mkdocsEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | mkdocs<2.0.0,>=1.4.0; extra == "dev" | runtime |
mkdocs-materialEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | mkdocs-material<10.0.0,>=8.5.0; extra == "dev" | runtime |
mkdocstringsEnvironment{
"extras": "python",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | mkdocstrings[python]<1.0.0,>=0.25.0; extra == "dev" | runtime |
jinja2Environment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | jinja2<4.0.0,>=3.1.0; extra == "dev" | runtime |
mkdocs-jupyterEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | mkdocs-jupyter<1.0.0,>=0.22.0; extra == "dev" | runtime |
matplotlibEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | matplotlib<4.0.0,>=3.4.3; extra == "dev" | runtime |
plotnineEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | plotnine<1.0.0,>=0.8.0; extra == "dev" | runtime |
pytestEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest<9.0.0,>=5.4.3; extra == "test" | runtime |
blackEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | black<25.0.0,>=22.12.0; extra == "test" | runtime |
ruffEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | ruff<1.0.0,>=0.7.4; extra == "test" | runtime |
mktestdocsEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | mktestdocs<1.0.0,>=0.2.2; extra == "test" | runtime |
pytest-covEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-cov<7.0.0,>=2.10.1; extra == "test" | runtime |
pytest-sugarEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-sugar<2.0.0,>=0.9.4; extra == "test" | runtime |
pytest-slow-lastEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-slow-last<1.0.0,>=0.1.3; extra == "test" | runtime |
coverageEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | coverage<8.0.0,>=7.6.7; extra == "test" | runtime |
pytest-reportlogEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-reportlog<1.0.0,>=0.4.0; extra == "test" | runtime |
pytest-duration-insightsEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-duration-insights<1.0.0,>=0.1.2; extra == "test" | runtime |
pytest-clarityEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-clarity<2.0.0,>=1.0.1; extra == "test" | runtime |
pytest-xdistEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest-xdist<4.0.0,>=3.6.1; extra == "test" | runtime |
mkdocsEnvironment{
"extras": "",
"marker": "extra == \"docs\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | mkdocs<2.0.0,>=1.4.0; extra == "docs" | runtime |
mkdocs-materialEnvironment{
"extras": "",
"marker": "extra == \"docs\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | mkdocs-material<10.0.0,>=8.5.0; extra == "docs" | runtime |
mkdocstringsEnvironment{
"extras": "",
"marker": "extra == \"docs\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | mkdocstrings<1.0.0,>=0.18.0; extra == "docs" | runtime |
jinja2Environment{
"extras": "",
"marker": "extra == \"docs\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | jinja2<4.0.0,>=3.1.0; extra == "docs" | runtime |
mkdocs-jupyterEnvironment{
"extras": "",
"marker": "extra == \"docs\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | mkdocs-jupyter<1.0.0,>=0.22.0; extra == "docs" | runtime |
plotnineEnvironment{
"extras": "",
"marker": "extra == \"docs\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | plotnine<1.0.0,>=0.8.0; extra == "docs" | runtime |
matplotlibEnvironment{
"extras": "",
"marker": "extra == \"docs\"",
"requires_python": "<3.13,>=3.9",
"evaluation": "NOT_EVALUATED"
} | matplotlib<4.0.0,>=3.4.3; extra == "docs" | runtime |
bdist_wheel · 76.82 KB · Python py3
sdist · 62.58 KB · Python source
Observed since 20 Oct, 17:28 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:e074e2874c1836d455c8c52d240c74ef8936a72fefbd3a863828043f4199f482