Spaces:
Running on Zero
Running on Zero
| """ | |
| ADES groundwater level data loader. | |
| Single Responsibility: Load groundwater (karst) connectivity features. | |
| """ | |
| import pandas as pd | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import matplotlib.dates as mdates | |
| from pathlib import Path | |
| from typing import Optional, List | |
| from .base import BaseDataLoader | |
| class ADESLoader(BaseDataLoader): | |
| """ | |
| Loads ADES groundwater level data for karst connectivity analysis. | |
| """ | |
| def __init__( | |
| self, | |
| data_path: Path, | |
| file_format: str = "csv", | |
| station_ids: Optional[List[str]] = None | |
| ): | |
| """ | |
| Initialize ADES loader. | |
| Args: | |
| data_path: Path to ADES data directory | |
| file_format: Data format (csv, json, etc.) | |
| station_ids: Optional list of groundwater station IDs | |
| """ | |
| super().__init__(data_path) | |
| self.file_format = file_format.lower() | |
| self.station_ids = station_ids | |
| def load(self) -> pd.DataFrame: | |
| """ | |
| Load ADES groundwater data. | |
| Returns: | |
| DataFrame with groundwater level measurements | |
| """ | |
| if self.file_format == "csv": | |
| df = self._load_csv() | |
| else: | |
| raise ValueError(f"Unsupported format: {self.file_format}") | |
| # Filter by station IDs if provided | |
| if self.station_ids: | |
| if "station_id" in df.columns: | |
| df = df[df["station_id"].isin(self.station_ids)] | |
| # Ensure date column is datetime if present | |
| if "date" in df.columns: | |
| df["date"] = pd.to_datetime(df["date"]) | |
| return df | |
| def _load_csv(self) -> pd.DataFrame: | |
| """Load from CSV file(s).""" | |
| if self.data_path.is_file(): | |
| return pd.read_csv(self.data_path) | |
| elif self.data_path.is_dir(): | |
| # Load groundwater levels and stations, then merge | |
| levels_path = self.data_path / "groundwater_levels_watershed.csv" | |
| stations_path = self.data_path / "groundwater_stations.csv" | |
| if levels_path.exists() and stations_path.exists(): | |
| levels = pd.read_csv(levels_path) | |
| stations = pd.read_csv(stations_path) | |
| # Merge to get coordinates with each measurement | |
| df = pd.merge( | |
| levels[['code_bss', 'date_mesure', 'niveau_nappe_eau', 'profondeur_nappe']], | |
| stations[['code_bss', 'x', 'y']], | |
| on='code_bss', | |
| how='left' | |
| ) | |
| df = df.rename(columns={ | |
| 'date_mesure': 'date', | |
| 'niveau_nappe_eau': 'groundwater_level_m', | |
| 'profondeur_nappe': 'groundwater_depth_m', | |
| 'x': 'lon', | |
| 'y': 'lat' | |
| }) | |
| return df | |
| else: | |
| csv_files = list(self.data_path.glob("*.csv")) | |
| if not csv_files: | |
| raise FileNotFoundError(f"No CSV files found in {self.data_path}") | |
| dfs = [pd.read_csv(f) for f in csv_files] | |
| return pd.concat(dfs, ignore_index=True) | |
| else: | |
| raise ValueError(f"Invalid path: {self.data_path}") | |
| def aggregate_to_stations( | |
| self, | |
| groundwater_df: pd.DataFrame, | |
| station_coords: pd.DataFrame, | |
| max_distance_km: float = 20.0 | |
| ) -> pd.DataFrame: | |
| """ | |
| Aggregate groundwater levels spatially to hydrometric stations. | |
| Args: | |
| groundwater_df: ADES data with [date, code_bss, groundwater_level_m, lat, lon] | |
| station_coords: Station coordinates [station_code, lat, lon] | |
| max_distance_km: Max distance to consider wells (km) | |
| Returns: | |
| DataFrame with [date, station_code, avg_groundwater_level_m, n_wells] | |
| """ | |
| # Haversine distance | |
| def haversine(lat1, lon1, lat2, lon2): | |
| R = 6371 # Earth radius in km | |
| lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2]) | |
| dlat = lat2 - lat1 | |
| dlon = lon2 - lon1 | |
| a = np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2 | |
| return R * 2 * np.arcsin(np.sqrt(a)) | |
| results = [] | |
| for _, station in station_coords.iterrows(): | |
| station_code = station['station_code'] | |
| station_lat = station['lat'] | |
| station_lon = station['lon'] | |
| # Find wells within radius | |
| groundwater_df['distance_km'] = groundwater_df.apply( | |
| lambda row: haversine(station_lat, station_lon, row['lat'], row['lon']), | |
| axis=1 | |
| ) | |
| nearby_wells = groundwater_df[groundwater_df['distance_km'] <= max_distance_km].copy() | |
| if len(nearby_wells) == 0: | |
| continue | |
| # Aggregate by date | |
| daily_agg = nearby_wells.groupby('date').agg({ | |
| 'groundwater_level_m': 'mean', | |
| 'groundwater_depth_m': 'mean', | |
| 'code_bss': 'nunique' | |
| }).reset_index() | |
| daily_agg['station_code'] = station_code | |
| daily_agg = daily_agg.rename(columns={ | |
| 'groundwater_level_m': 'avg_groundwater_level_m', | |
| 'groundwater_depth_m': 'avg_groundwater_depth_m', | |
| 'code_bss': 'n_wells' | |
| }) | |
| results.append(daily_agg) | |
| if not results: | |
| return pd.DataFrame() | |
| return pd.concat(results, ignore_index=True) | |
| def get_metadata(self) -> dict: | |
| """Get ADES data metadata.""" | |
| meta = super().get_metadata() | |
| meta.update({ | |
| "data_type": "ades_groundwater", | |
| "format": self.file_format | |
| }) | |
| return meta | |
| def plot_levels( | |
| self, | |
| df: Optional[pd.DataFrame] = None, | |
| wells: Optional[List[str]] = None, | |
| figsize: tuple = (12, 5), | |
| save_path: Optional[Path] = None, | |
| ) -> plt.Axes: | |
| """ | |
| Plot groundwater level time series, one line per well/station. | |
| Works with either raw well-level data (code_bss, groundwater_level_m) | |
| or data already aggregated to hydrometric stations via | |
| `aggregate_to_stations` (station_code, avg_groundwater_level_m). | |
| Args: | |
| df: Optional pre-loaded DataFrame. If not provided, data is loaded from disk. | |
| wells: Optional list of well/station IDs to plot. Defaults to all. | |
| figsize: Figure size in inches. | |
| save_path: If provided, saves the figure to this path. | |
| Returns: | |
| The matplotlib Axes object. | |
| """ | |
| if df is None: | |
| df = self.load() | |
| if "station_code" in df.columns and "avg_groundwater_level_m" in df.columns: | |
| id_col, value_col = "station_code", "avg_groundwater_level_m" | |
| elif "code_bss" in df.columns and "groundwater_level_m" in df.columns: | |
| id_col, value_col = "code_bss", "groundwater_level_m" | |
| else: | |
| raise ValueError( | |
| "Could not find a recognizable (id, level) column pair in the data" | |
| ) | |
| plot_df = df.dropna(subset=[value_col]) | |
| if wells: | |
| plot_df = plot_df[plot_df[id_col].isin(wells)] | |
| if plot_df.empty: | |
| raise ValueError("No groundwater level data available to plot") | |
| fig, ax = plt.subplots(figsize=figsize) | |
| for well_id, group in plot_df.groupby(id_col): | |
| group = group.sort_values("date") | |
| ax.plot(group["date"], group[value_col], linewidth=1, marker="o", | |
| markersize=2, label=well_id) | |
| ax.set_title("Groundwater Level") | |
| ax.set_xlabel("Date") | |
| ax.set_ylabel("Groundwater level (m)") | |
| ax.xaxis.set_major_locator(mdates.AutoDateLocator()) | |
| ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(ax.xaxis.get_major_locator())) | |
| ax.legend(title=id_col, fontsize=8) | |
| ax.grid(True, alpha=0.3) | |
| plt.tight_layout() | |
| if save_path: | |
| plt.savefig(save_path, dpi=150, bbox_inches="tight") | |
| print(f"Plot saved to {save_path}") | |
| return ax | |
| def plot_well_locations( | |
| self, | |
| df: Optional[pd.DataFrame] = None, | |
| figsize: tuple = (8, 8), | |
| save_path: Optional[Path] = None, | |
| ) -> plt.Axes: | |
| """ | |
| Scatter map of well locations, colored by mean groundwater level. | |
| Requires raw well-level data with lat/lon (i.e. before aggregation | |
| to hydrometric stations). | |
| Args: | |
| df: Optional pre-loaded DataFrame. If not provided, data is loaded from disk. | |
| figsize: Figure size in inches. | |
| save_path: If provided, saves the figure to this path. | |
| Returns: | |
| The matplotlib Axes object. | |
| """ | |
| if df is None: | |
| df = self.load() | |
| required = {"code_bss", "lat", "lon", "groundwater_level_m"} | |
| if not required.issubset(df.columns): | |
| raise ValueError( | |
| f"plot_well_locations requires columns {required}; " | |
| "this looks like already-aggregated station data." | |
| ) | |
| summary = df.dropna(subset=["lat", "lon"]).groupby( | |
| ["code_bss", "lat", "lon"] | |
| )["groundwater_level_m"].mean().reset_index() | |
| fig, ax = plt.subplots(figsize=figsize) | |
| scatter = ax.scatter( | |
| summary["lon"], summary["lat"], c=summary["groundwater_level_m"], | |
| cmap="viridis_r", s=40, edgecolor="black", linewidth=0.3, | |
| ) | |
| cbar = plt.colorbar(scatter, ax=ax) | |
| cbar.set_label("Mean groundwater level (m)") | |
| ax.set_title(f"Groundwater Well Locations (n={len(summary)})") | |
| ax.set_xlabel("Longitude") | |
| ax.set_ylabel("Latitude") | |
| ax.set_aspect("equal") | |
| plt.tight_layout() | |
| if save_path: | |
| plt.savefig(save_path, dpi=150, bbox_inches="tight") | |
| print(f"Plot saved to {save_path}") | |
| return ax |