Extract IW zone taxonomy to tinymfv.zones, extend to WVS 90 countries

Second consumer (the WVS map) needs the same curated zone data, so move it out of
the showcase script into the package rather than duplicate it (avoids drift in
research data). Extended IW_ZONE to cover all 90 WVS countries + SAR/name-variant
aliases. Showcase script now imports zones_for from tinymfv.zones.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-07-04 20:26:44 +08:00
co-authored by Claudypoo
parent b2532acd90
commit 2af00b374c
2 changed files with 106 additions and 90 deletions
+1 -90
View File
@@ -35,101 +35,12 @@ 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
from tinymfv.zones import zones_for
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 _zone_of(country: str) -> str | None:
"""IW zone of a verbatim country string, or None for a known-corrupt row. KeyErrors (fail loud)
on an unrecognised country so a normalization bug can't silently drop it."""
canon = _COUNTRY_CANON.get(country, country)
return None if canon is None else IW_ZONE[canon]
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:
z = _zone_of(c)
if z is None:
dropped.append(c)
continue
groups.setdefault(z, []).append(c)
if _COUNTRY_CANON.get(c, c) 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)
+105
View File
@@ -0,0 +1,105 @@
"""Inglehart-Welzel cultural zones: country -> zone taxonomy shared by every map.
Two consumers now (the instrument showcase maps and the WVS map), and the assignment is
hand-curated research data, so it lives here once rather than drifting between copies. Membership is
VALUE-based, not geographic (Inglehart-Welzel), so several placements are judgment calls -- flagged
inline. Source: WVS Findings + en.wikipedia.org/wiki/Inglehart-Welzel_cultural_map_of_the_world.
Fuzziest calls (noted so a reader can override): ex-Soviet Muslim-majority states (Azerbaijan,
Kazakhstan, Kyrgyzstan, Tajikistan) -> African-Islamic on religion, not Orthodox on history;
Philippines -> Latin America; Hungary -> Catholic Europe; Cyprus/Armenia/Georgia -> Orthodox.
-- authored by Claude
"""
from __future__ import annotations
from loguru import logger
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", "Northern 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", "Iceland": "Protestant Europe", "Austria": "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", "Czechia": "Catholic Europe", "Slovakia": "Catholic Europe",
"Slovenia": "Catholic Europe", "Hungary": "Catholic Europe", "Andorra": "Catholic Europe",
# Orthodox / Ex-Communist
"Russia": "Orthodox", "Ukraine": "Orthodox", "Bulgaria": "Orthodox", "Serbia": "Orthodox",
"Greece": "Orthodox", "Romania": "Orthodox", "Bosnia Herzegovina": "Orthodox", "Belarus": "Orthodox",
"Georgia": "Orthodox", "Armenia": "Orthodox", "Montenegro": "Orthodox", "North Macedonia": "Orthodox",
"Moldova": "Orthodox", "Cyprus": "Orthodox", "Albania": "Orthodox",
# Baltic
"Estonia": "Baltic", "Latvia": "Baltic", "Lithuania": "Baltic",
# Confucian
"Japan": "Confucian", "China": "Confucian", "South Korea": "Confucian", "Hong Kong SAR": "Confucian",
"Taiwan ROC": "Confucian", "Vietnam": "Confucian", "Singapore": "Confucian", "Macau SAR": "Confucian",
"Mongolia": "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", "Bolivia": "Latin America",
"Guatemala": "Latin America", "Nicaragua": "Latin America", "Puerto Rico": "Latin America",
"Uruguay": "Latin America", "Venezuela": "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",
"Jordan": "African-Islamic", "Iraq": "African-Islamic", "Lebanon": "African-Islamic",
"Libya": "African-Islamic", "Tunisia": "African-Islamic", "Ethiopia": "African-Islamic",
"Zimbabwe": "African-Islamic", "Azerbaijan": "African-Islamic", "Kazakhstan": "African-Islamic",
"Kyrgyzstan": "African-Islamic", "Tajikistan": "African-Islamic",
# South Asia
"India": "South Asia", "Pakistan": "South Asia", "Thailand": "South Asia",
"Bangladesh": "South Asia", "Maldives": "South Asia", "Myanmar": "South Asia",
}
# raw string (as it appears in a data file) -> canonical IW_ZONE key. Covers ISO2 codes
# (big5/16pf), a "Columbia" typo (mfq2/mfv), a "&" spelling and WVS SAR names, and one corrupt row
# (`None` = deliberately excluded from hulls, surfaced by a warning; anything NOT here and NOT a
# canonical key KeyErrors, so a real normalization bug fails loud).
_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 SAR", "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", "Bosnia & Herzegovina": "Bosnia Herzegovina",
"(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.
ECONOMIST_OUTLIERS = {"China", "South Korea", "United States", "Great Britain", "Japan",
"Nigeria", "Pakistan", "Sweden"}
def zone_of(country: str) -> str | None:
"""IW zone of a verbatim country string, or None for a known-corrupt row. KeyErrors (fail loud)
on an unrecognised country so a normalization bug can't silently drop it from its zone."""
canon = _CANON.get(country, country)
return None if canon is None else IW_ZONE[canon]
def zones_for(countries: list[str]) -> tuple[dict[str, list[str]], set[str]]:
"""Group verbatim country strings by IW zone + the subset to emphasize (Economist outliers).
Known-corrupt rows are dropped with a warning; unrecognised countries KeyError via zone_of."""
groups: dict[str, list[str]] = {}
dropped: list[str] = []
emph: set[str] = set()
for c in countries:
z = zone_of(c)
if z is None:
dropped.append(c)
continue
groups.setdefault(z, []).append(c)
if _CANON.get(c, c) in ECONOMIST_OUTLIERS:
emph.add(c)
if dropped:
logger.warning(f"excluded known-unmapped countries from zone hulls: {dropped}")
return groups, emph