Metadata-Version: 2.4
Name: pymab
Version: 2.0.1
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0 ; extra == 'analysis'
Requires-Dist: scipy>=1.11 ; extra == 'bayes'
Requires-Dist: sphinx>=7.0 ; extra == 'docs'
Requires-Dist: sphinx-rtd-theme>=2.0 ; extra == 'docs'
Requires-Dist: plotly>=5.0 ; extra == 'plot'
Provides-Extra: analysis
Provides-Extra: bayes
Provides-Extra: docs
Provides-Extra: plot
License-File: LICENSE
Summary: Reliable, statistically rigorous multi-armed bandit experiments.
Keywords: reinforcement-learning,bandits,experimentation,data-science
Home-Page: https://github.com/danielaLopes/pymab
Author-email: Daniela Lopes <danielalopes_97@hotmail.com>
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/danielaLopes/pymab/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/danielaLopes/pymab
Project-URL: Issues, https://github.com/danielaLopes/pymab/issues
Project-URL: Repository, https://github.com/danielaLopes/pymab
Project-URL: Rust, https://crates.io/crates/pymab

<p align="center">
  <img src="assets/pymab-lucky-lever.png" alt="PyMAB Lucky Lever slot machine" width="280">
</p>

# PyMAB

[Website](https://danielalopes.github.io/pymab/) ·
[Interactive Arcade](https://danielalopes.github.io/pymab/demo/) ·
[Documentation](https://danielalopes.github.io/pymab/docs/)

PyMAB is a typed Python library for reliable, reproducible multi-armed bandit
experiments. Version 2 separates environment, policy, decision, and observation
randomness; treats replicates as the independent unit of analysis; and records
enough provenance to reproduce saved results.

## Install

```bash
pip install pymab
```

Optional features are available through `pymab[plot]`, `pymab[analysis]`, and
`pymab[bayes]`.

Binary wheels execute all 27 built-in policies and built-in experiment loops in
Rust while preserving the Python API. The same core is available to Rust users:

```bash
cargo add pymab
```

Use `ExperimentConfig(backend="rust", ...)` to require native execution,
`backend="python"` for the private behavioral reference, or the default
`backend="auto"` to use Rust whenever every component is native-compatible.

## Basic experiment

```python
import numpy as np

from pymab import BanditEnvironment, Experiment, ExperimentConfig
from pymab.policies import EpsilonGreedyPolicy, UCBPolicy

result = Experiment(
    environment=BanditEnvironment(means=np.array([0.1, 0.4, 0.8])),
    policies={
        "epsilon-greedy": EpsilonGreedyPolicy(n_arms=3, epsilon=0.1),
        "ucb": UCBPolicy(n_arms=3),
    },
    config=ExperimentConfig(horizon=100, n_replicates=20, seed=42),
).run()

print(result.average_reward_by_step[-1])
print(result.cumulative_regret[-1])
```

Policy IDs are explicit and stable. Adding or reordering policies does not
change another policy's random stream or the simulated environment path.

## Paired comparison

```python
from pymab import compare
from pymab.benchmarking import BenchmarkConfig
from pymab.policies import RandomPolicy, UCBPolicy
from pymab.statistics import BootstrapConfig

benchmark = compare(
    {"random": RandomPolicy(n_arms=3), "ucb": UCBPolicy(n_arms=3)},
    environment=BanditEnvironment(means=np.array([0.1, 0.3, 0.8])),
    config=ExperimentConfig(horizon=100, n_replicates=20, seed=7),
    analysis=BenchmarkConfig(
        bootstrap=BootstrapConfig(seed=91),
        baseline="random",
    ),
)

print(benchmark.lowest_mean_regret_policy)
print(benchmark.compare_to_baseline())
```

Intervals are deterministic paired bootstrap intervals over independent
replicates. A lower point estimate is not presented as statistical proof of a
winner.

## Offline evaluation

Fixed-policy estimators require logged-action propensities and report both raw
and post-clipping overlap diagnostics:

```python
from pymab import LoggedBanditDataset, estimate_policy_value
from pymab.offline import EstimateMethod, EstimatorConfig
from pymab.statistics import BootstrapConfig


class FixedTarget:
    def probabilities(self, context):
        return np.array([0.25, 0.75])


logged = LoggedBanditDataset(
    actions=np.array([0, 1, 0, 1]),
    rewards=np.array([0.0, 1.0, 0.0, 1.0]),
    propensities=np.full(4, 0.5),
    n_arms=2,
)
estimate = estimate_policy_value(
    logged,
    FixedTarget(),
    config=EstimatorConfig(
        method=EstimateMethod.SNIPS,
        bootstrap=BootstrapConfig(n_resamples=500, seed=7),
    ),
)
assert estimate.estimate == 0.75
```

Adaptive replay requires the logging design explicitly. Use
`logging_scheme="uniform"` only for uniformly randomized logs; non-uniform
logs also require propensities and use rejection sampling.

For a detailed treatment of single-slot recommendations and defensive
verification, including fit checks, offline evaluation and rollout guardrails,
see [Contextual bandit use cases](docs/contextual-bandit-use-cases.md).

## Probability environments

Additive Gaussian drift is intentionally rejected for Bernoulli means. Use
log-odds drift instead:

```python
from pymab import BernoulliReward, ProbabilityDrift

environment = BanditEnvironment(
    means=np.array([0.2, 0.5, 0.8]),
    reward_model=BernoulliReward(),
    dynamics=ProbabilityDrift(logit_std=0.05),
)
```

## Development

```bash
make sync
make format
make lint
make security
make test
make docs
make docs-linkcheck  # external network check; run separately
make benchmark       # release-mode same-machine backend comparison
make pages-build
make pages-serve     # http://127.0.0.1:8080/pymab/
```

`make docs` performs a clean warnings-as-errors HTML build, executes Sphinx
doctests, enforces 100% API docstring coverage, and runs every Python snippet
in this README.

The complete v1-to-v2 migration is documented in `docs/source/migration_v2.rst`;
the Rust backend migration is documented in `docs/source/migration_rust.rst`.

