project-003 · Analytics and statistics · insurance
Cost-sharing and doctor visits: pricing plan tiers from a randomized insurance experiment
A health insurer is designing plan tiers with different coinsurance rates. How many outpatient doctor visits per member-year should it expect under 25%, 50% and 95% coinsurance compared with free care, and how uncertain are those numbers? Use randomized evidence instead of the insurer's own claims, where members choose their plans.
- Licence
- public domain (as stated by statsmodels)
- Reference run
- executed
At a glance
Data
RAND Health Insurance Experiment subset (statsmodels.datasets.randhie) (bundled-with-library) · 20,190 person-years; 10 columns (modelling file) or 45 columns (source file); under 1 MB
Details on dataBaseline
Raw visit rates per eligible person-year by coinsurance level and their ratios to free care, with naive Poisson intervals.
Details on baselineValidation
The causal contrast comes from randomization; models are compared on fit and calibration: GroupKFold by zper (5 folds) for held-out log-likelihood and the predicted…
Details on validationMetric
Rate ratios with 95% clustered intervals; overdispersion estimate; held-out log-likelihood; predicted versus observed zero share and upper-tail share by arm.
Details on metric
Success criterion
- Visit rate ratios for each coinsurance level versus free care, with 95% intervals that account for overdispersion and repeated years per person (person-clustered).
- The chosen count model reproduces the observed share of zero-visit person-years within 2 percentage points on person-grouped held-out folds.
- A pricing table of expected visits per 1,000 member-years by tier with intervals, and a statement of how far the experimental population and era limit its transfer to today's members.
Context
The RAND Health Insurance Experiment randomized people to plans with different cost sharing; statsmodels ships a subset used by Cameron and Trivedi. Because plan assignment was randomized, the comparison across coinsurance levels is not confounded by plan choice, unlike an insurer's own claims. Pre-randomization health measures can still improve precision. Check the RAND documentation for the randomization unit: the bundled subset includes a person id and site but no family id, which limits how errors can be clustered.
Observed when loading (statsmodels 0.15.0): 20,190 person-years; 31% have zero visits; the variance of mdvis
is about 7 times its mean; raw mean visits are not monotone in coinsurance (the 100% group is above the 95%
group), which needs explanation before modelling.
Data
- Dataset
- RAND Health Insurance Experiment subset (statsmodels.datasets.randhie) (bundled-with-library)
- Licence
- public domain (as stated by statsmodels)
- Size
- 20,190 person-years; 10 columns (modelling file) or 45 columns (source file); under 1 MB
- Verification
- download verified
Access
Ships with statsmodels; no download, no account. sm.datasets.randhie.load_pandas().data gives the 10-column
modelling version. The 45-column source file (with plan, site, coinsurance level, year, person id, age, income,
eligible time) is installed at Path(sm.datasets.randhie.__file__).parent / "src" / "randhie.csv", with
variable labels in src/randdesc.txt.
Grain
One row per person per study year (zper x year, years 1-5); 5,912 persons across 6 sites and 18 plans
(observed when loading). Persons contribute up to five correlated rows.
| Column | Meaning |
|---|---|
| mdvis | Number of face-to-face outpatient visits to an MD in the year (outcome) |
| coins | Coinsurance rate in percent: 0, 25, 50, 95 or 100 (source file) |
| lncoins | ln(coinsurance + 1) (modelling file) |
| idp | 1 if individual deductible plan |
| lpi / fmde | Log participation incentive; function of the maximum dollar expenditure offer |
| physlm / disea / hlthg / hlthf / hlthp | Baseline physical limitation, chronic disease measure, self-rated health (excellent omitted) |
| zper / site / plan / year | Person id (leading digit is site), site, plan number, study year (source file) |
| time | Fraction of the year the person was eligible (source file); exposure for rates |
EDA and data-quality rules
diseais documented as a count of chronic diseases but takes non-integer values (mean about 11, max about 59 when loaded); confirm its definition in the source description before treating it as a count.Use
time(eligible fraction of the year) as exposure; person-years with very small exposure are listed and handled consistently (offset, not deletion).Tabulate coinsurance by site and plan; explain the non-monotone raw means (composition by site, idp plans, age) before modelling.
Check that baseline covariates are balanced across coinsurance levels; imbalance points to the randomization unit or to subsetting.
Outcome distribution: zero share and upper tail by arm; decide on the count family from these, not by habit.
Baseline
Raw visit rates per eligible person-year by coinsurance level and their ratios to free care, with naive Poisson intervals. These are unbiased under randomization but their intervals are too narrow because of overdispersion and repeated years per person.
Features
Treatment: coinsurance as a categorical variable (0 reference, 25, 50, 95, 100), not the log-linear lncoins
term, so the shape is estimated rather than assumed; idp as a separate plan feature. Precision covariates
(pre-randomization only): age, sex, child, log income, family size, baseline health (physlm, disea,
self-rated health, mental health index), site, study year. Exposure: log(time) as an offset.
Candidate methods
Poisson GLM with log link, exposure offset and person-clustered (sandwich) errors
Consistent for rate ratios even under overdispersion when the mean is right; the robust errors fix the intervals.
Negative binomial regression
Models overdispersion explicitly and gives a full predictive distribution, needed for the zero-share check and pricing bands.
Two-part (hurdle) model: logit for any visit, truncated count for visits given any
Cost sharing may act mainly on the decision to seek care; the two parts show where the effect sits.
Validation design
The causal contrast comes from randomization; models are compared on fit and calibration:
- GroupKFold by
zper(5 folds) for held-out log-likelihood and the predicted versus observed zero share. - Agreement check: adjusted and unadjusted rate ratios should be close; a large gap suggests imbalance worth reporting.
- Posterior-predictive style check: simulated visit distributions per arm against observed histograms.
Leakage checks
Do not adjust for anything measured after randomization that the plan could affect: expenditures (meddol,
outpdol), admissions or other utilisation. They are outcomes, and adjusting for them removes part of the effect.
Bias and representativeness
Attrition or non-participation that differs by plan cannot be checked in this subset; the subset itself is described as differing slightly from the published figures; the unit of randomization may be the family, which is not identified here.
Robustness checks
Estimates by site and by study year; excluding partial-year rows; clustering by person versus by site (only 6 sites, so site-clustered errors are unreliable and are shown only as a stress test).
Technical metrics
Rate ratios with 95% clustered intervals; overdispersion estimate; held-out log-likelihood; predicted versus observed zero share and upper-tail share by arm.
Business interpretation
Expected visits per 1,000 member-years per tier with intervals; with a hypothetical cost per visit, the expected outpatient visit cost difference per tier. Labelled as a transfer from historical experimental data.
Error slices
Children versus adults.
Baseline self-rated health (poor/fair versus good/excellent).
Income tertiles: cost sharing may bind more for low-income members.
Study year 1 versus later years.
Implementation steps
Load both files, join labels, and reproduce the 10-column modelling variables from the source file to confirm alignment.
Run the EDA rules; write a short note on
disea, exposure and the non-monotone raw means.Fit the baseline rates, then the three candidates with the same covariates and offset; extract rate ratios with clustered errors.
Compare candidates with person-grouped CV and the zero-share check; choose one for pricing.
Build the pricing table with uncertainty (parametric bootstrap or clustered bootstrap by person).
Write the memo: estimates, chosen model, transfer limits.
Reproduction
Learner-built; data ship with statsmodels, so the run is offline. Pin statsmodels via python/uv.lock (the
bundled data could change between versions), record the shape and a hash of the loaded frames, and fix bootstrap seeds.
Limitations
Historical experimental population and prices; utilisation patterns and care delivery have changed.
Subset of the original data without family identifiers, so family-level clustering and some original analyses cannot be reproduced.
Visits only; expenditure and health outcomes are not modelled here.
Production and monitoring
Not a deployed model: the output is a documented pricing assumption. Track actual versus expected visit frequency by tier in current claims every quarter, and revise the assumption when the ratio drifts outside agreed bands.
Interview questions
Why not estimate the coinsurance effect by regressing visits on plan in our own claims data?
Poisson or negative binomial when the estimand is a rate ratio? What changes and what does not?
How do you handle members observed for only part of a year?
Postmortem and alternatives
A hierarchical model with site effects would separate site differences from plan effects more cleanly than fixed site terms. If the business question were total spending, a two-part cost model would be needed, because a few hospital stays dominate expenditure while visits are frequent and small.
Reference implementation
Status: executed — outputs below are from the recorded run
r11 results - cost sharing and doctor visits
Recorded run of cd python && uv run python -m reference.r11_cost_sharing_two_part.run (full mode). Every number below is
copied from the files in outputs/ written by that run (metrics.json, baseline_rates.csv, rate_ratios.csv,
agreement_adjusted_vs_unadjusted.csv, interval_comparison.csv, cv_summary.csv, count_distribution_check.csv,
model_fit_slices.csv, pricing_table.csv, hurdle_parts.csv, hurdle_decomposition.csv, slice_effects.csv,
robustness_by_site.csv, robustness_by_year.csv, balance.csv). The data are the RAND Health Insurance Experiment subset
bundled with statsmodels; the dollar cost per visit used in pricing_table.csv (100 USD) is hypothetical and only
illustrates the translation to cost.
| Item | Value |
|---|---|
| Run date | 2026-10-12 |
| Seeds | CV folds 20261012 (+ repeat index); bootstrap spawned from 20261013 (200 person-level resamples) |
| Python / numpy / pandas / scipy / statsmodels | 3.12.13 / 2.5.3 / 3.0.6 / 1.18.1 / 0.15.0 |
| Runtime of the recorded run | 91.9 s inside run.py (95.0 s wall); 10 joblib workers, one BLAS thread each; Apple M5, 10 cores, 16 GB |
| Smoke mode | --smoke: 1 CV repeat, 8 bootstrap draws, 28.6 s inside run.py (31.9 s wall) |
| Data | 20,190 person-years, 5,912 persons, 6 sites, 18 plans; years 1 to 5 have 5,638 / 5,575 / 5,548 / 1,715 / 1,714 rows; no missing values in the used columns; source file SHA-256 fe64f3c8...5c541 |
| Rows per person | 3 rows for 3,743 persons, 5 for 1,584, 1 for 269, 2 for 246, 4 for 70; every person stays in one plan cell |
| Convergence | all 15 CV fits per model converged; full-data fits converged; bootstrap failed draws: 0 / 0 / 0 (Poisson / NB / hurdle) |
| Alignment checks | modelling frame equals the source file on mdvis, idp, lpi, fmde, physlm, disea, health dummies, and lncoins = ln(coins + 1); the source column logc does not equal ln(coins + 1) for the individual-deductible rows (see Finding 1) |
Findings
-
What the data look like. 31.2% of person-years have zero visits, mean visits are 2.86, the variance is 7.09 times the mean and the maximum is 77. Exposure is nearly complete (mean 0.999 of a year); 45 person-years are below one year and 21 below half a year (smallest 0.077), so the offset matters for a handful of rows, not for the headline.
diseais documented as a count but 91.1% of its values are non-integer (0 to 58.6, mean 11.2); it is used as a continuous severity index. The six populatedcoinsxidpcells are free care (6,822 person-years), 25% (4,065), 50% (1,401), 95% (2,653), individual deductible withcoins = 0(4,175) and individual deductible withcoins = 100(1,074, site 1 only). In the source filelogcequals ln(96) for the individual-deductible plan withcoins = 0and 0 for the plans withcoins = 100, which is not what thecoinscolumn implies; I did not resolve this against the RAND documentation (the bundledranddesc.txtonly labels the columns), so those two cells are named by what the files contain and kept out of the four pricing tiers. -
The raw means are not monotone, and the reason is composition. Mean visits fall from 3.56 (free) to 2.79 (25%), 2.56 (50%) and 2.11 (95%) in the non-deductible cells, but the
coins = 100deductible cell sits at 2.68, above 95%. That cell exists only at site 1, and 50% plans are offered only at sites 1, 3, 4, 5 and 6, so cell means mix sites. With site in the model the ratio for thecoins = 100deductible cell drops from 0.755 (unadjusted) to 0.571 and the 50% ratio moves from 0.720 to 0.653 (agreement_adjusted_vs_unadjusted.csv). Adding the precision covariates and year then changes no 25/50/95 ratio by more than 0.025 (for example 50%: 0.653 to 0.676). Site is therefore the covariate that matters; the rest only tighten intervals. -
Headline rate ratios (visit rate versus free care, adjusted, person-clustered 95% intervals).
plan cell Poisson NB2 raw ratio with naive interval 25% coinsurance 0.769 [0.705, 0.840] 0.733 [0.674, 0.798] 0.784 [0.767, 0.802] 50% coinsurance 0.676 [0.594, 0.768] 0.660 [0.585, 0.746] 0.720 [0.695, 0.746] 95% coinsurance 0.637 [0.567, 0.716] 0.602 [0.536, 0.676] 0.594 [0.577, 0.611] individual deductible, coins = 00.710 [0.649, 0.776] 0.678 [0.623, 0.739] 0.681 [0.666, 0.697] individual deductible, coins = 100(site 1)0.587 [0.496, 0.694] 0.585 [0.492, 0.696] 0.755 [0.726, 0.785] The NB2 ratios are 2 to 6 percent lower than the Poisson ones. Poisson with a log link is consistent for the rate ratio whenever the mean model is right, whatever the variance; NB2 weights observations differently, so a gap between them is a hint that the mean model is not exactly right (the income-tertile misfit in Finding 6 points the same way), not a sampling-error statement. I did not investigate it further.
-
The naive intervals are far too narrow: 3.6 to 4.2 times in log-width for the unadjusted ratios (
baseline_rates.csv). For the three tiers the standard error of the log rate ratio is 0.011 / 0.018 / 0.015 under naive Poisson, 0.028 / 0.045 / 0.037 under the Pearson-scaled quasi-Poisson, 0.029 / 0.043 / 0.039 under HC1 robust errors that treat rows as independent, and 0.045 / 0.066 / 0.059 when clustered by person (interval_comparison.csv). Overdispersion explains roughly two thirds of the gap in log terms (Pearson chi-square / df = 5.93 for the Poisson fit, NB2 alpha = 1.21 with interval 1.15 to 1.27), repeated years per person the rest. Clustering on the inferred family key (2,528 families; the bundled subset has no family id, so this is a heuristic built from consecutive person ids, plan and family size) widens the standard errors further to 0.052 / 0.077 / 0.070, and clustering on the 6 sites (a stress test with too few clusters to trust) gives 0.069 / 0.086 / 0.050. If the randomization unit was the family, as the project record suggests it may have been, the person-clustered intervals shown above are somewhat too narrow; the family-clustered version of the 95% tier is [0.556, 0.730] against [0.567, 0.716]. -
Only the count model that separates "any visit" reproduces the zero share. On person-grouped held-out folds (5 folds x 3 repeats) the predicted zero share is 0.101 for adjusted Poisson, 0.319 for adjusted NB2 and 0.313 for the adjusted hurdle model, against 0.312 observed. The held-out log-likelihood per person-year is -3.023 (Poisson), -2.131 (NB2) and -2.107 (hurdle). Paired differences with a person-level standard error: NB2 over Poisson +0.893 per person-year (z = 20.0), hurdle over NB2 +0.024 (z = 11.1); adding the covariates to NB2 over arm-only NB2 +0.051 (z = 12.3). The spread across the three repeats is below 0.001. Under the rule fixed before the run (within 2 pp pooled, then best log-likelihood) the chosen model is the hurdle. Success criterion 2 (pooled zero share within 2 percentage points) is met: gap +0.008 pp for the hurdle, +0.63 pp for NB2.
-
The hurdle's near-zero gap is partly mechanical; the slices are the real test. The logit part contains one dummy per plan cell, so it reproduces each cell's zero share almost by construction (worst plan cell,
c50: -0.07 pp). NB2 misses 95% coinsurance by -6.5 pp (arm-only NB2 by -6.7 pp). The informative checks are slices the model did not condition on as separate parameters. By income tertile the hurdle's held-out zero share is off by -3.1 pp (low), -1.2 pp (middle) and +4.4 pp (high), and observed mean visits are 3.29 against 3.06 predicted in the high tertile and 2.53 against 2.69 in the low one. So the criterion holds pooled and by plan cell, and does not hold within the two outer income tertiles. Log income enters only linearly in both parts; I did not try a more flexible income term. By child/adult, baseline health, and study year the gaps are within 0.05 pp (fair or poor health: +0.04 pp for the hurdle, -2.6 pp for NB2). -
Where the effect sits: mostly in whether anyone visits. Hurdle parts (odds ratio for any visit; rate ratio for visits given any): 25%: 0.603 [0.533, 0.683] and 0.786 [0.709, 0.873]; 50%: 0.487 [0.406, 0.586] and 0.735 [0.633, 0.853]; 95%: 0.330 [0.287, 0.378] and 0.798 [0.693, 0.918]. Standardized to the whole sample, the probability of at least one visit is 0.784 under free care, 0.696 (25%), 0.654 (50%) and 0.570 (95%), a ratio of 0.73 at 95%; mean visits given any visit go from 4.49 to 3.78, 3.60 and 3.81 (ratio 0.85 at 95%). The conditional ratio is flat from 50% to 95% while the any-visit ratio keeps falling.
-
Pricing table (chosen hurdle model, expected visits per 1,000 member-years, exposure = 1 year, whole-sample covariate mix, person-level cluster bootstrap with 200 draws, 95% percentile intervals).
plan cell visits per 1,000 member-years 95% interval ratio to free care 95% interval free care 3,613 [3,449, 3,782] 1.000 25% coinsurance 2,723 [2,546, 2,928] 0.754 [0.701, 0.823] 50% coinsurance 2,450 [2,150, 2,718] 0.678 [0.592, 0.772] 95% coinsurance 2,288 [2,060, 2,573] 0.633 [0.566, 0.720] individual deductible, coins = 02,550 [2,350, 2,735] 0.706 [0.657, 0.770] individual deductible, coins = 100(site 1)2,107 [1,816, 2,455] 0.583 [0.494, 0.694] With the hypothetical 100 USD per visit, expected outpatient visit cost per member-year falls from 361 USD to 272, 245 and 229 USD for the three tiers (differences of -89, -116 and -133 USD). The intervals of the 25%, 50% and 95% tiers overlap; I did not compute paired differences between tiers, so the ordering among them is suggestive, while the difference of each tier from free care is well established.
-
Model choice moves the pricing table by a few percent, not by much more. Free-care visits per 1,000 member-years are 3,583 (Poisson), 3,709 (NB2) and 3,613 (hurdle); the 25% tier 2,756, 2,720 and 2,723; the 95% tier 2,282, 2,232 and 2,288. The ratio to free care for the 95% tier is 0.637, 0.602 and 0.633. Differences between models are smaller than the bootstrap interval half-widths (about 250 visits at 95%).
-
Heterogeneity. Joint Wald tests that the three coinsurance ratios do not vary by slice give p = 0.37 (child versus adult), 0.080 (baseline health), 0.091 (income tertile) and 0.72 (study year 1 versus 2 to 5). Nothing is significant at 5%, and the two borderline ones are exactly where the data are thin: 1,862 person-years have fair or poor baseline health and the 95% ratio there is 0.942 [0.685, 1.294] against 0.599 [0.530, 0.677] for good or excellent health. The 25% ratio is 0.793, 0.674 and 0.824 in the low, middle and high income tertiles: no gradient in the direction "cost sharing binds more for low-income members". With four slice families and several ratios each, I read the differences as noise rather than a pattern.
-
Robustness. By site (arm-site cells that exist) the 25% ratio ranges from 0.585 (site 6) to 0.907 (site 2), the 95% ratio from 0.524 (site 6) to 0.715 (site 5); site 4's 50% ratio is 0.978 [0.729, 1.312] and site 5's is 0.409 [0.215, 0.777], i.e. single sites are noisy and their intervals are wide. By study year the 25% ratio runs from 0.696 to 0.811, no trend. Dropping the 45 partial-year rows changes the ratios by at most 0.011 (50%: 0.665 against 0.676). Restricting to the four sites that offer all four tiers (3 to 6, 11,692 person-years) gives 0.694 / 0.681 / 0.612, close to the pooled 0.769 / 0.676 / 0.637 for 50% and 95% but lower for 25%.
-
Balance is acceptable within site except for family size. The largest within-site standardized mean difference against free care is -0.21 (family size, individual-deductible cell with
coins = 0); the other values above 0.1 are family size in the 50% cell (-0.18) and in thecoins = 100cell (-0.12) and "poor" self-rated health in thecoins = 100cell (-0.10). Pooled over sites the chronic-disease index differs by up to 0.32 SD between cells, which shrinks to 0.08 within site: this is site composition, not a randomization failure. I cannot tell from these files whether the family-size differences reflect randomization at the family level or subsetting. -
Leakage demonstration. Adding same-year log outpatient expenditure (
outpdol, an outcome of the plan) to the Poisson model moves the rate ratios from 0.769 / 0.676 / 0.637 to 0.948 / 0.908 / 0.967, with the 25% and 95% intervals now covering 1. It is shown only to size the damage; the headline models do not include it.
Success criteria of project-003
| # | Criterion | Result |
|---|---|---|
| 1 | Rate ratios for each coinsurance level versus free care with 95% intervals that allow for overdispersion and person clustering | Met: Poisson-robust and NB2 ratios with person-clustered intervals for all five non-free cells (Finding 3) |
| 2 | Chosen count model reproduces the observed zero share within 2 percentage points on person-grouped held-out folds | Met pooled (+0.008 pp) and in every plan cell (worst -0.07 pp). Not met within the income tertiles (+4.4 pp high, -3.1 pp low), which the criterion did not ask for but which is reported (Finding 6) |
| 3 | Pricing table with intervals per tier and a statement of how far the experimental population and era limit transfer | Table: Finding 8. Transfer limits: next section |
What this does not prove
- Transfer to today's members. The experiment is historical (the bundled documentation gives no dates; the RAND literature places it in the 1970s and early 1980s) and covers six sites; prices, care delivery (telehealth, urgent care, specialist access) and benefit design have changed. The pricing table says what visit rates were under those plans then, not what a current member will do under a 25%, 50% or 95% plan. The effect of a deductible plan, copayments, or out-of-pocket maxima is not identified by coinsurance level alone.
- The randomization unit is not verified. The bundled subset has no family id, so I clustered by person and, as a check, by an inferred family key and by site. If plans were assigned to families, the person-clustered intervals are slightly too narrow (Finding 4).
- Visits only. Spending, admissions, health outcomes and welfare effects are not modelled. A visit-rate reduction says nothing about whether the visits avoided were low or high value.
- Subset and attrition. The bundled subset is not the published analysis file; participation and attrition by plan cannot be checked (rows per person are 1 to 5, with 1,715 and 1,714 rows in years 4 and 5), and I assume the subset is representative of the full experiment.
- The model class is limited. All three candidates use a log-linear mean with linear income, age and health terms. The
income-tertile misfit (Finding 6) shows that this is not exact; the hurdle's logit also uses
log(time)as a covariate rather than an offset, which is a modelling choice, not an identity. - Intervals are conditional on the model form. The bootstrap resamples persons and refits the same model class; it does not include model-selection uncertainty (the chosen model won on the full sample, not inside each draw). Percentile endpoints from 200 draws are noisy at the second digit.
- Pricing standardizes to this sample. "Per 1,000 member-years" uses the covariate mix of the experiment, one full year of eligibility and no interaction between plan and covariates; a different membership mix gives different totals even if the ratios hold.
- Slice and robustness results are not tuned and not corrected for multiplicity. Single-site and single-year estimates have wide intervals and are shown to show instability, not to rank sites.
- The 100 USD cost per visit is invented for illustration.
Reproduce
cd python && uv run python -m reference.r11_cost_sharing_two_part.run # full run, about 90 seconds
cd python && uv run python -m reference.r11_cost_sharing_two_part.run --smoke # about 30 seconds, writes to a temp dir
cd python && uv run pytest tests/test_r11_cost_sharing_two_part.py # 12 testsRubric3 criteria
| Criterion | Meets | Exceeds |
|---|---|---|
| Estimand and design | States the rate ratio versus free care as the estimand and explains why randomization identifies it. | Discusses randomization unit, attrition and subset caveats and their effect on intervals. |
| Count modelling | Uses exposure, handles overdispersion and clusters by person. | Compares Poisson-robust, negative binomial and hurdle models with grouped CV and zero-share checks. |
| Business translation | Pricing table with intervals per tier. | Shows sensitivity of tier pricing to model choice and to the transfer assumption. |
References5
- RAND Health Insurance Experiment Data (statsmodels.datasets.randhie)
Data source, variable definitions and subset caveats.
- Regression with Discrete Dependent Variable (statsmodels)
Negative binomial and zero-inflated/hurdle count models.
- Generalized Linear Models (statsmodels)
Poisson GLM with offsets and robust covariance.
- Causal Inference: What If
Why randomization identifies the effect and covariates only add precision.
- An Introduction to Statistical Learning: with Applications in Python
Poisson regression and generalized linear models.