HSeg_Demo / src /components /statistics.py
jmisidro's picture
Upload 7 files
39c1a17 verified
Raw
History Blame Contribute Delete
10.2 kB
"""
Statistics Panel — LLM Subject Extraction Demo
Shows aggregate and per-municipality metrics for a selected evaluation run.
"""
import streamlit as st
import plotly.graph_objects as go
import plotly.express as px
import pandas as pd
from typing import Dict, Any, Optional
def _fmt(val, decimals: int = 3) -> str:
if isinstance(val, (int, float)):
return f"{val:.{decimals}f}"
return str(val)
def render_statistics(run_data: Dict[str, Any]) -> None:
"""Render the statistics dashboard for a loaded run."""
aggregate = run_data.get("aggregate", {})
config = run_data.get("config", {})
documents = run_data.get("documents", {})
# ── Run Info ──────────────────────────────────────────────────────────────
st.markdown("### ⚙️ Run Configuration")
pcfg = config.get("pipeline_config", {})
cfg_cols = st.columns(4)
cfg_cols[0].metric("Backend", pcfg.get("backend", "—"))
cfg_cols[1].metric("Model", pcfg.get("model_name", "—"))
cfg_cols[2].metric("Temperature", pcfg.get("temperature", "—"))
cfg_cols[3].metric("Split", config.get("split", "—"))
extra_cols = st.columns(4)
extra_cols[0].metric("Few-shot", str(pcfg.get("use_few_shot", "—")))
extra_cols[1].metric("Fix Gaps", str(pcfg.get("fix_gaps", "—")))
extra_cols[2].metric("Max Tokens", pcfg.get("max_tokens", "—"))
extra_cols[3].metric("Tolerance (sent)", config.get("tolerance_sentences", "—"))
st.divider()
# ── High-level counts ─────────────────────────────────────────────────────
st.markdown("### 📦 Dataset Overview")
agg_cols = st.columns(4)
agg_cols[0].metric("Total Documents", aggregate.get("total_documents", "—"))
agg_cols[1].metric("Successful", aggregate.get("successful", "—"))
agg_cols[2].metric("Failed", aggregate.get("failed", 0))
agg_cols[3].metric(
"Municipalities", len(aggregate.get("municipalities", {}))
)
overall = aggregate.get("overall", {})
ov_cols = st.columns(4)
ov_cols[0].metric("Agenda Items GT", overall.get("total_agenda_items_gt", "—"))
ov_cols[1].metric("Agenda Items Predicted", overall.get("total_agenda_items_predicted", "—"))
ov_cols[2].metric("Subjects GT", overall.get("total_subjects_gt", "—"))
ov_cols[3].metric("Subjects Predicted", overall.get("total_subjects_predicted", "—"))
extra2_cols = st.columns(2)
extra2_cols[0].metric(
"Avg Subjects/Item (Predicted)",
_fmt(overall.get("avg_subjects_per_item_predicted", 0), 2),
)
extra2_cols[1].metric(
"Avg Subjects/Item (GT)",
_fmt(overall.get("avg_subjects_per_item_gt", 0), 2),
)
st.divider()
# ── Metric tables ─────────────────────────────────────────────────────────
def _metric_table(title: str, section_key: str):
st.markdown(f"### 📐 {title} Metrics")
m = aggregate.get(section_key, {})
if not m:
st.info(f"No {title.lower()} metrics available.")
return
metric_rows = [
("Boundary Precision", m.get("boundary_precision")),
("Boundary Recall", m.get("boundary_recall")),
("Boundary F1", m.get("boundary_f1")),
("Boundary Similarity", m.get("boundary_similarity")),
("BED Precision", m.get("bed_precision")),
("BED Recall", m.get("bed_recall")),
("BED F-measure", m.get("bed_fmeasure")),
("Segeval Pk", m.get("segeval_pk")),
("Segeval WindowDiff", m.get("segeval_windowdiff")),
]
# subject-only extras
if section_key == "subjects":
metric_rows += [
("Theme Accuracy", m.get("theme_accuracy")),
("Topic Precision", m.get("topic_precision")),
("Topic Recall", m.get("topic_recall")),
("Topic F1", m.get("topic_f1")),
]
count_rows = [
("True Positives", m.get("true_positives")),
("False Positives", m.get("false_positives")),
("False Negatives", m.get("false_negatives")),
("Num Predicted", m.get("num_predicted")),
("Num Ground Truth", m.get("num_ground_truth")),
]
left, right = st.columns(2)
with left:
st.markdown("**Boundary / BED / Segeval**")
df = pd.DataFrame(
[(name, _fmt(val)) for name, val in metric_rows if val is not None],
columns=["Metric", "Value"],
)
st.dataframe(df, use_container_width=True, hide_index=True)
with right:
st.markdown("**Counts**")
df2 = pd.DataFrame(
[(name, val) for name, val in count_rows if val is not None],
columns=["Metric", "Value"],
)
st.dataframe(df2, use_container_width=True, hide_index=True)
# Radar chart of key metrics
radar_metrics = {
"Boundary P": m.get("boundary_precision", 0),
"Boundary R": m.get("boundary_recall", 0),
"Boundary F1": m.get("boundary_f1", 0),
"BED F-measure": m.get("bed_fmeasure", 0),
"BS": m.get("boundary_similarity", 0),
}
labels = list(radar_metrics.keys())
values = list(radar_metrics.values())
values.append(values[0]) # close the polygon
fig = go.Figure(
data=[
go.Scatterpolar(
r=values,
theta=labels + [labels[0]],
fill="toself",
line_color="#4C72B0",
fillcolor="rgba(76,114,176,0.25)",
name=title,
)
],
layout=go.Layout(
polar=dict(radialaxis=dict(visible=True, range=[0, 1])),
showlegend=False,
margin=dict(l=30, r=30, t=40, b=20),
height=280,
),
)
st.plotly_chart(fig, use_container_width=True)
_metric_table("Agenda Items", "agenda_items")
st.divider()
_metric_table("Subjects", "subjects")
st.divider()
# ── Municipality breakdown ────────────────────────────────────────────────
st.markdown("### 🏛️ Municipality Breakdown")
municipalities = aggregate.get("municipalities", {})
if municipalities:
muni_rows = []
for muni, info in municipalities.items():
muni_rows.append(
{
"Municipality": muni,
"Documents": info.get("documents", 0),
"Agenda Items GT": info.get("agenda_items_gt", 0),
"Subjects GT": info.get("subjects_gt", 0),
}
)
mdf = pd.DataFrame(muni_rows)
st.dataframe(mdf, use_container_width=True, hide_index=True)
fig = px.bar(
mdf,
x="Municipality",
y=["Agenda Items GT", "Subjects GT"],
barmode="group",
title="Ground-Truth Counts by Municipality",
color_discrete_sequence=["#4C72B0", "#DD8452"],
)
fig.update_layout(height=350, margin=dict(t=40, b=20))
st.plotly_chart(fig, use_container_width=True)
# ── Per-document metric distribution ─────────────────────────────────────
if documents:
st.divider()
st.markdown("### 📄 Per-Document Metric Distribution")
doc_rows = []
for doc_id, doc_data in documents.items():
ev = doc_data.get("evaluation", {})
ai = ev.get("agenda_items", {})
subj = ev.get("subjects", {})
doc_rows.append(
{
"Document": doc_id,
"Municipality": doc_data.get("municipality", ""),
"AI Boundary F1": ai.get("boundary_f1"),
"AI BED F-measure": ai.get("bed_fmeasure"),
"Subj Boundary F1": subj.get("boundary_f1"),
"Subj BED F-measure": subj.get("bed_fmeasure"),
"Subj Boundary Sim": subj.get("boundary_similarity"),
"AI #Predicted": ai.get("num_predicted"),
"AI #GT": ai.get("num_ground_truth"),
"Subj #Predicted": subj.get("num_predicted"),
"Subj #GT": subj.get("num_ground_truth"),
}
)
doc_df = pd.DataFrame(doc_rows)
st.dataframe(doc_df, use_container_width=True, hide_index=True)
# Distribution plots
metric_col = st.selectbox(
"Plot distribution for metric",
["AI Boundary F1", "AI BED F-measure", "Subj Boundary F1", "Subj BED F-measure"],
key="stats_dist_metric",
)
fig2 = px.histogram(
doc_df,
x=metric_col,
color="Municipality",
nbins=15,
title=f"Distribution of {metric_col}",
opacity=0.8,
)
fig2.update_layout(height=350, margin=dict(t=40, b=20))
st.plotly_chart(fig2, use_container_width=True)
# Scatter: AI vs Subject performance
fig3 = px.scatter(
doc_df,
x="AI Boundary F1",
y="Subj Boundary F1",
color="Municipality",
hover_data=["Document"],
title="Agenda Items vs Subject Boundary F1 per Document",
size_max=12,
)
fig3.update_layout(height=400, margin=dict(t=40, b=20))
st.plotly_chart(fig3, use_container_width=True)