River_Network / src /generate_plots.py
ageraustine's picture
Upload folder using huggingface_hub
a74054f verified
Raw
History Blame Contribute Delete
12.5 kB
"""
Generate a curated, high-quality set of plots for every data loader in the
project: hydrometric, ADES groundwater, IDPR, SAFRAN/ERA5, watershed
shapefile, and station elevations.
Each loader's plots are generated independently and skipped gracefully
(with a clear message) if its data files aren't present or an optional
dependency (geopandas, xarray) isn't installed — one missing dataset
never stops the others from running.
Usage:
python generate_plots.py --data-root DATASETS_DIR --output-root OUTPUT_DIR
Expected layout under DATASETS_DIR (any missing piece is just skipped):
hydrometric/discharge_observations.csv
hydrometric/waterlevel_observations.csv
ades/groundwater_levels_watershed.csv
ades/groundwater_stations.csv
idpr.csv
safran/era5_*.nc
shapefile/*.shp
station_elevations.csv
"""
import argparse
from pathlib import Path
import pandas as pd
import matplotlib.pyplot as plt
try:
from .data.loaders.base import BaseDataLoader # noqa: F401 (import check only)
from .data.loaders.hydrometric import HydrometricLoader
from .data.loaders.ades import ADESLoader
from .data.loaders.idpr import IDPRLoader
from .data.loaders.station_elevations import StationElevationsLoader
except ImportError:
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from src.data.loaders.hydrometric import HydrometricLoader
from src.data.loaders.ades import ADESLoader
from src.data.loaders.idpr import IDPRLoader
from src.data.loaders.station_elevations import StationElevationsLoader
# ----------------------------------------------------------------------
# Helpers
# ----------------------------------------------------------------------
def _section(title: str) -> None:
print(f"\n{'=' * 60}\n{title}\n{'=' * 60}")
def select_complete_stations(
df, n: int, recent_years: int = 5, require_both_variables: bool = True
) -> list:
"""Pick the n stations with the most complete data in the most recent window."""
cutoff = df["date"].max() - pd.DateOffset(years=recent_years)
recent = df[df["date"] >= cutoff]
expected_days = (recent["date"].max() - recent["date"].min()).days + 1
scores = []
for station_code, group in recent.groupby("station_code"):
disc_frac = group["discharge_m3s"].notna().sum() / expected_days
wl_frac = group["waterlevel_mm"].notna().sum() / expected_days
if require_both_variables and (disc_frac == 0 or wl_frac == 0):
continue
scores.append((station_code, min(disc_frac, wl_frac)))
scores.sort(key=lambda x: x[1], reverse=True)
selected = [s for s, _ in scores[:n]]
if len(selected) < n:
counts = df.dropna(subset=["discharge_m3s"]).groupby("station_code").size()
fallback = counts.sort_values(ascending=False).head(n).index.tolist()
selected = list(dict.fromkeys(selected + fallback))[:n]
return selected
def filter_recent(df, years: int = 5):
cutoff = df["date"].max() - pd.DateOffset(years=years)
return df[df["date"] >= cutoff]
# ----------------------------------------------------------------------
# Per-loader plot generators — each returns True if it ran, False if skipped
# ----------------------------------------------------------------------
def generate_hydrometric_plots(data_dir: Path, output_dir: Path, recent_years: int) -> bool:
_section("HYDROMETRIC")
if not data_dir.exists():
print(f"Skipped: {data_dir} not found")
return False
loader = HydrometricLoader(data_path=data_dir)
try:
full_df = loader.load()
except FileNotFoundError as e:
print(f"Skipped: {e}")
return False
df = filter_recent(full_df, years=recent_years)
print(f"{len(full_df)} rows, {full_df['station_code'].nunique()} stations. "
f"Using last {recent_years}y: {df['date'].min().date()}{df['date'].max().date()}")
comparison_stations = select_complete_stations(full_df, n=3, recent_years=recent_years)
deepdive_station = select_complete_stations(full_df, n=1, recent_years=recent_years)[0]
print(f"Comparison stations: {comparison_stations} | Deep dive: {deepdive_station}")
output_dir.mkdir(parents=True, exist_ok=True)
loader.plot_waterlevel(df=df, stations=comparison_stations,
save_path=output_dir / "waterlevel_comparison.png")
plt.close("all")
loader.plot_discharge(df=df, stations=comparison_stations,
save_path=output_dir / "discharge_comparison.png")
plt.close("all")
loader.plot_seasonal_climatology("discharge_m3s", df=df, stations=comparison_stations,
save_path=output_dir / "climatology_discharge.png")
plt.close("all")
station_df = df[df["station_code"] == deepdive_station]
loader.plot_station(deepdive_station, df=station_df,
save_path=output_dir / "station_deepdive.png")
plt.close("all")
try:
loader.plot_rating_curve(deepdive_station, df=station_df,
save_path=output_dir / "rating_curve.png")
plt.close("all")
except ValueError as e:
print(f" Skipped rating curve: {e}")
print(f"Saved plots to {output_dir}")
return True
def generate_ades_plots(data_dir: Path, output_dir: Path, n_wells: int = 3) -> bool:
_section("ADES GROUNDWATER")
if not data_dir.exists():
print(f"Skipped: {data_dir} not found")
return False
loader = ADESLoader(data_path=data_dir)
try:
df = loader.load()
except FileNotFoundError as e:
print(f"Skipped: {e}")
return False
if df.empty:
print("Skipped: no groundwater data loaded")
return False
output_dir.mkdir(parents=True, exist_ok=True)
# Pick the n_wells with the most observations, for a readable comparison.
id_col = "code_bss" if "code_bss" in df.columns else "station_code"
value_col = "groundwater_level_m" if "groundwater_level_m" in df.columns else "avg_groundwater_level_m"
top_wells = (df.dropna(subset=[value_col]).groupby(id_col).size()
.sort_values(ascending=False).head(n_wells).index.tolist())
print(f"Wells: {top_wells}")
loader.plot_levels(df=df, wells=top_wells, save_path=output_dir / "groundwater_levels.png")
plt.close("all")
try:
loader.plot_well_locations(df=df, save_path=output_dir / "well_locations.png")
plt.close("all")
except ValueError as e:
print(f" Skipped well map: {e}")
print(f"Saved plots to {output_dir}")
return True
def generate_idpr_plots(data_path: Path, output_dir: Path) -> bool:
_section("IDPR")
if not data_path.exists():
print(f"Skipped: {data_path} not found")
return False
loader = IDPRLoader(data_path=data_path)
df = loader.load()
output_dir.mkdir(parents=True, exist_ok=True)
loader.plot_distribution(df=df, save_path=output_dir / "idpr_distribution.png")
plt.close("all")
try:
loader.plot_spatial(df=df, save_path=output_dir / "idpr_spatial.png")
plt.close("all")
except ValueError as e:
print(f" Skipped spatial plot: {e}")
print(f"Saved plots to {output_dir}")
return True
def generate_safran_plots(
data_dir: Path, output_dir: Path, station_coords: "pd.DataFrame | None"
) -> bool:
_section("SAFRAN / ERA5")
if not data_dir.exists():
print(f"Skipped: {data_dir} not found")
return False
if station_coords is None or station_coords.empty:
print("Skipped: no station coordinates available (needs station_elevations.csv "
"or another source of station lat/lon)")
return False
try:
from .data.loaders.safran import SAFRANLoader
except ImportError:
try:
from src.data.loaders.safran import SAFRANLoader
except ImportError as e:
print(f"Skipped: {e}")
return False
try:
import xarray # noqa: F401
except ImportError:
print("Skipped: xarray is not installed in this environment")
return False
loader = SAFRANLoader(data_path=data_dir, station_coords=station_coords)
try:
df = loader.load()
except FileNotFoundError as e:
print(f"Skipped: {e}")
return False
output_dir.mkdir(parents=True, exist_ok=True)
loader.plot_temperature(df=df, save_path=output_dir / "temperature.png")
plt.close("all")
loader.plot_precipitation(df=df, save_path=output_dir / "precipitation_cumulative.png")
plt.close("all")
print(f"Saved plots to {output_dir}")
return True
def generate_shapefile_plots(data_path: Path, output_dir: Path) -> bool:
_section("WATERSHED SHAPEFILE")
if not data_path.exists():
print(f"Skipped: {data_path} not found")
return False
try:
from .data.loaders.shapefile import ShapefileLoader
except ImportError:
try:
from src.data.loaders.shapefile import ShapefileLoader
except ImportError as e:
print(f"Skipped: {e}")
return False
try:
import geopandas # noqa: F401
except ImportError:
print("Skipped: geopandas is not installed in this environment")
return False
loader = ShapefileLoader(data_path=data_path)
try:
gdf = loader.load()
except (FileNotFoundError, ValueError) as e:
print(f"Skipped: {e}")
return False
output_dir.mkdir(parents=True, exist_ok=True)
loader.plot(gdf=gdf, save_path=output_dir / "watershed_boundary.png")
plt.close("all")
print(f"Saved plots to {output_dir}")
return True
def generate_station_elevation_plots(data_path: Path, output_dir: Path) -> "pd.DataFrame | None":
"""Returns the coordinates DataFrame (for reuse by SAFRAN) or None if skipped."""
_section("STATION ELEVATIONS")
if not data_path.exists():
print(f"Skipped: {data_path} not found")
return None
loader = StationElevationsLoader(data_path=data_path)
df = loader.load()
output_dir.mkdir(parents=True, exist_ok=True)
loader.plot_elevation_bar(df=df, save_path=output_dir / "elevation_bar.png")
plt.close("all")
loader.plot_station_map(df=df, save_path=output_dir / "station_map.png")
plt.close("all")
print(f"Saved plots to {output_dir}")
return loader.get_coordinates_df()
# ----------------------------------------------------------------------
# Orchestrator
# ----------------------------------------------------------------------
def generate_all(data_root: Path, output_root: Path, recent_years: int = 5) -> None:
results = {}
# Station elevations first: SAFRAN needs its coordinates output.
station_coords = generate_station_elevation_plots(
data_root / "station_elevations.csv", output_root / "station_elevations"
)
results["station_elevations"] = station_coords is not None
results["hydrometric"] = generate_hydrometric_plots(
data_root / "hydrometric", output_root / "hydrometric", recent_years
)
results["ades"] = generate_ades_plots(
data_root / "ades", output_root / "ades"
)
results["idpr"] = generate_idpr_plots(
data_root / "idpr.csv", output_root / "idpr"
)
results["safran"] = generate_safran_plots(
data_root / "safran", output_root / "safran", station_coords
)
results["shapefile"] = generate_shapefile_plots(
data_root / "shapefile", output_root / "shapefile"
)
_section("SUMMARY")
for name, ok in results.items():
print(f" {'✓' if ok else '—'} {name}")
def main() -> None:
parser = argparse.ArgumentParser(description="Generate plots for all data loaders")
parser.add_argument("--data-root", type=Path, default=Path("datasets"),
help="Root directory containing each loader's data subfolder/file")
parser.add_argument("--output-root", type=Path, default=Path("plots"),
help="Root directory to save generated plots (one subfolder per loader)")
parser.add_argument("--recent-years", type=int, default=5,
help="Recent-years window for hydrometric station selection/plots")
args = parser.parse_args()
generate_all(args.data_root, args.output_root, recent_years=args.recent_years)
if __name__ == "__main__":
main()