Coverage for HARK / Calibration / Income / IncomeProcesses.py: 90%

375 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-08 05:31 +0000

1""" 

2This file has various classes and functions for constructing income processes. 

3""" 

4 

5import numpy as np 

6from scipy.stats import norm 

7from HARK.metric import MetricObject 

8from HARK.distributions import ( 

9 add_discrete_outcome, 

10 add_discrete_outcome_constant_mean, 

11 combine_indep_dstns, 

12 DiscreteDistribution, 

13 DiscreteDistributionLabeled, 

14 IndexDistribution, 

15 MeanOneLogNormal, 

16 Lognormal, 

17 Uniform, 

18 make_tauchen_ar1, 

19) 

20from HARK.interpolation import IdentityFunction, LinearInterp 

21from HARK.utilities import get_percentiles, make_polynomial_params 

22 

23 

24class BinaryIncShkDstn(DiscreteDistribution): 

25 """ 

26 A one period income shock distribution (transitory, permanent, or other) 

27 with only two outcomes. One probability and value are specified, and the 

28 other is implied to make it a mean one distribution. 

29 

30 Parameters 

31 ---------- 

32 shk_prob : float 

33 Probability of one of the income shock outcomes. 

34 shk_val : float 

35 Value of the specified income shock outcome. 

36 seed : int, optional 

37 Random seed. The default is 0. 

38 

39 Returns 

40 ------- 

41 ShkDstn : DiscreteDistribution 

42 Binary income shock distribuion. 

43 """ 

44 

45 def __init__(self, shk_prob, shk_val, seed=0): 

46 if shk_prob > 1.0 or shk_prob < 0.0: 

47 raise ValueError("Shock probability must be between 0 and 1!") 

48 

49 other_prob = 1.0 - shk_prob 

50 other_val = (1.0 - shk_prob * shk_val) / other_prob 

51 probs = [shk_prob, other_prob] 

52 vals = [shk_val, other_val] 

53 super().__init__(pmv=probs, atoms=vals, seed=seed) 

54 

55 

56class LognormPermIncShk(DiscreteDistribution): 

57 """ 

58 A one-period distribution of a multiplicative lognormal permanent income shock. 

59 Parameters 

60 ---------- 

61 sigma : float 

62 Standard deviation of the log-shock. 

63 n_approx : int 

64 Number of points to use in the discrete approximation. 

65 neutral_measure : Bool, optional 

66 Whether to use Harmenberg's permanent-income-neutral measure. The default is False. 

67 seed : int, optional 

68 Random seed. The default is 0. 

69 

70 Returns 

71 ------- 

72 PermShkDstn : DiscreteDistribution 

73 Permanent income shock distribution. 

74 

75 """ 

76 

77 def __init__(self, sigma, n_approx, neutral_measure=False, seed=0): 

78 # Construct an auxiliary discretized normal 

79 lognormal_dstn = MeanOneLogNormal(sigma) 

80 logn_approx = lognormal_dstn.discretize( 

81 n_approx if sigma > 0.0 else 1, method="equiprobable", tail_N=0 

82 ) 

83 

84 limit = { 

85 "dist": lognormal_dstn, 

86 "method": "equiprobable", 

87 "N": n_approx, 

88 "endpoints": False, 

89 "infimum": logn_approx.limit["infimum"], 

90 "supremum": logn_approx.limit["supremum"], 

91 } 

92 

93 # Change the pmv if necessary 

94 if neutral_measure: 

95 logn_approx.pmv = (logn_approx.atoms * logn_approx.pmv).flatten() 

96 

97 super().__init__( 

98 pmv=logn_approx.pmv, atoms=logn_approx.atoms, limit=limit, seed=seed 

99 ) 

100 

101 

102class MixtureTranIncShk(DiscreteDistribution): 

103 """ 

104 A one-period distribution for transitory income shocks that are a mixture 

105 between a log-normal and a single-value unemployment shock. 

106 

107 Parameters 

108 ---------- 

109 sigma : float 

110 Standard deviation of the log-shock. 

111 UnempPrb : float 

112 Probability of the "unemployment" shock. 

113 IncUnemp : float 

114 Income shock in the "unemployment" state. 

115 n_approx : int 

116 Number of points to use in the discrete approximation. 

117 seed : int, optional 

118 Random seed. The default is 0. 

119 

120 Returns 

121 ------- 

122 TranShkDstn : DiscreteDistribution 

123 Transitory income shock distribution. 

124 

125 """ 

126 

127 def __init__(self, sigma, UnempPrb, IncUnemp, n_approx, seed=0): 

128 dstn_orig = MeanOneLogNormal(sigma) 

129 dstn_approx = dstn_orig.discretize( 

130 n_approx if sigma > 0.0 else 1, method="equiprobable", tail_N=0 

131 ) 

132 

133 if UnempPrb > 0.0: 

134 dstn_approx = add_discrete_outcome_constant_mean( 

135 dstn_approx, p=UnempPrb, x=IncUnemp 

136 ) 

137 

138 super().__init__( 

139 pmv=dstn_approx.pmv, 

140 atoms=dstn_approx.atoms, 

141 limit=dstn_approx.limit, 

142 seed=seed, 

143 ) 

144 

145 

146class MixtureTranIncShk_HANK(DiscreteDistribution): 

147 """ 

148 A one-period distribution for transitory income shocks that are a mixture 

149 between a log-normal and a single-value unemployment shock. This version 

150 has additional parameters that makes it useful for HANK models. 

151 

152 Parameters 

153 ---------- 

154 sigma : float 

155 Standard deviation of the log-shock. 

156 UnempPrb : float 

157 Probability of the "unemployment" shock. 

158 IncUnemp : float 

159 Income shock in the "unemployment" state. 

160 n_approx : int 

161 Number of points to use in the discrete approximation. 

162 tax_rate : float 

163 Flat tax rate on labor income. 

164 labor : float 

165 Intensive margin labor supply. 

166 wage : float 

167 Wage rate scaling factor. 

168 seed : int, optional 

169 Random seed. The default is 0. 

170 Returns 

171 ------- 

172 TranShkDstn : DiscreteDistribution 

173 Transitory income shock distribution. 

174 """ 

175 

176 def __init__( 

177 self, 

178 sigma, 

179 UnempPrb, 

180 IncUnemp, 

181 n_approx, 

182 wage, 

183 labor, 

184 tax_rate, 

185 seed=0, 

186 ): 

187 dstn_approx = MeanOneLogNormal(sigma).discretize( 

188 n_approx if sigma > 0.0 else 1, method="equiprobable", tail_N=0 

189 ) 

190 

191 if UnempPrb > 0.0: 

192 dstn_approx = add_discrete_outcome_constant_mean( 

193 dstn_approx, p=UnempPrb, x=IncUnemp 

194 ) 

195 # Rescale the transitory shock values to account for new features 

196 TranShkMean_temp = (1.0 - tax_rate) * labor * wage 

197 dstn_approx.atoms *= TranShkMean_temp 

198 super().__init__( 

199 pmv=dstn_approx.pmv, 

200 atoms=dstn_approx.atoms, 

201 limit=dstn_approx.limit, 

202 seed=seed, 

203 ) 

204 

205 

206class BufferStockIncShkDstn(DiscreteDistributionLabeled): 

207 """ 

208 A one-period distribution object for the joint distribution of income 

209 shocks (permanent and transitory), as modeled in the Buffer Stock Theory 

210 paper: 

211 - Lognormal, discretized permanent income shocks. 

212 - Transitory shocks that are a mixture of: 

213 - A lognormal distribution in normal times. 

214 - An "unemployment" shock. 

215 

216 Parameters 

217 ---------- 

218 sigma_Perm : float 

219 Standard deviation of the log- permanent shock. 

220 sigma_Tran : float 

221 Standard deviation of the log- transitory shock. 

222 n_approx_Perm : int 

223 Number of points to use in the discrete approximation of the permanent shock. 

224 n_approx_Tran : int 

225 Number of points to use in the discrete approximation of the transitory shock. 

226 UnempPrb : float 

227 Probability of the "unemployment" shock. 

228 IncUnemp : float 

229 Income shock in the "unemployment" state. 

230 neutral_measure : Bool, optional 

231 Whether to use Hamenberg's permanent-income-neutral measure. The default is False. 

232 seed : int, optional 

233 Random seed. The default is 0. 

234 

235 Returns 

236 ------- 

237 IncShkDstn : DiscreteDistribution 

238 Income shock distribution. 

239 

240 """ 

241 

242 def __init__( 

243 self, 

244 sigma_Perm, 

245 sigma_Tran, 

246 n_approx_Perm, 

247 n_approx_Tran, 

248 UnempPrb, 

249 IncUnemp, 

250 neutral_measure=False, 

251 seed=0, 

252 ): 

253 perm_dstn = LognormPermIncShk( 

254 sigma=sigma_Perm, n_approx=n_approx_Perm, neutral_measure=neutral_measure 

255 ) 

256 tran_dstn = MixtureTranIncShk( 

257 sigma=sigma_Tran, 

258 UnempPrb=UnempPrb, 

259 IncUnemp=IncUnemp, 

260 n_approx=n_approx_Tran, 

261 ) 

262 joint_dstn = combine_indep_dstns(perm_dstn, tran_dstn) 

263 

264 super().__init__( 

265 name="Joint distribution of permanent and transitory shocks to income", 

266 var_names=["PermShk", "TranShk"], 

267 pmv=joint_dstn.pmv, 

268 atoms=joint_dstn.atoms, 

269 limit=joint_dstn.limit, 

270 seed=seed, 

271 ) 

272 

273 

274class IncShkDstn_HANK(DiscreteDistributionLabeled): 

275 """ 

276 A one-period distribution object for the joint distribution of income 

277 shocks (permanent and transitory), as modeled in the Buffer Stock Theory 

278 paper: 

279 - Lognormal, discretized permanent income shocks. 

280 - Transitory shocks that are a mixture of: 

281 - A lognormal distribution in normal times. 

282 - An "unemployment" shock. 

283 

284 This version has additional features that make it particularly useful for HANK models. 

285 

286 Parameters 

287 ---------- 

288 sigma_Perm : float 

289 Standard deviation of the log- permanent shock. 

290 sigma_Tran : float 

291 Standard deviation of the log- transitory shock. 

292 n_approx_Perm : int 

293 Number of points to use in the discrete approximation of the permanent shock. 

294 n_approx_Tran : int 

295 Number of points to use in the discrete approximation of the transitory shock. 

296 UnempPrb : float 

297 Probability of the "unemployment" shock. 

298 IncUnemp : float 

299 Income shock in the "unemployment" state. 

300 tax_rate : float 

301 Flat tax rate on labor income. 

302 labor : float 

303 Intensive margin labor supply. 

304 wage : float 

305 Wage rate scaling factor. 

306 neutral_measure : Bool, optional 

307 Whether to use Harmenberg's permanent-income-neutral measure. The default is False. 

308 seed : int, optional 

309 Random seed. The default is 0. 

310 Returns 

311 ------- 

312 IncShkDstn : DiscreteDistribution 

313 Income shock distribution. 

314 """ 

315 

316 def __init__( 

317 self, 

318 sigma_Perm, 

319 sigma_Tran, 

320 n_approx_Perm, 

321 n_approx_Tran, 

322 UnempPrb, 

323 IncUnemp, 

324 tax_rate, 

325 labor, 

326 wage, 

327 neutral_measure=False, 

328 seed=0, 

329 ): 

330 perm_dstn = LognormPermIncShk( 

331 sigma=sigma_Perm, n_approx=n_approx_Perm, neutral_measure=neutral_measure 

332 ) 

333 tran_dstn = MixtureTranIncShk_HANK( 

334 sigma=sigma_Tran, 

335 UnempPrb=UnempPrb, 

336 IncUnemp=IncUnemp, 

337 n_approx=n_approx_Tran, 

338 wage=wage, 

339 labor=labor, 

340 tax_rate=tax_rate, 

341 ) 

342 joint_dstn = combine_indep_dstns(perm_dstn, tran_dstn) 

343 

344 super().__init__( 

345 name="HANK", 

346 var_names=["PermShk", "TranShk"], 

347 pmv=joint_dstn.pmv, 

348 atoms=joint_dstn.atoms, 

349 limit=joint_dstn.limit, 

350 seed=seed, 

351 ) 

352 

353 

354############################################################################### 

355 

356 

357def construct_lognormal_wage_dstn( 

358 T_cycle, WageRteMean, WageRteStd, WageRteCount, IncUnemp, UnempPrb, RNG 

359): 

360 """ 

361 Constructor for an age-dependent wage rate distribution. The distribution 

362 at each age is (equiprobably discretized) lognormal with a point mass to 

363 represent unemployment. This is effectively a "transitory only" income process. 

364 

365 Parameters 

366 ---------- 

367 T_cycle : int 

368 Number of periods in the agent's cycle or sequence. 

369 WageRteMean : [float] 

370 Age-varying list (or array) of mean wage rates. 

371 WageRteStd : [float] 

372 Age-varying standard deviations of (log) wage rates. 

373 WageRteCount : int 

374 Number of equiprobable nodes in the lognormal approximation. 

375 UnempPrb : [float] or float 

376 Age-varying probability of unemployment; can be specified to be constant. 

377 IncUnemp : [float] or float 

378 Age-varying "wage" rate when unemployed, maybe representing benefits. 

379 Can be specified to be constant. 

380 RNG : np.random.RandomState 

381 Agent's internal random number generator. 

382 

383 Returns 

384 ------- 

385 WageRteDstn : [DiscreteDistribution] 

386 Age-varying list of discrete approximations to the lognormal wage distribution. 

387 """ 

388 if len(WageRteMean) != T_cycle: 

389 raise ValueError("WageRteMean must be a list of length T_cycle!") 

390 if len(WageRteStd) != T_cycle: 

391 raise ValueError("WageRteStd must be a list of length T_cycle!") 

392 if not (isinstance(UnempPrb, float) or len(UnempPrb) == T_cycle): 

393 raise ValueError("UnempPrb must be a single value or list of length T_cycle!") 

394 if not (isinstance(IncUnemp, float) or len(IncUnemp) == T_cycle): 

395 raise ValueError("IncUnemp must be a single value or list of length T_cycle!") 

396 

397 WageRteDstn = [] 

398 N = WageRteCount # lazy typing 

399 for t in range(T_cycle): 

400 # Get current period values 

401 W_sig = WageRteStd[t] 

402 W_mu = np.log(WageRteMean[t]) - 0.5 * W_sig**2 

403 B = IncUnemp if isinstance(IncUnemp, float) else IncUnemp[t] 

404 U = UnempPrb if isinstance(UnempPrb, float) else UnempPrb[t] 

405 temp_dstn = Lognormal(mu=W_mu, sigma=W_sig, seed=RNG.integers(0, 2**31 - 1)) 

406 temp_dstn_alt = add_discrete_outcome(temp_dstn.discretize(N), B, U) 

407 WageRteDstn.append(temp_dstn_alt) 

408 return WageRteDstn 

409 

410 

411def construct_lognormal_income_process_unemployment( 

412 T_cycle, 

413 PermShkStd, 

414 PermShkCount, 

415 TranShkStd, 

416 TranShkCount, 

417 T_retire, 

418 UnempPrb, 

419 IncUnemp, 

420 UnempPrbRet, 

421 IncUnempRet, 

422 RNG, 

423 neutral_measure=False, 

424): 

425 r""" 

426 Generates a list of discrete approximations to the income process for each 

427 life period, from end of life to beginning of life. Permanent shocks (:math:`\psi`) are mean 

428 one lognormally distributed with standard deviation PermShkStd[t] during the 

429 working life, and degenerate at 1 in the retirement period. Transitory shocks (:math:`\theta`) 

430 are mean one lognormally distributed with a point mass at IncUnemp with 

431 probability UnempPrb while working; they are mean one with a point mass at 

432 IncUnempRet with probability UnempPrbRet. Retirement occurs 

433 after t=T_retire periods of working. 

434 

435 .. math:: 

436 \begin{align*} 

437 \psi_t &\sim \begin{cases} 

438 \exp(\mathcal{N}(-\textbf{PermShkStd}_{t}^{2}/2,\textbf{PermShkStd}_{t}^{2})) & \text{if } t \leq t_{\text{retire}}\\ 

439 1 & \text{if } t > t_{\text{retire}} 

440 \end{cases}\\ 

441 p_{\text{unemp}} & = \begin{cases} 

442 \textbf{UnempPrb} & \text{if } t \leq t_{\text{retire}} \\ 

443 \textbf{UnempPrbRet} & \text{if } t > t_{\text{retire}} \\ 

444 \end{cases}\\ 

445 &\text{if } p > p_{\text{unemp}} \\ 

446 \theta_t &\sim\begin{cases} 

447 \exp(\mathcal{N}(-\textbf{PermShkStd}_{t}^{2}/2-\ln(\frac{1-\textbf{IncUnemp }\textbf{UnempPrb}}{1-\textbf{UnempPrb}}),\textbf{PermShkStd}_{t}^{2})) & \text{if } t\leq t_{\text{retire}}\\ 

448 \frac{1-\textbf{UnempPrbRet }\textbf{IncUnempRet}}{1-\textbf{UnempPrbRet}} & \text{if } t > t_{\text{retire}} \\ 

449 \end{cases}\\ 

450 &\text{otherwise}\\ 

451 \theta_t &\sim\begin{cases} 

452 \textbf{IncUnemp} & \text{if } t\leq t_{\text{retire}}\\ 

453 \textbf{IncUnempRet} & \text{if } t\leq t_{\text{retire}}\\ 

454 \end{cases}\\ 

455 \mathbb{E}[\psi]&=\mathbb{E}[\theta] = 1.\\ 

456 \end{align*} 

457 

458 All time in this function runs forward, from t=0 to t=T 

459 

460 Parameters 

461 ---------- 

462 PermShkStd : [float] 

463 List of standard deviations in log permanent income uncertainty during 

464 the agent's life. 

465 PermShkCount : int 

466 The number of approximation points to be used in the discrete approximation 

467 to the permanent income shock distribution. 

468 TranShkStd : [float] 

469 List of standard deviations in log transitory income uncertainty during 

470 the agent's life. 

471 TranShkCount : int 

472 The number of approximation points to be used in the discrete approximation 

473 to the permanent income shock distribution. 

474 UnempPrb : float or [float] 

475 The probability of becoming unemployed during the working period. 

476 UnempPrbRet : float or None 

477 The probability of not receiving typical retirement income when retired. 

478 T_retire : int 

479 The index value for the final working period in the agent's life. 

480 If T_retire <= 0 then there is no retirement. 

481 IncUnemp : float or [float] 

482 Transitory income received when unemployed. 

483 IncUnempRet : float or None 

484 Transitory income received while "unemployed" when retired. 

485 T_cycle : int 

486 Total number of non-terminal periods in the consumer's sequence of periods. 

487 RNG : np.random.RandomState 

488 Random number generator for this type. 

489 neutral_measure : bool 

490 Indicator for whether the permanent-income-neutral measure should be used. 

491 

492 Returns 

493 ------- 

494 IncShkDstn : [distribution.Distribution] 

495 A list with T_cycle elements, each of which is a 

496 discrete approximation to the income process in a period. 

497 """ 

498 if T_retire > 0: 

499 normal_length = T_retire 

500 retire_length = T_cycle - T_retire 

501 else: 

502 normal_length = T_cycle 

503 retire_length = 0 

504 

505 if all( 

506 [ 

507 isinstance(x, (float, int)) or (x is None) 

508 for x in [UnempPrb, IncUnemp, UnempPrbRet, IncUnempRet] 

509 ] 

510 ): 

511 UnempPrb_list = [UnempPrb] * normal_length + [UnempPrbRet] * retire_length 

512 IncUnemp_list = [IncUnemp] * normal_length + [IncUnempRet] * retire_length 

513 

514 elif all([isinstance(x, list) for x in [UnempPrb, IncUnemp]]): 

515 UnempPrb_list = UnempPrb 

516 IncUnemp_list = IncUnemp 

517 

518 else: 

519 raise Exception( 

520 "Unemployment must be specified either using floats for UnempPrb," 

521 + "IncUnemp, UnempPrbRet, and IncUnempRet, in which case the " 

522 + "unemployment probability and income change only with retirement, or " 

523 + "using lists of length T_cycle for UnempPrb and IncUnemp, specifying " 

524 + "each feature at every age." 

525 ) 

526 

527 PermShkCount_list = [PermShkCount] * normal_length + [1] * retire_length 

528 TranShkCount_list = [TranShkCount] * normal_length + [1] * retire_length 

529 neutral_measure_list = [neutral_measure] * len(PermShkCount_list) 

530 

531 IncShkDstn = IndexDistribution( 

532 engine=BufferStockIncShkDstn, 

533 conditional={ 

534 "sigma_Perm": PermShkStd, 

535 "sigma_Tran": TranShkStd, 

536 "n_approx_Perm": PermShkCount_list, 

537 "n_approx_Tran": TranShkCount_list, 

538 "neutral_measure": neutral_measure_list, 

539 "UnempPrb": UnempPrb_list, 

540 "IncUnemp": IncUnemp_list, 

541 }, 

542 RNG=RNG, 

543 seed=RNG.integers(0, 2**31 - 1), 

544 ) 

545 return IncShkDstn 

546 

547 

548def construct_markov_lognormal_income_process_unemployment( 

549 T_cycle, 

550 PermShkStd, 

551 PermShkCount, 

552 TranShkStd, 

553 TranShkCount, 

554 T_retire, 

555 UnempPrb, 

556 IncUnemp, 

557 UnempPrbRet, 

558 IncUnempRet, 

559 RNG, 

560 neutral_measure=False, 

561): 

562 """ 

563 Generates a nested list of discrete approximations to the income process for each 

564 life period, from end of life to beginning of life, for each discrete Markov 

565 state in the problem. This function calls construct_lognormal_income_process_unemployment 

566 for each Markov state, then rearranges the output. Permanent shocks are mean 

567 one lognormally distributed with standard deviation PermShkStd[t] during the 

568 working life, and degenerate at 1 in the retirement period. Transitory shocks 

569 are mean one lognormally distributed with a point mass at IncUnemp with 

570 probability UnempPrb while working; they are mean one with a point mass at 

571 IncUnempRet with probability UnempPrbRet. Retirement occurs after t=T_retire 

572 periods of working. The problem is specified as having T=T_cycle periods and 

573 K discrete Markov states. 

574 

575 Parameters 

576 ---------- 

577 PermShkStd : np.array 

578 2D array of shape (T,K) of standard deviations of log permanent income 

579 uncertainty during the agent's life. 

580 PermShkCount : int 

581 The number of approximation points to be used in the discrete approxima- 

582 tion to the permanent income shock distribution. 

583 TranShkStd : np.array 

584 2D array of shape (T,K) standard deviations in log transitory income 

585 uncertainty during the agent's life. 

586 TranShkCount : int 

587 The number of approximation points to be used in the discrete approxima- 

588 tion to the permanent income shock distribution. 

589 UnempPrb : np.array 

590 1D or 2D array of the probability of becoming unemployed during the working 

591 portion of the agent's life. If 1D, it should be size K, providing one 

592 unemployment probability per discrete Markov state. If 2D, it should be 

593 shape (T,K). 

594 UnempPrbRet : np.array or None 

595 The probability of not receiving typical retirement income when retired. 

596 If not None, should be size K. Can be set to None when T_retire is 0. 

597 T_retire : int 

598 The index value for the final working period in the agent's life. 

599 If T_retire <= 0 then there is no retirement. 

600 IncUnemp : np.array 

601 1D or 2D array of transitory income received when unemployed during the 

602 working portion of the agent's life. If 1D, it should be size K, providing 

603 one unemployment probability per discrete Markov state. If 2D, it should be 

604 shape (T,K). 

605 IncUnempRet : np.array or None 

606 Transitory income received while "unemployed" when retired. If provided, 

607 should be size K. Can be None when T_retire is 0. 

608 T_cycle : int 

609 Total number of non-terminal periods in the consumer's sequence of periods. 

610 RNG : np.random.RandomState 

611 Random number generator for this type. 

612 neutral_measure : bool 

613 Indicator for whether the permanent-income-neutral measure should be used. 

614 

615 Returns 

616 ------- 

617 IncShkDstn : [[distribution.Distribution]] 

618 A list with T_cycle elements, each of which is a discrete approximation 

619 to the income process in a period. 

620 """ 

621 if T_retire > 0: 

622 normal_length = T_retire 

623 retire_length = T_cycle - T_retire 

624 else: 

625 normal_length = T_cycle 

626 retire_length = 0 

627 

628 # Check dimensions of inputs 

629 try: 

630 PermShkStd_K = PermShkStd.shape[1] 

631 TranShkStd_K = TranShkStd.shape[1] 

632 if UnempPrb.ndim == 2: 

633 UnempPrb_K = UnempPrb.shape[1] 

634 else: 

635 UnempPrb_K = UnempPrb.shape[0] 

636 if IncUnemp.ndim == 2: 

637 IncUnemp_K = IncUnemp.shape[1] 

638 else: 

639 IncUnemp_K = IncUnemp.shape[0] 

640 K = PermShkStd_K 

641 assert K == TranShkStd_K 

642 assert K == UnempPrb_K 

643 assert K == IncUnemp_K 

644 except: 

645 raise Exception( 

646 "The last dimension of PermShkStd, TranShkStd, IncUnemp," 

647 + " and UnempPrb must all be K, the number of discrete states." 

648 ) 

649 try: 

650 if T_retire > 0: 

651 assert K == UnempPrbRet.size 

652 assert K == IncUnempRet.size 

653 except: 

654 raise Exception( 

655 "When T_retire is not zero, UnempPrbRet and IncUnempRet" 

656 + " must be specified as arrays of size K, the number of " 

657 + "discrete Markov states." 

658 ) 

659 try: 

660 D = UnempPrb.ndim 

661 assert D == IncUnemp.ndim 

662 if T_retire > 0: 

663 assert D == UnempPrbRet.ndim 

664 assert D == IncUnempRet.ndim 

665 except: 

666 raise Exception( 

667 "If any of UnempPrb, IncUnemp, or UnempPrbRet, or IncUnempRet " 

668 + "are 2D arrays, then they must *all* be 2D arrays." 

669 ) 

670 try: 

671 assert D == 1 or D == 2 

672 except: 

673 raise Exception( 

674 "UnempPrb, IncUnemp, or UnempPrbRet, or IncUnempRet must " 

675 + "all be 1D or 2D arrays." 

676 ) 

677 

678 # Prepare lists that don't vary by Markov state 

679 PermShkCount_list = [PermShkCount] * normal_length + [1] * retire_length 

680 TranShkCount_list = [TranShkCount] * normal_length + [1] * retire_length 

681 neutral_measure_list = [neutral_measure] * len(PermShkCount_list) 

682 

683 # Loop through the Markov states, constructing the lifecycle income process for each one 

684 IncShkDstn_by_Mrkv = [] 

685 for k in range(K): 

686 if D == 1: # Unemployment parameters don't vary by age other than retirement 

687 if T_retire > 0: 

688 UnempPrb_list = [UnempPrb[k]] * normal_length + [ 

689 UnempPrbRet[k] 

690 ] * retire_length 

691 IncUnemp_list = [IncUnemp[k]] * normal_length + [ 

692 IncUnempRet[k] 

693 ] * retire_length 

694 else: 

695 UnempPrb_list = [UnempPrb[k]] * normal_length 

696 IncUnemp_list = [IncUnemp[k]] * normal_length 

697 else: # Unemployment parameters vary by age 

698 UnempPrb_list = UnempPrb[:, k].tolist() 

699 IncUnemp_list = IncUnemp[:, k].tolist() 

700 

701 PermShkStd_k = PermShkStd[:, k].tolist() 

702 TranShkStd_k = TranShkStd[:, k].tolist() 

703 

704 IncShkDstn_k = IndexDistribution( 

705 engine=BufferStockIncShkDstn, 

706 conditional={ 

707 "sigma_Perm": PermShkStd_k, 

708 "sigma_Tran": TranShkStd_k, 

709 "n_approx_Perm": PermShkCount_list, 

710 "n_approx_Tran": TranShkCount_list, 

711 "neutral_measure": neutral_measure_list, 

712 "UnempPrb": UnempPrb_list, 

713 "IncUnemp": IncUnemp_list, 

714 }, 

715 RNG=RNG, 

716 seed=RNG.integers(0, 2**31 - 1), 

717 ) 

718 IncShkDstn_by_Mrkv.append(IncShkDstn_k) 

719 

720 # Rearrange the list that was just constructed, so that element [t][k] represents 

721 # the income shock distribution in period t, Markov state k. 

722 IncShkDstn = [] 

723 for t in range(T_cycle): 

724 IncShkDstn_t = [IncShkDstn_by_Mrkv[k][t] for k in range(K)] 

725 IncShkDstn.append(IncShkDstn_t) 

726 

727 return IncShkDstn 

728 

729 

730def construct_HANK_lognormal_income_process_unemployment( 

731 T_cycle, 

732 PermShkStd, 

733 PermShkCount, 

734 TranShkStd, 

735 TranShkCount, 

736 T_retire, 

737 UnempPrb, 

738 IncUnemp, 

739 UnempPrbRet, 

740 IncUnempRet, 

741 tax_rate, 

742 labor, 

743 wage, 

744 RNG, 

745 neutral_measure=False, 

746): 

747 """ 

748 Generates a list of discrete approximations to the income process for each 

749 life period, from end of life to beginning of life. Permanent shocks are mean 

750 one lognormally distributed with standard deviation PermShkStd[t] during the 

751 working life, and degenerate at 1 in the retirement period. Transitory shocks 

752 are mean one lognormally distributed with a point mass at IncUnemp with 

753 probability UnempPrb while working; they are mean one with a point mass at 

754 IncUnempRet with probability UnempPrbRet. Retirement occurs after t=T_retire 

755 periods of working. 

756 

757 This version of the function incorporates additional flexibility with respect 

758 to transitory income (continuous labor supply, wage rate, tax rate) and thus 

759 is useful in HANK models (hence the name!). 

760 

761 Note 1: All time in this function runs forward, from t=0 to t=T 

762 

763 Note 2: All parameters are passed as attributes of the input parameters. 

764 

765 Parameters (passed as attributes of the input parameters) 

766 --------------------------------------------------------- 

767 PermShkStd : [float] 

768 List of standard deviations in log permanent income uncertainty during 

769 the agent's life. 

770 PermShkCount : int 

771 The number of approximation points to be used in the discrete approxima- 

772 tion to the permanent income shock distribution. 

773 TranShkStd : [float] 

774 List of standard deviations in log transitory income uncertainty during 

775 the agent's life. 

776 TranShkCount : int 

777 The number of approximation points to be used in the discrete approxima- 

778 tion to the permanent income shock distribution. 

779 UnempPrb : float or [float] 

780 The probability of becoming unemployed during the working period. 

781 UnempPrbRet : float or None 

782 The probability of not receiving typical retirement income when retired. 

783 T_retire : int 

784 The index value for the final working period in the agent's life. 

785 If T_retire <= 0 then there is no retirement. 

786 IncUnemp : float or [float] 

787 Transitory income received when unemployed. 

788 IncUnempRet : float or None 

789 Transitory income received while "unemployed" when retired. 

790 tax_rate : [float] 

791 List of flat tax rates on labor income, age-varying. 

792 labor : [float] 

793 List of intensive margin labor supply, age-varying. 

794 wage : [float] 

795 List of wage rate scaling factors, age-varying. 

796 T_cycle : int 

797 Total number of non-terminal periods in the consumer's sequence of periods. 

798 

799 Returns 

800 ------- 

801 IncShkDstn : [distribution.Distribution] 

802 A list with T_cycle elements, each of which is a 

803 discrete approximation to the income process in a period. 

804 PermShkDstn : [[distribution.Distributiony]] 

805 A list with T_cycle elements, each of which 

806 a discrete approximation to the permanent income shocks. 

807 TranShkDstn : [[distribution.Distribution]] 

808 A list with T_cycle elements, each of which 

809 a discrete approximation to the transitory income shocks. 

810 """ 

811 if T_retire > 0: 

812 normal_length = T_retire 

813 retire_length = T_cycle - T_retire 

814 else: 

815 normal_length = T_cycle 

816 retire_length = 0 

817 

818 if all( 

819 [ 

820 isinstance(x, (float, int)) or (x is None) 

821 for x in [UnempPrb, IncUnemp, UnempPrbRet, IncUnempRet] 

822 ] 

823 ): 

824 UnempPrb_list = [UnempPrb] * normal_length + [UnempPrbRet] * retire_length 

825 IncUnemp_list = [IncUnemp] * normal_length + [IncUnempRet] * retire_length 

826 

827 elif all([isinstance(x, list) for x in [UnempPrb, IncUnemp]]): 

828 UnempPrb_list = UnempPrb 

829 IncUnemp_list = IncUnemp 

830 

831 else: 

832 raise Exception( 

833 "Unemployment must be specified either using floats for UnempPrb," 

834 + "IncUnemp, UnempPrbRet, and IncUnempRet, in which case the " 

835 + "unemployment probability and income change only with retirement, or " 

836 + "using lists of length T_cycle for UnempPrb and IncUnemp, specifying " 

837 + "each feature at every age." 

838 ) 

839 

840 PermShkCount_list = [PermShkCount] * normal_length + [1] * retire_length 

841 TranShkCount_list = [TranShkCount] * normal_length + [1] * retire_length 

842 neutral_measure_list = [neutral_measure] * len(PermShkCount_list) 

843 

844 IncShkDstn = IndexDistribution( 

845 engine=IncShkDstn_HANK, 

846 conditional={ 

847 "sigma_Perm": PermShkStd, 

848 "sigma_Tran": TranShkStd, 

849 "n_approx_Perm": PermShkCount_list, 

850 "n_approx_Tran": TranShkCount_list, 

851 "neutral_measure": neutral_measure_list, 

852 "UnempPrb": UnempPrb_list, 

853 "IncUnemp": IncUnemp_list, 

854 "wage": wage, 

855 "tax_rate": tax_rate, 

856 "labor": labor, 

857 }, 

858 RNG=RNG, 

859 ) 

860 

861 return IncShkDstn 

862 

863 

864def construct_lognormal_income_process_with_mvg_medical_expenses( 

865 T_cycle, 

866 PermShkStd, 

867 PermShkCount, 

868 TranShkStd, 

869 TranShkCount, 

870 T_retire, 

871 UnempPrb, 

872 IncUnemp, 

873 UnempPrbRet, 

874 IncUnempRet, 

875 age_min, 

876 RNG, 

877 CollegeBool=True, 

878): 

879 """ 

880 Extension of the standard permanent-transitory income process that adds medical 

881 expense shocks, modeled as *negative* transitory income shocks. That is, 

882 TranShk = 1.0 - ExpShk. The expense shock calibration is "hardwired" based on 

883 table 12 of Mateo Velasquez-Giraldo's "Life-Cycle Portfolio Choices and Heterogeneous 

884 Stock Market Expectations" (https://www.federalreserve.gov/econres/feds/files/2024097pap.pdf). 

885 

886 Expense shocks are only active after age 50, and are specified as seven equi- 

887 probable point masses for each five-year age range. Because the expense shocks 

888 are simply "negative transitory income shocks", they are homothetic with respect 

889 to permanent income. This is not a particularly justified assumption, as income 

890 elasticity of demand for medical care is around 0.15, but it's workable for a 

891 representation of medical expenses that can be used with HARK's workhorse models. 

892 This function assumes an annual frequency. If net income would ever be negative, 

893 it is truncated to zero instead. 

894 

895 For microeconomic models that incorporate medical expenses that are not homothetic 

896 in income, see ConsMedModel. 

897 

898 Relative to the standard construct_lognormal_income_process_unemployment, this 

899 function takes two additional parameters: age_min and CollegeBool (default True). 

900 This function assumes that the calibration does not include ages above 120 years. 

901 

902 Parameters 

903 ---------- 

904 T_cycle : int 

905 Total number of non-terminal periods in the consumer's sequence of periods. 

906 PermShkStd : [float] 

907 List of standard deviations in log permanent income uncertainty during 

908 the agent's life. 

909 PermShkCount : int 

910 The number of approximation points to be used in the discrete approximation 

911 to the permanent income shock distribution. 

912 TranShkStd : [float] 

913 List of standard deviations in log transitory income uncertainty during 

914 the agent's life. 

915 TranShkCount : int 

916 The number of approximation points to be used in the discrete approximation 

917 to the permanent income shock distribution. 

918 UnempPrb : float or [float] 

919 The probability of becoming unemployed during the working period. 

920 UnempPrbRet : float or None 

921 The probability of not receiving typical retirement income when retired. 

922 T_retire : int 

923 The index value for the final working period in the agent's life. 

924 If T_retire <= 0 then there is no retirement. 

925 IncUnemp : float or [float] 

926 Transitory income received when unemployed. 

927 IncUnempRet : float or None 

928 Transitory income received while "unemployed" when retired. 

929 age_min : int 

930 Age (in years) of agents at model birth. 

931 RNG : np.random.RandomState 

932 Random number generator for this type. 

933 CollegeBool : bool 

934 Indicator for whether to use the college (True, default) or high school 

935 (False) calibration. 

936 

937 Returns 

938 ------- 

939 IncShkDstn : [distribution.Distribution] 

940 A list with T_cycle elements, each of which is a 

941 discrete approximation to the income process in a period. 

942 """ 

943 # First, make the standard income process 

944 IncShkDstnBase = construct_lognormal_income_process_unemployment( 

945 T_cycle, 

946 PermShkStd, 

947 PermShkCount, 

948 TranShkStd, 

949 TranShkCount, 

950 T_retire, 

951 UnempPrb, 

952 IncUnemp, 

953 UnempPrbRet, 

954 IncUnempRet, 

955 RNG, 

956 ) 

957 PermShkDstnBase = get_PermShkDstn_from_IncShkDstn(IncShkDstnBase, RNG) 

958 TranShkDstnBase = get_TranShkDstn_from_IncShkDstn(IncShkDstnBase, RNG) 

959 

960 equiprobable_one_seventh = np.ones(7) / 7.0 

961 if CollegeBool: 

962 # Copy Mateo's college-educated expense shock distribution 

963 exp_shks_51_to_55 = np.array([0.000, 0.003, 0.007, 0.012, 0.021, 0.039, 0.121]) 

964 exp_shks_56_to_60 = np.array([0.001, 0.005, 0.010, 0.016, 0.027, 0.049, 0.163]) 

965 exp_shks_61_to_65 = np.array([0.002, 0.007, 0.014, 0.023, 0.040, 0.078, 0.227]) 

966 exp_shks_66_to_70 = np.array([0.003, 0.010, 0.019, 0.031, 0.050, 0.089, 0.227]) 

967 exp_shks_71_to_75 = np.array([0.004, 0.013, 0.024, 0.039, 0.060, 0.103, 0.262]) 

968 exp_shks_76_to_80 = np.array([0.004, 0.015, 0.028, 0.047, 0.074, 0.123, 0.294]) 

969 exp_shks_81_to_85 = np.array([0.004, 0.017, 0.033, 0.054, 0.089, 0.155, 0.410]) 

970 exp_shks_86_to_90 = np.array([0.002, 0.015, 0.033, 0.057, 0.100, 0.191, 0.719]) 

971 exp_shks_91_plus = np.array([0.000, 0.015, 0.039, 0.075, 0.160, 0.389, 1.485]) 

972 exp_shks_all = ( 

973 50 * [None] 

974 + 5 * [exp_shks_51_to_55] 

975 + 5 * [exp_shks_56_to_60] 

976 + 5 * [exp_shks_61_to_65] 

977 + 5 * [exp_shks_66_to_70] 

978 + 5 * [exp_shks_71_to_75] 

979 + 5 * [exp_shks_76_to_80] 

980 + 5 * [exp_shks_81_to_85] 

981 + 5 * [exp_shks_86_to_90] 

982 + 31 * [exp_shks_91_plus] 

983 ) 

984 

985 else: 

986 # Copy Mateo's high school-educated expense shock distribution 

987 exp_shks_51_to_55 = np.array([0.000, 0.004, 0.010, 0.019, 0.033, 0.064, 0.205]) 

988 exp_shks_56_to_60 = np.array([0.000, 0.005, 0.013, 0.023, 0.040, 0.077, 0.245]) 

989 exp_shks_61_to_65 = np.array([0.000, 0.008, 0.018, 0.032, 0.055, 0.104, 0.290]) 

990 exp_shks_66_to_70 = np.array([0.001, 0.011, 0.023, 0.038, 0.064, 0.111, 0.264]) 

991 exp_shks_71_to_75 = np.array([0.002, 0.014, 0.028, 0.046, 0.074, 0.126, 0.293]) 

992 exp_shks_76_to_80 = np.array([0.001, 0.015, 0.031, 0.053, 0.084, 0.143, 0.346]) 

993 exp_shks_81_to_85 = np.array([0.001, 0.016, 0.033, 0.059, 0.096, 0.168, 0.433]) 

994 exp_shks_86_to_90 = np.array([0.000, 0.016, 0.036, 0.066, 0.110, 0.229, 0.849]) 

995 exp_shks_91_plus = np.array([0.000, 0.011, 0.034, 0.069, 0.131, 0.301, 1.479]) 

996 exp_shks_all = ( 

997 50 * [None] 

998 + 5 * [exp_shks_51_to_55] 

999 + 5 * [exp_shks_56_to_60] 

1000 + 5 * [exp_shks_61_to_65] 

1001 + 5 * [exp_shks_66_to_70] 

1002 + 5 * [exp_shks_71_to_75] 

1003 + 5 * [exp_shks_76_to_80] 

1004 + 5 * [exp_shks_81_to_85] 

1005 + 5 * [exp_shks_86_to_90] 

1006 + 31 * [exp_shks_91_plus] 

1007 ) 

1008 

1009 # Incorporate the expense shock distribution into the transitory income shock distribution 

1010 IncShkDstn = [] 

1011 for t in range(T_cycle): 

1012 age = age_min + t 

1013 if age >= len(exp_shks_all): 

1014 raise ValueError( 

1015 f"Age {age} is outside the supported calibration range for expense shocks " 

1016 f"(maximum supported index is {len(exp_shks_all) - 1})." 

1017 ) 

1018 exp_shks_t = exp_shks_all[age] 

1019 

1020 # If age <= 50, just use the baseline income shock distribution 

1021 if exp_shks_t is None: 

1022 IncShkDstn.append(IncShkDstnBase[t]) 

1023 continue 

1024 

1025 # Otherwise, make a distribution of net transitory income as the difference 

1026 # between the transitory income shock and the medical expense shock 

1027 seed_t = RNG.integers(0, 2**31 - 1) 

1028 PermShkDstn_t = PermShkDstnBase[t] 

1029 ExpShkDstn_t = DiscreteDistribution( 

1030 pmv=equiprobable_one_seventh, 

1031 atoms=exp_shks_t, 

1032 ) 

1033 

1034 # Prepare these for multiplication with broadcasting 

1035 TranAtomsBase = TranShkDstnBase[t].atoms[0][:, np.newaxis] 

1036 TranProbsBase = TranShkDstnBase[t].pmv[:, np.newaxis] 

1037 ExpAtoms = ExpShkDstn_t.atoms[0][np.newaxis, :] 

1038 ExpProbs = ExpShkDstn_t.pmv[np.newaxis, :] 

1039 

1040 # Calculate net income and joint probability 

1041 TranAtoms = np.maximum(TranAtomsBase - ExpAtoms, 0.0).flatten() 

1042 TranProbs = (TranProbsBase * ExpProbs).flatten() 

1043 

1044 # Make this period's joint income distribution 

1045 TranShkDstn_t = DiscreteDistribution(pmv=TranProbs, atoms=TranAtoms) 

1046 IncShkDstn_t = DiscreteDistributionLabeled.from_unlabeled( 

1047 combine_indep_dstns(PermShkDstn_t, TranShkDstn_t, seed=seed_t), 

1048 name="Income shocks with medical expenses", 

1049 var_names=["PermShk", "TranShk"], 

1050 ) 

1051 IncShkDstn.append(IncShkDstn_t) 

1052 

1053 return IncShkDstn 

1054 

1055 

1056############################################################################### 

1057 

1058 

1059def get_PermShkDstn_from_IncShkDstn(IncShkDstn, RNG): 

1060 PermShkDstn = [ 

1061 this.make_univariate(0, seed=RNG.integers(0, 2**31 - 1)) for this in IncShkDstn 

1062 ] 

1063 return IndexDistribution(distributions=PermShkDstn, seed=RNG.integers(0, 2**31 - 1)) 

1064 

1065 

1066def get_TranShkDstn_from_IncShkDstn(IncShkDstn, RNG): 

1067 TranShkDstn = [ 

1068 this.make_univariate(1, seed=RNG.integers(0, 2**31 - 1)) for this in IncShkDstn 

1069 ] 

1070 return IndexDistribution(distributions=TranShkDstn, seed=RNG.integers(0, 2**31 - 1)) 

1071 

1072 

1073def get_PermShkDstn_from_IncShkDstn_markov(IncShkDstn, RNG): 

1074 PermShkDstn = [ 

1075 [ 

1076 this.make_univariate(0, seed=RNG.integers(0, 2**31 - 1)) 

1077 for this in IncShkDstn_t 

1078 ] 

1079 for IncShkDstn_t in IncShkDstn 

1080 ] 

1081 return PermShkDstn 

1082 

1083 

1084def get_TranShkDstn_from_IncShkDstn_markov(IncShkDstn, RNG): 

1085 TranShkDstn = [ 

1086 [ 

1087 this.make_univariate(1, seed=RNG.integers(0, 2**31 - 1)) 

1088 for this in IncShkDstn_t 

1089 ] 

1090 for IncShkDstn_t in IncShkDstn 

1091 ] 

1092 return TranShkDstn 

1093 

1094 

1095def get_TranShkGrid_from_TranShkDstn(T_cycle, TranShkDstn): 

1096 TranShkGrid = [TranShkDstn[t].atoms.flatten() for t in range(T_cycle)] 

1097 return TranShkGrid 

1098 

1099 

1100def make_polynomial_PermGroFac(T_cycle, PermGroFac_coeffs, age_0=0.0, age_step=1.0): 

1101 """ 

1102 Construct the profile of permanent growth factors by age using polynomial coefficients. 

1103 

1104 Parameters 

1105 ---------- 

1106 T_cycle : int 

1107 Number of non-terminal period's in this agent's cycle. 

1108 PermGroFac_coeffs : [float] 

1109 Arbitrary length list or 1D vector of polynomial coefficients of age on 

1110 permanent income growth factor. 

1111 age_0 : float, optional 

1112 Initial age of agents (when t_age=0), with respect to the polynomial coefficients. 

1113 The default is 0. 

1114 age_step : float, optional 

1115 Age increment in the model, with respect to the polynomial coefficients. 

1116 The default is 1. 

1117 

1118 Returns 

1119 ------- 

1120 PermGroFac : [float] 

1121 List of permanent income growth factors, one per period. 

1122 """ 

1123 PermGroFac = make_polynomial_params( 

1124 PermGroFac_coeffs, T_cycle, offset=0.0, step=1.0 

1125 ) 

1126 return PermGroFac.tolist() 

1127 

1128 

1129def make_polynomial_PermShkStd(T_cycle, PermShkStd_coeffs, age_0=0.0, age_step=1.0): 

1130 """ 

1131 Construct the profile of (log) permanent income shock standard deviations by 

1132 age using polynomial coefficients. 

1133 

1134 Parameters 

1135 ---------- 

1136 T_cycle : int 

1137 Number of non-terminal period's in this agent's cycle. 

1138 PermGroFac_coeffs : [float] 

1139 Arbitrary length list or 1D vector of polynomial coefficients of age on 

1140 (log) permanent income shock standard deviation. 

1141 age_0 : float, optional 

1142 Initial age of agents (when t_age=0), with respect to the polynomial coefficients. 

1143 The default is 0. 

1144 age_step : float, optional 

1145 Age increment in the model, with respect to the polynomial coefficients. 

1146 The default is 1. 

1147 

1148 Returns 

1149 ------- 

1150 PermShkStd : [float] 

1151 List of (log) permanent income shock standard deviations, one per period. 

1152 """ 

1153 PermShkStd = make_polynomial_params( 

1154 PermShkStd_coeffs, T_cycle, offset=0.0, step=1.0 

1155 ) 

1156 return PermShkStd.tolist() 

1157 

1158 

1159def make_polynomial_TranShkStd(T_cycle, TranShkStd_coeffs, age_0=0.0, age_step=1.0): 

1160 """ 

1161 Construct the profile of (log) transitory income shock standard deviations by 

1162 age using polynomial coefficients. 

1163 

1164 Parameters 

1165 ---------- 

1166 T_cycle : int 

1167 Number of non-terminal period's in this agent's cycle. 

1168 PermGroFac_coeffs : [float] 

1169 Arbitrary length list or 1D vector of polynomial coefficients of age on 

1170 (log) transitory income shock standard deviation. 

1171 age_0 : float, optional 

1172 Initial age of agents (when t_age=0), with respect to the polynomial coefficients. 

1173 The default is 0. 

1174 age_step : float, optional 

1175 Age increment in the model, with respect to the polynomial coefficients. 

1176 The default is 1. 

1177 

1178 Returns 

1179 ------- 

1180 TranShkStd : [float] 

1181 List of (log) permanent income shock standard deviations, one per period. 

1182 """ 

1183 TranShkStd = make_polynomial_params( 

1184 TranShkStd_coeffs, T_cycle, offset=0.0, step=1.0 

1185 ) 

1186 return TranShkStd.tolist() 

1187 

1188 

1189class pLvlFuncAR1(MetricObject): 

1190 """ 

1191 A class for representing AR1-style persistent income growth functions. 

1192 

1193 Parameters 

1194 ---------- 

1195 pLogMean : float 

1196 Log persistent income level toward which we are drawn. 

1197 PermGroFac : float 

1198 Autonomous (e.g. life cycle) pLvl growth (does not AR1 decay). 

1199 Corr : float 

1200 Correlation coefficient on log income. 

1201 """ 

1202 

1203 def __init__(self, pLogMean, PermGroFac, Corr): 

1204 self.pLogMean = pLogMean 

1205 self.LogGroFac = np.log(PermGroFac) 

1206 self.Corr = Corr 

1207 

1208 def __call__(self, pLvlNow): 

1209 """ 

1210 Returns expected persistent income level next period as a function of 

1211 this period's persistent income level. 

1212 

1213 Parameters 

1214 ---------- 

1215 pLvlNow : np.array 

1216 Array of current persistent income levels. 

1217 

1218 Returns 

1219 ------- 

1220 pLvlNext : np.array 

1221 Identically shaped array of next period persistent income levels. 

1222 """ 

1223 pLvlNext = np.exp( 

1224 self.Corr * np.log(pLvlNow) 

1225 + (1.0 - self.Corr) * self.pLogMean 

1226 + self.LogGroFac 

1227 ) 

1228 return pLvlNext 

1229 

1230 

1231def make_PermGroFac_from_ind_and_agg(PermGroFacInd, PermGroFacAgg): 

1232 """ 

1233 A very simple function that constructs *overall* permanent income growth over 

1234 the lifecycle as the sum of idiosyncratic productivity growth PermGroFacInd and 

1235 aggregate productivity growth PermGroFacAgg. In most HARK models, PermGroFac 

1236 is treated as the overall permanent income growth factor, regardless of source. 

1237 In applications in which the user has estimated permanent income growth from 

1238 *cross sectional* data, or wants to investigate how a change in aggregate 

1239 productivity growth affects behavior or outcomes, this function can be used 

1240 as the constructor for PermGroFac. 

1241 

1242 To use this function, specify idiosyncratic productivity growth in the attribute 

1243 PermGroFacInd (or construct it), and put this function as the entry for PermGroFac 

1244 in the constructors dictionary of your AgentType subclass instances. 

1245 

1246 Parameters 

1247 ---------- 

1248 PermGroFacInd : [float] or np.array 

1249 Lifecycle sequence of idiosyncratic permanent productivity growth factors. 

1250 These represent individual-based productivity factors, like experience. 

1251 PermGroFacAgg : float 

1252 Constant aggregate permanent growth factor, representing (e.g.) TFP. 

1253 

1254 Returns 

1255 ------- 

1256 PermGroFac : [float] or np.array 

1257 Lifecycle sequence of overall permanent productivity growth factors. 

1258 Returns same type as PermGroFacInd. 

1259 """ 

1260 PermGroFac = [PermGroFacAgg * G for G in PermGroFacInd] 

1261 if type(PermGroFacInd) is np.array: 

1262 PermGroFac = np.array(PermGroFac) 

1263 return PermGroFac 

1264 

1265 

1266############################################################################### 

1267 

1268# Define income processes that can be used in the ConsGenIncProcess model 

1269 

1270 

1271def make_trivial_pLvlNextFunc(T_cycle): 

1272 r""" 

1273 A dummy function that creates default trivial permanent income dynamics: 

1274 none at all! Simply returns a list of IdentityFunctions, one for each period. 

1275 

1276 .. math:: 

1277 G_{t}(x) = x 

1278 

1279 Parameters 

1280 ---------- 

1281 T_cycle : int 

1282 Number of non-terminal periods in the agent's problem. 

1283 

1284 Returns 

1285 ------- 

1286 pLvlNextFunc : [IdentityFunction] 

1287 List of trivial permanent income dynamic functions. 

1288 """ 

1289 pLvlNextFunc_basic = IdentityFunction() 

1290 pLvlNextFunc = T_cycle * [pLvlNextFunc_basic] 

1291 return pLvlNextFunc 

1292 

1293 

1294def make_explicit_perminc_pLvlNextFunc(T_cycle, PermGroFac): 

1295 r""" 

1296 A function that creates permanent income dynamics as a sequence of linear 

1297 functions, indicating constant expected permanent income growth across 

1298 permanent income levels. 

1299 

1300 .. math:: 

1301 G_{t+1} (x) = \Gamma_{t+1} x 

1302 

1303 Parameters 

1304 ---------- 

1305 T_cycle : int 

1306 Number of non-terminal periods in the agent's problem. 

1307 PermGroFac : [float] 

1308 List of permanent income growth factors over the agent's problem. 

1309 

1310 Returns 

1311 ------- 

1312 pLvlNextFunc : [LinearInterp] 

1313 List of linear functions representing constant permanent income growth 

1314 rate, regardless of current permanent income level. 

1315 """ 

1316 pLvlNextFunc = [] 

1317 for t in range(T_cycle): 

1318 pLvlNextFunc.append( 

1319 LinearInterp(np.array([0.0, 1.0]), np.array([0.0, PermGroFac[t]])) 

1320 ) 

1321 return pLvlNextFunc 

1322 

1323 

1324def make_AR1_style_pLvlNextFunc(T_cycle, pLogInitMean, PermGroFac, PrstIncCorr): 

1325 r""" 

1326 A function that creates permanent income dynamics as a sequence of AR1-style 

1327 functions. If cycles=0, the product of PermGroFac across all periods must be 

1328 1.0, otherwise this method is invalid. 

1329 

1330 .. math:: 

1331 \begin{align} 

1332 log(G_{t+1} (x)) &=\varphi log(x) + (1-\varphi) log(\overline{P}_{t})+log(\Gamma_{t+1}) + log(\psi_{t+1}), \\ 

1333 \overline{P}_{t+1} &= \overline{P}_{t} \Gamma_{t+1} \\ 

1334 \end{align} 

1335 

1336 Parameters 

1337 ---------- 

1338 T_cycle : int 

1339 Number of non-terminal periods in the agent's problem. 

1340 pLogInitMean : float 

1341 Mean of log permanent income at initialization. 

1342 PermGroFac : [float] 

1343 List of permanent income growth factors over the agent's problem. 

1344 PrstIncCorr : float 

1345 Correlation coefficient on log permanent income today on log permanent 

1346 income in the succeeding period. 

1347 

1348 Returns 

1349 ------- 

1350 pLvlNextFunc : [pLvlFuncAR1] 

1351 List of AR1-style persistent income dynamics functions 

1352 """ 

1353 pLvlNextFunc = [] 

1354 pLogMean = pLogInitMean # Initial mean (log) persistent income 

1355 for t in range(T_cycle): 

1356 pLvlNextFunc.append(pLvlFuncAR1(pLogMean, PermGroFac[t], PrstIncCorr)) 

1357 pLogMean += np.log(PermGroFac[t]) 

1358 return pLvlNextFunc 

1359 

1360 

1361############################################################################### 

1362 

1363 

1364def make_basic_pLvlPctiles( 

1365 pLvlPctiles_count, 

1366 pLvlPctiles_bound=[0.001, 0.999], 

1367 pLvlPctiles_tail_count=0, 

1368 pLvlPctiles_tail_order=np.e, 

1369): 

1370 """ 

1371 Make a relatively basic specification for pLvlPctiles by choosing the number 

1372 of uniformly spaced nodes in the "body", the percentile boundaries for the 

1373 body, the number of nodes in each tail, and the order/factor by which the 

1374 tail percentiles approach 0 and 1 respectively. 

1375 

1376 Parameters 

1377 ---------- 

1378 pLvlPctile_count : int 

1379 Number of nodes in the "body" of the percentile set. 

1380 pLvlPctile_bound : [float,float], optional 

1381 Percentile bounds for the "body" of the set. The default is [0.0, 1.0]. 

1382 pLvlPctile_tail_count : int, optional 

1383 Number of nodes in each extant tail of the set. The default is 0. 

1384 pLvlPctile_tail_order : float, optional 

1385 Factor by which tail percentiles shrink toward 0 and 1. The default is np.e. 

1386 

1387 Returns 

1388 ------- 

1389 pLvlPctiles : np.array 

1390 Array of percentiles of pLvl, usually used to construct pLvlGrid using 

1391 the function below. 

1392 """ 

1393 bound = pLvlPctiles_bound 

1394 fac = 1.0 / pLvlPctiles_tail_order 

1395 body = np.linspace(bound[0], bound[1], num=pLvlPctiles_count) 

1396 

1397 if bound[0] > 0.0: 

1398 lower = [] 

1399 val = bound[0] 

1400 for i in range(pLvlPctiles_tail_count): 

1401 val *= fac 

1402 lower.append(val) 

1403 lower.reverse() 

1404 lower = np.array(lower) 

1405 else: 

1406 lower = np.array([]) 

1407 

1408 if bound[1] < 1.0: 

1409 upper = [] 

1410 val = 1.0 - bound[1] 

1411 for i in range(pLvlPctiles_tail_count): 

1412 val *= fac 

1413 upper.append(val) 

1414 upper = 1.0 - np.array(upper) 

1415 else: 

1416 upper = np.array([]) 

1417 

1418 pLvlPctiles = np.concatenate((lower, body, upper)) 

1419 return pLvlPctiles 

1420 

1421 

1422def make_pLvlGrid_by_simulation( 

1423 cycles, 

1424 T_cycle, 

1425 PermShkDstn, 

1426 pLvlNextFunc, 

1427 LivPrb, 

1428 pLogInitMean, 

1429 pLogInitStd, 

1430 pLvlPctiles, 

1431 pLvlExtra=None, 

1432): 

1433 """ 

1434 Construct the permanent income grid for each period of the problem by simulation. 

1435 If the model is infinite horizon (cycles=0), an approximation of the long run 

1436 steady state distribution of permanent income is used (by simulating many periods). 

1437 If the model is lifecycle (cycles=1), explicit simulation is used. In either 

1438 case, the input pLvlPctiles is used to choose percentiles from the distribution. 

1439 

1440 If the problem is neither infinite horizon nor life-cycle, this method will fail. 

1441 If the problem is infinite horizon, cumprod(PermGroFac) must equal one. 

1442 

1443 Parameters 

1444 ---------- 

1445 cycles : int 

1446 Number of times the sequence of periods happens for the agent; must be 0 or 1. 

1447 T_cycle : int 

1448 Number of non-terminal periods in the agent's problem. 

1449 PermShkDstn : [distribution] 

1450 List of permanent shock distributions in each period of the problem. 

1451 pLvlNextFunc : [function] 

1452 List of permanent income dynamic functions. 

1453 LivPrb : [float] 

1454 List of survival probabilities by period of the cycle. Only used in infinite 

1455 horizon specifications. 

1456 pLogInitMean : float 

1457 Mean of log permanent income at initialization. 

1458 pLogInitStd : float 

1459 Standard deviaition of log permanent income at initialization. 

1460 pLvlPctiles : [float] 

1461 List or array of percentiles (between 0 and 1) of permanent income to 

1462 use for the pLvlGrid. 

1463 pLvlExtra : None or [float], optional 

1464 Additional pLvl values to automatically include in pLvlGrid. 

1465 

1466 Returns 

1467 ------- 

1468 pLvlGrid : [np.array] 

1469 List of permanent income grids for each period, constructed by simulating 

1470 the permanent income process and extracting specified percentiles. 

1471 """ 

1472 LivPrbAll = np.array(LivPrb) 

1473 Agent_N = 100000 

1474 

1475 # Simulate the distribution of persistent income levels by t_cycle in a lifecycle model 

1476 if cycles == 1: 

1477 pLvlNow = Lognormal(pLogInitMean, sigma=pLogInitStd, seed=31382).draw(Agent_N) 

1478 pLvlGrid = [] # empty list of time-varying persistent income grids 

1479 # Calculate distribution of persistent income in each period of lifecycle 

1480 for t in range(T_cycle): 

1481 if t > 0: 

1482 PermShkNow = PermShkDstn[t - 1].draw(N=Agent_N) 

1483 pLvlNow = pLvlNextFunc[t - 1](pLvlNow) * PermShkNow 

1484 pLvlGrid.append(get_percentiles(pLvlNow, percentiles=pLvlPctiles)) 

1485 

1486 # Calculate "stationary" distribution in infinite horizon (might vary across periods of cycle) 

1487 elif cycles == 0: 

1488 T_long = ( 

1489 1000 # Number of periods to simulate to get to "stationary" distribution 

1490 ) 

1491 pLvlNow = Lognormal(mu=pLogInitMean, sigma=pLogInitStd, seed=31382).draw( 

1492 Agent_N 

1493 ) 

1494 t_cycle = np.zeros(Agent_N, dtype=int) 

1495 for t in range(T_long): 

1496 # Determine who dies and replace them with newborns 

1497 LivPrb = LivPrbAll[t_cycle] 

1498 draws = Uniform(seed=t).draw(Agent_N) 

1499 who_dies = draws > LivPrb 

1500 pLvlNow[who_dies] = Lognormal( 

1501 pLogInitMean, pLogInitStd, seed=t + 92615 

1502 ).draw(np.sum(who_dies)) 

1503 t_cycle[who_dies] = 0 

1504 

1505 for j in range(T_cycle): # Update persistent income 

1506 these = t_cycle == j 

1507 PermShkTemp = PermShkDstn[j].draw(N=np.sum(these)) 

1508 pLvlNow[these] = pLvlNextFunc[j](pLvlNow[these]) * PermShkTemp 

1509 t_cycle = t_cycle + 1 

1510 t_cycle[t_cycle == T_cycle] = 0 

1511 

1512 # We now have a "long run stationary distribution", extract percentiles 

1513 pLvlGrid = [] # empty list of time-varying persistent income grids 

1514 for t in range(T_cycle): 

1515 these = t_cycle == t 

1516 pLvlGrid.append(get_percentiles(pLvlNow[these], percentiles=pLvlPctiles)) 

1517 

1518 # Throw an error if cycles>1 

1519 else: 

1520 assert False, "Can only handle cycles=0 or cycles=1!" 

1521 

1522 # Insert any additional requested points into the pLvlGrid 

1523 if pLvlExtra is not None: 

1524 pLvlExtra_alt = np.array(pLvlExtra) 

1525 for t in range(T_cycle): 

1526 pLvlGrid_t = pLvlGrid[t] 

1527 pLvlGrid[t] = np.unique(np.concatenate((pLvlGrid_t, pLvlExtra_alt))) 

1528 

1529 return pLvlGrid 

1530 

1531 

1532############################################################################### 

1533 

1534 

1535def make_persistent_income_process_dict( 

1536 cycles, 

1537 T_cycle, 

1538 PermShkStd, 

1539 PermShkCount, 

1540 pLogInitMean, 

1541 pLogInitStd, 

1542 PermGroFac, 

1543 PrstIncCorr, 

1544 pLogCount, 

1545 pLogRange, 

1546): 

1547 """ 

1548 Constructs a dictionary with several elements that characterize the income 

1549 process for an agent with AR(1) persistent income process and lognormal transitory 

1550 shocks (with unemployment). The produced dictionary includes permanent income 

1551 grids and transition matrices and a mean permanent income lifecycle sequence. 

1552 

1553 This function only works with cycles>0 or T_cycle=1. 

1554 

1555 Parameters 

1556 ---------- 

1557 cycles : int 

1558 Number of times the agent's sequence of periods repeats. 

1559 T_cycle : int 

1560 Number of periods in the sequence. 

1561 PermShkStd : [float] 

1562 Standard deviation of mean one permanent income shocks in each period, 

1563 assumed to be lognormally distributed. 

1564 PermShkCount : int 

1565 Number of discrete nodes in the permanent income shock distribution (can 

1566 be used during simulation). 

1567 pLogInitMean : float 

1568 Mean of log permanent income at model entry. 

1569 pLogInitStd : float 

1570 Standard deviation of log permanent income at model entry. 

1571 PermGroFac : [float] 

1572 Lifecycle sequence of permanent income growth factors, *not* offset by 

1573 one period as in most other HARK models. 

1574 PrstIncCorr : float 

1575 Correlation coefficient of the persistent component of income. 

1576 pLogCount : int 

1577 Number of gridpoints in the grid of (log) persistent income deviations. 

1578 pLogRange : float 

1579 Upper bound of log persistent income grid, in standard deviations from 

1580 the mean; grid has symmetric lower bound. 

1581 

1582 Returns 

1583 ------- 

1584 IncomeProcessDict : dict 

1585 Dictionary with the following entries. 

1586 

1587 pLogGrid : [np.array] 

1588 Age-dependent grids of log persistent income, in deviations from mean. 

1589 pLvlMean : [float] 

1590 Mean persistent income level by age. 

1591 pLogMrkvArray : [np.array] 

1592 Age-dependent Markov transition arrays among pLog levels at the start of 

1593 each period in the sequence. 

1594 """ 

1595 if cycles == 0: 

1596 if T_cycle > 1: 

1597 raise ValueError( 

1598 "Can't handle infinite horizon models with more than one period!" 

1599 ) 

1600 if PermGroFac[0] != 1.0: 

1601 raise ValueError( 

1602 "Can't handle permanent income growth in infinite horizon!" 

1603 ) 

1604 

1605 # The single pLogGrid and transition matrix can be generated by the basic 

1606 # Tauchen AR(1) method from HARK.distributions. 

1607 pLogGrid, pLogMrkvArray = make_tauchen_ar1( 

1608 pLogCount, 

1609 sigma=PermShkStd[0], 

1610 ar_1=PrstIncCorr, 

1611 bound=pLogRange, 

1612 ) 

1613 pLogGrid = [pLogGrid] 

1614 pLogMrkvArray = [pLogMrkvArray] 

1615 pLvlMean = [np.exp(pLogInitMean + 0.5 * pLogInitStd**2)] 

1616 

1617 else: 

1618 # Start with the pLog distribution at model entry 

1619 pLvlMeanNow = np.exp(pLogInitMean + 0.5 * pLogInitStd**2) 

1620 pLogStdNow = pLogInitStd 

1621 pLogGridPrev = np.linspace( 

1622 -pLogRange * pLogStdNow, pLogRange * pLogStdNow, pLogCount 

1623 ) 

1624 

1625 # Initialize empty lists to hold output 

1626 pLogGrid = [] 

1627 pLogMrkvArray = [] 

1628 pLvlMean = [] 

1629 

1630 for c in range(cycles): 

1631 for t in range(T_cycle): 

1632 # Update the distribution of persistent income deviations from mean 

1633 pLvlMeanNow *= PermGroFac[t] 

1634 pLogStdNow = np.sqrt( 

1635 (PrstIncCorr * pLogStdNow) ** 2 + PermShkStd[t] ** 2 

1636 ) 

1637 pLogGridNow = np.linspace( 

1638 -pLogRange * pLogStdNow, pLogRange * pLogStdNow, pLogCount 

1639 ) 

1640 

1641 # Compute transition distances from prior grid to this one 

1642 pLogCuts = (pLogGridNow[1:] + pLogGridNow[:-1]) / 2.0 

1643 pLogCuts = np.concatenate(([-np.inf], pLogCuts, [np.inf])) 

1644 distances = np.reshape(pLogCuts, (1, pLogCount + 1)) - np.reshape( 

1645 PrstIncCorr * pLogGridPrev, (pLogCount, 1) 

1646 ) 

1647 distances /= PermShkStd 

1648 

1649 # Compute transition probabilities, ensuring that very small 

1650 # probabilities are treated identically in both directions 

1651 cdf_array = norm.cdf(distances) 

1652 sf_array = norm.sf(distances) 

1653 pLogMrkvNow = cdf_array[:, 1:] - cdf_array[:, :-1] 

1654 pLogMrkvNowAlt = sf_array[:, :-1] - sf_array[:, 1:] 

1655 pLogMrkvNow = np.maximum(pLogMrkvNow, pLogMrkvNowAlt) 

1656 pLogMrkvNow /= np.sum(pLogMrkvNow, axis=1, keepdims=True) 

1657 

1658 # Add this period's output to the lists 

1659 pLogGrid.append(pLogGridNow) 

1660 pLogMrkvArray.append(pLogMrkvNow) 

1661 pLvlMean.append(pLvlMeanNow) 

1662 pLogGridPrev = pLogGridNow 

1663 

1664 # Gather and return the output 

1665 IncomeProcessDict = { 

1666 "pLogGrid": pLogGrid, 

1667 "pLogMrkvArray": pLogMrkvArray, 

1668 "pLvlMean": pLvlMean, 

1669 } 

1670 return IncomeProcessDict 

1671 

1672 

1673############################################################################### 

1674 

1675 

1676def combine_ind_and_agg_income_shocks(IncShkDstnInd, AggShkDstn, RNG, T_cycle): 

1677 """ 

1678 Updates attribute IncShkDstn by combining idiosyncratic shocks with aggregate shocks. 

1679 

1680 Parameters 

1681 ---------- 

1682 IncShkDstnInd : [DiscreteDistribution] 

1683 Age-varying list of idiosyncratic income shock distributions. 

1684 AggShkDstn : DiscreteDistribution 

1685 Aggregate productivity shock distribution. 

1686 RNG : RandomState 

1687 Internal random number generator. 

1688 T_cycle : int 

1689 Number of periods in the agent's sequence of problems. 

1690 

1691 Returns 

1692 ------- 

1693 IncShkDstn : [DiscreteDistribution] 

1694 Combined idiosyncratic and aggregate income shocks, discretized, by age. 

1695 """ 

1696 IncShkDstn = [ 

1697 combine_indep_dstns( 

1698 IncShkDstnInd[t], AggShkDstn, seed=RNG.integers(0, 2**31 - 1) 

1699 ) 

1700 for t in range(T_cycle) 

1701 ] 

1702 return IncShkDstn 

1703 

1704 

1705def combine_markov_ind_and_agg_income_shocks( 

1706 IncShkDstnInd, AggShkDstn, MrkvArray, RNG, T_cycle 

1707): 

1708 """ 

1709 Updates attribute IncShkDstn by combining idiosyncratic shocks with aggregate shocks, 

1710 for a model with an aggregate Markov discrete state. 

1711 

1712 Parameters 

1713 ---------- 

1714 IncShkDstnInd : [DiscreteDistribution] 

1715 Age-varying list of idiosyncratic income shock distributions. 

1716 AggShkDstn : DiscreteDistribution 

1717 Aggregate productivity shock distribution. 

1718 MrkvArray : np.array 

1719 Markov transition matrix among discrete macroeconomic states. 

1720 RNG : RandomState 

1721 Internal random number generator. 

1722 T_cycle : int 

1723 Number of periods in the agent's sequence of problems. 

1724 

1725 Returns 

1726 ------- 

1727 IncShkDstn : [[DiscreteDistribution]] 

1728 Combined idiosyncratic and aggregate income shocks, discretized, by age 

1729 and Markov state. 

1730 """ 

1731 IncShkDstn = [] 

1732 N = MrkvArray.shape[0] 

1733 for t in range(T_cycle): 

1734 IncShkDstn.append( 

1735 [ 

1736 combine_indep_dstns( 

1737 IncShkDstnInd[t][n], 

1738 AggShkDstn[n], 

1739 seed=RNG.integers(0, 2**31 - 1), 

1740 ) 

1741 for n in range(N) 

1742 ] 

1743 ) 

1744 return IncShkDstn