Spaces:
Sleeping
Sleeping
Fixed app.py and data insights page
Browse files
scheduler/dashboard/app.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
| 1 |
"""Main dashboard application for Court Scheduling System.
|
| 2 |
|
| 3 |
This is the entry point for the Streamlit multi-page dashboard.
|
| 4 |
-
Launch with: uv run court-scheduler dashboard
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
-
import subprocess
|
| 10 |
-
from pathlib import Path
|
| 11 |
|
| 12 |
import streamlit as st
|
| 13 |
|
|
@@ -21,29 +19,17 @@ st.set_page_config(
|
|
| 21 |
initial_sidebar_state="expanded",
|
| 22 |
)
|
| 23 |
|
| 24 |
-
# Enforce `uv` availability for all dashboard-triggered commands
|
| 25 |
-
try:
|
| 26 |
-
uv_check = subprocess.run(["uv", "--version"], capture_output=True, text=True)
|
| 27 |
-
if uv_check.returncode != 0:
|
| 28 |
-
raise RuntimeError(uv_check.stderr or "uv not available")
|
| 29 |
-
except Exception:
|
| 30 |
-
import streamlit as st
|
| 31 |
-
|
| 32 |
-
st.error(
|
| 33 |
-
"'uv' is required to run this dashboard's commands. Please install uv and rerun.\n\n"
|
| 34 |
-
"Install on macOS/Linux: `curl -LsSf https://astral.sh/uv/install.sh | sh`\n"
|
| 35 |
-
"Install on Windows (PowerShell): `irm https://astral.sh/uv/install.ps1 | iex`"
|
| 36 |
-
)
|
| 37 |
-
st.stop()
|
| 38 |
-
|
| 39 |
# Main page content
|
| 40 |
st.title("Court Scheduling System Dashboard")
|
| 41 |
-
st.markdown(
|
|
|
|
|
|
|
| 42 |
|
| 43 |
st.markdown("---")
|
| 44 |
|
| 45 |
# Introduction
|
| 46 |
-
st.markdown(
|
|
|
|
| 47 |
### Overview
|
| 48 |
|
| 49 |
This system provides data-driven scheduling recommendations while maintaining judicial control and autonomy.
|
|
@@ -57,7 +43,8 @@ This system provides data-driven scheduling recommendations while maintaining ju
|
|
| 57 |
- Reinforcement learning optimization
|
| 58 |
|
| 59 |
Use the sidebar to navigate between sections.
|
| 60 |
-
"""
|
|
|
|
| 61 |
|
| 62 |
# System status
|
| 63 |
status_header_col1, status_header_col2 = st.columns([3, 1])
|
|
@@ -93,47 +80,57 @@ with col3:
|
|
| 93 |
st.caption("Run EDA pipeline to generate visualizations")
|
| 94 |
|
| 95 |
# Setup Controls
|
| 96 |
-
eda_ready =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
if not eda_ready:
|
| 99 |
st.markdown("---")
|
| 100 |
st.markdown("### Initial Setup")
|
| 101 |
-
st.warning(
|
|
|
|
|
|
|
| 102 |
|
| 103 |
col1, col2 = st.columns([2, 1])
|
| 104 |
|
| 105 |
with col1:
|
| 106 |
-
st.markdown(
|
|
|
|
| 107 |
The EDA pipeline:
|
| 108 |
- Loads and cleans historical court case data
|
| 109 |
- Extracts statistical parameters (distributions, transition probabilities)
|
| 110 |
- Generates analysis visualizations
|
| 111 |
|
| 112 |
This is required before using other dashboard features.
|
| 113 |
-
"""
|
|
|
|
| 114 |
|
| 115 |
with col2:
|
| 116 |
if st.button("Run EDA Pipeline", type="primary", use_container_width=True):
|
| 117 |
-
import
|
|
|
|
|
|
|
| 118 |
|
| 119 |
with st.spinner("Running EDA pipeline... This may take a few minutes."):
|
| 120 |
try:
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
with st.expander("Show error details"):
|
| 134 |
-
st.code(result.stderr, language="text")
|
| 135 |
except Exception as e:
|
| 136 |
-
st.error(
|
|
|
|
|
|
|
| 137 |
|
| 138 |
with st.expander("Run manually via CLI"):
|
| 139 |
st.code("uv run court-scheduler eda", language="bash")
|
|
@@ -148,7 +145,8 @@ st.markdown("### Dashboard Sections")
|
|
| 148 |
col1, col2 = st.columns(2)
|
| 149 |
|
| 150 |
with col1:
|
| 151 |
-
st.markdown(
|
|
|
|
| 152 |
#### 1. Data & Insights
|
| 153 |
Explore historical case data, view analysis visualizations, and review extracted parameters.
|
| 154 |
|
|
@@ -157,10 +155,12 @@ with col1:
|
|
| 157 |
|
| 158 |
#### 3. Simulation Workflow
|
| 159 |
Generate cases, configure simulation parameters, run scheduling simulations, and view results.
|
| 160 |
-
"""
|
|
|
|
| 161 |
|
| 162 |
with col2:
|
| 163 |
-
st.markdown(
|
|
|
|
| 164 |
#### 4. Cause Lists & Overrides
|
| 165 |
View generated cause lists, make judge overrides, and track modification history.
|
| 166 |
|
|
@@ -169,13 +169,15 @@ with col2:
|
|
| 169 |
|
| 170 |
#### 6. Analytics & Reports
|
| 171 |
Compare simulation runs, analyze performance metrics, and export comprehensive reports.
|
| 172 |
-
"""
|
|
|
|
| 173 |
|
| 174 |
st.markdown("---")
|
| 175 |
|
| 176 |
# Typical Workflow
|
| 177 |
with st.expander("Typical Usage Workflow"):
|
| 178 |
-
st.markdown(
|
|
|
|
| 179 |
**Step 1: Initial Setup**
|
| 180 |
- Run EDA pipeline to process historical data (one-time setup)
|
| 181 |
|
|
@@ -202,7 +204,8 @@ with st.expander("Typical Usage Workflow"):
|
|
| 202 |
- Use Analytics & Reports to evaluate fairness and efficiency
|
| 203 |
- Compare different scheduling policies
|
| 204 |
- Identify bottlenecks and improvement opportunities
|
| 205 |
-
"""
|
|
|
|
| 206 |
|
| 207 |
# Footer
|
| 208 |
st.markdown("---")
|
|
|
|
| 1 |
"""Main dashboard application for Court Scheduling System.
|
| 2 |
|
| 3 |
This is the entry point for the Streamlit multi-page dashboard.
|
| 4 |
+
Launch with: uv run court-scheduler dashboard (or `streamlit run` directly)
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
|
|
|
|
|
|
| 9 |
|
| 10 |
import streamlit as st
|
| 11 |
|
|
|
|
| 19 |
initial_sidebar_state="expanded",
|
| 20 |
)
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# Main page content
|
| 23 |
st.title("Court Scheduling System Dashboard")
|
| 24 |
+
st.markdown(
|
| 25 |
+
"**Karnataka High Court - Algorithmic Decision Support for Fair Scheduling**"
|
| 26 |
+
)
|
| 27 |
|
| 28 |
st.markdown("---")
|
| 29 |
|
| 30 |
# Introduction
|
| 31 |
+
st.markdown(
|
| 32 |
+
"""
|
| 33 |
### Overview
|
| 34 |
|
| 35 |
This system provides data-driven scheduling recommendations while maintaining judicial control and autonomy.
|
|
|
|
| 43 |
- Reinforcement learning optimization
|
| 44 |
|
| 45 |
Use the sidebar to navigate between sections.
|
| 46 |
+
"""
|
| 47 |
+
)
|
| 48 |
|
| 49 |
# System status
|
| 50 |
status_header_col1, status_header_col2 = st.columns([3, 1])
|
|
|
|
| 80 |
st.caption("Run EDA pipeline to generate visualizations")
|
| 81 |
|
| 82 |
# Setup Controls
|
| 83 |
+
eda_ready = (
|
| 84 |
+
data_status["cleaned_data"]
|
| 85 |
+
and data_status["parameters"]
|
| 86 |
+
and data_status["eda_figures"]
|
| 87 |
+
)
|
| 88 |
|
| 89 |
if not eda_ready:
|
| 90 |
st.markdown("---")
|
| 91 |
st.markdown("### Initial Setup")
|
| 92 |
+
st.warning(
|
| 93 |
+
"Run the EDA pipeline to process historical data and extract parameters."
|
| 94 |
+
)
|
| 95 |
|
| 96 |
col1, col2 = st.columns([2, 1])
|
| 97 |
|
| 98 |
with col1:
|
| 99 |
+
st.markdown(
|
| 100 |
+
"""
|
| 101 |
The EDA pipeline:
|
| 102 |
- Loads and cleans historical court case data
|
| 103 |
- Extracts statistical parameters (distributions, transition probabilities)
|
| 104 |
- Generates analysis visualizations
|
| 105 |
|
| 106 |
This is required before using other dashboard features.
|
| 107 |
+
"""
|
| 108 |
+
)
|
| 109 |
|
| 110 |
with col2:
|
| 111 |
if st.button("Run EDA Pipeline", type="primary", use_container_width=True):
|
| 112 |
+
from eda.load_clean import run_load_and_clean
|
| 113 |
+
from eda.exploration import run_exploration
|
| 114 |
+
from eda.parameters import run_parameter_export
|
| 115 |
|
| 116 |
with st.spinner("Running EDA pipeline... This may take a few minutes."):
|
| 117 |
try:
|
| 118 |
+
# Step 1: Load & clean data
|
| 119 |
+
run_load_and_clean()
|
| 120 |
+
|
| 121 |
+
# Step 2: Generate visualizations
|
| 122 |
+
run_exploration()
|
| 123 |
+
|
| 124 |
+
# Step 3: Extract parameters
|
| 125 |
+
run_parameter_export()
|
| 126 |
+
|
| 127 |
+
st.success("EDA pipeline completed")
|
| 128 |
+
st.rerun()
|
| 129 |
+
|
|
|
|
|
|
|
| 130 |
except Exception as e:
|
| 131 |
+
st.error("Pipeline failed while running inside the dashboard.")
|
| 132 |
+
with st.expander("Show error details"):
|
| 133 |
+
st.exception(e)
|
| 134 |
|
| 135 |
with st.expander("Run manually via CLI"):
|
| 136 |
st.code("uv run court-scheduler eda", language="bash")
|
|
|
|
| 145 |
col1, col2 = st.columns(2)
|
| 146 |
|
| 147 |
with col1:
|
| 148 |
+
st.markdown(
|
| 149 |
+
"""
|
| 150 |
#### 1. Data & Insights
|
| 151 |
Explore historical case data, view analysis visualizations, and review extracted parameters.
|
| 152 |
|
|
|
|
| 155 |
|
| 156 |
#### 3. Simulation Workflow
|
| 157 |
Generate cases, configure simulation parameters, run scheduling simulations, and view results.
|
| 158 |
+
"""
|
| 159 |
+
)
|
| 160 |
|
| 161 |
with col2:
|
| 162 |
+
st.markdown(
|
| 163 |
+
"""
|
| 164 |
#### 4. Cause Lists & Overrides
|
| 165 |
View generated cause lists, make judge overrides, and track modification history.
|
| 166 |
|
|
|
|
| 169 |
|
| 170 |
#### 6. Analytics & Reports
|
| 171 |
Compare simulation runs, analyze performance metrics, and export comprehensive reports.
|
| 172 |
+
"""
|
| 173 |
+
)
|
| 174 |
|
| 175 |
st.markdown("---")
|
| 176 |
|
| 177 |
# Typical Workflow
|
| 178 |
with st.expander("Typical Usage Workflow"):
|
| 179 |
+
st.markdown(
|
| 180 |
+
"""
|
| 181 |
**Step 1: Initial Setup**
|
| 182 |
- Run EDA pipeline to process historical data (one-time setup)
|
| 183 |
|
|
|
|
| 204 |
- Use Analytics & Reports to evaluate fairness and efficiency
|
| 205 |
- Compare different scheduling policies
|
| 206 |
- Identify bottlenecks and improvement opportunities
|
| 207 |
+
"""
|
| 208 |
+
)
|
| 209 |
|
| 210 |
# Footer
|
| 211 |
st.markdown("---")
|
scheduler/dashboard/pages/1_Data_And_Insights.py
CHANGED
|
@@ -70,7 +70,9 @@ def load_dashboard_data():
|
|
| 70 |
|
| 71 |
with st.spinner("Loading data..."):
|
| 72 |
try:
|
| 73 |
-
cases_df, hearings_df, params, stats, total_cases, total_hearings =
|
|
|
|
|
|
|
| 74 |
except Exception as e:
|
| 75 |
st.error(f"Error loading data: {e}")
|
| 76 |
st.info("Please run the EDA pipeline first: `uv run court-scheduler eda`")
|
|
@@ -96,28 +98,25 @@ if cases_df.empty and hearings_df.empty:
|
|
| 96 |
|
| 97 |
with col1:
|
| 98 |
if st.button("Run EDA Pipeline Now", type="primary", use_container_width=True):
|
| 99 |
-
import
|
|
|
|
|
|
|
| 100 |
|
| 101 |
with st.spinner("Running EDA pipeline... This will take a few minutes."):
|
| 102 |
try:
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
)
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
st.
|
| 113 |
-
if st.button("Reload Page"):
|
| 114 |
-
st.rerun()
|
| 115 |
-
else:
|
| 116 |
-
st.error(f"Pipeline failed with error code {result.returncode}")
|
| 117 |
-
with st.expander("Error details"):
|
| 118 |
-
st.code(result.stderr, language="text")
|
| 119 |
except Exception as e:
|
| 120 |
-
st.
|
|
|
|
| 121 |
|
| 122 |
with col2:
|
| 123 |
with st.expander("Alternative: Run via CLI"):
|
|
@@ -133,7 +132,9 @@ col1, col2, col3, col4, col5 = st.columns(5)
|
|
| 133 |
with col1:
|
| 134 |
st.metric("Total Cases", f"{total_cases:,}")
|
| 135 |
if "YEAR_FILED" in cases_df.columns:
|
| 136 |
-
year_range =
|
|
|
|
|
|
|
| 137 |
st.caption(f"Years: {year_range}")
|
| 138 |
|
| 139 |
with col2:
|
|
@@ -176,7 +177,9 @@ with col5:
|
|
| 176 |
st.markdown("---")
|
| 177 |
|
| 178 |
# Main tabs
|
| 179 |
-
tab1, tab2, tab3 = st.tabs(
|
|
|
|
|
|
|
| 180 |
|
| 181 |
# TAB 1: Historical Analysis - Pre-generated figures
|
| 182 |
with tab1:
|
|
@@ -188,11 +191,15 @@ with tab1:
|
|
| 188 |
figures_dir = Path("reports/figures")
|
| 189 |
|
| 190 |
if not figures_dir.exists():
|
| 191 |
-
st.warning(
|
|
|
|
|
|
|
| 192 |
st.code("uv run court-scheduler eda")
|
| 193 |
else:
|
| 194 |
# Find latest versioned directory
|
| 195 |
-
version_dirs = [
|
|
|
|
|
|
|
| 196 |
|
| 197 |
if not version_dirs:
|
| 198 |
st.warning(
|
|
@@ -207,7 +214,9 @@ with tab1:
|
|
| 207 |
# List available figures from the versioned directory
|
| 208 |
# Exclude deprecated/removed visuals like the monthly waterfall
|
| 209 |
figure_files = [
|
| 210 |
-
f
|
|
|
|
|
|
|
| 211 |
]
|
| 212 |
|
| 213 |
if not figure_files:
|
|
@@ -227,10 +236,14 @@ with tab1:
|
|
| 227 |
if any(x in f.name for x in ["stage", "sankey", "transition"])
|
| 228 |
]
|
| 229 |
time_figs = [
|
| 230 |
-
f
|
|
|
|
|
|
|
| 231 |
]
|
| 232 |
other_figs = [
|
| 233 |
-
f
|
|
|
|
|
|
|
| 234 |
]
|
| 235 |
|
| 236 |
# Category 1: Case Distributions
|
|
@@ -325,7 +338,9 @@ with tab2:
|
|
| 325 |
selected_stages = st.sidebar.multiselect(
|
| 326 |
"Stages",
|
| 327 |
options=available_stages,
|
| 328 |
-
default=available_stages[:10]
|
|
|
|
|
|
|
| 329 |
key="stage_filter",
|
| 330 |
)
|
| 331 |
else:
|
|
@@ -334,12 +349,16 @@ with tab2:
|
|
| 334 |
|
| 335 |
# Apply filters with copy to ensure clean dataframes
|
| 336 |
if selected_case_types and case_type_col:
|
| 337 |
-
filtered_cases = cases_df[
|
|
|
|
|
|
|
| 338 |
else:
|
| 339 |
filtered_cases = cases_df.copy()
|
| 340 |
|
| 341 |
if selected_stages and stage_col:
|
| 342 |
-
filtered_hearings = hearings_df[
|
|
|
|
|
|
|
| 343 |
else:
|
| 344 |
filtered_hearings = hearings_df.copy()
|
| 345 |
|
|
@@ -370,9 +389,9 @@ with tab2:
|
|
| 370 |
|
| 371 |
with col4:
|
| 372 |
if "Outcome" in filtered_hearings.columns and len(filtered_hearings) > 0:
|
| 373 |
-
adj_rate_filtered = (
|
| 374 |
-
filtered_hearings
|
| 375 |
-
)
|
| 376 |
st.metric("Adjournment Rate", f"{adj_rate_filtered:.1%}")
|
| 377 |
else:
|
| 378 |
st.metric("Adjournment Rate", "N/A")
|
|
@@ -387,9 +406,15 @@ with tab2:
|
|
| 387 |
with sub_tab1:
|
| 388 |
st.markdown("#### Case Distribution by Type")
|
| 389 |
|
| 390 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
# Compute value counts and ensure proper structure
|
| 392 |
-
case_type_counts =
|
|
|
|
|
|
|
| 393 |
# Rename columns for clarity (works across pandas versions)
|
| 394 |
case_type_counts.columns = ["CaseType", "Count"]
|
| 395 |
|
|
@@ -428,7 +453,11 @@ with tab2:
|
|
| 428 |
with sub_tab2:
|
| 429 |
st.markdown("#### Stage Analysis")
|
| 430 |
|
| 431 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
stage_counts = filtered_hearings[stage_col].value_counts().reset_index()
|
| 433 |
stage_counts.columns = ["Stage", "Count"]
|
| 434 |
|
|
@@ -465,7 +494,10 @@ with tab2:
|
|
| 465 |
not_adjourned = total_hearings - adjourned
|
| 466 |
|
| 467 |
outcome_df = pd.DataFrame(
|
| 468 |
-
{
|
|
|
|
|
|
|
|
|
|
| 469 |
)
|
| 470 |
|
| 471 |
fig_pie = px.pie(
|
|
@@ -474,7 +506,10 @@ with tab2:
|
|
| 474 |
names="Outcome",
|
| 475 |
title=f"Outcome Distribution (Total: {total_hearings:,})",
|
| 476 |
color="Outcome",
|
| 477 |
-
color_discrete_map={
|
|
|
|
|
|
|
|
|
|
| 478 |
)
|
| 479 |
fig_pie.update_layout(height=400)
|
| 480 |
st.plotly_chart(fig_pie, use_container_width=True)
|
|
@@ -483,7 +518,9 @@ with tab2:
|
|
| 483 |
st.markdown("**By Stage**")
|
| 484 |
adj_by_stage = (
|
| 485 |
filtered_hearings.groupby(stage_col)["Outcome"]
|
| 486 |
-
.apply(
|
|
|
|
|
|
|
| 487 |
.reset_index()
|
| 488 |
)
|
| 489 |
adj_by_stage.columns = ["Stage", "Rate"]
|
|
@@ -507,7 +544,9 @@ with tab2:
|
|
| 507 |
with sub_tab4:
|
| 508 |
st.markdown("#### Raw Data")
|
| 509 |
|
| 510 |
-
data_view = st.radio(
|
|
|
|
|
|
|
| 511 |
|
| 512 |
if data_view == "Cases":
|
| 513 |
st.dataframe(
|
|
@@ -516,7 +555,9 @@ with tab2:
|
|
| 516 |
height=600,
|
| 517 |
)
|
| 518 |
|
| 519 |
-
st.markdown(
|
|
|
|
|
|
|
| 520 |
|
| 521 |
# Download button
|
| 522 |
csv = filtered_cases.to_csv(index=False).encode("utf-8")
|
|
@@ -533,7 +574,9 @@ with tab2:
|
|
| 533 |
height=600,
|
| 534 |
)
|
| 535 |
|
| 536 |
-
st.markdown(
|
|
|
|
|
|
|
| 537 |
|
| 538 |
# Download button
|
| 539 |
csv = filtered_hearings.to_csv(index=False).encode("utf-8")
|
|
@@ -559,7 +602,10 @@ with tab3:
|
|
| 559 |
st.markdown("#### Case Types")
|
| 560 |
if "case_types" in params and params["case_types"]:
|
| 561 |
case_types_df = pd.DataFrame(
|
| 562 |
-
{
|
|
|
|
|
|
|
|
|
|
| 563 |
)
|
| 564 |
st.dataframe(case_types_df, use_container_width=True, hide_index=True)
|
| 565 |
st.caption(f"Total: {len(params['case_types'])} case types")
|
|
@@ -594,9 +640,13 @@ with tab3:
|
|
| 594 |
with st.expander(f"From: {stage}"):
|
| 595 |
trans_df = pd.DataFrame(transitions)
|
| 596 |
if not trans_df.empty:
|
| 597 |
-
st.dataframe(
|
|
|
|
|
|
|
| 598 |
|
| 599 |
-
st.caption(
|
|
|
|
|
|
|
| 600 |
else:
|
| 601 |
st.info("No stage transition data found")
|
| 602 |
|
|
@@ -609,8 +659,12 @@ with tab3:
|
|
| 609 |
|
| 610 |
# Create heatmap
|
| 611 |
adj_stats = params["adjournment_stats"]
|
| 612 |
-
stages_list = list(adj_stats.keys())[
|
| 613 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
|
| 615 |
if stages_list and case_types_list:
|
| 616 |
heatmap_data = []
|
|
@@ -656,7 +710,12 @@ with tab3:
|
|
| 656 |
""")
|
| 657 |
|
| 658 |
config_tab1, config_tab2, config_tab3, config_tab4 = st.tabs(
|
| 659 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
)
|
| 661 |
|
| 662 |
with config_tab1:
|
|
@@ -857,7 +916,10 @@ UNRIPE cases: 0.7x priority
|
|
| 857 |
from scheduler.data.config import MONTHLY_SEASONALITY
|
| 858 |
|
| 859 |
season_df = pd.DataFrame(
|
| 860 |
-
[
|
|
|
|
|
|
|
|
|
|
| 861 |
)
|
| 862 |
st.dataframe(season_df, use_container_width=True, hide_index=True)
|
| 863 |
st.caption("1.0 = average, >1.0 = more cases, <1.0 = fewer cases")
|
|
@@ -900,7 +962,9 @@ Ripe purposes (80% probability):
|
|
| 900 |
""",
|
| 901 |
language="text",
|
| 902 |
)
|
| 903 |
-
st.caption(
|
|
|
|
|
|
|
| 904 |
|
| 905 |
with config_tab4:
|
| 906 |
st.markdown("#### Simulation Defaults")
|
|
@@ -930,8 +994,12 @@ Formula:
|
|
| 930 |
st.markdown("**Courtroom Capacity**")
|
| 931 |
if params and "court_capacity_global" in params:
|
| 932 |
cap = params["court_capacity_global"]
|
| 933 |
-
st.metric(
|
| 934 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 935 |
else:
|
| 936 |
st.info("Run EDA to load capacity statistics")
|
| 937 |
|
|
|
|
| 70 |
|
| 71 |
with st.spinner("Loading data..."):
|
| 72 |
try:
|
| 73 |
+
cases_df, hearings_df, params, stats, total_cases, total_hearings = (
|
| 74 |
+
load_dashboard_data()
|
| 75 |
+
)
|
| 76 |
except Exception as e:
|
| 77 |
st.error(f"Error loading data: {e}")
|
| 78 |
st.info("Please run the EDA pipeline first: `uv run court-scheduler eda`")
|
|
|
|
| 98 |
|
| 99 |
with col1:
|
| 100 |
if st.button("Run EDA Pipeline Now", type="primary", use_container_width=True):
|
| 101 |
+
from eda.load_clean import run_load_and_clean
|
| 102 |
+
from eda.exploration import run_exploration
|
| 103 |
+
from eda.parameters import run_parameter_export
|
| 104 |
|
| 105 |
with st.spinner("Running EDA pipeline... This will take a few minutes."):
|
| 106 |
try:
|
| 107 |
+
# Step 1: Load & clean data
|
| 108 |
+
run_load_and_clean()
|
| 109 |
+
# Step 2: Generate visualizations
|
| 110 |
+
run_exploration()
|
| 111 |
+
# Step 3: Extract parameters
|
| 112 |
+
run_parameter_export()
|
| 113 |
+
st.success("EDA pipeline completed successfully!")
|
| 114 |
+
st.info("Reload this page to see the updated data.")
|
| 115 |
+
if st.button("Reload Page"):
|
| 116 |
+
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
except Exception as e:
|
| 118 |
+
with st.expander("Error details"):
|
| 119 |
+
st.exception(e)
|
| 120 |
|
| 121 |
with col2:
|
| 122 |
with st.expander("Alternative: Run via CLI"):
|
|
|
|
| 132 |
with col1:
|
| 133 |
st.metric("Total Cases", f"{total_cases:,}")
|
| 134 |
if "YEAR_FILED" in cases_df.columns:
|
| 135 |
+
year_range = (
|
| 136 |
+
f"{cases_df['YEAR_FILED'].min():.0f}-{cases_df['YEAR_FILED'].max():.0f}"
|
| 137 |
+
)
|
| 138 |
st.caption(f"Years: {year_range}")
|
| 139 |
|
| 140 |
with col2:
|
|
|
|
| 177 |
st.markdown("---")
|
| 178 |
|
| 179 |
# Main tabs
|
| 180 |
+
tab1, tab2, tab3 = st.tabs(
|
| 181 |
+
["Historical Analysis", "Interactive Exploration", "Parameters"]
|
| 182 |
+
)
|
| 183 |
|
| 184 |
# TAB 1: Historical Analysis - Pre-generated figures
|
| 185 |
with tab1:
|
|
|
|
| 191 |
figures_dir = Path("reports/figures")
|
| 192 |
|
| 193 |
if not figures_dir.exists():
|
| 194 |
+
st.warning(
|
| 195 |
+
"EDA figures not found. Run the EDA pipeline to generate visualizations."
|
| 196 |
+
)
|
| 197 |
st.code("uv run court-scheduler eda")
|
| 198 |
else:
|
| 199 |
# Find latest versioned directory
|
| 200 |
+
version_dirs = [
|
| 201 |
+
d for d in figures_dir.iterdir() if d.is_dir() and d.name.startswith("v")
|
| 202 |
+
]
|
| 203 |
|
| 204 |
if not version_dirs:
|
| 205 |
st.warning(
|
|
|
|
| 214 |
# List available figures from the versioned directory
|
| 215 |
# Exclude deprecated/removed visuals like the monthly waterfall
|
| 216 |
figure_files = [
|
| 217 |
+
f
|
| 218 |
+
for f in sorted(latest_dir.glob("*.html"))
|
| 219 |
+
if "waterfall" not in f.name.lower()
|
| 220 |
]
|
| 221 |
|
| 222 |
if not figure_files:
|
|
|
|
| 236 |
if any(x in f.name for x in ["stage", "sankey", "transition"])
|
| 237 |
]
|
| 238 |
time_figs = [
|
| 239 |
+
f
|
| 240 |
+
for f in figure_files
|
| 241 |
+
if any(x in f.name for x in ["monthly", "load", "gap"])
|
| 242 |
]
|
| 243 |
other_figs = [
|
| 244 |
+
f
|
| 245 |
+
for f in figure_files
|
| 246 |
+
if f not in distribution_figs + stage_figs + time_figs
|
| 247 |
]
|
| 248 |
|
| 249 |
# Category 1: Case Distributions
|
|
|
|
| 338 |
selected_stages = st.sidebar.multiselect(
|
| 339 |
"Stages",
|
| 340 |
options=available_stages,
|
| 341 |
+
default=available_stages[:10]
|
| 342 |
+
if len(available_stages) > 10
|
| 343 |
+
else available_stages,
|
| 344 |
key="stage_filter",
|
| 345 |
)
|
| 346 |
else:
|
|
|
|
| 349 |
|
| 350 |
# Apply filters with copy to ensure clean dataframes
|
| 351 |
if selected_case_types and case_type_col:
|
| 352 |
+
filtered_cases = cases_df[
|
| 353 |
+
cases_df[case_type_col].isin(selected_case_types)
|
| 354 |
+
].copy()
|
| 355 |
else:
|
| 356 |
filtered_cases = cases_df.copy()
|
| 357 |
|
| 358 |
if selected_stages and stage_col:
|
| 359 |
+
filtered_hearings = hearings_df[
|
| 360 |
+
hearings_df[stage_col].isin(selected_stages)
|
| 361 |
+
].copy()
|
| 362 |
else:
|
| 363 |
filtered_hearings = hearings_df.copy()
|
| 364 |
|
|
|
|
| 389 |
|
| 390 |
with col4:
|
| 391 |
if "Outcome" in filtered_hearings.columns and len(filtered_hearings) > 0:
|
| 392 |
+
adj_rate_filtered = (
|
| 393 |
+
filtered_hearings["Outcome"] == "ADJOURNED"
|
| 394 |
+
).sum() / len(filtered_hearings)
|
| 395 |
st.metric("Adjournment Rate", f"{adj_rate_filtered:.1%}")
|
| 396 |
else:
|
| 397 |
st.metric("Adjournment Rate", "N/A")
|
|
|
|
| 406 |
with sub_tab1:
|
| 407 |
st.markdown("#### Case Distribution by Type")
|
| 408 |
|
| 409 |
+
if (
|
| 410 |
+
case_type_col
|
| 411 |
+
and case_type_col in filtered_cases.columns
|
| 412 |
+
and len(filtered_cases) > 0
|
| 413 |
+
):
|
| 414 |
# Compute value counts and ensure proper structure
|
| 415 |
+
case_type_counts = (
|
| 416 |
+
filtered_cases[case_type_col].value_counts().reset_index()
|
| 417 |
+
)
|
| 418 |
# Rename columns for clarity (works across pandas versions)
|
| 419 |
case_type_counts.columns = ["CaseType", "Count"]
|
| 420 |
|
|
|
|
| 453 |
with sub_tab2:
|
| 454 |
st.markdown("#### Stage Analysis")
|
| 455 |
|
| 456 |
+
if (
|
| 457 |
+
stage_col
|
| 458 |
+
and stage_col in filtered_hearings.columns
|
| 459 |
+
and len(filtered_hearings) > 0
|
| 460 |
+
):
|
| 461 |
stage_counts = filtered_hearings[stage_col].value_counts().reset_index()
|
| 462 |
stage_counts.columns = ["Stage", "Count"]
|
| 463 |
|
|
|
|
| 494 |
not_adjourned = total_hearings - adjourned
|
| 495 |
|
| 496 |
outcome_df = pd.DataFrame(
|
| 497 |
+
{
|
| 498 |
+
"Outcome": ["ADJOURNED", "NOT ADJOURNED"],
|
| 499 |
+
"Count": [adjourned, not_adjourned],
|
| 500 |
+
}
|
| 501 |
)
|
| 502 |
|
| 503 |
fig_pie = px.pie(
|
|
|
|
| 506 |
names="Outcome",
|
| 507 |
title=f"Outcome Distribution (Total: {total_hearings:,})",
|
| 508 |
color="Outcome",
|
| 509 |
+
color_discrete_map={
|
| 510 |
+
"ADJOURNED": "#ef4444",
|
| 511 |
+
"NOT ADJOURNED": "#22c55e",
|
| 512 |
+
},
|
| 513 |
)
|
| 514 |
fig_pie.update_layout(height=400)
|
| 515 |
st.plotly_chart(fig_pie, use_container_width=True)
|
|
|
|
| 518 |
st.markdown("**By Stage**")
|
| 519 |
adj_by_stage = (
|
| 520 |
filtered_hearings.groupby(stage_col)["Outcome"]
|
| 521 |
+
.apply(
|
| 522 |
+
lambda x: (x == "ADJOURNED").sum() / len(x) if len(x) > 0 else 0
|
| 523 |
+
)
|
| 524 |
.reset_index()
|
| 525 |
)
|
| 526 |
adj_by_stage.columns = ["Stage", "Rate"]
|
|
|
|
| 544 |
with sub_tab4:
|
| 545 |
st.markdown("#### Raw Data")
|
| 546 |
|
| 547 |
+
data_view = st.radio(
|
| 548 |
+
"Select data to view:", ["Cases", "Hearings"], horizontal=True
|
| 549 |
+
)
|
| 550 |
|
| 551 |
if data_view == "Cases":
|
| 552 |
st.dataframe(
|
|
|
|
| 555 |
height=600,
|
| 556 |
)
|
| 557 |
|
| 558 |
+
st.markdown(
|
| 559 |
+
f"**Showing first 500 of {len(filtered_cases):,} filtered cases**"
|
| 560 |
+
)
|
| 561 |
|
| 562 |
# Download button
|
| 563 |
csv = filtered_cases.to_csv(index=False).encode("utf-8")
|
|
|
|
| 574 |
height=600,
|
| 575 |
)
|
| 576 |
|
| 577 |
+
st.markdown(
|
| 578 |
+
f"**Showing first 500 of {len(filtered_hearings):,} filtered hearings**"
|
| 579 |
+
)
|
| 580 |
|
| 581 |
# Download button
|
| 582 |
csv = filtered_hearings.to_csv(index=False).encode("utf-8")
|
|
|
|
| 602 |
st.markdown("#### Case Types")
|
| 603 |
if "case_types" in params and params["case_types"]:
|
| 604 |
case_types_df = pd.DataFrame(
|
| 605 |
+
{
|
| 606 |
+
"Case Type": params["case_types"],
|
| 607 |
+
"Index": range(len(params["case_types"])),
|
| 608 |
+
}
|
| 609 |
)
|
| 610 |
st.dataframe(case_types_df, use_container_width=True, hide_index=True)
|
| 611 |
st.caption(f"Total: {len(params['case_types'])} case types")
|
|
|
|
| 640 |
with st.expander(f"From: {stage}"):
|
| 641 |
trans_df = pd.DataFrame(transitions)
|
| 642 |
if not trans_df.empty:
|
| 643 |
+
st.dataframe(
|
| 644 |
+
trans_df, use_container_width=True, hide_index=True
|
| 645 |
+
)
|
| 646 |
|
| 647 |
+
st.caption(
|
| 648 |
+
f"Total: {len(params['stage_graph'])} stages with transition data"
|
| 649 |
+
)
|
| 650 |
else:
|
| 651 |
st.info("No stage transition data found")
|
| 652 |
|
|
|
|
| 659 |
|
| 660 |
# Create heatmap
|
| 661 |
adj_stats = params["adjournment_stats"]
|
| 662 |
+
stages_list = list(adj_stats.keys())[
|
| 663 |
+
:20
|
| 664 |
+
] # Limit to 20 stages for readability
|
| 665 |
+
case_types_list = params.get("case_types", [])[
|
| 666 |
+
:15
|
| 667 |
+
] # Limit to 15 case types
|
| 668 |
|
| 669 |
if stages_list and case_types_list:
|
| 670 |
heatmap_data = []
|
|
|
|
| 710 |
""")
|
| 711 |
|
| 712 |
config_tab1, config_tab2, config_tab3, config_tab4 = st.tabs(
|
| 713 |
+
[
|
| 714 |
+
"EDA Parameters",
|
| 715 |
+
"Ripeness Classifier",
|
| 716 |
+
"Case Generator",
|
| 717 |
+
"Simulation Defaults",
|
| 718 |
+
]
|
| 719 |
)
|
| 720 |
|
| 721 |
with config_tab1:
|
|
|
|
| 916 |
from scheduler.data.config import MONTHLY_SEASONALITY
|
| 917 |
|
| 918 |
season_df = pd.DataFrame(
|
| 919 |
+
[
|
| 920 |
+
{"Month": i, "Factor": MONTHLY_SEASONALITY.get(i, 1.0)}
|
| 921 |
+
for i in range(1, 13)
|
| 922 |
+
]
|
| 923 |
)
|
| 924 |
st.dataframe(season_df, use_container_width=True, hide_index=True)
|
| 925 |
st.caption("1.0 = average, >1.0 = more cases, <1.0 = fewer cases")
|
|
|
|
| 962 |
""",
|
| 963 |
language="text",
|
| 964 |
)
|
| 965 |
+
st.caption(
|
| 966 |
+
"Early ADMISSION: 40% bottleneck, Advanced stages: mostly ripe"
|
| 967 |
+
)
|
| 968 |
|
| 969 |
with config_tab4:
|
| 970 |
st.markdown("#### Simulation Defaults")
|
|
|
|
| 994 |
st.markdown("**Courtroom Capacity**")
|
| 995 |
if params and "court_capacity_global" in params:
|
| 996 |
cap = params["court_capacity_global"]
|
| 997 |
+
st.metric(
|
| 998 |
+
"Median slots/day", f"{cap.get('slots_median_global', 151):.0f}"
|
| 999 |
+
)
|
| 1000 |
+
st.metric(
|
| 1001 |
+
"P90 slots/day", f"{cap.get('slots_p90_global', 200):.0f}"
|
| 1002 |
+
)
|
| 1003 |
else:
|
| 1004 |
st.info("Run EDA to load capacity statistics")
|
| 1005 |
|
scheduler/dashboard/pages/2_Ripeness_Classifier.py
CHANGED
|
@@ -99,7 +99,9 @@ RipenessClassifier.set_thresholds(
|
|
| 99 |
)
|
| 100 |
|
| 101 |
# Main content
|
| 102 |
-
tab1, tab2, tab3 = st.tabs(
|
|
|
|
|
|
|
| 103 |
|
| 104 |
with tab1:
|
| 105 |
st.markdown("### Current Classifier Configuration")
|
|
@@ -153,7 +155,10 @@ with tab1:
|
|
| 153 |
stage_rules = {
|
| 154 |
"PRE-TRIAL": {"min_days": 60, "keywords": ["affidavit filed", "reply filed"]},
|
| 155 |
"TRIAL": {"min_days": 45, "keywords": ["evidence complete", "cross complete"]},
|
| 156 |
-
"POST-TRIAL": {
|
|
|
|
|
|
|
|
|
|
| 157 |
"FINAL DISPOSAL": {"min_days": 15, "keywords": ["disposed", "judgment"]},
|
| 158 |
}
|
| 159 |
|
|
@@ -190,8 +195,12 @@ with tab2:
|
|
| 190 |
service_hearings_count = st.number_input(
|
| 191 |
"Service Hearings", min_value=0, max_value=20, value=3
|
| 192 |
)
|
| 193 |
-
days_in_stage = st.number_input(
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
# Keywords
|
| 197 |
has_keywords = st.multiselect(
|
|
@@ -213,7 +222,7 @@ with tab2:
|
|
| 213 |
|
| 214 |
test_case = Case(
|
| 215 |
case_id=case_id,
|
| 216 |
-
case_type=case_type,
|
| 217 |
filed_date=filed_date,
|
| 218 |
current_stage=case_stage,
|
| 219 |
status=CaseStatus.PENDING,
|
|
@@ -286,15 +295,25 @@ with tab3:
|
|
| 286 |
|
| 287 |
with col1:
|
| 288 |
pct = classifications["RIPE"] / len(cases) * 100
|
| 289 |
-
st.metric(
|
|
|
|
|
|
|
| 290 |
|
| 291 |
with col2:
|
| 292 |
pct = classifications["UNKNOWN"] / len(cases) * 100
|
| 293 |
-
st.metric(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
|
| 295 |
with col3:
|
| 296 |
pct = classifications["UNRIPE"] / len(cases) * 100
|
| 297 |
-
st.metric(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
|
| 299 |
# Pie chart
|
| 300 |
fig = px.pie(
|
|
@@ -302,7 +321,11 @@ with tab3:
|
|
| 302 |
names=list(classifications.keys()),
|
| 303 |
title="Classification Distribution",
|
| 304 |
color=list(classifications.keys()),
|
| 305 |
-
color_discrete_map={
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
)
|
| 307 |
st.plotly_chart(fig, use_container_width=True)
|
| 308 |
|
|
@@ -311,4 +334,6 @@ with tab3:
|
|
| 311 |
|
| 312 |
# Footer
|
| 313 |
st.markdown("---")
|
| 314 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
| 99 |
)
|
| 100 |
|
| 101 |
# Main content
|
| 102 |
+
tab1, tab2, tab3 = st.tabs(
|
| 103 |
+
["Current Configuration", "Interactive Testing", "Batch Classification"]
|
| 104 |
+
)
|
| 105 |
|
| 106 |
with tab1:
|
| 107 |
st.markdown("### Current Classifier Configuration")
|
|
|
|
| 155 |
stage_rules = {
|
| 156 |
"PRE-TRIAL": {"min_days": 60, "keywords": ["affidavit filed", "reply filed"]},
|
| 157 |
"TRIAL": {"min_days": 45, "keywords": ["evidence complete", "cross complete"]},
|
| 158 |
+
"POST-TRIAL": {
|
| 159 |
+
"min_days": 30,
|
| 160 |
+
"keywords": ["arguments complete", "written note"],
|
| 161 |
+
},
|
| 162 |
"FINAL DISPOSAL": {"min_days": 15, "keywords": ["disposed", "judgment"]},
|
| 163 |
}
|
| 164 |
|
|
|
|
| 195 |
service_hearings_count = st.number_input(
|
| 196 |
"Service Hearings", min_value=0, max_value=20, value=3
|
| 197 |
)
|
| 198 |
+
days_in_stage = st.number_input(
|
| 199 |
+
"Days in Stage", min_value=0, max_value=365, value=45
|
| 200 |
+
)
|
| 201 |
+
case_age = st.number_input(
|
| 202 |
+
"Case Age (days)", min_value=0, max_value=3650, value=120
|
| 203 |
+
)
|
| 204 |
|
| 205 |
# Keywords
|
| 206 |
has_keywords = st.multiselect(
|
|
|
|
| 222 |
|
| 223 |
test_case = Case(
|
| 224 |
case_id=case_id,
|
| 225 |
+
case_type=case_type,
|
| 226 |
filed_date=filed_date,
|
| 227 |
current_stage=case_stage,
|
| 228 |
status=CaseStatus.PENDING,
|
|
|
|
| 295 |
|
| 296 |
with col1:
|
| 297 |
pct = classifications["RIPE"] / len(cases) * 100
|
| 298 |
+
st.metric(
|
| 299 |
+
"RIPE Cases", f"{classifications['RIPE']:,}", f"{pct:.1f}%"
|
| 300 |
+
)
|
| 301 |
|
| 302 |
with col2:
|
| 303 |
pct = classifications["UNKNOWN"] / len(cases) * 100
|
| 304 |
+
st.metric(
|
| 305 |
+
"UNKNOWN Cases",
|
| 306 |
+
f"{classifications['UNKNOWN']:,}",
|
| 307 |
+
f"{pct:.1f}%",
|
| 308 |
+
)
|
| 309 |
|
| 310 |
with col3:
|
| 311 |
pct = classifications["UNRIPE"] / len(cases) * 100
|
| 312 |
+
st.metric(
|
| 313 |
+
"UNRIPE Cases",
|
| 314 |
+
f"{classifications['UNRIPE']:,}",
|
| 315 |
+
f"{pct:.1f}%",
|
| 316 |
+
)
|
| 317 |
|
| 318 |
# Pie chart
|
| 319 |
fig = px.pie(
|
|
|
|
| 321 |
names=list(classifications.keys()),
|
| 322 |
title="Classification Distribution",
|
| 323 |
color=list(classifications.keys()),
|
| 324 |
+
color_discrete_map={
|
| 325 |
+
"RIPE": "green",
|
| 326 |
+
"UNKNOWN": "orange",
|
| 327 |
+
"UNRIPE": "red",
|
| 328 |
+
},
|
| 329 |
)
|
| 330 |
st.plotly_chart(fig, use_container_width=True)
|
| 331 |
|
|
|
|
| 334 |
|
| 335 |
# Footer
|
| 336 |
st.markdown("---")
|
| 337 |
+
st.markdown(
|
| 338 |
+
"*Adjust thresholds in the sidebar to see real-time impact on classification*"
|
| 339 |
+
)
|