Two-part models

Expected outcomes from a model of whether the outcome is positive and a model of how large it is

Outcomes such as medical expenditures are zero for many people and spread over a long right tail for the rest. A two-part model handles them in two steps: a binary model of whether the outcome is positive, and a model of its size for the people with a positive outcome. The expected outcome is the product of the two:

\[E(y) = \Pr(y > 0) \times E(y \mid y > 0)\]

This is the model the twopm command fits (Belotti, Deb, Manning, and Norton 2015). With suest2, fit the two parts as separate models, combine them, and give margins the product of the two predictions in expression(). Each predict() names its model with model(), so the same combined results also give effects within each part, comparisons across groups, and comparisons across two-part models.

The examples use the medical expenditures of 19,386 adults in the 2004 Medical Expenditure Panel Survey.

Fit the two parts and combine them

The first part is a logit of whether a person had any medical expenditures. The second part is a glm with a gamma family and a log link, fit to the people who had any. Both cluster on the dwelling unit (duid).

use "https://tdmize.github.io/data/data/meps", clear
(MEPS 2004 adults, total medical expenditures (from R package twopartm))
generate byte any = exp_tot > 0
tabulate any
        any |      Freq.     Percent        Cum.
------------+-----------------------------------
          0 |      3,440       17.74       17.74
          1 |     15,946       82.26      100.00
------------+-----------------------------------
      Total |     19,386      100.00
local x "c.age i.female c.lninc c.pcs12 c.mcs12 i.ins_unins i.anylim"
quietly logit any `x', vce(cluster duid)
estimates store Any
quietly glm exp_tot `x' if any, family(gamma) link(log) vce(cluster duid)
estimates store Amount
suest2 Any Amount
Simultaneous results for Any, Amount                    Number of obs = 19,386

                                (Std. err. adjusted for 10,940 clusters in duid)
--------------------------------------------------------------------------------
               |               Robust
               | Coefficient  std. err.      z    P>|z|     [95% conf. interval]
---------------+----------------------------------------------------------------
Any_any        |
           age |      0.030      0.002   19.358   0.000        0.027       0.033
      1.female |      0.957      0.044   21.744   0.000        0.871       1.044
         lninc |      0.349      0.026   13.273   0.000        0.298       0.401
         pcs12 |     -0.040      0.003  -12.130   0.000       -0.046      -0.033
         mcs12 |     -0.016      0.003   -6.430   0.000       -0.021      -0.011
   1.ins_unins |     -1.644      0.050  -32.946   0.000       -1.742      -1.546
      1.anylim |      0.799      0.073   10.991   0.000        0.657       0.942
         _cons |     -0.695      0.342   -2.033   0.042       -1.364      -0.025
---------------+----------------------------------------------------------------
Amount_exp_tot |
           age |      0.015      0.001   14.849   0.000        0.013       0.017
      1.female |      0.166      0.033    5.066   0.000        0.102       0.230
         lninc |      0.071      0.016    4.397   0.000        0.039       0.103
         pcs12 |     -0.038      0.002  -23.361   0.000       -0.041      -0.034
         mcs12 |     -0.013      0.001   -8.964   0.000       -0.016      -0.010
   1.ins_unins |     -0.835      0.061  -13.712   0.000       -0.954      -0.716
      1.anylim |      0.335      0.037    8.949   0.000        0.261       0.408
         _cons |      8.986      0.211   42.638   0.000        8.573       9.399
--------------------------------------------------------------------------------

Expected expenditures and effects on them

predict(model(Any)) is the probability of any expenditures and predict(model(Amount)) the expected expenditures of someone who has any. Their product is the expected expenditures:

local twopart "predict(model(Any)) * predict(model(Amount))"
margins, expression(`twopart')
Predictive margins                                      Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(Any)) * predict(model(Amount))

------------------------------------------------------------------------------
             |            Delta-method
             |     Margin   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
       _cons |   3761.151     68.812   54.659   0.000     3626.283    3896.020
------------------------------------------------------------------------------
margins, dydx(age female ins_unins anylim) expression(`twopart')
Average marginal effects                                Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(Any)) * predict(model(Amount))
dy/dx wrt:  age 1.female 1.ins_unins 1.anylim

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
         age |     62.535      3.924   15.936   0.000       54.844      70.226
    1.female |    847.694    115.729    7.325   0.000      620.870    1074.518
 1.ins_unins |  -2613.180    111.904  -23.352   0.000    -2832.508   -2393.852
    1.anylim |   1442.154    143.638   10.040   0.000     1160.629    1723.680
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.

Expected expenditures average about $3,760 a year. Being uninsured lowers them by about $2,610, and each year of age raises them by about $63.

Effects in each part

Naming one model in predict() gives the effect in that part alone:

margins, dydx(ins_unins) predict(model(Any))
Average marginal effects                                Number of obs = 19,386
------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
   ins_unins |
          1  |     -0.242      0.009  -28.369   0.000       -0.259      -0.226
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.
margins, dydx(ins_unins) predict(model(Amount))
Average marginal effects                                Number of obs = 19,386
Model VCE: Robust

Expression: Predicted mean, predict(model(Amount))
dy/dx wrt:  1.ins_unins

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
 1.ins_unins |  -2483.980    133.881  -18.554   0.000    -2746.382   -2221.577
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.

Being uninsured lowers the probability of any expenditures by 24 percentage points, and the expected expenditures of someone who has any by about $2,480. Both are averaged over the whole sample.

Comparing groups

over() gives the effect within each group, and over(r.) the difference between the groups, with its test:

local twopart "predict(model(Any)) * predict(model(Amount))"
margins, dydx(ins_unins) over(female) expression(`twopart')
Average marginal effects                                Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(Any)) * predict(model(Amount))
dy/dx wrt:  1.ins_unins
Over:       female

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
0.ins_unins  |  (base outcome)
-------------+----------------------------------------------------------------
1.ins_unins  |
      female |
          0  |  -2169.846     95.791  -22.652   0.000    -2357.593   -1982.098
          1  |  -2976.536    139.569  -21.327   0.000    -3250.086   -2702.986
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.
margins, dydx(ins_unins) over(r.female) expression(`twopart')
Contrasts of average marginal effects                   Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(Any)) * predict(model(Amount))
dy/dx wrt:  1.ins_unins
Over:       female

------------------------------------------------
             |         df        chi2     P>chi2
-------------+----------------------------------
0b.ins_unins |
      female |  (omitted)
-------------+----------------------------------
1.ins_unins  |
      female |          1       69.50     0.0000
------------------------------------------------

--------------------------------------------------------------
             |   Contrast Delta-method
             |      dy/dx   std. err.     [95% conf. interval]
-------------+------------------------------------------------
0.ins_unins  |  (base outcome)
-------------+------------------------------------------------
1.ins_unins  |
      female |
   (1 vs 0)  |   -806.691     96.763      -996.343    -617.038
--------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the
      base level.

Being uninsured lowers expected expenditures by about $2,170 for men and $2,980 for women; the difference, about $810, is significant.

Other models for the second part

A linear second part. Fit regress among the people with any expenditures, and use its prediction in place of the glm one:

local x "c.age i.female c.lninc c.pcs12 c.mcs12 i.ins_unins i.anylim"
quietly regress exp_tot `x' if any, vce(cluster duid)
estimates store Linear
quietly suest2 Any Linear
margins, dydx(ins_unins) expression(predict(model(Any)) * predict(model(Linear)))
Average marginal effects                                Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(Any)) * predict(model(Linear))
dy/dx wrt:  1.ins_unins

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
 1.ins_unins |  -2287.542    109.406  -20.909   0.000    -2501.975   -2073.110
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.

A regression of log expenditures. When the second part is a regression of the log of the outcome, its prediction is on the log scale. Exponentiating it gives exp(xb), which understates the mean; the expected outcome is exp(xb) times the mean of the exponentiated errors. Two estimates of that constant are common: Duan’s (1983) smearing estimate, the mean of the exponentiated residuals, and, if the errors are normal, exp(s²/2), where s² is the residual variance. Compute either one from the stored regression and include it in expression():

local x "c.age i.female c.lninc c.pcs12 c.mcs12 i.ins_unins i.anylim"
generate double lnexp = ln(exp_tot) if any
(3,440 missing values generated)
quietly regress lnexp `x' if any, vce(cluster duid)
estimates store LnAmount
local s2 = e(rss) / e(N)
predict double res if e(sample), residuals
(3,440 missing values generated)
generate double expres = exp(res)
(3,440 missing values generated)
summarize expres, meanonly
local duan = r(mean)
display "Smearing constant: " %5.3f `duan' _n "Normal constant:   " %5.3f exp(`s2' / 2)
Smearing constant: 2.401
Normal constant:   2.586
quietly suest2 Any LnAmount
margins, dydx(ins_unins) expression(predict(model(Any)) * exp(predict(model(LnAmount))) * `duan')
Average marginal effects                                Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(Any)) * exp(predict(model(LnAmount))) * 2.400751486817317
dy/dx wrt:  1.ins_unins

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
 1.ins_unins |  -3110.501     79.897  -38.931   0.000    -3267.096   -2953.906
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.
margins, dydx(ins_unins) expression(predict(model(Any)) * exp(predict(model(LnAmount)) + `s2' / 2)
)
Average marginal effects                                Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(Any)) * exp(predict(model(LnAmount)) + 1.900226370523759 / 2)
dy/dx wrt:  1.ins_unins

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
 1.ins_unins |  -3350.519     86.062  -38.931   0.000    -3519.197   -3181.841
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.

margins treats the constant as known, so the standard errors do not include the uncertainty in estimating it.

Weights and survey data

Sampling weights. Fit each part with the weights but without vce(), and give the clustering to suest2 with cluster():

local x "c.age i.female c.lninc c.pcs12 c.mcs12 i.ins_unins i.anylim"
quietly logit any `x' [pw = wtdper]
estimates store AnyW
quietly glm exp_tot `x' if any [pw = wtdper], family(gamma) link(log)
estimates store AmountW
quietly suest2 AnyW AmountW, cluster(duid)
margins, dydx(ins_unins) expression(predict(model(AnyW)) * predict(model(AmountW)))
Average marginal effects                                Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(AnyW)) * predict(model(AmountW))
dy/dx wrt:  1.ins_unins

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
 1.ins_unins |  -2480.770    163.599  -15.164   0.000    -2801.418   -2160.122
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.

Survey data. svyset the data and fit each part with svy:. Fit the second part with if any, as above, not with subpop(); suest2 computes the standard errors from the full survey design.

local x "c.age i.female c.lninc c.pcs12 c.mcs12 i.ins_unins i.anylim"
svyset duid [pw = wtdper]
Sampling weights: wtdper
             VCE: linearized
      Subpop. df: off
     Single unit: missing
        Strata 1: <one>
 Sampling unit 1: duid
           FPC 1: <zero>
quietly svy: logit any `x'
estimates store AnyS
quietly svy: glm exp_tot `x' if any, family(gamma) link(log)
estimates store AmountS
quietly suest2 AnyS AmountS
margins, dydx(ins_unins) expression(predict(model(AnyS)) * predict(model(AmountS)))
Average marginal effects

Number of strata =      1                        Number of obs   =      19,386
Number of PSUs   = 10,940                        Population size = 187,973,715
Model VCE: Linearized                            Design df       =      10,939

Expression: predict(model(AnyS)) * predict(model(AmountS))
dy/dx wrt:  1.ins_unins

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
 1.ins_unins |  -2480.770    163.599  -15.164   0.000    -2801.454   -2160.087
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.

Comparing two two-part models

suest2 can combine more than one two-part model. Here a two-part model without the health measures (pcs12, mcs12, and anylim) is combined with the full one, to ask how much the effect of being uninsured changes when health is held constant. The difference between the two expected outcomes goes in one expression():

local x1 "c.age i.female c.lninc i.ins_unins"
quietly logit any `x1', vce(cluster duid)
estimates store Any1
quietly glm exp_tot `x1' if any, family(gamma) link(log) vce(cluster duid)
estimates store Amount1
quietly suest2 Any1 Amount1 Any Amount
local tp1 "predict(model(Any1)) * predict(model(Amount1))"
local tp2 "predict(model(Any)) * predict(model(Amount))"
margins, dydx(ins_unins) expression(`tp1')
Average marginal effects                                Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(Any1)) * predict(model(Amount1))
dy/dx wrt:  1.ins_unins

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
 1.ins_unins |  -2842.902    119.446  -23.801   0.000    -3077.013   -2608.792
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.
margins, dydx(ins_unins) expression(`tp2')
Average marginal effects                                Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(Any)) * predict(model(Amount))
dy/dx wrt:  1.ins_unins

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
 1.ins_unins |  -2613.180    111.904  -23.352   0.000    -2832.508   -2393.852
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.
margins, dydx(ins_unins) expression(`tp1' - `tp2')
Average marginal effects                                Number of obs = 19,386
Model VCE: Robust

Expression: predict(model(Any1)) * predict(model(Amount1)) - predict(model(Any)) *
            predict(model(Amount))
dy/dx wrt:  1.ins_unins

------------------------------------------------------------------------------
             |            Delta-method
             |      dy/dx   std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
 1.ins_unins |   -229.722     65.246   -3.521   0.000     -357.602    -101.842
------------------------------------------------------------------------------
Note: dy/dx for factor levels is the discrete change from the base level.

Without the health measures, being uninsured lowers expected expenditures by about $2,840; with them, by about $2,610. The difference, about $230, is significant.

References

Belotti, Federico, Partha Deb, Willard G. Manning, and Edward C. Norton. 2015. “twopm: Two-part Models.” Stata Journal 15(1): 3–20. https://doi.org/10.1177/1536867X1501500102

Duan, Naihua. 1983. “Smearing Estimate: A Nonparametric Retransformation Method.” Journal of the American Statistical Association 78(383): 605–610. https://doi.org/10.2307/2288126

Back to top