Gridlock / src /cleaning.py
parvmittal07's picture
Initial Demo complete
01eb82e
Raw
History Blame Contribute Delete
4.55 kB
"""Data cleaning: NULL normalisation, type coercion, timestamp parsing,
coordinate sanity fixes, de-duplication and auto-resolution flagging.
Nothing here builds features or targets; it only produces a typed, sane frame.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from . import config as C
from .data_loading import load_raw
def _strip_null_tokens(df: pd.DataFrame) -> pd.DataFrame:
"""Replace the dataset's many textual NULL spellings with real NaN."""
return df.map(
lambda v: np.nan if isinstance(v, str) and v.strip() in C.NULL_TOKENS else v
)
def _parse_datetimes(df: pd.DataFrame) -> pd.DataFrame:
for col in C.DATETIME_COLUMNS:
if col in df.columns:
df[col] = pd.to_datetime(df[col], utc=True, errors="coerce")
return df
def _coerce_numeric(df: pd.DataFrame) -> pd.DataFrame:
for col in ["latitude", "longitude", "endlatitude", "endlongitude", "age_of_truck"]:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df
def _coerce_booleans(df: pd.DataFrame) -> pd.DataFrame:
if "requires_road_closure" in df.columns:
df["requires_road_closure"] = (
df["requires_road_closure"].astype(str).str.upper().map({"TRUE": True, "FALSE": False})
)
if "authenticated" in df.columns:
df["authenticated"] = (
df["authenticated"].astype(str).str.lower().map({"yes": True, "no": False})
)
return df
def _fix_coordinates(df: pd.DataFrame) -> pd.DataFrame:
"""Sentinel 0/placeholder coordinates -> NaN; keep only plausible Bengaluru.
End coordinates use ``0`` as a "no end point" sentinel. Main coordinates
occasionally fall outside a sane bounding box (data entry noise).
"""
for col in ["endlatitude", "endlongitude"]:
if col in df.columns:
df.loc[df[col].abs() < 1e-6, col] = np.nan
# Bengaluru metropolitan bounding box (generous).
lat_ok = df["latitude"].between(12.6, 13.4)
lon_ok = df["longitude"].between(77.2, 77.9)
df.loc[~(lat_ok), "latitude"] = np.nan
df.loc[~(lon_ok), "longitude"] = np.nan
if {"endlatitude", "endlongitude"}.issubset(df.columns):
elat_ok = df["endlatitude"].between(12.6, 13.4)
elon_ok = df["endlongitude"].between(77.2, 77.9)
df.loc[~elat_ok, "endlatitude"] = np.nan
df.loc[~elon_ok, "endlongitude"] = np.nan
return df
def _flag_auto_resolution(df: pd.DataFrame) -> pd.DataFrame:
"""Flag rows whose resolution/modification looks like a nightly batch job.
Many records are auto-closed at minute/second ``35:47`` or ``05:46`` which
are clearly system timestamps rather than the true clearance time. These
make the duration label unreliable and are flagged so targets.py can decide
how to treat them.
"""
suspicious = pd.Series(False, index=df.index)
for col in ["modified_datetime", "resolved_datetime", "closed_datetime"]:
if col in df.columns:
ts = df[col]
mark = (ts.dt.minute == 35) & (ts.dt.second.between(47, 48))
mark |= (ts.dt.second == 46) & (ts.dt.minute.isin([5, 35]))
suspicious = suspicious | mark.fillna(False)
df["auto_resolved_flag"] = suspicious
return df
def clean(df: pd.DataFrame | None = None, save: bool = True) -> pd.DataFrame:
"""Run the full cleaning pipeline and (optionally) persist a parquet."""
if df is None:
df = load_raw()
df = _strip_null_tokens(df)
df = _parse_datetimes(df)
df = _coerce_numeric(df)
df = _coerce_booleans(df)
df = _fix_coordinates(df)
# Drop exact duplicate event ids (keep the first occurrence).
if "id" in df.columns:
df = df.drop_duplicates(subset="id", keep="first")
# Require a usable start time and location to be a modelable event.
df = df[df["start_datetime"].notna()].copy()
df = df[df["latitude"].notna() & df["longitude"].notna()].copy()
df = _flag_auto_resolution(df)
# Order chronologically by report/creation time -> enables temporal splits.
df["order_time"] = df["created_date"].fillna(df["start_datetime"])
df = df.sort_values("order_time").reset_index(drop=True)
if save:
df.to_parquet(C.CLEAN_PARQUET, index=False)
return df
if __name__ == "__main__": # pragma: no cover
out = clean()
print("clean shape:", out.shape)
print(out[["event_type", "event_cause", "requires_road_closure", "priority"]].describe(include="all"))