| """ |
| 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", {}) |
|
|
| |
| 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() |
|
|
| |
| 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() |
|
|
| |
| 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")), |
| ] |
| |
| 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_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]) |
|
|
| 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() |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|