mirror of
https://github.com/wassname/moral-maps.git
synced 2026-08-20 12:30:40 +08:00
The MFV foundation readout is centered log-ratio (clr), not a logit. Renames the showcase JSON/CSV consumers' keys (dclr/dclr_sem) and the shared reader-space-shift column to match steering-lite's writers; the survey 'C = logit contrast' readout keeps its logit naming. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
139 lines
4.9 KiB
Python
139 lines
4.9 KiB
Python
"""Summarize a steering-lite all-instrument showcase for the README table."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
import plot_steer_showcase as P
|
|
from moralmaps import get_instrument
|
|
|
|
DISPLAY = {
|
|
"mfv": "MFV vignettes",
|
|
"humor_styles": "Humor Styles",
|
|
"big5": "Big Five",
|
|
"mfq2": "MFQ-2 survey",
|
|
}
|
|
|
|
|
|
def _rows(path: Path) -> list[dict[str, str]]:
|
|
with path.open(newline="") as fh:
|
|
return list(csv.DictReader(fh))
|
|
|
|
|
|
def _fmt(x: float, digits: int = 2) -> str:
|
|
return f"{x:+.{digits}f}"
|
|
|
|
|
|
def _fmt_pct(x: float) -> str:
|
|
return f"{x:+.0f}%"
|
|
|
|
|
|
def _ci_sem(lo: float, hi: float) -> float:
|
|
return (hi - lo) / (2 * 1.96)
|
|
|
|
|
|
def _cs_label(cs: list[float]) -> str:
|
|
return ", ".join(f"{c:+g}" if c else "0" for c in sorted(cs))
|
|
|
|
|
|
def _coherent_cs(run_dir: Path, instruments: list[str], coherence_frac: float,
|
|
contrast_frac: float, margin_frac: float) -> list[float]:
|
|
ordinal = [name for name in instruments if name != "mfv"]
|
|
quality = P.shared_quality_score(run_dir, ordinal, pmass_frac=coherence_frac,
|
|
contrast_frac=contrast_frac, margin_frac=margin_frac)
|
|
return P.coherent_prefix_cs(sorted(quality), quality, 1.0)
|
|
|
|
|
|
def _survey_rows(run_dir: Path, name: str, cs: list[float]) -> list[dict[str, str]]:
|
|
instr = get_instrument(name)
|
|
rows = _rows(run_dir / f"{name}_profiles.csv")
|
|
by_key = {(r["foundation"], float(r["c"])): r for r in rows}
|
|
humans = P.human_strip(instr)
|
|
pos_c = max(c for c in cs if c > 0)
|
|
neg_c = min(c for c in cs if c < 0)
|
|
|
|
out = []
|
|
for dim in instr.dimensions:
|
|
neg = by_key[(dim, neg_c)]
|
|
pos = by_key[(dim, pos_c)]
|
|
human_vals = np.array([v for _country, v in humans[dim]], dtype=float)
|
|
human_sd = float(human_vals.std(ddof=1))
|
|
profile_delta = float(pos["mean"]) - float(neg["mean"])
|
|
logit_delta = float(pos["C"]) - float(neg["C"])
|
|
sem = float(np.hypot(
|
|
_ci_sem(float(pos["C_ci95_lo"]), float(pos["C_ci95_hi"])),
|
|
_ci_sem(float(neg["C_ci95_lo"]), float(neg["C_ci95_hi"])),
|
|
))
|
|
out.append({
|
|
"dataset": DISPLAY[name],
|
|
"axis": dim,
|
|
"c path": _cs_label(cs),
|
|
"profile shift / human SD": _fmt_pct(100 * profile_delta / human_sd),
|
|
"profile shift": _fmt(profile_delta),
|
|
"reader-space shift": f"{_fmt(logit_delta)} ± {sem:.2f}",
|
|
})
|
|
return out
|
|
|
|
|
|
def _mfv_rows(run_dir: Path, cs: list[float]) -> list[dict[str, str]]:
|
|
founds, countries, human_M, prof, _pmass = P._mfv_zspace(run_dir)
|
|
rows = _rows(run_dir / "mfv_profiles.csv")
|
|
by_key = {(r["foundation"], float(r["c"])): r for r in rows}
|
|
pos_c = max(c for c in cs if c > 0)
|
|
neg_c = min(c for c in cs if c < 0)
|
|
|
|
out = []
|
|
for j, foundation in enumerate(founds):
|
|
neg = by_key[(foundation, neg_c)]
|
|
pos = by_key[(foundation, pos_c)]
|
|
human_sd = float(human_M[:, j].std(ddof=1))
|
|
profile_delta = float(prof[pos_c][j] - prof[neg_c][j])
|
|
clr_delta = float(pos["dclr"]) - float(neg["dclr"])
|
|
sem = float(np.hypot(float(pos["dclr_sem"]), float(neg["dclr_sem"])))
|
|
out.append({
|
|
"dataset": DISPLAY["mfv"],
|
|
"axis": foundation,
|
|
"c path": _cs_label(cs),
|
|
"profile shift / human SD": _fmt_pct(100 * profile_delta / human_sd),
|
|
"profile shift": _fmt(profile_delta),
|
|
"reader-space shift": f"{_fmt(clr_delta)} ± {sem:.2f}",
|
|
})
|
|
return out
|
|
|
|
|
|
def _markdown_table(rows: list[dict[str, str]]) -> str:
|
|
cols = ["dataset", "axis", "c path", "profile shift / human SD", "profile shift", "reader-space shift"]
|
|
lines = ["| " + " | ".join(cols) + " |", "| " + " | ".join(["---"] * len(cols)) + " |"]
|
|
for row in rows:
|
|
lines.append("| " + " | ".join(row[c] for c in cols) + " |")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--run-dir", type=Path, required=True)
|
|
ap.add_argument("--coherence-frac", type=float, default=0.99)
|
|
ap.add_argument("--contrast-frac", type=float, default=0.50)
|
|
ap.add_argument("--margin-frac", type=float, default=0.50)
|
|
ap.add_argument("--instruments", nargs="+", default=["mfv", "humor_styles", "big5", "mfq2"])
|
|
args = ap.parse_args()
|
|
|
|
cs = _coherent_cs(args.run_dir, args.instruments, args.coherence_frac,
|
|
args.contrast_frac, args.margin_frac)
|
|
assert any(c > 0 for c in cs) and any(c < 0 for c in cs), f"need both signed arms, got {cs}"
|
|
|
|
rows: list[dict[str, str]] = []
|
|
if "mfv" in args.instruments:
|
|
rows.extend(_mfv_rows(args.run_dir, cs))
|
|
for name in args.instruments:
|
|
if name != "mfv":
|
|
rows.extend(_survey_rows(args.run_dir, name, cs))
|
|
print(_markdown_table(rows))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|