Skip to main content

elastica

Estimate how price-sensitive your customers really are from plain sales data, with double machine learning, and turn that into a price recommendation you can defend.

CI License: MIT

Leer en español

Estimated vs true elasticity on 50 simulated products: naive OLS and OLS with linear controls are biased toward zero, elastica lands on the diagonal

Why

The usual way to measure price elasticity, regressing log quantity on log price, is wrong whenever something moves price and demand together. Retailers raise prices in high season, when demand is high anyway, so the naive slope says customers barely react to price. In the benchmark above it is not just off: for many products it comes out positive.

elastica removes that confounding with double machine learning (Chernozhukov et al., 2018): it predicts both price and demand from the calendar and your controls with gradient boosting, then estimates the elasticity from what is left. You get a confidence interval, diagnostics that tell you when not to trust the number, and a price recommendation that stays close to the prices you have actually tried.

Benchmark

examples/benchmark.py simulates 50 products with a known elasticity, seasonality and two week-long sales peaks that raise both price and demand, then compares three estimators on the same data:

Method Mean absolute error Mean bias
Naive OLS (log q on log p) 1.59 +1.59
OLS + the same controls, linear 0.55 +0.55
elastica (DML) 0.12 +0.09

elastica's 95 % confidence interval contains the true value in 80 % of runs. The shortfall from 95 % comes from the sharp sales peaks: with two years of daily data the nuisance models cannot fully learn them, which leaves a small bias toward zero. Without the peaks, coverage is 19 of 20 runs. Reproduce it with uv run --group docs python examples/benchmark.py.

Install

pip install elastica

Requires Python 3.11+. Dependencies: numpy, pandas, scikit-learn.

Quickstart

Generate a demo dataset with known answers, then estimate and price (from a clone of this repository, prefix each command with uv run):

$ elastica simulate -o sales.csv
true elasticities: SKU-001 -2.2, SKU-002 -1.87, SKU-003 -1.53, SKU-004 -1.2

$ elastica estimate sales.csv --controls promo
Product  Elasticity  95% CI low  95% CI high  Naive  Periods
-------  ----------  ----------  -----------  -----  -------
SKU-001       -2.02       -2.23        -1.81  -0.55      730
SKU-002       -1.77       -1.97        -1.58  -0.32      730
SKU-003       -1.47       -1.66        -1.27  -0.05      730
SKU-004       -1.07       -1.26        -0.89   0.35      730

$ elastica price sales.csv --controls promo --cost unit_cost
Product  Elasticity  Current  Recommended  Range low  Range high  Profit  Revenue
-------  ----------  -------  -----------  ---------  ----------  ------  -------
SKU-001       -2.02   105.34       109.15      99.95      123.10   +0.1%    -3.6%
SKU-002       -1.77    67.42        72.35      64.06       80.91   +0.4%    -5.3%
SKU-003       -1.47   128.62       154.35     146.68      154.35   +4.6%    -8.1%
SKU-004       -1.07    96.10       115.33     115.33      115.33  +13.1%    -1.3%

SKU-003: price capped at the top of the band; consider testing a wider band in small steps

SKU-004: the confidence interval includes -1; elastic vs inelastic is uncertain; price capped at the top of the band; consider testing a wider band in small steps

Range is the recommended price at each end of the elasticity's confidence interval. Profit and Revenue are the expected changes per period if the model's assumptions hold.

Your data

One row per sale or per product and day, in CSV or Parquet. Only three columns are required; name them however you like:

Field Flag Required Notes
Date --date yes Any format pandas can parse.
Price --price yes Unit price actually paid.
Quantity --quantity yes Units sold.
Product --product no Pass --product '' if the file has a single product.
Unit cost --cost no Needed for profit-maximizing prices; or pass --unit-cost 42 for all products.
Controls --controls a,b no Anything else that moves price or demand: promotions, store, channel, stock-outs. Text columns are one-hot encoded.

Rows are aggregated to product × day (--frequency W or M for weekly or monthly) with a quantity-weighted price. Calendar features (trend, month, day of year, week of year, day of week) are added automatically. Periods with zero sales are dropped and counted.

How it works

For each product, on log-log data:

log q = θ · log p + g(X) + ε      θ is the elasticity
log p =         m(X) + v
  1. Cross-fitting. g and m are learned with gradient boosting on K−1 folds and predicted on the held-out fold, so no observation is predicted by a model that saw it.
  2. Partialling out. θ is the slope of the outcome residuals on the price residuals: the part of price variation the calendar and controls cannot explain.
  3. Inference. Standard errors come from the Neyman-orthogonal score; the whole procedure is repeated with new splits (3 by default) and aggregated by the median.
  4. Pricing. Demand is treated as constant-elasticity around the current price, so the profit-maximizing price is cost · θ / (1 + θ) when θ < −1. The result is clamped to a band around the current price (default 80 %–120 %), because the approximation is only reliable near prices you have observed.

Every estimate reports the naive slope and an OLS with linear controls next to the DML estimate, plus the cross-fitted R² of both nuisance models.

Warnings you may see

Warning Meaning
weakly identified The controls explain almost all price changes. There is too little independent price variation to learn from; run a price test.
positive elasticity Demand rises with price. A confounder is probably missing from --controls.
confidence interval includes −1 The data cannot tell elastic from inelastic demand, so the direction of the optimal price change is uncertain.
capped at the band The optimum is outside the prices you have tried. Move toward it in small, measured steps.

Python API

import elastica

sim = elastica.simulate()
panel = elastica.prepare(sim.transactions, elastica.Schema(cost="unit_cost", controls=("promo",)))

for item in elastica.estimate(panel):
    r = item.result
    rec = elastica.recommend(item)
    print(
        f"{item.product}: {r.elasticity:.2f} [{r.ci_low:.2f}, {r.ci_high:.2f}]  naive {r.naive_elasticity:+.2f}  ->  {rec.current_price:.2f} to {rec.recommended_price:.2f} ({rec.reason})"
    )
SKU-001: -2.02 [-2.23, -1.81]  naive -0.55  ->  105.34 to 109.15 (optimum inside the band)
SKU-002: -1.77 [-1.97, -1.58]  naive -0.32  ->  67.42 to 72.35 (optimum inside the band)
SKU-003: -1.47 [-1.66, -1.27]  naive -0.05  ->  128.62 to 154.35 (capped at the top of the band)
SKU-004: -1.07 [-1.26, -0.89]  naive +0.35  ->  96.10 to 115.33 (capped at the top of the band)

More:

  • elastica.estimate(panel, pooled=True): one estimate across products with product fixed effects, for SKUs with little history.
  • elastica.EstimateOptions(learner=..., folds=Folds.TIME, n_folds=5, n_repeats=3, confidence=0.9): any scikit-learn regressor works as learner. Time-blocked folds are more conservative and need about three seasons of history.
  • elastica.estimate_elasticity(y, d, x): the estimator on raw arrays.
  • elastica.demand_curve(...), estimates_frame, recommendations_frame: tables for plotting and reporting.

Limits

  • Causal only under the usual assumption: everything that moves both price and demand is in the date or in --controls. If a competitor's price drives yours and is not in the data, the estimate is biased.
  • Own-price elasticity only; cross-price effects and substitution between products are not modelled.
  • The price recommendation is a starting point for a test, not an order.

Development

uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check . && uv run mypy .

See CONTRIBUTING.md.

License

MIT © Diego Tepichin

Metadata

Release files for elastica 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for elastica 1.0.0
File Size Uploaded Provenance Actions
elastica-1.0.0.tar.gz 27.6 kB Without provenance Details for elastica-1.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for elastica 1.0.0
File Interpreter ABI Platform Actions
elastica-1.0.0-py3-none-any.whl Python 3 none any Details for elastica-1.0.0-py3-none-any.whl

Total release size: 48.0 kB

Release files / elastica-1.0.0.tar.gz

Download URL elastica-1.0.0.tar.gz
Size 27.6 kB
Tags Source
SHA-256 checksum
How to use checksums
bfeabda6cae858eae5eff94c6b8c49340a4f1d1994334ac1ce2a1f26c3ddec1f
BLAKE2b-256 checksum
How to use checksums
d5f0af6fe0cfe3c280c1074535adbe62e7edb2954ae0ae00edb822fd108f0ec8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / elastica-1.0.0-py3-none-any.whl

Download URL elastica-1.0.0-py3-none-any.whl
Size 20.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
36fdb42929a92c00ad123df7921b8b17aabed351cd4878581113961bd57b2d81
BLAKE2b-256 checksum
How to use checksums
3e0002453bb64dbcdd92df50041d9a34b6df571ce79c4fd7ddffda0463d6f4bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for elastica 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

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