RoyAalekh commited on
Commit
9eaac57
·
1 Parent(s): 7dfc8d9

enhancements, added view for scehduled cases as tickets

Browse files
app.py CHANGED
@@ -136,6 +136,31 @@ if not eda_ready:
136
  st.code("uv run court-scheduler eda", language="bash")
137
  else:
138
  st.success("System ready - all data processed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  st.markdown("---")
141
 
@@ -164,8 +189,8 @@ with col2:
164
  #### 4. Cause Lists & Overrides
165
  View generated cause lists, make judge overrides, and track modification history.
166
 
167
- #### 5. RL Training
168
- Train reinforcement learning models for optimized scheduling policies.
169
 
170
  #### 6. Analytics & Reports
171
  Compare simulation runs, analyze performance metrics, and export comprehensive reports.
@@ -210,3 +235,4 @@ with st.expander("Typical Usage Workflow"):
210
  # Footer
211
  st.markdown("---")
212
  st.caption("Court Scheduling System - Code4Change Hackathon - Karnataka High Court")
 
 
136
  st.code("uv run court-scheduler eda", language="bash")
137
  else:
138
  st.success("System ready - all data processed")
139
+ # Allow user to override and re-run EDA even if it's already completed
140
+ st.markdown("\n")
141
+ if st.button("Re-run EDA Pipeline (override)", use_container_width=False):
142
+ from eda.load_clean import run_load_and_clean
143
+ from eda.exploration import run_exploration
144
+ from eda.parameters import run_parameter_export
145
+
146
+ with st.spinner("Re-running EDA pipeline... This may take a few minutes."):
147
+ try:
148
+ # Step 1: Load & clean data
149
+ run_load_and_clean()
150
+
151
+ # Step 2: Generate visualizations
152
+ run_exploration()
153
+
154
+ # Step 3: Extract parameters
155
+ run_parameter_export()
156
+
157
+ st.success("EDA pipeline re-run completed")
158
+ st.rerun()
159
+
160
+ except Exception as e:
161
+ st.error("Pipeline failed while re-running inside the dashboard.")
162
+ with st.expander("Show error details"):
163
+ st.exception(e)
164
 
165
  st.markdown("---")
166
 
 
189
  #### 4. Cause Lists & Overrides
190
  View generated cause lists, make judge overrides, and track modification history.
191
 
192
+ #### 5. Scheduled Cases Explorer
193
+ Browse individual case, view status timelines, and understand scheduling decisions.
194
 
195
  #### 6. Analytics & Reports
196
  Compare simulation runs, analyze performance metrics, and export comprehensive reports.
 
235
  # Footer
236
  st.markdown("---")
237
  st.caption("Court Scheduling System - Code4Change Hackathon - Karnataka High Court")
238
+ st.caption("Developed by Aalekh Roy")
eda/exploration.py CHANGED
@@ -21,6 +21,7 @@ from datetime import timedelta
21
 
22
  import plotly.express as px
23
  import plotly.graph_objects as go
 
24
  import polars as pl
25
 
26
  from eda.config import (
@@ -31,6 +32,11 @@ from eda.config import (
31
  )
32
 
33
 
 
 
 
 
 
34
  def load_cleaned():
35
  cases = pl.read_parquet(_get_cases_parquet())
36
  hearings = pl.read_parquet(_get_hearings_parquet())
@@ -41,10 +47,7 @@ def load_cleaned():
41
 
42
  def run_exploration() -> None:
43
  cases, hearings = load_cleaned()
44
- # Keep transformations in Polars; convert only small, final results for plotting
45
-
46
- # --------------------------------------------------
47
- # 1. Case Type Distribution (aggregated to reduce plot data size)
48
  # --------------------------------------------------
49
  try:
50
  ct_counts = (
@@ -70,18 +73,26 @@ def run_exploration() -> None:
70
  print("Case type distribution error:", e)
71
 
72
  # --------------------------------------------------
73
- # 2. Filing Trends by Year
74
  # --------------------------------------------------
 
75
  if "YEAR_FILED" in cases.columns:
76
- year_counts = cases.group_by("YEAR_FILED").agg(pl.len().alias("Count"))
77
- fig2 = px.bar(
78
- year_counts.to_pandas(),
 
 
 
 
 
79
  x="YEAR_FILED",
80
  y="Count",
 
81
  title="Cases Filed by Year",
82
  )
83
- fig2.update_traces(line_color="royalblue")
84
- fig2.update_layout(xaxis=dict(rangeslider=dict(visible=True)))
 
85
  f2 = "2_cases_filed_by_year.html"
86
  safe_write_figure(fig2, f2)
87
 
 
21
 
22
  import plotly.express as px
23
  import plotly.graph_objects as go
24
+ import plotly.io as pio
25
  import polars as pl
26
 
27
  from eda.config import (
 
32
  )
33
 
34
 
35
+ px.defaults.template = "plotly_white"
36
+ px.defaults.color_discrete_sequence = px.colors.qualitative.Set2
37
+ pio.templates.default = "plotly_white"
38
+
39
+
40
  def load_cleaned():
41
  cases = pl.read_parquet(_get_cases_parquet())
42
  hearings = pl.read_parquet(_get_hearings_parquet())
 
47
 
48
  def run_exploration() -> None:
49
  cases, hearings = load_cleaned()
50
+ # 1. Case Type Distribution
 
 
 
51
  # --------------------------------------------------
52
  try:
53
  ct_counts = (
 
73
  print("Case type distribution error:", e)
74
 
75
  # --------------------------------------------------
76
+ # 2. Filing Trends by Year (single line, no slider)
77
  # --------------------------------------------------
78
+
79
  if "YEAR_FILED" in cases.columns:
80
+ year_counts = (
81
+ cases.group_by("YEAR_FILED")
82
+ .agg(pl.len().alias("Count"))
83
+ .sort("YEAR_FILED", descending=False)
84
+ )
85
+ df_year = year_counts.to_pandas()
86
+ fig2 = px.line(
87
+ df_year,
88
  x="YEAR_FILED",
89
  y="Count",
90
+ markers=True,
91
  title="Cases Filed by Year",
92
  )
93
+ fig2.update_layout(xaxis_title="Year", yaxis_title="Cases")
94
+ # Fix y-axis max to 10k (counts are known to be < 10k)
95
+ fig2.update_yaxes(range=[0, 10000])
96
  f2 = "2_cases_filed_by_year.html"
97
  safe_write_figure(fig2, f2)
98
 
pages/5_Scheduled_Cases_Explorer.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Wrapper to expose the cause lists & overrides page to Streamlit's pages system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import runpy
7
+
8
+
9
+ ORIG = (
10
+ Path(__file__).resolve().parents[1]
11
+ / "src"
12
+ / "dashboard"
13
+ / "pages"
14
+ / "5_Scheduled_Cases_Explorer.py"
15
+ )
16
+
17
+ runpy.run_path(str(ORIG), run_name="__main__")
src/config/paths.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ # Centralized paths used across the dashboard and simulation
8
+ # One source of truth for simulation run directories.
9
+
10
+
11
+ def get_runs_base() -> Path:
12
+ """Return the base directory where simulation runs are stored.
13
+
14
+ Priority order:
15
+ 1) Env var DASHBOARD_RUNS_BASE
16
+ 2) Default: outputs/simulation_runs
17
+ """
18
+ env = os.getenv("DASHBOARD_RUNS_BASE")
19
+ if env:
20
+ return Path(env)
21
+ return Path("outputs") / "simulation_runs"
22
+
23
+
24
+ def list_run_dirs(base: Path | None = None) -> list[Path]:
25
+ """List immediate child directories representing simulation runs."""
26
+ base = base or get_runs_base()
27
+ if not base.exists():
28
+ return []
29
+ return sorted([p for p in base.iterdir() if p.is_dir()], reverse=True)
30
+
31
+
32
+ def make_new_run_dir(run_id: str) -> Path:
33
+ """Create and return a new run directory at the configured base.
34
+
35
+ Does not overwrite existing; ensures parent exists.
36
+ """
37
+ base = get_runs_base()
38
+ path = base / run_id
39
+ path.mkdir(parents=True, exist_ok=True)
40
+ return path
src/dashboard/pages/3_Simulation_Workflow.py CHANGED
@@ -17,6 +17,7 @@ import plotly.express as px
17
  import streamlit as st
18
 
19
  from src.output.cause_list import CauseListGenerator
 
20
 
21
  CLI_VERSION = "1.0.0"
22
  # Page configuration
@@ -366,8 +367,8 @@ elif st.session_state.workflow_step == 2:
366
 
367
  log_dir = st.text_input(
368
  "Output directory",
369
- value="outputs/simulation_runs",
370
- help="Directory to save simulation outputs",
371
  )
372
 
373
  with col2:
 
17
  import streamlit as st
18
 
19
  from src.output.cause_list import CauseListGenerator
20
+ from src.config.paths import get_runs_base
21
 
22
  CLI_VERSION = "1.0.0"
23
  # Page configuration
 
367
 
368
  log_dir = st.text_input(
369
  "Output directory",
370
+ value=str(get_runs_base()),
371
+ help="Directory to save simulation outputs (override with DASHBOARD_RUNS_BASE env var)",
372
  )
373
 
374
  with col2:
src/dashboard/pages/4_Cause_Lists_And_Overrides.py CHANGED
@@ -45,7 +45,9 @@ if "draft_modifications" not in st.session_state:
45
  st.session_state.draft_modifications = []
46
 
47
  # Main tabs
48
- tab1, tab2, tab3 = st.tabs(["View Cause Lists", "Judge Override Interface", "Audit Trail"])
 
 
49
 
50
  # TAB 1: View Cause Lists
51
  with tab1:
@@ -55,18 +57,24 @@ with tab1:
55
  )
56
 
57
  # Check for available cause lists
58
- # Look specifically under outputs/simulation_runs where dashboard writes per-run folders
59
- outputs_dir = Path("outputs") / "simulation_runs"
 
 
60
 
61
  if not outputs_dir.exists():
62
- st.warning("No simulation outputs found. Run a simulation first to generate cause lists.")
 
 
63
  st.markdown("Go to **Simulation Workflow** to run a simulation.")
64
  else:
65
  # Look for simulation runs (each is a subdirectory in outputs/simulation_runs)
66
  sim_runs = [d for d in outputs_dir.iterdir() if d.is_dir()]
67
 
68
  if not sim_runs:
69
- st.info("No simulation runs found. Generate cause lists by running a simulation.")
 
 
70
  else:
71
  st.markdown(f"**{len(sim_runs)} simulation run(s) found**")
72
 
@@ -75,7 +83,9 @@ with tab1:
75
 
76
  with col1:
77
  selected_run = st.selectbox(
78
- "Select simulation run", options=[d.name for d in sim_runs], key="view_sim_run"
 
 
79
  )
80
 
81
  with col2:
@@ -138,18 +148,25 @@ with tab1:
138
  st.metric("Unique Cases", unique_cases)
139
 
140
  with col2:
141
- st.metric("Dates", df["date"].nunique() if "date" in df.columns else "N/A")
 
 
 
142
 
143
  with col3:
144
  st.metric(
145
  "Courtrooms",
146
- df["courtroom"].nunique() if "courtroom" in df.columns else "N/A",
 
 
147
  )
148
 
149
  with col4:
150
  st.metric(
151
  "Case Types",
152
- df["case_type"].nunique() if "case_type" in df.columns else "N/A",
 
 
153
  )
154
 
155
  # Filters
@@ -206,7 +223,9 @@ with tab1:
206
  ]
207
 
208
  st.markdown("---")
209
- st.markdown(f"**Showing {len(filtered_df):,} of {len(df):,} hearings**")
 
 
210
 
211
  # Display table
212
  st.dataframe(
@@ -226,7 +245,9 @@ with tab1:
226
 
227
  # Load into override interface
228
  if st.button(
229
- "Load into Override Interface", type="primary", use_container_width=True
 
 
230
  ):
231
  st.session_state.current_cause_list = {
232
  "source": str(cause_list_path),
@@ -235,7 +256,9 @@ with tab1:
235
  "loaded_at": datetime.now().isoformat(),
236
  }
237
  st.success("Cause list loaded into Override Interface")
238
- st.info("Navigate to 'Judge Override Interface' tab to review and modify.")
 
 
239
 
240
  except Exception as e:
241
  st.error(f"Error loading cause list: {e}")
@@ -248,7 +271,9 @@ with tab2:
248
  )
249
 
250
  if not st.session_state.current_cause_list:
251
- st.info("No cause list loaded. Go to 'View Cause Lists' tab and load a cause list first.")
 
 
252
  else:
253
  cause_list_info = st.session_state.current_cause_list
254
 
@@ -295,7 +320,9 @@ with tab2:
295
 
296
  # Remove from draft
297
  draft_df = draft_df[draft_df["case_id"] != case_to_remove]
298
- st.session_state.current_cause_list["data"] = draft_df.to_dict("records")
 
 
299
 
300
  st.success(f"Removed case {case_to_remove}")
301
  st.rerun()
@@ -312,7 +339,9 @@ with tab2:
312
  )
313
 
314
  new_priority = st.selectbox(
315
- "New priority", options=["HIGH", "MEDIUM", "LOW"], key="new_priority"
 
 
316
  )
317
 
318
  if case_to_prioritize != "(None)" and st.button("Update Priority"):
@@ -328,11 +357,11 @@ with tab2:
328
 
329
  # Update priority in draft
330
  if "priority" in draft_df.columns:
331
- draft_df.loc[draft_df["case_id"] == case_to_prioritize, "priority"] = (
332
- new_priority
333
- )
334
- st.session_state.current_cause_list["data"] = draft_df.to_dict(
335
- "records"
336
  )
337
 
338
  st.success(f"Updated priority for case {case_to_prioritize}")
@@ -342,7 +371,9 @@ with tab2:
342
 
343
  # Display draft with modifications
344
  st.markdown("### Current Draft")
345
- st.caption(f"{len(st.session_state.draft_modifications)} modification(s) made")
 
 
346
 
347
  st.dataframe(
348
  draft_df,
@@ -396,14 +427,18 @@ with tab2:
396
  st.success(f"Draft saved to {draft_file}")
397
 
398
  with approval_col3:
399
- if st.button("Approve & Finalize", type="primary", use_container_width=True):
 
 
400
  # Record approval
401
  approval = {
402
  "timestamp": datetime.now().isoformat(),
403
  "action": "APPROVE",
404
  "source": cause_list_info["source"],
405
  "final_count": len(draft_df),
406
- "modifications_count": len(st.session_state.draft_modifications),
 
 
407
  "modifications": st.session_state.draft_modifications.copy(),
408
  }
409
  st.session_state.override_history.append(approval)
@@ -413,7 +448,9 @@ with tab2:
413
  approved_path.mkdir(parents=True, exist_ok=True)
414
 
415
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
416
- approved_file = approved_path / f"approved_cause_list_{timestamp}.csv"
 
 
417
 
418
  draft_df.to_csv(approved_file, index=False)
419
 
@@ -439,7 +476,9 @@ with tab3:
439
  if not st.session_state.override_history:
440
  st.info("No approval history yet. Approve cause lists to build audit trail.")
441
  else:
442
- st.markdown(f"**{len(st.session_state.override_history)} approval(s) recorded**")
 
 
443
 
444
  # Summary statistics
445
  st.markdown("#### Summary Statistics")
 
45
  st.session_state.draft_modifications = []
46
 
47
  # Main tabs
48
+ tab1, tab2, tab3 = st.tabs(
49
+ ["View Cause Lists", "Judge Override Interface", "Audit Trail"]
50
+ )
51
 
52
  # TAB 1: View Cause Lists
53
  with tab1:
 
57
  )
58
 
59
  # Check for available cause lists
60
+ # Use centralized runs base directory
61
+ from src.config.paths import get_runs_base
62
+
63
+ outputs_dir = get_runs_base()
64
 
65
  if not outputs_dir.exists():
66
+ st.warning(
67
+ "No simulation outputs found. Run a simulation first to generate cause lists."
68
+ )
69
  st.markdown("Go to **Simulation Workflow** to run a simulation.")
70
  else:
71
  # Look for simulation runs (each is a subdirectory in outputs/simulation_runs)
72
  sim_runs = [d for d in outputs_dir.iterdir() if d.is_dir()]
73
 
74
  if not sim_runs:
75
+ st.info(
76
+ "No simulation runs found. Generate cause lists by running a simulation."
77
+ )
78
  else:
79
  st.markdown(f"**{len(sim_runs)} simulation run(s) found**")
80
 
 
83
 
84
  with col1:
85
  selected_run = st.selectbox(
86
+ "Select simulation run",
87
+ options=[d.name for d in sim_runs],
88
+ key="view_sim_run",
89
  )
90
 
91
  with col2:
 
148
  st.metric("Unique Cases", unique_cases)
149
 
150
  with col2:
151
+ st.metric(
152
+ "Dates",
153
+ df["date"].nunique() if "date" in df.columns else "N/A",
154
+ )
155
 
156
  with col3:
157
  st.metric(
158
  "Courtrooms",
159
+ df["courtroom"].nunique()
160
+ if "courtroom" in df.columns
161
+ else "N/A",
162
  )
163
 
164
  with col4:
165
  st.metric(
166
  "Case Types",
167
+ df["case_type"].nunique()
168
+ if "case_type" in df.columns
169
+ else "N/A",
170
  )
171
 
172
  # Filters
 
223
  ]
224
 
225
  st.markdown("---")
226
+ st.markdown(
227
+ f"**Showing {len(filtered_df):,} of {len(df):,} hearings**"
228
+ )
229
 
230
  # Display table
231
  st.dataframe(
 
245
 
246
  # Load into override interface
247
  if st.button(
248
+ "Load into Override Interface",
249
+ type="primary",
250
+ use_container_width=True,
251
  ):
252
  st.session_state.current_cause_list = {
253
  "source": str(cause_list_path),
 
256
  "loaded_at": datetime.now().isoformat(),
257
  }
258
  st.success("Cause list loaded into Override Interface")
259
+ st.info(
260
+ "Navigate to 'Judge Override Interface' tab to review and modify."
261
+ )
262
 
263
  except Exception as e:
264
  st.error(f"Error loading cause list: {e}")
 
271
  )
272
 
273
  if not st.session_state.current_cause_list:
274
+ st.info(
275
+ "No cause list loaded. Go to 'View Cause Lists' tab and load a cause list first."
276
+ )
277
  else:
278
  cause_list_info = st.session_state.current_cause_list
279
 
 
320
 
321
  # Remove from draft
322
  draft_df = draft_df[draft_df["case_id"] != case_to_remove]
323
+ st.session_state.current_cause_list["data"] = draft_df.to_dict(
324
+ "records"
325
+ )
326
 
327
  st.success(f"Removed case {case_to_remove}")
328
  st.rerun()
 
339
  )
340
 
341
  new_priority = st.selectbox(
342
+ "New priority",
343
+ options=["HIGH", "MEDIUM", "LOW"],
344
+ key="new_priority",
345
  )
346
 
347
  if case_to_prioritize != "(None)" and st.button("Update Priority"):
 
357
 
358
  # Update priority in draft
359
  if "priority" in draft_df.columns:
360
+ draft_df.loc[
361
+ draft_df["case_id"] == case_to_prioritize, "priority"
362
+ ] = new_priority
363
+ st.session_state.current_cause_list["data"] = (
364
+ draft_df.to_dict("records")
365
  )
366
 
367
  st.success(f"Updated priority for case {case_to_prioritize}")
 
371
 
372
  # Display draft with modifications
373
  st.markdown("### Current Draft")
374
+ st.caption(
375
+ f"{len(st.session_state.draft_modifications)} modification(s) made"
376
+ )
377
 
378
  st.dataframe(
379
  draft_df,
 
427
  st.success(f"Draft saved to {draft_file}")
428
 
429
  with approval_col3:
430
+ if st.button(
431
+ "Approve & Finalize", type="primary", use_container_width=True
432
+ ):
433
  # Record approval
434
  approval = {
435
  "timestamp": datetime.now().isoformat(),
436
  "action": "APPROVE",
437
  "source": cause_list_info["source"],
438
  "final_count": len(draft_df),
439
+ "modifications_count": len(
440
+ st.session_state.draft_modifications
441
+ ),
442
  "modifications": st.session_state.draft_modifications.copy(),
443
  }
444
  st.session_state.override_history.append(approval)
 
448
  approved_path.mkdir(parents=True, exist_ok=True)
449
 
450
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
451
+ approved_file = (
452
+ approved_path / f"approved_cause_list_{timestamp}.csv"
453
+ )
454
 
455
  draft_df.to_csv(approved_file, index=False)
456
 
 
476
  if not st.session_state.override_history:
477
  st.info("No approval history yet. Approve cause lists to build audit trail.")
478
  else:
479
+ st.markdown(
480
+ f"**{len(st.session_state.override_history)} approval(s) recorded**"
481
+ )
482
 
483
  # Summary statistics
484
  st.markdown("#### Summary Statistics")
src/dashboard/pages/5_Scheduled_Cases_Explorer.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Ticket Explorer (Post-Run)
2
+
3
+ Browse simulation runs as a CMS of tickets (cases). After a run finishes,
4
+ we build compact Parquet artifacts from events.csv and render case timelines.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ import streamlit as st
12
+ import polars as pl
13
+
14
+ import pandas as pd
15
+ import plotly.express as px
16
+
17
+ from src.dashboard.utils.ticket_views import build_ticket_views, load_ticket_views
18
+ from src.config.paths import get_runs_base, list_run_dirs
19
+
20
+
21
+ st.set_page_config(page_title="Ticket Explorer", page_icon="tickets", layout="wide")
22
+ st.title("Scheduled Cases Explorer (Post-Run)")
23
+ st.caption(
24
+ "Inspect each case as a ticket with a full audit trail after the simulation run."
25
+ )
26
+
27
+
28
+ def _list_runs(base: Path) -> list[Path]:
29
+ if not base.exists():
30
+ return []
31
+ # run dirs are expected to be leaf directories under base
32
+ return sorted([p for p in base.iterdir() if p.is_dir()], reverse=True)
33
+
34
+
35
+ runs_base = get_runs_base()
36
+ run_dirs = list_run_dirs(runs_base)
37
+
38
+ if not run_dirs:
39
+ st.warning(f"No simulation runs found in {runs_base}. Run a simulation first.")
40
+ st.stop()
41
+
42
+ labels = [d.name for d in run_dirs]
43
+ idx = st.selectbox(
44
+ "Select a run", options=list(range(len(labels))), format_func=lambda i: labels[i]
45
+ )
46
+ run_dir = run_dirs[idx]
47
+
48
+ st.markdown(f"Run directory: `{run_dir}`")
49
+
50
+ col_a, col_b, col_c = st.columns([1, 1, 2])
51
+ with col_a:
52
+ if st.button(
53
+ "Rebuild ticket views", help="Recompute Parquet artifacts from events.csv"
54
+ ):
55
+ build_ticket_views(run_dir)
56
+ st.success("Ticket views rebuilt")
57
+ st.rerun()
58
+ with col_b:
59
+ events_path = run_dir / "events.csv"
60
+ st.download_button(
61
+ "Download events.csv",
62
+ data=events_path.read_bytes() if events_path.exists() else b"",
63
+ file_name="events.csv",
64
+ mime="text/csv",
65
+ disabled=not events_path.exists(),
66
+ )
67
+
68
+ # Load views (build if missing)
69
+ journal_df, summary_df, spans_df = load_ticket_views(run_dir)
70
+
71
+
72
+ # Normalize to pandas for Streamlit controls
73
+ def _to_pandas(df):
74
+ if pl is not None and isinstance(df, pl.DataFrame):
75
+ return df.to_pandas()
76
+ return df
77
+
78
+
79
+ journal_pd: pd.DataFrame = _to_pandas(journal_df)
80
+ summary_pd: pd.DataFrame = _to_pandas(summary_df)
81
+ spans_pd: pd.DataFrame = _to_pandas(spans_df)
82
+
83
+ with st.sidebar:
84
+ st.header("Filters")
85
+ case_q = st.text_input("Search case_id contains")
86
+ types = sorted([x for x in summary_pd["case_type"].dropna().unique().tolist()])
87
+ sel_types = st.multiselect("Case types", options=types, default=[])
88
+ statuses = ["ACTIVE", "DISPOSED"]
89
+ sel_status = st.multiselect("Final status", options=statuses, default=[])
90
+
91
+ hearings_min, hearings_max = st.slider(
92
+ "Total hearings",
93
+ min_value=int(summary_pd.get("total_hearings", pd.Series([0])).min() or 0),
94
+ max_value=int(summary_pd.get("total_hearings", pd.Series([0])).max() or 0),
95
+ value=(0, int(summary_pd.get("total_hearings", pd.Series([0])).max() or 0)),
96
+ )
97
+
98
+ # Apply filters
99
+ filtered = summary_pd.copy()
100
+ if case_q:
101
+ filtered = filtered[
102
+ filtered["case_id"].astype(str).str.contains(case_q, case=False, na=False)
103
+ ]
104
+ if sel_types:
105
+ filtered = filtered[filtered["case_type"].isin(sel_types)]
106
+ if sel_status:
107
+ filtered = filtered[filtered["final_status"].isin(sel_status)]
108
+ filtered = filtered[
109
+ (filtered.get("total_hearings", 0) >= hearings_min)
110
+ & (filtered.get("total_hearings", 0) <= hearings_max)
111
+ ]
112
+
113
+ st.markdown("### Filtered Cases")
114
+
115
+ # Pagination
116
+ page_size = st.selectbox("Rows per page", [25, 50, 100], index=0)
117
+ total_rows = len(filtered)
118
+ page = st.number_input(
119
+ "Page",
120
+ min_value=1,
121
+ max_value=max(1, (total_rows - 1) // page_size + 1),
122
+ value=1,
123
+ step=1,
124
+ )
125
+ start, end = (page - 1) * page_size, min(page * page_size, total_rows)
126
+ st.caption(f"Showing {start + 1}–{end} of {total_rows}")
127
+
128
+ cols_to_show = [
129
+ "case_id",
130
+ "case_type",
131
+ "final_status",
132
+ "current_stage",
133
+ "total_hearings",
134
+ "heard_count",
135
+ "adjourned_count",
136
+ "last_seen_date",
137
+ ]
138
+ cols_to_show = [c for c in cols_to_show if c in filtered.columns]
139
+ st.dataframe(
140
+ filtered.iloc[start:end][cols_to_show], use_container_width=True, hide_index=True
141
+ )
142
+
143
+ st.markdown("### Scheduled Case event details")
144
+ sel_case = st.selectbox(
145
+ "Choose a case_id",
146
+ options=filtered["case_id"].tolist() if not filtered.empty else [],
147
+ )
148
+
149
+ if sel_case:
150
+ row = summary_pd[summary_pd["case_id"] == sel_case].iloc[0]
151
+ kpi1, kpi2, kpi3, kpi4 = st.columns(4)
152
+ with kpi1:
153
+ st.metric("Total hearings", int(row.get("total_hearings", 0)))
154
+ with kpi2:
155
+ st.metric("Heard", int(row.get("heard_count", 0)))
156
+ with kpi3:
157
+ st.metric("Adjourned", int(row.get("adjourned_count", 0)))
158
+ with kpi4:
159
+ st.metric("Status", str(row.get("final_status", "")))
160
+
161
+ # Journal slice
162
+ j = journal_pd[journal_pd["case_id"] == sel_case].copy()
163
+ j.sort_values(["date", "seq_no"], inplace=True)
164
+
165
+ # Export button
166
+ csv_bytes = j.to_csv(index=False).encode("utf-8")
167
+ st.download_button(
168
+ "Download this ticket's journal (CSV)",
169
+ data=csv_bytes,
170
+ file_name=f"{sel_case}_journal.csv",
171
+ mime="text/csv",
172
+ )
173
+
174
+ # Timeline table
175
+ st.subheader("Event journal")
176
+ show_cols = [
177
+ c
178
+ for c in [
179
+ "date",
180
+ "type",
181
+ "detail",
182
+ "stage",
183
+ "courtroom_id",
184
+ "priority_score",
185
+ "readiness_score",
186
+ "ripeness_status",
187
+ "days_since_hearing",
188
+ ]
189
+ if c in j.columns
190
+ ]
191
+ st.dataframe(j[show_cols].tail(100), use_container_width=True, hide_index=True)
192
+
193
+ # Stage spans chart (if available)
194
+ s = spans_pd[spans_pd["case_id"] == sel_case].copy()
195
+ if not s.empty:
196
+ s["start_date"] = pd.to_datetime(s["start_date"])
197
+ s["end_date"] = pd.to_datetime(s["end_date"])
198
+ fig = px.timeline(
199
+ s,
200
+ x_start="start_date",
201
+ x_end="end_date",
202
+ y="stage",
203
+ color="stage",
204
+ title="Stage spans",
205
+ )
206
+ fig.update_yaxes(autorange="reversed")
207
+ st.plotly_chart(fig, use_container_width=True)
208
+ else:
209
+ st.info("No stage change spans available for this ticket.")
src/dashboard/pages/6_Analytics_And_Reports.py CHANGED
@@ -47,9 +47,10 @@ with tab1:
47
  "Compare multiple simulation runs to evaluate different policies and parameters."
48
  )
49
 
50
- # Check for available simulation runs
51
- outputs_dir = Path("outputs")
52
- runs_dir = outputs_dir / "simulation_runs"
 
53
 
54
  if not runs_dir.exists():
55
  st.warning(
@@ -252,9 +253,10 @@ with tab2:
252
  st.markdown("### Performance Trends")
253
  st.markdown("Analyze performance metrics across all simulation runs.")
254
 
255
- # Use simulation_runs directory recursively
256
- outputs_dir = Path("outputs")
257
- runs_dir = outputs_dir / "simulation_runs"
 
258
 
259
  if not runs_dir.exists():
260
  st.warning("No simulation outputs found.")
@@ -273,7 +275,11 @@ with tab2:
273
  try:
274
  df = pd.read_csv(metrics_path)
275
  # Use relative label for clarity across nested structures
276
- df["run"] = str(run_dir.relative_to(runs_dir))
 
 
 
 
277
  all_metrics.append(df)
278
  except Exception:
279
  pass # Skip invalid metrics files
@@ -345,8 +351,9 @@ with tab3:
345
  - **Case Type Balance**: Ensures no case type is systematically disadvantaged
346
  """)
347
 
348
- outputs_dir = Path("outputs")
349
- runs_dir = outputs_dir / "simulation_runs"
 
350
 
351
  if not runs_dir.exists():
352
  st.warning("No simulation outputs found.")
 
47
  "Compare multiple simulation runs to evaluate different policies and parameters."
48
  )
49
 
50
+ # Check for available simulation runs (centralized base)
51
+ from src.config.paths import get_runs_base
52
+
53
+ runs_dir = get_runs_base()
54
 
55
  if not runs_dir.exists():
56
  st.warning(
 
253
  st.markdown("### Performance Trends")
254
  st.markdown("Analyze performance metrics across all simulation runs.")
255
 
256
+ # Use centralized runs directory recursively
257
+ from src.config.paths import get_runs_base
258
+
259
+ runs_dir = get_runs_base()
260
 
261
  if not runs_dir.exists():
262
  st.warning("No simulation outputs found.")
 
275
  try:
276
  df = pd.read_csv(metrics_path)
277
  # Use relative label for clarity across nested structures
278
+ try:
279
+ df["run"] = str(run_dir.relative_to(runs_dir))
280
+ except ValueError:
281
+ # Fallback to folder name if not under base (shouldn't happen)
282
+ df["run"] = run_dir.name
283
  all_metrics.append(df)
284
  except Exception:
285
  pass # Skip invalid metrics files
 
351
  - **Case Type Balance**: Ensures no case type is systematically disadvantaged
352
  """)
353
 
354
+ from src.config.paths import get_runs_base
355
+
356
+ runs_dir = get_runs_base()
357
 
358
  if not runs_dir.exists():
359
  st.warning("No simulation outputs found.")
src/dashboard/utils/ticket_views.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Tuple
5
+ import polars as pl
6
+
7
+
8
+ def build_ticket_views(run_dir: Path) -> Tuple[Path, Path, Path]:
9
+ """Materialize post-run ticket views from events.csv into Parquet files.
10
+
11
+ Creates three artifacts in the run directory:
12
+ - ticket_journal.parquet
13
+ - ticket_summary.parquet
14
+ - ticket_state_spans.parquet
15
+
16
+ Returns paths to the three files in the order above.
17
+ """
18
+ run_dir = Path(run_dir)
19
+ events_csv = run_dir / "events.csv"
20
+ if not events_csv.exists():
21
+ raise FileNotFoundError(f"events.csv not found in run dir: {events_csv}")
22
+
23
+ journal_pq = run_dir / "ticket_journal.parquet"
24
+ summary_pq = run_dir / "ticket_summary.parquet"
25
+ spans_pq = run_dir / "ticket_state_spans.parquet"
26
+
27
+ events = pl.scan_csv(str(events_csv))
28
+ # Normalize and order
29
+ journal = (
30
+ events.with_columns(
31
+ [
32
+ pl.col("date").str.to_date().alias("date"),
33
+ ]
34
+ )
35
+ .sort(["case_id", "date"]) # lazy
36
+ .with_columns(
37
+ [
38
+ pl.arange(0, pl.len()).over("case_id").alias("seq_no"),
39
+ ]
40
+ )
41
+ .collect(streaming=True)
42
+ )
43
+ journal.write_parquet(str(journal_pq))
44
+
45
+ # Outcomes for counts
46
+ heard = journal.filter(pl.col("type") == "outcome").with_columns(
47
+ (pl.col("detail") == "heard").alias("is_heard")
48
+ )
49
+
50
+ base_summary = journal.group_by("case_id").agg(
51
+ [
52
+ pl.first("case_type").alias("case_type"),
53
+ pl.first("date").alias("first_seen_date"),
54
+ pl.last("date").alias("last_seen_date"),
55
+ pl.col("stage").sort_by("seq_no").last().alias("current_stage"),
56
+ (pl.col("type") == "stage_change")
57
+ .cast(pl.Int64)
58
+ .sum()
59
+ .alias("stage_changes"),
60
+ (pl.col("type") == "ripeness_change")
61
+ .cast(pl.Int64)
62
+ .sum()
63
+ .alias("ripeness_transitions"),
64
+ ]
65
+ )
66
+ outcome_summary = (
67
+ heard.group_by("case_id")
68
+ .agg(
69
+ [
70
+ pl.len().alias("total_hearings"),
71
+ pl.col("is_heard").cast(pl.Int64).sum().alias("heard_count"),
72
+ ]
73
+ )
74
+ .with_columns(
75
+ (pl.col("total_hearings") - pl.col("heard_count")).alias("adjourned_count")
76
+ )
77
+ )
78
+ disposed = (
79
+ journal.filter(pl.col("type") == "disposed")
80
+ .group_by("case_id")
81
+ .agg([pl.min("date").alias("disposal_date")])
82
+ )
83
+
84
+ summary = (
85
+ base_summary.join(outcome_summary, on="case_id", how="left")
86
+ .with_columns(
87
+ [
88
+ pl.col("total_hearings").fill_null(0),
89
+ pl.col("heard_count").fill_null(0),
90
+ pl.col("adjourned_count").fill_null(0),
91
+ ]
92
+ )
93
+ .join(disposed, on="case_id", how="left")
94
+ .with_columns(
95
+ [
96
+ # Compute age in full days from first to last seen.
97
+ # Use total_days() on duration to be compatible across Polars versions.
98
+ (pl.col("last_seen_date") - pl.col("first_seen_date"))
99
+ .dt.total_days()
100
+ .alias("age_days_end"),
101
+ pl.when(pl.col("disposal_date").is_not_null())
102
+ .then(pl.lit("DISPOSED"))
103
+ .otherwise(pl.lit("ACTIVE"))
104
+ .alias("final_status"),
105
+ ]
106
+ )
107
+ )
108
+ summary.write_parquet(str(summary_pq))
109
+
110
+ # Spans from stage changes
111
+ sc = (
112
+ journal.filter(pl.col("type") == "stage_change")
113
+ .select(["case_id", "date", "stage"])
114
+ .rename({"date": "start_date"})
115
+ )
116
+ spans = sc.with_columns(
117
+ [
118
+ pl.col("start_date").shift(-1).over("case_id").alias("end_date"),
119
+ ]
120
+ ).with_columns(
121
+ [
122
+ pl.when(pl.col("end_date").is_null())
123
+ .then(pl.col("start_date"))
124
+ .otherwise(pl.col("end_date"))
125
+ .alias("end_date")
126
+ ]
127
+ )
128
+ spans.write_parquet(str(spans_pq))
129
+ return journal_pq, summary_pq, spans_pq
130
+
131
+
132
+ def load_ticket_views(run_dir: Path):
133
+ """Load ticket views; build them if missing. Returns (journal, summary, spans).
134
+
135
+ Uses Polars DataFrames if Polars is available; otherwise returns pandas DataFrames.
136
+ """
137
+ run_dir = Path(run_dir)
138
+ journal_pq = run_dir / "ticket_journal.parquet"
139
+ summary_pq = run_dir / "ticket_summary.parquet"
140
+ spans_pq = run_dir / "ticket_state_spans.parquet"
141
+
142
+ if not (journal_pq.exists() and summary_pq.exists() and spans_pq.exists()):
143
+ build_ticket_views(run_dir)
144
+
145
+ journal = pl.read_parquet(str(journal_pq))
146
+ summary = pl.read_parquet(str(summary_pq))
147
+ spans = pl.read_parquet(str(spans_pq))
148
+ return journal, summary, spans
src/simulation/engine.py CHANGED
@@ -36,6 +36,7 @@ from src.simulation.allocator import AllocationStrategy, CourtroomAllocator
36
  from src.simulation.events import EventWriter
37
  from src.simulation.policies import get_policy
38
  from src.utils.calendar import CourtCalendar
 
39
 
40
 
41
  @dataclass
@@ -86,11 +87,11 @@ class CourtSim:
86
  self._log_dir: Path | None = None
87
  if self.cfg.log_dir:
88
  self._log_dir = Path(self.cfg.log_dir)
 
89
  else:
90
- # default run folder
91
  run_id = time.strftime("%Y%m%d_%H%M%S")
92
- self._log_dir = Path("data") / "sim_runs" / run_id
93
- self._log_dir.mkdir(parents=True, exist_ok=True)
94
  self._metrics_path = self._log_dir / "metrics.csv"
95
  with self._metrics_path.open("w", newline="", encoding="utf-8") as f:
96
  w = csv.writer(f)
 
36
  from src.simulation.events import EventWriter
37
  from src.simulation.policies import get_policy
38
  from src.utils.calendar import CourtCalendar
39
+ from src.config.paths import make_new_run_dir
40
 
41
 
42
  @dataclass
 
87
  self._log_dir: Path | None = None
88
  if self.cfg.log_dir:
89
  self._log_dir = Path(self.cfg.log_dir)
90
+ self._log_dir.mkdir(parents=True, exist_ok=True)
91
  else:
92
+ # default run folder (centralized base path)
93
  run_id = time.strftime("%Y%m%d_%H%M%S")
94
+ self._log_dir = make_new_run_dir(run_id)
 
95
  self._metrics_path = self._log_dir / "metrics.csv"
96
  with self._metrics_path.open("w", newline="", encoding="utf-8") as f:
97
  w = csv.writer(f)