From 103ea110399032d0abaab491c4bfd5e62507acb7 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:31:58 +0800 Subject: [PATCH] wvs map: colour model stars by lab family (region-hued), declutter labels Models are now stars coloured by lab family instead of one Economist red, hue chosen to echo the lab's home region: Chinese labs warm (qwen orange, deepseek pink) near the East-Asia red, US labs cool (claude purple, gpt blue, gemini/gemma sea blue, grok indigo, llama steel), Europe green (mistral). Legend keys each family. Also drop the ' (rated)' tag from on-map labels and Nigeria from the always-on landmarks (Egypt already anchors the African-Islamic corner). Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com> --- scripts/wvs_map.py | 5 +++- src/tinymfv/maps.py | 58 +++++++++++++++++++++++++++++++++----------- src/tinymfv/zones.py | 6 +++-- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/scripts/wvs_map.py b/scripts/wvs_map.py index 19706d1..bda4abc 100644 --- a/scripts/wvs_map.py +++ b/scripts/wvs_map.py @@ -286,10 +286,13 @@ def main() -> None: # signposts through the human median, 4 auto-selected zone hulls, textalloc labels, model stars. _, emph = zones_for(countries) # Title + caption live in the README (nicer voice, editable), not baked into the figure. + # Drop the " (rated)" readout tag from the on-map labels (the cache/CI-table keep it) -- the map is + # crowded and every model here is rated, so the tag adds nothing. + plot_models = {k.replace(" (rated)", ""): v for k, v in models.items()} fig = maps.plot_value_map( "WVS Inglehart-Welzel", countries, P, ("Survival", "Self-expression", "Traditional", "Secular-Rational"), - models=models, emphasize=emph) + models=plot_models, emphasize=emph) fig.savefig(args.out, dpi=200, bbox_inches="tight") logger.info(f"wrote {args.out}") diff --git a/src/tinymfv/maps.py b/src/tinymfv/maps.py index f29f47f..62711c9 100644 --- a/src/tinymfv/maps.py +++ b/src/tinymfv/maps.py @@ -223,9 +223,31 @@ def _pole_signposts(ax, med_x: float, med_y: float, poles: tuple[str, str, str, ax.annotate(xp, xy=(0.994, med_y), xytext=(0.9, med_y), xycoords=tY, arrowprops=awp, **kw) -# Economist convention: every model is the SAME bold red (bigger than the grey society dots), told -# apart by its on-map label, not by colour. Distinct from the muted ZONE_COLORS. +# Economist convention: every model is the SAME bold red, told apart by its label. We keep MODEL_RED +# as the fallback but colour each model STAR by its lab FAMILY, with the hue chosen to echo the lab's +# home region (Chinese labs warm / near the East-Asia red; US labs cool blue-purple; Europe green) so a +# family clusters by colour at a glance, not just by reading labels. -- added by Claude MODEL_RED = "#d0021b" +MODEL_FAMILY_COLORS = { + "deepseek": "#ff6fa3", # DeepSeek (China) -> pink, a lighter East-Asia red + "qwen": "#f28e2b", # Qwen / Alibaba (China) -> orange + "claude": "#7b3fa0", # Anthropic (US) -> purple + "gpt": "#1f77b4", # OpenAI (US) -> blue + "gemini": "#1198a6", # Gemini / Google (US) -> sea blue + "gemma": "#5fc9d3", # Gemma / Google open sibling -> lighter sea blue + "grok": "#3b4cc0", # Grok / xAI (US) -> indigo + "llama": "#4e79a7", # Llama / Meta (US) -> steel blue + "mistral": "#59a14f", # Mistral (France / Europe) -> green +} + + +def model_family_color(name: str) -> str: + """The lab-family colour for a model key (substring match on the family name), MODEL_RED if none.""" + key = name.lower() + for fam, col in MODEL_FAMILY_COLORS.items(): + if fam in key: + return col + return MODEL_RED def plot_value_map(display: str, countries: list[str], P: np.ndarray, @@ -269,15 +291,16 @@ def plot_value_map(display: str, countries: list[str], P: np.ndarray, sx, sy = list(P[:, 0]), list(P[:, 1]) if models: # models carry (x, y[, x_se, y_se]); the CI is NOT drawn -- with a dozen+ models the whisker - # crosses overlap into noise. Uncertainty lives in the companion table (wvs_map save_ci_table), - # which also shows it's item-disagreement (irreducible by N), not sampling noise. + # crosses overlap into noise. Uncertainty lives in the companion table (wvs_map's CI table), + # which also shows it's item-disagreement (irreducible by N), not sampling noise. Each model is + # a STAR coloured by its lab family (model_family_color), and its label takes the same colour. mnames = list(models) mx = np.array([models[k][0] for k in mnames]) my = np.array([models[k][1] for k in mnames]) - ax.scatter(mx, my, s=120, marker="o", c=MODEL_RED, # Economist: bigger red dots - edgecolors="white", linewidths=1.0, zorder=8) + mcols = [model_family_color(k) for k in mnames] + ax.scatter(mx, my, s=230, marker="*", c=mcols, edgecolors="white", linewidths=0.8, zorder=8) tx += list(mx); ty += list(my); txt += mnames - tcol += [MODEL_RED] * len(mnames) + tcol += mcols sx += list(mx); sy += list(my) if steer: bx, by, _ = steer["base"] @@ -300,14 +323,21 @@ def plot_value_map(display: str, countries: list[str], P: np.ndarray, textsize=9, textcolor=tcol, linecolor="#aaa", linewidth=0.6, draw_lines=True) _pole_signposts(ax, med_x, med_y, poles) ax.set_xticks([]); ax.set_yticks([]); ax.set_xlabel(""); ax.set_ylabel("") - if models: # minimal colour legend (red = models, grey = societies) + if models: # legend: one star swatch per lab family present from matplotlib.lines import Line2D - handles = [Line2D([], [], marker="o", linestyle="none", markerfacecolor=MODEL_RED, - markeredgecolor="white", markersize=11, label="AI models"), - Line2D([], [], marker="o", linestyle="none", markerfacecolor="#8f8a80", - markeredgecolor="white", markersize=8, label=f"{len(countries)} societies")] - ax.legend(handles=handles, loc="upper left", fontsize=9, frameon=False, - borderaxespad=0.8, handletextpad=0.3).set_zorder(11) + fams, seen_f = [], set() + for k in mnames: # keep map order, dedupe to one entry per family + low = k.lower() + fam = next((f for f in MODEL_FAMILY_COLORS if f in low), None) + if fam and fam not in seen_f: + seen_f.add(fam) + fams.append((fam, MODEL_FAMILY_COLORS[fam])) + handles = [Line2D([], [], marker="*", linestyle="none", markerfacecolor=col, + markeredgecolor="white", markersize=12, label=fam) for fam, col in fams] + handles.append(Line2D([], [], marker="o", linestyle="none", markerfacecolor="#8f8a80", + markeredgecolor="white", markersize=8, label=f"{len(countries)} societies")) + ax.legend(handles=handles, loc="upper left", fontsize=8, frameon=False, + borderaxespad=0.6, handletextpad=0.3, labelspacing=0.3, ncol=2).set_zorder(11) # Title + caption are OFF by default -- the README carries the headline + sources (nicer voice # there than baked jargon). Pass title/note only for a standalone figure. if title: diff --git a/src/tinymfv/zones.py b/src/tinymfv/zones.py index 66ca270..bbbc563 100644 --- a/src/tinymfv/zones.py +++ b/src/tinymfv/zones.py @@ -74,9 +74,11 @@ _CANON = { "(nu": None, # corrupt big5 row (n=369); country unidentifiable from the aggregate CSV } -# The named outliers on the Economist chart, bolded on our maps where present. +# The named outliers on the Economist chart, bolded on our maps where present. Nigeria dropped: Egypt +# already anchors the bottom-left corner (it's the corner-outlier auto-label), so both crowds the +# African-Islamic corner. -- Claude ECONOMIST_OUTLIERS = {"China", "South Korea", "United States", "Great Britain", "Japan", - "Nigeria", "Pakistan", "Sweden"} + "Pakistan", "Sweden"} # Coarser macro-zones for the maps. The nine fine IW zones over-fragment low-dimensional maps: the # English-speaking world and the European religions (Protestant/Catholic/Baltic) don't separate, so