Skip to main content

pypricing

Bayesian own-price elasticity estimation with PyMC for long-format panels (one row per SKU–time observation).

This is currently an MVP for log-demand panels (per-SKU intercept + per-SKU elasticity) with:

  • optional shared control regressors (control_* columns)
  • optional hierarchical / partial pooling across group columns
  • optional cross-price elasticities (CrossElasticitySpec)
  • simple diagnostics + plotting helpers
  • posterior predictive simulation for counterfactual price scenarios
  • per-SKU revenue price optimization (optimize_prices)

Install

pip install pypricing

Optional model-graph rendering (also needs the system Graphviz binaries):

pip install 'pypricing[graphviz]'

Docs: pypricing.readthedocs.io

Development

From a clone of this repository:

uv sync --extra dev
uv run pytest

For local docs builds: uv sync --extra docs.

Quickstart

import numpy as np

from pypricing import LogLogDemandModel, generate_mock_data

# 1) Example data (replace with your own panel)
df = generate_mock_data(
    n_periods=20,
    n_skus=5,
    n_controls=2,     # creates control_1, control_2
    random_state=0,
)

# 2) Fit
model = LogLogDemandModel()
idata = model.fit(
    df,
    draws=1000,
    tune=1000,
    chains=4,
    random_seed=42,
)

print(model.run_diagnostics())
print(model.fit_summary().head())

# 3) Counterfactual prediction (quantity column not required)
df_scenario = df.drop(columns=["quantity"]).copy()
df_scenario["price"] = df_scenario["price"] * 1.05  # +5% price scenario

pred = model.predict(df_scenario, hdi_prob=0.9, random_seed=123)
print(pred[["sku", "period", "price", "quantity_mean", "quantity_hdi_lower", "quantity_hdi_upper"]].head())

# 4) Plots
_ = model.plot_elasticity_posterior(hdi_prob=0.9)

sku0 = model.sku_levels_[0]
price_grid = np.linspace(df["price"].min(), df["price"].max(), 25)
controls = {c: float(df.loc[df["sku"] == sku0, c].iloc[0]) for c in model.control_names_}
_ = model.plot_response_curve(sku=sku0, price_grid=price_grid, controls=controls, hdi_prob=0.9)

Data format

LogLogDemandModel.fit(df) expects a pandas DataFrame with level-scale columns:

  • Required
    • sku (configurable via sku_col)
    • price (configurable via price_col) — must be strictly positive
    • quantity (configurable via quantity_col) — must be non-negative
  • Optional
    • control_* columns (or pass an explicit control_columns=(...) via PanelColumns) — must be numeric, no NaNs
    • hierarchy columns (e.g. category_1, category_2) via PanelColumns(group_columns=...)
    • period (and often region) for fit_train_test() / cross-elasticity market cells

Internally the model works on logs:

  • log_price = log(price)
  • log_quantity = log(max(quantity, quantity_floor))

The default quantity_floor=1.0 allows zero quantities without \log(0).

What model is being fit?

At a high level this package fits log-demand with Gaussian noise:

\log Q \sim \mathcal{N}(\mu, \sigma)

Where \mu is a per-SKU demand curve plus optional global control effects.

Demand curve model classes

Pick the model class directly:

  • LogLogDemandModel (default/simple)
  • QuadraticLogDemandModel
  • SigmoidSaturationDemandModel

log_log (default)

Constant elasticity log-log:

$\mu = \alpha_{\text{sku}} + \epsilon_{\text{sku}} \log P + X\beta$

  • Interpretation: \epsilon_{\text{sku}} is own-price elasticity.
    • Example: \epsilon=-1.5 implies a 1% price increase → ~1.5% quantity decrease (locally / in expectation).
  • Best when: you want a simple constant-elasticity approximation.

quadratic

Allows elasticity to vary with price (curvature in log-price):

$\mu = \alpha_{\text{sku}} + \beta_{1,\text{sku}}\log P + c_{\text{sku}} (\log P)^2 + X\beta$

This parameterization enforces that the elasticity at a per-SKU midpoint price equals elasticity_sku. The midpoint is computed from the training data as the median log-price per SKU.

  • Best when: elasticity changes with price level (e.g., premium vs discount regimes).

sigmoid

Saturating response curve in level price (softplus / log-sigmoid form):

\mu = \alpha_{\text{sku}} - \mathrm{softplus}(b_{\text{sku}}(P - P_{\text{center,sku}})) + X\beta

The per-SKU center P_{\text{center,sku}} = \exp(\texttt{log\_price\_center\_sku}) is a learned parameter. Its prior is centered at the empirical per-SKU median log-price from training data (log_price_midpoint_sku_), with default sigma=0.5. The curve is parameterized so the elasticity at P_{\text{center,sku}} equals elasticity_sku (via b_sku = -2 * elasticity_sku / price_center_sku).

  • Best when: response “flattens out” at extreme prices (a simple saturation behavior).

Controls (control_*)

If your frame contains control_* columns (or you pass control_columns=(...)), the model includes a shared linear term X\beta:

  • one global coefficient vector beta_control shared across all SKUs
  • controls must also be provided at prediction time

Priors and customization (model_config)

You can override priors by passing model_config to each model class constructor. Each entry uses:

{
    "dist": <PyMC distribution constructor>,
    "kwargs": { ... },
}

Example:

import pymc as pm
from pypricing import QuadraticLogDemandModel

model = QuadraticLogDemandModel(
    model_config={
        "alpha_sku": {"dist": pm.Normal, "kwargs": {"mu": 0.0, "sigma": 1.0}},
        "elasticity_sku": {"dist": pm.Normal, "kwargs": {"mu": -1.0, "sigma": 0.7}},
        "sigma": {"dist": pm.HalfNormal, "kwargs": {"sigma": 0.3}},
        # "curvature_sku": ... (only used by QuadraticLogDemandModel)
        # "beta_control": ...  (only used when you have controls)
    }
)

Defaults today:

  • alpha_sku ~ Normal(mu=6, sigma=2)
  • elasticity_sku ~ Normal(mu=-1, sigma=2)
  • sigma ~ HalfNormal(sigma=0.5)
  • beta_control ~ Normal(mu=0, sigma=0.5) (if controls exist)
  • curvature_sku ~ Normal(mu=0, sigma=0.2) (only for quadratic)
  • log_price_center_sku ~ Normal(mu=log_price_midpoint_sku_, sigma=0.5) (only for sigmoid)

Prediction

  • sample_posterior_predictive(df) returns an xarray.Dataset with posterior draws for log_quantity and quantity.
  • predict(df) returns a DataFrame with:
    • quantity_mean
    • quantity_hdi_lower
    • quantity_hdi_upper

Prediction requires:

  • sku and price
  • all control columns used during fit (if any)
  • quantity is not required

Unknown SKUs at prediction time raise an error (no cold-start handling yet).

Train/test evaluation (time-aware)

fit_train_test(df, period_col="period", test_size=0.2, ...):

  • holds out the last fraction of unique periods (to avoid time leakage)
  • fits on train, predicts on test
  • returns:
    • rmse on quantity_mean
    • hdi_coverage: fraction of true quantities inside the predicted HDI
    • test_predictions: the prediction frame

Save / load

<ModelClass>.save(path) saves the model InferenceData to NetCDF (.nc) with model attrs.

<ModelClass>.load(path) restores the model from that NetCDF artifact and rebuilds the PyMC model.

Interpretation helper

pypricing.posterior.summarize_quantity_multiplier_one_sku(...) converts posterior elasticity draws into a posterior over relative quantity change for a price multiplier m via m^{\epsilon}.

For all SKUs at once, use the model method:

  • model.quantity_multiplier_summary(price_multiplier=..., hdi_prob=...)
  • returns one row per SKU with mean and HDI bounds of the quantity multiplier
  • optionally, model.quantity_multiplier_summary(..., return_draws=True) returns (summary_df, draws_da) where draws_da has dims ("chain", "draw", "sku")

This is only valid for LogLogDemandModel (constant elasticity).

Price optimization

optimize_prices (also available as model.optimize_prices(...)) chooses one price per SKU that maximizes the posterior mean of revenue price * exp(μ), where μ is the demand-curve mean log-quantity (same convention as prediction; residual σ is not folded into exp(μ)).

from pypricing import LogLogDemandModel, generate_mock_data

df = generate_mock_data(n_periods=20, n_skus=5, n_controls=1, random_state=0)
model = LogLogDemandModel()
model.fit(df, draws=500, tune=500, chains=2, random_seed=0)

price_bounds = {
    sku: (float(g["price"].min() * 0.8), float(g["price"].max() * 1.3))
    for sku, g in df.groupby("sku")
}
controls_df = (
    df.groupby("sku", as_index=False)[model.control_names_].median()
)
opt = model.optimize_prices(price_bounds=price_bounds, controls_df=controls_df)
print(opt.head())

Caveats:

  • Optimization is per-SKU and independent (1D SciPy search on log-price within bounds).
  • Not supported when cross_elasticity is enabled (revenue then depends on the full market cell).
  • For LogLogDemandModel, expected revenue is proportional to p^(1+ε) draw-wise; if elasticity is roughly constant and |ε| ≠ 1, the optimum often sits on a bound.
  • If the model was fit with controls, pass controls_df with one row per SKU.

See docs/source/notebooks/quickstart.ipynb for plots (plot_revenue_vs_price, plot_optimization_summary).

Hierarchy and cross-elasticity

Pass column mapping via PanelColumns:

from pypricing import CrossElasticitySpec, LogLogDemandModel, PanelColumns

model = LogLogDemandModel(
    panel_columns=PanelColumns(group_columns=("category_1", "category_2")),
    cross_elasticity=CrossElasticitySpec(mode="within_group", group_level=0),
)

Omit cross_elasticity for own-price only. Use mode="all" for every directed SKU pair (no group_level).

What is NOT implemented (yet)

  • Cold-start prediction for unseen SKUs
  • Joint / cross-aware price optimization (use counterfactual prediction on a full market cell instead)

Documentation

Hosted docs (API + notebooks): https://pypricing.readthedocs.io

To build HTML locally (Sphinx + notebooks, no notebook re-execution):

uv sync --extra docs
cd docs && uv run make html
# open docs/build/html/index.html

Development

uv sync --extra dev
uv run pytest                 # unit + integration (default)
uv run pytest -m unit         # fast only
uv run pytest -m integration  # short MCMC smoke
uv run pytest -m recovery     # parameter recovery (slower)
uv run ruff check src tests

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pypricing-0.0.1.tar.gz (46.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pypricing-0.0.1-py3-none-any.whl (51.2 kB view details)

Uploaded Python 3

File details

Details for the file pypricing-0.0.1.tar.gz.

File metadata

  • Download URL: pypricing-0.0.1.tar.gz
  • Upload date:
  • Size: 46.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pypricing-0.0.1.tar.gz
Algorithm Hash digest
SHA256 dc0bd25d4f0b253aa7e9a2d7a6668472c4cd5de5ec8fe780ca826e91a7927085
MD5 d9b3888ee323d495fd80788673f1aaff
BLAKE2b-256 9dbeb513b4ed20efd3097c40332dd7ef1b53e276863089487299529d1db6d6e2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypricing-0.0.1.tar.gz:

Publisher: release.yml on arthurmello/pypricing

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pypricing-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: pypricing-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 51.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pypricing-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 208a4cf9954366d3e4ae2cfa0be13d86909e0c29a6de1e9a84afa125c45caf31
MD5 305481272fa75246bf2743aaccb31dee
BLAKE2b-256 01520e4f351d267256e2046f9e5f4c4148a323327280783152e3cbda6756df7f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypricing-0.0.1-py3-none-any.whl:

Publisher: release.yml on arthurmello/pypricing

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.0.1 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page