🇬🇧 English · 🇪🇸 Español
HJB en tiempo continuo, KFE adjunta y equilibrio general de Aiyagari continuo
puremacro.vfi.hjb_achdou (reexportado desde puremacro.vfi) implementa el conjunto de herramientas para agentes heterogéneos en tiempo continuo de Achdou, Han, Lasry, Lions y Moll (2022, Review of Economic Studies): un solucionador implícito de diferencias finitas contra el viento (upwind) para la ecuación estacionaria de Hamilton-Jacobi-Bellman (HJB) del problema de fluctuaciones del ingreso (solve_hjb_achdou), la ecuación de Kolmogorov hacia adelante (KFE, Kolmogorov Forward Equation) adjunta que entrega la distribución estacionaria de la riqueza sin simulación (solve_kfe_achdou) y un algoritmo de búsqueda de raíces del equilibrio general para la economía de Aiyagari (1994) / Huggett (1993) en tiempo continuo (solve_aiyagari_continuous_hjb). Los resultados se devuelven como las dataclass inmutables HJBSolution y AiyagariContinuousHJBResult, que conservan el acceso al estilo de diccionario para el código escrito contra la interfaz anterior a 3.4.0.
En tiempo continuo, el problema del hogar es una ecuación en derivadas parciales de primer orden y no un operador de Bellman sobre una malla de elecciones del periodo siguiente. Al discretizarla con un esquema upwind, cada paso de mejora de la política se convierte en una única resolución de un sistema lineal disperso, y el mismo generador infinitesimal \(\mathbf{A}\) que empuja la función de valor hacia atrás en el tiempo empuja la distribución transversal hacia adelante. La densidad estacionaria es, por tanto, un vector nulo de \(\mathbf{A}^\top\), que se obtiene con precisión de máquina y sin el ruido de Monte Carlo. Esta página es la contraparte en tiempo continuo de vfi_continuous_equilibrium.md, que cubre la simulación no estocástica de Young (2010) en tiempo discreto y solve_aiyagari_continuous; la Sección 2.2 detalla cómo se relacionan ambas y dónde encajan los cuadernos.
1. Marco teórico y algorítmico
1.1 La ecuación HJB estacionaria
Un hogar descuenta a la tasa \(\rho > 0\), tiene utilidad CRRA \(u(c) = c^{1-\gamma}/(1-\gamma)\) (con \(u(c) = \log c\) cuando \(\gamma = 1\)), mantiene activos \(a \in [a_{\min}, a_{\max}]\) que rinden la tasa de interés \(r\) y recibe ingreso laboral \(w e_j\), donde el estado de productividad \(e_j \in \{e_1, \dots, e_{N_e}\}\) sigue una cadena de Markov en tiempo continuo con generador \(\boldsymbol{\Lambda} = (\lambda_{jk})\): \(\lambda_{jk} \ge 0\) para \(k \ne j\) y cada fila suma cero. La función de valor \(v_j(a) = v(a, e_j)\) resuelve
La condición de primer orden entrega la regla de consumo \(c_j(a) = \big(v_j'(a)\big)^{-1/\gamma}\) y la deriva del ahorro \(s_j(a) = r a + w e_j - c_j(a)\). El límite de endeudamiento \(a \ge a_{\min}\) entra como una condición de frontera de restricción de estado (Achdou et al. 2022): la deriva no puede apuntar fuera del espacio de estados en \(a_{\min}\), lo que equivale a \(v_j'(a_{\min}) \ge u'(r a_{\min} + w e_j)\). La condición especular \(v_j'(a_{\max}) \le u'(r a_{\max} + w e_j)\) (ausencia de deriva hacia afuera en el extremo superior) se impone en la frontera superior artificial \(a_{\max}\).
1.2 Diferencias finitas upwind y la frontera de restricción de estado
Sobre una malla de activos \(a_1 < a_2 < \dots < a_{N_a}\) con espaciamientos locales \(\Delta a_i^{+} = a_{i+1} - a_i\) y \(\Delta a_i^{-} = a_i - a_{i-1}\), la derivada se aproxima de forma unilateral en la dirección en que se mueve el estado («contra el viento» o upwind):
Cada derivada candidata implica un nivel de consumo \(c = (v')^{-1/\gamma}\) y una deriva \(s_F\) o \(s_B\). El esquema usa la diferencia hacia adelante donde \(s_{F} > 0\), la diferencia hacia atrás donde \(s_{B} < 0\) y, en caso contrario, fija la deriva en cero con \(c_{i,j} = r a_i + w e_j\) (el hogar consume su ingreso corriente y permanece donde está). En los dos extremos de la malla, la derivada unilateral que falta se reemplaza por \(u'(r a_i + w e_j)\), lo que hace que la deriva correspondiente sea exactamente cero, de modo que ningún flujo de probabilidad abandona jamás \([a_{\min}, a_{\max}]\). solve_hjb_achdou además enmascara la rama hacia adelante en \(a_{\max}\) y la rama hacia atrás en \(a_{\min}\), de modo que las filas de frontera solo pueden llevar una deriva que apunte hacia adentro o se anule. Por eso, en el Ejemplo 4.1, el hogar de ingreso bajo en \(a = 0\) tiene \(c = 0.2 = w e_1\) y \(s = 0\) exactamente.
Se dispone de dos alternativas al tratamiento de restricción de estado:
- Derivadas de frontera prescritas (
v_prime_boundary=(v'_{\min}, v'_{\max})): condiciones de Neumann. Las máscaras se desactivan, se usa la derivada dada en la fila de frontera y cualquier flujo hacia afuera \(s \cdot v'_{\text{frontera}}\) se traslada al lado derecho del sistema lineal como un término inhomogéneo. Una entrada dejada enNonerecurre a la derivada de restricción de estado en ese lado. - Referencia del problema del pastel (cake-eating) (
w_rate=0.0): sin ingreso laboral el problema tiene la forma cerrada \(c(a) = \mu a\), \(\mu = (\rho - (1-\gamma) r)/\gamma\). El solucionador inicializa entonces \(v\) a partir de esa solución, desactiva las máscaras de frontera (como en el caso de Neumann) y usa las utilidades marginales analíticas \((\mu a_{\min})^{-\gamma}\) y \((\mu a_{\max})^{-\gamma}\) en las filas de frontera, que es comotests/test_hjb_implicit.pyvalida el esquema contra una solución exacta. Como \((\mu a)^{-\gamma}\) es singular en el origen, este modo exige una malla que empiece estrictamente por encima de cero:w_rate=0.0cona_grid[0] <= 0lanzaValueErrorindicando que pasea_min > 0.
1.3 El esquema implícito y la propiedad de M-matriz
Al reunir las derivas upwind en el generador infinitesimal disperso \(\mathbf{A}^n\) (una matriz de \(N_a N_e \times N_a N_e\) indexada en orden por columnas, estado \((i, j) \mapsto j N_a + i\)), cada fila del bloque de activos tiene entradas
colocadas en la subdiagonal, la superdiagonal y la diagonal, respectivamente, y el proceso de ingreso aporta \(\boldsymbol{\Lambda} \otimes \mathbf{I}_{N_a}\). Las entradas fuera de la diagonal son no negativas y cada fila de \(\mathbf{A}^n\) suma cero (las sumas por fila del generador devuelto en el Ejemplo 4.1 son cero hasta \(2.2 \times 10^{-16}\)). En lugar de avanzar la función de valor de forma explícita, lo que está sujeto a una restricción de Courant-Friedrichs-Lewy \(\Delta t = O(\Delta a)\), la actualización es implícita en \(v^{n+1}\):
Con derivadas de frontera prescritas, el término de flujo hacia afuera de la Sección 1.2 se añade al lado derecho; bajo la restricción de estado es idénticamente cero. La matriz \(\mathbf{B}^n = (\rho + 1/\Delta)\mathbf{I} - \mathbf{A}^n\) tiene diagonal positiva, entradas fuera de la diagonal no positivas y sumas por fila iguales a \(\rho + 1/\Delta > 0\), de modo que es estrictamente diagonalmente dominante y, por tanto, una M-matriz no singular con \((\mathbf{B}^n)^{-1} \ge 0\) elemento a elemento. Una inversa no negativa hace que el esquema discreto sea monótono, y el teorema de Barles-Souganidis (1991) garantiza entonces que un esquema monótono, estable y consistente converge a la solución de viscosidad de la ecuación HJB a medida que se refina la malla. solve_hjb_achdou factoriza \(\mathbf{B}^n\) con scipy.sparse.linalg.spsolve (almacenamiento CSC) en cada iteración y se detiene cuando \(\| v^{n+1} - v^n \|_\infty <\) tol.
El papel de \(\Delta\). Cuando \(\Delta \to \infty\) la actualización se convierte en la iteración de políticas de Howard, es decir, el método de Newton sobre la ecuación HJB, razón por la cual el valor por defecto Delta=1e4 converge en un puñado de iteraciones; un \(\Delta\) pequeño convierte el mismo sistema en una marcha amortiguada en pseudotiempo que necesita muchos más barridos. Medido con los valores por defecto (Na=100, tol=1e-8, max_iter=100; Ejemplo 4.5):
Delta |
Iteraciones | Convergió | \(\max \lvert v - v_{\Delta = 10^4} \rvert\) |
|---|---|---|---|
| \(10^4\) | 9 | sí | referencia |
| \(10^2\) | 14 | sí | \(9.8 \times 10^{-10}\) |
| \(10\) | 50 | sí | \(1.8 \times 10^{-8}\) |
| \(1\) | 100 | no (necesita 360 con max_iter=1000) |
\(6.7 \times 10^{-2}\) al alcanzar el tope |
El esquema es incondicionalmente estable para todo \(\Delta > 0\); lo que depende de \(\Delta\) es el número de iteraciones, no la estabilidad.
1.4 La KFE adjunta y la normalización de la masa
La distribución conjunta estacionaria \(g(a, e)\) del proceso controlado es el vector nulo del generador adjunto,
donde los \(w_i\) son los pesos de cuadratura por ancho de celda de la malla de activos,
de modo que \(w_i = \Delta a\) en todos los nodos de una malla uniforme. El generador actúa sobre los nodos de la malla, así que el vector nulo de \(\mathbf{A}^\top\) es el vector de masas nodales estacionarias \(\pi_{i,j}\), no una densidad. Por eso solve_kfe_achdou toma el generador de la última iteración de la HJB, reemplaza la primera fila de \(\mathbf{A}^\top\) por una fila de unos y la primera entrada del lado derecho por \(1\) (el reemplazo estándar de una ecuación redundante por la normalización \(\sum \pi = 1\)), resuelve con spsolve, recorta a cero cualquier negativo de redondeo, reescala \(\pi\) para que sume exactamente uno y solo entonces convierte a densidad dividiendo por el ancho local de la celda,
Así \(\sum_{i,j} g_{i,j} w_i = 1\) y \(\sum_i g_{i,j} w_i = \pi_j\) (la distribución estacionaria del generador del ingreso) en cualquier malla, uniforme o no, y los agregados de la forma \(\sum_{i,j} a_i g_{i,j} w_i\) son la aproximación por cuadratura correcta de \(\int a\, g(a, e)\, da\) — los mismos pesos que solve_aiyagari_continuous_hjb usa para la oferta de capital. En una malla uniforme ambas lecturas coinciden: \(w_i = \Delta a\), la masa de probabilidad en el nodo \(i\) es \(g_{i,j} \Delta a\), y \(\sum_{i,j} g_{i,j} \Delta a = 1\) (el Ejemplo 4.2 imprime 1.000000000000).
El mass_residual reportado es \(\lvert \sum_{i,j} g_{i,j} w_i - 1 \rvert\) medido después de esa normalización, por lo que es del orden de \(10^{-16}\) por construcción (1.11e-16 en el Ejemplo 4.1); certifica la normalización, no la exactitud de la resolución lineal. Antes de resolver, solve_kfe_achdou comprueba además que el grafo de transiciones del generador tenga exactamente una clase comunicante cerrada, que es la condición exacta para que el espacio nulo sea unidimensional, y lanza ValueError en caso contrario: un generador de ingreso reducible como A_z=np.zeros((2, 2)) se rechaza con un mensaje que indica el número de clases cerradas, en lugar de devolver un vector nulo arbitrario.
1.5 Equilibrio general: búsqueda de raíces para \(r\) con la condición de primer orden de la empresa
Las empresas producen \(Y = K^\alpha L^{1-\alpha}\) y alquilan capital a \(r + \delta\), de modo que, dada una tasa de interés tentativa, la razón capital-trabajo y el salario se siguen de las condiciones de primer orden de la empresa:
donde \(\boldsymbol{\pi}\) es la distribución estacionaria del generador del ingreso (\(\boldsymbol{\pi}^\top \boldsymbol{\Lambda} = 0\); con el proceso simétrico de dos estados por defecto, \(\pi = (0.5, 0.5)\) y \(L = 0.6\)). Para cada \(r\) tentativo, solve_aiyagari_continuous_hjb calcula \(w(r)\), llama a solve_hjb_achdou (con la KFE), agrega la riqueza de los hogares \(K^s(r) = \sum_{i,j} a_i\, g_{i,j}(r)\, w_i\) con los pesos de cuadratura de la Sección 1.4 y evalúa el exceso de oferta \(\Phi(r) = K^s(r) - K^d(r)\). El método de Brent (scipy.optimize.root_scalar, method="brentq", a lo sumo max_iter_ge iteraciones por pasada) localiza \(r^\ast\) con \(\Phi(r^\ast) = 0\) en el intervalo \([r_{\min}, r_{\max}]\), cuyo extremo superior por defecto es \(\rho - 0.002\): con una restricción de endeudamiento, la distribución estacionaria solo existe para \(r < \rho\), porque con \(r \ge \rho\) los hogares acumularían sin límite. Antes de llamar a Brent, el solucionador comprueba el signo de \(\Phi\) en ambos extremos; si \(\Phi(r_{\min}) > 0\) divide \(r_{\min}\) a la mitad (sin bajar de \(0.001\), a lo sumo cinco veces) y si \(\Phi(r_{\max}) < 0\) desplaza \(r_{\max}\) hasta la mitad del camino hacia \(\rho\) (a lo sumo cinco veces). Un intervalo inválido (\(r_{\min} \ge r_{\max}\), \(r_{\max} \ge \rho\) o \(r_{\min} \le -\delta\)) lanza ValueError. Si no se encuentra ningún cambio de signo, devuelve la tasa tentativa almacenada en caché con el menor \(\lvert \Phi \rvert\). Cada solución HJB se almacena en caché por \(r\), de modo que la ejecución del Ejemplo 4.3 necesitó siete resoluciones del problema del hogar para seis iteraciones de Brent.
tol_ge gobierna el residuo de vaciado del mercado. No es un argumento decorativo: la tolerancia en \(x\) que se entrega a Brent se deriva de tol_ge y de la pendiente secante del intervalo, xtol = min(1e-5, max(0.5 * tol_ge / slope, 1e-12)), y tras la primera pasada el solucionador vuelve a acotar el intervalo con sus evaluaciones en caché y reejecuta Brent —hasta tres pasadas más— hasta que \(\lvert K^s(r^\ast) - K^d(r^\ast) \rvert <\) tol_ge. El converged devuelto es True solo cuando Brent convergió y se cumple esa desigualdad, de modo que significa lo que dice: el mercado de capital se vacía con tolerancia tol_ge. Ajustar la tolerancia ajusta el residuo: con la calibración por defecto, tol_ge=1e-4 se detiene en \(\Phi = -1.76 \times 10^{-6}\) tras 6 iteraciones de Brent, mientras que tol_ge=1e-8 alcanza \(\Phi = -7.4 \times 10^{-13}\) en 7.
2. Opciones metodológicas y del modelo
2.1 Tabla de opciones
| Característica | Argumento(s) | Valor por defecto | Notas |
|---|---|---|---|
| Proceso de ingreso | e_grid, A_z |
[0.2, 1.0], conmutación de Poisson simétrica con intensidad \(0.1\) |
Cualquier \(N_e\). A_z se valida: forma \((N_e, N_e)\), entradas finitas, entradas fuera de la diagonal no negativas y sumas por fila iguales a cero (ValueError en caso contrario, con una pista si pasó una matriz de probabilidades de transición). Con A_z=None y \(N_e > 2\) el valor por defecto es \(\lambda_{jk} = 0.2/(N_e-1)\) fuera de la diagonal y \(-0.2\) en ella. |
| Malla de activos | Na, a_min, a_max, a_grid |
100 nodos en \([0, 30]\), uniforme | a_grid anula los tres escalares y puede ser no uniforme; la KFE devuelve una densidad en ambos casos (Sección 1.4). Debe ser estrictamente creciente y tener al menos dos puntos. |
| Tratamiento de la frontera | v_prime_boundary, w_rate |
restricción de estado en ambos extremos | Una tupla de derivadas cambia a Neumann; w_rate=0.0 selecciona las fronteras analíticas del problema del pastel y entonces exige a_grid[0] > 0. |
| Paso implícito | Delta |
\(10^4\) | Cuanto mayor, más cerca de la iteración de políticas (menos barridos); véase la tabla de la Sección 1.3. |
| Regla de parada | max_iter, tol |
100, \(10^{-8}\) | Norma del supremo sobre funciones de valor sucesivas; max_iter debe ser un entero \(\ge 1\). En 3.3.0 los valores por defecto eran 500 y \(10^{-7}\). |
| Distribución | compute_kfe |
True |
False deja g_dist=None y mass_residual=0.0; A_generator se devuelve de todos modos, de modo que solve_kfe_achdou puede llamarse después. |
| Intervalo del equilibrio general | r_min, r_max, tol_ge, max_iter_ge |
\(0.005\), \(\rho - 0.002\), \(10^{-4}\), 40 | tol_ge es la tolerancia de vaciado del mercado: fija la tolerancia en \(x\) de Brent y converged significa \(\lvert K^s - K^d \rvert <\) tol_ge (Sección 1.5). |
| Resolución del hogar dentro del equilibrio general | max_iter_hjb, tol_hjb, Delta |
100, \(10^{-8}\), \(10^4\) | Se pasan a solve_hjb_achdou en cada tasa tentativa. |
2.2 Relación con las páginas en tiempo discreto y con los cuadernos
vfi_continuous_equilibrium.mdresuelve la misma economía de Aiyagari en tiempo discreto: los hogares descuentan con \(\beta = 0.96\), la política continua de ahorro se proyecta sobre un histograma con las loterías lineales de Young (2010) ysolve_aiyagari_continuousvacía el mercado de capital con Brent. Ese módulo también incluyeContinuousStationaryDistribution.gini()y.lorenz(). La página que está leyendo reemplaza la proyección por loterías con la KFE adjunta y el operador de Bellman con el generador de la HJB; aquí no hay una clase auxiliar de distribución, de modo que los estadísticos de desigualdad se calculan directamente a partir deg_dist, como en el Ejemplo 4.2. Las calibraciones difieren (\(\rho = 0.05\) frente a \(\beta = 0.96\), dos estados de ingreso de Poisson frente a una cadena de Tauchen o Rouwenhorst), por lo que las cifras de equilibrio no son comparables entre las dos páginas.notebooks/22_continuous_time_hjb.py(y su gemelo_es) introduce el esquema upwind en el problema de fluctuaciones del ingreso. Se escribió contra la interfaz 3.3.0: lee el resultado conresults["V"]y pasa explícitamentemax_iter=500, tol=1e-7, que son los valores por defecto antiguos. Ambas cosas siguen funcionando porqueHJBSolutionimplementa el protocoloMapping(Sección 6). Sus cifras, en cambio, no son las de 3.3.0: el solucionador explícito de 3.3.0 no tenía término de conmutación del ingreso, de modo que cada estado de productividad era un problema separado de ingreso determinista, mientras que desde 3.4.0 los estados están ligados por el generador de PoissonA_z(por defecto, simétrico con \(\lambda = 0.1\)). PasarA_z=np.zeros((Ne, Ne))junto concompute_kfe=Falsereproduce la economía de 3.3.0; hay que desactivar la KFE porque un generador sin conmutación no tiene una distribución estacionaria única (Sección 1.4).notebooks/56_implicit_hjb_and_continuous_kfe.py(en inglés y en español) es el cuaderno de demostración de este módulo: iteración implícita de la HJB, distribución de la riqueza por KFE con curva de Lorenz y coeficiente de Gini, el problema del pastel de referencia frente a la forma cerrada y el equilibrio general de Aiyagari continuo con curvas de oferta y demanda. La entrada del catálogo está ennotebooks.md.
3. Calibración canónica
Los valores por defecto configuran un problema de fluctuaciones del ingreso con dos estados, en el espíritu de Achdou et al. (2022), con parámetros de frecuencia anual:
| Parámetro | Símbolo | Valor por defecto | Argumento |
|---|---|---|---|
| Tasa de descuento | \(\rho\) | \(0.05\) | rho_val |
| Aversión relativa al riesgo | \(\gamma\) | \(2.0\) | gamma_r |
| Tasa de interés (equilibrio parcial) | \(r\) | \(0.03\) | r_rate |
| Salario (equilibrio parcial) | \(w\) | \(1.0\) | w_rate |
| Estados de productividad | \(e\) | \(\{0.2, 1.0\}\) | e_grid |
| Intensidades de conmutación | \(\lambda_{12} = \lambda_{21}\) | \(0.1\) | A_z |
| Malla de activos | \([a_{\min}, a_{\max}]\), \(N_a\) | \([0, 30]\), 100 nodos uniformes | a_min, a_max, Na |
| Paso implícito | \(\Delta\) | \(10^4\) | Delta |
| Participación del capital (equilibrio general) | \(\alpha\) | \(0.33\) | alpha |
| Depreciación (equilibrio general) | \(\delta\) | \(0.05\) | delta |
| Intervalo de la tasa de interés (equilibrio general) | \([r_{\min}, r_{\max}]\) | \([0.005, 0.048]\) | r_min, r_max |
4. Ejemplos prácticos ejecutables
Todos los ejemplos son deterministas (no hay generación de números aleatorios en ningún punto del módulo), no necesitan acceso a la red y terminan en bastante menos de una décima de segundo de tiempo de solucionador en un portátil; las cifras citadas a continuación son las que imprimió cada script.
4.1 Resolución en equilibrio parcial y summary()
import numpy as np
from puremacro.vfi import solve_hjb_achdou
sol = solve_hjb_achdou(r_rate=0.03, w_rate=1.0, rho_val=0.05, gamma_r=2.0, Na=100, a_max=30.0)
print(sol.n_iter, sol.converged, f"{sol.mass_residual:.1e}")
print(sol.V.shape, sol.c_policy.shape, sol.g_dist.shape, sol.A_generator.shape)
print(sol.summary())
# El acceso heredado al estilo de diccionario sigue funcionando (12 claves); los campos nuevos son atributos.
print(sol["n_iter"] == sol.n_iter, list(sol.keys())[:4], "g_dist" in sol, "g_dist" in sol.keys())
print(f"c(0, e_low) = {sol.c_policy[0, 0]:.4f} c(0, e_high) = {sol.c_policy[0, 1]:.4f}")
print(f"s(0, e_low) = {sol.s_drift[0, 0]:.4f} s(0, e_high) = {sol.s_drift[0, 1]:.4f}")
El solucionador converge en 9 iteraciones a tol=1e-8 con un residuo de masa de 1.1e-16; los arrays son (100, 2) y el generador es una matriz dispersa (200, 200). summary() devuelve un DataFrame indexado por métrica: 200 estados, tiempo transcurrido 0.0144 s (dependiente de la máquina), valor medio \(-22.000181\), consumo entre \(0.200000\) y \(1.972175\). Las dos últimas líneas muestran la restricción de estado en acción: el hogar de ingreso bajo en \(a = 0\) consume exactamente su ingreso laboral \(0.2000\) con deriva cero, mientras que el hogar de ingreso alto consume \(0.4896\) y ahorra a la tasa \(0.5104\). "g_dist" in sol es True pero "g_dist" in sol.keys() es False, porque keys() enumera solo las doce claves heredadas, mientras que la pertenencia y el subíndice cubren todos los campos de la dataclass (Sección 6).
4.2 Curva de Lorenz y coeficiente de Gini a partir de g_dist
import numpy as np
from puremacro.vfi import solve_hjb_achdou
sol = solve_hjb_achdou(r_rate=0.03, w_rate=1.0, rho_val=0.05, gamma_r=2.0, Na=100, a_max=30.0)
a, g = sol.a_grid, sol.g_dist
da = a[1] - a[0] # malla uniforme: masa nodal = g * da
mass_a = g.sum(axis=1) * da # distribución marginal de la riqueza, suma uno
mean_a = float(np.sum(a * mass_a))
# Curva de Lorenz: participación acumulada de la población frente a participación acumulada de la riqueza
pop = np.concatenate([[0.0], np.cumsum(mass_a)])
wealth = np.concatenate([[0.0], np.cumsum(a * mass_a) / mean_a])
gini = float(1.0 - np.sum((wealth[1:] + wealth[:-1]) * np.diff(pop)))
top10 = 1.0 - float(np.interp(0.90, pop, wealth))
print(f"mass total = {mass_a.sum():.12f}")
print(f"mean wealth (capital supply) = {mean_a:.4f}")
print(f"share of households at a = 0 : {mass_a[0]:.4f}")
print(f"Gini coefficient of wealth : {gini:.4f}")
print(f"wealth share of the top 10% : {top10:.4f}")
print(f"stationary income shares : {g.sum(axis=0) * da}")
Las masas nodales suman 1.000000000000. Con \(r = 0.03\) y \(w = 1\), la riqueza media (la oferta de capital en equilibrio parcial) es \(6.2150\), el \(3.82\%\) de los hogares se sitúa exactamente en el límite de endeudamiento, el coeficiente de Gini de la riqueza es \(0.3611\) y el decil superior posee el \(21.99\%\) de la riqueza. La marginal del ingreso es [0.5 0.5], la distribución estacionaria de la cadena simétrica de dos estados. La fórmula del Gini es el área trapezoidal entre la curva de Lorenz \((p, L(p))\) y la diagonal, la misma expresión que usa el cuaderno 56.
4.3 Equilibrio general: \(r^\ast\), \(w^\ast\), \(K^\ast\)
from puremacro.vfi import solve_aiyagari_continuous_hjb
ge = solve_aiyagari_continuous_hjb(alpha=0.33, delta=0.05, rho_val=0.05, gamma_r=2.0, Na=100, a_max=30.0)
print(f"r* = {ge.r_star:.5f} w* = {ge.w_star:.4f} K* = {ge.K_star:.4f} L* = {ge.L_star:.3f} Y* = {ge.Y_star:.4f}")
print(f"excess capital = {ge.excess_capital:.2e} converged = {ge.converged} Brent iterations = {ge.n_iter_ge}")
print(f"K/Y = {ge.K_star / ge.Y_star:.3f} household HJB iterations at r*: {ge.hjb_solution.n_iter}")
print(ge.to_markdown())
El mercado se vacía en \(r^\ast = 0.02004\), \(w^\ast = 1.4376\), \(K^\ast = 6.0656\) con \(L^\ast = 0.600\) e \(Y^\ast = 1.2874\) (razón capital-producto \(4.712\)); el exceso de oferta en la raíz es \(-1.76 \times 10^{-6}\), muy dentro del tol_ge=1e-4 por defecto, de modo que converged es True tras 6 iteraciones de Brent, y el problema del hogar en \(r^\ast\) requirió 10 iteraciones de la HJB. Pasar tol_ge=1e-8 lleva la misma ejecución a \(\Phi = -7.4 \times 10^{-13}\) en 7 iteraciones. to_markdown() genera la tabla resumen (la fila del tiempo transcurrido depende de la máquina):
| Metric | Value |
|--------------------------------|-----------|
| Equilibrium Interest Rate (r*) | 0.020041 |
| Equilibrium Wage (w*) | 1.437585 |
| Aggregate Capital (K*) | 6.065605 |
| Capital Supply (Ks) | 6.065605 |
| Capital Demand (Kd) | 6.065607 |
| Capital Market Clearing Error | -1.76e-06 |
| Aggregate Labor (L*) | 0.600000 |
| Aggregate Output (Y*) | 1.287389 |
| GE Root-finding Converged | True |
| GE Iterations | 6 |
| Elapsed Time (s) | 0.0421 |
4.4 Un proceso de ingreso personalizado: pasar e_grid y A_z
import numpy as np
from puremacro.vfi import solve_hjb_achdou
e_grid = np.array([0.5, 1.0, 1.5])
A_z = np.array([[-0.20, 0.20, 0.00],
[ 0.10,-0.20, 0.10],
[ 0.00, 0.20,-0.20]]) # generador: fuera de la diagonal >= 0, las filas suman cero
assert np.allclose(A_z.sum(axis=1), 0.0)
sol = solve_hjb_achdou(r_rate=0.03, w_rate=1.0, Na=100, a_max=30.0, e_grid=e_grid, A_z=A_z)
da = sol.a_grid[1] - sol.a_grid[0]
print(sol.n_iter, sol.converged, sol.V.shape)
print("stationary income shares:", np.round(sol.g_dist.sum(axis=0) * da, 4))
print("mean wealth by income state:", np.round((sol.a_grid[:, None] * sol.g_dist * da).sum(axis=0) / (sol.g_dist.sum(axis=0) * da), 3))
A_z es un generador, no una matriz de probabilidades de transición: la entrada \((j, k)\) es la intensidad de Poisson de saltar del estado \(j\) al estado \(k\), la diagonal es menos la tasa total de salida y las filas suman cero. El solucionador lo comprueba por usted —una matriz cuyas filas suman uno se rechaza con ValueError: A_z rows must sum to zero (continuous-time generator) ... A_z looks like a transition-probability matrix P; the solver expects a continuous-time generator with zero row sums, e.g. (P - I) / dt—, de modo que el assert anterior es una precaución y no una necesidad. La cadena de nacimiento y muerte de tres estados converge en 8 iteraciones; la KFE recupera la distribución estacionaria de la cadena [0.25 0.5 0.25] como marginal del ingreso, y la riqueza media aumenta con la productividad, [1.686 2.837 4.227].
4.5 Número de iteraciones en función de Delta
import numpy as np
from puremacro.vfi import solve_hjb_achdou
base = solve_hjb_achdou(Delta=1e4)
for Delta in (1e4, 1e2, 10.0, 1.0):
s = solve_hjb_achdou(Delta=Delta) # max_iter=100, tol=1e-8
print(f"Delta = {Delta:>8.0f}: n_iter = {s.n_iter:3d} converged = {s.converged!s:5} "
f"max|V - V_ref| = {np.max(np.abs(s.V - base.V)):.1e}")
print("Delta = 1, max_iter = 1000:", solve_hjb_achdou(Delta=1.0, max_iter=1000).n_iter)
Esta es la fuente de la tabla de la Sección 1.3: 9, 14, 50 y 100 iteraciones para \(\Delta = 10^4, 10^2, 10, 1\), con converged=False en \(\Delta = 1\) bajo el max_iter=100 por defecto (la función de valor aún está a \(6.7 \times 10^{-2}\) de la referencia) y 360 iteraciones cuando el tope se eleva a 1000. Compruebe siempre converged después de reducir Delta.
5. Especificación completa de la API
solve_hjb_achdou(
r_rate: float = 0.03,
w_rate: float = 1.0,
rho_val: float = 0.05,
gamma_r: float = 2.0,
Na: int = 100,
max_iter: int = 100,
tol: float = 1e-8,
*,
a_min: float = 0.0,
a_max: float = 30.0,
a_grid: np.ndarray | None = None,
e_grid: np.ndarray | None = None,
A_z: np.ndarray | None = None,
Delta: float = 1e4,
compute_kfe: bool = True,
v_prime_boundary: tuple[float | None, float | None] | None = None,
) -> HJBSolution
solve_kfe_achdou(
A: scipy.sparse.spmatrix,
a_grid: np.ndarray,
e_grid: np.ndarray,
return_residual: bool = False,
) -> np.ndarray | tuple[np.ndarray, float]
solve_aiyagari_continuous_hjb(
alpha: float = 0.33,
delta: float = 0.05,
rho_val: float = 0.05,
gamma_r: float = 2.0,
Na: int = 100,
a_min: float = 0.0,
a_max: float = 30.0,
a_grid: np.ndarray | None = None,
e_grid: np.ndarray | None = None,
A_z: np.ndarray | None = None,
r_min: float = 0.005,
r_max: float | None = None,
tol_ge: float = 1e-4,
max_iter_ge: int = 40,
max_iter_hjb: int = 100,
tol_hjb: float = 1e-8,
Delta: float = 1e4,
) -> AiyagariContinuousHJBResult
Parámetros de solve_hjb_achdou
| Parámetro | Significado |
|---|---|
r_rate, w_rate |
Tasa de interés sobre los activos y salario por unidad de eficiencia. w_rate=0.0 cambia al tratamiento de frontera del problema del pastel (Sección 1.2) y entonces exige a_grid[0] > 0, porque la forma cerrada \((\mu a)^{-\gamma}\) es singular en \(a = 0\); ValueError en caso contrario. |
rho_val, gamma_r |
Tasa de descuento \(\rho\) y coeficiente CRRA \(\gamma\); \(\gamma = 1\) selecciona utilidad logarítmica. |
Na, a_min, a_max |
Malla uniforme de activos np.linspace(a_min, a_max, Na); a_min es el límite de endeudamiento. |
a_grid |
Malla explícita; anula Na, a_min, a_max. Puede ser no uniforme; la KFE pondera cada nodo por el ancho de su celda (Sección 1.4). Debe ser unidimensional, finita y estrictamente creciente, con al menos dos puntos (ValueError en caso contrario). |
e_grid |
Estados de productividad, forma (Ne,); por defecto [0.2, 1.0]. |
A_z |
Generador del ingreso, forma (Ne, Ne), entradas finitas, fuera de la diagonal no negativas y sumas por fila iguales a cero — todo ello validado, con ValueError ante cualquier incumplimiento. Por defecto: intensidad simétrica \(0.1\) para dos estados, \(0.2/(N_e - 1)\) fuera de la diagonal con \(-0.2\) en ella en otro caso, [[0.]] para un solo estado. |
max_iter, tol |
Tope de iteraciones (entero \(\ge 1\), ValueError en caso contrario) y tolerancia en norma del supremo sobre \(v^{n+1} - v^n\). converged es False cuando se alcanza el tope. |
Delta |
Paso implícito \(\Delta\) (Sección 1.3). |
compute_kfe |
Resolver la KFE adjunta tras la convergencia y rellenar g_dist y mass_residual. |
v_prime_boundary |
Derivadas de Neumann (v'_min, v'_max); cualquiera de las dos entradas puede ser None. |
Parámetros de solve_kfe_achdou
| Parámetro | Significado |
|---|---|
A |
Generador de HJBSolution.A_generator (o cualquier matriz dispersa con la misma estructura), forma (Na*Ne, Na*Ne) en orden de estados por columnas. Una forma incompatible, o un generador con más de una clase comunicante cerrada, lanza ValueError. |
a_grid, e_grid |
Las mallas sobre las que se construyó el generador; solo se usan sus longitudes y los espaciamientos de activos. a_grid debe ser estrictamente creciente. |
return_residual |
True devuelve (g, mass_residual); False devuelve solo g. g es una densidad: masa por unidad de \(a\), con \(\sum_{i,j} g_{i,j} w_i = 1\). |
Parámetros de solve_aiyagari_continuous_hjb
| Parámetro | Significado |
|---|---|
alpha, delta |
Participación del capital en la Cobb-Douglas y tasa de depreciación. |
rho_val, gamma_r, Na, a_min, a_max, a_grid, e_grid, A_z, Delta |
Como en solve_hjb_achdou; se pasan a cada resolución del problema del hogar. |
r_min, r_max |
Intervalo de Brent; r_max=None significa \(\rho - 0.002\). El intervalo se amplía automáticamente como se describe en la Sección 1.5. r_min >= r_max, r_max >= rho_val o r_min <= -delta lanza ValueError. |
tol_ge |
Tolerancia de vaciado del mercado sobre \(\lvert K^s - K^d \rvert\). Fija la tolerancia en \(x\) de Brent, impulsa las pasadas de reacotamiento y decide converged (Sección 1.5). |
max_iter_ge |
Número máximo de iteraciones de Brent por pasada de acotamiento. |
max_iter_hjb, tol_hjb |
Pasados a solve_hjb_achdou como max_iter y tol. |
6. Interfaz de resultados y exportación a manuscritos
HJBSolution
Una @dataclass(frozen=True): asignar a un campo lanza FrozenInstanceError. Campos:
| Campo | Tipo / forma | Contenido |
|---|---|---|
V |
(Na, Ne) |
Función de valor convergida \(v_j(a_i)\). |
c_policy, s_drift |
(Na, Ne) |
Regla de consumo y deriva del ahorro \(s = r a + w e - c\). |
a_grid, e_grid |
(Na,), (Ne,) |
Mallas efectivamente utilizadas. |
n_iter, elapsed, converged |
int, float, bool |
Iteraciones realizadas, segundos transcurridos, si se alcanzó tol. |
r_rate, w_rate, rho_val, gamma_r |
float |
Copia de los parámetros de calibración. |
g_dist |
(Na, Ne) o None |
Densidad estacionaria de la KFE — masa por unidad de \(a\), de modo que la masa nodal es \(g_{i,j} w_i\) (None cuando compute_kfe=False). |
A_generator |
scipy.sparse.csr_matrix |
Generador \(\mathbf{A}\) de la última iteración, (Na*Ne, Na*Ne). |
mass_residual |
float |
\(\lvert \sum g_{i,j} w_i - 1 \rvert\) tras la renormalización. |
Compatibilidad hacia atrás con Mapping. HJBSolution hereda de collections.abc.Mapping. sol["V"], dict(sol), list(sol), len(sol) y sol.get(key, default) funcionan todos. keys() (y por tanto items(), values(), la iteración, dict(sol) y **sol) devuelve exactamente doce claves: las siete que llevaba el diccionario de 3.3.0, V, c_policy, s_drift, a_grid, e_grid, n_iter, elapsed, más las copias de los parámetros r_rate, w_rate, rho_val, gamma_r y la bandera converged. El subíndice, in y get están restringidos a los campos de la dataclass, es decir, esos doce más g_dist, A_generator y mass_residual: "g_dist" in sol es True, mientras que "summary" in sol es False y sol["summary"] lanza KeyError, de modo que ningún método ni atributo privado se filtra por la interfaz de mapeo. == compara los campos uno a uno y es consciente de arrays y matrices dispersas (tenga en cuenta que elapsed es tiempo de reloj, así que dos resoluciones distintas del mismo problema no resultan iguales); __hash__ es None, de modo que las instancias no son hashables, como un dict, y la dataclass es inmutable, de modo que asignar a un atributo lanza FrozenInstanceError y asignar a un elemento lanza TypeError.
Métodos:
summary() -> pd.DataFrame: catorce filas indexadas porMetric(tamaños de malla, iteraciones, convergencia, tiempo transcurrido, los cuatro parámetros, valor medio, consumo mínimo y máximo, error de masa de la KFE), como se imprime en el Ejemplo 4.1.to_frame() -> pd.DataFrame: una fila por estado, con columnasa_idx,e_idx,asset_a,prod_e,value_V,consumption_c,savings_drift_sy, cuando se resolvió la KFE,density_g(la densidad, no la masa nodal). Este es elDataFrameque conviene usar para gráficos personalizados de la distribución.to_markdown(**kwargs),to_latex(**kwargs),to_typst(**kwargs) -> str: la tabla resumen generada mediantepuremacro.reports.df_to_markdown,df_to_latexydf_to_typst(que aceptan sus argumentosindexydigits).plot(*, ax=None, figsize=None, show=False): tres paneles, función de valor, regla de consumo y deriva del ahorro, una línea por estado de ingreso. Devuelve laFigurecuando la crea, o losAxesque usted pasó; una secuencia de al menos tres ejes rellena todos los paneles, un único eje dibuja solo la función de valor. Apto para entornos sin pantalla (headless): no se muestra nada salvo queshow=True. La distribución estacionaria no se grafica; constrúyala a partir deg_distoto_frame().
AiyagariContinuousHJBResult
También una dataclass inmutable con el mismo protocolo Mapping: aquí keys() enumera los trece campos, el subíndice, in y get están restringidos a esos campos, == es la igualdad campo a campo consciente de arrays y las instancias no son hashables.
| Campo | Contenido |
|---|---|
r_star, w_star |
Tasa de interés y salario que vacían el mercado. |
K_star, Ks_star, Kd_star |
Capital de equilibrio (\(K^\ast = K^s(r^\ast)\)), oferta de los hogares y demanda de las empresas. |
L_star, Y_star |
Trabajo efectivo \(\sum_j e_j \pi_j\) y producto \(Y = (K^d)^\alpha L^{1-\alpha}\). |
excess_capital |
\(K^s(r^\ast) - K^d(r^\ast)\). |
hjb_solution |
La HJBSolution en \(r^\ast\), \(w^\ast\). |
g_dist |
Alias de hjb_solution.g_dist (una densidad). |
converged, n_iter_ge, elapsed |
True solo cuando Brent convergió y \(\lvert K^s - K^d \rvert <\) tol_ge; iteraciones de Brent sumadas sobre las pasadas de acotamiento; segundos transcurridos en toda la resolución del equilibrio general. |
Métodos: summary() (once filas: precios, oferta y demanda de capital, error de vaciado, trabajo, producto, convergencia, iteraciones, tiempo transcurrido), to_frame() (delega en hjb_solution.to_frame(), de modo que tabula la solución del hogar en el equilibrio), to_markdown(), to_latex(), to_typst() (la tabla resumen, como en el Ejemplo 4.3) y plot() (delega en hjb_solution.plot(), de modo que muestra los tres paneles del hogar, no la distribución ni el diagrama del mercado de capital).
7. Advertencias y limitaciones
g_distes una densidad, no un vector de masas nodales. Multiplique por los pesos de ancho de celda antes de sumar: la masa nodal es \(g_{i,j} w_i\), y \(w_i = \Delta a\) solo en una malla uniforme. Los ejemplos de esta página hacen exactamente eso. Esto importa sobre todo en mallas no uniformes: en la malla cuadráticanp.linspace(0, 1, 100)**2 * 30la oferta de capital ponderada es \(5.9155\), y leerg_distcomo un vector de masas daría un resultado equivocado por el ancho local de la celda en cada nodo.- El número de iteraciones depende de
Delta. ConDelta=1elmax_iter=100por defecto no basta (Sección 4.5).convergedes el campo que hay que comprobar; el solucionador no emite advertencias. - El presupuesto de iteraciones de Brent es por pasada.
max_iter_geacota cada pasada de acotamiento, yn_iter_gereporta la suma sobre las pasadas, de modo que el número total de resoluciones del hogar puede superarmax_iter_gecuandotol_gefuerza un reacotamiento. Las soluciones HJB se almacenan en caché por \(r\), así que repetir una tasa tentativa no cuesta nada. mass_residualse mide tras la renormalización y por ello es del orden de \(10^{-16}\) con independencia de la exactitud de la resolución lineal; certifica la normalización, no el residuo de la KFE. CalculeA_generator.T @ (g_dist * w[:, None]).ravel(order="F")—el generador actúa sobre masas nodales— si desea este último.- El orden de los estados es por columnas.
A_generatorindexa el estado \((i, j)\) como \(j N_a + i\); reorganice los vectores conorder="F", como hacesolve_kfe_achdou. keys()es más estrecho quein. La pertenencia y el subíndice cubren los quince campos de la dataclass, perokeys()(y por tantodict(sol),**soly la iteración) enumera solo las doce claves heredadas, de modo quedict(sol)omiteg_dist,A_generatorymass_residual. Léalos por atributo.- Truncamiento en
a_max. La frontera superior es una restricción de estado artificial. Si una masa visible deg_distse acumula en los últimos nodos, aumentea_max(el valor por defecto 30 es holgado para la calibración por defecto: la riqueza media es de alrededor de 6). - La interfaz y la economía cambiaron en 3.4.0.
solve_hjb_achdoudevolvía un diccionario plano con siete claves y usabamax_iter=500,tol=1e-7y un esquema explícito; 3.4.0 devuelveHJBSolution, usa por defectomax_iter=100,tol=1e-8y el esquema implícito, y hace que todos los argumentos posteriores atolsean exclusivamente por nombre (keyword-only). También añade el término de conmutación de Poisson del ingreso que el solucionador explícito omitía, de modo que el valor y el consumo para argumentos idénticos difieren de forma sustancial respecto a 3.3.0 (Sección 2.2). El código que usaba el diccionario sigue funcionando; el código que dependía de los valores por defecto antiguos debe pasarlos explícitamente, y el código que necesita las cifras antiguas debe pasarA_z=np.zeros((Ne, Ne))concompute_kfe=False. - NumPy/SciPy puros, float64, sin aceleradores, sin red. El generador se ensambla en bucles de Python sobre
Na * Neestados y se factoriza con SuperLU; unos pocos miles de estados se resuelven en milisegundos, pero el costo de ensamblaje crece linealmente conNa * Ne**2. El módulo importa solo NumPy, SciPy, pandas ypuremacro.reports—matplotlib se importa de forma diferida dentro deplot, así queimport puremacro.vfidejamatplotlibfuera desys.modules— y todo se ejecuta sin conexión. Esa primera importación sigue tardando segundos (2.4 s en la ejecución en frío en la que se basa esta página), mucho más que cualquier resolución de esta página.
Referencias
- Achdou, Y., Han, J., Lasry, J.-M., Lions, P.-L., & Moll, B. (2022). "Income and Wealth Distribution in Macroeconomics: A Continuous-Time Approach." The Review of Economic Studies, 89(1), 45–86.
- Aiyagari, S. R. (1994). "Uninsured Idiosyncratic Risk and Aggregate Saving." The Quarterly Journal of Economics, 109(3), 659–684.
- Barles, G., & Souganidis, P. E. (1991). "Convergence of approximation schemes for fully nonlinear second order equations." Asymptotic Analysis, 4(3), 271–283.
- Huggett, M. (1993). "The risk-free rate in heterogeneous-agent incomplete-insurance economies." Journal of Economic Dynamics and Control, 17(5–6), 953–969.
- Young, E. R. (2010). "Solving the incomplete markets model with aggregate uncertainty using the Krusell-Smith algorithm and non-stochastic simulations." Journal of Economic Dynamics and Control, 34(1), 36–41.