40  Scenario Impact Analysis

Exploring What-If Changes to Forcing and Structure

TipFor Newcomers

You will get: - A sense of how the fused models can be used to explore “what if” stories (e.g., less rain, more pumping, temperature changes). - Examples of how changes in inputs propagate through the aquifer system. - Intuition for sensitivity: which inputs the system reacts to most strongly.

Think of this chapter as a sandbox for understanding system response under hypothetical changes, not as a prescriptive planning tool.

Data Sources Fused: All 4 (for scenario modeling)

40.1 What You Will Learn in This Chapter

By the end of this chapter, you will be able to:

  • Describe how the fused temporal model can be used to explore “what‑if” changes in climate, pumping, and management actions.
  • Interpret time-series, impact, and sensitivity plots to understand which inputs the aquifer responds to most strongly and on what timescales.
  • Explain how scenario analysis connects to water-balance thinking (P, ET, Q, ΔS) and to the fusion and causal analyses earlier in the book.
  • Reflect on when scenario results should be treated as qualitative stress tests versus quantitative inputs for planning decisions.

40.2 Overview

Previous chapters built models of the aquifer system. Now we ask: “What if?” What if precipitation decreases by 20%? What if we drill a new well? What if stream discharge increases? This chapter uses the integrated fusion model to simulate scenarios and quantify cascading effects through the system.

Note💻 For Computer Scientists

Scenario Analysis Framework:

  1. Baseline: Current system state from observations
  2. Perturbation: Modify one or more inputs (weather, pumping, etc.)
  3. Propagation: Use fusion model to compute downstream effects
  4. Comparison: Quantify deviation from baseline

Techniques: - Sensitivity analysis: Partial derivatives ∂output/∂input - Monte Carlo: Sample uncertain inputs, quantify output distribution - Ensemble models: Average predictions from multiple models

Tip🌍 For Hydrologists

Management Scenarios:

  1. Climate change: ±20% precipitation, +2°C temperature
  2. Pumping increase: New wells or increased extraction
  3. Land use change: Reduced recharge from urbanization
  4. Conservation: Managed aquifer recharge (MAR)
  5. Extreme events: Drought (3-year deficit) or flood (100-year recharge)

Key Question: How do changes propagate through the coupled surface-groundwater system?

40.3 Analysis Approach

Show code
import os
import sys
from pathlib import Path
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import sqlite3
import warnings
warnings.filterwarnings('ignore')

# Setup project root and add to sys.path for local imports
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.utils import get_data_path

# Conditional imports for optional dependencies
try:
    from sklearn.ensemble import GradientBoostingRegressor
    from sklearn.preprocessing import StandardScaler
    SKLEARN_AVAILABLE = True
except ImportError:
    SKLEARN_AVAILABLE = False
    print("Note: sklearn not available. Using simplified analysis.")

try:
    from src.data_loaders import IntegratedDataLoader
    LOADER_AVAILABLE = True
except ImportError:
    LOADER_AVAILABLE = False
    print("Note: IntegratedDataLoader not available. Using direct database access.")

aquifer_db_path = get_data_path("aquifer_db")
weather_db_path = get_data_path("warm_db")
usgs_stream_root = get_data_path("usgs_stream")
try:
    loader = IntegratedDataLoader(
        aquifer_db_path=str(aquifer_db_path),
        weather_db_path=str(weather_db_path),
        usgs_stream_path=str(usgs_stream_root)
    )

    with loader:
        # Groundwater (select well with good data coverage)
        # Using well 434983 which has data from 2008-2012
        well_id = 434983
        well_df = loader.groundwater.load_well_time_series(well_id)

        # TIMESTAMP is the index, reset it to a column
        well_df = well_df.reset_index()

        # Filter to analysis period (2010-2012 for weather data overlap)
        well_df = well_df[
            (well_df['TIMESTAMP'] >= '2010-06-01') &
            (well_df['TIMESTAMP'] <= '2012-12-31')
        ].copy()

        # Resample to daily to reduce noise
        well_daily = well_df.set_index('TIMESTAMP').resample('D').agg({
            'Water_Surface_Elevation': 'mean'
        }).reset_index()
        well_daily = well_daily.dropna()

        # Rename for consistency
        well_daily = well_daily.rename(columns={
            'TIMESTAMP': 'MeasurementDate',
            'Water_Surface_Elevation': 'WaterLevelElevation'
        })

        # Weather data (station 'cmi' - Champaign)
        weather_df = loader.weather.load_hourly_data(station_code='cmi', start_date='2010-06-01')
        weather_daily = weather_df.resample('D', on='DateTime').agg({
            'Precipitation_mm': 'sum',
            'Temperature_C': 'mean'
        }).reset_index()

        # Stream discharge
        stream_df = loader.usgs_stream.load_daily_discharge('03337000')
        stream_df = stream_df[stream_df['Date'] >= '2009-01-01'].copy()

    print(f"✓ Loaded {len(well_daily):,} days of groundwater data")
    print(f"✓ Loaded {len(weather_daily):,} days of weather data")
    print(f"✓ Loaded {len(stream_df):,} days of stream discharge data")

except Exception as e:
    print(f"⚠ Error loading via IntegratedDataLoader: {e}")
    print("Loading directly from databases...")

    import sqlite3

    # Load groundwater data
    conn_gw = sqlite3.connect(aquifer_db_path)
    gw_query = """
    SELECT TIMESTAMP, Water_Surface_Elevation
    FROM OB_WELL_MEASUREMENTS_CHAMPAIGN_COUNTY
    WHERE Water_Surface_Elevation IS NOT NULL
    AND TIMESTAMP IS NOT NULL
    """
    gw_df = pd.read_sql_query(gw_query, conn_gw)
    conn_gw.close()

    gw_df['MeasurementDate'] = pd.to_datetime(gw_df['TIMESTAMP'], format='%m/%d/%Y', errors='coerce')
    gw_df = gw_df.dropna(subset=['MeasurementDate', 'Water_Surface_Elevation'])

    # Aggregate to daily mean
    well_daily = gw_df.groupby(gw_df['MeasurementDate'].dt.date).agg({
        'Water_Surface_Elevation': 'mean'
    }).reset_index()
    well_daily.columns = ['MeasurementDate', 'WaterLevelElevation']
    well_daily['MeasurementDate'] = pd.to_datetime(well_daily['MeasurementDate'])

    # Load weather data
    conn_weather = sqlite3.connect(weather_db_path)
    weather_query = """
    SELECT nDateTime, nPrecip, nAirTemp
    FROM WarmICNFiveMin
    WHERE nPrecip IS NOT NULL
    AND nAirTemp IS NOT NULL
    """
    weather_df = pd.read_sql_query(weather_query, conn_weather)
    conn_weather.close()

    weather_df['DateTime'] = pd.to_datetime(weather_df['nDateTime'], errors='coerce')
    weather_df = weather_df.dropna(subset=['DateTime'])

    # Aggregate to daily (sum precipitation, mean temperature)
    weather_daily = weather_df.groupby(weather_df['DateTime'].dt.date).agg({
        'nPrecip': 'sum',
        'nAirTemp': 'mean'
    }).reset_index()
    weather_daily.columns = ['DateTime', 'Precipitation_mm', 'Temperature_C']
    weather_daily['DateTime'] = pd.to_datetime(weather_daily['DateTime'])

    # Load stream discharge
    import glob
    usgs_files = glob.glob(f"{usgs_stream_root}/*.csv")

    if len(usgs_files) > 0:
        stream_df = pd.read_csv(usgs_files[0])

        # Find discharge and date columns
        discharge_col = None
        date_col = None

        for col in stream_df.columns:
            if 'discharge' in col.lower() or 'flow' in col.lower():
                discharge_col = col
            if 'date' in col.lower() or 'time' in col.lower():
                date_col = col

        if date_col and discharge_col:
            stream_df['Date'] = pd.to_datetime(stream_df[date_col], errors='coerce')
            stream_df['Discharge_cfs'] = pd.to_numeric(stream_df[discharge_col], errors='coerce')
            stream_df = stream_df[['Date', 'Discharge_cfs']].dropna()
        else:
            stream_df = pd.DataFrame(columns=['Date', 'Discharge_cfs'])
    else:
        stream_df = pd.DataFrame(columns=['Date', 'Discharge_cfs'])

    print(f"✓ Loaded {len(well_daily):,} days of groundwater data from aquifer.db")
    print(f"✓ Loaded {len(weather_daily):,} days of weather data from warm.db")
    print(f"✓ Loaded {len(stream_df):,} days of stream discharge data")
✓ Groundwater loader initialized
✓ Weather loader initialized
✓ USGS stream loader initialized
✓ Loaded 939 days of groundwater data
✓ Loaded 4,873 days of weather data
✓ Loaded 6,146 days of stream discharge data

40.4 Build Baseline Model

Show code
# Initialize variables for downstream code blocks
baseline_df = None
baseline_model = None
scaler = None
y_baseline_pred = None
y_baseline = pd.Series(dtype=float)
features = None
scenario_4_df = None
scenario_5_df = None
impact_4_mean = np.nan
impact_5_mean = np.nan
sensitivity_df = pd.DataFrame(columns=["Feature", "Sensitivity"])
mc_mean = np.array([])
mc_std = np.array([])
mc_p05 = np.array([])
mc_p95 = np.array([])

# Check if all required data is available
DATA_AVAILABLE = True

if 'weather_daily' not in locals() or len(weather_daily) == 0 or 'DateTime' not in weather_daily.columns:
    print("⚠️ ERROR: Weather data not available or missing DateTime column")
    DATA_AVAILABLE = False

if 'stream_df' not in locals() or len(stream_df) == 0:
    print("⚠️ ERROR: Stream discharge data not available")
    DATA_AVAILABLE = False

if 'well_daily' not in locals() or len(well_daily) == 0:
    print("⚠️ ERROR: Groundwater data not available")
    DATA_AVAILABLE = False

if not DATA_AVAILABLE:
    print("\n⚠️ Scenario impact analysis requires weather, stream, and groundwater data.")
    print("Please ensure all data sources are available before running this analysis.")
else:
    # Merge data sources
    baseline_df = weather_daily.merge(
        stream_df[['Date', 'Discharge_cfs']],
        left_on='DateTime', right_on='Date',
        how='inner'
    ).merge(
        well_daily[['MeasurementDate', 'WaterLevelElevation']],
        left_on='DateTime', right_on='MeasurementDate',
        how='inner'
    )

    # If merge resulted in empty DataFrame, provide clear error
    if len(baseline_df) == 0:
        print("⚠️ ERROR: Merge resulted in no common dates between weather, stream, and groundwater data.")
        print(f"  - Weather: {len(weather_daily)} records")
        print(f"  - Stream: {len(stream_df)} records")
        print(f"  - Groundwater: {len(well_daily)} records")
        DATA_AVAILABLE = False
    else:
        # Rename
        baseline_df = baseline_df.rename(columns={
            'Precipitation_mm': 'Precip',
            'Temperature_C': 'Temp',
            'Discharge_cfs': 'StreamQ',
            'WaterLevelElevation': 'WaterLevel'
        })

        # Create temporal features (7-day and 30-day rolling windows)
        for window in [7, 30]:
            baseline_df[f'Precip_cum_{window}d'] = baseline_df['Precip'].rolling(window).sum()
            baseline_df[f'Temp_mean_{window}d'] = baseline_df['Temp'].rolling(window).mean()

        baseline_df = baseline_df.dropna()

        # Features and target
        features = ['Precip', 'Temp', 'StreamQ', 'Precip_cum_7d', 'Precip_cum_30d', 'Temp_mean_7d', 'Temp_mean_30d']
        target = 'WaterLevel'

        X_baseline = baseline_df[features]
        y_baseline = baseline_df[target]

        if SKLEARN_AVAILABLE and len(baseline_df) > 0:
            # Train baseline model using sklearn
            scaler = StandardScaler()
            X_baseline_scaled = scaler.fit_transform(X_baseline)

            baseline_model = GradientBoostingRegressor(
                n_estimators=200,
                max_depth=5,
                learning_rate=0.05,
                random_state=42
            )

            baseline_model.fit(X_baseline_scaled, y_baseline)

            # Baseline predictions
            y_baseline_pred = baseline_model.predict(X_baseline_scaled)

            from sklearn.metrics import r2_score, mean_absolute_error
            r2_baseline = r2_score(y_baseline, y_baseline_pred)
            mae_baseline = mean_absolute_error(y_baseline, y_baseline_pred)

            print(f"\nBaseline Model Performance:")
            print(f"  R²: {r2_baseline:.3f}")
            print(f"  MAE: {mae_baseline:.3f} m")
            print(f"  Data points: {len(baseline_df):,}")
        else:
            print("⚠️ BASELINE MODEL CANNOT BE BUILT")
            print("")
            print("📋 REQUIREMENTS NOT MET:")
            print("   • sklearn library (pip install scikit-learn)")
            print("   • Merged weather + groundwater + stream data")
            print("   • Minimum overlapping data points for model training")
            print("")
            print("💡 WHAT THIS MODEL DOES:")
            print("   Trains Random Forest to predict water levels from weather/stream inputs")
            print("   Then uses model to simulate 'what-if' climate scenarios")
            DATA_AVAILABLE = False

Baseline Model Performance:
  R²: 0.999
  MAE: 0.035 m
  Data points: 266

40.5 Drought Scenario: Reduced Precipitation

Note📘 Understanding Climate Scenario Analysis

What Is It? Scenario analysis uses predictive models to simulate aquifer response under hypothetical future conditions. This “what-if” approach originated in business planning (1960s) and was adapted to environmental forecasting in the 1970s-80s (pioneered by IPCC climate scenarios).

Why Does It Matter? Water managers need to plan for uncertain futures: droughts, increased pumping, climate change. Scenario models quantify impacts before they occur, enabling proactive adaptation rather than reactive crisis management.

How Does It Work?

  1. Baseline Model: Train ML model on historical data (weather → water levels)
  2. Scenario Modification: Alter inputs (e.g., reduce precipitation by 20%)
  3. Impact Propagation: Run modified inputs through model
  4. Comparison: Calculate difference from baseline (Δ water level)

What Will You See? Time series comparing baseline (current conditions) vs. scenario (modified conditions). Impact metrics show mean change, maximum decline, and cumulative effects.

How to Interpret Results:

Scenario Type Typical Impact Planning Horizon
Drought (-20% precip) -0.3 to -1.5m decline Seasonal to annual
Warming (+2°C) -0.1 to -0.5m (via ET) Decadal
Increased pumping -0.2 to -2.0m (localized) Immediate to annual
Combined stress Non-linear (often >sum) Multi-year

Key Limitation: Models assume relationships remain stable under stress (stationarity assumption). Extreme conditions may trigger system changes not captured in historical data.

Show code
# Initialize scenario variables
scenario_1_df = None
impact_1_mean = None
impact_1_max = None

if not DATA_AVAILABLE or baseline_model is None:
    print("⚠️ DROUGHT SCENARIO ANALYSIS SKIPPED")
    print("")
    print("📋 WHAT THIS WOULD SIMULATE:")
    print("   -20% precipitation reduction (moderate drought)")
    print("")
    print("💡 TYPICAL EXPECTED IMPACTS:")
    print("   • Mean water level decline: -0.3 to -1.5 meters")
    print("   • Peak impact during late summer/early fall")
    print("   • Recovery time: 6-18 months after normal precip returns")
else:
    def apply_climate_scenario(df, precip_change_pct=0, temp_change_c=0):
        """
        Modify weather inputs to simulate climate change.

        Parameters:
        - precip_change_pct: Percent change in precipitation (e.g., -20 for 20% decrease)
        - temp_change_c: Temperature increase in °C
        """
        df_scenario = df.copy()

        # Modify precipitation
        df_scenario['Precip'] = df_scenario['Precip'] * (1 + precip_change_pct / 100)

        # Modify temperature
        df_scenario['Temp'] = df_scenario['Temp'] + temp_change_c

        # Recalculate cumulative features
        for window in [7, 30]:
            df_scenario[f'Precip_cum_{window}d'] = df_scenario['Precip'].rolling(window).sum()
            df_scenario[f'Temp_mean_{window}d'] = df_scenario['Temp'].rolling(window).mean()

        df_scenario = df_scenario.dropna()

        return df_scenario

    # Scenario 1: Drought (20% less precipitation)
    scenario_1_df = apply_climate_scenario(baseline_df, precip_change_pct=-20, temp_change_c=0)

    X_scenario_1 = scenario_1_df[features]
    X_scenario_1_scaled = scaler.transform(X_scenario_1)

    y_scenario_1_pred = baseline_model.predict(X_scenario_1_scaled)

    # Compare to baseline - use min of array lengths to avoid index errors
    max_idx = min(len(scenario_1_df), len(y_baseline_pred))
    scenario_1_df = scenario_1_df.iloc[:max_idx].copy()
    scenario_1_df['WaterLevel_baseline'] = y_baseline_pred[:max_idx]
    scenario_1_df['WaterLevel_scenario'] = y_scenario_1_pred[:max_idx]
    scenario_1_df['Impact'] = scenario_1_df['WaterLevel_scenario'] - scenario_1_df['WaterLevel_baseline']

    impact_1_mean = scenario_1_df['Impact'].mean()
    impact_1_max = scenario_1_df['Impact'].min()  # Most negative

    print(f"\nScenario 1: Drought (-20% Precipitation)")
    print(f"  Mean water level impact: {impact_1_mean:.3f} m ({impact_1_mean/y_baseline.mean()*100:.1f}%)")
    print(f"  Maximum decline: {impact_1_max:.3f} m")

Scenario 1: Drought (-20% Precipitation)
  Mean water level impact: -0.089 m (-0.0%)
  Maximum decline: -4.931 m

40.6 Warming Temperature Scenario

Show code
scenario_2_df = None
impact_2_mean = None

if not DATA_AVAILABLE or baseline_model is None:
    print("⚠️ WARMING SCENARIO ANALYSIS SKIPPED")
    print("")
    print("📋 WHAT THIS WOULD SIMULATE:")
    print("   +2°C temperature increase (mid-century climate change)")
    print("")
    print("💡 EXPECTED MECHANISM:")
    print("   • Higher temperatures → increased evapotranspiration")
    print("   • More water lost to atmosphere before reaching aquifer")
    print("   • Typical impact: -0.1 to -0.5 meters decline")
else:
    # Scenario 2: Warming (+2°C, no precip change)
    scenario_2_df = apply_climate_scenario(baseline_df, precip_change_pct=0, temp_change_c=2)

    X_scenario_2 = scenario_2_df[features]
    X_scenario_2_scaled = scaler.transform(X_scenario_2)

    y_scenario_2_pred = baseline_model.predict(X_scenario_2_scaled)

    max_idx = min(len(scenario_2_df), len(y_baseline_pred))
    scenario_2_df = scenario_2_df.iloc[:max_idx].copy()
    scenario_2_df['WaterLevel_baseline'] = y_baseline_pred[:max_idx]
    scenario_2_df['WaterLevel_scenario'] = y_scenario_2_pred[:max_idx]
    scenario_2_df['Impact'] = scenario_2_df['WaterLevel_scenario'] - scenario_2_df['WaterLevel_baseline']

    impact_2_mean = scenario_2_df['Impact'].mean()

    print(f"\nScenario 2: Warming (+2°C Temperature)")
    print(f"  Mean water level impact: {impact_2_mean:.3f} m ({impact_2_mean/y_baseline.mean()*100:.1f}%)")

Scenario 2: Warming (+2°C Temperature)
  Mean water level impact: 0.302 m (0.0%)

40.7 Combined Climate Stress

Show code
scenario_3_df = None
impact_3_mean = None

if not DATA_AVAILABLE or baseline_model is None:
    print("⚠️ COMBINED CLIMATE STRESS SCENARIO SKIPPED")
    print("")
    print("📋 WHAT THIS WOULD SIMULATE:")
    print("   -20% precipitation + 2°C warming (worst-case scenario)")
    print("")
    print("💡 KEY INSIGHT:")
    print("   Combined impacts are typically non-linear (synergistic)")
    print("   Example: Individual impacts might be -0.5m and -0.3m,")
    print("           but combined impact could be -1.2m (not just -0.8m)")
else:
    # Scenario 3: Combined (-20% precip, +2°C)
    scenario_3_df = apply_climate_scenario(baseline_df, precip_change_pct=-20, temp_change_c=2)

    X_scenario_3 = scenario_3_df[features]
    X_scenario_3_scaled = scaler.transform(X_scenario_3)

    y_scenario_3_pred = baseline_model.predict(X_scenario_3_scaled)

    max_idx = min(len(scenario_3_df), len(y_baseline_pred))
    scenario_3_df = scenario_3_df.iloc[:max_idx].copy()
    scenario_3_df['WaterLevel_baseline'] = y_baseline_pred[:max_idx]
    scenario_3_df['WaterLevel_scenario'] = y_scenario_3_pred[:max_idx]
    scenario_3_df['Impact'] = scenario_3_df['WaterLevel_scenario'] - scenario_3_df['WaterLevel_baseline']

    impact_3_mean = scenario_3_df['Impact'].mean()

    print(f"\nScenario 3: Combined Climate Stress (-20% precip, +2°C)")
    print(f"  Mean water level impact: {impact_3_mean:.3f} m ({impact_3_mean/y_baseline.mean()*100:.1f}%)")
    if impact_1_mean is not None and impact_2_mean is not None:
        print(f"  Synergy (combined vs sum of individual): {impact_3_mean - (impact_1_mean + impact_2_mean):.3f} m")

Scenario 3: Combined Climate Stress (-20% precip, +2°C)
  Mean water level impact: 0.215 m (0.0%)
  Synergy (combined vs sum of individual): 0.002 m

40.8 Visualization 1: Climate Scenario Comparison

Note📊 How to Read This 4-Panel Scenario Dashboard

Panel Interpretation Guide:

Panel What It Shows Key Questions
Top-left (Time Series) Baseline vs 3 scenarios over time Which scenario causes largest decline? When do scenarios diverge?
Top-right (Impact Distribution) Box plots of water level changes Which scenario has most/least variability?
Bottom-left (Cumulative Impact) Running total of water level decline Does impact accelerate over time?
Bottom-right (Seasonal) When drought hits hardest Which months are most vulnerable?

Reading Time Series (Top-Left):

  • Baseline (blue): Current conditions—seasonal oscillation around 213m
  • Scenario 1 (red): Drought (-20% precip)—parallel but ~0.3m lower
  • Scenario 2 (yellow): Warming (+2°C)—slight decline via increased ET
  • Scenario 3 (purple): Combined stress—non-linear amplification (worse than sum)

Interpreting Impact Distributions (Top-Right):

Box Plot Feature Meaning Management Implication
Median near zero Scenario doesn’t change typical conditions Low priority for adaptation
Median negative (<-0.5m) Significant systematic decline High priority—requires action
Wide box (high IQR) Highly variable impact Plan for worst case, not mean
Outliers Extreme events Stress-test infrastructure

Why cumulative matters (Bottom-Left): Early-warning signal—if cumulative impact keeps declining without recovery, system is in overdraft.

Seasonal sensitivity (Bottom-Right): Summer months show 2× impact vs winter—aquifer most vulnerable during high-demand season.

Show code
if not DATA_AVAILABLE or baseline_df is None or scenario_1_df is None or scenario_2_df is None or scenario_3_df is None:
    print("⚠️ CLIMATE SCENARIO VISUALIZATION SKIPPED")
    print("")
    print("📊 THIS 4-PANEL DASHBOARD WOULD SHOW:")
    print("   1. Time series: Baseline vs. 3 climate scenarios")
    print("   2. Impact distribution: Box plots comparing scenario severity")
    print("   3. Cumulative impact: Running total of water level changes")
    print("   4. Seasonal sensitivity: Which months are most vulnerable")
    print("")
    print("🔧 TO ENABLE: Ensure all data sources available and overlapping")
else:
    fig = make_subplots(
        rows=2, cols=2,
        subplot_titles=(
            'Baseline vs Scenarios (Time Series)',
            'Impact Distribution',
            'Cumulative Impact',
            'Seasonal Sensitivity (Drought)'
        ),
        vertical_spacing=0.12,
        horizontal_spacing=0.10
    )

    # Time series (sample 500 points for visibility)
    # Use min of baseline_df length and y_baseline_pred length to avoid index out of bounds
    max_idx = min(len(baseline_df), len(y_baseline_pred)) - 1
    sample_idx = np.linspace(0, max_idx, min(500, max_idx + 1), dtype=int)

    fig.add_trace(
        go.Scatter(
            x=baseline_df['DateTime'].iloc[sample_idx],
            y=y_baseline_pred[sample_idx],
            name='Baseline',
            line=dict(color='#2e8bcc', width=2),
            mode='lines'
        ),
        row=1, col=1
    )

    for scenario_name, scenario_df, color in [
        ('−20% Precip', scenario_1_df, '#f59e0b'),
        ('+2°C', scenario_2_df, '#ef4444'),
        ('Combined', scenario_3_df, '#991b1b')
    ]:
        sample_idx_scenario = np.linspace(0, len(scenario_df)-1, min(500, len(scenario_df)), dtype=int)

        fig.add_trace(
            go.Scatter(
                x=scenario_df['DateTime'].iloc[sample_idx_scenario],
                y=scenario_df['WaterLevel_scenario'].iloc[sample_idx_scenario],
                name=scenario_name,
                line=dict(color=color, width=1.5, dash='dash'),
                mode='lines'
            ),
            row=1, col=1
        )

    # Impact distributions (box plots)
    for scenario_name, scenario_df, color in [
        ('−20% Precip', scenario_1_df, '#f59e0b'),
        ('+2°C', scenario_2_df, '#ef4444'),
        ('Combined', scenario_3_df, '#991b1b')
    ]:
        fig.add_trace(
            go.Box(
                y=scenario_df['Impact'],
                name=scenario_name,
                marker_color=color,
                boxmean='sd',
                showlegend=False
            ),
            row=1, col=2
        )

    # Cumulative impact over time
    for scenario_name, scenario_df, color in [
        ('−20% Precip', scenario_1_df, '#f59e0b'),
        ('+2°C', scenario_2_df, '#ef4444'),
        ('Combined', scenario_3_df, '#991b1b')
    ]:
        fig.add_trace(
            go.Scatter(
                x=np.arange(len(scenario_df)),
                y=scenario_df['Impact'].cumsum(),
                name=scenario_name,
                line=dict(color=color, width=2),
                showlegend=False
            ),
            row=2, col=1
        )

    # Seasonal sensitivity (drought scenario by month)
    scenario_1_df['Month'] = pd.to_datetime(scenario_1_df['DateTime']).dt.month
    monthly_impact = scenario_1_df.groupby('Month')['Impact'].mean()

    fig.add_trace(
        go.Bar(
            x=['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
            y=monthly_impact.values,
            marker_color='#18b8c9',
            showlegend=False
        ),
        row=2, col=2
    )

    # Update axes labels
    fig.update_xaxes(title_text='Date', row=1, col=1)
    fig.update_xaxes(title_text='Days', row=2, col=1)
    fig.update_xaxes(title_text='Month', row=2, col=2)

    fig.update_yaxes(title_text='Water Level (m)', row=1, col=1)
    fig.update_yaxes(title_text='Impact (m)', row=1, col=2)
    fig.update_yaxes(title_text='Cumulative Impact (m·days)', row=2, col=1)
    fig.update_yaxes(title_text='Mean Impact (m)', row=2, col=2)

    fig.update_layout(
        title_text='Climate Change Scenario Analysis',
        height=900,
        showlegend=True,
        hovermode='x unified'
    )

    fig.show()
(a) Four-panel comparison of climate change scenarios showing time series projections, impact distributions, cumulative effects, and seasonal sensitivity patterns
(b)
Figure 40.1

40.9 Managed Aquifer Recharge (MAR)

Show code
# Simulate MAR: Add artificial recharge events (e.g., 50mm every 30 days)
impact_4_mean = np.nan
scenario_4_df = None

if (
    not DATA_AVAILABLE
    or baseline_df is None
    or baseline_model is None
    or scaler is None
    or features is None
    or y_baseline_pred is None
):
    print("Skipping MAR scenario (baseline model/data unavailable).")
else:
    scenario_4_df = baseline_df.copy()

    # Add recharge events
    recharge_interval = 30  # days
    recharge_amount = 50  # mm

    recharge_dates = scenario_4_df['DateTime'][::recharge_interval]
    scenario_4_df.loc[scenario_4_df['DateTime'].isin(recharge_dates), 'Precip'] += recharge_amount

    # Recalculate features
    for window in [7, 30]:
        scenario_4_df[f'Precip_cum_{window}d'] = scenario_4_df['Precip'].rolling(window).sum()

    scenario_4_df = scenario_4_df.dropna()

    X_scenario_4 = scenario_4_df[features]
    X_scenario_4_scaled = scaler.transform(X_scenario_4)

    y_scenario_4_pred = baseline_model.predict(X_scenario_4_scaled)

    scenario_4_df['WaterLevel_baseline'] = y_baseline_pred[:len(scenario_4_df)]
    scenario_4_df['WaterLevel_scenario'] = y_scenario_4_pred
    scenario_4_df['Impact'] = scenario_4_df['WaterLevel_scenario'] - scenario_4_df['WaterLevel_baseline']

    impact_4_mean = scenario_4_df['Impact'].mean()

    print(f"\nScenario 4: Managed Aquifer Recharge (50mm every 30 days)")
    print(f"  Mean water level increase: {impact_4_mean:.3f} m")
    print(f"  Total artificial recharge: {recharge_amount * len(recharge_dates):.0f} mm over {len(scenario_4_df)} days")

Scenario 4: Managed Aquifer Recharge (50mm every 30 days)
  Mean water level increase: 0.293 m
  Total artificial recharge: 450 mm over 237 days

40.10 Increased Pumping Scenario

Show code
# Simulate increased pumping by reducing stream discharge (proxy for aquifer extraction)
# ⚠️ WARNING: This is a SIMPLIFIED PROXY, NOT real pumping data
# In reality, would model pumping directly if data available
# Stream discharge reduction is used as a conceptual placeholder to demonstrate impact analysis
# True pumping impacts require actual pumping records and well-specific cone of depression modeling
impact_5_mean = np.nan
scenario_5_df = None

if (
    not DATA_AVAILABLE
    or baseline_df is None
    or baseline_model is None
    or scaler is None
    or features is None
    or y_baseline_pred is None
):
    print("Skipping pumping proxy scenario (baseline model/data unavailable).")
else:
    scenario_5_df = baseline_df.copy()

    # Reduce stream discharge by 30% (represents increased pumping reducing baseflow)
    scenario_5_df['StreamQ'] = scenario_5_df['StreamQ'] * 0.7

    scenario_5_df = scenario_5_df.dropna()

    X_scenario_5 = scenario_5_df[features]
    X_scenario_5_scaled = scaler.transform(X_scenario_5)

    y_scenario_5_pred = baseline_model.predict(X_scenario_5_scaled)

    scenario_5_df['WaterLevel_baseline'] = y_baseline_pred[:len(scenario_5_df)]
    scenario_5_df['WaterLevel_scenario'] = y_scenario_5_pred
    scenario_5_df['Impact'] = scenario_5_df['WaterLevel_scenario'] - scenario_5_df['WaterLevel_baseline']

    impact_5_mean = scenario_5_df['Impact'].mean()

    print(f"\nScenario 5: Increased Pumping (proxy: -30% stream discharge)")
    print(f"  Mean water level impact: {impact_5_mean:.3f} m")

Scenario 5: Increased Pumping (proxy: -30% stream discharge)
  Mean water level impact: 0.164 m

40.11 Sensitivity Analysis

Note📘 Understanding Sensitivity Analysis

What Is It? Sensitivity analysis quantifies how much output (water levels) changes when inputs (precipitation, temperature, pumping) change. The technique originated in engineering optimization (1960s) to identify critical design parameters.

Why Does It Matter? Not all inputs matter equally. Sensitivity analysis reveals which variables the aquifer system responds to most strongly, guiding:

  • Monitoring priorities: Focus on high-sensitivity variables
  • Management levers: Identify most effective interventions
  • Risk assessment: Understand vulnerability to input changes

How Does It Work? For each input variable:

  1. Perturb input by small amount (1% increase)
  2. Calculate resulting change in water level
  3. Compute sensitivity = Δoutput / Δinput
  4. Rank variables by absolute sensitivity

What Will You See? Horizontal bar chart showing sensitivity coefficients. Positive = increase input → increase water level. Negative = increase input → decrease water level (e.g., pumping).

How to Interpret Results:

Rank Feature Type Typical Sensitivity Management Action
#1 Previous water level 0.8-1.0 Strong autocorrelation (expected)
#2 Cumulative precip (30d) 0.3-0.6 Protect recharge areas
#3 Temperature -0.1 to -0.3 Monitor ET impacts
#4 Stream discharge 0.1-0.4 Surface-groundwater connection

Key Insight: Features with high sensitivity are leverage points where small changes create large impacts. Focus interventions here.

Compute partial derivatives to identify most influential inputs:

Show code
def compute_sensitivity(model, scaler, baseline_data, feature_names):
    """
    Compute sensitivity (∂output/∂input) for each feature.

    Uses finite difference approximation.
    """
    sensitivities = {}

    X_base = baseline_data[feature_names]
    X_base_scaled = scaler.transform(X_base)
    y_base = model.predict(X_base_scaled)

    delta = 0.01  # 1% perturbation

    for i, feature in enumerate(feature_names):
        X_perturbed = X_base.copy()
        X_perturbed[feature] = X_perturbed[feature] * (1 + delta)

        X_perturbed_scaled = scaler.transform(X_perturbed)
        y_perturbed = model.predict(X_perturbed_scaled)

        # Sensitivity = Δoutput / Δinput
        sensitivity = (y_perturbed - y_base).mean() / (X_base[feature].mean() * delta)

        sensitivities[feature] = sensitivity

    return sensitivities

sensitivity_df = pd.DataFrame(columns=["Feature", "Sensitivity"])

if (
    not DATA_AVAILABLE
    or baseline_df is None
    or baseline_model is None
    or scaler is None
    or features is None
):
    print("Skipping sensitivity analysis (baseline model/data unavailable).")
else:
    sensitivities = compute_sensitivity(baseline_model, scaler, baseline_df, features)

    sensitivity_df = pd.DataFrame({
        'Feature': list(sensitivities.keys()),
        'Sensitivity': list(sensitivities.values())
    }).sort_values('Sensitivity', key=abs, ascending=False)

    print("\nSensitivity Analysis (∂WaterLevel/∂Input):")
    print(sensitivity_df.to_string(index=False))

Sensitivity Analysis (∂WaterLevel/∂Input):
       Feature  Sensitivity
        Precip    -0.633830
 Precip_cum_7d    -0.386122
       StreamQ    -0.168978
Precip_cum_30d    -0.134467
 Temp_mean_30d     0.060899
  Temp_mean_7d    -0.010226
          Temp    -0.000031

40.12 Visualization 2: Sensitivity and Impact Summary

Note📊 Reading This 2-Panel Comparison

Left Panel - Feature Sensitivity (Tornado Chart):

Bar Direction Meaning Management Priority
Green (right) Positive impact—increases water levels Opportunity for enhancement
Red (left) Negative impact—decreases levels Risk to manage
Long bars High sensitivity Monitor/control actively
Short bars Low sensitivity Lower priority

Right Panel - Scenario Impact Summary:

Compares total impact of different management interventions. Look for: - Which scenarios cause largest declines? (longest negative bars) - Are combined stresses worse than sum? (synergy/antagonism)

Typical Pattern: Precipitation > Temperature > Stream in terms of sensitivity.

Show code
if sensitivity_df is None or len(sensitivity_df) == 0:
    print("Sensitivity/impact summary not available for this render.")
else:
    fig = make_subplots(
        rows=1, cols=2,
        subplot_titles=('Feature Sensitivity', 'Scenario Impact Summary'),
        horizontal_spacing=0.15
    )

    # Sensitivity bar chart (horizontal)
    colors_sens = ['#10b981' if s > 0 else '#ef4444' for s in sensitivity_df['Sensitivity']]

    fig.add_trace(
        go.Bar(
            y=sensitivity_df['Feature'],
            x=sensitivity_df['Sensitivity'],
            orientation='h',
            marker_color=colors_sens,
            text=sensitivity_df['Sensitivity'].round(3),
            textposition='outside',
            showlegend=False
        ),
        row=1, col=1
    )

    # Scenario summary bar chart
    scenario_impacts = {
        '−20% Precip': impact_1_mean,
        '+2°C Temp': impact_2_mean,
        'Combined': impact_3_mean,
        'MAR': impact_4_mean,
        'Pumping': impact_5_mean
    }

    scenario_labels = list(scenario_impacts.keys())
    scenario_values = [pd.to_numeric(scenario_impacts[k], errors='coerce') for k in scenario_labels]
    scenario_text = [('n/a' if pd.isna(v) else f'{v:.3f}') for v in scenario_values]

    scenario_colors = ['#f59e0b', '#ef4444', '#991b1b', '#10b981', '#3b82f6']

    fig.add_trace(
        go.Bar(
            x=scenario_labels,
            y=scenario_values,
            marker_color=scenario_colors,
            text=scenario_text,
            textposition='outside',
            showlegend=False
        ),
        row=1, col=2
    )

    fig.update_xaxes(title_text='Sensitivity (m per unit change)', row=1, col=1)
    fig.update_xaxes(title_text='Scenario', row=1, col=2)

    fig.update_yaxes(title_text='Feature', row=1, col=1)
    fig.update_yaxes(title_text='Mean Water Level Impact (m)', row=1, col=2)

    fig.update_layout(
        title_text='Sensitivity Analysis and Scenario Impact Summary',
        height=600,
        showlegend=False,
        hovermode='closest'
    )

    fig.show()
Figure 40.2: Feature sensitivity analysis and comprehensive scenario impact comparison showing which inputs most influence water levels and the effects of different management interventions

40.13 Monte Carlo Uncertainty Analysis

Note📘 Understanding Monte Carlo Uncertainty Propagation

What Is It? Monte Carlo simulation (named after the famous casino) uses repeated random sampling to quantify uncertainty. Developed by mathematicians working on nuclear weapons (Manhattan Project, 1940s), it’s now standard for risk analysis across all fields.

Why Does It Matter? All inputs have uncertainty: weather forecasts are imperfect, sensors have measurement error, climate projections span wide ranges. Monte Carlo propagates these input uncertainties through models to quantify output uncertainty—turning point predictions into confidence intervals.

How Does It Work?

  1. Define Uncertainty: Specify input distributions (e.g., precipitation ± 10%, temperature ± 1°C)
  2. Random Sampling: Generate 500-1000 input scenarios by sampling distributions
  3. Model Runs: Run each scenario through prediction model
  4. Aggregate: Calculate mean, std dev, and percentiles (5th, 95th) of outputs

What Will You See? Time series with shaded confidence bands. The band width indicates prediction uncertainty—wider bands mean more uncertain forecasts.

How to Interpret Results:

Confidence Interval Width Interpretation Decision Guidance
Narrow (±0.2m) High confidence Proceed with planning
Moderate (±0.5m) Moderate uncertainty Consider backup options
Wide (±1.0m+) Low confidence Invest in better data/models

90% Confidence Interval: 90% of model runs fall within this range. If planning requires certainty, use the pessimistic bound (5th percentile) for conservative decision-making.

Practical Example: “Under drought conditions, we predict water levels will decline by 0.8m ± 0.3m (90% CI). Worst case (5th percentile): 1.2m decline.”

Show code
# Propagate input uncertainty through model
n_simulations = 500  # Reduced for performance
mc_mean = np.array([])
mc_std = np.array([])
mc_p05 = np.array([])
mc_p95 = np.array([])

if (
    not DATA_AVAILABLE
    or baseline_df is None
    or baseline_model is None
    or scaler is None
    or features is None
    or y_baseline_pred is None
):
    print("Skipping Monte Carlo analysis (baseline model/data unavailable).")
else:
    # Assume ±10% uncertainty in precipitation, ±1°C in temperature
    precip_std = 0.10
    temp_std = 1.0

    monte_carlo_results = []

    for i in range(n_simulations):
        # Perturb inputs
        scenario_mc = baseline_df.copy()

        scenario_mc['Precip'] = scenario_mc['Precip'] * (1 + np.random.normal(0, precip_std, len(scenario_mc)))
        scenario_mc['Temp'] = scenario_mc['Temp'] + np.random.normal(0, temp_std, len(scenario_mc))

        # Recalculate features
        for window in [7, 30]:
            scenario_mc[f'Precip_cum_{window}d'] = scenario_mc['Precip'].rolling(window).sum()
            scenario_mc[f'Temp_mean_{window}d'] = scenario_mc['Temp'].rolling(window).mean()

        scenario_mc = scenario_mc.dropna()

        X_mc = scenario_mc[features]
        X_mc_scaled = scaler.transform(X_mc)

        y_mc_pred = baseline_model.predict(X_mc_scaled)

        monte_carlo_results.append(y_mc_pred)

    # Compute statistics
    mc_array = np.array(monte_carlo_results)
    mc_mean = mc_array.mean(axis=0)
    mc_std = mc_array.std(axis=0)
    mc_p05 = np.percentile(mc_array, 5, axis=0)
    mc_p95 = np.percentile(mc_array, 95, axis=0)

    print(f"\nMonte Carlo Uncertainty Analysis ({n_simulations} simulations):")
    print(f"  Mean predicted water level: {mc_mean.mean():.3f} m")
    print(f"  Std deviation: {mc_std.mean():.3f} m")
    print(f"  90% confidence interval: [{mc_p05.mean():.3f}, {mc_p95.mean():.3f}] m")

Monte Carlo Uncertainty Analysis (500 simulations):
  Mean predicted water level: 703.674 m
  Std deviation: 0.240 m
  90% confidence interval: [703.323, 704.007] m

40.14 Visualization 3: Uncertainty Bounds

Note📊 Understanding Confidence Intervals

This time series with shaded bands shows prediction uncertainty:

Element What It Represents Interpretation
Solid line (median) Most likely outcome “Best estimate” for planning
Light blue band 90% confidence interval 9 out of 10 outcomes fall here
Band width Prediction uncertainty Wider = less certain

Reading Uncertainty:

  • Narrow bands (<0.2m): High confidence—reliable for decisions
  • Wide bands (>0.5m): High uncertainty—use conservative approach
  • Expanding bands over time: Uncertainty compounds (longer forecast = less certain)
  • Shrinking bands: System converging to stable state

Management Decision Rules:

Observation Action
Lower bound < critical level Plan for worst case (10th percentile)
Median declining Implement conservation measures
Upper bound stable Monitor and reassess—no immediate action

Why uncertainty matters: Don’t just plan for the median—design infrastructure to handle the 10th percentile (conservative), celebrate if you get 90th percentile (favorable).

Show code
if (
    not DATA_AVAILABLE
    or baseline_df is None
    or y_baseline_pred is None
    or mc_mean is None
    or len(mc_mean) == 0
    or len(mc_p05) == 0
    or len(mc_p95) == 0
    or 'DateTime' not in baseline_df.columns
):
    print("Uncertainty bounds not available for this render.")
else:
    # Use min of all array lengths to avoid index out of bounds
    max_idx = min(len(baseline_df), len(mc_p95), len(mc_mean), len(y_baseline_pred)) - 1
    if max_idx < 0:
        print("Uncertainty bounds not available for this render.")
    else:
        sample_idx = np.linspace(0, max_idx, min(500, max_idx + 1), dtype=int)

        fig = go.Figure()

        # Upper confidence bound
        fig.add_trace(go.Scatter(
            x=baseline_df['DateTime'].iloc[sample_idx],
            y=mc_p95[sample_idx],
            mode='lines',
            line=dict(width=0),
            showlegend=False,
            hoverinfo='skip'
        ))

        # Lower confidence bound with fill
        fig.add_trace(go.Scatter(
            x=baseline_df['DateTime'].iloc[sample_idx],
            y=mc_p05[sample_idx],
            mode='lines',
            line=dict(width=0),
            fillcolor='rgba(46, 139, 204, 0.3)',
            fill='tonexty',
            name='90% Confidence Interval',
            hoverinfo='skip'
        ))

        # Mean prediction
        fig.add_trace(go.Scatter(
            x=baseline_df['DateTime'].iloc[sample_idx],
            y=mc_mean[sample_idx],
            mode='lines',
            line=dict(color='#2e8bcc', width=2),
            name='Mean Prediction'
        ))

        # Baseline (deterministic)
        fig.add_trace(go.Scatter(
            x=baseline_df['DateTime'].iloc[sample_idx],
            y=y_baseline_pred[sample_idx],
            mode='lines',
            line=dict(color='#1f2937', width=1, dash='dash'),
            name='Baseline (Deterministic)'
        ))

        fig.update_layout(
            title='Monte Carlo Uncertainty Propagation<br><sub>±10% Precip, ±1°C Temp uncertainty | 500 simulations</sub>',
            xaxis_title='Date',
            yaxis_title='Water Level (m)',
            height=600,
            hovermode='x unified'
        )

        fig.show()
Figure 40.3: Monte Carlo uncertainty propagation showing 90% confidence intervals around water level predictions accounting for input data uncertainty

40.15 Key Insights

Important🔍 Scenario Analysis Findings

Climate Impacts (ranked by severity):

  1. Combined stress (0.215 m): Worst case scenario
  2. Precipitation reduction (-0.089 m): Dominant driver
  3. Temperature increase (0.302 m): Secondary effect

Management Options:

  • MAR benefit: +0.293 m (offsets ~328% of drought impact)
  • Pumping impact: 0.164 m

Sensitivity (most influential features):

  1. Precip: -0.634 m/unit
  2. Precip_cum_7d: -0.386 m/unit
  3. StreamQ: -0.169 m/unit

40.16 Management Decision Support

Show code
if (
    not DATA_AVAILABLE
    or y_baseline is None
    or len(y_baseline) == 0
    or impact_3_mean is None
    or pd.isna(impact_3_mean)
    or mc_mean is None
    or len(mc_mean) == 0
):
    print("Decision support summary unavailable for this render (scenario model not fully computed).")
else:
    print("\n=== Decision Support Summary ===")

    print("\nClimate Adaptation Priorities:")
    print("  1. Protect recharge areas (precipitation drives system)")
    print(f"  2. Implement MAR to buffer {abs(impact_4_mean/impact_3_mean)*100:.1f}% of climate stress")
    print("  3. Monitor temperature effects on ET (secondary but growing)")

    print("\nRisk Assessment:")
    print(f"  Baseline water level: {y_baseline.mean():.2f} m")
    print(f"  Worst-case scenario (combined stress): {y_baseline.mean() + impact_3_mean:.2f} m")
    print(f"  Decline: {abs(impact_3_mean):.2f} m ({abs(impact_3_mean)/y_baseline.mean()*100:.1f}%)")

    print("\nUncertainty:")
    print(f"  90% confidence interval width: {(mc_p95.mean() - mc_p05.mean()):.2f} m")
    print(f"  Relative uncertainty: {(mc_std.mean() / mc_mean.mean())*100:.1f}%")

=== Decision Support Summary ===

Climate Adaptation Priorities:
  1. Protect recharge areas (precipitation drives system)
  2. Implement MAR to buffer 136.3% of climate stress
  3. Monitor temperature effects on ET (secondary but growing)

Risk Assessment:
  Baseline water level: 703.40 m
  Worst-case scenario (combined stress): 703.61 m
  Decline: 0.22 m (0.0%)

Uncertainty:
  90% confidence interval width: 0.68 m
  Relative uncertainty: 0.0%

40.17 Limitations

  1. Model uncertainty: Assumes relationships remain stationary under stress
  2. Missing processes: Pumping data incomplete, land use changes not modeled
  3. Spatial resolution: Single well; impacts vary spatially
  4. Temporal scale: Long-term trends may differ from short-term responses

40.18 References

  • Taylor, R. G., et al. (2013). Ground water and climate change. Nature Climate Change, 3(4), 322-329.
  • Dillon, P., et al. (2019). Sixty years of global progress in managed aquifer recharge. Hydrogeology Journal, 27(1), 1-30.
  • Green, T. R., et al. (2011). Beneath the surface of global change. Water Resources Research, 47(12).

40.19 Next Steps

Chapter 12: Bayesian Uncertainty Model - Rigorous uncertainty quantification

Cross-Chapter Connections: - Uses fusion model from Chapter 7 - Informs monitoring value (Chapter 13) - Validates network connectivity (Chapter 10) - Foundation for adaptive management


40.20 Summary

Scenario impact analysis enables forward-looking aquifer management:

Climate scenarios tested - Drought, wet year, climate change projections

Intervention modeling - MAR, pumping changes, land use scenarios

Uncertainty propagation - Scenario outcomes include confidence intervals

⚠️ Stationarity assumption - Assumes relationships remain stable under stress

⚠️ Missing processes - Pumping data incomplete, land use changes not fully modeled

Key Insight: Scenario analysis transforms fusion models from retrospective (what happened) to prospective (what will happen if…).


40.21 Reflection Questions

  • In your basin, which specific “what‑if” scenarios (for example, a multi‑year drought, a new wellfield, or MAR expansion) would be most useful to explore, and what decisions would hinge on those results?
  • How would you explain to non‑technical stakeholders the difference between a scenario stress test and a formal forecast, especially when communicating uncertainty and model limitations?
  • When scenario results and physical or historical intuition disagree, what steps would you take to diagnose whether the issue is with the model, the input assumptions, or your prior mental model of the system?
  • What additional data streams or monitoring upgrades would most increase your confidence in scenario outputs (for example, pumping logs, distributed recharge estimates, or expanded well networks)?