Spaces:
Sleeping
Sleeping
| import csv | |
| import json | |
| import os | |
| import math | |
| from collections import defaultdict, Counter | |
| from datetime import datetime | |
| class DataEngine: | |
| def __init__(self, csv_path: str): | |
| self.csv_path = csv_path | |
| self.records = [] | |
| # Bounding box of Bengaluru violations (to be computed dynamically) | |
| self.min_lat = 90.0 | |
| self.max_lat = -90.0 | |
| self.min_lon = 180.0 | |
| self.max_lon = -180.0 | |
| # Grid settings | |
| self.grid_size = 100 # 100x100 grid cells | |
| # Overall statistics cached | |
| self.stats = {} | |
| # Load and process dataset | |
| self._load_data() | |
| def _load_data(self): | |
| print(f"Loading dataset from {self.csv_path}...") | |
| start_time = datetime.now() | |
| if not os.path.exists(self.csv_path): | |
| raise FileNotFoundError(f"Dataset file not found at: {self.csv_path}") | |
| temp_records = [] | |
| with open(self.csv_path, 'r', encoding='utf-8', errors='ignore') as f: | |
| reader = csv.reader(f) | |
| headers = next(reader) | |
| # Find column indices | |
| lat_idx = headers.index('latitude') | |
| lon_idx = headers.index('longitude') | |
| v_type_idx = headers.index('vehicle_type') | |
| up_v_type_idx = headers.index('updated_vehicle_type') | |
| violation_idx = headers.index('violation_type') | |
| time_idx = headers.index('created_datetime') | |
| ps_idx = headers.index('police_station') | |
| for row in reader: | |
| try: | |
| lat = float(row[lat_idx]) | |
| lon = float(row[lon_idx]) | |
| # Skip rows with invalid or zero coordinates | |
| if lat == 0.0 or lon == 0.0: | |
| continue | |
| # Track bounds | |
| if lat < self.min_lat: self.min_lat = lat | |
| if lat > self.max_lat: self.max_lat = lat | |
| if lon < self.min_lon: self.min_lon = lon | |
| if lon > self.max_lon: self.max_lon = lon | |
| # Vehicle Type fallback | |
| v_type = row[up_v_type_idx] | |
| if v_type == 'NULL' or not v_type: | |
| v_type = row[v_type_idx] | |
| # Parse violation list | |
| violation_str = row[violation_idx] | |
| try: | |
| violations = json.loads(violation_str) | |
| except: | |
| # Fallback for irregular json format | |
| violations = [v.strip('[]"\' ') for v in violation_str.split(',')] | |
| # Parse timestamp (e.g. 2023-11-20 00:28:46+00) | |
| time_str = row[time_idx].split('+')[0].strip() | |
| dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S") | |
| police_station = row[ps_idx] | |
| if police_station == 'NULL': | |
| police_station = 'Unknown' | |
| temp_records.append({ | |
| 'lat': lat, | |
| 'lon': lon, | |
| 'vehicle_type': v_type, | |
| 'violations': violations, | |
| 'hour': dt.hour, | |
| 'day_of_week': dt.weekday(), # 0=Monday, 6=Sunday | |
| 'month': dt.month, | |
| 'police_station': police_station | |
| }) | |
| except Exception as e: | |
| # Skip corrupted rows silently | |
| continue | |
| # Apply a sanity padding to bounds to prevent out-of-bounds calculations | |
| self.min_lat -= 0.005 | |
| self.max_lat += 0.005 | |
| self.min_lon -= 0.005 | |
| self.max_lon += 0.005 | |
| self.records = temp_records | |
| duration = (datetime.now() - start_time).total_seconds() | |
| print(f"Loaded {len(self.records)} valid records in {duration:.2f} seconds.") | |
| print(f"Coordinates Bounds: Lat({self.min_lat:.4f} to {self.max_lat:.4f}), Lon({self.min_lon:.4f} to {self.max_lon:.4f})") | |
| # Precompute overall statistics | |
| self._precompute_stats() | |
| def get_grid_indices(self, lat: float, lon: float) -> tuple: | |
| """Map latitude and longitude to grid cell indices (x, y).""" | |
| x = int((lon - self.min_lon) / (self.max_lon - self.min_lon) * self.grid_size) | |
| y = int((lat - self.min_lat) / (self.max_lat - self.min_lat) * self.grid_size) | |
| # Clamp to bounds | |
| x = max(0, min(self.grid_size - 1, x)) | |
| y = max(0, min(self.grid_size - 1, y)) | |
| return (x, y) | |
| def get_grid_coordinates(self, x: int, y: int) -> tuple: | |
| """Get latitude and longitude of the center of a grid cell (x, y).""" | |
| cell_lon_width = (self.max_lon - self.min_lon) / self.grid_size | |
| cell_lat_height = (self.max_lat - self.min_lat) / self.grid_size | |
| lon = self.min_lon + (x + 0.5) * cell_lon_width | |
| lat = self.min_lat + (y + 0.5) * cell_lat_height | |
| return (lat, lon) | |
| def _get_vehicle_weight(self, v_type: str) -> float: | |
| v_type = v_type.upper() | |
| if any(keyword in v_type for keyword in ['BUS', 'TRUCK', 'HEAVY', 'CONSTRUCTION', 'STAGE CARRIAGE']): | |
| return 3.0 | |
| if any(keyword in v_type for keyword in ['MAXI-CAB', 'LGV', 'VAN', 'TRACTOR']): | |
| return 2.0 | |
| if any(keyword in v_type for keyword in ['CAR', 'GOODS AUTO', 'PASSENGER AUTO', 'AUTO']): | |
| return 1.5 | |
| if any(keyword in v_type for keyword in ['SCOOTER', 'MOTOR CYCLE', 'MOPED', 'TWO WHEELER']): | |
| return 0.5 | |
| return 1.0 | |
| def _get_violation_weight(self, violations: list) -> float: | |
| max_weight = 0.5 | |
| for v in violations: | |
| v = v.upper() | |
| if any(keyword in v for keyword in ['DOUBLE PARKING', 'TRAFFIC LIGHT', 'ZEBRA CROSS']): | |
| weight = 2.5 | |
| elif any(keyword in v for keyword in ['WRONG PARKING', 'MAIN ROAD', 'FOOTPATH']): | |
| weight = 2.0 | |
| elif any(keyword in v for keyword in ['NO PARKING', 'BUSTOP', 'SCHOOL', 'ROAD CROSSING']): | |
| weight = 1.5 | |
| else: | |
| weight = 0.5 | |
| max_weight = max(max_weight, weight) | |
| return max_weight | |
| def _get_priority_tier(self, gci: float) -> str: | |
| if gci >= 40.0: | |
| return "Critical" | |
| if gci >= 15.0: | |
| return "High" | |
| if gci >= 5.0: | |
| return "Watch" | |
| return "Low" | |
| def _build_explanation(self, records: list, gci: float, count: int) -> dict: | |
| vehicle_counter = Counter() | |
| violation_counter = Counter() | |
| hour_counter = Counter() | |
| peak_weighted = 0 | |
| evidence_count = len(records) | |
| for r in records: | |
| vehicle_counter[r['vehicle_type']] += 1 | |
| hour_counter[r['hour']] += 1 | |
| if 8 <= r['hour'] <= 11 or 17 <= r['hour'] <= 20: | |
| peak_weighted += 1 | |
| for v in r['violations']: | |
| violation_counter[v] += 1 | |
| top_vehicle = vehicle_counter.most_common(1)[0][0] if vehicle_counter else "Unknown" | |
| top_violation = violation_counter.most_common(1)[0][0] if violation_counter else "Unknown" | |
| peak_hour = hour_counter.most_common(1)[0][0] if hour_counter else None | |
| peak_share = round((peak_weighted / max(1, count)) * 100.0, 1) | |
| tier = self._get_priority_tier(gci) | |
| if evidence_count >= 25: | |
| confidence = "High" | |
| elif evidence_count >= 8: | |
| confidence = "Medium" | |
| else: | |
| confidence = "Low" | |
| reasons = [] | |
| if top_violation != "Unknown": | |
| reasons.append(f"{top_violation} is the dominant violation") | |
| if top_vehicle != "Unknown": | |
| reasons.append(f"{top_vehicle} appears most often") | |
| if peak_share >= 35.0: | |
| reasons.append(f"{peak_share}% of records fall in peak traffic windows") | |
| if count >= 10: | |
| reasons.append("high repeat frequency in this cell") | |
| return { | |
| 'priority_tier': tier, | |
| 'top_vehicle_type': top_vehicle, | |
| 'top_violation_type': top_violation, | |
| 'peak_hour': peak_hour, | |
| 'peak_window_share_pct': peak_share, | |
| 'evidence_records': evidence_count, | |
| 'confidence': confidence, | |
| 'reason': "; ".join(reasons[:3]) if reasons else "limited historical evidence, monitor for recurrence" | |
| } | |
| def attach_grid_explanations(self, grid_data: dict, day_of_week: int = None, hour: int = None, police_station: str = None) -> dict: | |
| """Attach explainability metadata to existing grid cells using historical records.""" | |
| records_by_grid = defaultdict(list) | |
| fallback_records_by_grid = defaultdict(list) | |
| for r in self.records: | |
| if police_station is not None and police_station != 'All Stations' and r['police_station'] != police_station: | |
| continue | |
| x, y = self.get_grid_indices(r['lat'], r['lon']) | |
| grid_key = f"{x}_{y}" | |
| fallback_records_by_grid[grid_key].append(r) | |
| if day_of_week is not None and r['day_of_week'] != day_of_week: | |
| continue | |
| if hour is not None and r['hour'] != hour: | |
| continue | |
| records_by_grid[grid_key].append(r) | |
| for key, cell in grid_data.items(): | |
| records = records_by_grid.get(key, []) or fallback_records_by_grid.get(key, []) | |
| cell['explanation'] = self._build_explanation(records, cell.get('gci', 0.0), cell.get('count', len(records))) | |
| return grid_data | |
| def _precompute_stats(self): | |
| """Precompute overall metadata to feed static API endpoints.""" | |
| total = len(self.records) | |
| vehicle_counter = Counter() | |
| violation_counter = Counter() | |
| ps_counter = Counter() | |
| for r in self.records: | |
| vehicle_counter[r['vehicle_type']] += 1 | |
| ps_counter[r['police_station']] += 1 | |
| for v in r['violations']: | |
| violation_counter[v] += 1 | |
| # Format stats | |
| self.stats = { | |
| 'total_violations': total, | |
| 'top_vehicle_types': [{'type': k, 'count': v, 'pct': round(v/total*100, 1)} for k, v in vehicle_counter.most_common(10)], | |
| 'top_violation_types': [{'violation': k, 'count': v, 'pct': round(v/total*100, 1)} for k, v in violation_counter.most_common(10)], | |
| 'top_police_stations': [{'station': k, 'count': v, 'pct': round(v/total*100, 1)} for k, v in ps_counter.most_common(10)], | |
| 'bounds': { | |
| 'min_lat': self.min_lat, | |
| 'max_lat': self.max_lat, | |
| 'min_lon': self.min_lon, | |
| 'max_lon': self.max_lon | |
| } | |
| } | |
| def compute_grid_gci(self, day_of_week: int = None, hour: int = None, police_station: str = None) -> dict: | |
| """ | |
| Calculate GCI for all grid cells, optionally filtered by day, hour, and police station. | |
| Returns a dictionary of grid_key: gci_score. | |
| """ | |
| grid_scores = defaultdict(float) | |
| grid_counts = defaultdict(int) | |
| for r in self.records: | |
| # Apply filters | |
| if day_of_week is not None and r['day_of_week'] != day_of_week: | |
| continue | |
| if hour is not None and r['hour'] != hour: | |
| continue | |
| if police_station is not None and police_station != 'All Stations' and r['police_station'] != police_station: | |
| continue | |
| # Calculate weight | |
| v_weight = self._get_vehicle_weight(r['vehicle_type']) | |
| severity = self._get_violation_weight(r['violations']) | |
| # Temporal weight: Peak traffic hours (8-11 AM, 5-8 PM) | |
| time_factor = 1.5 if (8 <= r['hour'] <= 11 or 17 <= r['hour'] <= 20) else 1.0 | |
| violation_gci = v_weight * severity * time_factor | |
| # Grid mapping | |
| x, y = self.get_grid_indices(r['lat'], r['lon']) | |
| grid_key = f"{x}_{y}" | |
| grid_scores[grid_key] += violation_gci | |
| grid_counts[grid_key] += 1 | |
| # Structure grid output | |
| grid_data = {} | |
| for key, score in grid_scores.items(): | |
| x, y = map(int, key.split('_')) | |
| lat, lon = self.get_grid_coordinates(x, y) | |
| grid_data[key] = { | |
| 'x': x, | |
| 'y': y, | |
| 'lat': round(lat, 6), | |
| 'lon': round(lon, 6), | |
| 'gci': round(score, 1), | |
| 'count': grid_counts[key] | |
| } | |
| return self.attach_grid_explanations(grid_data, day_of_week, hour, police_station) | |
| def compute_road_hotspots(self, day_of_week: int = None, hour: int = None, police_station: str = None, limit: int = 900) -> dict: | |
| """ | |
| Aggregate violations at near-real coordinates for road-shaped map rendering. | |
| This avoids drawing artificial square grids over the road map. | |
| """ | |
| buckets = {} | |
| for r in self.records: | |
| if day_of_week is not None and r['day_of_week'] != day_of_week: | |
| continue | |
| if hour is not None and r['hour'] != hour: | |
| continue | |
| if police_station is not None and police_station != 'All Stations' and r['police_station'] != police_station: | |
| continue | |
| # Roughly 10-12m buckets, enough to merge repeated violations on the same road edge. | |
| key = f"{round(r['lat'], 4)}_{round(r['lon'], 4)}" | |
| if key not in buckets: | |
| buckets[key] = { | |
| 'lat_sum': 0.0, | |
| 'lon_sum': 0.0, | |
| 'gci': 0.0, | |
| 'count': 0, | |
| 'records': [] | |
| } | |
| v_weight = self._get_vehicle_weight(r['vehicle_type']) | |
| severity = self._get_violation_weight(r['violations']) | |
| time_factor = 1.5 if (8 <= r['hour'] <= 11 or 17 <= r['hour'] <= 20) else 1.0 | |
| buckets[key]['lat_sum'] += r['lat'] | |
| buckets[key]['lon_sum'] += r['lon'] | |
| buckets[key]['gci'] += v_weight * severity * time_factor | |
| buckets[key]['count'] += 1 | |
| buckets[key]['records'].append(r) | |
| ranked = sorted(buckets.items(), key=lambda item: item[1]['gci'], reverse=True)[:limit] | |
| hotspots = {} | |
| for idx, (_, bucket) in enumerate(ranked): | |
| lat = bucket['lat_sum'] / bucket['count'] | |
| lon = bucket['lon_sum'] / bucket['count'] | |
| x, y = self.get_grid_indices(lat, lon) | |
| key = f"road_{idx}" | |
| gci = round(bucket['gci'], 1) | |
| hotspots[key] = { | |
| 'id': key, | |
| 'render_type': 'road_hotspot', | |
| 'x': x, | |
| 'y': y, | |
| 'lat': round(lat, 6), | |
| 'lon': round(lon, 6), | |
| 'gci': gci, | |
| 'count': bucket['count'], | |
| 'explanation': self._build_explanation(bucket['records'], gci, bucket['count']) | |
| } | |
| return hotspots | |
| def rank_police_stations(self, day_of_week: int = None, hour: int = None, limit: int = 10) -> list: | |
| """Rank stations by current hotspot burden using only historical violation data.""" | |
| station_scores = defaultdict(float) | |
| station_counts = defaultdict(int) | |
| station_hours = defaultdict(Counter) | |
| station_violations = defaultdict(Counter) | |
| for r in self.records: | |
| if r['police_station'] == 'Unknown': | |
| continue | |
| if day_of_week is not None and r['day_of_week'] != day_of_week: | |
| continue | |
| if hour is not None and r['hour'] != hour: | |
| continue | |
| v_weight = self._get_vehicle_weight(r['vehicle_type']) | |
| severity = self._get_violation_weight(r['violations']) | |
| time_factor = 1.5 if (8 <= r['hour'] <= 11 or 17 <= r['hour'] <= 20) else 1.0 | |
| score = v_weight * severity * time_factor | |
| station = r['police_station'] | |
| station_scores[station] += score | |
| station_counts[station] += 1 | |
| station_hours[station][r['hour']] += 1 | |
| for v in r['violations']: | |
| station_violations[station][v] += 1 | |
| rankings = [] | |
| for station, score in station_scores.items(): | |
| recommended_units = 1 | |
| if score >= 600: | |
| recommended_units = 4 | |
| elif score >= 300: | |
| recommended_units = 3 | |
| elif score >= 120: | |
| recommended_units = 2 | |
| top_violation = station_violations[station].most_common(1) | |
| peak_hour = station_hours[station].most_common(1) | |
| rankings.append({ | |
| 'station': station, | |
| 'total_gci': round(score, 1), | |
| 'records': station_counts[station], | |
| 'peak_hour': peak_hour[0][0] if peak_hour else None, | |
| 'top_violation': top_violation[0][0] if top_violation else 'Unknown', | |
| 'recommended_units': recommended_units | |
| }) | |
| rankings.sort(key=lambda row: row['total_gci'], reverse=True) | |
| return rankings[:limit] | |