mirror of
https://github.com/wassname/moral-maps.git
synced 2026-09-09 11:27:22 +08:00
Add Inglehart-Welzel zone hulls + outlier labels to ipsative maps
Echoes the Economist WVS 'Godless hippies' chart: shaded convex-hull blobs per IW cultural zone (inline 2D hull, no scipy dep so the maps extra stays matplotlib-only) and bold-first labels for named outliers. Caller owns the zone taxonomy + name/ISO2 normalizer, fails loud on unmapped countries; the corrupt '(nu' big5 row is explicitly excluded with a warning. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
@@ -35,11 +35,94 @@ matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from loguru import logger
|
||||
|
||||
import tinymfv as T
|
||||
from tinymfv import get_instrument
|
||||
|
||||
ORDINAL = ["mfq2", "big5", "16pf", "humor_styles"]
|
||||
|
||||
# --- Inglehart-Welzel cultural zones (for the map's zone hulls) ---------------------------------
|
||||
# WVS Wave 7 nine-cluster taxonomy. Membership is VALUE-based not geographic, so a handful are
|
||||
# judgment calls: Ireland->English-Speaking, Switzerland->Protestant Europe, Philippines->Latin
|
||||
# America, South Africa/Turkey->African-Islamic, India/Pakistan/Thailand->South Asia. Source: WVS
|
||||
# Findings + en.wikipedia.org/wiki/Inglehart-Welzel_cultural_map_of_the_world. -- added by Claude
|
||||
IW_ZONE = {
|
||||
# English-Speaking
|
||||
"United States": "English-Speaking", "Great Britain": "English-Speaking",
|
||||
"Australia": "English-Speaking", "Canada": "English-Speaking",
|
||||
"New Zealand": "English-Speaking", "Ireland": "English-Speaking",
|
||||
# Protestant Europe
|
||||
"Germany": "Protestant Europe", "Sweden": "Protestant Europe", "Norway": "Protestant Europe",
|
||||
"Denmark": "Protestant Europe", "Netherlands": "Protestant Europe",
|
||||
"Finland": "Protestant Europe", "Switzerland": "Protestant Europe",
|
||||
# Catholic Europe
|
||||
"France": "Catholic Europe", "Belgium": "Catholic Europe", "Italy": "Catholic Europe",
|
||||
"Spain": "Catholic Europe", "Poland": "Catholic Europe", "Portugal": "Catholic Europe",
|
||||
"Croatia": "Catholic Europe",
|
||||
# Orthodox / Ex-Communist
|
||||
"Russia": "Orthodox", "Ukraine": "Orthodox", "Bulgaria": "Orthodox", "Serbia": "Orthodox",
|
||||
"Greece": "Orthodox", "Romania": "Orthodox", "Bosnia & Herzegovina": "Orthodox",
|
||||
"Hungary": "Orthodox",
|
||||
# Baltic
|
||||
"Latvia": "Baltic", "Estonia": "Baltic",
|
||||
# Confucian
|
||||
"Japan": "Confucian", "China": "Confucian", "South Korea": "Confucian",
|
||||
"Hong Kong": "Confucian", "Vietnam": "Confucian", "Singapore": "Confucian",
|
||||
# Latin America
|
||||
"Argentina": "Latin America", "Chile": "Latin America", "Colombia": "Latin America",
|
||||
"Mexico": "Latin America", "Peru": "Latin America", "Brazil": "Latin America",
|
||||
"Ecuador": "Latin America", "Philippines": "Latin America",
|
||||
# African-Islamic
|
||||
"Egypt": "African-Islamic", "Kenya": "African-Islamic", "Morocco": "African-Islamic",
|
||||
"Nigeria": "African-Islamic", "Saudi Arabia": "African-Islamic",
|
||||
"United Arab Emirates": "African-Islamic", "Turkey": "African-Islamic",
|
||||
"Iran": "African-Islamic", "Indonesia": "African-Islamic", "Malaysia": "African-Islamic",
|
||||
"South Africa": "African-Islamic",
|
||||
# South Asia
|
||||
"India": "South Asia", "Pakistan": "South Asia", "Thailand": "South Asia",
|
||||
}
|
||||
|
||||
# raw country string (as it appears in the human CSVs) -> canonical IW_ZONE key. Our CSVs mix full
|
||||
# names (mfv/mfq2/humor, with a "Columbia" typo) and ISO2 codes (big5/16pf). A `None` value marks a
|
||||
# row we KNOW is corrupt and deliberately exclude from hulls (surfaced by a loud warning, not a
|
||||
# silent drop); an unrecognised country that is NOT here falls through to IW_ZONE and KeyErrors.
|
||||
_COUNTRY_CANON = {
|
||||
"AE": "United Arab Emirates", "AU": "Australia", "BR": "Brazil", "CA": "Canada",
|
||||
"CN": "China", "DE": "Germany", "DK": "Denmark", "EC": "Ecuador", "ES": "Spain",
|
||||
"FI": "Finland", "FR": "France", "GB": "Great Britain", "GR": "Greece", "HK": "Hong Kong",
|
||||
"HR": "Croatia", "ID": "Indonesia", "IE": "Ireland", "IN": "India", "IT": "Italy",
|
||||
"MX": "Mexico", "MY": "Malaysia", "NL": "Netherlands", "NO": "Norway", "NZ": "New Zealand",
|
||||
"PH": "Philippines", "PK": "Pakistan", "PL": "Poland", "RO": "Romania", "SE": "Sweden",
|
||||
"SG": "Singapore", "TH": "Thailand", "TR": "Turkey", "US": "United States", "ZA": "South Africa",
|
||||
"Columbia": "Colombia", "UAE": "United Arab Emirates",
|
||||
"(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. -- added by Claude
|
||||
ECONOMIST_OUTLIERS = {"China", "South Korea", "United States", "Great Britain", "Japan",
|
||||
"Nigeria", "Pakistan", "Sweden"}
|
||||
|
||||
|
||||
def zones_for(countries: list[str]) -> tuple[dict[str, list[str]], set[str]]:
|
||||
"""Group verbatim country strings by IW zone + the subset to emphasize. Fails loud (KeyError)
|
||||
on a country absent from the taxonomy so a name-normalization bug can't silently drop a dot from
|
||||
its hull; a `None` canon (known-corrupt row) is excluded with a warning instead."""
|
||||
groups: dict[str, list[str]] = {}
|
||||
dropped: list[str] = []
|
||||
emph: set[str] = set()
|
||||
for c in countries:
|
||||
canon = _COUNTRY_CANON.get(c, c)
|
||||
if canon is None:
|
||||
dropped.append(c)
|
||||
continue
|
||||
groups.setdefault(IW_ZONE[canon], []).append(c)
|
||||
if canon in ECONOMIST_OUTLIERS:
|
||||
emph.add(c)
|
||||
if dropped:
|
||||
logger.warning(f"excluded known-unmapped countries from zone hulls: {dropped}")
|
||||
return groups, emph
|
||||
|
||||
|
||||
def _frac(x, scale_max: int) -> np.ndarray:
|
||||
return (np.asarray(x, float) - 1) / (scale_max - 1)
|
||||
@@ -191,10 +274,11 @@ def plot_ordinal(run_dir: Path, out: Path, name: str, vec_label: str, C: float,
|
||||
else:
|
||||
respondents, haze = None, human_haze(instr)
|
||||
traj = {c: _frac(prof_c[c], instr.scale_max) for c in coh_cs}
|
||||
zones, emph = zones_for(countries)
|
||||
figm = T.maps.plot_ipsative_pca(instr, dims, countries, Mfrac,
|
||||
_frac(base, instr.scale_max), _frac(pos, instr.scale_max),
|
||||
_frac(neg, instr.scale_max), respondents=respondents, haze=haze,
|
||||
traj=traj, labels=labels)
|
||||
traj=traj, zones=zones, emphasize=emph, labels=labels)
|
||||
figm.axes[0].set_title(f"{instr.display}: humans vs LLMs steered for {vec_label}", fontsize=10)
|
||||
paths = [T.maps.save_both(figm, out / name, "map_pca_ipsative")]
|
||||
plt.close(figm)
|
||||
@@ -268,8 +352,9 @@ def plot_mfv_map(run_dir: Path, out: Path, vec_label: str, C: float, coh_cs: lis
|
||||
neg_c = min(c for c in coh_cs if c < 0.0)
|
||||
labels = ("base (c=0)", f"c={pos_c:+g}", f"c={neg_c:+g}")
|
||||
traj = {c: prof[c] for c in coh_cs}
|
||||
zones, emph = zones_for(countries)
|
||||
fig = T.maps.plot_ipsative_pca(_MFV_INSTR, founds, countries, M, prof[0.0], prof[pos_c], prof[neg_c],
|
||||
traj=traj, labels=labels)
|
||||
traj=traj, zones=zones, emphasize=emph, labels=labels)
|
||||
fig.axes[0].set_title(f"MFV vignettes: humans vs LLMs steered for {vec_label}", fontsize=10)
|
||||
path = T.maps.save_both(fig, out / "mfv", "map_pca_ipsative")
|
||||
plt.close(fig)
|
||||
|
||||
Reference in New Issue
Block a user