Buckets:

glennmatlin's picture
download
raw
10.7 kB
#!/usr/bin/env python3
# pyright: reportAttributeAccessIssue=false, reportCallIssue=false
"""Visualization suite for RQ4 lexical results (appendix-bound).
Roster-driven; reads per_bin_profiles + top_bins + the bootstrap CSVs and the
format-cluster YAML. Emits several plot types for review:
fig-lex-cluster-composition.pdf stacked bars: top-N format-cluster mix / benchmark
fig-lex-group-contrast-forest.pdf CI forest of pooled group deltas / composite
fig-lex-group-profile.pdf grouped bars: mean composite z per roster group
fig-lex-benchmark-composite-dots per-benchmark mean composite z (dot plot)
fig-lex-scatter-words-mental.pdf per-bin words vs mental-state, coloured by cluster
All plots use the Okabe-Ito colourblind-safe palette. Nothing is benchmark-
hardcoded; adding a benchmark to the roster extends every plot automatically.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
matplotlib.rcParams["pdf.fonttype"] = 42
matplotlib.rcParams["ps.fonttype"] = 42
import matplotlib.pyplot as plt # noqa: E402
import numpy as np # noqa: E402
import pandas as pd # noqa: E402
import sys # noqa: E402
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src"))
from data_attribution.rq4_lexical.roster import load_roster # noqa: E402
OKABE_ITO = {
# roster groups
"social_reasoning": "#009E73", # green
"commonsense_reasoning": "#CC79A7", # purple
"knowledge_recall": "#E69F00", # orange
}
# Colours for the 6 interactional format groups (Okabe-Ito), interactional -> expository.
GROUP_COLORS = {
"dialogic": "#0072B2", # blue
"personal": "#56B4E9", # sky blue
"expository": "#D55E00", # vermillion
"structured": "#E69F00", # orange
"news": "#009E73", # green
"boilerplate": "#999999", # grey
}
GROUP_LEGEND = {
"dialogic": "dialogic",
"personal": "personal",
"expository": "expository",
"structured": "structured",
"news": "news",
"boilerplate": "boilerplate",
}
COMPOSITES = [
("mean_words_per_doc", "mean words/doc"),
("mental_state_per_1k", "mental-state /1k"),
("dialogue_composite_z", "dialogue (z)"),
("empath_social_z", "social (z)"),
("empath_affect_z", "affect (z)"),
]
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser()
p.add_argument("--per-bin", required=True)
p.add_argument(
"--top-bins", required=True, help="top_bins_lexical_profiles.parquet"
)
p.add_argument("--group-contrast", required=True)
p.add_argument(
"--per-benchmark-sep",
required=True,
help="cluster_separation_bootstrap_all_benchmarks.csv",
)
p.add_argument("--format-clusters", required=True)
p.add_argument("--roster", required=True)
p.add_argument("--out-dir", required=True)
return p.parse_args()
def _clusters(path: Path):
from data_attribution.rq4_lexical.format_clusters import load_format_clusters
return load_format_clusters(path)
def plot_cluster_composition(top: pd.DataFrame, clusters, roster, out: Path) -> None:
"""6-way stacked bars: count of each benchmark's top-N bins per format group."""
fmt_to_group = clusters.format_to_group()
groups = clusters.ordered_groups()
bids = [b for b in roster.ids if b in set(top["benchmark"])]
labels = [roster.shorts.get(b, b) for b in bids]
counts = {g: [] for g in groups}
for b in bids:
sub = top[top.benchmark == b]
grp_of = sub["bin_format"].map(fmt_to_group)
for g in groups:
counts[g].append(int((grp_of == g).sum()))
fig, ax = plt.subplots(figsize=(max(7, 1.0 * len(labels) + 2), 4.2))
x = np.arange(len(labels))
bottom = np.zeros(len(labels))
for g in groups:
vals = np.array(counts[g], dtype=float)
ax.bar(
x,
vals,
bottom=bottom,
color=GROUP_COLORS.get(g, "#777777"),
label=GROUP_LEGEND.get(g, g),
)
bottom += vals
ax.set_xticks(x)
ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=8)
ax.set_ylabel("top-20 bins")
ax.set_title("Format-cluster composition of top-20 high-influence bins")
ax.legend(fontsize=8, loc="upper right")
fig.tight_layout()
fig.savefig(out, bbox_inches="tight")
plt.close(fig)
print(f"wrote {out}", flush=True)
def plot_group_contrast_forest(gc: pd.DataFrame, out: Path) -> None:
pairs = gc[["group_a", "group_b"]].drop_duplicates().itertuples(index=False)
pairs = list(pairs)
fig, axes = plt.subplots(
1, len(COMPOSITES), figsize=(3.0 * len(COMPOSITES), 3.2), sharey=True
)
for ax, (feat, fl) in zip(axes, COMPOSITES):
sub = gc[gc.feature == feat]
ys = np.arange(len(pairs))
for y, (ga, gb) in zip(ys, pairs):
row = sub[(sub.group_a == ga) & (sub.group_b == gb)]
if row.empty:
continue
r = row.iloc[0]
sig = (r.ci_low > 0) or (r.ci_high < 0)
color = "#000000" if sig else "#999999"
ax.plot([r.ci_low, r.ci_high], [y, y], color=color, lw=2)
ax.plot(r.delta_point, y, "o", color=color, ms=5)
ax.axvline(0, color="#D55E00", ls="--", lw=1)
ax.set_title(fl, fontsize=9)
ax.set_yticks(ys)
ax.set_yticklabels([f"{a[:10]}\nvs {b[:10]}" for a, b in pairs], fontsize=7)
fig.suptitle(
"Pooled group contrast (Δ = mean A − mean B), 95% bootstrap CI", fontsize=10
)
fig.tight_layout()
fig.savefig(out, bbox_inches="tight")
plt.close(fig)
print(f"wrote {out}", flush=True)
def plot_benchmark_profile(top: pd.DataFrame, roster, out: Path) -> None:
"""Per-benchmark grouped bars: one cluster per benchmark, four composite bars.
Composites are z-scores against all 576 bins, averaged over that benchmark's
top-20 most-influential bins (0 = corpus-average bin)."""
composites = [
("mental_state_z", "mental-state (z)"),
("dialogue_composite_z", "dialogue (z)"),
("empath_social_z", "social (z)"),
("empath_affect_z", "affect (z)"),
]
comp_colors = ["#000000", "#0072B2", "#009E73", "#CC79A7"]
bids = [b for b in roster.ids if b in set(top["benchmark"])]
labels = [roster.shorts.get(b, b) for b in bids]
fig, ax = plt.subplots(figsize=(max(7, 1.1 * len(bids) + 2), 4.2))
width = 0.8 / len(composites)
x = np.arange(len(bids))
for i, (feat, fl) in enumerate(composites):
means = [top[top.benchmark == b][feat].mean(skipna=True) for b in bids]
ax.bar(x + i * width, means, width, label=fl, color=comp_colors[i])
ax.axhline(0, color="black", lw=0.8)
ax.set_xticks(x + width * (len(composites) - 1) / 2)
ax.set_xticklabels(labels, rotation=30, ha="right", fontsize=8)
ax.set_ylabel("mean $z$ over top-20 bins (0 = corpus-average bin)")
ax.set_title(
"Lexical profile per benchmark (averaged over its top-20 high-influence bins)"
)
ax.legend(fontsize=8, ncol=4, loc="upper center", bbox_to_anchor=(0.5, -0.18))
fig.tight_layout()
fig.savefig(out, bbox_inches="tight")
plt.close(fig)
print(f"wrote {out}", flush=True)
def plot_benchmark_composite_dots(top: pd.DataFrame, roster, out: Path) -> None:
feats = [
("dialogue_composite_z", "dialogue z"),
("empath_social_z", "social z"),
("empath_affect_z", "affect z"),
]
bids = [b for b in roster.ids if b in set(top["benchmark"])]
fig, ax = plt.subplots(figsize=(7, max(4, 0.4 * len(bids) + 1)))
ys = np.arange(len(bids))
markers = {"dialogue z": "o", "social z": "s", "affect z": "^"}
for feat, fl in feats:
means = [top[top.benchmark == b][feat].mean(skipna=True) for b in bids]
grp_colors = [OKABE_ITO.get(roster.groups.get(b), "#000000") for b in bids]
ax.scatter(means, ys, c=grp_colors, marker=markers[fl], s=40, label=fl)
ax.axvline(0, color="#D55E00", ls="--", lw=1)
ax.set_yticks(ys)
ax.set_yticklabels([roster.shorts.get(b, b) for b in bids], fontsize=8)
ax.set_xlabel("mean composite z over top-20 bins")
ax.set_title(
"Composite lexical loadings per benchmark\n(colour = group, marker = composite)"
)
ax.legend(fontsize=8, loc="lower right")
fig.tight_layout()
fig.savefig(out, bbox_inches="tight")
plt.close(fig)
print(f"wrote {out}", flush=True)
def plot_scatter_words_mental(top: pd.DataFrame, clusters, out: Path) -> None:
fig, ax = plt.subplots(figsize=(6.5, 5))
fmt_to_group = clusters.format_to_group()
grp_of = top["bin_format"].map(fmt_to_group)
for g in clusters.ordered_groups():
sub = top[grp_of == g]
if sub.empty:
continue
ax.scatter(
sub["mean_words_per_doc"],
sub["mental_state_per_1k"],
c=GROUP_COLORS.get(g, "#777777"),
label=GROUP_LEGEND.get(g, g),
alpha=0.7,
s=28,
edgecolors="none",
)
ax.set_xscale("log")
ax.set_xlabel("mean words / doc (log)")
ax.set_ylabel("mental-state verbs / 1k words")
ax.set_title("Top-20 bins (all benchmarks): length vs mental-state density")
ax.legend(fontsize=8)
fig.tight_layout()
fig.savefig(out, bbox_inches="tight")
plt.close(fig)
print(f"wrote {out}", flush=True)
def main() -> None:
args = parse_args()
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
roster = load_roster(Path(args.roster))
top = pd.read_parquet(args.top_bins)
per_bin = pd.read_parquet(args.per_bin)
if "mental_state_z" in per_bin.columns and "mental_state_z" not in top.columns:
top = top.merge(
per_bin[["bin_topic", "bin_format", "mental_state_z"]],
on=["bin_topic", "bin_format"],
how="left",
)
gc = pd.read_csv(args.group_contrast)
sep = pd.read_csv(args.per_benchmark_sep)
clusters = _clusters(Path(args.format_clusters))
del sep # composition is now computed from top-bins + taxonomy, not the sep CSV
plot_cluster_composition(
top, clusters, roster, out_dir / "fig-lex-cluster-composition.pdf"
)
plot_group_contrast_forest(gc, out_dir / "fig-lex-group-contrast-forest.pdf")
plot_benchmark_profile(top, roster, out_dir / "fig-lex-benchmark-profile.pdf")
plot_benchmark_composite_dots(
top, roster, out_dir / "fig-lex-benchmark-composite-dots.pdf"
)
plot_scatter_words_mental(
top, clusters, out_dir / "fig-lex-scatter-words-mental.pdf"
)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
10.7 kB
·
Xet hash:
381cb35ba77ddab5f609ef0e2f3ae6f699a01861c99484284d816680687d6710

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.