flipkart_gridlock_2.0 / backend /data_engine.py
Harshith
Initial commit of essential files for Gridlock Smart Patrol dashboard
e3351a1
Raw
History Blame Contribute Delete
17.7 kB
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]