partworth
partworth estimates hierarchical Bayes (HB) models for choice-based conjoint (CBC) studies in Python. It is built on PyMC.
It covers the whole workflow in one package: data checks, HB estimation, respondent-level part-worths, Sawtooth-style output files, a market simulator and willingness to pay (WTP). The validation scripts and their results are in the repository.
Status: pre-alpha. The API may change.
What you can do with it
flowchart TB
subgraph S1["1. Your survey"]
direction LR
A["CBC answers<br/>(long format)"]
B["Design from ChoiceDesign<br/>(wide format)"]
A ~~~ B
end
subgraph S2["2. Fit one model"]
direction LR
C["ChoiceData<br/>checks the data"] --> D["HierarchicalMNL<br/>HB estimation on PyMC"] --> E["Diagnostics<br/>R-hat, ESS, divergences"]
end
subgraph S3["3. Read the results"]
direction LR
F["Part-worths<br/>for each respondent"]
G["Importances and RLH"]
H["Sawtooth-style CSV"]
F ~~~ G ~~~ H
end
subgraph S4["4. Answer business questions"]
direction LR
I["Which product wins?<br/>shares with<br/>credible intervals"]
J["What if we change<br/>the price?<br/>price curves"]
K["How much will<br/>people pay?<br/>WTP for a feature"]
I ~~~ J ~~~ K
end
S1 --> S2 --> S3 --> S4
Steps 1 to 3 replace the usual "estimate in one tool, export, simulate in a spreadsheet" work. Step 4 uses all posterior draws, so every share, price curve and WTP comes with its uncertainty.
Why partworth
- Sawtooth CBC/HB is commercial and runs outside Python. partworth gives the same kind of outputs (raw utilities, zero-centred diffs, importances, RLH) in an open Python package.
- pymc-marketing's
MixedLogitis a general choice model. partworth is built for CBC studies: long-format survey data, correlated random coefficients, a negative price coefficient for every respondent, and a simulator with credible intervals. - Each result can be checked. The repository compares partworth with R
bayesmon public data and tests that it recovers known parameters.
What works now
- Long-format choice data with checks, tasks of different sizes and a none option
- Import of wide designs, such as the output of ChoiceDesign
- Effects, dummy and linear coding, with an optional sign constraint (for price)
- Hierarchical MNL with correlated coefficients (LKJ prior), respondent covariates and the nutpie sampler
- Part-worths, zero-centred diffs, importances, RLH and convergence diagnostics
- Sawtooth-style utilities CSV (read and write)
- Market simulator with credible intervals, price curves and WTP
Not planned: experimental design (use ChoiceDesign), latent class, MaxDiff and ACBC.
Installation
pip install "partworth[nutpie,datasets]"
nutpie adds the faster sampler. datasets adds the reader for the public camera data.
It needs Python 3.12 or later.
On macOS 26 or later, PyTensor 3.3.3 or earlier can fail with
ld: library 'd64' not found. Set PYTENSOR_FLAGS=cxx= to avoid it. The fix is merged in
PyTensor (pymc-devs/pytensor#2432) but
not yet released.
Quick look
This example uses simulated data, so you can paste it as it is.
import pandas as pd
import partworth as pw
from partworth.datasets import simulate_cbc
# 300 respondents, 12 tasks, 3 products + "none" per task.
# mu: true population means of the coded coefficients (price on the log scale).
data, truth = simulate_cbc(
300, 12, 3,
{"brand": ["A", "B", "C"], "size": ["S", "M", "L"], "price": [2.0, 2.5, 3.0, 3.5]},
price="price", mu=[0.4, -0.1, -0.3, 0.1, -0.5, -1.0], sd=0.6, seed=1,
)
print(data.describe()) # how often each level was shown and chosen
# more tuning and draws than the defaults: the SD of "none" mixes slowly (see the guide)
result = pw.HierarchicalMNL(data).fit(draws=2000, tune=3000, chains=4, random_seed=1)
print(result.diagnostics().report()) # R-hat, ESS, divergences, what to try
result.part_worths() # raw utilities, one column per level
result.part_worths("zc_diffs") # zero-centred diffs
result.importances()
result.to_sawtooth_csv("utilities.csv")
scenario = pd.DataFrame({"product": ["Ours", "Theirs"], "brand": ["A", "B"],
"size": ["M", "M"], "price": [3.0, 2.5]})
sim = pw.MarketSimulator(result)
print(sim.shares(scenario)) # shares with 90% credible intervals
print(sim.price_curve(scenario, product="Ours", grid=[2.0, 2.5, 3.0, 3.5]))
print(pw.wtp(result, attribute="size", from_level="S", to_level="L"))
For your own data, build ChoiceData from a long table (one row per respondent, task and
alternative):
data = pw.ChoiceData.from_long(
df, # your table
attributes={"brand": pw.Categorical(["A", "B", "C"]),
"price": pw.Linear(sign="negative")},
none="is_none", # optional "no purchase" rows
)
examples/retail_survey.ipynb is a notebook on a dummy retail survey. It goes from the survey table to shares, prices and profit. A Japanese version is in examples/retail_survey_ja.ipynb. examples/camera.py runs the same steps on public data. docs/guide.md explains the choices you make along the way.
Validation
Each script in validation/ writes its report to validation/results/.
- Parameter recovery (recovery_report.md).
300 simulated respondents and 12 tasks. Respondent-level 90% intervals covered 88.7% of
the true values. 5 of 6 population means and 6 of 6 SDs were inside their 90%
intervals. The price mean (log scale) was just outside. The convergence targets were met.
Correlations reached R-hat 1.013 and ESS 399, just short of the targets.
pytest --run-slowruns the same fit and checks. - Comparison with R bayesm (camera_report.md).
bayesm::rhierMnlRwMixtureon the public camera data. Respondent-level posterior means correlate at 0.997 or more for every coefficient. The hold-out hit rate is 0.729 for partworth and 0.730 for bayesm. Population means differ by up to 0.21 posterior SDs. Our target was 0.1, so this target is not met. The two packages use different priors. - Simulation-based calibration (sbc_report.md). 200 simulated data sets, 8 population parameters. The smallest uniformity p-value was 0.017. None was below 0.006, the threshold after correcting for 8 tests. Earlier runs showed low p-values for different parameters each time. 15 of the 200 fits had a population ESS below 99, which weakens the check.
- Performance (performance.md). 300 respondents took 74 s and 1,000 respondents took 290 s on one laptop.
Data and licences
partworth is licensed under Apache-2.0. The camera data belongs to the R package
bayesm, which is GPL. partworth downloads it
from CRAN on first use and never includes it. No bayesm code is used. The comparison runs
R in a separate process.
All examples use public or simulated data.
How this is built
Design decisions are in docs/adr/. Parts of the code were written with an AI coding assistant. The model, the priors and the validation targets are the author's decisions. The ADRs record them.
Licence
Apache License 2.0. See LICENSE.
Metadata
Release files for partworth 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| partworth-0.0.1.tar.gz | 1.2 MB | Without provenance | Details for partworth-0.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| partworth-0.0.1-py3-none-any.whl | Python 3 | none | any | Details for partworth-0.0.1-py3-none-any.whl |
Total release size: 1.2 MB
Release files / partworth-0.0.1.tar.gz
| Download URL | partworth-0.0.1.tar.gz |
|---|---|
| Size | 1.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ff83acc33e18f4fabb6ae666840456b89acc06e7acc92c73e2cb1438121127db
|
|
BLAKE2b-256 checksum How to use checksums |
a8f3f374952826fd78c0cfd792016e2e724d1712d552b9257c03e25694ab7331
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","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 / partworth-0.0.1-py3-none-any.whl
| Download URL | partworth-0.0.1-py3-none-any.whl |
|---|---|
| Size | 36.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
023bb1e2b0aaabae6c83c5d05d04289deb45b2564a2a97e9c0fe625487c7a25e
|
|
BLAKE2b-256 checksum How to use checksums |
dbe248da3fe8daa38b95f720c5db7b0c272ec55102fb3c28b5e9a08cf9289ba3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","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 partworth 0.0.1
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.