Spaces:
Running
Running
File size: 12,180 Bytes
4d0ffdd f6c65ef 4d0ffdd 6a28f91 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd 6a28f91 4d0ffdd 6a28f91 4d0ffdd f6c65ef 4d0ffdd 6a28f91 4d0ffdd 6a28f91 4d0ffdd 6a28f91 4d0ffdd 6a28f91 4d0ffdd 6a28f91 4d0ffdd 6a28f91 4d0ffdd 6a28f91 4d0ffdd f6c65ef 4d0ffdd f6c65ef 4d0ffdd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
"""Load parameters extracted from exploratory data analysis.
This module reads all parameter files generated by the EDA pipeline and makes
them available to the scheduler.
"""
import json
from pathlib import Path
from typing import Dict, List, Optional
import pandas as pd
from src.data.config import get_latest_params_dir
class ParameterLoader:
"""Loads and manages parameters from EDA outputs.
Performance notes:
- Builds in-memory lookup caches to avoid repeated DataFrame filtering.
"""
def __init__(self, params_dir: Optional[Path] = None):
"""Initialize parameter loader.
Args:
params_dir: Directory containing parameter files. If None, uses latest.
"""
self.params_dir = params_dir or get_latest_params_dir()
# Cached parameters
self._transition_probs: Optional[pd.DataFrame] = None
self._stage_duration: Optional[pd.DataFrame] = None
self._court_capacity: Optional[Dict] = None
self._adjournment_proxies: Optional[pd.DataFrame] = None
self._case_type_summary: Optional[pd.DataFrame] = None
self._transition_entropy: Optional[pd.DataFrame] = None
# caches
self._duration_map: Optional[Dict[str, Dict[str, float]]] = (
None # stage -> {"median": x, "p90": y}
)
self._transitions_map: Optional[Dict[str, List[tuple]]] = (
None # stage_from -> [(stage_to, cum_p), ...]
)
self._adj_map: Optional[Dict[str, Dict[str, float]]] = (
None # stage -> {case_type: p_adj}
)
@property
def transition_probs(self) -> pd.DataFrame:
"""Stage transition probabilities.
Returns:
DataFrame with columns: STAGE_FROM, STAGE_TO, N, row_n, p
"""
if self._transition_probs is None:
file_path = self.params_dir / "stage_transition_probs.csv"
self._transition_probs = pd.read_csv(file_path)
return self._transition_probs
def get_transition_prob(self, stage_from: str, stage_to: str) -> float:
"""Get probability of transitioning from one stage to another.
Args:
stage_from: Current stage
stage_to: Next stage
Returns:
Transition probability (0-1)
"""
df = self.transition_probs
match = df[(df["STAGE_FROM"] == stage_from) & (df["STAGE_TO"] == stage_to)]
if len(match) == 0:
return 0.0
return float(match.iloc[0]["p"])
def _build_transitions_map(self) -> None:
if self._transitions_map is not None:
return
df = self.transition_probs
self._transitions_map = {}
# group by STAGE_FROM, build cumulative probs for fast sampling
for st_from, group in df.groupby("STAGE_FROM"):
cum = 0.0
lst = []
for _, row in group.sort_values("p").iterrows():
cum += float(row["p"])
lst.append((str(row["STAGE_TO"]), cum))
# ensure last cum is 1.0 to guard against rounding
if lst:
to_last, _ = lst[-1]
lst[-1] = (to_last, 1.0)
self._transitions_map[str(st_from)] = lst
def get_stage_transitions(self, stage_from: str) -> pd.DataFrame:
"""Get all possible transitions from a given stage.
Args:
stage_from: Current stage
Returns:
DataFrame with STAGE_TO and p columns
"""
df = self.transition_probs
return df[df["STAGE_FROM"] == stage_from][["STAGE_TO", "p"]].reset_index(
drop=True
)
def get_stage_transitions_fast(self, stage_from: str) -> List[tuple]:
"""Fast lookup: returns list of (stage_to, cum_p)."""
self._build_transitions_map()
if not self._transitions_map:
return []
return self._transitions_map.get(stage_from, [])
@property
def stage_duration(self) -> pd.DataFrame:
"""Stage duration statistics.
Returns:
DataFrame with columns: STAGE, RUN_MEDIAN_DAYS, RUN_P90_DAYS,
HEARINGS_PER_RUN_MED, N_RUNS
"""
if self._stage_duration is None:
file_path = self.params_dir / "stage_duration.csv"
self._stage_duration = pd.read_csv(file_path)
return self._stage_duration
def _build_duration_map(self) -> None:
if self._duration_map is not None:
return
df = self.stage_duration
self._duration_map = {}
for _, row in df.iterrows():
st = str(row["STAGE"])
self._duration_map.setdefault(st, {})
self._duration_map[st]["median"] = float(row["RUN_MEDIAN_DAYS"])
self._duration_map[st]["p90"] = float(row["RUN_P90_DAYS"])
def get_stage_duration(self, stage: str, percentile: str = "median") -> float:
"""Get typical duration for a stage.
Args:
stage: Stage name
percentile: 'median' or 'p90'
Returns:
Duration in days
"""
self._build_duration_map()
if not self._duration_map or stage not in self._duration_map:
return 30.0
p = "median" if percentile == "median" else "p90"
return float(self._duration_map[stage].get(p, 30.0))
@property
def court_capacity(self) -> Dict:
"""Court capacity metrics.
Returns:
Dict with keys: slots_median_global, slots_p90_global
"""
if self._court_capacity is None:
file_path = self.params_dir / "court_capacity_global.json"
with open(file_path, "r") as f:
self._court_capacity = json.load(f)
return self._court_capacity
@property
def daily_capacity_median(self) -> int:
"""Median daily capacity per courtroom."""
return int(self.court_capacity["slots_median_global"])
@property
def daily_capacity_p90(self) -> int:
"""90th percentile daily capacity per courtroom."""
return int(self.court_capacity["slots_p90_global"])
@property
def adjournment_proxies(self) -> pd.DataFrame:
"""Adjournment probabilities by stage and case type.
Returns:
DataFrame with columns: Remappedstages, casetype,
p_adjourn_proxy, p_not_reached_proxy, n
"""
if self._adjournment_proxies is None:
file_path = self.params_dir / "adjournment_proxies.csv"
self._adjournment_proxies = pd.read_csv(file_path)
return self._adjournment_proxies
def _build_adj_map(self) -> None:
if self._adj_map is not None:
return
df = self.adjournment_proxies
self._adj_map = {}
for _, row in df.iterrows():
st = str(row["Remappedstages"])
ct = str(row["casetype"])
p = float(row["p_adjourn_proxy"])
self._adj_map.setdefault(st, {})[ct] = p
def get_adjournment_prob(self, stage: str, case_type: str) -> float:
"""Get probability of adjournment for given stage and case type.
Args:
stage: Stage name
case_type: Case type (e.g., 'RSA', 'CRP')
Returns:
Adjournment probability (0-1)
"""
self._build_adj_map()
if not self._adj_map:
return 0.4
if stage in self._adj_map and case_type in self._adj_map[stage]:
return float(self._adj_map[stage][case_type])
# fallback: average across types for this stage
if stage in self._adj_map and self._adj_map[stage]:
vals = list(self._adj_map[stage].values())
return float(sum(vals) / len(vals))
return 0.4
@property
def case_type_summary(self) -> pd.DataFrame:
"""Summary statistics by case type.
Returns:
DataFrame with columns: CASE_TYPE, n_cases, disp_median,
disp_p90, hear_median, gap_median
"""
if self._case_type_summary is None:
file_path = self.params_dir / "case_type_summary.csv"
self._case_type_summary = pd.read_csv(file_path)
return self._case_type_summary
def get_case_type_stats(self, case_type: str) -> Dict:
"""Get statistics for a specific case type.
Args:
case_type: Case type (e.g., 'RSA', 'CRP')
Returns:
Dict with disp_median, disp_p90, hear_median, gap_median
"""
df = self.case_type_summary
match = df[df["CASE_TYPE"] == case_type]
if len(match) == 0:
raise ValueError(f"Unknown case type: {case_type}")
return match.iloc[0].to_dict()
@property
def transition_entropy(self) -> pd.DataFrame:
"""Stage transition entropy (predictability metric).
Returns:
DataFrame with columns: STAGE_FROM, entropy
"""
if self._transition_entropy is None:
file_path = self.params_dir / "stage_transition_entropy.csv"
self._transition_entropy = pd.read_csv(file_path)
return self._transition_entropy
def get_stage_predictability(self, stage: str) -> float:
"""Get predictability of transitions from a stage (inverse of entropy).
Args:
stage: Stage name
Returns:
Predictability score (0-1, higher = more predictable)
"""
df = self.transition_entropy
match = df[df["STAGE_FROM"] == stage]
if len(match) == 0:
return 0.5 # Default: medium predictability
entropy = float(match.iloc[0]["entropy"])
# Convert entropy to predictability (lower entropy = higher predictability)
# Max entropy ~1.4, so normalize
predictability = max(0.0, 1.0 - (entropy / 1.5))
return predictability
def get_stage_stationary_distribution(self) -> Dict[str, float]:
"""Approximate stationary distribution over stages from transition matrix.
Returns stage -> probability summing to 1.0.
"""
df = self.transition_probs.copy()
# drop nulls and ensure strings
df = df[df["STAGE_FROM"].notna() & df["STAGE_TO"].notna()]
df["STAGE_FROM"] = df["STAGE_FROM"].astype(str)
df["STAGE_TO"] = df["STAGE_TO"].astype(str)
stages = sorted(set(df["STAGE_FROM"]).union(set(df["STAGE_TO"])))
idx = {s: i for i, s in enumerate(stages)}
n = len(stages)
# build dense row-stochastic matrix
P = [[0.0] * n for _ in range(n)]
for _, row in df.iterrows():
i = idx[str(row["STAGE_FROM"])]
j = idx[str(row["STAGE_TO"])]
P[i][j] += float(row["p"])
# ensure rows sum to 1 by topping up self-loop
for i in range(n):
s = sum(P[i])
if s < 0.999:
P[i][i] += 1.0 - s
elif s > 1.001:
# normalize if slightly over
P[i] = [v / s for v in P[i]]
# power iteration
pi = [1.0 / n] * n
for _ in range(200):
new = [0.0] * n
for j in range(n):
acc = 0.0
for i in range(n):
acc += pi[i] * P[i][j]
new[j] = acc
# normalize
z = sum(new)
if z == 0:
break
new = [v / z for v in new]
# check convergence
if sum(abs(new[k] - pi[k]) for k in range(n)) < 1e-9:
pi = new
break
pi = new
return {stages[i]: pi[i] for i in range(n)}
def __repr__(self) -> str:
return f"ParameterLoader(params_dir={self.params_dir})"
# Convenience function for quick access
def load_parameters(params_dir: Optional[Path] = None) -> ParameterLoader:
"""Load parameters from EDA outputs.
Args:
params_dir: Directory containing parameter files. If None, uses latest.
Returns:
ParameterLoader instance
"""
return ParameterLoader(params_dir)
|