From 2af00b374c56059377edfecc10c55ad7acd30bf6 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Sat, 4 Jul 2026 20:26:44 +0800 Subject: [PATCH] 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> --- scripts/plot_steer_showcase.py | 91 +--------------------------- src/tinymfv/zones.py | 105 +++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 90 deletions(-) create mode 100644 src/tinymfv/zones.py diff --git a/scripts/plot_steer_showcase.py b/scripts/plot_steer_showcase.py index 38b1924..e791ffa 100644 --- a/scripts/plot_steer_showcase.py +++ b/scripts/plot_steer_showcase.py @@ -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) diff --git a/src/tinymfv/zones.py b/src/tinymfv/zones.py new file mode 100644 index 0000000..658a723 --- /dev/null +++ b/src/tinymfv/zones.py @@ -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