From b768395ee8944b43189170d30e01eea335c9130a Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:42:15 +0800 Subject: [PATCH] wvs map: drop CI whiskers (too messy), move uncertainty to a table With a dozen+ models the 95% CI crosses overlap into noise. Remove the whiskers from plot_value_map (clean red dots) and instead write a widest-first CI table (wvs_model_ci.md) next to the figure. The table also documents that the wide CIs are item-disagreement (a model rating different WVS items inconsistently on an axis), which more samples will not shrink -- not sampling noise. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com> --- scripts/wvs_map.py | 13 +++++++++++++ src/tinymfv/maps.py | 11 ++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/scripts/wvs_map.py b/scripts/wvs_map.py index efd736b..19706d1 100644 --- a/scripts/wvs_map.py +++ b/scripts/wvs_map.py @@ -269,6 +269,19 @@ def main() -> None: x, y, xs, ys = models[key] logger.info(f"cached {key}: ({x:.2f}, {y:.2f}) +-({1.96*xs:.02f}, {1.96*ys:.02f}) 95% CI") + # Uncertainty as a TABLE, not whiskers on the map (the CI crosses overlap into noise with a dozen+ + # models). Sorted widest-first so the mushy models are obvious. The CI is bootstrap over items + + # samples; for the wide ones it's item-disagreement (the model rates different WVS items + # inconsistently on an axis), which more samples will NOT shrink -- see the readme/journal note. + ci_rows = [(k, v[0], v[1], 1.96 * v[2], 1.96 * v[3]) for k, v in models.items() if len(v) > 3] + if ci_rows: + from tabulate import tabulate + ci_rows.sort(key=lambda r: -(r[3] + r[4])) + table = tabulate(ci_rows, headers=["model", "x self-expr", "y secular", "x 95%CI", "y 95%CI"], + tablefmt="pipe", floatfmt="+.2f") + Path(args.out).with_name("wvs_model_ci.md").write_text(table + "\n") + logger.info("model coords + 95% CI (widest first):\n" + table) + # Render through the SHARED value-map renderer (same one the instrument value maps use): pole # signposts through the human median, 4 auto-selected zone hulls, textalloc labels, model stars. _, emph = zones_for(countries) diff --git a/src/tinymfv/maps.py b/src/tinymfv/maps.py index 82e3362..f29f47f 100644 --- a/src/tinymfv/maps.py +++ b/src/tinymfv/maps.py @@ -268,16 +268,13 @@ def plot_value_map(display: str, countries: list[str], P: np.ndarray, tcol = ["#111"] * len(lab_i) 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. mnames = list(models) mx = np.array([models[k][0] for k in mnames]) my = np.array([models[k][1] for k in mnames]) - # optional bootstrap SE (rated models carry (x, y, x_se, y_se); logprob models just (x, y)) - xse = np.array([models[k][2] if len(models[k]) > 2 else 0.0 for k in mnames]) - yse = np.array([models[k][3] if len(models[k]) > 3 else 0.0 for k in mnames]) - if (xse > 0).any() or (yse > 0).any(): # 95% CI -> a mushy model reads as uncertain - ax.errorbar(mx, my, xerr=1.96 * xse, yerr=1.96 * yse, fmt="none", ecolor=MODEL_RED, - elinewidth=1.0, alpha=0.4, capsize=2.5, capthick=0.8, zorder=7) - ax.scatter(mx, my, s=150, marker="o", c=MODEL_RED, # Economist: bigger red dots + ax.scatter(mx, my, s=120, marker="o", c=MODEL_RED, # Economist: bigger red dots edgecolors="white", linewidths=1.0, zorder=8) tx += list(mx); ty += list(my); txt += mnames tcol += [MODEL_RED] * len(mnames)