The Budget Simplex and the Demand Law: Becker (1962) meets β¶
EconIAC Tutorial — Rationality as Temperature¶
In 1962, Gary Becker published a short paper with a startling claim: downward-sloping demand does not require rational maximisation. Random agents, sampling uniformly from a budget set, produce aggregate demand curves with negative slope — because the geometry of the budget simplex does the work, not cognition.
In EconIAC, Becker's result is the $\beta \to 0$ limit of the Gibbs ensemble: at infinite temperature, agents are pure noise, but the budget constraint is still there, and it has the right shape.
This notebook sweeps $\beta$ from $0$ (Becker's random agents) through $\beta^*$ (calibrated from observed choice variance) to $\beta \to \infty$ (classical rational agents), showing that the demand law is a topological invariant of the budget simplex — present at every temperature, sharpening but never changing sign.
Jason Smith's Information Transfer Economics (2020) observes that macro regularities hold even for random agents. We show why: the mutual information $I(p; q)$ between the price signal and the quantity response is $\log Z_\beta - \log Z_0$, always positive, and equal to Smith's transfer index at $\beta = \beta^*$.
The Becker–Smith–EGT triangle in one diagram.
References:
- Becker, G. S. (1962). Irrational Behavior and Economic Theory. Journal of Political Economy, 70(1), 1–13.
- Smith, J. (2020). Information Transfer Economics. informationtransfereconomics.blogspot.com
- Paper 289: Buckley (2026) The Temperature of Rationality — doi:10.5281/zenodo.20234841
# Uncomment to install in Colab or a fresh environment
# !pip install "econiac[tutorials]" "jax[cpu]" matplotlib
import jax
import jax.numpy as jnp
import numpy as np
import matplotlib.pyplot as plt
from functools import partial
from econiac.core.ensemble import gibbs_weights
plt.rcParams.update({'figure.dpi': 120, 'font.size': 11})
jax.config.update('jax_enable_x64', True)
print(f'JAX version: {jax.__version__}')
print(f'Backend: {jax.default_backend()}')
1. The Budget Simplex¶
Two goods. Income $M = 100$. Prices $p_1, p_2 > 0$.
The budget set is: $$\mathcal{B}(p_1, p_2) = \{(q_1, q_2) \geq 0 : p_1 q_1 + p_2 q_2 \leq M\}$$
This is a right triangle — a 2-simplex — in $(q_1, q_2)$ space.
Becker's key observation: if an agent samples uniformly at random from $\mathcal{B}$, the expected quantity of good 1 is the centroid of the triangle along the $q_1$ axis: $$\mathbb{E}[q_1] = \frac{M}{3 p_1}$$
So $\partial \mathbb{E}[q_1] / \partial p_1 < 0$ — the demand curve slopes downward — with no utility function, no preferences, no rationality.
The simplex geometry enforces this. A higher price shrinks the triangle asymmetrically along the $q_1$ axis, pulling the centroid inward.
M = 100.0 # income
p2 = 5.0 # price of good 2 (held fixed)
# Sample N agents uniformly from the budget simplex at a given (p1, p2)
# Uniform on a 2-simplex: sample (u1, u2) ~ Dirichlet(1,1,1), take (u1, u2) share of M/p
def sample_budget_simplex(key, p1: float, n_agents: int = 10_000):
"""
Sample n_agents consumption bundles uniformly from the budget simplex.
Returns q1 quantities (shape n_agents).
"""
# Uniform on triangle {(s1,s2,s3): s1+s2+s3=1, si>=0} via Dirichlet(1,1,1)
# (s1, s2) are budget shares; s3 is unspent (savings)
s = jax.random.dirichlet(key, alpha=jnp.ones(3), shape=(n_agents,))
q1 = s[:, 0] * M / p1
return q1
# Becker's analytical prediction: E[q1] = M / (3 * p1)
def becker_demand(p1: float) -> float:
return M / (3.0 * p1)
# Verify at p1=4
key = jax.random.PRNGKey(42)
p1_base = 4.0
q1_samples = sample_budget_simplex(key, p1_base)
print(f"p1 = {p1_base}, p2 = {p2}, M = {M}")
print(f"Monte Carlo mean q1: {float(q1_samples.mean()):.4f}")
print(f"Becker analytical: {becker_demand(p1_base):.4f}")
print(f"Agreement: {abs(float(q1_samples.mean()) - becker_demand(p1_base)) < 0.5}")
# Visualise the budget simplex at two prices
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
p1_values_vis = [3.0, 5.0, 8.0]
colors = ['#3f51b5', '#e91e63', '#4caf50']
for ax, p1_v, col in zip(axes, p1_values_vis, colors):
key_v = jax.random.PRNGKey(int(p1_v * 10))
n_plot = 2000
s = jax.random.dirichlet(key_v, alpha=jnp.ones(3), shape=(n_plot,))
q1_v = s[:, 0] * M / p1_v
q2_v = s[:, 1] * M / p2
ax.scatter(q1_v, q2_v, alpha=0.15, s=4, color=col)
# Budget line
q1_max = M / p1_v
q2_max = M / p2
ax.plot([q1_max, 0], [0, q2_max], color='black', linewidth=1.8,
label='Budget line')
# Centroid
cent_q1 = becker_demand(p1_v)
cent_q2 = M / (3.0 * p2)
ax.scatter([cent_q1], [cent_q2], color='red', s=120, zorder=5,
label=f'Centroid ($\\bar{{q}}_1$={cent_q1:.1f})')
ax.set_xlim(-0.5, 40)
ax.set_ylim(-0.5, 24)
ax.set_xlabel('$q_1$ (good 1 quantity)')
ax.set_ylabel('$q_2$ (good 2 quantity)')
ax.set_title(f'$p_1 = {p1_v}$', fontsize=11)
ax.legend(fontsize=9)
plt.suptitle(
'Budget simplex at three prices — centroid (red) moves left as $p_1$ rises\n'
'No utility function. No preferences. The geometry does the work.',
fontsize=11
)
plt.tight_layout()
plt.show()
print("The budget simplex shrinks along the q1 axis as p1 rises.")
print("The centroid — mean demand for a uniform random agent — moves left.")
print("This IS Becker's demand law: geometry, not preferences.")
The demand law is a topological invariant of the budget simplex. It holds for any agent that samples from the budget set — rational, random, or anything in between.
The question is not whether the demand law holds but how sharply it holds. That sharpness is exactly what $\beta$ measures.
2. Introducing β — The Gibbs Ensemble on the Budget Simplex¶
Rather than sampling uniformly, a Gibbs agent samples from the budget simplex with probability proportional to $e^{\beta U(q)}$, where $U(q)$ is utility.
We use the simplest utility: Cobb-Douglas $U(q_1, q_2) = \alpha \log q_1 + (1-\alpha) \log q_2$ with $\alpha = 0.5$ (equal preference). The Marshallian optimum is at $q_1^* = \alpha M / p_1 = M / (2 p_1)$.
At $\beta = 0$: Gibbs weight $= 1$ everywhere → uniform sampling → Becker's centroid $M/(3p_1)$.
At $\beta \to \infty$: Gibbs weight $\to \delta(q - q^*)$ → hard argmax → $M/(2p_1)$.
Both slopes are negative. The demand law is preserved at every $\beta$.
alpha = 0.5 # Cobb-Douglas preference parameter
def cobb_douglas_utility(q1: jax.Array, q2: jax.Array) -> jax.Array:
"""Cobb-Douglas U = alpha*log(q1) + (1-alpha)*log(q2), defined where q>0."""
q1_safe = jnp.maximum(q1, 1e-8)
q2_safe = jnp.maximum(q2, 1e-8)
return alpha * jnp.log(q1_safe) + (1.0 - alpha) * jnp.log(q2_safe)
def gibbs_demand_q1(key, p1: float, beta: float, n_agents: int = 20_000) -> float:
"""
Monte Carlo Gibbs ensemble demand for good 1.
Sample uniformly from the budget simplex, reweight by exp(beta * U).
This is importance sampling: E_Gibbs[q1] = E_uniform[q1 * exp(beta*U)] / Z.
"""
s = jax.random.dirichlet(key, alpha=jnp.ones(3), shape=(n_agents,))
q1 = s[:, 0] * M / p1
q2 = s[:, 1] * M / p2
U = cobb_douglas_utility(q1, q2)
# Numerically stable Gibbs weights via log-sum-exp
log_w = beta * U
log_w_stable = log_w - log_w.max()
w = jnp.exp(log_w_stable)
w = w / w.sum()
return float((w * q1).sum())
# Analytical Marshallian demand (beta → ∞ limit)
def marshallian_demand(p1: float) -> float:
return alpha * M / p1
# Test at p1=4, several beta values
print(f"Demand for good 1 at p1={p1_base}, M={M}, p2={p2}:")
print(f" Becker (β=0, analytical): {becker_demand(p1_base):.4f}")
for beta_test in [0.01, 0.1, 1.0, 5.0, 20.0]:
key_t = jax.random.PRNGKey(int(beta_test * 100))
d = gibbs_demand_q1(key_t, p1_base, beta_test)
print(f" Gibbs (β={beta_test:5.2f}): {d:.4f}")
print(f" Marshallian (β→∞): {marshallian_demand(p1_base):.4f}")
# Aggregate demand curve at three β values
p1_range = np.linspace(2.0, 12.0, 30)
betas_plot = [0.001, 1.0, 10.0]
beta_labels = ['β→0 (Becker: random agents)', 'β=1 (intermediate)', 'β=10 (near-rational)']
beta_colors = ['#3f51b5', '#e91e63', '#4caf50']
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax = axes[0]
# Analytical Becker line
ax.plot(p1_range, [becker_demand(p) for p in p1_range],
color='#3f51b5', linewidth=2, linestyle='--', label='β→0 (Becker, analytical)')
# Marshallian demand
ax.plot(p1_range, [marshallian_demand(p) for p in p1_range],
color='black', linewidth=2, linestyle=':', label='β→∞ (Marshallian rational)')
# Gibbs at β=1
demands_beta1 = []
for i, p1_v in enumerate(p1_range):
key_i = jax.random.PRNGKey(i + 1000)
demands_beta1.append(gibbs_demand_q1(key_i, p1_v, 1.0))
ax.plot(p1_range, demands_beta1, color='#e91e63', linewidth=2, label='β=1 (intermediate)')
ax.set_xlabel('Price $p_1$')
ax.set_ylabel('Mean quantity demanded $\\mathbb{E}[q_1]$')
ax.set_title('Demand curve at different β values\n(negative slope preserved at ALL temperatures)')
ax.legend(fontsize=9)
ax.set_xlim(2, 12)
# Right panel: β-sweep of elasticity at a fixed price
# Numerical elasticity: d(log q1)/d(log p1) at p1=4
beta_sweep = np.logspace(-2, 2, 40)
p1_lo, p1_hi = 3.9, 4.1
elasticities = []
for beta_v in beta_sweep:
key_lo = jax.random.PRNGKey(int(beta_v * 1000))
key_hi = jax.random.PRNGKey(int(beta_v * 1000) + 1)
d_lo = gibbs_demand_q1(key_lo, p1_lo, beta_v)
d_hi = gibbs_demand_q1(key_hi, p1_hi, beta_v)
# d(log q)/d(log p) ≈ (Δq/q) / (Δp/p)
q_mid = (d_lo + d_hi) / 2
p_mid = (p1_lo + p1_hi) / 2
elas = ((d_hi - d_lo) / q_mid) / ((p1_hi - p1_lo) / p_mid)
elasticities.append(elas)
becker_elas = -1.0 # M/(3p) → d(log q)/d(log p) = -1
marshallian_elas = -1.0 # alpha*M/p → d(log q)/d(log p) = -1 (both = -1 for this utility!)
ax2 = axes[1]
ax2.semilogx(beta_sweep, elasticities, color='#3f51b5', linewidth=2)
ax2.axhline(-1.0, color='grey', linestyle='--', linewidth=1.2,
label='Theoretical elasticity = −1')
ax2.axhline(0, color='black', linewidth=0.6)
ax2.set_xlabel('β (log scale)')
ax2.set_ylabel('Price elasticity $d(\\log q_1)/d(\\log p_1)$')
ax2.set_title('Price elasticity across the full β range\n(negative at ALL temperatures — the topological invariant)')
ax2.legend(fontsize=9)
ax2.set_ylim(-2.0, 0.5)
plt.suptitle(
'The demand law is a topological invariant: negative slope at every β, from random to rational',
fontsize=11
)
plt.tight_layout()
plt.show()
print(f"Becker prediction: elasticity = {becker_elas:.2f} at β=0")
print(f"Marshallian prediction: elasticity = {marshallian_elas:.2f} at β→∞")
print(f"Both = −1 for Cobb-Douglas with equal shares. This is a known result.")
print(f"The key insight: the sign never changes. The budget simplex guarantees it.")
3. Calibrating β* from Observed Choice Variance¶
In the Gibbs ensemble, the variance of the allocation across agents is: $$\text{Var}_{\beta}[q_1] = -\frac{\partial^2 \log Z_\beta}{\partial(\beta p_1)^2}$$
This is the susceptibility — large at low β (random agents, high variance), small at high β (rational agents, concentrated near the optimum).
Given cross-sectional data on household expenditure shares, we can invert this to recover $\beta^*$ — the temperature of the observed population.
We simulate a dataset of 500 households, generate their Gibbs allocations at a true $\beta^* = 2.5$, then recover $\beta^*$ from the observed variance using a simple moment estimator.
beta_true = 2.5
n_households = 500
p1_obs = 4.0
# Generate observed household allocations at beta_true
key_obs = jax.random.PRNGKey(99)
s_obs = jax.random.dirichlet(key_obs, alpha=jnp.ones(3), shape=(n_households,))
q1_obs_raw = s_obs[:, 0] * M / p1_obs
q2_obs_raw = s_obs[:, 1] * M / p2
U_obs = cobb_douglas_utility(q1_obs_raw, q2_obs_raw)
log_w_obs = beta_true * U_obs
log_w_obs_stable = log_w_obs - log_w_obs.max()
w_obs = jnp.exp(log_w_obs_stable)
w_obs = w_obs / w_obs.sum()
# Sample households according to Gibbs weights (multinomial resample)
key_samp = jax.random.PRNGKey(100)
indices = jax.random.choice(key_samp, n_households, shape=(n_households,), replace=True, p=w_obs)
q1_observed = q1_obs_raw[indices]
obs_mean = float(q1_observed.mean())
obs_var = float(q1_observed.var())
print(f"True β* = {beta_true}")
print(f"Observed mean q1: {obs_mean:.3f} (Marshallian: {marshallian_demand(p1_obs):.3f})")
print(f"Observed variance: {obs_var:.3f}")
# Moment estimator: variance decreases with β
# We use a simple grid search over β to match observed variance
beta_grid = jnp.linspace(0.1, 10.0, 200)
def model_variance(beta_v: float, key_v, n: int = 5000) -> float:
key_mv = jax.random.PRNGKey(int(beta_v * 1000))
s = jax.random.dirichlet(key_mv, alpha=jnp.ones(3), shape=(n,))
q1 = s[:, 0] * M / p1_obs
q2 = s[:, 1] * M / p2
U = cobb_douglas_utility(q1, q2)
log_w = beta_v * U
log_w = log_w - log_w.max()
w = jnp.exp(log_w) / jnp.exp(log_w).sum()
mean_q1 = (w * q1).sum()
var_q1 = (w * (q1 - mean_q1)**2).sum()
return float(var_q1)
# Grid search
variances = [model_variance(float(b), None) for b in beta_grid]
best_idx = np.argmin(np.abs(np.array(variances) - obs_var))
beta_hat = float(beta_grid[best_idx])
print(f"\nCalibrated β* (MoM from variance): {beta_hat:.2f} (true: {beta_true})")
# Visualise: variance vs β, with observed variance and calibrated β*
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax = axes[0]
ax.plot(beta_grid, variances, color='#3f51b5', linewidth=2)
ax.axhline(obs_var, color='#e53935', linestyle='--', linewidth=1.5,
label=f'Observed variance = {obs_var:.2f}')
ax.axvline(beta_hat, color='#e91e63', linestyle=':', linewidth=1.5,
label=f'Calibrated β* = {beta_hat:.2f}')
ax.axvline(beta_true, color='black', linestyle='--', linewidth=1.2,
label=f'True β* = {beta_true}')
ax.set_xlabel('β')
ax.set_ylabel('Var[q₁] — Gibbs ensemble variance')
ax.set_title('Calibrating β* from cross-sectional choice variance\n(moment estimator)')
ax.legend(fontsize=9)
# Right: household allocation histogram at β=0, β=β*, β→∞
ax2 = axes[1]
for beta_v, col, label in [
(0.01, '#3f51b5', f'β=0 (Becker: random)'),
(beta_hat, '#e91e63', f'β*={beta_hat:.1f} (calibrated)'),
(20.0, '#4caf50', 'β→∞ (Marshallian rational)'),
]:
key_h = jax.random.PRNGKey(int(beta_v * 77 + 1))
s_h = jax.random.dirichlet(key_h, alpha=jnp.ones(3), shape=(10_000,))
q1_h = s_h[:, 0] * M / p1_obs
q2_h = s_h[:, 1] * M / p2
U_h = cobb_douglas_utility(q1_h, q2_h)
log_w_h = beta_v * U_h
log_w_h = log_w_h - log_w_h.max()
w_h = jnp.exp(log_w_h) / jnp.exp(log_w_h).sum()
key_s2 = jax.random.PRNGKey(int(beta_v * 33))
idx_h = jax.random.choice(key_s2, 10_000, shape=(2000,), replace=True, p=w_h)
ax2.hist(np.array(q1_h[idx_h]), bins=50, alpha=0.45, color=col, label=label, density=True)
ax2.axvline(marshallian_demand(p1_obs), color='black', linestyle='--', linewidth=1.5,
label=f'Marshallian q* = {marshallian_demand(p1_obs):.1f}')
ax2.set_xlabel('q₁ (quantity of good 1)')
ax2.set_ylabel('Density')
ax2.set_title('Distribution of household allocations\n(narrows as β increases)')
ax2.legend(fontsize=9)
plt.suptitle(
'β* calibration: variance identifies the temperature of the observed population',
fontsize=11
)
plt.tight_layout()
plt.show()
print(f"At β=0: allocation is uniform → mean = M/(3p₁) = {becker_demand(p1_obs):.2f} (Becker)")
print(f"At β*={beta_hat:.1f}: allocation concentrates near M/(2p₁) = {marshallian_demand(p1_obs):.2f}")
print(f"At β→∞: delta mass at Marshallian optimum")
4. Smith's Information Transfer Index as a Function of β¶
Jason Smith's Information Transfer Economics (2020) observes that macro regularities hold even when agents behave randomly. His key object is the information transfer index $\kappa$, which measures how much of the supply-side information gets transmitted to the demand side.
In the Gibbs framework, $\kappa$ is identified with the normalised mutual information between the price signal $p$ and the quantity response $q$:
$$I_\beta(p; q) = \log Z_\beta - \log Z_0 = \log Z_\beta$$
(since $Z_0 = 1$ when utilities are normalised). This is always non-negative — the price signal always conveys some information — and increases monotonically with $\beta$.
At $\beta = \beta^*$, this gives Smith's information transfer index as an explicit function of the calibrated temperature. Two independent estimators of the same $\beta^*$:
- From choice variance (previous section)
- From the price-quantity mutual information (this section)
They should agree. This is a falsifiable prediction of the Becker–Smith–EGT triangle.
def log_partition(key, p1: float, beta: float, n: int = 30_000) -> float:
"""Monte Carlo log Z_β = log E_uniform[exp(β U)] via importance sampling."""
s = jax.random.dirichlet(key, alpha=jnp.ones(3), shape=(n,))
q1 = s[:, 0] * M / p1
q2 = s[:, 1] * M / p2
U = cobb_douglas_utility(q1, q2)
# log Z = log E[exp(beta * U)] = logsumexp(beta*U) - log(n)
log_z = jax.scipy.special.logsumexp(beta * U) - jnp.log(float(n))
return float(log_z)
# Smith's transfer index I(p;q) = log Z_β (with Z_0 = 1 at β=0)
# At β=0: log Z_0 ≈ log(mean of exp(0)) = 0 — consistent
beta_sweep2 = np.logspace(-2, 1.5, 50)
p1_vals_smith = [3.0, 5.0, 8.0]
smith_colors = ['#3f51b5', '#e91e63', '#4caf50']
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax = axes[0]
for p1_s, col in zip(p1_vals_smith, smith_colors):
smith_index = []
# log Z at β=0 baseline (uniform)
key_base = jax.random.PRNGKey(77)
logZ_0 = log_partition(key_base, p1_s, 0.001)
for i, beta_v in enumerate(beta_sweep2):
key_sv = jax.random.PRNGKey(i + int(p1_s * 10))
logZ_b = log_partition(key_sv, p1_s, float(beta_v))
smith_index.append(logZ_b - logZ_0)
ax.semilogx(beta_sweep2, smith_index, color=col, linewidth=2, label=f'$p_1 = {p1_s}$')
ax.axvline(beta_hat, color='black', linestyle='--', linewidth=1.5,
label=f'β* = {beta_hat:.1f} (calibrated)')
ax.set_xlabel('β (log scale)')
ax.set_ylabel('$\\log Z_\\beta - \\log Z_0 = I(p;q)$')
ax.set_title("Smith's information transfer index = log Z_β\n(always positive; increases with β)")
ax.legend(fontsize=9)
# Right: at β*, show how I(p;q) varies with price — this is the demand signal strength
ax2 = axes[1]
p1_range2 = np.linspace(2.0, 12.0, 30)
key_base2 = jax.random.PRNGKey(55)
logZ0_ref = log_partition(key_base2, 4.0, 0.001)
smith_at_betastar = []
for i, p1_v in enumerate(p1_range2):
key_sv2 = jax.random.PRNGKey(i + 500)
logZ_b2 = log_partition(key_sv2, float(p1_v), beta_hat)
smith_at_betastar.append(logZ_b2 - logZ0_ref)
ax2.plot(p1_range2, smith_at_betastar, color='#e91e63', linewidth=2)
ax2.axvline(p1_obs, color='black', linestyle='--', linewidth=1.2,
label=f'Reference price $p_1 = {p1_obs}$')
ax2.set_xlabel('Price $p_1$')
ax2.set_ylabel('$I(p;q)$ at $\\beta^*$')
ax2.set_title('Information transfer index at β* vs price\n(positive everywhere: signal always transmitted)')
ax2.legend(fontsize=9)
plt.suptitle(
"Smith's transfer index = log Z_β: EGT makes it explicit and β-parameterised",
fontsize=11
)
plt.tight_layout()
plt.show()
print("Smith observed: macro regularities hold even for random agents.")
print("EGT explains why: I(p;q) = log Z_β > 0 at ALL β — the budget constraint")
print("ensures the price signal is always transmitted with the correct sign.")
print("β* is the parameter that quantifies HOW MUCH signal is transmitted.")
5. The Becker–Smith–EGT Triangle¶
Let us now assemble the complete picture.
# The unifying diagram: three regimes, one β-axis
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
beta_axis = np.logspace(-2, 2, 80)
p1_fixed = 4.0
# 1. Mean demand vs β
mean_demands = []
for i, beta_v in enumerate(beta_axis):
key_i = jax.random.PRNGKey(i + 200)
mean_demands.append(gibbs_demand_q1(key_i, p1_fixed, float(beta_v), n_agents=8000))
ax0 = axes[0]
ax0.semilogx(beta_axis, mean_demands, color='#3f51b5', linewidth=2)
ax0.axhline(becker_demand(p1_fixed), color='#3f51b5', linestyle='--', linewidth=1.2,
label=f'Becker: {becker_demand(p1_fixed):.2f}')
ax0.axhline(marshallian_demand(p1_fixed), color='black', linestyle='--', linewidth=1.2,
label=f'Marshallian: {marshallian_demand(p1_fixed):.2f}')
ax0.axvline(beta_hat, color='#e53935', linestyle=':', linewidth=1.5,
label=f'β*={beta_hat:.1f}')
ax0.set_xlabel('β (log scale)')
ax0.set_ylabel('Mean $q_1$')
ax0.set_title('Mean demand: Becker → β* → Marshallian')
ax0.legend(fontsize=8)
# 2. Variance vs β (susceptibility)
ax1 = axes[1]
ax1.semilogx(beta_grid, variances, color='#e91e63', linewidth=2)
ax1.axhline(obs_var, color='#e53935', linestyle='--', linewidth=1.2,
label=f'Observed variance')
ax1.axvline(beta_hat, color='#e53935', linestyle=':', linewidth=1.5,
label=f'β*={beta_hat:.1f}')
ax1.set_xlabel('β (log scale)')
ax1.set_ylabel('Var[$q_1$]')
ax1.set_title('Choice variance (susceptibility)\n→ identifies β*')
ax1.legend(fontsize=8)
# 3. Smith index vs β
smith_ref_vals = []
key_ref3 = jax.random.PRNGKey(66)
logZ0_3 = log_partition(key_ref3, p1_fixed, 0.001)
for i, beta_v in enumerate(beta_axis):
key_i3 = jax.random.PRNGKey(i + 700)
logZ_i = log_partition(key_i3, p1_fixed, float(beta_v))
smith_ref_vals.append(logZ_i - logZ0_3)
ax2 = axes[2]
ax2.semilogx(beta_axis, smith_ref_vals, color='#4caf50', linewidth=2)
ax2.axvline(beta_hat, color='#e53935', linestyle=':', linewidth=1.5,
label=f'β*={beta_hat:.1f}')
ax2.set_xlabel('β (log scale)')
ax2.set_ylabel('$I(p;q) = \\log Z_\\beta - \\log Z_0$')
ax2.set_title("Smith's transfer index\n→ always positive, β* is calibrated value")
ax2.legend(fontsize=8)
plt.suptitle(
'The Becker–Smith–EGT triangle: β interpolates continuously from random to rational agents\n'
'Left: mean demand | Centre: choice variance | Right: information transfer index',
fontsize=10
)
plt.tight_layout()
plt.savefig('becker_smith_egt.png', dpi=150, bbox_inches='tight')
plt.show()
Summary¶
| Result | Author | β regime | EGT statement |
|---|---|---|---|
| Demand law holds for random agents | Becker (1962) | β → 0 | Topological invariant of budget simplex; centroid at M/(3p₁) |
| Macro regularities from info constraints | Smith (2020) | β = β* | I(p;q) = log Z_β > 0 at all β; β* calibrated from choice variance |
| Classical rational agent | Marshall (1890) | β → ∞ | Hard argmax; demand at M/(2p₁) for equal Cobb-Douglas |
| β* calibration from choice variance | New | — | MoM estimator: match Var[q₁] to cross-section data |
| β* from information transfer | New | — | Match I(p;q) = log Z_β to price-quantity mutual information |
What Becker proved¶
The demand law is a topological invariant of the budget simplex. It does not require rationality — it requires only that agents cannot spend more than their income.
What Smith added¶
Macro regularities are not about rationality but about information channels. The price signal carries information that is transmitted to the quantity response regardless of agent cognition. The channel capacity is what determines the strength of the relationship.
What EGT adds¶
The explicit bridge: β parameterises the entire continuum from Becker's random agents to Marshall's rational ones. Both are special cases of the Gibbs ensemble.
Smith's index made explicit: I(p;q) = log Z_β is computable from first principles, differentiable with respect to β, and calibratable from data.
**Two independent estimators of β***: choice variance and mutual information should agree — a falsifiable prediction.
Differentiability:
jax.gradthrough the entire chain — from prices to allocations to welfare — is now possible at every β.
Becker (1962) showed the demand law doesn't need rationality. Smith (2020) showed macro regularities don't need it either. EGT shows why: they are both consequences of the budget simplex geometry at the β→0 limit of the Gibbs ensemble. β tells you how far from that random-agent limit the observed population actually sits.*