if not happiness_ts.empty and not gdp_ts.empty:
merged_all = happiness_ts.merge(gdp_ts, on=['country_code', 'year'])
# Short-run: 5-year rolling windows
short_run = []
for cc, group in merged_all.groupby('country_code'):
group = group.sort_values('year').dropna()
for i in range(len(group)):
for j in range(i + 1, len(group)):
yr_diff = group.iloc[j]['year'] - group.iloc[i]['year']
if 4 <= yr_diff <= 6: # approximately 5 years
gdp_i = group.iloc[i]['gdp_per_capita_ppp']
gdp_j = group.iloc[j]['gdp_per_capita_ppp']
if gdp_i > 0:
short_run.append({
'country_code': cc,
'delta_log_gdp': np.log(gdp_j) - np.log(gdp_i),
'delta_happiness': (group.iloc[j]['happiness_score']
- group.iloc[i]['happiness_score']),
'yr_diff': yr_diff,
})
break # only closest ~5yr window per start year
short_df = pd.DataFrame(short_run)
if len(short_df) >= 10 and 'delta_df' in dir() and len(delta_df) >= 5:
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Panel 1: Short-run (5-year windows)
ax = axes[0]
ax.scatter(short_df['delta_log_gdp'] * 100,
short_df['delta_happiness'],
alpha=0.3, s=30, color='#4878a8', edgecolors='none')
x_s = (short_df['delta_log_gdp'] * 100).values
y_s = short_df['delta_happiness'].values
m_s = np.isfinite(x_s) & np.isfinite(y_s)
if m_s.sum() > 3:
sl, it, r, p, _ = stats.linregress(x_s[m_s], y_s[m_s])
xf = np.linspace(x_s[m_s].min(), x_s[m_s].max(), 100)
ax.plot(xf, it + sl * xf, color='#c75b5b', linewidth=2)
ax.text(0.05, 0.95, f'r = {r:.3f}, p = {p:.4f}\n'
f'n = {m_s.sum()} windows',
transform=ax.transAxes, fontsize=10, va='top',
bbox=dict(boxstyle='round', facecolor='white',
alpha=0.8))
ax.axhline(0, color='gray', alpha=0.3, linewidth=0.5)
ax.set_xlabel('GDP growth over ~5 years (log %)')
ax.set_ylabel('Happiness change')
ax.set_title('Short-Run (~5 years)\nBusiness-cycle fluctuations')
# Panel 2: Long-run (full period)
ax = axes[1]
colors = [tier_color(c) for c in delta_df['country_code']]
ax.scatter(delta_df['delta_log_gdp'] * 100,
delta_df['delta_happiness'],
c=colors, s=80, zorder=5, edgecolors='white',
linewidth=0.5, alpha=0.85)
x_l = (delta_df['delta_log_gdp'] * 100).values
y_l = delta_df['delta_happiness'].values
m_l = np.isfinite(x_l) & np.isfinite(y_l)
if m_l.sum() > 3:
sl, it, r, p, _ = stats.linregress(x_l[m_l], y_l[m_l])
xf = np.linspace(x_l[m_l].min(), x_l[m_l].max(), 100)
ax.plot(xf, it + sl * xf, color='#c75b5b', linewidth=2)
ax.text(0.05, 0.95, f'r = {r:.3f}, p = {p:.4f}\n'
f'n = {m_l.sum()} countries',
transform=ax.transAxes, fontsize=10, va='top',
bbox=dict(boxstyle='round', facecolor='white',
alpha=0.8))
ax.axhline(0, color='gray', alpha=0.3, linewidth=0.5)
ax.set_xlabel('Cumulative GDP growth (log %)')
ax.set_ylabel('Happiness change')
ax.set_title('Long-Run (~15-19 years)\nSecular growth')
annotate_countries(
ax, (delta_df['delta_log_gdp'] * 100).values,
delta_df['delta_happiness'].values,
delta_df['country_code'].values, fontsize=7)
fig.suptitle('Short-Run vs. Long-Run: Does the Income-Happiness\n'
'Gradient Disappear Over Longer Horizons?',
fontsize=13, y=1.03)
plt.tight_layout()
save_figure(fig, '08_short_vs_long_run')
plt.show()
print(f"\nShort-run (5-year) gradient: pooled across "
f"{len(short_df)} country-windows")
print(f"Long-run (full-period) gradient: {len(delta_df)} countries")
else:
print('Insufficient data for short-run vs. long-run comparison.')
else:
print('Data not available for short-run vs. long-run analysis.')