---
title: "xaiHydro: Explainable AI Tools for Hydro-Climate Modelling"
subtitle: "A Unified R Workflow — Book Chapter Companion Vignette"
author:
  - name: "Sadikul Islam"
    affiliation: "Department of Hydrology, [Your Institution], India"
    email: "sadikul.islam@institution.ac.in"
date: "`r Sys.Date()`"
output:
  rmarkdown::html_vignette:
    toc: true
    toc_depth: 4
    number_sections: true
    fig_width: 8
    fig_height: 5
    df_print: kable
vignette: >
  %\VignetteIndexEntry{xaiHydro: Explainable AI for Hydro-Climate Models}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
bibliography: references.bib
link-citations: true
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse  = TRUE,
  comment   = "#>",
  fig.align = "center",
  warning   = FALSE,
  message   = FALSE,
  fig.width  = 8,
  fig.height = 5,
  out.width  = "100%"
)
```

---

> **Citation**: Islam, S. (2026). Explainable AI for Hydro-Climate Models:
> A Unified R Workflow Using SHAP, LIME and Partial Dependence. In:
> *Hydro-Climate Analytics: Remote Sensing, AI and Geospatial Modelling*
> (Springer). DOI: 10.XXXX/XXXXXXX

---

# Introduction

## Background

Machine learning models are increasingly used in hydro-climate science
to predict streamflow, estimate drought indices, map flood risk, and
project evapotranspiration under changing climate scenarios. These
models — random forests, gradient boosting machines, deep neural networks
— often outperform traditional physically-based models in predictive
accuracy [@breiman2001random]. However, they operate as **black boxes**:
their internal decision logic is opaque to practitioners, regulators, and
policy makers who must trust model outputs for operational water
management and climate adaptation decisions.

Explainable Artificial Intelligence (XAI) addresses this gap by providing
mathematically rigorous tools to attribute model predictions to individual
input features. XAI transforms a black-box model into an interpretable
system without sacrificing predictive performance.

## The xaiHydro package

`xaiHydro` provides a **unified, hydrology-aware R workflow** for applying
three complementary XAI families to any trained hydro-climate model:

| Method | Scope | Key advantage |
|--------|-------|---------------|
| **SHAP** [@lundberg2017unified] | Global + local | Theoretically grounded; satisfies efficiency, symmetry, and dummy axioms |
| **LIME** [@ribeiro2016should] | Local | Interpretable surrogate; works with any model type |
| **PDP / ALE** [@friedman2001greedy; @apley2020visualizing] | Global marginal | Visualises feature response curves across the predictor range |

All three methods require only a **trained model** and its **training
data** — no additional field data collection, no satellite downloads, no
pre-processing pipelines. This makes `xaiHydro` uniquely accessible for
researchers who already have a fitted model and want to explain it.

## Scope of this vignette

This vignette serves as both:

1. **A package tutorial** — covering every function in `xaiHydro` with
   reproducible code.
2. **The book chapter case study** — the outputs produced here are the
   figures and analyses referenced in the accompanying Springer chapter.

---

# Installation

```r
# CRAN release (recommended)
install.packages("xaiHydro")

# Development version from GitHub
# install.packages("remotes")
remotes::install_github("sadikul-islam/xaiHydro")
```

Load the package:

```{r load-pkg}
library(xaiHydro)
```

---

# Simulated datasets

`xaiHydro` ships with two built-in data simulators so every example in
this vignette is fully reproducible without external data.

## Daily streamflow dataset

```{r sim-streamflow}
df_flow <- sim_streamflow_data(n = 365, seed = 2026)
head(df_flow, 6)
```

```{r sim-str}
str(df_flow)
```

The function generates a synthetic daily hydro-meteorological dataset
mimicking realistic seasonal dynamics:

| Column | Description | Unit |
|--------|-------------|------|
| `precipitation` | Daily rainfall | mm |
| `temperature` | Mean daily air temperature | °C |
| `evapotranspiration` | Reference ET (Penman–Monteith style) | mm/day |
| `soil_moisture` | Volumetric soil moisture | m³/m³ |
| `snow_water_equiv` | Snow water equivalent | mm |
| `ndvi` | NDVI vegetation greenness index | dimensionless |
| `lag1_precip` | Previous-day precipitation | mm |
| `lag3_precip` | 3-day moving mean precipitation | mm |
| `streamflow` | **Response variable** — simulated discharge | m³/s |

The response is generated as a non-linear function of predictors with
added noise, including a precipitation × NDVI interaction term to
capture vegetation-modulated runoff.

## Monthly drought index dataset

```{r sim-drought-show}
df_drought <- sim_drought_data(n = 240, seed = 2026)
head(df_drought, 6)
```

This dataset simulates 20 years of monthly hydro-climatic anomalies with
SPEI (Standardised Precipitation-Evapotranspiration Index) as the
response, driven by precipitation anomalies, temperature anomalies, SPI
at 3- and 12-month scales, PDSI, NDVI anomaly, and ET deficit.

---

# Model training

## Random Forest — streamflow prediction

We train a Random Forest model on the simulated streamflow dataset.
The same workflow applies to any regression model supported by `predict()`.

```{r train-rf, eval=FALSE}
library(randomForest)

# Separate predictors and response
y_flow <- df_flow$streamflow
X_flow <- df_flow[, setdiff(names(df_flow), "streamflow")]

set.seed(42)
rf_model <- randomForest(
  x         = X_flow,
  y         = y_flow,
  ntree     = 300,
  mtry      = 3,
  importance = TRUE
)

print(rf_model)
# Mean of squared residuals: ~X.XX
# % Var explained:            ~XX.X
```

## XGBoost — drought index prediction

```{r train-xgb, eval=FALSE}
library(xgboost)

y_spei   <- df_drought$spei
X_drought <- df_drought[, setdiff(names(df_drought), "spei")]

xgb_mat   <- xgboost::xgb.DMatrix(as.matrix(X_drought), label = y_spei)
xgb_model <- xgboost::xgboost(
  data      = xgb_mat,
  nrounds   = 150,
  max_depth = 4,
  eta       = 0.1,
  objective = "reg:squarederror",
  verbose   = 0
)
```

---

# The hydro explainer

## Creating an explainer

`hydro_explainer()` is the single entry point for all XAI analyses.
It wraps [DALEX::explain()] [@biecek2021explanatory] and attaches
hydrology-specific metadata used for axis labels, titles, and
interpretation messages throughout the package.

```{r explainer, eval=FALSE}
exp_flow <- hydro_explainer(
  model    = rf_model,
  data     = X_flow,
  y        = y_flow,
  variable = "streamflow",
  units    = "m3/s",
  label    = "RandomForest"
)

print(exp_flow)
# == xaiHydro Explainer ==
#   Target variable : streamflow [m3/s]
#   Model label     : RandomForest
#   Observations    : 365
#   Predictors      : 8
#   Model class     : randomForest
```

The explainer object is compatible with all `xaiHydro` functions and
can also be passed directly to any `DALEX` function for additional
analyses.

## Explainer for XGBoost (custom predict wrapper)

For models that require a non-standard prediction call (XGBoost, Keras,
PyTorch via `torch`), supply a `predict_function`:

```{r explainer-xgb, eval=FALSE}
pfun_xgb <- function(model, newdata) {
  predict(model, xgboost::xgb.DMatrix(as.matrix(newdata)))
}

exp_drought <- hydro_explainer(
  model            = xgb_model,
  data             = X_drought,
  y                = y_spei,
  variable         = "drought_index",
  units            = "SPEI",
  label            = "XGBoost",
  predict_function = pfun_xgb
)
```

---

# SHAP Analysis

SHAP (SHapley Additive exPlanations) values decompose each model
prediction into additive contributions from each feature, satisfying
three desirable axioms: efficiency (contributions sum to the prediction
gap from baseline), symmetry (equal features receive equal credit), and
dummy (non-contributing features get zero) [@lundberg2017unified].

## Computing SHAP values

```{r shap-compute, eval=FALSE}
shap_vals <- hydro_shap(
  explainer = exp_flow,
  nsim      = 50,     # Monte Carlo replicates — increase for stability
  seed      = 42
)

# Returns a tidy data frame:
# id | feature | shap_value | feature_value | mean_abs_shap
head(shap_vals, 8)
```

**Tip:** `nsim = 50` is adequate for publication figures. Use
`nsim = 100–200` for final manuscript outputs.

## Global SHAP summary plot

```{r shap-summary, eval=FALSE}
plot_shap_summary(shap_vals, top_n = 8)
```

The beeswarm summary plot shows:

- **Y-axis** — features ranked by mean |SHAP| (most important at top).
- **X-axis** — SHAP value (positive = pushes prediction up; negative =
  pulls it down).
- **Colour** — raw feature value (red = high, blue = low).

Features where red points cluster on the right indicate that high
values of that predictor increase predicted streamflow — physically
consistent with rainfall–runoff theory.

## Single-observation waterfall

```{r shap-waterfall, eval=FALSE}
# Explain a high-flow day (e.g., observation 180 after a storm event)
plot_shap_waterfall(shap_vals, obs_id = 180, top_n = 7)
```

The waterfall plot shows exactly how prediction for day 180 was built
up from the global baseline (mean predicted streamflow) through additive
feature contributions. Red bars push the prediction above baseline;
blue bars pull it below.

---

# LIME Explanation

LIME (Local Interpretable Model-agnostic Explanations) fits a locally
weighted linear surrogate model in the neighbourhood of a specific
observation [@ribeiro2016should]. It provides a simple, interpretable
approximation of the black-box model's local behaviour.

```{r lime, eval=FALSE}
lime_result <- hydro_lime(
  explainer    = exp_flow,
  new_obs      = X_flow[180, , drop = FALSE],
  n_features   = 6,      # top-6 features in the local model
  kernel_width = 0.75
)

# Local R² indicates surrogate model quality (closer to 1 = better)
plot_lime_hydro(lime_result)
```

The local R² is printed in the subtitle. Values above 0.7 indicate the
surrogate captures the model's local structure well.

---

# Partial Dependence Profiles

Partial Dependence Plots (PDP) show the **marginal effect** of a single
predictor on the response, averaging over all other predictors
[@friedman2001greedy]. Accumulated Local Effects (ALE) are preferred
when predictors are correlated, as they avoid averaging over unrealistic
covariate combinations [@apley2020visualizing].

## PDP — marginal effects

```{r pdp, eval=FALSE}
pdp_result <- hydro_pdp(
  explainer   = exp_flow,
  variable    = c("precipitation", "soil_moisture",
                   "lag1_precip",  "temperature"),
  type        = "partial",
  grid_points = 60
)

plot_pdp_hydro(pdp_result, ncol = 2)
```

## ALE — preferred for correlated predictors

```{r ale, eval=FALSE}
ale_result <- hydro_pdp(
  explainer = exp_flow,
  variable  = c("precipitation", "soil_moisture"),
  type      = "accumulated"
)

plot_pdp_hydro(ale_result, ncol = 2)
```

In hydrology, precipitation and soil moisture are typically positively
correlated (wet soils follow wet periods). ALE correctly handles this
by computing effects within observed data slices rather than averaging
over impossible combinations.

---

# Variable Importance

Permutation importance measures how much model error increases when
each feature's values are randomly shuffled, breaking the feature–
response relationship [@breiman2001random]. Features causing large
error increases are the most important.

```{r importance, eval=FALSE}
imp <- hydro_importance(
  explainer     = exp_flow,
  loss_function = "rmse",
  B             = 20       # repetitions — reduces variance of estimate
)

plot_importance_hydro(imp, top_n = 8)
```

---

# Prediction Breakdown

The breakdown profile decomposes a single prediction into ordered
additive contributions from each feature [@biecek2021explanatory].
Unlike SHAP (which averages over all orderings), the sequential Break
Down algorithm shows contributions in a specific order; the SHAP-type
breakdown averages over `B` random orderings for a more stable result.

```{r breakdown, eval=FALSE}
bd <- hydro_breakdown(
  explainer = exp_flow,
  new_obs   = X_flow[180, , drop = FALSE],
  type      = "shap",   # or "break_down"
  B         = 25
)

plot_breakdown_hydro(bd, max_features = 8)
```

---

# Residual Diagnostics

Before trusting XAI explanations, model residuals should be examined.
Systematic patterns in residuals (heteroscedasticity, temporal
autocorrelation) can bias SHAP and PDP estimates.

```{r residuals, eval=FALSE}
hydro_residuals(exp_flow)
```

The four-panel figure checks:

1. **Residual histogram** — departure from normality (skewness, heavy
   tails).
2. **Residuals vs predicted** — heteroscedasticity; systematic bias at
   high flows.
3. **Observed vs predicted** — overall fit; 1:1 line is perfect
   prediction.
4. **Residuals over index** — temporal autocorrelation in daily data
   (important for hydrological models with lag effects).

---

# Full automated XAI report

`hydro_xai_report()` runs all key analyses in one call and assembles
them into a publication-ready multi-panel figure. This is the figure
used in the book chapter case study.

```{r report, eval=FALSE}
hydro_xai_report(
  explainer = exp_flow,
  nsim      = 50,
  top_n     = 6,
  new_obs   = X_flow[180, , drop = FALSE],
  seed      = 42,
  save_path = "xaiHydro_streamflow_report.png",
  width     = 16,
  height    = 11,
  dpi       = 300
)
```

---

# Complete drought index case study

This section demonstrates the full `xaiHydro` workflow on an XGBoost
drought model, reproducing the second case study in the book chapter.

```{r drought-full, eval=FALSE}
library(xgboost)

# Data
df_d  <- sim_drought_data(n = 240, seed = 2026)
y_d   <- df_d$spei
X_d   <- df_d[, setdiff(names(df_d), "spei")]

# Model
xgb_m <- xgboost::xgboost(
  data      = xgboost::xgb.DMatrix(as.matrix(X_d), label = y_d),
  nrounds   = 150, max_depth = 4, eta = 0.1,
  objective = "reg:squarederror", verbose = 0
)

# Explainer
pfun  <- function(m, nd) predict(m, xgboost::xgb.DMatrix(as.matrix(nd)))
exp_d <- hydro_explainer(xgb_m, X_d, y_d,
                          variable         = "drought_index",
                          units            = "SPEI",
                          label            = "XGBoost",
                          predict_function = pfun)

# XAI suite
shap_d <- hydro_shap(exp_d, nsim = 50)
imp_d  <- hydro_importance(exp_d, B = 20)
pdp_d  <- hydro_pdp(exp_d, variable = c("spi_3", "spi_12", "et_deficit"))

plot_shap_summary(shap_d, top_n = 6,
                  title = "SHAP Summary — SPEI drought model (XGBoost)")
plot_importance_hydro(imp_d)
plot_pdp_hydro(pdp_d, ncol = 3)
hydro_residuals(exp_d)
```

---

# Interpreting XAI results in a hydro-climate context

## Physical interpretation of SHAP

When `precipitation` and `lag1_precip` appear in the top-ranked SHAP
features with positive effects (red points on the right), this confirms
the model has correctly learnt that higher antecedent and current
rainfall drives higher streamflow — consistent with rainfall–runoff
theory. Conversely, high `evapotranspiration` having negative SHAP
(blue points on left) indicates that high ET demand reduces net water
available for runoff.

## ALE vs PDP selection guide

Use **ALE** when:
- Predictors are correlated (precipitation–soil moisture, temperature–ET).
- The training data does not uniformly cover the predictor space.
- You suspect extrapolation artefacts in PDP.

Use **PDP** when:
- Predictors are approximately independent.
- You want a simpler figure for a non-technical audience.

## LIME local R² threshold

An R² < 0.5 for the LIME surrogate means the local linear approximation
is unreliable for that observation. Increase `kernel_width` (to use a
broader neighbourhood) or reduce `n_features` (for a sparser model).

---

# Session information

```{r session}
sessionInfo()
```

---

# References
