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>
This commit is contained in:
wassname
2026-07-05 10:42:15 +08:00
co-authored by Claudypoo
parent a1f89ecf08
commit b768395ee8
2 changed files with 17 additions and 7 deletions
+13
View File
@@ -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)
+4 -7
View File
@@ -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)