Prescriber segmentation and sales-force allocation for a branded Parkinson's levodopa franchise, built on the public CMS Medicare Part D prescriber data.
The question: given a branded product in a market dominated by cheap generics, where should a limited field force spend its calls to grow share? The analysis sizes the market by molecule, segments prescribers by volume and loyalty, isolates the high-volume-low-share targets, and allocates a fixed call budget to where the untapped volume actually sits.
Primary source: CMS "Medicare Part D Prescribers - by Provider and Drug"
(data.cms.gov, public). src/fetch_cms.py resolves the current release UUID from
the data.cms.gov catalog (data.json) and pulls the Parkinson's molecules via the
data-api filter syntax. The current outputs are built from the real calendar
year 2024 file (2026 release): 279,965 prescriber-drug rows, 144,884 prescribers,
10.6M claims, $738M drug cost.
One thing the real data forced: most of these molecules are multi-indication. Ropinirole and pramipexole are mostly restless-legs scripts, benztropine and trihexyphenidyl mostly manage antipsychotic side effects, rivastigmine is mostly dementia. The opportunity segment therefore requires at least some levodopa-class prescribing; without that screen the "target list" fills up with psychiatrists who have never written a levodopa script.
If no real pull is present, the analysis falls back to src/simulate_rx.py, a
generator calibrated to published Part D magnitudes with the same schema, and
every chart and slide carries a clear "synthetic, illustrative" label. Synthetic
runs write to outputs/synthetic/ so they cannot overwrite the committed real
results, and deck/make_deck.js refuses to build from a synthetic summary.
Limitation worth stating up front: Part D is Medicare only (~2/3 of beneficiaries, skews 65+). It is the older-patient slice, not the entire PD market.
Run everything from the repository root.
pip install pandas numpy matplotlib pytest
python -m pytest # 22 tests, no data or network needed
# option A: synthetic (works anywhere, immediately) -> outputs/synthetic/
python src/simulate_rx.py
python src/analyze.py
# option B: real CMS data (needs open network) -> outputs/
python src/fetch_cms.py # writes data/rx_raw.csv + rx_raw.meta.json
python src/analyze.py # auto-detects the real file
node deck/make_deck.js # 8-slide deck, needs pptxgenjs, real data onlydata/rx_raw.csv is 41 MB and gitignored, so a fresh clone runs the synthetic
path. The numbers quoted above and in outputs/ come from the real pull.
outputs/market_share.csv molecule volume/cost/share table
outputs/prescriber_segments.csv per-prescriber volume, loyalty, tier, opportunity flag
outputs/rep_allocation.csv call budget by state
outputs/levodopa_brands.csv brand split inside the levodopa line (real pull only)
outputs/headroom_sensitivity.csv how much the target list depends on the scoring rule
outputs/charts/*.png 5 charts
outputs/summary.json headline numbers, plus pull provenance
deck/parkinsons_commercial_analytics.pptx
Targets are ranked by headroom, volume x (1 - loyalty), rather than by raw volume,
so the field force is pointed at convertible demand instead of at prescribers who
are already loyal. That choice is worth checking rather than asserting, so
headroom_sensitivity() sweeps the exponent in volume x (1 - loyalty)**w and
compares each variant's top 50 against the shipped rule at w=1:
| w | overlap with w=1 | shared targets |
|---|---|---|
| 0.0 (pure volume) | 0.34 | 17 of 50 |
| 0.5 | 0.66 | 33 |
| 1.0 (shipped) | 1.00 | 50 |
| 1.5 | 0.92 | 46 |
| 2.0 | 0.80 | 40 |
Ranking by raw volume shares only 17 of 50 targets with the headroom rule, so the weighting decides two thirds of the call list and is not cosmetic. The list is also stable to the exact exponent: anything from w=1 to w=2 keeps at least 80% of the same prescribers, so the result does not hinge on w being exactly 1.
Two years of Parkinson's biomarker research (EEG, DAT-SPECT, gait) on the clinical side; this is the commercial side of the same disease.