.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/forecasting/plot_future_quantiles_map.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_forecasting_plot_future_quantiles_map.py: Future quantile maps with ``forecast_view`` =========================================== This lesson teaches how to read **future forecast maps** with :func:`geoprior.plot.forecast_view`. Why this figure matters ----------------------- A forecasting workflow usually produces more than one kind of question. At first, a user may ask: - how does one sample evolve through the horizon? - do the intervals widen with lead time? - how close are predictions to held-out actuals? That is the role of ``plot_forecasts``. But once the forecast table is already available, another question becomes central: **What does the forecast look like across space for each future year, and how does that picture change across quantiles?** That is the role of ``forecast_view``. The function is public in ``geoprior.plot`` and is designed to build a grid of forecast maps organized by **time slice** and **metric** (such as quantiles), while accepting both long-format and wide-format forecast tables. It also supports filtering by selected years and quantiles. What this lesson teaches ------------------------ This page is a practical guide to the future-forecast map view. We will: 1. create a compact synthetic future forecast table, 2. inspect the table structure, 3. render the real ``forecast_view`` figure, 4. learn how to read the map row-by-row and column-by-column, 5. connect the visual story to a small numerical summary. The lesson uses a synthetic dataset so the page is fully executable during the documentation build. What ``forecast_view`` expects ------------------------------ The plotting backend accepts a forecast DataFrame and can detect whether it is in long or wide format. In the common long-format case, the table includes a time column such as ``coord_t``, spatial coordinates such as ``coord_x`` and ``coord_y``, and prediction columns such as ``subsidence_q10``, ``subsidence_q50``, and ``subsidence_q90``. It supports: - ``kind="pred_only"`` to display prediction maps only, - ``kind="dual"`` to place actual and predicted panels side-by-side when actuals exist, - ``view_quantiles`` to choose which quantiles to show, - ``view_years`` to choose which years or time slices to show. .. GENERATED FROM PYTHON SOURCE LINES 68-71 Imports ------- We use the real public plotting function from GeoPrior. .. GENERATED FROM PYTHON SOURCE LINES 71-80 .. code-block:: Python from __future__ import annotations import numpy as np import pandas as pd from geoprior.plot import forecast_view from geoprior.scripts import utils as script_utils .. GENERATED FROM PYTHON SOURCE LINES 81-93 Step 1 - Build a compact synthetic future forecast table -------------------------------------------------------- We now build the synthetic future map from the shared spatial helpers in ``geoprior.scripts.utils``. This is better than rebuilding a plain grid and hotspot manually inside the lesson because it keeps: - the spatial support reusable, - the hotspot geometry coherent across years, - the quantile family internally consistent, - the uncertainty widening easy to control and explain. .. GENERATED FROM PYTHON SOURCE LINES 93-228 .. code-block:: Python rng = np.random.default_rng(17) years = [2025, 2027, 2030] # Build one reusable support cloud. # # Key parameters # -------------- # city: # Only a label for this synthetic lesson. # center_x, center_y: # Center of the projected coordinate system. # span_x, span_y: # Half-width and half-height of the map domain. # nx, ny: # Number of support locations before masking. # jitter_x, jitter_y: # Small perturbations so the support is not an overly rigid grid. # footprint: # Shape of the retained support. "ellipse" is a good generic # choice for future-map lessons. # seed: # Makes the support reproducible across doc builds. support = script_utils.make_spatial_support( script_utils.SpatialSupportSpec( city="FutureForecastDemo", center_x=6_000.0, center_y=4_000.0, span_x=5_400.0, span_y=3_700.0, nx=24, ny=18, jitter_x=32.0, jitter_y=26.0, footprint="ellipse", seed=17, ) ) # Build one baseline central field. # # Key parameters # -------------- # amplitude: # Overall field strength. # drift_x, drift_y: # Broad spatial gradient across the domain. # phase: # Phase of the smooth oscillatory component. # local_weight: # Small local texture so the map is smooth but not too perfect. base_mean = script_utils.make_spatial_field( support, amplitude=2.35, drift_x=1.05, drift_y=0.55, phase=0.60, local_weight=0.14, ) # Build one baseline uncertainty field. # # Key parameters # -------------- # base: # Global uncertainty floor. # x_weight: # Gradual east-west spread increase. # hotspot_weight: # Extra spread near the dominant hotspot. # # This makes uncertainty spatially meaningful instead of constant. base_scale = script_utils.make_spatial_scale( support, base=0.30, x_weight=0.10, hotspot_weight=0.06, ) rows: list[pd.DataFrame] = [] for i, year in enumerate(years): growth = { 2025: 1.00, 2027: 1.35, 2030: 1.80, }[year] scale_mult = { 2025: 1.00, 2027: 1.20, 2030: 1.42, }[year] # Evolve the same spatial pattern through time. # # The hotspot stays broadly in the same place but intensifies as # the forecast horizon moves farther into the future. mean_year = script_utils.evolve_spatial_field( support, base_field=base_mean, step=i, growth=growth, ) # Widen uncertainty with year. scale_year = base_scale * scale_mult # Pack one year into a forecast-ready table. # # Quantiles come from one coherent mean-plus-scale model, so # q10 < q50 < q90 is naturally preserved. rows.append( script_utils.make_spatial_quantile_frame( support, year=year, forecast_step=year - 2023, mean=mean_year, scale=scale_year, quantiles=(0.1, 0.5, 0.9), prefix="subsidence", unit="mm", ) ) future_df = pd.concat(rows, ignore_index=True) print("Future forecast table shape:", future_df.shape) print("") print(future_df.head(10).to_string(index=False)) .. rst-class:: sphx-glr-script-out .. code-block:: none Future forecast table shape: (564, 9) sample_idx forecast_step coord_t coord_x coord_y subsidence_unit subsidence_q10 subsidence_q50 subsidence_q90 0 2 2025 4427.8303 1630.3905 mm 0.8775 1.3078 1.7381 1 2 2025 4829.1242 1620.3869 mm 0.7540 1.1894 1.6247 2 2 2025 5253.3778 1559.9362 mm 0.5951 1.0361 1.4771 3 2 2025 5785.9073 1555.8001 mm 0.4148 0.8639 1.3130 4 2 2025 6257.5795 1612.8343 mm 0.3125 0.7704 1.2284 5 2 2025 6667.6284 1625.1130 mm 0.2461 0.7123 1.1785 6 2 2025 7191.0527 1656.3394 mm 0.2101 0.6870 1.1639 7 2 2025 7645.6848 1620.0601 mm 0.1292 0.6122 1.0952 8 2 2025 3000.7968 2065.0576 mm 1.1078 1.5211 1.9345 9 2 2025 3433.5082 2026.0950 mm 1.1156 1.5340 1.9524 .. GENERATED FROM PYTHON SOURCE LINES 229-240 Step 2 - Inspect the table before plotting ------------------------------------------ A useful habit is to verify the data structure before opening the figure. In a future-forecast map page, the key checks are: - are the future years present? - do all quantile columns exist? - does the median field increase through time? - does interval width also increase through time? We compute a small summary by year. .. GENERATED FROM PYTHON SOURCE LINES 240-263 .. code-block:: Python future_df["interval_width"] = ( future_df["subsidence_q90"] - future_df["subsidence_q10"] ) summary = ( future_df.groupby("coord_t") .agg( n_pixels=("sample_idx", "size"), q10_mean=("subsidence_q10", "mean"), q50_mean=("subsidence_q50", "mean"), q90_mean=("subsidence_q90", "mean"), width_mean=("interval_width", "mean"), q50_max=("subsidence_q50", "max"), ) .reset_index() .rename(columns={"coord_t": "year"}) ) print("") print("Per-year summary") print(summary.to_string(index=False)) .. rst-class:: sphx-glr-script-out .. code-block:: none Per-year summary year n_pixels q10_mean q50_mean q90_mean width_mean q50_max 2025 188 1.1006 1.5626 2.0246 0.9240 2.9416 2027 188 1.5236 2.0780 2.6324 1.1088 4.3439 2030 188 2.3298 2.9858 3.6419 1.3121 6.7919 .. GENERATED FROM PYTHON SOURCE LINES 264-290 Step 3 - Render the real future quantile map figure --------------------------------------------------- We now call the real ``forecast_view`` backend. Important choices: - ``kind="pred_only"`` because this page is about future forecasts, not holdout comparison; - ``value_prefixes=["subsidence"]`` because the columns are named ``subsidence_q10``, ``subsidence_q50``, ``subsidence_q90``; - ``view_quantiles=[0.1, 0.5, 0.9]`` so the page focuses on lower, median, and upper scenarios; - ``view_years=[2025, 2027, 2030]`` so rows follow a natural temporal story; - ``cbar="uniform"`` so all panels share the same color scale and can be compared visually without ambiguity. The function is documented to arrange a grid by year and metric and to support quantile and year filtering in this way. .. GENERATED FROM PYTHON SOURCE LINES 290-309 .. code-block:: Python forecast_view( forecast_df=future_df, value_prefixes=["subsidence"], kind="pred_only", view_quantiles=[0.1, 0.5, 0.9], view_years=[2025, 2027, 2030], spatial_cols=("coord_x", "coord_y"), time_col="coord_t", cmap="viridis", cbar="uniform", axis_off=False, show_grid=True, grid_props={"linestyle": ":", "alpha": 0.45}, figsize=(10.5, 8.4), show=True, verbose=0, ) .. image-sg:: /auto_examples/forecasting/images/sphx_glr_plot_future_quantiles_map_001.png :alt: Forecast Visualization for 'SUBSIDENCE', Q10 (2025), Q50 (2025), Q90 (2025), Q10 (2027), Q50 (2027), Q90 (2027), Q10 (2030), Q50 (2030), Q90 (2030) :srcset: /auto_examples/forecasting/images/sphx_glr_plot_future_quantiles_map_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 310-348 Step 4 - Learn how to read the figure ------------------------------------- A future-quantile map should be read in a stable order. First read the rows ~~~~~~~~~~~~~~~~~~~ Each row corresponds to one future year. The row-level question is: - how does the overall forecast field evolve through time? In this synthetic example, later years should show a stronger response overall, especially in the high-risk hotspot. Then read the columns ~~~~~~~~~~~~~~~~~~~~~ Each prediction column corresponds to a different quantile: - ``q10`` : lower scenario, - ``q50`` : central scenario, - ``q90`` : upper scenario. The column-level question is: - how much uncertainty surrounds the central forecast? Comparing left-to-right within the same year tells you how wide the plausible range is for that future slice. Why a uniform color bar matters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Because we used ``cbar="uniform"``, all panels are on the same scale. That makes cross-panel comparison honest: - a bright region in 2030 really is larger in magnitude than a mid-tone region in 2025, - apparent changes are not just artifacts of per-panel re-scaling. .. GENERATED FROM PYTHON SOURCE LINES 350-372 Step 5 - Read the median forecast first --------------------------------------- In practice, the best reading strategy is to start with the median column. Why? Because ``q50`` is the central scenario. It usually provides the cleanest first interpretation of: - the main hotspot location, - the broad regional gradient, - whether intensification over time is localized or widespread. In the synthetic figure, the median maps should show: - one dominant hotspot, - one weaker secondary elevated zone, - a general increase from 2025 to 2030. Only after reading the median maps should you compare them with q10 and q90. .. GENERATED FROM PYTHON SOURCE LINES 374-395 Step 6 - Use q10 and q90 to read uncertainty spatially ------------------------------------------------------ The lower and upper quantile maps answer a different question. They are not "competing forecasts" in the same sense as different models. Instead they describe the lower and upper parts of the predictive distribution. What to look for: - Does the hotspot remain in the same place across q10, q50, q90? - Are uncertain regions concentrated in the same zones as the strongest predicted response? - Does uncertainty widen only locally or across the whole map? In many practical workflows, a particularly important sign is when: - the hotspot exists even in q10, which suggests a robust high-risk signal; - the hotspot becomes much stronger in q90, which suggests substantial upside uncertainty in magnitude. .. GENERATED FROM PYTHON SOURCE LINES 397-410 Step 7 - Add a small numerical summary for interpretation --------------------------------------------------------- A lesson page becomes more useful when the visual story is paired with a simple numerical check. Here we track: - the mean median forecast by year, - the mean interval width by year, - the fraction of pixels above a chosen median threshold. This is not required by ``forecast_view`` itself, but it helps explain the map progression quantitatively. .. GENERATED FROM PYTHON SOURCE LINES 410-436 .. code-block:: Python threshold = 6.0 risk_summary = ( future_df.groupby("coord_t") .apply( lambda g: pd.Series( { "mean_q50": g["subsidence_q50"].mean(), "mean_width": ( g["subsidence_q90"] - g["subsidence_q10"] ).mean(), "share_q50_above_6": ( g["subsidence_q50"] > threshold ).mean(), } ) ) .reset_index() .rename(columns={"coord_t": "year"}) ) print("") print("Interpretation summary") print(risk_summary.to_string(index=False)) .. rst-class:: sphx-glr-script-out .. code-block:: none Interpretation summary year mean_q50 mean_width share_q50_above_6 2025 1.5626 0.9240 0.0000 2027 2.0780 1.1088 0.0000 2030 2.9858 1.3121 0.0426 .. GENERATED FROM PYTHON SOURCE LINES 437-452 How to interpret this summary ----------------------------- A good map lesson should connect the picture to the numbers. In this synthetic example, the summary should show: - rising mean q50 through time, - widening mean interval width through time, - a growing share of pixels above the chosen threshold. That confirms the same story already visible in the panels: - the forecast strengthens with year, - uncertainty also grows with year, - the high-response footprint expands over time. .. GENERATED FROM PYTHON SOURCE LINES 454-479 Step 8 - Practical reading guide for real workflows --------------------------------------------------- When you use ``forecast_view`` on a real GeoPrior forecast table, these are the most useful questions to ask: 1. Spatial consistency Does the forecast field look coherent, or does it look noisy and implausible? 2. Temporal progression Do the forecast maps evolve gradually from year to year, or are there abrupt jumps that need explanation? 3. Quantile separation Is there a meaningful difference between q10, q50, and q90? 4. Hotspot robustness Does the main hotspot appear already in q10, or only in q90? 5. Scale honesty Are the panels being compared on a common color scale? Those five questions usually give a strong first reading of a future-forecast map grid before you move on to holdout comparison, coverage diagnostics, or publication-style figures. .. GENERATED FROM PYTHON SOURCE LINES 481-497 Step 9 - Why this page comes after ``plot_forecasts`` ----------------------------------------------------- This lesson is a natural second step in the forecasting gallery. - ``plot_forecasts`` teaches how to read forecast trajectories for individual samples or steps. - ``forecast_view`` teaches how to read year-by-year forecast fields in space. So the two pages complement each other: - one is sample-centric, - the other is map-centric. Together they give the user a strong first understanding of what the forecasting pipeline is producing. .. GENERATED FROM PYTHON SOURCE LINES 499-514 Command-line and workflow note ------------------------------ In practice, this kind of page is usually used after Stage-4 inference has already produced a forecast table with: - ``coord_t``, - ``coord_x``, ``coord_y``, - and target quantile columns. The recommended habit is: 1. inspect the table structure, 2. open a median-first map view, 3. compare lower/median/upper quantiles, 4. then move to eval-versus-future comparison if needed. .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 0.994 seconds) .. _sphx_glr_download_auto_examples_forecasting_plot_future_quantiles_map.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_future_quantiles_map.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_future_quantiles_map.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_future_quantiles_map.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_