"""Central configuration: paths, column groupings, and modeling constants. The column groupings here are the single most important guard against data leakage. Any column produced *after* an event is reported (resolution times, status, post-hoc edits, assigned officers) must never become a model feature. """ from __future__ import annotations import os from pathlib import Path # --------------------------------------------------------------------------- # # Paths # --------------------------------------------------------------------------- # ROOT = Path(__file__).resolve().parents[1] DATA_DIR = ROOT / "data" RAW_DIR = DATA_DIR / "raw" PROCESSED_DIR = DATA_DIR / "processed" MODELS_DIR = ROOT / "models" REPORTS_DIR = ROOT / "reports" FIGURES_DIR = REPORTS_DIR / "figures" RAW_CSV = RAW_DIR / "astram_events.csv" CLEAN_PARQUET = PROCESSED_DIR / "events_clean.parquet" FEATURES_PARQUET = PROCESSED_DIR / "features.parquet" EMBED_CACHE = PROCESSED_DIR / "text_embeddings.npy" # Persisted artefacts that make the causal/spatial features reproducible at # inference time (a new event must see the SAME geo bins and the accumulated # historical target rates the model was trained on). HISTORY_PARQUET = PROCESSED_DIR / "history.parquet" GEO_KMEANS_PATH = MODELS_DIR / "geo_kmeans.joblib" for _d in (PROCESSED_DIR, MODELS_DIR, REPORTS_DIR, FIGURES_DIR): _d.mkdir(parents=True, exist_ok=True) RANDOM_STATE = 42 # --------------------------------------------------------------------------- # # Raw column groupings # --------------------------------------------------------------------------- # # Identifiers / opaque tokens -> dropped from features entirely. ID_COLUMNS = [ "id", "kgid", "veh_no", "client_id", "created_by_id", "last_modified_by_id", "assigned_to_police_id", # post-assignment -> would leak the resourcing label "citizen_accident_id", "closed_by_id", "resolved_by_id", "map_file", "meta_data", "gba_identifier", ] # Columns only known AFTER the event unfolds -> never features. Some are used # strictly to construct duration / status labels in targets.py. LEAKAGE_COLUMNS = [ "status", "modified_datetime", "end_datetime", # used to derive duration label only "closed_datetime", # used to derive duration label only "resolved_datetime", # used to derive duration label only "resolved_at_address", "resolved_at_latitude", "resolved_at_longitude", "comment", # frequently appended after resolution # The end-point / diversion-route geometry is only populated when a closure # or diversion SEGMENT is drawn -> it encodes the closure decision itself # (has_end_point alone gives AP~0.98 on closure). Treat as leakage so the # model must forecast from genuine report-time signal instead. "endlatitude", "endlongitude", "end_address", "route_path", ] # The three prediction targets (also excluded from the feature matrix). TARGET_CLOSURE = "y_road_closure" # binary (barricading / diversion need) TARGET_PRIORITY = "y_high_priority" # binary (manpower tier) TARGET_DURATION = "y_duration_min" # numeric (impact duration, minutes) TARGET_DURATION_LOG = "y_duration_log" # log1p transform actually modeled TARGET_COLUMNS_RAW = ["requires_road_closure", "priority"] # Timestamp columns parsed to tz-aware datetimes. DATETIME_COLUMNS = [ "start_datetime", "end_datetime", "modified_datetime", "created_date", "closed_datetime", "resolved_datetime", ] # --------------------------------------------------------------------------- # # Feature columns (available at event-creation time) # --------------------------------------------------------------------------- # CATEGORICAL_FEATURES = [ "event_type", "event_cause", "event_family", # engineered grouping of event_cause "veh_type", "direction", "corridor", "zone", "junction", "police_station", "reason_breakdown", "cargo_material", "authenticated", "pincode", # engineered from address "geo_cluster", # engineered unsupervised spatial bin ] NUMERIC_FEATURES = [ "latitude", "longitude", "age_of_truck", # temporal "start_hour", "start_dow", "start_month", "start_weekofyear", "start_day", "is_weekend", "is_morning_peak", "is_evening_peak", "is_night", "lead_time_hours", "has_advance_notice", # cyclical encodings "hour_sin", "hour_cos", "dow_sin", "dow_cos", "month_sin", "month_cos", # spatial (report-time location only; end-point/route geometry is leakage) "hist_hotspot_count", "loc_event_density", # recurrence "same_loc_cause_hist", "same_day_loc_reports", # causal target-rate encodings (past-only smoothed closure rate per group; # adapts to drift and gives the rare closure target far more signal than the # static category alone). See feature_engineering._causal_target_features. "clo_rate_family", "clo_rate_cause", "clo_rate_corridor", "clo_rate_police", "clo_rate_junction", "clo_rate_geocluster", "clo_rate_zone", "clo_rate_pincode", # ambient duration level: rolling mean log-duration of recently-resolved # events (tracks the heavy non-stationarity in clearance times). "dur_recent_level", # text-derived scalar features "desc_len", "desc_word_count", "desc_is_kannada", "desc_has_text", "lex_closed", "lex_slow", "lex_normal", "lex_diversion", "lex_blocked", "lex_heavy", "lex_festival", # missingness flags "veh_type_missing", "desc_missing", ] # Free-text column for embeddings / lexicon. TEXT_COLUMN = "description" # --------------------------------------------------------------------------- # # event_cause -> event_family mapping # Keys are lowercased/stripped event_cause values (the raw data mixes case, # e.g. "Debris"/"debris", and has 17 distinct causes). # --------------------------------------------------------------------------- # EVENT_FAMILY_MAP = { # event-driven / planned gatherings (the core of the problem statement) "public_event": "gathering", "procession": "gathering", "protest": "gathering", "vip_movement": "vip", "construction": "construction", # unplanned incidents "vehicle_breakdown": "breakdown", "accident": "accident", "tree_fall": "obstruction", "water_logging": "obstruction", "debris": "obstruction", "fog / low visibility": "obstruction", "pot_holes": "road_condition", "road_conditions": "road_condition", "congestion": "congestion", "test_demo": "other", "others": "other", } EVENT_FAMILY_DEFAULT = "other" # Features excluded from the PRIORITY model only: `corridor` is ~99.8% # deterministic of `priority` (High == named corridor, Low == Non-corridor), # so using it makes the task trivially leaky. We force the model to infer # priority from geography / cause / text / time instead. PRIORITY_EXCLUDE_FEATURES = ["corridor"] # --------------------------------------------------------------------------- # # Multilingual impact lexicon (English + transliterated + Kannada) # Used both as interpretable features and as a fallback when transformer # embeddings are unavailable. # --------------------------------------------------------------------------- # LEXICON = { "lex_closed": ["closed", "close", "block", "blocked", "shut", "ಮುಚ್ಚ", "ಬಂದ್", "ಕ್ಲೋಸ್"], "lex_slow": ["slow", "slowly", "moment", "movement", "nidhana", "ನಿಧಾನ", "ಸ್ಲೋ", "slow moment"], "lex_normal": ["normal", "no problem", "no traffic", "clear", "ಸಮಸ್ಯ ಇಲ್ಲ", "ಯಾವುದೇ ತೊಂದರೆ"], "lex_diversion": ["diversion", "divert", "u turn", "uturn", "alternate", "ಡೈವರ್ಶನ್", "ಬದಲಾವಣೆ"], "lex_blocked": ["jam", "gridlock", "heavy traffic", "congestion", "ಟ್ರಾಫಿಕ್ ಜಾಮ್", "ದಟ್ಟಣೆ"], "lex_heavy": ["lorry", "truck", "bus", "heavy", "container", "ಲಾರಿ", "ಬಸ್", "ಟ್ರಕ್"], "lex_festival": ["festival", "utsava", "uthsava", "palakki", "pallakki", "procession", "rally", "ಉತ್ಸವ", "ಪಲ್ಲಕ್ಕಿ", "ಹಬ್ಬ"], } # --------------------------------------------------------------------------- # # Modeling constants # --------------------------------------------------------------------------- # # Fraction of the (time-ordered) data held out as the final temporal test set. TEST_FRACTION = 0.20 N_FOLDS = 5 # Number of unsupervised spatial bins (KMeans on report-time coordinates). GEO_CLUSTERS = 40 # Duration label handling DURATION_MIN_MINUTES = 1.0 # drop non-positive / sub-minute durations DURATION_WINSOR_UPPER_Q = 0.99 # cap extreme long-tail durations # Causal target-rate encoding. Each (group key -> column) pair becomes a # past-only smoothed mean of the closure label within that group. Smoothing # shrinks low-count groups toward the running global rate (empirical-Bayes). CAUSAL_TARGET_SMOOTHING = 25 CLOSURE_RATE_KEYS = { "event_family": "clo_rate_family", "event_cause": "clo_rate_cause", "corridor": "clo_rate_corridor", "police_station": "clo_rate_police", "junction": "clo_rate_junction", "geo_cluster": "clo_rate_geocluster", "zone": "clo_rate_zone", "pincode": "clo_rate_pincode", } # Window (in recently-resolved events) for the ambient duration-level feature. DUR_RECENT_WINDOW = 200 # Transformer embedding model (multilingual, supports Kannada). Set the env var # GRIDLOCK_NO_TRANSFORMER=1 to force the lightweight TF-IDF fallback. EMBED_MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" # Default PCA width for the 384-dim embeddings. Kept modest (32) for the # duration / priority models: duration has only ~2.5k valid rows, so wide # embeddings overfit (validated: R2_log 0.139@32 -> 0.10@128). The closure # model, trained on all rows, benefits from a wider projection and overrides # this with CLOSURE_EMB_PCA_COMPONENTS (validated: AP 0.314@32 -> 0.335@96). EMBED_PCA_COMPONENTS = 32 CLOSURE_EMB_PCA_COMPONENTS = 96 USE_TRANSFORMER = os.environ.get("GRIDLOCK_NO_TRANSFORMER", "0") != "1" # Optuna OPTUNA_TRIALS = int(os.environ.get("GRIDLOCK_OPTUNA_TRIALS", "40")) OPTUNA_TIMEOUT = int(os.environ.get("GRIDLOCK_OPTUNA_TIMEOUT", "600")) # seconds/target NULL_TOKENS = {"NULL", "null", "None", "none", "", "nan", "NaN", "[]"}