Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/NASAARSET/PM2.5_AQ_Online_2026/llms.txt

Use this file to discover all available pages before exploring further.

The training notebooks use four complementary visualization patterns to evaluate MERRA-2 CNN and SatPM output against ground monitors. Spatial maps reveal geographic biases; scatter plots expose the full distribution of paired estimates; diel cycle plots diagnose hour-by-hour behavior; and time series charts track individual grid-cell trajectories. All patterns share a common color vocabulary (Oranges colormap) and rely on either Cartopy, seaborn, or matplotlib directly.

Spatial Maps with F_plot_map

F_plot_map renders a Cartopy PlateCarree projection with coastlines, country borders, US state lines, and major lakes. It simultaneously draws a filled gridded field (from an xarray.DataArray) and an overlaid scatter of point observations, making it easy to compare gridded model output with station measurements at a glance. The function uses xarray’s built-in .plot() method for the gridded field and ax.scatter() for the point overlay.

Signature

def F_plot_map(a_data_plot=None,
               a_data_plot_point=None,
               n_lat_min=-90,
               n_lat_max=90,
               n_lon_min=-180,
               n_lon_max=180,
               my_cmap=plt.get_cmap('Oranges'),
               Colorbar_Label=None,
               Plot_Title=None,
               Colorbar_Limits=[0, 1],
               n_pointsize=20,
               n_latlon_gridspacing=1,
               o_figure=None,
               o_axis=None):
    """
    a_data_plot        : xarray.DataArray with dims (lat, lon) or (lat, lon, time).
                         If time dim is present, it is averaged before plotting.
    a_data_plot_point  : pandas DataFrame or Series with lat/lon columns for point overlay.
    n_lat_min/max      : bounding box latitude limits.
    n_lon_min/max      : bounding box longitude limits.
    my_cmap            : matplotlib colormap (default 'Oranges').
    Colorbar_Label     : label string for the colorbar.
    Plot_Title         : title string for the axes.
    Colorbar_Limits    : [vmin, vmax] for the color scale.
    n_pointsize        : scatter marker size for point data.
    n_latlon_gridspacing : spacing in degrees for gridlines.
    o_figure / o_axis  : existing figure/axis objects to reuse.

    Returns: o_figure, o_axis, o_plot
    """

Feature Set

The function draws coastlines plus three Cartopy features with distinct line styles:
o_axis.coastlines()
o_axis.add_feature(cartopy.feature.LAKES,   edgecolor='black', facecolor='none', linestyle='-')
o_axis.add_feature(cartopy.feature.BORDERS, edgecolor='black', facecolor='none', linestyle='--')
o_axis.add_feature(cartopy.feature.STATES,  edgecolor='black', facecolor='none', linestyle=':')

Example Calls

Plot the time-averaged SatPM gridded field:
F_plot_map(a_data_plot=a_data_SatPM['PM25_filtered'],
           n_lat_min=n_lat_min,
           n_lat_max=n_lat_max,
           n_lon_min=n_lon_min,
           n_lon_max=n_lon_max,
           Colorbar_Label='PM$_{2.5}$ [µg/m$^{3}$]',
           Plot_Title=f'SatPM\n(Average {t_start} to {t_end})',
           Colorbar_Limits=[0, 40])
Overlay the MERRA-2 CNN gridded field with monitor point observations:
F_plot_map(a_data_plot=a_data_MERRA2_CNN['PM25_filtered'],
           a_data_plot_point=f_data_monitors['PM25_filtered'],
           n_lat_min=n_lat_min,
           n_lat_max=n_lat_max,
           n_lon_min=n_lon_min,
           n_lon_max=n_lon_max,
           Colorbar_Label='PM$_{2.5}$ [µg/m$^{3}$]',
           Plot_Title=f'Comparing MERRA-2 CNN to Monitors\n(Average {t_start} to {t_end})',
           Colorbar_Limits=[0, 40])
Set Colorbar_Limits to the 95th percentile of the data rather than a fixed value when comparing across very different seasons or regions. This prevents most of the colormap from being washed out by a handful of extreme events.

Scatter Plot: Satellite vs. Monitor Comparison

Seaborn’s scatterplot is used to plot paired (monitor, model) values, colored by a third dimension. The notebook uses o_ax = sns.scatterplot(...) directly — there is no wrapping plt.subplots() call; the returned axes object is used for all subsequent formatting.

Hourly Comparison — Colored by Hour of Day

The hourly scatter uses local_hour_of_day as the hue and draws a 1:1 reference line spanning [0, 200]:
import seaborn as sns

o_ax = sns.scatterplot(data=f_data_combined_gridded.dropna(),
                       x='Average',
                       y='MERRA2_CNN',
                       hue='local_hour_of_day')
o_ax.plot([0, 200], [0, 200], 'k--')
o_ax.legend(title='Local Hour of Day')
o_ax.set_xlabel('Monitor Hourly Average PM$_{2.5}$ [$\mu$g/m$^{3}$]')
o_ax.set_ylabel('MERRA-2 CNN Hourly Average PM$_{2.5}$ [$\mu$g/m$^{3}$]')
o_ax.set_title('Hourly comparison: MERRA-2 CNN v. Ground Monitors')

Daily Comparison — Colored by Latitude

The daily scatter uses lat_grid as the hue and draws a 1:1 reference line spanning [0, 100]:
o_ax = sns.scatterplot(data=f_data_combined_gridded_daily.dropna(),
                       x='Average',
                       y='MERRA2_CNN',
                       hue='lat_grid')
o_ax.plot([0, 100], [0, 100], 'k--')
o_ax.legend(title='Latitude')
o_ax.set_xlabel('Monitor Daily Average PM$_{2.5}$ [$\mu$g/m$^{3}$]')
o_ax.set_ylabel('MERRA-2 CNN Daily Average PM$_{2.5}$ [$\mu$g/m$^{3}$]')
o_ax.set_title('Daily comparison: MERRA-2 CNN v. Ground Monitors')
The 1:1 reference line coordinates differ between the hourly scatter ([0, 200]) and the daily scatter ([0, 100]), reflecting the wider dynamic range possible in hourly vs. daily-averaged PM2.5.

Diel Cycle Plot

The diel (diurnal) cycle plot shows the median PM2.5 for each hour of day (0–23) with an interquartile range shaded band. This pattern is ideal for diagnosing whether MERRA-2 CNN captures morning rush-hour peaks or nocturnal boundary-layer buildup.

Compute the Diel Statistics

All monitor and model diel statistics are stored in a single DataFrame. Note the MERRA-2 CNN IQR columns use distinct names with the MERRA2_CNN_ prefix:
f_data_combined_gridded_by_timeofday = f_data_combined_gridded.groupby('local_hour_of_day')[['Average']].median()
f_data_combined_gridded_by_timeofday['Count'] = f_data_combined_gridded.groupby('local_hour_of_day')[['Average']].count()
f_data_combined_gridded_by_timeofday['75th_percentile'] = f_data_combined_gridded.groupby('local_hour_of_day')[['Average']].quantile(0.75)
f_data_combined_gridded_by_timeofday['25th_percentile'] = f_data_combined_gridded.groupby('local_hour_of_day')[['Average']].quantile(0.25)

f_data_combined_gridded_by_timeofday['MERRA2_CNN'] = f_data_combined_gridded.groupby('local_hour_of_day')[['MERRA2_CNN']].median()
f_data_combined_gridded_by_timeofday['MERRA2_CNN_75th_percentile'] = f_data_combined_gridded.groupby('local_hour_of_day')[['MERRA2_CNN']].quantile(0.75)
f_data_combined_gridded_by_timeofday['MERRA2_CNN_25th_percentile'] = f_data_combined_gridded.groupby('local_hour_of_day')[['MERRA2_CNN']].quantile(0.25)

Plot Monitor and Model Together

The figure uses plt.figure() / add_subplot() (not plt.subplots()). Monitor elements use color='black'; MERRA-2 CNN elements use color='red':
import numpy as np
import matplotlib.pyplot as plt

o_fig = plt.figure()
o_ax = o_fig.add_subplot()

# Monitor IQR band and median
o_ax.fill_between(np.arange(0, 24),
                  f_data_combined_gridded_by_timeofday['25th_percentile'],
                  f_data_combined_gridded_by_timeofday['75th_percentile'],
                  color='black', alpha=0.25)
o_ax.plot(np.arange(0, 24),
          f_data_combined_gridded_by_timeofday['Average'],
          color='black', label='Monitor Median')

# MERRA-2 CNN IQR band and median
o_ax.fill_between(np.arange(0, 24),
                  f_data_combined_gridded_by_timeofday['MERRA2_CNN_25th_percentile'],
                  f_data_combined_gridded_by_timeofday['MERRA2_CNN_75th_percentile'],
                  color='red', alpha=0.25)
o_ax.plot(np.arange(0, 24),
          f_data_combined_gridded_by_timeofday['MERRA2_CNN'],
          color='red', label='MERRA-2 CNN Median')

o_ax.legend()
o_ax.set_xlabel('Local Hour of Day')
o_ax.set_ylabel('Diel PM$_{2.5}$ [$\mu$g/m$^{3}$]')
o_ax.set_title('Diel Cycle comparison: MERRA-2 CNN v. Ground Monitors')
Because the x-axis represents hours 0–23, set o_ax.set_xticks(np.arange(0, 24)) and optionally use o_ax.set_xticklabels([f'{h:02d}:00' for h in range(24)], rotation=45) for more readable tick labels.

Time Series — Single Grid Cell

A wide-format line plot (figsize=(15, 5)) shows daily PM2.5 over the full study period for one selected grid cell. The figure uses plt.figure() / add_subplot() (not plt.subplots()). Monitor data are plotted in black with the label 'Monitor Average'; MERRA-2 CNN data in red with the label 'MERRA-2 CNN Average':
import matplotlib.pyplot as plt

o_fig = plt.figure(figsize=(15, 5))
o_ax = o_fig.add_subplot()

o_ax.plot(f_data_combined_gridded_daily_at_Location.index,
          f_data_combined_gridded_daily_at_Location['Average'],
          color='black', label='Monitor Average')
o_ax.plot(f_data_combined_gridded_daily_at_Location.index,
          f_data_combined_gridded_daily_at_Location['MERRA2_CNN'],
          color='red', label='MERRA-2 CNN Average')

o_ax.legend()
o_ax.set_ylabel('Daily Average PM$_{2.5}$ [$\mu$g/m$^{3}$]')
o_ax.set_title(f'Location: {s_location}')
If the selected grid cell has no observations (because no monitor falls within 0.25° lat or 0.3125° lon of the target point, or because all hours failed the n_minimum_samples_per_grid filter), f_data_combined_gridded_daily_at_Location will be empty and the plot will be blank. Inspect f_data_combined_gridded_daily[['lat_grid','lon_grid']].drop_duplicates() first to identify populated cells.

Visual Checklist

Use this checklist when reviewing each figure before including it in a report.
  • Colorbar_Limits appropriate for the season and region (not driven by outliers)
  • Point markers visible at the map scale (adjust n_pointsize if needed)
  • Colorbar_Label set with units (e.g., 'PM$_{2.5}$ [µg/m$^{3}$]')
  • Plot_Title includes dataset name, statistic, and time period
  • Domain extent matches [n_lon_min, n_lon_max, n_lat_min, n_lat_max]
  • 1:1 line drawn with o_ax.plot([0,200],[0,200],'k--') for hourly or [0,100] for daily
  • Hue variable (hour-of-day or latitude) shown in legend
  • R² and Bias from F_compute_metrics annotated on figure or reported in caption
  • IQR shading drawn with fill_between before the median line so the line is not obscured
  • X-axis runs 0–23 with readable tick labels
  • Both monitor (color='black') and model (color='red') plotted on the same axes
  • Y-axis starts at 0 unless negative values are present
  • Figure width ≥ 15 inches to avoid crowded x-axis dates
  • Monitor line uses color='black', label 'Monitor Average'
  • MERRA-2 CNN line uses color='red', label 'MERRA-2 CNN Average'
  • X-axis date format legible (use o_fig.autofmt_xdate() if labels overlap)
  • Grid cell or location name stated in the title via s_location

Build docs developers (and LLMs) love