if not hours_snap.empty:
wb_data = health_social.get_wellbeing_data(year=2019)
if not wb_data.empty and 'gini_disp' in wb_data.columns:
merged = hours_snap.merge(
wb_data[['country_code', 'gini_disp', 'gdp_per_capita_ppp']],
on='country_code', how='inner')
merged = merged.dropna(subset=['gini_disp', 'hours_per_worker',
'gdp_per_capita_ppp'])
if len(merged) >= 8:
hours = merged['hours_per_worker'].values
gini = merged['gini_disp'].values
log_gdp = np.log(merged['gdp_per_capita_ppp'].values)
codes = merged['country_code'].values
mask = np.isfinite(hours) & np.isfinite(gini) & np.isfinite(log_gdp)
fig, axes = plt.subplots(1, 2, figsize=(14, 7))
# Panel 1: Hours vs Gini, controlling for log GDP
ax = axes[0]
sl_xz, it_xz, _, _, _ = stats.linregress(log_gdp[mask], gini[mask])
resid_gini = gini[mask] - (it_xz + sl_xz * log_gdp[mask])
sl_yz, it_yz, _, _, _ = stats.linregress(log_gdp[mask], hours[mask])
resid_hours_1 = hours[mask] - (it_yz + sl_yz * log_gdp[mask])
colors = [tier_color(c) for c in codes[mask]]
ax.scatter(resid_gini, resid_hours_1, c=colors, s=120, zorder=5,
edgecolors='white', linewidth=1, alpha=0.85)
sl_p, it_p, r_p, p_p, _ = stats.linregress(resid_gini, resid_hours_1)
xf = np.linspace(resid_gini.min(), resid_gini.max(), 100)
ax.plot(xf, it_p + sl_p * xf, color='gray', linestyle='--',
linewidth=2, alpha=0.6)
ax.text(0.05, 0.95, f'Partial r = {r_p:.3f}, p = {p_p:.3f}',
transform=ax.transAxes, fontsize=10, va='top',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
annotate_countries(ax, resid_gini, resid_hours_1, codes[mask],
fontsize=7)
ax.set_xlabel('Gini (residual, GDP removed)')
ax.set_ylabel('Hours (residual, GDP removed)')
ax.set_title('Inequality effect\n(controlling for income)')
# Panel 2: Hours vs log GDP, controlling for Gini
ax = axes[1]
sl_xz2, it_xz2, _, _, _ = stats.linregress(gini[mask], log_gdp[mask])
resid_lgdp = log_gdp[mask] - (it_xz2 + sl_xz2 * gini[mask])
sl_yz2, it_yz2, _, _, _ = stats.linregress(gini[mask], hours[mask])
resid_hours_2 = hours[mask] - (it_yz2 + sl_yz2 * gini[mask])
ax.scatter(resid_lgdp, resid_hours_2, c=colors, s=120, zorder=5,
edgecolors='white', linewidth=1, alpha=0.85)
sl_p2, it_p2, r_p2, p_p2, _ = stats.linregress(
resid_lgdp, resid_hours_2)
xf = np.linspace(resid_lgdp.min(), resid_lgdp.max(), 100)
ax.plot(xf, it_p2 + sl_p2 * xf, color='gray', linestyle='--',
linewidth=2, alpha=0.6)
ax.text(0.05, 0.95, f'Partial r = {r_p2:.3f}, p = {p_p2:.3f}',
transform=ax.transAxes, fontsize=10, va='top',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
annotate_countries(ax, resid_lgdp, resid_hours_2, codes[mask],
fontsize=7)
ax.set_xlabel('Log GDP (residual, Gini removed)')
ax.set_ylabel('Hours (residual, Gini removed)')
ax.set_title('Income effect\n(controlling for inequality)')
fig.suptitle('Disentangling Inequality and Income Effects on Working Hours',
fontsize=13, y=1.03)
plt.tight_layout()
save_figure(fig, '09_hours_gini_gdp_multivariable')
plt.show()
print(f'Partial r (hours vs Gini | GDP): {r_p:.3f}, p = {p_p:.4f}')
print(f'Partial r (hours vs GDP | Gini): {r_p2:.3f}, p = {p_p2:.4f}')
else:
print(f'Only {len(merged)} countries with all three variables.')
else:
print('Gini data not available.')
else:
print('Working hours data not available.')