Disclaimer: Kindly be aware that the questions and datasets featured in this tutorial were originally presented by Ryan Abernathy in “An Introduction to Earth and Environmental Data Science”.

Making Maps with Cartopy#

NARR is NCEP’s North American Regional Reanalysis, a widely used product for studying the weather and climate of the continental US. The data is available from NOAA’s Earth System Research Laboratory via OPeNDAP, meaing that xarray can open the data “remotely” without downloading a file.

#scientific computing
import numpy as np
import xarray as xr
from matplotlib import pyplot as plt

#mapping
import cartopy.crs as ccrs
import cartopy as cp

# default params of notebook
plt.rcParams['figure.figsize'] = (12, 6)
%config InlineBackend.figure_format = 'retina'
%xmode Minimal

import warnings
warnings.filterwarnings('ignore')
Exception reporting mode: Minimal

Plotting data from NARR#

NARR is NCEP’s North American Regional Reanalysis, a widely used product for studying the weather and climate of the continental US. The data is available from NOAA’s Earth System Research Laboratory via OPeNDAP, meaing that xarray can open the data “remotely” without downloading a file.

geopential height file:

https://www.esrl.noaa.gov/psd/thredds/dodsC/Datasets/NARR/Dailies/pressure/hgt.201810.nc

precipitation file:

https://www.esrl.noaa.gov/psd/thredds/dodsC/Datasets/NARR/Dailies/monolevel/apcp.2018.nc

Our goal will be to recreate the plot below.

narr_map

Loading the datasets#

geo_url = 'https://www.esrl.noaa.gov/psd/thredds/dodsC/Datasets/NARR/Dailies/pressure/hgt.201810.nc'

geo_potential = xr.open_dataset(geo_url, drop_variables=['time_bnds'] )
geo_potential
<xarray.Dataset>
Dimensions:            (time: 31, level: 29, y: 277, x: 349)
Coordinates:
  * time               (time) datetime64[ns] 2018-10-01 ... 2018-10-31
  * level              (level) float32 1e+03 975.0 950.0 ... 150.0 125.0 100.0
  * y                  (y) float32 0.0 3.246e+04 ... 8.927e+06 8.96e+06
  * x                  (x) float32 0.0 3.246e+04 ... 1.126e+07 1.13e+07
    lat                (y, x) float32 ...
    lon                (y, x) float32 ...
Data variables:
    Lambert_Conformal  int32 ...
    hgt                (time, level, y, x) float32 ...
Attributes: (12/17)
    _NCProperties:                   version=1|netcdflibversion=4.4.1.1|hdf5l...
    Conventions:                     CF-1.2
    centerlat:                       50.0
    centerlon:                       -107.0
    comments:                        
    institution:                     National Centers for Environmental Predi...
    ...                              ...
    title:                           Daily NARR
    history:                         created Sat Mar 26 07:07:59 MDT 2016 by ...
    dataset_title:                   NCEP North American Regional Reanalysis ...
    references:                      https://www.esrl.noaa.gov/psd/data/gridd...
    source:                          http://www.emc.ncep.noaa.gov/mmb/rreanl/...
    DODS_EXTRA.Unlimited_Dimension:  time
prcp_url = 'https://www.esrl.noaa.gov/psd/thredds/dodsC/Datasets/NARR/Dailies/monolevel/apcp.2018.nc'
prcp = xr.open_dataset(prcp_url, drop_variables=['time_bnds'] )
prcp
<xarray.Dataset>
Dimensions:            (time: 365, y: 277, x: 349)
Coordinates:
  * time               (time) datetime64[ns] 2018-01-01 ... 2018-12-31
  * y                  (y) float32 0.0 3.246e+04 ... 8.927e+06 8.96e+06
  * x                  (x) float32 0.0 3.246e+04 ... 1.126e+07 1.13e+07
    lat                (y, x) float32 ...
    lon                (y, x) float32 ...
Data variables:
    Lambert_Conformal  int32 ...
    apcp               (time, y, x) float32 ...
Attributes: (12/17)
    _NCProperties:                   version=1|netcdflibversion=4.4.1.1|hdf5l...
    Conventions:                     CF-1.2
    centerlat:                       50.0
    centerlon:                       -107.0
    comments:                        
    institution:                     National Centers for Environmental Predi...
    ...                              ...
    title:                           Daily NARR
    history:                         created Sat Mar 26 04:56:06 MDT 2016 by ...
    dataset_title:                   NCEP North American Regional Reanalysis ...
    references:                      https://www.esrl.noaa.gov/psd/data/gridd...
    source:                          http://www.emc.ncep.noaa.gov/mmb/rreanl/...
    DODS_EXTRA.Unlimited_Dimension:  time

With the datasets loaded, we can now use the sel() function to slice the dataset at the specified time.

geo_potential_slice = geo_potential.sel(time='2018-10-15', level=500).hgt

prcp_slice_total = prcp.sel(time='2018-10-15').apcp

Let’s start the preparation to construct our map. Note the Lambert_Conformal within our variables. This variable defines the type of projection we’ll utilize and the necessary parameters for its construction. This specific projection requires values for central latitude and longitude, as well as parameters for false easting and northing. I highly recommend reviewing cartopy projections here.

#map setup
cen_lon = prcp.Lambert_Conformal.attrs.get('longitude_of_central_meridian')
cen_lat = prcp.Lambert_Conformal.attrs.get('latitude_of_projection_origin')

false_easting = prcp.Lambert_Conformal.attrs.get('false_easting')
false_northing = prcp.Lambert_Conformal.attrs.get('false_northing')

lambert_conformal = ccrs.LambertConformal(central_longitude=cen_lon, 
                                          central_latitude=cen_lat, 
                                          false_easting = false_easting,
                                          false_northing= false_northing)

With the Lambert Conformal projection parameters set, we’re ready to construct the plot. With a cartopy project set, we get a GeoAxes object for plotting. We can then define the map’s extent by using set_extent([min_lon, max_lon, min_lat, max_lat]).

plt.figure(figsize=(14,6))

ax = plt.axes(projection=lambert_conformal)
ax.set_extent([-122,-75, 20,50])


# Plot states & borders
ax.add_feature(cp.feature.STATES, edgecolor='black', linewidth=0.5)
ax.add_feature(cp.feature.BORDERS, edgecolor='black', linewidth=1)
ax.coastlines()

#contour set up
levels=np.arange(5000,5900,50)
geo_contour = geo_potential_slice.plot.contour(ax=ax, 
                                               transform=lambert_conformal,
                                               levels=levels,
                                               colors='m')

ax.clabel(geo_contour, inline=True, fmt='%d m', colors='r')

#pcolormesh set up
cbar_kwargs= {'shrink': 0.4,
              'label':'Daily accumulated total\nprecipation at Surface\n[kg/m^2]'
             }
prcp_mesh = prcp_slice_total.plot.pcolormesh(ax=ax, 
                                              transform=lambert_conformal,
                                              cmap='Blues',vmax=50,
                                              cbar_kwargs=cbar_kwargs
                                             )


ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=1, color='gray',alpha=0.5, linestyle='-')


plt.show()
../../../_images/766b183b78650d95d54e63a336f3b80a6a4619d824122d916903bc2b76e6342b.png

Code Explanation#

  1. We define the figure area, setting the projection and extent of coverage:

plt.figure(figsize=(14, 6))
ax = plt.axes(projection=lambert_conformal)
ax.set_extent([-122, -75, 20, 50])
  1. Having prepared the figure, we add features to the map. We utilize cartopy.feature to incorporate states and borders onto the map, and coastlines to outline bodies of water.

ax.add_feature(cp.feature.STATES, edgecolor='black', linewidth=0.5)
ax.add_feature(cp.feature.BORDERS, edgecolor='black', linewidth=1)
ax.coastlines()
  1. Let’s now plot data on our map. First, we set up the contours. Pay attention to the range of geopotential height values. Then, we proceed to plot the precipitation. Here, we create a dictionary for customizations for the color bar and supplying it to the plot.pcolormesh function.

# Contour setup
levels = np.arange(5000, 5900, 50)
geo_contour = geo_potential_slice.plot.contour(ax=ax,
                                               transform=lambert_conformal,
                                               levels=levels,
                                               colors='m')

ax.clabel(geo_contour, inline=True, fmt='%d m', colors='r')

# Pcolormesh setup
cbar_kwargs = {'shrink': 0.4,
               'label': 'Daily accumulated total\nprecipitation at Surface\n[kg/m^2]'
              }
prcp_mesh = prcp_slice_total.plot.pcolormesh(ax=ax,
                                             transform=lambert_conformal,
                                             cmap='Blues', vmax=50,
                                             cbar_kwargs=cbar_kwargs
                                            )
  1. Lastly, we set up the gridlines. Note that only ccrs.PlateCaree() is available for this function.

ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=1, color='gray', alpha=0.5, linestyle='-')
plt.show()



Antarctic Sea Ice#

Download this file and then use it to plot the concentration of Antarctic Sea Ice on Aug. 7, 2017. Again, you will need to explore the file contents in order to determine the correct projection.

sea_ice_map

import xarray as xr
import pooch
url = 'https://polarwatch.noaa.gov/erddap/files/nsidcCDRiceSQsh1day/2017/seaice_conc_daily_sh_f17_20170807_v03r01.nc'
fname = pooch.retrieve(url, known_hash='19b74e7e97f1c0786da0c674c4d5e4af0da5b32e2fe8c66a8f1a8a9a1241e73c')
ds_ice = xr.open_dataset(fname, drop_variables='melt_onset_day_seaice_conc_cdr')
ds_ice
<xarray.Dataset>
Dimensions:                     (time: 1, ygrid: 332, xgrid: 316)
Coordinates:
  * time                        (time) datetime64[ns] 2017-08-07T12:00:00
  * ygrid                       (ygrid) float32 4.338e+06 ... -3.938e+06
  * xgrid                       (xgrid) float32 -3.938e+06 ... 3.938e+06
    latitude                    (ygrid, xgrid) float64 ...
    longitude                   (ygrid, xgrid) float64 ...
Data variables:
    projection                  |S1 ...
    seaice_conc_cdr             (time, ygrid, xgrid) float32 ...
    stdev_of_seaice_conc_cdr    (time, ygrid, xgrid) float32 ...
    qa_of_seaice_conc_cdr       (time, ygrid, xgrid) float32 ...
    goddard_merged_seaice_conc  (time, ygrid, xgrid) float32 ...
    goddard_nt_seaice_conc      (time, ygrid, xgrid) float32 ...
    goddard_bt_seaice_conc      (time, ygrid, xgrid) float32 ...
Attributes: (12/70)
    references:                             Comiso, J. C., and F. Nishio. 200...
    program:                                NOAA Climate Data Record Program
    cdr_variable:                           seaice_conc_cdr
    software_version_id:                    git@bitbucket.org:nsidc/seaice_cd...
    Metadata_Link:                          https://nsidc.org/api/dataset/met...
    product_version:                        v03r01
    ...                                     ...
    scaling_factor:                         1.0
    false_easting:                          0.0
    false_northing:                         0.0
    semimajor_radius:                       6378273.0
    semiminor_radius:                       6356889.449
    proj_units:                             meters

Let’s take a look at the metadata

#loop through the variables
for variable in ds_ice.variables:
    #get the long name from attributes
    long_name = ds_ice[variable].attrs.get("long_name",'')
    print(f"{variable} -- {long_name}.")
    print()
projection -- .

seaice_conc_cdr -- NOAA/NSIDC Climate Data Record of Passive Microwave Daily Southern Hemisphere Sea Ice Concentration.

stdev_of_seaice_conc_cdr -- Passive Microwave Daily Southern Hemisphere Sea Ice Concentration Source Estimated Standard Deviation.

qa_of_seaice_conc_cdr -- Passive Microwave Daily Southern Hemisphere Sea Ice Concentration QC flags.

goddard_merged_seaice_conc -- Goddard Edited Climate Data Record of Passive Microwave Daily Southern Hemisphere Sea Ice Concentration, Goddard Edited.

goddard_nt_seaice_conc -- Passive Microwave Daily Southern Hemisphere Sea Ice Concentration by NASA Team algorithm with Goddard QC.

goddard_bt_seaice_conc -- Passive Microwave Daily Southern Hemisphere Sea Ice Concentration by Bootstrap algorithm with Goddard QC.

time -- Centered Time.

ygrid -- projection_grid_y_centers.

xgrid -- projection_grid_x_centers.

latitude -- latitude.

longitude -- longitude.

After printing the long_name, we’ve gained insight into the metadata for with each variable. Let’s proceed to build our plot.

It’s important to note that the landmass of Antarctica will be displayed in white within the plot.

plt.figure(figsize=(14, 6))
ax = plt.axes(projection=ccrs.SouthPolarStereo())


ax.add_feature(cp.feature.LAND.with_scale('10m'), facecolor='white', zorder=2)
ax.coastlines(zorder=3)
ds_ice.seaice_conc_cdr.sel(time='2017-08-07').plot(ax=ax,
                                                    vmax=1.0,
                                                    transform=ccrs.SouthPolarStereo(), 
                                                    cbar_kwargs={'shrink':0.4},
                                                    zorder=1)

ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=1, color='gray',alpha=0.5, linestyle='-')

plt.show()
../../../_images/1d3c87618e43812050cb546b7bf850733f36d23042b7781e7822acefe1632f71.png

The code is relatively straightforward, but there are two concepts that may be unfamiliar.

In Matplotlib, each element on your figure has a default zorder, indicating the stacking order on the plot. In creating this figure, the sea ice data includes information about ice coverage on the Antarctic landmass. To ensure that the landmass masks this data, we need to stack it above the sea ice data. Adjusting the zorder argument accomplishes this. You can experiment with different zorder values for a better understanding. Explore more about zorder.

We also introduce an additional function in Cartopy, cartopy.feature.LAND, specifically with_scale(), which allows you to control the scale at which the landmass is rendered in your plot. Larger scale values result in a less detailed representation of the landmass area.

Global USGS Earthquakes#

Given our current knowledge, let’s enhance the earthquake plot introduced in the Pandas tutorial.

earthquake_map

import pandas as pd
url = 'http://www.ldeo.columbia.edu/~rpa/usgs_earthquakes_2014.csv'
earthquakes = pd.read_csv(url, parse_dates=['time','updated'], index_col='id')
earthquakes.head()
time latitude longitude depth mag magType nst gap dmin rms net updated place type
id
ak11155107 2014-01-31 23:53:37.000 60.252000 -152.7081 90.20 1.10 ml NaN NaN NaN 0.2900 ak 2014-02-05 19:34:41.515000+00:00 26km S of Redoubt Volcano, Alaska earthquake
nn00436847 2014-01-31 23:48:35.452 37.070300 -115.1309 0.00 1.33 ml 4.0 171.43 0.34200 0.0247 nn 2014-02-01 01:35:09+00:00 32km S of Alamo, Nevada earthquake
ak11151142 2014-01-31 23:47:24.000 64.671700 -149.2528 7.10 1.30 ml NaN NaN NaN 1.0000 ak 2014-02-01 00:03:53.010000+00:00 12km NNW of North Nenana, Alaska earthquake
ak11151135 2014-01-31 23:30:54.000 63.188700 -148.9575 96.50 0.80 ml NaN NaN NaN 1.0700 ak 2014-01-31 23:41:25.007000+00:00 22km S of Cantwell, Alaska earthquake
ci37171541 2014-01-31 23:30:52.210 32.616833 -115.6925 10.59 1.34 ml 6.0 285.00 0.04321 0.2000 ci 2014-02-01 00:13:20.107000+00:00 10km WNW of Progreso, Mexico earthquake
plt.figure(figsize=(14, 6))

top_50_quakes = earthquakes.nlargest(50, 'mag')

ax = plt.axes(projection=ccrs.Robinson(central_longitude=180))

ax.stock_img()
ax.coastlines()

scatter = ax.scatter(top_50_quakes.longitude,
           top_50_quakes.latitude,
           c=top_50_quakes.mag,
           zorder=2,
           s=20,
           transform = ccrs.PlateCarree(),
           cmap='YlOrBr')

cbar = plt.colorbar(scatter, ax=ax, orientation='vertical', label='magnitude', shrink=0.6)

plt.show()
../../../_images/2b137aeb27dbe09f95b143b57ebf478956114a4c926dbaf36b1ea74e331d8866.png

You might have observed the line ax.stock_img() in the code block, this command triggers the default cartopy background.

Final Thoughts#

This tutorial aimed to offer a fundamental understanding of creating plots using cartopy and matplotlib. I highly recommend delving into the documentation. Next, we’ll dive into working with Global Circulation Models.