---
title: "Weather-Response Fusion"
subtitle: "Climate Forcing of Aquifer Dynamics"
code-fold: true
---
::: {.callout-tip icon=false}
## For Newcomers
**You will learn:**
- How rain becomes groundwater (and why it takes time)
- What "lag" means in aquifer response to weather
- How to measure the time delay between precipitation and water level change
- Why temperature matters (evapotranspiration competes for water)
Rain falls, but the water table doesn't respond instantly. This chapter measures how long it takes and how much the signal is dampened—revealing fundamental properties of the aquifer system.
:::
**Data Sources Fused**: Weather Stations + Groundwater Wells
## What You Will Learn in This Chapter
By the end of this chapter, you will be able to:
- Explain what it means for an aquifer to “lag” behind weather and why that lag reflects storage, connectivity, and recharge efficiency.
- Interpret precipitation–groundwater overlays, cross-correlation functions, and cumulative-window plots to quantify lag and attenuation.
- Describe how evapotranspiration and net water (P − PET) modify the relationship between climate forcing and water-level response.
- Connect weather–response metrics from this chapter to recharge estimation, drought early warning, and multi-source fusion in later chapters.
## Overview
Weather drives the hydrologic cycle - precipitation recharges aquifers, evapotranspiration depletes them, and temperature modulates both processes. But the connection is not instantaneous. Water infiltrates through soil, percolates through the vadose zone, and eventually reaches the water table. This chapter quantifies the **lag, attenuation, and transformation** of weather signals as they propagate into the aquifer.
::: {.callout-note icon=false}
## 💻 For Computer Scientists
This is a **signal processing problem**:
- **Input signal**: Precipitation (high frequency, spiky)
- **System**: Soil, vadose zone, aquifer (low-pass filter)
- **Output signal**: Water level (smoothed, lagged)
**Key techniques:**
- Cross-correlation for lag detection
- Transfer function modeling (input→output)
- Spectral analysis to identify dominant frequencies
:::
::: {.callout-tip icon=false}
## 🌍 For Hydrologists
Weather-aquifer response depends on aquifer properties:
| Aquifer Type | Recharge Lag | Signal Attenuation | Physical Mechanism |
|-------------|--------------|-------------------|-------------------|
| Unconfined, shallow | Days to weeks | Low (~70% preserved) | Direct infiltration |
| Unconfined, deep | Weeks to months | Moderate (~50%) | Vadose zone delay |
| Confined | Months to years | High (~20%) | Pressure propagation |
**Key insight**: Lag time reveals aquifer connectivity and storage properties.
:::
## Analysis Approach
```{python}
#| code-fold: true
import os
import sys
from pathlib import Path
# Add project root to path for imports (using find_repo_root pattern)
def find_repo_root(start: Path) -> Path:
for candidate in [start, *start.parents]:
if (candidate / "src").exists():
return candidate
return start
quarto_project = Path(os.environ.get("QUARTO_PROJECT_DIR", str(Path.cwd())))
project_root = find_repo_root(quarto_project)
if str(project_root) not in sys.path:
sys.path.append(str(project_root))
from src.data_loaders.weather_loader import WeatherLoader
from src.data_loaders.groundwater_loader import GroundwaterLoader
from src.utils import get_data_path
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
try:
from scipy import signal
from scipy.stats import pearsonr
SCIPY_AVAILABLE = True
except ImportError:
SCIPY_AVAILABLE = False
signal = None
pearsonr = None
print("Note: scipy not available. Some signal processing analyses will be simplified.")
import warnings
warnings.filterwarnings('ignore')
# Load weather and groundwater data using config-driven paths
weather_db_path = get_data_path("warm_db")
aquifer_db_path = get_data_path("aquifer_db")
weather_loader = None
gw_loader = None
stations = pd.DataFrame()
weather_df = pd.DataFrame()
weather_daily = pd.DataFrame(columns=['Date', 'Precip_mm', 'Temp_C', 'RH'])
wells_summary = pd.DataFrame()
try:
weather_loader = WeatherLoader(weather_db_path)
except Exception as e:
print(f"WeatherLoader init error: {e}")
try:
gw_loader = GroundwaterLoader(aquifer_db_path)
except Exception as e:
print(f"GroundwaterLoader init error: {e}")
# Get available stations
primary_station = 'cmi' # fallback (Champaign station)
try:
if weather_loader is None:
raise RuntimeError("Weather loader unavailable")
stations = weather_loader.get_station_summary()
print(f"Available weather stations: {len(stations)}")
if len(stations) > 0 and 'nStationCode' in stations.columns:
primary_station = stations.iloc[0]['nStationCode']
print(f"Using primary station: {primary_station}")
except Exception as e:
print(f"Station lookup error: {e}")
# Load hourly weather data (from WarmICNData table)
try:
if weather_loader is None:
raise RuntimeError("Weather loader unavailable")
weather_df = weather_loader.load_hourly_data(
station_code=primary_station,
start_date='2010-01-01',
limit=50000
)
print(f"Weather records loaded: {len(weather_df)}")
except Exception as e:
print(f"Weather load error: {e}")
weather_df = pd.DataFrame()
# Convert to daily aggregations
if 'datetime' in weather_df.columns and len(weather_df) > 0:
weather_df['date'] = weather_df['datetime'].dt.date
weather_daily = weather_df.groupby('date').agg({
'nPrecip': 'sum', # Sum hourly precip for daily total
'nAirTemp': 'mean', # Mean temperature
'nRelHumid': 'mean' # Mean humidity
}).reset_index()
weather_daily.columns = ['Date', 'Precip_mm', 'Temp_C', 'RH']
weather_daily['Date'] = pd.to_datetime(weather_daily['Date'])
print(f"Daily weather records: {len(weather_daily)}")
else:
print("No weather data available")
weather_daily = pd.DataFrame(columns=['Date', 'Precip_mm', 'Temp_C', 'RH'])
# Groundwater wells summary
try:
if gw_loader is None:
raise RuntimeError("Groundwater loader unavailable")
wells_summary = gw_loader.get_well_summary_statistics(min_measurements=500)
except Exception as e:
print(f"Well summary error: {e}")
wells_summary = pd.DataFrame()
print(f"Available wells (>500 measurements): {len(wells_summary)}")
```
## Well Selection Criteria
```{python}
#| code-fold: true
# CRITICAL: Select wells that have data OVERLAPPING with weather data period
# Weather data is from 2011-2017, so we need wells with data in that range
example_well_id = None
weather_start = None
weather_end = None
# Get weather date range
if len(weather_daily) > 0:
weather_start = weather_daily['Date'].min()
weather_end = weather_daily['Date'].max()
print(f"Weather data date range: {weather_start.date()} to {weather_end.date()}")
# Find wells with data overlapping weather period
if gw_loader is not None and weather_start is not None:
try:
# Query wells with measurements in the weather data period
query = f"""
SELECT P_Number,
MIN(TIMESTAMP) as first_date,
MAX(TIMESTAMP) as last_date,
COUNT(*) as n_records
FROM OB_WELL_MEASUREMENTS_CHAMPAIGN_COUNTY
WHERE TIMESTAMP IS NOT NULL
AND TIMESTAMP >= '{weather_start.strftime('%m/%d/%Y')}'
AND TIMESTAMP <= '{weather_end.strftime('%m/%d/%Y')}'
GROUP BY P_Number
HAVING COUNT(*) >= 100
ORDER BY n_records DESC
LIMIT 10
"""
overlapping_wells = pd.read_sql_query(query, gw_loader.conn)
if len(overlapping_wells) > 0:
print(f"Found {len(overlapping_wells)} wells with data in weather period")
top_well = overlapping_wells.iloc[0]
print(f"Selected well: {top_well['P_Number']} with {top_well['n_records']} measurements in overlap period")
example_well_id = str(int(top_well['P_Number']))
else:
# Fallback: use well 444863 which has data from 2009-2022
print("No wells found in exact weather period, using well 444863 (known overlap)")
example_well_id = '444863'
except Exception as e:
print(f"Well overlap query error: {e}")
example_well_id = '444863' # Fallback to known good well
elif len(wells_summary) > 0:
# Fallback to most data if weather date range unknown
top_wells = wells_summary.head(10)
print(f"Weather dates unavailable, using wells by data count")
try:
example_well_id = str(int(top_wells.iloc[0]['P_Number']))
except Exception as e:
print(f"Well id parse error: {e}")
example_well_id = None
else:
print("No wells available for analysis")
print(f"Example well for analysis: {example_well_id}")
```
## Time Series Preparation
```{python}
#| code-fold: true
# Load time series for representative well
merged = pd.DataFrame()
if example_well_id is not None and gw_loader is not None:
try:
well_ts = gw_loader.load_well_time_series(
well_id=example_well_id,
start_date='2010-01-01'
)
# Reset index to access TIMESTAMP as column
well_ts_reset = well_ts.reset_index()
# Aggregate to daily values FIRST (multiple measurements per day exist)
well_ts_reset['Date'] = pd.to_datetime(well_ts_reset['TIMESTAMP']).dt.normalize()
well_daily = well_ts_reset.groupby('Date').agg({
'Water_Surface_Elevation': 'mean' # Daily average
}).reset_index()
print(f"Well daily records: {len(well_daily)}")
# Merge weather and well data
merged = pd.merge(
weather_daily,
well_daily,
on='Date',
how='inner'
).sort_values('Date')
print(f"Merged data points: {len(merged)}")
if len(merged) > 0:
print(f"Date range: {merged['Date'].min().date()} to {merged['Date'].max().date()}")
# Fill missing days with interpolation (using unique dates now)
merged = merged.set_index('Date').resample('D').asfreq()
merged['Water_Surface_Elevation'] = merged['Water_Surface_Elevation'].interpolate(method='linear', limit=7)
merged['Precip_mm'] = merged['Precip_mm'].fillna(0) # No precip = 0
merged['Temp_C'] = merged['Temp_C'].interpolate(method='linear', limit=7)
merged = merged.reset_index()
# Remove remaining NaNs
merged = merged.dropna()
print(f"After interpolation: {len(merged)} records")
else:
print("No overlapping data between weather and well measurements")
except Exception as e:
print(f"Well time series load/merge error: {e}")
import traceback
traceback.print_exc()
merged = pd.DataFrame()
else:
print("No well data available for merging")
```
## Cross-Correlation Analysis
### What Is Cross-Correlation?
**Cross-correlation** is a statistical technique that measures how similar two time series are at different time shifts (lags). Developed in radar signal processing during World War II (1940s), it was later adapted to hydrology in the 1960s-70s to understand how aquifers respond to climate forcing.
### Why Does It Matter for Aquifer Analysis?
For water resource management, cross-correlation answers the critical question: **"How long after it rains does the water table respond?"** This lag time reveals fundamental aquifer properties:
- **Fast response (days)**: Shallow, unconfined aquifer with direct rainfall infiltration
- **Slow response (months)**: Deep vadose zone or confined aquifer with pressure propagation
- **No response**: Aquifer disconnected from surface recharge (deep confined system)
Understanding lag times enables:
- **Drought forecasting**: Predicting water level declines weeks in advance
- **Recharge mapping**: Identifying which precipitation events actually recharge the aquifer
- **Aquifer characterization**: Distinguishing confined from unconfined conditions
### How Does Cross-Correlation Work?
The method works in four steps:
1. **Normalize both time series** (precipitation and water levels) to remove scale differences
2. **Shift one series forward in time** (0 to 180 days lag)
3. **Calculate correlation** at each time shift
4. **Identify peak correlation** which reveals the optimal lag
The lag at peak correlation tells us the **travel time** for precipitation to reach the water table.
### What Will You See Below?
The visualization shows two components:
**Top Panel**: Water level (blue line) and cumulative precipitation (teal area) plotted together with the optimal lag applied—when they align well, recharge is active
**Bottom Panel**: Cross-correlation function showing correlation coefficient vs. lag days, with a red star marking the peak (optimal lag)
### How to Interpret Cross-Correlation Results
| Lag Time | Aquifer Type | Physical Interpretation | Management Implication |
|----------|--------------|------------------------|------------------------|
| **1-7 days** | Shallow unconfined | Direct infiltration through sandy soils | Rapid drought impact, fast recovery |
| **7-30 days** | Deep unconfined | Thick vadose zone delays percolation | Moderate buffering capacity |
| **30-90 days** | Semi-confined | Pressure propagation dominates over flow | High drought resilience |
| **>90 days** | Confined aquifer | Minimal direct recharge, regional flow | Very stable, slow to respond to climate |
**Correlation strength also matters:**
- **r > 0.5**: Strong precipitation-recharge connection
- **r = 0.3-0.5**: Moderate connection (ET or pumping reduces signal)
- **r < 0.3**: Weak connection (confined system or confounding factors)
```{python}
#| code-fold: true
def compute_weather_aquifer_lag(precip, water_level, max_lag_days=180):
"""
Compute cross-correlation between precipitation and water level.
Returns optimal lag (days) and correlation coefficient.
"""
# Normalize series
precip_norm = (precip - precip.mean()) / precip.std()
wl_norm = (water_level - water_level.mean()) / water_level.std()
# Cross-correlation
correlation = signal.correlate(wl_norm, precip_norm, mode='full')
lags = signal.correlation_lags(len(wl_norm), len(precip_norm), mode='full')
# Restrict to positive lags (precip leads water level)
valid_idx = (lags >= 0) & (lags <= max_lag_days)
lags = lags[valid_idx]
correlation = correlation[valid_idx]
# Find optimal lag
max_idx = np.argmax(correlation)
optimal_lag = lags[max_idx]
max_corr = correlation[max_idx]
return optimal_lag, max_corr, lags, correlation
# Compute lag only if we have data
if len(merged) > 100:
optimal_lag, max_corr, lags, correlations = compute_weather_aquifer_lag(
merged['Precip_mm'].values,
merged['Water_Surface_Elevation'].values
)
print(f"Optimal lag: {optimal_lag} days")
print(f"Maximum correlation: {max_corr:.3f}")
else:
print("⚠️ INSUFFICIENT DATA for cross-correlation analysis")
print("")
print("📋 WHAT'S MISSING:")
print(f" • Merged weather + groundwater records: {len(merged)} (need >100)")
print("")
print("🔧 TO FIX THIS, you need:")
print(" 1. Weather database: data/warm.db")
print(" - Must contain WarmICNData table with hourly readings")
print(" - Required columns: nPrecip, nAirTemp, nRelHumid, datetime")
print("")
print(" 2. Groundwater database: data/aquifer.db")
print(" - Must contain OB_WELL_MEASUREMENTS_CHAMPAIGN_COUNTY table")
print(" - Required columns: TIMESTAMP, Water_Surface_Elevation, P_Number")
print("")
print(" 3. Overlapping date ranges between weather and well measurements")
print(" - Both datasets must have data in the same time period (e.g., 2010-2020)")
print("")
print("📥 DATA SOURCES:")
print(" • Weather: Illinois Climate Network (ICN) via warm.db")
print(" • Wells: Illinois State Water Survey monitoring network")
optimal_lag, max_corr = 0, 0
lags, correlations = np.array([0]), np.array([0])
```
## Visualization 1: Precipitation-Groundwater Response Overlay
::: {.callout-note icon=false}
## 📊 How to Read This 2-Panel Analysis
**Top Panel - Time Series Overlay:**
| Element | What It Shows | What to Look For |
|---------|---------------|------------------|
| **Blue bars** | Daily precipitation events | Timing and magnitude of rainfall |
| **Red line** | Water level (shifted by optimal lag) | Does it rise after major storms? |
| **Alignment** | How well precip predicts water level | Good alignment = strong recharge connection |
**Bottom Panel - Cross-Correlation Function:**
| Feature | Interpretation | Management Meaning |
|---------|---------------|-------------------|
| **Peak location (x-axis)** | Optimal lag in days | Aquifer response time |
| **Peak height (y-axis)** | Correlation strength (r) | How well precip predicts levels |
| **Sharp peak** | Consistent, predictable lag | Reliable forecasting possible |
| **Broad peak** | Variable response time | Forecasting uncertainty higher |
**Physical Interpretation by Lag Time:**
- **1-7 days**: Shallow unconfined aquifer, direct infiltration
- **7-30 days**: Deep vadose zone or semi-confined
- **30-90 days**: Confined aquifer, pressure propagation
- **>90 days**: Minimal recharge connection (deep confined)
**Why this matters:** Knowing the lag enables drought forecasting—if precipitation deficit persists for >lag days, water level decline is imminent.
:::
```{python}
#| code-fold: true
#| label: fig-precip-groundwater-overlay
#| fig-cap: "Cross-correlation analysis showing temporal lag between precipitation events and aquifer response"
if len(merged) > 100:
fig = make_subplots(
rows=2, cols=1,
subplot_titles=(
f'Precipitation vs Water Level - Well {example_well_id}',
'Cross-Correlation Function'
),
specs=[[{"secondary_y": True}], [{"secondary_y": False}]],
row_heights=[0.5, 0.5],
vertical_spacing=0.12
)
# Time series comparison (with optimal lag applied)
merged_copy = merged.copy()
merged_copy['WL_shifted'] = merged_copy['Water_Surface_Elevation'].shift(-int(optimal_lag))
# Subsample for visibility
plot_data = merged_copy.iloc[::7] # Weekly points
fig.add_trace(
go.Bar(
x=plot_data['Date'],
y=plot_data['Precip_mm'],
name='Precipitation',
marker_color='rgba(30, 144, 255, 0.6)',
yaxis='y'
),
row=1, col=1,
secondary_y=False
)
fig.add_trace(
go.Scatter(
x=plot_data['Date'],
y=plot_data['WL_shifted'],
name=f'Water Level (shifted {int(optimal_lag)}d)',
line=dict(color='red', width=2),
mode='lines'
),
row=1, col=1,
secondary_y=True
)
# Cross-correlation function
fig.add_trace(
go.Scatter(
x=lags,
y=correlations,
mode='lines',
line=dict(color='green', width=2),
name='Cross-Correlation',
showlegend=True
),
row=2, col=1
)
# Mark optimal lag
fig.add_trace(
go.Scatter(
x=[optimal_lag],
y=[max_corr],
mode='markers',
marker=dict(size=15, color='red', symbol='star'),
name=f'Peak: {int(optimal_lag)}d lag',
showlegend=True
),
row=2, col=1
)
fig.update_xaxes(title_text='Date', row=1, col=1)
fig.update_xaxes(title_text='Lag (days)', row=2, col=1)
fig.update_yaxes(title_text='Precipitation (mm/day)', row=1, col=1, secondary_y=False)
fig.update_yaxes(title_text='Water Level Elevation (m)', row=1, col=1, secondary_y=True)
fig.update_yaxes(title_text='Correlation Coefficient', row=2, col=1)
fig.update_layout(
title_text=f'Weather-Aquifer Response Analysis (r={max_corr:.3f} at {int(optimal_lag)}-day lag)',
height=800,
hovermode='x unified',
showlegend=True
)
fig.show()
else:
print("⚠️ VISUALIZATION SKIPPED - Insufficient overlapping data")
print("")
print("📊 This visualization requires:")
print(" • >100 days of merged weather + groundwater data")
print(" • Cross-correlation results from previous analysis")
print("")
print("🔍 CURRENT STATUS:")
print(f" • Merged records available: {len(merged)}")
print(f" • Well selected: {example_well_id if example_well_id else 'None'}")
print("")
print("💡 WHAT YOU'D SEE with sufficient data:")
print(" • Top panel: Precipitation bars overlaid with lagged water levels")
print(" • Bottom panel: Cross-correlation function showing optimal lag")
print(" • Typical lags range from 7-90 days depending on aquifer type")
```
## Cumulative Precipitation Effect
### What Is Cumulative Precipitation Analysis?
**Cumulative precipitation analysis** examines how rainfall totals over different time windows (7, 14, 30, 60, 90 days) correlate with groundwater levels. This concept emerged from agricultural water balance studies in the 1950s, pioneered by C.W. Thornthwaite and others studying crop water requirements.
### Why Does It Matter for Aquifer Analysis?
A single rainstorm rarely changes aquifer levels significantly—water must accumulate over time before substantial recharge occurs. This analysis reveals:
- **Aquifer memory**: How long past precipitation influences current water levels
- **Integration timescale**: The window over which the aquifer "sums up" rainfall events
- **Recharge efficiency**: Whether short intense storms or prolonged wet periods drive recharge
Understanding the optimal window helps:
- **Forecast water levels**: Use weather forecasts to predict aquifer response
- **Design monitoring**: Schedule measurements to capture response timescales
- **Assess drought risk**: Estimate how long dry periods impact the system
### How Does Cumulative Precipitation Work?
For each time window (e.g., 30 days), the analysis follows four steps:
1. **Sum all precipitation** over the rolling window for each day
2. **Correlate cumulative totals** with water levels measured on that day
3. **Compare correlation strengths** across different window lengths (7, 14, 30, 60, 90 days)
4. **Identify optimal window** that gives strongest prediction of water levels
The window with highest correlation reveals the aquifer's **characteristic response time**.
### What Will You See Below?
Four visualization panels show:
**Top-left**: Correlation vs. window length—peak shows optimal accumulation period
**Top-right**: Scatter plot of cumulative precipitation (optimal window) vs. water level with trendline
**Bottom-left**: Monthly aggregated precipitation and water levels
**Bottom-right**: Seasonal patterns averaging across all years
### How to Interpret Cumulative Precipitation Results
| Best Window | Physical Meaning | Aquifer Type | Management Implication |
|-------------|------------------|--------------|------------------------|
| **7-14 days** | Fast-responding system | Shallow unconfined | Droughts impact quickly, recovery is rapid |
| **30-60 days** | Moderate buffering | Semi-confined | System has monthly memory, gradual response |
| **60-90 days** | High buffering capacity | Confined or deep | Resilient to short droughts, slow recovery |
**Correlation strength interpretation:**
- **r > 0.6**: Cumulative precipitation is dominant control on water levels
- **r = 0.4-0.6**: Precipitation important but other factors (ET, pumping) also matter
- **r < 0.4**: Precipitation has weak influence—check for confinement or heavy pumping
**Key Insight**: The optimal window length approximates **aquifer residence time** for recharge—how long water takes to fully infiltrate from surface to water table.
```{python}
#| code-fold: true
# Calculate cumulative precipitation over various windows
if len(merged) > 100:
windows = [7, 14, 30, 60, 90]
for window in windows:
merged[f'Precip_cum_{window}d'] = merged['Precip_mm'].rolling(window=window, min_periods=1).sum()
# Correlation with water level
correlations_by_window = {}
for window in windows:
valid_data = merged[[f'Precip_cum_{window}d', 'Water_Surface_Elevation']].dropna()
if len(valid_data) > 10:
corr, pval = pearsonr(
valid_data[f'Precip_cum_{window}d'],
valid_data['Water_Surface_Elevation']
)
correlations_by_window[window] = corr
else:
correlations_by_window[window] = 0
print("Correlation by cumulative precipitation window:")
for window, corr in correlations_by_window.items():
print(f" {window} days: {corr:.3f}")
# Find optimal window
optimal_window = max(correlations_by_window, key=correlations_by_window.get)
print(f"\nOptimal cumulative window: {optimal_window} days (r={correlations_by_window[optimal_window]:.3f})")
else:
print("⚠️ CUMULATIVE PRECIPITATION ANALYSIS SKIPPED")
print("")
print("📋 WHAT THIS ANALYSIS DOES:")
print(" Calculates how multi-day precipitation totals (7, 14, 30, 60, 90 days)")
print(" correlate with groundwater levels to find optimal 'memory window'")
print("")
print("🔧 REQUIREMENTS NOT MET:")
print(f" • Merged records: {len(merged)} (need >100)")
print(" • Need continuous daily weather + groundwater measurements")
print("")
print("💡 EXPECTED FINDINGS (with sufficient data):")
print(" • Shallow unconfined aquifers: best correlation at 7-14 day window")
print(" • Deep unconfined aquifers: best correlation at 30-60 day window")
print(" • Confined aquifers: best correlation at 60-90 day window")
correlations_by_window = {7: 0, 14: 0, 30: 0, 60: 0, 90: 0}
optimal_window = 30
```
## Visualization 2: Precipitation Response Patterns
::: {.callout-note icon=false}
## 📊 Reading This 4-Panel Dashboard
**Understanding the Layout:**
| Panel Position | What It Shows | Key Question |
|---------------|---------------|--------------|
| **Top-left** | Correlation vs. window length (7-90 days) | What accumulation period best predicts water levels? |
| **Top-right** | Scatter: cumulative precip vs water level | Is the relationship linear? Strong? |
| **Bottom-left** | Monthly aggregated time series | How do precipitation and water levels track over time? |
| **Bottom-right** | Seasonal patterns averaged across years | Which months dominate recharge? |
**Top-Left Panel Interpretation:**
- **Peak at 7-14 days**: Fast-responding system (shallow, unconfined)
- **Peak at 30-60 days**: Moderate buffering (deep vadose zone)
- **Peak at 60-90 days**: High buffering (semi-confined)
- **Flat line (no peak)**: No clear response window (confined or pumping-dominated)
**Top-Right Panel - Scatter Plot:**
- **Points cluster on trendline**: Strong predictive relationship
- **Points widely scattered**: Other factors (ET, pumping) dominate
- **Positive slope**: More cumulative precip → higher water levels (as expected)
- **Negative slope**: Anomalous—investigate data quality
**Bottom Panels - Temporal Patterns:**
- **Monthly (left)**: Identify wet/dry periods and aquifer response lag
- **Seasonal (right)**: Winter-spring often dominates recharge (low ET)
**Why this matters:** The optimal window approximates aquifer residence time—how long water takes to infiltrate from surface to water table.
:::
```{python}
#| code-fold: true
#| label: fig-precip-response
#| fig-cap: "Multi-scale precipitation effects showing optimal accumulation windows and seasonal patterns"
if len(merged) > 100:
from plotly.subplots import make_subplots
fig = make_subplots(
rows=2, cols=2,
subplot_titles=(
'Correlation vs Accumulation Window',
f'Optimal Window: {optimal_window}-day Cumulative Precip vs Water Level',
'Monthly Aggregation',
'Seasonal Response Patterns'
),
specs=[
[{"secondary_y": False}, {"secondary_y": False}],
[{"secondary_y": True}, {"secondary_y": True}]
],
vertical_spacing=0.12,
horizontal_spacing=0.12
)
# Plot 1: Correlation by window
fig.add_trace(
go.Scatter(
x=list(correlations_by_window.keys()),
y=list(correlations_by_window.values()),
mode='lines+markers',
marker=dict(size=10, color='steelblue'),
line=dict(width=2),
name='Correlation',
showlegend=False
),
row=1, col=1
)
# Plot 2: Scatter plot with optimal window
valid_scatter = merged[[f'Precip_cum_{optimal_window}d', 'Water_Surface_Elevation']].dropna()
fig.add_trace(
go.Scatter(
x=valid_scatter[f'Precip_cum_{optimal_window}d'],
y=valid_scatter['Water_Surface_Elevation'],
mode='markers',
marker=dict(size=4, color='coral', opacity=0.4),
name=f'{optimal_window}-day cumulative',
showlegend=False,
hovertemplate='Cumulative Precip: %{x:.1f} mm<br>Water Level: %{y:.2f} m<extra></extra>'
),
row=1, col=2
)
# Add trendline if sufficient data
if len(valid_scatter) > 10:
z = np.polyfit(valid_scatter[f'Precip_cum_{optimal_window}d'],
valid_scatter['Water_Surface_Elevation'], 1)
p = np.poly1d(z)
x_trend = np.linspace(valid_scatter[f'Precip_cum_{optimal_window}d'].min(),
valid_scatter[f'Precip_cum_{optimal_window}d'].max(), 100)
fig.add_trace(
go.Scatter(
x=x_trend,
y=p(x_trend),
mode='lines',
line=dict(color='red', dash='dash', width=2),
name='Trend',
showlegend=False
),
row=1, col=2
)
# Plot 3: Monthly aggregation
merged_copy = merged.copy()
merged_copy['YearMonth'] = pd.to_datetime(merged_copy['Date']).dt.to_period('M')
monthly = merged_copy.groupby('YearMonth').agg({
'Precip_mm': 'sum',
'Water_Surface_Elevation': 'mean'
}).reset_index()
monthly['YearMonth'] = monthly['YearMonth'].dt.to_timestamp()
fig.add_trace(
go.Bar(
x=monthly['YearMonth'],
y=monthly['Precip_mm'],
name='Monthly Precip',
marker_color='rgba(30, 144, 255, 0.6)',
showlegend=False
),
row=2, col=1,
secondary_y=False
)
fig.add_trace(
go.Scatter(
x=monthly['YearMonth'],
y=monthly['Water_Surface_Elevation'],
name='Monthly Water Level',
line=dict(color='red', width=2),
showlegend=False
),
row=2, col=1,
secondary_y=True
)
# Plot 4: Seasonal patterns
merged_copy['Month'] = pd.to_datetime(merged_copy['Date']).dt.month
seasonal = merged_copy.groupby('Month').agg({
'Precip_mm': 'mean',
'Water_Surface_Elevation': 'mean'
}).reset_index()
month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
fig.add_trace(
go.Scatter(
x=month_names,
y=seasonal['Precip_mm'],
name='Avg Precip',
line=dict(color='blue', width=2),
mode='lines+markers',
showlegend=False
),
row=2, col=2,
secondary_y=False
)
fig.add_trace(
go.Scatter(
x=month_names,
y=seasonal['Water_Surface_Elevation'],
name='Avg Water Level',
line=dict(color='red', width=2),
mode='lines+markers',
showlegend=False
),
row=2, col=2,
secondary_y=True
)
# Update axes
fig.update_xaxes(title_text='Accumulation Window (days)', row=1, col=1)
fig.update_xaxes(title_text=f'Cumulative Precip ({optimal_window}d, mm)', row=1, col=2)
fig.update_xaxes(title_text='Date', row=2, col=1)
fig.update_xaxes(title_text='Month', row=2, col=2)
fig.update_yaxes(title_text='Correlation', row=1, col=1)
fig.update_yaxes(title_text='Water Level (m)', row=1, col=2)
fig.update_yaxes(title_text='Precip (mm)', row=2, col=1, secondary_y=False)
fig.update_yaxes(title_text='Water Level (m)', row=2, col=1, secondary_y=True)
fig.update_yaxes(title_text='Precip (mm)', row=2, col=2, secondary_y=False)
fig.update_yaxes(title_text='Water Level (m)', row=2, col=2, secondary_y=True)
fig.update_layout(
title_text=f'Cumulative Precipitation Effects on Aquifer (Optimal: {optimal_window}d, r={correlations_by_window[optimal_window]:.3f})',
height=800,
showlegend=False
)
fig.show()
else:
print("⚠️ PRECIPITATION RESPONSE VISUALIZATION SKIPPED")
print("")
print("📊 THIS 4-PANEL DASHBOARD WOULD SHOW:")
print(" 1. Top-left: Correlation strength vs accumulation window (7-90 days)")
print(" 2. Top-right: Scatter plot of cumulative precip vs water level")
print(" 3. Bottom-left: Monthly precipitation and water level trends")
print(" 4. Bottom-right: Seasonal (monthly average) response patterns")
print("")
print("🔧 DATA REQUIREMENTS:")
print(f" • Current merged records: {len(merged)} (need >100)")
print(" • Weather data with: precipitation, temperature, humidity")
print(" • Groundwater data with: water surface elevation, timestamps")
print("")
print("📥 TO ENABLE THIS VISUALIZATION:")
print(" 1. Ensure data/warm.db contains weather station data")
print(" 2. Ensure data/aquifer.db contains well measurements")
print(" 3. Verify date ranges overlap (e.g., both have 2010-2020 data)")
```
## Evapotranspiration Effects
### What Is Evapotranspiration (ET)?
**Evapotranspiration (ET)** is the combined loss of water through soil evaporation and plant transpiration. The concept was formalized by C.W. Thornthwaite in 1948, who developed temperature-based estimation methods that revolutionized water balance studies and remain widely used today in agriculture and hydrology.
### Why Does ET Matter for Aquifer Analysis?
Not all precipitation becomes groundwater recharge—a substantial fraction never reaches the water table. Understanding ET is critical because:
- **Summer water loss**: During warm months, ET can consume 70-90% of rainfall before it infiltrates
- **Seasonal recharge patterns**: Winter dominates annual recharge because ET is minimal
- **Drought assessment**: Dry periods with high ET are more severe than simple precipitation deficits
- **Climate change impacts**: Rising temperatures increase ET, reducing recharge even if precipitation stays constant
Ignoring ET leads to **systematic overestimation** of recharge rates and misunderstanding of when aquifers actually receive water.
### How Does ET Work?
The simplified Thornthwaite method estimates **potential evapotranspiration (PET)**—the maximum possible water loss—from temperature:
**Temperature-based PET estimation:**
- **Cool months (T < 0°C)**: PET ≈ 0 mm/day (dormant vegetation, frozen soil)
- **Moderate months (0-20°C)**: PET ≈ 1-4 mm/day (increasing with temperature)
- **Warm months (T > 20°C)**: PET ≈ 4-6 mm/day (active transpiration by crops/vegetation)
**Net water calculation:**
$$\text{Net Water} = \text{Precipitation} - \text{PET}$$
- **Positive net water**: Surplus available for infiltration and recharge
- **Negative net water**: Deficit where ET demand exceeds rainfall (aquifer discharge mode)
### What Will You See Below?
The three-panel water balance visualization shows:
**Top Panel**: Monthly precipitation (blue bars) vs. PET (orange bars)—when PET exceeds precipitation, the system is in deficit
**Middle Panel**: Net water (P - PET)—positive values (green) indicate recharge periods; negative values (red) indicate deficit periods
**Bottom Panel**: Scatter plot of cumulative net water vs. water levels, colored by temperature—shows how accounting for ET strengthens the precipitation-groundwater relationship
### How to Interpret ET Results
| Season | P vs PET | Net Water | Aquifer Response | Management Action |
|--------|----------|-----------|------------------|-------------------|
| **Winter** | P > PET | Large surplus | Active recharge period | Favorable for pumping |
| **Spring** | P ≈ PET | Near zero | Transition period | Monitor closely |
| **Summer** | P < PET | Deficit (negative) | Aquifer discharge, declining levels | Reduce pumping if possible |
| **Fall** | P > PET | Surplus resumes | Recovery begins | Prepare for winter recharge |
**Correlation improvement from adding ET:**
- **Typical improvement**: +0.1 to +0.2 correlation points (e.g., r = 0.65 → 0.75-0.85)
- **Why it helps**: Removes seasonal confounding—both temperature and water levels vary seasonally, but for different reasons
- **Physical meaning**: Net water (P - PET) represents **actual water availability** for recharge, not just rainfall
**Key insight**: In humid climates, ET is the **dominant water loss pathway**, consuming 50-60% of annual precipitation. Recharge happens primarily during the dormant season when ET is minimal.
```{python}
#| code-fold: true
# Calculate potential evapotranspiration (simplified Thornthwaite)
def calculate_pet_simple(temp_c, month):
"""
Simplified PET calculation (Thornthwaite method).
PET (mm/day) ≈ 1.6 * (10 * T / 365)^1.5 for T > 0
"""
if pd.isna(temp_c) or temp_c <= 0:
return 0
return max(0, 1.6 * ((10 * temp_c / 365) ** 1.5))
if len(merged) > 100:
merged_copy = merged.copy()
merged_copy['Month_num'] = pd.to_datetime(merged_copy['Date']).dt.month
merged_copy['PET_mm'] = merged_copy.apply(
lambda row: calculate_pet_simple(row['Temp_C'], row['Month_num']),
axis=1
)
# Net water availability (Precip - PET)
merged_copy['NetWater_mm'] = merged_copy['Precip_mm'] - merged_copy['PET_mm']
# Cumulative net water
merged_copy['NetWater_cum_30d'] = merged_copy['NetWater_mm'].rolling(window=30, min_periods=1).sum()
# Correlation
valid_net = merged_copy[['NetWater_cum_30d', 'Water_Surface_Elevation']].dropna()
if len(valid_net) > 10:
corr_net, pval_net = pearsonr(
valid_net['NetWater_cum_30d'],
valid_net['Water_Surface_Elevation']
)
print(f"Correlation (Net Water vs Water Level): {corr_net:.3f} (p={pval_net:.4f})")
if 30 in correlations_by_window:
print(f"Improvement over precip-only: {corr_net - correlations_by_window[30]:.3f}")
else:
corr_net = 0
pval_net = 1.0
print("Insufficient data for correlation")
# Update merged for visualization
merged = merged_copy.copy()
else:
print("⚠️ EVAPOTRANSPIRATION (ET) ANALYSIS SKIPPED")
print("")
print("📋 WHAT THIS ANALYSIS DOES:")
print(" • Calculates Potential ET (PET) using Thornthwaite method")
print(" • Computes Net Water = Precipitation - PET")
print(" • Tests if net water predicts groundwater levels better than precip alone")
print("")
print("🔧 DATA REQUIREMENTS:")
print(f" • Current merged records: {len(merged)} (need >100)")
print(" • Temperature data (for PET calculation)")
print(" • Precipitation data")
print(" • Groundwater levels")
print("")
print("💡 WHY ET MATTERS:")
print(" • In humid climates, ET consumes 50-60% of annual precipitation")
print(" • Summer months: ET often exceeds precipitation (deficit)")
print(" • Winter months: Low ET allows surplus for recharge")
print(" • Ignoring ET overestimates recharge rates by 2-3x")
corr_net = 0
pval_net = 1.0
```
## Visualization 3: Multi-Variable Climate-Groundwater Relationships
::: {.callout-note icon=false}
## 📊 Understanding the Water Balance Visualization
**This 3-panel figure reveals how ET affects recharge:**
| Panel | What It Shows | Critical Insight |
|-------|---------------|------------------|
| **Top** | Monthly P (blue) vs PET (orange) | When does ET exceed precipitation? |
| **Middle** | Net water (P - PET) with color coding | Green bars = surplus (recharge); Red bars = deficit (discharge) |
| **Bottom** | Net water vs water level scatter | Does accounting for ET improve correlation? |
**Reading Precipitation vs. PET (Top Panel):**
- **Blue bars taller than orange**: Surplus period—excess water available for infiltration
- **Orange bars taller than blue**: Deficit period—ET consumes all precipitation + soil moisture
- **Seasonal pattern**: Winter surplus (low ET) → Summer deficit (high ET) typical in temperate climates
**Interpreting Net Water Bars (Middle Panel):**
| Bar Color | Meaning | Aquifer Response |
|-----------|---------|------------------|
| **Green (positive)** | P > PET, water available for recharge | Water levels should rise in following weeks |
| **Red (negative)** | P < PET, atmospheric demand exceeds supply | Water levels static or declining |
| **Large green bars** | Major recharge events | Expect sharp water level increases |
| **Persistent red bars** | Drought conditions | Sustained water level decline |
**Bottom Scatter Plot - The ET Effect:**
- **Color gradient (temperature)**: Shows ET is temperature-driven
- **Tighter clustering vs. top-right panel**: Net water (P-PET) predicts better than P alone
- **Correlation improvement**: Typically +0.1 to +0.2 (e.g., r = 0.65 → 0.75-0.85)
**Why ET matters:** In humid climates, ET consumes 50-60% of annual precipitation. Ignoring it causes systematic overestimation of recharge rates.
:::
```{python}
#| code-fold: true
#| label: fig-water-balance
#| fig-cap: "Water balance analysis showing evapotranspiration effects and net water availability impacts on aquifer levels"
if len(merged) > 100 and 'NetWater_cum_30d' in merged.columns:
fig = make_subplots(
rows=3, cols=1,
subplot_titles=(
'Precipitation and PET',
'Net Water Availability (P - PET)',
'Aquifer Response to Net Water Budget'
),
row_heights=[0.3, 0.3, 0.4],
vertical_spacing=0.08
)
# Row 1: Precip and PET
merged_copy = merged.copy()
merged_copy['YearMonth'] = pd.to_datetime(merged_copy['Date']).dt.to_period('M')
monthly_balance = merged_copy.groupby('YearMonth').agg({
'Precip_mm': 'sum',
'PET_mm': 'sum',
'NetWater_mm': 'sum',
'Water_Surface_Elevation': 'mean'
}).reset_index()
monthly_balance['YearMonth'] = monthly_balance['YearMonth'].dt.to_timestamp()
fig.add_trace(
go.Bar(
x=monthly_balance['YearMonth'],
y=monthly_balance['Precip_mm'],
name='Precipitation',
marker_color='rgba(30, 144, 255, 0.7)',
legendgroup='balance'
),
row=1, col=1
)
fig.add_trace(
go.Bar(
x=monthly_balance['YearMonth'],
y=monthly_balance['PET_mm'],
name='PET',
marker_color='rgba(255, 140, 0, 0.7)',
legendgroup='balance'
),
row=1, col=1
)
# Row 2: Net water
colors = ['rgba(0, 128, 0, 0.7)' if x > 0 else 'rgba(255, 0, 0, 0.7)'
for x in monthly_balance['NetWater_mm']]
fig.add_trace(
go.Bar(
x=monthly_balance['YearMonth'],
y=monthly_balance['NetWater_mm'],
name='Net Water (P - PET)',
marker_color=colors,
showlegend=False
),
row=2, col=1
)
# Add zero line
fig.add_hline(y=0, line_dash="dash", line_color="black", opacity=0.5, row=2, col=1)
# Row 3: Scatter with net water
valid_scatter = merged_copy[['NetWater_cum_30d', 'Water_Surface_Elevation', 'Temp_C']].dropna()
fig.add_trace(
go.Scatter(
x=valid_scatter['NetWater_cum_30d'],
y=valid_scatter['Water_Surface_Elevation'],
mode='markers',
marker=dict(
size=4,
color=valid_scatter['Temp_C'],
colorscale='RdBu_r',
showscale=True,
colorbar=dict(title='Temp<br>(°C)', y=0.15, len=0.3, x=1.02)
),
name='Daily Observations',
showlegend=False,
hovertemplate='Net Water (30d): %{x:.1f} mm<br>Water Level: %{y:.2f} m<extra></extra>'
),
row=3, col=1
)
# Add trendline
if len(valid_scatter) > 10:
z = np.polyfit(valid_scatter['NetWater_cum_30d'], valid_scatter['Water_Surface_Elevation'], 1)
p = np.poly1d(z)
x_trend = np.linspace(valid_scatter['NetWater_cum_30d'].min(),
valid_scatter['NetWater_cum_30d'].max(), 100)
fig.add_trace(
go.Scatter(
x=x_trend,
y=p(x_trend),
mode='lines',
line=dict(color='red', dash='dash', width=2),
name='Trend',
showlegend=False
),
row=3, col=1
)
fig.update_xaxes(title_text='Date', row=1, col=1)
fig.update_xaxes(title_text='Date', row=2, col=1)
fig.update_xaxes(title_text='Cumulative Net Water (30d, mm)', row=3, col=1)
fig.update_yaxes(title_text='Monthly Total (mm)', row=1, col=1)
fig.update_yaxes(title_text='Net Water (mm)', row=2, col=1)
fig.update_yaxes(title_text='Water Level (m)', row=3, col=1)
fig.update_layout(
title_text=f'Water Balance Analysis<br><sub>Net Water Correlation: r={corr_net:.3f} | Accounting for ET improves prediction</sub>',
height=1000,
showlegend=True,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)
fig.show()
else:
print("⚠️ WATER BALANCE VISUALIZATION SKIPPED")
print("")
print("📊 THIS 3-PANEL FIGURE WOULD SHOW:")
print(" 1. Top: Monthly Precipitation vs PET comparison")
print(" 2. Middle: Net Water (P - PET) - green=surplus, red=deficit")
print(" 3. Bottom: Scatter of cumulative net water vs water level")
print("")
print("🔧 DATA STATUS:")
print(f" • Merged records: {len(merged)} (need >100)")
has_net_water = 'NetWater_cum_30d' in merged.columns if len(merged) > 0 else False
print(f" • Net water calculated: {'Yes' if has_net_water else 'No'}")
print("")
print("💡 KEY INSIGHTS THIS WOULD REVEAL:")
print(" • Which months have water surplus (recharge) vs deficit")
print(" • Seasonal pattern of aquifer recharge/discharge")
print(" • Whether accounting for ET improves water level prediction")
print("")
print("📥 TO ENABLE: Ensure weather database has temperature data for PET calculation")
```
## Key Insights
```{python}
#| code-fold: true
#| echo: false
# Prepare summary statistics for display
if len(merged) > 100:
summary_text = f"""
::: {{.callout-important icon=false}}
## 🔍 Weather-Response Fusion Findings
**Temporal Lag:**
- **Optimal lag**: {int(optimal_lag)} days (Well {example_well_id})
- **Interpretation**: Vadose zone travel time + aquifer response time
- **Maximum correlation**: r = {max_corr:.3f}
**Signal Transformation:**
- **Best predictor**: {optimal_window}-day cumulative precipitation
- **Correlation**: r = {correlations_by_window[optimal_window]:.3f}
- **Improvement with PET**: Δr = {abs(corr_net - correlations_by_window.get(30, 0)):.3f}
**Physical Interpretation:**
- Precipitation events take {int(optimal_lag)} days to propagate through soil and reach water table
- Cumulative effects over {optimal_window} days provide strongest predictive power
- Accounting for evapotranspiration {'improves' if corr_net > correlations_by_window.get(30, 0) else 'does not significantly improve'} correlation
:::
"""
from IPython.display import Markdown
display(Markdown(summary_text))
else:
from IPython.display import Markdown
display(Markdown("""
::: {.callout-warning icon=false}
## ⚠️ Analysis Incomplete - Data Requirements Not Met
**This chapter requires fused weather + groundwater data that is not currently available.**
### What Data Is Needed
| Data Source | Required File | Key Tables/Columns |
|-------------|---------------|-------------------|
| **Weather** | `data/warm.db` | WarmICNData: nPrecip, nAirTemp, nRelHumid, datetime |
| **Groundwater** | `data/aquifer.db` | OB_WELL_MEASUREMENTS: TIMESTAMP, Water_Surface_Elevation, P_Number |
### Data Sources
- **Weather data**: Illinois Climate Network (ICN) - Contact Illinois State Water Survey
- **Groundwater data**: Illinois State Water Survey monitoring network
### What This Analysis Would Show (with sufficient data)
1. **Temporal Lag Analysis**: How many days after precipitation before water levels respond (typically 7-90 days)
2. **Cumulative Precipitation Windows**: Which accumulation period (7, 14, 30, 60, or 90 days) best predicts water levels
3. **Evapotranspiration Effects**: How accounting for ET (water lost to atmosphere) improves recharge estimates
4. **Seasonal Patterns**: When recharge actually occurs (typically winter when ET is low)
### Expected Results by Aquifer Type
| Aquifer Type | Expected Lag | Best Window | ET Impact |
|--------------|--------------|-------------|-----------|
| Shallow unconfined | 1-7 days | 7-14 days | High (summer deficit) |
| Deep unconfined | 7-30 days | 30-60 days | Moderate |
| Confined | 30-90+ days | 60-90 days | Low (indirect) |
:::
"""))
```
## Implications for Management
1. **Drought Response**: Multi-day precipitation deficits predict water level decline with quantifiable lag
2. **Recharge Forecasting**: Weather forecasts can predict water level changes with multi-day lead time
3. **Monitoring Strategy**: High-frequency monitoring during/after major precipitation events captures system response
4. **Climate Resilience**: Temporal lags indicate buffering capacity against short-term droughts
5. **Water Balance**: Evapotranspiration must be considered for accurate recharge estimation
## References
- Healy, R. W., & Cook, P. G. (2002). Using groundwater levels to estimate recharge. *Hydrogeology Journal*, 10(1), 91-109.
- Larocque, M., et al. (1998). Contribution of correlation and spectral analyses to the regional study of a large karst aquifer. *Journal of Hydrology*, 205(3-4), 217-231.
- Taylor, R. G., et al. (2013). Ground water and climate change. *Nature Climate Change*, 3(4), 322-329.
## Next Steps
→ **Chapter 7**: Temporal Fusion Engine - Combining all 4 data sources for complete picture
**Cross-Chapter Connections:**
- Weather data from Part 1
- Time series methods from Part 2
- Informs recharge estimation (Chapter 3)
- Complements HTEM structure analysis (Chapter 5)
---
## Summary
Weather-response fusion quantifies **climate forcing of aquifer dynamics**:
✅ **Lag times identified** - Multi-day precipitation deficits predict water level decline
✅ **Buffering capacity measured** - Temporal lags indicate aquifer resilience
✅ **Recharge forecasting** - Weather forecasts predict water level changes with lead time
✅ **ET impact quantified** - Evapotranspiration critical for water balance
⚠️ **Seasonal confounding** - Both temperature and water levels vary seasonally
**Key Insight**: Weather data is the **input signal**; aquifer water levels are the **output response**. The lag structure reveals system properties (storage, connectivity, recharge efficiency).
---
## Reflection Questions
- In the aquifer you care about, what would you expect the lag between major storms and water-level recovery to be, and what measurements (or plots like those in this chapter) would you use to test that expectation?
- When cross-correlation and cumulative-window analyses show strong seasonal structure, how would you separate “true” recharge-response lags from confounding seasonal cycles in both climate and water levels?
- If PET and net water (P − PET) substantially improve correlation with water levels, how should that influence how you use raw precipitation data in recharge or forecasting models?
- How might you use lag and attenuation estimates from this chapter to design drought early-warning triggers or to choose which weather stations and wells are worth fusing in a real-time monitoring system?
---
## Related Chapters
- [Weather Station Data](../part-1-foundations/weather-station-data.qmd) - Source climate data
- [Precipitation Patterns](../part-3-temporal/precipitation-patterns.qmd) - Climate analysis
- [Recharge Rate Estimation](recharge-rate-estimation.qmd) - Water balance calculations
- [Temporal Fusion Engine](temporal-fusion-engine.qmd) - 4-source integration