# -----------------------------------------------------------------------------.
# MIT License
# Copyright (c) 2024 GPM-API developers
#
# This file is part of GPM-API.
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------.
"""This module contains plotting utility for SR/GR validation."""
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np
import gpm
from gpm.visualization import plot_colorbar
[docs]
def compare_maps(
gdf,
sr_column,
gr_column,
sr_label=None,
gr_label=None,
sr_title=None,
gr_title=None,
cmap="Spectral_r",
vmin=None,
vmax=None,
grid_color="grey",
grid_linewidth=0.25,
unified_color_scale=True,
shared_colorbar=False,
subplot_kwargs=None,
fig_kwargs=None,
cbar_kwargs=None,
**plot_kwargs,
):
"""
Compare and plot side-by-side maps of SR and GR data from a GeoDataFrame.
Parameters
----------
gdf : geopandas.GeoDataFrame
The GeoDataFrame containing the matched SR/GR data.
sr_column : str
The column name for SR data.
gr_column : str
The column name for GR data.
sr_label : str, optional
Label for the SR sensor colorbar.
gr_label : str, optional
Label for the GR sensor colorbar.
cmap : str, optional
Colormap to use for both plots. The default is "Spectral_r".
vmin : float, optional
Minimum value for color scale.
Defaults to the minimum value across SR and GR data if ``unified_color_scale`` is ``True``.
vmax : float, optional
Maximum value for color scale.
Defaults to the maximum value across SR and GR data if ``unified_color_scale`` is ``True``.
unified_color_scale: str, optional
If True and ``vmin`` or ``vmax`` are not specified,
it automatically set a common vmin/vmax value.
The default is ``True``.
shared_colorbar : bool, optional
If True, a single colorbar is shared across both plots.
grid_color : str, optional
Color of the grid lines. The default is "grey".
grid_linewidth : float, optional
Linewidth of the grid lines. The default is 0.25.
cbar_kwargs : dict, optional
Keyword arguments for customizing the colorbar passed to :py:class:`matplotlib.pyplot.colorbar`.
fig_kwargs : dict, optional
Keyword arguments for customizing the figure passed to :py:class:`matplotlib.pyplot.subplots`.
The default is ``None`` and ``figsize`` is set to (14,7).
subplot_kwargs : dict, optional
Keyword arguments for :py:class:`matplotlib.pyplot.subplots`.
Can include a `projection` key to set a Cartopy CRS.
Returns
-------
None
Displays SR and GR fields side-by-side.
"""
# Create default kwargs dictionary
fig_kwargs = {} if fig_kwargs is None else fig_kwargs
subplot_kwargs = {} if subplot_kwargs is None else subplot_kwargs
cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs
# Define default colorbar label
if sr_label is None:
sr_label = sr_column
if gr_label is None:
gr_label = gr_column
# Default common vmin/vmax if not specified
min_value = np.nanmin([gdf[sr_column].min(), gdf[gr_column].min()])
max_value = np.nanmax([gdf[sr_column].max(), gdf[gr_column].max()])
if unified_color_scale:
if vmax is None:
vmax = max_value
if vmin is None:
vmin = min_value
# Determine the extent for the plots
extent_xy = gdf.total_bounds[[0, 2, 1, 3]]
# Define cbar_kwargs
cbar_kwargs1 = cbar_kwargs.copy()
cbar_kwargs1["label"] = sr_label
cbar_kwargs2 = cbar_kwargs.copy()
cbar_kwargs2["label"] = gr_label
# Create the figure
if "figsize" not in fig_kwargs:
fig_kwargs["figsize"] = (14, 7)
fig, axes = plt.subplots(1, 2, subplot_kw=subplot_kwargs, **fig_kwargs)
# Plot SR data
_ = _plot_gdf_map(
ax=axes[0],
gdf=gdf,
column=sr_column,
title=sr_title,
extent_xy=extent_xy,
# Gridline settings
grid_linewidth=grid_linewidth,
grid_color=grid_color,
# Colorbar settings
add_colorbar=not shared_colorbar,
cbar_kwargs=cbar_kwargs1,
# Plot settings
cmap=cmap,
vmin=vmin,
vmax=vmax,
**plot_kwargs,
)
# Plot GR data
_ = _plot_gdf_map(
ax=axes[1],
gdf=gdf,
column=gr_column,
title=gr_title,
extent_xy=extent_xy,
# Gridline settings
grid_linewidth=grid_linewidth,
grid_color=grid_color,
# Colorbar settings
add_colorbar=True,
cbar_kwargs=cbar_kwargs2,
# Plot settings
cmap=cmap,
vmin=vmin,
vmax=vmax,
**plot_kwargs,
)
return fig
def _plot_gdf_map(gdf, column, extent_xy, title, grid_linewidth, grid_color, add_colorbar, cbar_kwargs, **plot_kwargs):
# Set default title
if title is None:
title = column
# Plot data
p = gdf.plot(
column=column,
legend=False,
**plot_kwargs,
)
p.axes.set_xlim(extent_xy[0:2])
p.axes.set_ylim(extent_xy[2:4])
p.axes.set_title(title)
# Convert x and y axis tick labels to kilometers
x_formatter = mticker.FuncFormatter(lambda x, pos: f"{x/1000:.0f}") # noqa
y_formatter = mticker.FuncFormatter(lambda y, pos: f"{y/1000:.0f}") # noqa
p.axes.xaxis.set_major_formatter(x_formatter)
p.axes.yaxis.set_major_formatter(y_formatter)
# Set axis labels
p.axes.set_xlabel("x (km)", fontsize=12)
p.axes.set_ylabel("y (km)", fontsize=12)
# Set aspect ratio
p.axes.set_aspect("equal")
# Set grid line
p.axes.grid(lw=grid_linewidth, color=grid_color)
# Add colorbar
if add_colorbar:
plot_colorbar(p=p.collections[0], ax=p.axes, cbar_kwargs=cbar_kwargs)
return p
[docs]
def reflectivity_scatterplot(
df,
gr_z_column,
sr_z_column,
hue_column,
ax=None,
gr_range=None,
sr_range=None,
add_colorbar=True,
marker="+",
title="GR / SR Offset",
cbar_kwargs=None,
**plot_kwargs,
):
"""
Plots a scatterplot comparing GR and SR reflectivity columns.
Parameters
----------
df : pandas.DataFrame, geopandas.GeoDataFrame
The dataframe containing the reflectivity data.
gr_z_column : str
The column name for the GR (ground radar) reflectivity data on the x-axis.
sr_z_column : str
The column name for the SR (spaceborne radar) reflectivity data on the y-axis.
hue_column : str
The column name used for color mapping in the scatter plot.
ax : matplotlib.axes.Axes, optional
The axes to plot on. If ``None``, a new figure and axis are created.
gr_range : list, optional
The limits for the x-axis as [min, max]. If ``None``, limits are determined from data.
sr_range : list, optional
The limits for the y-axis as [min, max]. If ``None``, limits are determined from data.
add_colorbar : bool, optional
If True, adds a colorbar to the plot. The default is True.
marker : str, optional
The marker style for the scatter plot. The default is ``"+"``.
title : str, optional
The title of the scatter plot. The default is "GR / SR Offset".
cbar_kwargs : dict, optional
Additional keyword arguments for the colorbar.
**plot_kwargs : dict
Additional keyword arguments passed to ``ax.scatter``.
Returns
-------
matplotlib.collections.PathCollection
The scatter plot object.
"""
cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs
# Initialize plot if necessary
if ax is None:
# Create a new figure and axis
_, ax = plt.subplots()
# Define plot limits if not specified
if gr_range is None:
gr_range = [df[gr_z_column].min() - 2, df[gr_z_column].max() + 2]
if sr_range is None:
sr_range = [df[sr_z_column].min() - 2, df[sr_z_column].max() + 2]
# Retrieve plot_kwargs, cbar_kwargs
plot_kwargs, cbar_kwargs = gpm.get_plot_kwargs(
name=hue_column,
user_plot_kwargs=plot_kwargs,
user_cbar_kwargs=cbar_kwargs,
)
# Display scatterplot
p = ax.scatter(
df[gr_z_column],
df[sr_z_column],
c=df[hue_column],
marker=marker,
**plot_kwargs,
)
# Add colorbar
if add_colorbar:
plot_colorbar(p=p, ax=p.axes, cbar_kwargs=cbar_kwargs)
# Add 1:1 line
ax.plot([-10, 70], [-10, 70], linestyle="solid", color="black")
# Restrict limits
ax.set_xlim(*gr_range)
ax.set_ylim(*sr_range)
# Add labels
ax.set_xlabel("GR reflectivity (dBZ)")
ax.set_ylabel("SR reflectivity (dBZ)")
# Add title
ax.set_title(title)
# Set aspect ratio
p.axes.set_aspect("auto")
return p
[docs]
def reflectivity_scatterplots(
df,
gr_z_column,
sr_z_column,
hue_columns,
ncols=2,
fig_kwargs=None,
gr_range=None,
sr_range=None,
marker="+",
cbar_kwargs=None,
**plot_kwargs,
):
"""
Plots multiple scatter plots comparing GR and SR reflectivity for each specified hue variable.
Parameters
----------
df : pandas.DataFrame
The dataframe containing the reflectivity data.
gr_z_column : str
The column name for the GR (ground radar) reflectivity data on the x-axis.
sr_z_column : str
The column name for the SR (spaceborne radar) reflectivity data on the y-axis.
hue_variables : list of str
A list of column names to be used as hue variables for individual scatter plots.
ncols : int, optional
Number of columns in the subplot grid. The default is 2.
gr_range : list, optional
The limits for the x-axis as [min, max]. If None, limits are determined from data.
sr_range : list, optional
The limits for the y-axis as [min, max]. If None, limits are determined from data.
marker : str, optional
The marker style for the scatter plot. The default is "+".
fig_kwargs : dict, optional
Keyword arguments for customizing the figure passed to :py:class:`matplotlib.pyplot.subplots`.
If ``figsize`` is not specified, defaults to (ncols * 6, nrows * 5).
cbar_kwargs : dict, optional
Additional keyword arguments common to all colorbars.
**plot_kwargs : dict
Additional keyword arguments common to all scatter plot.
Returns
-------
matplotlib.figure.Figure
The figure object containing all the scatter plots.
"""
# Deal with default kwargs
fig_kwargs = {} if fig_kwargs is None else fig_kwargs
cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs
# Deal with case with 1 hue column
if isinstance(hue_columns, list) and len(hue_columns) == 1:
hue_columns = hue_columns[0]
if isinstance(hue_columns, str):
p = reflectivity_scatterplot(
df=df,
gr_z_column=gr_z_column,
sr_z_column=sr_z_column,
hue_column=hue_columns,
ax=plot_kwargs.pop("ax", None),
gr_range=gr_range,
sr_range=sr_range,
marker=marker,
cbar_kwargs=cbar_kwargs,
**plot_kwargs,
)
return p
# Identify number of rows required
nrows = (len(hue_columns) + 1) // ncols # Calculate the number of rows needed
# Specify default fig size if not specified
if "figsize" not in fig_kwargs:
fig_kwargs["figsize"] = (ncols * 6, nrows * 5)
# Define plot limits if not specified
if gr_range is None:
gr_range = [df[gr_z_column].min() - 2, df[gr_z_column].max() + 2]
if sr_range is None:
sr_range = [df[sr_z_column].min() - 2, df[sr_z_column].max() + 2]
# Create a figure and a grid of subplots
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, **fig_kwargs)
axes = axes.flatten() # Flatten axes array to make iteration easier
for i, hue_column in enumerate(hue_columns):
ax = axes[i]
reflectivity_scatterplot(
df=df,
gr_z_column=gr_z_column,
sr_z_column=sr_z_column,
hue_column=hue_column,
ax=ax,
gr_range=gr_range,
sr_range=sr_range,
marker=marker,
title=hue_column,
cbar_kwargs=cbar_kwargs.copy(),
**plot_kwargs.copy(),
)
# Only set x-labels for bottom row and y-labels for left column
if i % ncols != 0:
ax.set_ylabel("")
ax.set_yticks([])
ax.set_yticklabels([])
if i < (nrows - 1) * ncols:
ax.set_xlabel("")
ax.set_xticks([])
ax.set_xticklabels([])
# Turn off axes for any empty subplots (in case the grid is larger than the number of plots)
for j in range(i + 1, len(axes)):
fig.delaxes(axes[j])
return fig
[docs]
def reflectivity_distribution(
df,
gr_z_column,
sr_z_column,
ax=None,
bin_width=2,
):
"""
Plots overlaid histograms of GR and SR reflectivity distributions.
Parameters
----------
df : pandas.DataFrame, geopandas.GeoDataFrame
The dataframe containing the reflectivity data.
gr_z_column : str
The column name for the GR (ground radar) reflectivity data.
sr_z_column : str
The column name for the SR (spaceborne radar) reflectivity data.
ax : matplotlib.axes.Axes, optional
The axes to plot on. If ``None``, a new figure and axis are created.
bin_width : float, optional
The width of the histogram bins. The default is 2 dBZ.
Returns
-------
tuple
The histogram plot objects for GR and SR.
"""
# Initialize plot if necessary
if ax is None:
# Create a new figure and axis
_, ax = plt.subplots()
# Default vmin/vmax
vmin = np.nanmin([df[gr_z_column].min(), df[sr_z_column].min()])
vmax = np.nanmax([df[gr_z_column].max(), df[sr_z_column].max()])
# Plot histograms
p = ax.hist(
df[gr_z_column],
bins=np.arange(vmin, vmax, bin_width),
edgecolor="None",
label="GR",
)
p = ax.hist(
df[sr_z_column],
bins=np.arange(vmin, vmax, bin_width),
edgecolor="red",
facecolor="None",
label="SR",
)
ax.set_xlabel("Reflectivity (dBZ)")
ax.legend()
return p
[docs]
def calibration_summary(
df,
gr_z_column,
sr_z_column,
# Scatterplot options
hue_column,
gr_range=None,
sr_range=None,
marker="+",
cbar_kwargs=None,
# Histogram options
bin_width=2,
**plot_kwargs,
):
"""
Creates a summary plot for comparison between GR and SR reflectivity data.
The function draw a scatter plot and overlaid histograms of GR and SR reflectivity values.
Parameters
----------
df : pandas.DataFrame, geopandas.GeoDataFrame
The dataframe containing the reflectivity data.
gr_z_column : str
The column name for the GR (ground radar) reflectivity data.
sr_z_column : str
The column name for the SR (spaceborne radar) reflectivity data.
hue_column : str
The column name used for color mapping in the scatter plot.
gr_range : list, optional
The limits for the x-axis in the scatter plot as [min, max]. If ``None``, limits are determined from data.
sr_range : list, optional
The limits for the y-axis in the scatter plot as [min, max]. If ``None``, limits are determined from data.
marker : str, optional
The marker style for the scatter plot. The default is ``"+"``.
cbar_kwargs : dict, optional
Additional keyword arguments for the colorbar.
bin_width : float, optional
The width of the histogram bins. The default is 2 dBZ.
**plot_kwargs : dict
Additional keyword arguments passed to the scatter plot.
Returns
-------
matplotlib.figure.Figure
The figure object containing the calibration summary plots.
"""
cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs
# Compute bias
dz = df[gr_z_column] - df[sr_z_column]
# Compute bias statistics
bias = np.nanmean(dz).round(2)
rob_bias = np.nanmedian(dz).round(2)
# - Histograms
fig = plt.figure(figsize=(12, 5))
ax1 = fig.add_subplot(121, aspect="equal")
_ = reflectivity_scatterplot(
df=df,
gr_z_column=gr_z_column,
sr_z_column=sr_z_column,
hue_column=hue_column,
gr_range=gr_range,
sr_range=sr_range,
marker=marker,
ax=ax1,
cbar_kwargs=cbar_kwargs,
**plot_kwargs,
)
ax1.set_title(f"GR-SR (Robust) Bias: ({rob_bias}) {bias} dBZ")
ax2 = fig.add_subplot(122)
_ = reflectivity_distribution(
df=df,
gr_z_column=gr_z_column,
sr_z_column=sr_z_column,
ax=ax2,
bin_width=bin_width,
)
fig.suptitle("GR / SR Calibration Summary")
fig.tight_layout()
return fig