"""Cumulative welfare trends over ccVersion: rate | ratio | absolute counts (vconcat).
Panels 1 and 2 use the count-weighted cumulative aggregator
(`prompt_analysis.cumulative_count_by_version`) — `Σ num / Σ den` over files
in versions ≤ V. The latest-version point on each curve equals the canonical
HEADLINE value by construction, so the curve endpoint cross-checks against
`headline_numbers()`.
The cumulative pool below 20 files is too small to produce a stable ratio
(a single outlier file dominates the running statistic), so all three
panels suppress data before that threshold and start the visible curve
from the first version where `n_files_so_far ≥ 20` (v2.1.18 in the current
corpus). Earlier versions exist and contribute to the cumulative running
state; they're just not plotted because the rate at that point isn't a
defensible corpus claim.
"""
from prompt_analysis import cumulative_count_by_version
ver_order_cum = (
alt_df[alt_df["ccVersion"] != ""]
.drop_duplicates("ccVersion").sort_values("ccVersion_sort")["ccVersion"].tolist()
)
SMALL_N_THRESHOLD = 20 # below this many cumulative files, the running ratio is unstable
# --- panel 1: cumulative rule-explanation rate (count-weighted) ---
cum_re_para = cumulative_count_by_version(
alt_df, "rule_n_explained_para", "rule_n", pct=True,
metric_label="paragraph window",
)
cum_re_same = cumulative_count_by_version(
alt_df, "rule_n_explained_same", "rule_n", pct=True,
metric_label="same sentence",
)
cum_re = pd.concat([cum_re_para, cum_re_same], ignore_index=True)
cum_re = cum_re[cum_re["n_files_so_far"] >= SMALL_N_THRESHOLD]
def _line_with_points(df, color_enc, y_title, title, height=200, value_title="count-weighted ratio"):
base = alt.Chart(df).encode(
x=alt.X("ccVersion:N", sort=ver_order_cum, title=None,
axis=alt.Axis(labelAngle=-90, labelLimit=80,
labelOverlap=False, labelFontSize=8)),
y=alt.Y("value:Q", title=y_title),
color=color_enc,
tooltip=[
alt.Tooltip("ccVersion:N"),
alt.Tooltip("metric:N"),
alt.Tooltip("value:Q", format=".3f", title=value_title),
alt.Tooltip("num_so_far:Q", format=",.0f", title="Σ numerator"),
alt.Tooltip("den_so_far:Q", format=",.0f", title="Σ denominator"),
alt.Tooltip("n_files_so_far:Q", title="files ≤ V"),
],
)
line = base.mark_line(strokeWidth=2.0)
pts = base.mark_point(filled=True, size=45)
return alt.layer(line, pts).properties(width=820, height=height, title=title)
cum_re_chart = _line_with_points(
cum_re,
color_enc=alt.Color("metric:N",
scale=alt.Scale(domain=["paragraph window", "same sentence"],
range=["#4e79a7", "#e15759"]),
legend=alt.Legend(title="pairing scope", orient="right")),
y_title="% rule sentences explained (count-weighted)",
title="Cumulative rule-explanation rate over ccVersion (Σ explained / Σ rules, from n≥20)",
height=200,
value_title="count-weighted %",
)
# --- panel 2: cumulative judgment-to-procedural ratio (count-weighted) ---
cum_jp = cumulative_count_by_version(
alt_df, "judgment_count", "procedural_count",
metric_label="judgment / procedural",
)
cum_jp = cum_jp[cum_jp["n_files_so_far"] >= SMALL_N_THRESHOLD]
cum_jp_chart = _line_with_points(
cum_jp,
color_enc=alt.value("#4e79a7"),
y_title="judgment / procedural (count-weighted)",
title="Cumulative judgment-to-procedural ratio (Σ judgment / Σ procedural, from n≥20) — welfare-thesis trend",
height=200,
)
# --- panel 3: cumulative absolute counts of explained vs unexplained rule sentences ---
abs_df = alt_df[(alt_df["ccVersion"] != "") & (alt_df["rule_n"] > 0)].copy()
abs_df["_explained_n"] = abs_df["rule_n_explained_para"].astype(float)
abs_df["_unexplained_n"] = abs_df["rule_n"].astype(float) - abs_df["_explained_n"]
cum_abs_explained = cumulative_by_version(abs_df, ["_explained_n"], agg="sum")
cum_abs_explained["status"] = "explained (paragraph)"
cum_abs_unexplained = cumulative_by_version(abs_df, ["_unexplained_n"], agg="sum")
cum_abs_unexplained["status"] = "unexplained"
cum_abs_df = pd.concat([cum_abs_explained, cum_abs_unexplained], ignore_index=True)
cum_abs_df = cum_abs_df[cum_abs_df["n_files_so_far"] >= SMALL_N_THRESHOLD]
cum_abs_chart = (
alt.Chart(cum_abs_df)
.mark_line(point=alt.OverlayMarkDef(filled=True, size=40), strokeWidth=2.5)
.encode(
x=alt.X("ccVersion:N", sort=ver_order_cum,
title="ccVersion (oldest → newest)",
axis=alt.Axis(labelAngle=-90, labelLimit=80, labelOverlap=False, labelFontSize=8)),
y=alt.Y("value:Q", title="cumulative rule sentences"),
color=alt.Color("status:N",
scale=alt.Scale(domain=["explained (paragraph)", "unexplained"],
range=["#59a14f", "#e15759"]),
legend=alt.Legend(title="rule status", orient="right")),
tooltip=[
alt.Tooltip("ccVersion:N"),
alt.Tooltip("status:N"),
alt.Tooltip("value:Q", format=",.0f", title="cumulative count"),
alt.Tooltip("n_files_so_far:Q", title="files ≤ V"),
],
)
.properties(width=820, height=220,
title="Cumulative absolute count of explained vs unexplained rule sentences (from n≥20)")
)
cumulative_welfare = alt.vconcat(cum_re_chart, cum_jp_chart, cum_abs_chart).resolve_scale(
color="independent", x="shared"
).properties(
title=alt.TitleParams(
"Cumulative welfare trends over ccVersion",
subtitle=[
"Top: rate of rules explained, count-weighted (Σ explained / Σ rules).",
"Middle: judgment/procedural ratio, count-weighted. Bottom: absolute volume.",
"All panels start at the first version with ≥20 cumulative files (v2.1.18).",
],
anchor="start",
)
)
save_chart(cumulative_welfare, "15-cumulative-welfare-trends")