River_Network / src /data /processors /cleaner.py
ageraustine's picture
Upload folder using huggingface_hub
a74054f verified
Raw
History Blame Contribute Delete
5.9 kB
"""
Data cleaning utilities.
Single Responsibility: Clean and preprocess hydrological/meteorological data.
"""
import pandas as pd
import numpy as np
from typing import Optional, List
class DataCleaner:
"""
Cleans hydrological and meteorological datasets.
Handles missing values, outliers, and data quality issues.
"""
def __init__(
self,
missing_threshold: float = 0.3,
outlier_method: str = "iqr"
):
"""
Initialize data cleaner.
Args:
missing_threshold: Max fraction of missing values allowed per column
outlier_method: Method for outlier detection ("iqr", "zscore", "none")
"""
self.missing_threshold = missing_threshold
self.outlier_method = outlier_method
def clean(
self,
df: pd.DataFrame,
date_col: str = "date",
station_col: str = "station_id"
) -> pd.DataFrame:
"""
Clean dataframe with multiple steps.
Args:
df: Input dataframe
date_col: Name of date column
station_col: Name of station ID column
Returns:
Cleaned dataframe
"""
df = df.copy()
# Remove duplicates
df = self._remove_duplicates(df, date_col, station_col)
# Handle missing values
df = self._handle_missing(df, date_col, station_col)
# Remove outliers if requested
if self.outlier_method != "none":
df = self._remove_outliers(df, date_col, station_col)
# Sort by date and station
if date_col in df.columns and station_col in df.columns:
df = df.sort_values([station_col, date_col]).reset_index(drop=True)
return df
def _remove_duplicates(
self,
df: pd.DataFrame,
date_col: str,
station_col: str
) -> pd.DataFrame:
"""Remove duplicate rows based on date and station."""
if date_col in df.columns and station_col in df.columns:
df = df.drop_duplicates(subset=[date_col, station_col], keep="first")
return df
def _handle_missing(
self,
df: pd.DataFrame,
date_col: str,
station_col: str
) -> pd.DataFrame:
"""
Handle missing values.
- Drop columns with >threshold missing
- Forward fill small gaps in time series
"""
# Drop columns with too many missing values
missing_frac = df.isnull().mean()
cols_to_keep = missing_frac[missing_frac <= self.missing_threshold].index
df = df[cols_to_keep]
# Forward fill small gaps (max 3 days) within each station
if station_col in df.columns:
numeric_cols = df.select_dtypes(include=[np.number]).columns
df[numeric_cols] = df.groupby(station_col)[numeric_cols].transform(
lambda x: x.fillna(method='ffill', limit=3)
)
return df
def _remove_outliers(
self,
df: pd.DataFrame,
date_col: str,
station_col: str
) -> pd.DataFrame:
"""
Remove outliers using IQR or Z-score method.
Only applies to numeric columns.
"""
numeric_cols = df.select_dtypes(include=[np.number]).columns
numeric_cols = [c for c in numeric_cols if c != station_col]
if self.outlier_method == "iqr":
df = self._remove_outliers_iqr(df, numeric_cols, station_col)
elif self.outlier_method == "zscore":
df = self._remove_outliers_zscore(df, numeric_cols, station_col)
return df
def _remove_outliers_iqr(
self,
df: pd.DataFrame,
cols: List[str],
station_col: str
) -> pd.DataFrame:
"""Remove outliers using IQR method (per station)."""
for col in cols:
if col in df.columns:
# Calculate IQR per station
Q1 = df.groupby(station_col)[col].transform(lambda x: x.quantile(0.25))
Q3 = df.groupby(station_col)[col].transform(lambda x: x.quantile(0.75))
IQR = Q3 - Q1
lower_bound = Q1 - 3 * IQR
upper_bound = Q3 + 3 * IQR
# Set outliers to NaN
df.loc[(df[col] < lower_bound) | (df[col] > upper_bound), col] = np.nan
return df
def _remove_outliers_zscore(
self,
df: pd.DataFrame,
cols: List[str],
station_col: str,
threshold: float = 4.0
) -> pd.DataFrame:
"""Remove outliers using Z-score method (per station)."""
for col in cols:
if col in df.columns:
# Calculate Z-scores per station
mean = df.groupby(station_col)[col].transform('mean')
std = df.groupby(station_col)[col].transform('std')
z_scores = np.abs((df[col] - mean) / std)
# Set outliers to NaN
df.loc[z_scores > threshold, col] = np.nan
return df
def get_cleaning_report(self, df_before: pd.DataFrame, df_after: pd.DataFrame) -> dict:
"""
Generate report of cleaning operations.
Args:
df_before: DataFrame before cleaning
df_after: DataFrame after cleaning
Returns:
Dictionary with cleaning statistics
"""
return {
"rows_before": len(df_before),
"rows_after": len(df_after),
"rows_removed": len(df_before) - len(df_after),
"cols_before": len(df_before.columns),
"cols_after": len(df_after.columns),
"cols_removed": len(df_before.columns) - len(df_after.columns)
}