# Use a compact feature set with high data coverage
tsne_cols = [
"co2_per_capita", "co2_per_gdp", "coal_co2", "oil_co2", "gas_co2",
"cumulative_co2", "share_global_co2",
]
tsne_cols = [c for c in tsne_cols if c in df_latest.columns]
df_tsne = df_latest[["country"] + tsne_cols].copy()
df_tsne = df_tsne.replace([np.inf, -np.inf], np.nan)
# Require at least co2_per_capita to be present
df_tsne = df_tsne.dropna(subset=["co2_per_capita"]).reset_index(drop=True)
# Fill remaining NaN with column median (so we don't lose countries)
for c in tsne_cols:
med = df_tsne[c].median()
df_tsne[c] = df_tsne[c].fillna(med if pd.notna(med) else 0)
print(f"t-SNE using {len(df_tsne)} countries × {len(tsne_cols)} features")
X = df_tsne[tsne_cols].values.astype(np.float64)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Assign income groups by GDP per capita quartiles
gdp_vals = df_latest.set_index("country")["gdp_per_capita"]
df_tsne["gdp_pc"] = df_tsne["country"].map(gdp_vals)
q25, q50, q75 = df_tsne["gdp_pc"].quantile([0.25, 0.50, 0.75])
def income_group(g):
if pd.isna(g): return "Unknown"
if g < q25: return "Low Income"
if g < q50: return "Lower-Middle"
if g < q75: return "Upper-Middle"
return "High Income"
df_tsne["income_group"] = df_tsne["gdp_pc"].apply(income_group)
# Run t-SNE
perplexity = min(30, len(df_tsne) - 1)
tsne = TSNE(n_components=2, perplexity=perplexity, random_state=42,
learning_rate="auto", init="pca", max_iter=1500)
embedding = tsne.fit_transform(X_scaled)
df_tsne["x"] = embedding[:, 0]
df_tsne["y"] = embedding[:, 1]
# Plot
group_colours = {
"Low Income": "#2A9D8F",
"Lower-Middle": "#E9C46A",
"Upper-Middle": "#F4A261",
"High Income": "#E63946",
"Unknown": "#999999",
}
highlight = [
"United States", "China", "India", "Brazil", "Russia", "Germany",
"United Kingdom", "Japan", "Saudi Arabia", "South Africa",
"Nigeria", "Australia", "France", "Indonesia", "Canada",
]
fig, ax = plt.subplots(figsize=(14, 10))
for grp, colour in group_colours.items():
mask = df_tsne["income_group"] == grp
ax.scatter(df_tsne.loc[mask, "x"], df_tsne.loc[mask, "y"],
c=colour, s=70, alpha=0.65, edgecolors="white",
linewidths=0.5, label=grp, zorder=3)
# Highlight and label major countries
for country in highlight:
row = df_tsne[df_tsne["country"] == country]
if row.empty: continue
ax.scatter(row["x"], row["y"], s=200, facecolors="none",
edgecolors="black", linewidths=1.8, zorder=5)
ax.annotate(country, (row["x"].values[0], row["y"].values[0]),
xytext=(7, 4), textcoords="offset points",
fontsize=8, fontweight="bold", color="#222222",
bbox=dict(boxstyle="round,pad=0.2", fc="white", ec="#CCCCCC", alpha=0.85))
ax.set_xlabel("t-SNE Dimension 1", fontsize=12)
ax.set_ylabel("t-SNE Dimension 2", fontsize=12)
ax.set_title(
f"t-SNE Embedding of Countries by Emission Profile ({latest})\n"
f"Features: {len(tsne_cols)} emission indicators | Coloured by income group",
fontsize=14, fontweight="bold",
)
ax.legend(title="Income Group", fontsize=10, title_fontsize=11,
loc="upper left", framealpha=0.9)
# Remove axis ticks (t-SNE units are not meaningful)
ax.set_xticks([]); ax.set_yticks([])
plt.tight_layout()
plt.savefig(OUT / "DS5_tsne_country_clusters.png")
plt.close()