Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Today we code the two-step CCP estimator of Class 12 for the Zurcher model and put it next to the NFXP estimator of Class 10. Finite dependence turns the dynamic model into a static logit that takes milliseconds to estimate. The price is paid in the first stage, and we will see exactly where.

One-period finite dependence in the Zurcher modelΒΆ

Engine replacement is a renewal action. After a replacement at t+1t+1, the mileage at t+2t+2 is drawn from Ο€(β‹…βˆ£0)\pi(\cdot|0) independent of what happened at tt. So the two choice sequences keep, then replace and replace, then replace reach the same distribution of the state after one period. Take d=keepd=\text{keep}, dβ€²=replaced'=\text{replace} and the renewal action r=replacer=\text{replace}, and use the EV1 mapping ψ(xβ€²,replace)=Ξ³βˆ’log⁑P(replace∣xβ€²)\psi(x',\text{replace}) = \gamma - \log P(\text{replace}|x'):

Ξ”v(x)=u(x,keep)βˆ’u(x,replace)+Ξ²βˆ‘xβ€²[u(xβ€²,replace)+Ξ³βˆ’log⁑P(replace∣xβ€²)][Ο€(xβ€²βˆ£x,keep)βˆ’Ο€(xβ€²βˆ£x,replace)]\begin{aligned} \Delta v(x) = {} & u(x,\text{keep}) - u(x,\text{replace}) \\ & + \beta \sum_{x'} \big[ u(x',\text{replace}) + \gamma - \log P(\text{replace}|x') \big] \big[ \pi(x'|x,\text{keep}) - \pi(x'|x,\text{replace}) \big] \end{aligned}

Neither u(xβ€²,replace)=βˆ’RCu(x',\text{replace}) = -RC nor Ξ³\gamma depends on xβ€²x'. Both multiply the difference of two probability distributions, each integrates to one giving difference of zero, so they drop out:

Ξ”v(x)=u(x,keep)βˆ’u(x,replace)βˆ’Ξ²βˆ‘xβ€²log⁑P(replace∣xβ€²)[Ο€(xβ€²βˆ£x,keep)βˆ’Ο€(xβ€²βˆ£x,replace)]\begin{aligned} \Delta v(x) = {} & u(x,\text{keep}) - u(x,\text{replace}) \\ & - \beta \sum_{x'} \log P(\text{replace}|x') \big[ \pi(x'|x,\text{keep}) - \pi(x'|x,\text{replace}) \big] \end{aligned}

Writing the future relative to keep instead does not work: v(x+1,keep)β‰ v(0,keep)v(x+1,\text{keep}) \ne v(0,\text{keep}), so nothing cancels.

Section 2 of Arcidiacono & Miller (2011) work with a modified Zurcher model where transition probabilities and costs are specified differently. With their deterministic increment of one mileage unit and a reset to zero, the sum collapses to Ξ²log⁑P(replace∣0)βˆ’Ξ²log⁑P(replace∣x+1)\beta \log P(\text{replace}|0) - \beta \log P(\text{replace}|x+1).

In the parameterization of zurcher.py, u(x,keep)=βˆ’0.001 c xu(x,\text{keep}) = -0.001\,c\,x and u(x,replace)=βˆ’RCu(x,\text{replace}) = -RC, and replacing moves the bus by the first row of the keep transition matrix Ξ \Pi. The choice probability becomes

P(keep∣x)=Ξ›(RCβˆ’0.001 c x+offset(x)),P(\text{keep}|x) = \Lambda\big( RC - 0.001\,c\,x + \text{offset}(x) \big),
offset(x)=βˆ’Ξ²βˆ‘xβ€²log⁑P(replace∣xβ€²) [Ξ x,xβ€²βˆ’Ξ 0,xβ€²]\text{offset}(x) = -\beta \sum_{x'} \log P(\text{replace}|x')\, \big[ \Pi_{x,x'} - \Pi_{0,x'} \big]

where Ξ›(βˆ™)=exp⁑(βˆ™)/[1+exp⁑(βˆ™)]\Lambda(\bullet) = \exp(\bullet)/[1+\exp(\bullet)] converts value difference to choice probability.

CCP estimatorΒΆ

Stage 1

Using data pairs (xt,dt)(x_t,d_t) over a panel with at least two periods:

using consistent estimator, e.g. the frequency estimator or a flexible logit model

Stage 2

Form a (pseudo) statistical criterion as a function of first stage estimates and the structural parameters (RC,c)(RC, c), and apply standard estimation technique, for example GMM, MSM or MLE.

In our case we have P(keep∣x)P(\text{keep}|x) as a function of (RC,c)(RC, c) and the first-stage CCPs, so we can use MLE in a standard static binary logit with a given offset.

There is no fixed point to solve, not once!

  • Pretty sweet, right?

  • Where do you expect issues to arise?

The NFXP benchmark and the offsetΒΆ

The representation is exact, not an approximation. Here we need the NFXP estimates as the benchmark to compare with, and the model’s own CCPs at those estimates for the plots below.

Source
NFXP estimates, beta = 0.9999, n = 175, N = 8156 bus-months
method bhhh, converged True, 0.438 s, 52 inner solves, 183 inner iterations
parameter     estimate        s.e.
RC             9.76863     1.22592
c              1.34286     0.31520
log-likelihood -8607.8894

Pi - Pi[0] broadcasts: row 0 is subtracted from every row, so the whole offset is one matrix-vector product. The code omits Ξ³\gamma from the logsum throughout, which is harmless because it cancels.

First stage: CCPs from the dataΒΆ

The two-step estimator replaces the model’s CCPs by estimates from the data, and the transition matrix by the increment frequencies that estim_zurcher already computes in its stage 1. The algorithm:

Input: panel (x_it, d_it), transition matrix Ξ  estimated from increment frequencies
Algorithm:
  1. Estimate log P(replace|x) on the whole mileage grid
  2. offset(x) = -Ξ² Ξ£_x' log P(replace|x') [Ξ (x,x') - Ξ (0,x')] for every grid point
  3. Maximize the logit likelihood of keep with index RC - 0.001 c x + offset(x)
Output: estimates of (RC, c), without solving the model

The natural first stage is the bin estimator, replacements over visits in each mileage bin:

24 of 175 bins never visited, 105 visited but no replacement observed
finite offsets: 0 of 175
/tmp/ipykernel_3569/769908632.py:5: RuntimeWarning: invalid value encountered in matmul
  return -model.beta * (Pi - Pi[0]) @ logP_rep     # replace: row 0, a fresh engine, for every x

It fails completely. Zurcher replaced 60 engines in 8156 bus-months, and most mileage bins either were never visited or never saw a replacement. The log of a zero frequency is βˆ’βˆž-\infty, and since the offset at every xx involves the bins right after a fresh engine, xβ€²=0,…,Jx' = 0, \dots, J, where nobody replaces, not a single offset is finite.

The first stage has to be smoothed. A flexible logit of the replacement dummy on a polynomial in mileage is the standard choice: it pools information across bins and extrapolates to the ones with no replacements. We need a logit estimator for the first stage and again for the second, so we write it once:

The structure is that of newton() in the Classic solvers lecture: tol, maxiter and an optional callback, and a for ... else loop that raises an error if the iterations run out.

The logit likelihood is globally concave, but that does not make plain Newton safe: from b=0b=0 the first step on the second-stage problem below lands at cβ‰ˆβˆ’230c \approx -230, the next at cβ‰ˆ120c \approx 120, and the iterations diverge. Halving the step until the likelihood improves fixes it. The first stage uses Chebyshev polynomials of mileage mapped to [βˆ’1,1][-1,1], because with the plain powers xkx^k the Hessian of the first stage is numerically singular already at degree 5:

                            degree 1          degree 2          degree 3          degree 4          degree 5         degree 10
T0                    -4.160 (0.131)    -5.752 (0.556)    -5.793 (0.683)    -5.476 (2.361)   -11.701 (5.767) 101.526 (101.267)
T1                     3.152 (0.344)     2.745 (0.483)     7.752 (2.336)     7.626 (2.424)   34.863 (16.502) 407.997 (298.969)
T2                                      -2.017 (0.635)    -2.050 (0.770)    -1.572 (3.504)   -10.589 (8.303) 221.165 (191.744)
T3                                                         2.269 (1.005)     2.219 (1.040)    18.469 (9.448) 260.330 (199.718)
T4                                                                           0.174 (1.250)    -2.715 (2.654) 214.513 (163.634)
T5                                                                                             4.728 (2.566)   98.775 (90.264)
T6                                                                                                           151.201 (111.326)
T7                                                                                                             16.798 (30.153)
T8                                                                                                             66.100 (48.843)
T9                                                                                                              -0.118 (6.598)
T10                                                                                                            13.684 (10.231)
log-likelihood               -306.92           -299.66           -296.70           -296.70           -294.55           -293.37
log P(replace|0)               -7.31            -10.51            -17.86            -16.72            -83.07            -15.59

Second stage: a static logit with an offsetΒΆ

With the offset computed from the first stage, the second stage is the logit of logit_mle with two regressors, the constant for RCRC and βˆ’0.001 x-0.001\,x for cc, and the offset passed through:

first stage           RC    s.e.       c    s.e.    loglik  -log P(rep|0)
NFXP               9.769   1.226   1.343   0.315   -300.57           9.77
logit, degree 1    7.346   0.374   0.720   3.960   -306.64           7.31
logit, degree 2   10.528   0.505   1.795   5.483   -299.60          10.51
logit, degree 3   17.471   0.499   1.324   5.423   -296.77          17.86
logit, degree 4   16.399   0.499   1.310   5.424   -296.77          16.72
logit, degree 5                    fails: singular Hessian          83.07
logit, degree 10                   fails: singular Hessian          15.59

The estimate of cc is fairly stable across the first stages, between 1.3 and 1.8 for degrees 2 to 4, around the NFXP value. The estimate of RCRC is not: it ranges from 7.3 to 17.5, and it follows the last column almost one for one. The plot of the first stages shows why.

Source
<Figure size 800x450 with 1 Axes>

Where replacements are observed, between roughly 120 and 300 thousand miles, all first stages agree with each other and with the model. Below 100 thousand miles there are no replacements at all, and each polynomial extrapolates in its own way, reaching log⁑P(replace∣0)\log P(\text{replace}|0) anywhere from -7 to -18. That is exactly where the offset needs them: offset(x)\text{offset}(x) contains Ξ²βˆ‘xβ€²Ξ 0,xβ€²log⁑P(replace∣xβ€²)\beta \sum_{x'} \Pi_{0,x'} \log P(\text{replace}|x'), the same constant for every observation, and a constant in the index of a logit is absorbed by the intercept, RCRC. The data identify RCRC only through the CCPs of a fresh engine, which Zurcher’s data never show being replaced. Push the degree to 5 and the extrapolated log⁑P(replace∣0)\log P(\text{replace}|0) falls below -80; the offsets are then so large that every predicted probability is 0 or 1, and the Hessian of the second stage is singular.

The same second stage with estimagicΒΆ

Writing the Newton iterations by hand shows what happens inside. In practice a package does the optimization and the inference:

The second version of logit_mle has the same inputs and outputs as the first:

Run both on the second stage with the degree 3 first stage:

Newton     RC  17.4709 (0.4991)  c  1.3238 (5.4234)  loglik -296.7745
estimagic  RC  17.4709 (0.4963)  c  1.3239 (5.3908)  loglik -296.7745

The two agree to the tolerance of the optimizer, about 10-4 in cc. The standard errors differ in the second digit, because estimagic differentiates numerically, and they share the same flaw discussed below: both treat the offset as data.

Where it breaksΒΆ

The comparison with NFXP isolates three problems of the two-step approach, none of which shows up in the likelihood value.

What the two-step estimator buys is speed. One CCP estimation takes about 8 ms here, against 0.18 s for the NFXP partial likelihood, and the gap grows with the state space, because the second stage never touches a matrix of the size of the state space beyond one matrix-vector product. Pseudo-likelihood methods, which update the CCPs using the model, are the way to keep the speed and restore the discipline, and they are the subject of Class 14.

  • Would a larger panel of Zurcher’s buses fix the identification of RCRC? What kind of data would?

  • Which of the three problems above would disappear if the first stage were a correctly specified parametric model?

  • NFXP and CCP give the same cc but very different RCRC. Which counterfactuals of Class 10 would be affected, and which would not?

ExercisesΒΆ

All four start from the objects defined above: data, est, res, logP_rep, P_freq and the functions fd_offset, logit_mle, ccp_logit and estimate_ccp.

Frequency first stage with a floorΒΆ

The bin estimator becomes usable once the zeros are replaced by a small floor Ξ΅\varepsilon, and so do the unvisited bins:

for eps in (1e-2, 1e-3, 1e-4, 1e-6):
    lp = np.log(np.clip(np.nan_to_num(P_freq), eps, 1))   # NaN -> 0 -> eps
    th, se, ll = estimate_ccp(est, lp)
    print(f'floor {eps:g}:  RC {th[0]:7.3f}  c {th[1]:7.3f}  loglik {ll:9.2f}')
  • How do RCRC and cc move with the floor, and why does cc move so much more than with the logit first stage?

  • Is there a principled way to choose Ξ΅\varepsilon?

Monte Carlo: which part of the error comes from the first stage?ΒΆ

Simulate panels from the model at the NFXP estimates, using sim_zurcher.py in the folder of this class, and estimate each of them three ways: the infeasible two-step estimator with the true CCPs logP_rep in the first stage, the feasible one with the flexible logit, and NFXP.

from sim_zurcher import simulate

for N in (100, 1000, 10000):                                # buses, 120 months each
    sim = simulate(est, N=N, T=120, seed=1, pk=pk, x0=0)    # new engines at the start, like the data
    m = estim_zurcher(sim, J=est.J, p=est.p)                # same grid and transition probabilities
    th_inf, se_inf, _ = estimate_ccp(m, logP_rep)           # true CCPs
    th_3, _, _ = estimate_ccp(m, ccp_logit(m, 3))           # estimated CCPs
    r = m.estimate(full=False)
    print(f'N {N:6d}  infeasible RC {th_inf[0]:6.3f} c {th_inf[1]:6.3f} (s.e. {se_inf[1]:.2f})  '
          f'logit-3 RC {th_3[0]:6.3f} c {th_3[1]:6.3f}  NFXP RC {r.theta[0]:6.3f} c {r.theta[1]:6.3f} (s.e. {r.se[1]:.2f})')
  • Is the infeasible estimator consistent? Is the feasible one, with the degree held at 3 as NN grows?

  • Compare the standard errors of cc of the infeasible estimator and NFXP. How many more buses does the two-step estimator need for the same precision?

Estimation under different Ξ²\betaΒΆ

for beta in (0.0, 0.9, 0.99, 0.9999):
    m = estim_zurcher(data, beta=beta)
    th, _, _ = estimate_ccp(m, ccp_logit(m, 3))
    r = m.estimate(full=False)
    print(f'beta {beta:<7} CCP RC {th[0]:8.3f} c {th[1]:7.3f}   NFXP RC {r.theta[0]:8.3f} c {r.theta[1]:7.3f}')
  • Why do the two estimators agree exactly at Ξ²=0\beta = 0?

  • Ξ²\beta enters the second stage as a coefficient on the offset. Why can it not be estimated as a third coefficient of the same logit?

Run timeΒΆ

%timeit estimate_ccp(est, ccp_logit(est, 3))
%timeit estim_zurcher(data).estimate(full=False)

Compare the two, then repeat on a grid of twice and four times as many bins, read_busdata(n=350, max_miles=900_000) and so on, and report how each time scales with nn.

ReferencesΒΆ
  1. Arcidiacono, P., & Miller, R. A. (2011). Conditional Choice Probability Estimation of Dynamic Discrete Choice Models With Unobserved Heterogeneity. Econometrica, 79(6), 1823–1867. 10.3982/ECTA7743
  2. Hotz, V. J., & Miller, R. A. (1993). Conditional Choice Probabilities and the Estimation of Dynamic Models. The Review of Economic Studies, 60(3), 497–529. 10.2307/2298122
  3. Rust, J. (1987). Optimal Replacement of GMC Bus Engines: An Empirical Model of Harold Zurcher. Econometrica, 55(5), 999. 10.2307/1911259
  4. Aguirregabiria, V., & Mira, P. (2002). Swapping the Nested Fixed Point Algorithm: A Class of Estimators for Discrete Markov Decision Models. Econometrica, 70(4), 1519–1543. 10.1111/1468-0262.00340