You are on page 1of 50

Python - Creating Plots 189

respectively. The marker colours (c keyword) are set by the correlation coefficients
saved in corr. The marker size values are passed to the s keyword via the msize array
calculated earlier in line 24. The colour table to be used is defined by the cmap and norm
keywords. The edgecolor used for drawing the makers is set to none and the marker
symbol (marker keyword) is set to be a square (s).
The map plot is further configured by formatting grid lines, x and y axis labels in
line 48 to 57.
A vertical colour bar is added in line 60 to 63. The colour bar is linked to the scatter
plot via the handle scat which is passed to the fig.colorbar() function.
In order to add a legend that shows what the marker size refers to five ‘non-existing’
black marker are plotted, one for each marker size (line 66 to 68). Labels are defined
for each of the five markers based on the number of observations bins saved in the
nobsbins variable. A legend is then placed in the lower left corner of the plot using
the ax.legend() function in line 69.

In newer versions of Matplotlib the legend creation for the scenario


described here can be done using PathCollections whereby two legends
are added to the plot, one for marker size and one for marker colour (see
Matplotlib v3.1.2 Scatter plots with a legend example¹⁶).

The plot is optimised, saved and closed in lines 72, 75 and 78, respectively.
¹⁶https://matplotlib.org/3.1.1/gallery/lines_bars_and_markers/scatter_with_legend.html
Python - Creating Plots 190

Figure 8.6.3.1: Using the plt.scatter() function function to plot markers on a map. The plot shows
Pearson correlation coefficients for ERAI versus observed 2m air temperature (T2m) at SYNOP
reporting stations with at least 100 records for 12 UTC for JJA. The marker size corresponds to the
range of available observations at the stations (see legend).

The 2m air temperature correlation coefficients (observed with ERA5) plotted for
African stations in Figure 8.6.3.1 show good correlations of mainly above 0.8 for
stations located north of the Sahel and south of about 10°S. ERA5 struggles to capture
the variability in 2m air temperature in the tropical regions, along coastlines, larger
rivers (see Nile) and in mountain regions.

8.4 Map Plots


Plotting data on maps is a major part of climate data visualisation. It is frequently
used to show spatial variability and other mappable features. The main Python
package used for creating map plots is Cartopy¹⁷. Cartopy has been employed here in
¹⁷https://scitools.org.uk/cartopy/docs/latest/
Python - Creating Plots 191

favour of the Basemap¹⁸ package as maintenance and support of the latter will end
in 2020 (see Cartopy, New Management, and EoL Announcement¹⁹ for more details).

The old map-plotting package Basemap is not covered in this book as it


nears the end of its life in 2020.

The Cartopy package is introduced in Section 7.x.x, including a brief discussion of


map projections and data transformations, before demonstrating example map plots
from Section 7.7.2 onwards.

8.4.1 Cartopy Map Projections and Data Transformation


Cartopy is the main Python package used to create map plots. However, the package
itself does not actually plot any data. It is used mainly to set up the map projection
and to control other map properties such as extent, colour of oceans or land,
background images, continent and country boundaries and inland water bodies such
as lakes and rivers. Plotting data on a map is not as trivial as it sounds but Cartopy
makes it fairly straightforward.
Many map projections have been created over the centuries. There is no perfect
projection as they all have their advantages and weaknesses. Choosing a projection
depends a lot on the data to be plotted, geographical domain as well as personal
preference. Cartopy currently supports 33 map projections but more may be added
in the future. A full list of map projections can be found on the Cartopy projections
webpage²⁰.
The following Code 8.7.1.1 plots long-term mean sea surface temperature data on a
selection of different projections as shown in Figure 8.7.1.1.

¹⁸https://matplotlib.org/basemap/
¹⁹https://matplotlib.org/basemap/users/intro.html#deprecation-notice
²⁰https://scitools.org.uk/cartopy/docs/latest/crs/projections.html
Python - Creating Plots 192

Code 8.7.1.1:Examples of the some of the more popular map projections in Cartopy.

1 import numpy as np
2 import matplotlib.pyplot as plt
3 from netCDF4 import Dataset
4 import cartopy.crs as ccrs
5 from cartopy import feature
6
7 # reading in netCDF file
8 f = Dataset('../data/HadISSTv1.1_ltm_sst_1980_2015.nc', mode='r')
9 lons = f.variables['longitude'][:]
10 lats = f.variables['latitude'][:]
11 fld = f.variables['sst'][0,:,:]
12 f.close()
13
14 # create 2D fields of lons and lats
15 [lons2d, lats2d] = np.meshgrid(lons, lats)
16
17 # define contour levels
18 levels = np.arange(0, 26, 1)
19
20 # set up figure and map projection
21 fig = plt.figure(figsize=(3.98, 5.5))
22
23 # PlateCarree (equidistant cylindrical projection)
24 ax1 = plt.subplot(4, 2, 1,
25 projection=ccrs.PlateCarree(central_longitude=180))
26 ax1.set_extent([-180, 180, -90, 90], crs=None)
27 ax1.coastlines(resolution='110m', linewidth=0.5)
28 ax1.gridlines(linewidth=0.5, color='black', alpha=0.5, linestyle=':')
29 ax1.set_title('PlateCaree', fontsize=6)
30 ax1.contourf(lons2d, lats2d, fld, levels, transform=ccrs.PlateCarree(),
31 cmap=plt.cm.viridis, extend='both')
32
33 # Robinson
34 ax2 = plt.subplot(4, 2, 2, projection=ccrs.Robinson())
35 ax2.coastlines(resolution='110m', linewidth=0.5)
36 ax2.gridlines(linewidth=0.5, color='black', alpha=0.5, linestyle=':')
37 ax2.set_title('Robinson', fontsize=6)
38 ax2.contourf(lons2d, lats2d, fld, levels, transform=ccrs.PlateCarree(),
Python - Creating Plots 193

39 cmap=plt.cm.viridis, extend='both')
40
41 # Mollweide
42 ax3 = plt.subplot(4, 2, 3, projection=ccrs.Mollweide())
43 ax3.coastlines(resolution='110m', linewidth=0.5)
44 ax3.gridlines(linewidth=0.5, color='black', alpha=0.5, linestyle=':')
45 ax3.set_title('Mollweide', fontsize=6)
46 ax3.contourf(lons2d, lats2d, fld, levels, transform=ccrs.PlateCarree(),
47 cmap=plt.cm.viridis, extend='both')
48
49 # LambertConformal
50 ax4 = plt.subplot(4, 2, 4, projection=ccrs.LambertConformal())
51 ax4.coastlines(resolution='110m', linewidth=0.5)
52 ax4.gridlines(linewidth=0.5, color='black', alpha=0.5, linestyle=':')
53 ax4.set_title('LambertConformal', fontsize=6)
54 ax4.contourf(lons2d, lats2d, fld, levels, transform=ccrs.PlateCarree(),
55 cmap=plt.cm.viridis, extend='both')
56
57 # NearsidePerspective
58 ax5 = plt.subplot(4, 2, 5,
59 projection=ccrs.NearsidePerspective(
60 satellite_height=2500000.0,
61 central_longitude=-1.253741,
62 central_latitude=51.758845))
63 ax5.coastlines(resolution='110m', linewidth=0.5)
64 ax5.add_feature(feature.BORDERS, linestyle='-', linewidth=0.25)
65 ax5.gridlines(linewidth=0.5, color='black', alpha=0.5, linestyle=':')
66 ax5.set_title('NearsidePerspective', fontsize=6)
67 ax5.contourf(lons2d, lats2d, fld, levels, transform=ccrs.PlateCarree(),
68 cmap=plt.cm.viridis, extend='both')
69
70 # InterruptedGoodeHomolosine
71 ax6 = plt.subplot(4, 2, 6, projection=ccrs.InterruptedGoodeHomolosine())
72 ax6.coastlines(resolution='110m', linewidth=0.5)
73 ax6.gridlines(linewidth=0.5, color='black', alpha=0.5, linestyle=':')
74 ax6.set_title('InterruptedGoodeHomolosine', fontsize=6)
75 ax6.contourf(lons2d, lats2d, fld, levels, transform=ccrs.PlateCarree(),
76 cmap=plt.cm.viridis, extend='both')
77
78 # RotatedPole
Python - Creating Plots 194

79 ax7 = plt.subplot(4, 2, 7, projection=ccrs.RotatedPole(pole_latitude=37.5,


80 pole_longitude=177.5))
81 ax7.coastlines(resolution='110m', linewidth=0.5)
82 ax7.gridlines(linewidth=0.5, color='black', alpha=0.5, linestyle=':')
83 ax7.set_title('RotatedPole', fontsize=6)
84 ax7.contourf(lons2d, lats2d, fld, levels, transform=ccrs.PlateCarree(),
85 cmap=plt.cm.viridis, extend='both')
86
87 # Orthographic
88 ax8 = plt.subplot(4, 2, 8, projection=ccrs.Orthographic())
89 ax8.coastlines(resolution='110m', linewidth=0.5)
90 ax8.gridlines(linewidth=0.5, color='black', alpha=0.5, linestyle=':')
91 ax8.set_title('Orthographic', fontsize=6)
92 myplot = ax8.contourf(lons2d, lats2d, fld, levels, transform=ccrs.PlateCarree(),
93 cmap=plt.cm.viridis, extend='both')
94
95 # add colorbar
96 plt.subplots_adjust(left=0.05, bottom=0.1, right=0.95, top=0.975)
97 cbaxes = fig.add_axes([0.2, 0.05, 0.6, 0.02])
98 cbar = plt.colorbar(myplot, orientation='horizontal', cax = cbaxes, pad=0)
99 cbar.set_label('sea surface temperature [C]', rotation=0, fontsize=5)
100 cbar.ax.tick_params(labelsize=5, length=0)
101
102 # save plot to png file
103 plt.savefig('../images/7_python_cartopy_projections_300dpi.png',
104 orientation='portrait', format='png', dpi=300)
105
106 # close file
107 plt.close()

First the coordinate reference system function is imported from the cartopy package
in line 4 and given the standard alias ccrs.
The map projection is defined as part of the figure axis instance creation using
the projection keyword. For instance, the map projection is set to Robinson for
the axis ax2 in line 34 (projection=ccrs.Robinson()) or to Mollweide in line 42
(projection=ccrs.Mollweide()).
In some cases, additional arguments that control certain aspects of the projection
such as extent, viewing angle or viewing distance can be added to the map projection
Python - Creating Plots 195

call. For instance, for the NearsidePerspective projection which assumes a viewpoint
somewhere above earth the satellite_height, central_longitude and central_latitude
are set to 2500000.0, -1.253741 and 51.758845, respectively, thereby controlling the
view of the mapped area (lines 59 to 62). Which arguments are accepted depends on
the map projection and should be checked in the Cartopy map projection list²¹.
After the axis has been created with a specific map projection the appearance of the
map can be configured. Here, coastlines, grid lines and a plot title are added (for
instance, lines 27 to 29). More examples for adding map features can be found in
the following sections as well as in the Cartopy Gallery²². See Figure 8.7.1.1 for an
overview of a selection of map projections.
²¹https://scitools.org.uk/cartopy/docs/latest/crs/projections.html
²²https://scitools.org.uk/cartopy/docs/latest/gallery/
Python - Creating Plots 196

Figure 8.7.1.1: Examples of the some of the more popular Cartopy map projections used in climate
science showing long-term mean HadISST (v1.1) SSTs.

The most difficult part when plotting data on a map is understanding the difference
between the map projection as discussed above and the projection in which the data
have been made available. They may or may not differ.
Python - Creating Plots 197

Global climate dataset are most commonly made available on a regular grid wherein
all data points are at an equal distance in the longitude and latitude direction with
the meridians having been mapped to straight vertical (north-south) lines (see also
Section 4.2 on gridded datasets). Within Cartopy this projection is referred to as the
Plate Carrée projection (PlateCarree()).

Most global climate datasets are being made available on an equirectangu-


lar grid which in Cartopy is defined as the Plate Carrée projection. When
plotting data the projection of the data has to be passed via the transform
keyword (as in transform=ccrs.PlateCarree()) to the plotting function.

If the data are on a different projection than the map then they have to be transformed
so that the data points are projected correctly onto the map projection. The projection
of the data is passed to the plotting routine via the transform keyword.
The SST data plotted in the above example are on a regular grid. Therefore, the
transform keyword in Code 8.7.1.1 has been set to ccrs.PlateCarree() for all map.
For ax1 the transform=ccrs.PlateCarree() keyword is not strictly required (line 30)
because the data projection matches the map projection. However, for all subsequent
panels (ax2 to ax8) the map projection is different from the projection of the data
and, therefore, the data have to be transformed.

If no keyword is supplied to the data plotting function (e.g.,


transform
ax.contourf())then Cartopy assumes that the data projection matches the
map projection.

Understanding the difference between map projection and projection of the data to
be plotted is important. A more detailed discussion and examples can be found on
the Cartopy webpage²³.

8.4.2 Simple Map of SST Anomalies


The code example below (Code 8.7.2.1) demonstrates how to plot a map of SST
anomalies over the Indian Ocean domain Figure 8.7.2.1. HadISST v1.1 SST data were
used to calculated the November 1979 anomaly compared to the 1980-2010 November
mean.
²³https://scitools.org.uk/cartopy/docs/latest/tutorials/understanding_transform.html
Python - Creating Plots 198

Code 8.7.2.1: Plotting Indian Ocean Dipole with Blue Marble background image.

1 import numpy as np
2 import matplotlib.pyplot as plt
3 import matplotlib.ticker as mticker
4 import cartopy.crs as ccrs
5 from netCDF4 import Dataset
6 from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
7
8 # reading in netCDF file
9 ifile = '../data/HadISST_sst_Nov1997_anom.nc'
10 f = Dataset(ifile, mode='r')
11 lons = f.variables['longitude'][:]
12 lats = f.variables['latitude'][:]
13 field = f.variables['sst'][0,:,:]
14 f.close()
15
16 # create 2D fields of lons and lats
17 [lons2d, lats2d] = np.meshgrid(lons, lats)
18
19 # set up figure and map projection
20 fig, ax = plt.subplots(figsize=(5.5, 3.98),
21 subplot_kw={'projection':ccrs.PlateCarree()})
22
23 # define contour levels
24 levels = np.linspace(-2, 2, 17)
25
26 # contour data
27 mymap = ax.contourf(lons2d, lats2d, field, levels, transform=ccrs.PlateCarree(),
28 cmap=plt.cm.RdBu_r, extend='both')
29 ax.stock_img()
30 ax.coastlines()
31 ax.set_extent([29.99, 120.01, -30.01, 30.01], crs=ccrs.PlateCarree())
32
33 # format gridlines and labels
34 gl = ax.gridlines(draw_labels=True, linewidth=0.5, color='black', alpha=0.5,
35 linestyle=':')
36 gl.xlabels_top = False
37 gl.xlocator = mticker.FixedLocator(np.arange(-180,180,30))
38 gl.xformatter = LONGITUDE_FORMATTER
Python - Creating Plots 199

39 gl.xlabel_style = {'size':7, 'color':'black'}


40 gl.ylabels_right = False
41 gl.ylocator = mticker.FixedLocator(np.arange(-90,90,30))
42 gl.yformatter = LATITUDE_FORMATTER
43 gl.ylabel_style = {'size':7, 'color':'black'}
44
45 # add colorbar
46 cbar = plt.colorbar(mymap, orientation='horizontal', shrink=0.7, pad=0.1)
47 cbar.set_label('SST [C]', rotation=0, fontsize=10)
48 cbar.ax.tick_params(labelsize=7, length=0)
49
50 # add title
51 ax.set_title('HadISST SST Nov 1997 anomaly (1980-2010)', fontsize=12)
52
53 # make plot look nice
54 plt.tight_layout()
55
56 # save figure to file
57 plt.savefig('../images/7_IOD_plot_300dpi.png', orientation='portrait',
58 format='png', dpi=300)
59 plt.close()

All packages and functions used in the script are imported in lines 1 to 6. The pre-
calculated SST anomalies are read in from netCDF file in lines 9 to 14 saving the
anomaly field in the variable field. The longitude (lons) and latitude (lats) variables
are converted to 2D grids using the np.meshgrid() function in line 17.
In line 20, the plot figure (figure) and axis (ax) are set up. Note that the map projection
(projection':ccrs.PlateCarree()) is passed to the plt.subplots() function via the
subplot_kw keyword.

Contour levels ranging from -2 to 2 in steps of 0.25 are defined in line 24 using the
np.linspace()function.
Filled contours of the SST anomalies are plotted in lines 27 and 28. As the data are on a
regular lat/lon grid the data projection ccrs.PlateCarree() is passed to ax.contourf()
via the transform keyword. The colour map red to blue reversed (plt.cm.RdBu_r) is
used and the colour scale is extended at both ends for lower and higher values
(extend='both').
Python - Creating Plots 200

The map is customised in lines 29 and 30 by adding the standard background image
and coastlines. The plot domain is set for the Indian Ocean in line 31. Note that the
coordinate reference system is set to crs=ccrs.PlateCarree() to make sure the values
presented to the ax.set_extent() function are projected correctly in line with the map
projection defined in line 27.
The map is further customised in lines 34 to 43. Black (color='black') semi-transparent
(alpha=0.5) dotted (linestyle=':') grid lines are added in lines 34 and 35 and axis
labels are switched on (draw_labels=True). Labels are switched off for the top and
right of the map in lines 36 and 40, respectively. The plotting of major ticks and
grid line positions is handled in lines 37 and 38 for meridians and in lines 42 and 42
for parallels. Label properties are set in lines 39 and 43 for x-axis and y-axis labels,
respectively.

The functions ax.set_xlabel() and ax.set_ylabel() are currently not sup-


ported in cartopy map projections.

A horizontal colour bar is added and customised in lines 46 to 48. Note that the handle
mymap created in line 27 has to be passed to the plt.colorbar() function in line 46. The
new handle cbar is then used to customise colour bar properties.
After adding a plot title in line 51 the map plot layout is optimised (line 54) and the
figure is saved to a file (lines 57 and 58) and closed (line 59).
Python - Creating Plots 201

Figure 8.7.2.1: Indian Ocean Diopol (IOD) November 1997 SST anomalies (reference period 1980-
2010) calculated from HadISST observed SSTs.

Figure 8.7.2.1 shows the Indian Ocean Dipole (IOD²⁴) as represented with November
1997 SST anomalies. In November 1997, the IOD was in an extreme positive (negative)
state with above (below) average SSTs in the western (eastern) Pacific.

8.4.3 Map with Stipples for Statistical Significance


The following Code 8.7.3.1 plots a global map of correlation coefficients between
the monthly mean HadISST v1.1 SST time series for the NINO3.4 domain and
monthly mean GPCP precipitation for each grid box. The correlation coefficients are
calculated as part of the script including a test for significance. Regions on the map
that show a statistically significant correlation are identifiable by stipples (Figure
8.7.3.1).

²⁴https://en.wikipedia.org/wiki/Indian_Ocean_Dipole
Python - Creating Plots 202

Code 8.7.3.1: Plotting NINO3.4 SST spatial correlation.

1 import numpy as np
2 from scipy import stats
3 import matplotlib.pyplot as plt
4 import matplotlib.ticker as mticker
5 import cartopy.crs as ccrs
6 from netCDF4 import Dataset
7 from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
8 from cartopy import feature
9
10 # reading in NINO3.4 SST timeseries
11 ifile = '../data/HadISSTv1.1_nino34_mm_sst_ts_1980_2015.nc'
12 f = Dataset(ifile, mode='r')
13 sst = f.variables['sst'][:,0,0]
14 f.close()
15
16 # reading in GCPC precipitation
17 ifile = '../data/precip_mm_1980_2015_anom.nc'
18 f = Dataset(ifile, mode='r')
19 lons = f.variables['lon'][:]
20 lats = f.variables['lat'][:]
21 pr = f.variables['precip'][:,:,:]
22 f.close()
23
24 # calculate correlation coefficients
25 rfield = np.empty([len(lats), len(lons)])
26 pfield = np.empty([len(lats), len(lons)])
27 for y in range(len(lats)):
28 for x in range(len(lons)):
29 r, p = stats.pearsonr(sst, pr[:,y,x])
30 rfield[y,x] = r
31 pfield[y,x] = p
32 print('r range:', rfield.min(), rfield.max())
33 print('p range:', pfield.min(), pfield.max())
34
35 # create 2D fields of lons and lats
36 [lons2d, lats2d] = np.meshgrid(lons, lats)
37
38 # set up figure and map projection
Python - Creating Plots 203

39 fig, ax = plt.subplots(figsize=(5.5, 3.98),


40 subplot_kw={
41 'projection':ccrs.Robinson(central_longitude=180)})
42
43 # define contour levels
44 levels = np.linspace(-0.7, 0.7, 15)
45
46 # contour data
47 mymap = ax.contourf(lons2d, lats2d, rfield, levels,
48 transform=ccrs.PlateCarree(), cmap=plt.cm.PiYG,
49 extend='neither')
50 ax.coastlines()
51
52 # add colorbar
53 cbar = plt.colorbar(mymap, orientation='horizontal', shrink=0.7, pad=0.1)
54 cbar.set_label('Pearson\'s r', rotation=0, fontsize=10)
55 cbar.ax.tick_params(labelsize=7, length=0)
56
57 # add stippling for statistically significant domains
58 pfieldm = np.ma.masked_greater(pfield, 0.05)
59 ax.contourf(lons2d, lats2d, pfieldm, transform=ccrs.PlateCarree(),
60 hatches=["..."], alpha=0.0)
61
62 # plot NINO3.4 domain
63 ax.plot([-170, -120, -120, -170, -170], [-5, -5, 5, 5, -5], color='red',
64 transform=ccrs.PlateCarree())
65
66 # add title
67 ax.set_title(
68 'Monthly mean NINO3.4 SST and precip\n anomaly correlation (1980-2015)',
69 fontsize=10)
70
71 # make plot look nice
72 plt.tight_layout(pad=2.5)
73
74 # save figure to file
75 plt.savefig('../images/7_python_correlation_stippled_plot_300dpi.png',
76 format='png', dpi=300)
77 plt.close()
Python - Creating Plots 204

All packages and functions needed are imported in lines 1 to 8.


First, a pre-calculated monthly mean time series of SSTs averaged over the NINO3.4
region (see red box in Figure 8.7.3.1) is read in in lines 11 to 14 and saved in the
variable sst. The variable sst is a one-dimensional NumPy array with 433 elements
covering the period 1980 to 2015 with each element representing one month.
Second, monthly mean precipitation values are read in in lines 17 to 22 and saved in
the variable pr. The variable pr is a three-dimensional ([432, 72, 144]) NumPy array
with the dimensions [time, lat, lon]. Both sst and pr have the same number of time
steps.
Correlation coefficients and associated p-values are calculated in lines 25 to 31 in
the following way. First, two empty two-dimensional NumPy arrays with the same
longitude/latitude dimension as the precipitation field pr are created in lines 25 and
26. Next, a nested loop is created in lines 27 and 28 that loops through each grid box of
the precipitation field. For each grid box the correlation coefficient (Pearson’s r) and
associated p-value p are calculated between the SST time series and the precipitation
time series for the current grid box using the stats.personr() function in line 29.
With each iteration through the nested loop the r and p value are saved in the
corresponding grid boxes of the variables rfield and pfield, respectively. For testing
purposes the range of rand p values is printed in lines 32 and 36.
The lons and lats variables associated with the pr field are converted to two-
dimensional grids in line 36.
A plot figure (fig) and single axis ax are set up in lines 39 to 41. For the map the
Robinson projection is specified and passed to the plt.subplots() function via the
subplot_kw keyword using a central longitude of 180.

Contour levels ranging from -0.7 to 0.7 in steps of 0.1 are defined in line 43 and saved
in the variable levels (used later for plotting in line 45).
Filled contours of the field holding the correlation coefficients (rfield) are now
plotted onto the map in lines 58 and 59. The term transform=ccrs.PlateCarree()
indicates that the data to be plotted are on a regular grid. They will now be
transformed to the Robinson projection internally. The plt.cm.PiYG colour map is
used but neither end of the colour scale is extended as there are no correlation
coefficients below 0 or above 1. Coast lines are added to the map in line 49.
Python - Creating Plots 205

In lines 52 to 54 a horizontal colour bar is added. Note that the handle mymap created
in line 46 is passed to the ax.colorbar() function and a new handle cbar is created
that is used in the following two lines to customise the colour bar.
Next, stipples are added to the map to indicate where the correlation coefficient
is statistically significant in the following way. The pfield variable holds the p-
values associated with the correlation coefficients for each grid box. Considering a
confidence level of 0.05 all values greater than 0.05 are masked in the pfield variable
using the np.ma.masked_greater() function in line 57. All grid boxes with p-values
smaller than 0.05 are now saved in the new variable pfieldm.
The ax.contourf() function is used to plot stipples in lines 58 and 59 by setting the
hatches keyword to ... . The density of the stipples can be controlled by the number
of repetitions of the hatching symbol used. In this example the hatching symbol is a
dot (.).
It is important to note that the value for alpha is set to 0 in the np.contourf() command
that generates the stipples in lines 58 to 59 (alpha=0). This makes sure that the filled
contours of the p-values field are not visible as they are made completely transparent.
They would overlay the filled contour of the correlation coefficients otherwise. Only
the hatching (in this case stipples) are shown. For more examples of the use of the
Contourf hatching keyword see the Matplotlib hatching demo²⁵.

Stipple density is controlled by the number of repetitions of the hatch


symbol used in the ax.contourf() function.

In order to indicate over which region the SSTs were averaged a red box is drawn
on the map that corresponds to the NINO3.4 domain in lines 63 and 64 using the
ax.plot() function. First, an array holding the x-coordinates of the box corners is
give ([-170, -120, -120, -170, -170]) followed by an array holding the y-coordinates
([-5, -5, 5, 5, -5]). The first coordinate pair ([-170, -5]) is repeated at the end so
that a closed box is drawn.
The box coordinates are given with reference to the ccrs.PlateCarree() coordinate
system which is passed via the transform keyword to the ax.plot() command. This
makes sure that the coordinates are transformed correctly to the Robinson map
projection defined in line 41. The colour of the box is set to red.
²⁵https://matplotlib.org/3.1.1/gallery/images_contours_and_fields/contourf_hatching.html
Python - Creating Plots 206

A plot title is added in lines 67 to 69. Note that title string is split into two lines by
forcing a line break with the \n character at the end of precip.
The plot layout is optimised in line 72 adding a padding of 2.5 on all sides of the plot.
Finally, the plot is saved in lines 75 and 76 and closed in line 77.

Figure 8.7.3.1: Spatial correlation of monthly mean HadISST (v1.1) SST time series for the NINO3.4
domain (red box) with monthly mean GPCP precipitation. Stipples indicate regions of statistically
significant correlations.

Figure 8.7.3.1 shows the correlation coefficients of monthly SSTs averaged over the
NINO3.4 domain and monthly precipitation around the world. There are clear signals
of statistically significant positive and negative correlations. El Nino and La Nina
events are not only changing precipitation patterns in the pacific but also in many
other parts of the world (tele-connections).
Python - Creating Plots 207

8.5 Bar Graphs

8.5.1 Anomalies Bar Graph


The Code 8.8.1.1 example shows how to create a bar graph with positive and negative
values. In this example the July-August-September (JAS) Sahel (20°W-30°E and 10°N-
18°N) annual rainfall anomalies have been calculated from the CRU TSv4.03 dataset
for the period 1950 to 2018 and plotted as shown in Figure 8.8.1.1.
Code 8.8.1.1: Plotting Sahel rainfall anomalies

1 import numpy as np
2 import matplotlib.pyplot as plt
3 from matplotlib.ticker import MultipleLocator
4 from netCDF4 import Dataset
5
6 # reading in netCDF file
7 ifile = '../data/Sahel_JAS_pre_anom.nc'
8 f = Dataset(ifile, mode='r')
9 field = f.variables['pre'][:,0,0]
10 f.close()
11
12 # set up figure and axis
13 fig, ax = plt.subplots(figsize=(5.5, 3.98))
14
15 # create array of values from 1950 to 2018
16 years = np.arange(1950, 2018+1, 1)
17
18 # create two arrays, one for negative and one for positive values
19 field_pos = np.where(field < 0, np.nan, field)
20 field_neg = np.where(field > 0, np.nan, field)
21
22 # create bar graph
23 ax.bar(years, field_pos, color='orangered', zorder=2)
24 ax.bar(years, field_neg, color='steelblue', zorder=2)
25
26 # add title
27 ax.set_title('CRU TSv4.03 mean Sahel JAS precipitation anomalies', fontsize=11)
28
Python - Creating Plots 208

29 # format x axis
30 ax.set_xlabel('Year')
31 ax.axes.set_xlim(1950, 2020)
32 ax.xaxis.set_minor_locator(MultipleLocator(1))
33
34 # format y axis
35 ax.set_ylim(-170, 170)
36 ax.set_ylabel('Precipitation departure from mean [mm]')
37 ax.yaxis.grid(color='black', linestyle=':', linewidth=0.5, zorder=1)
38 ax.yaxis.set_minor_locator(MultipleLocator(10))
39
40 # make plot look nice
41 plt.tight_layout()
42
43 # save figure to file
44 plt.savefig('../images/7_python_Sahel_pre_bar_graph_300dpi.png', format='png',
45 dpi=300)
46
47 # close
48 plt.close()

The Python packages and functions used in this script are imported in lines 1 to 4.
The pre-calculated data are read in from a netCDF file in lines 7 to 10. The variable
field is a one-dimensional NumPy array that holds the rainfall anomalies. No other
variables are read in from the file.
After setting up the figure and axis for the plot in line 13 the np.arange() function is
used to create a sequence of numbers from 1950 to 2018 saved in the variable years.
This variable holds the years that correspond to each bar in the graph. It is later
passed to the ax.bar() function in lines 23 and 24. The period covered in the file can
be identified, for instance, by using the CDO operator info (see Section 5.2.2).
The purpose of lines 19 and 20 is to create two new variables field_pos and field_-
neg separating the positive and negative anomaly values. In line 19 the np.where()
function is used to create a copy of the variable field but with all negative values
set to NaN (Not-a-Number). For more details about how the np.where()] function
works check the documentation²⁶. The function is also used in line 20 but instead of
²⁶https://docs.scipy.org/doc/numpy/reference/generated/numpy.where.html
Python - Creating Plots 209

all negative values all positive values are set to NaN.


To create a bar graph the ax.bar() function is used. It is used first in line 23 to plot
all positive values using the colour orangered. It is then used a second time in line 24
to plot all negative values using the colour steelblue. Setting the zorder to 2 makes
sure that the bars are plotted on top of the grid lines. More details about the use of
the Zorder can be found in the Documentation²⁷.
After setting the title in line 27 the x-axis and y-axis are formatted in lines 30 to 38.
Note that minor tickmarks are plotted for every year by setting the ax.xaxis.set_-
minor_locator(MultipleLocator(1)). Similarly, minor tickmarks are plotted for every
10 mm of rainfall on the y-axis by ax.yaxis.set_minor_locator(MultipleLocator(10))
(line 38). Horizontal dotted grid lines are added in line 37.
The figure is optimised in line 41, saved in line 44 and closed in line 48.

Figure 8.8.1.1: Sahel rainfall JAS anomalies (1950-2018) based on CRU TSv4.03 data.

²⁷https://matplotlib.org/3.1.1/gallery/misc/zorder_demo.html
Python - Creating Plots 210

The rainfall anomalies plotted in Figure 8.8.1.1 show that rainfall in the Sahel fell
sharply since the 1950s leading to a multi-year drought in the 1980s. Rainfall levels
recovered around the mid-2000s.

8.6 Hovmöller Plot

8.6.1 Hovmöller Plot with Time Axis Formatting

Code 8.9.1.1: Bodele LLJ

1 import numpy as np
2 import matplotlib.pyplot as plt
3 from netCDF4 import Dataset
4 from matplotlib import colors
5 from netCDF4 import num2date, date2num
6 from datetime import datetime
7 from matplotlib.ticker import MultipleLocator, FormatStrFormatter
8 import matplotlib.dates as mdates
9
10 # read u
11 f = Dataset('../data/era5_u_3d_bodele_2018_12.nc', mode='r')
12 levs = f.variables['level'][:]
13 time = f.variables['time'][:]
14 timeu = f.variables['time'].units
15 timec = f.variables['time'].calendar
16 field_u = f.variables['u'][:,:,0,0]
17 f.close()
18
19 # read v
20 f = Dataset('../data/era5_v_3d_bodele_2018_12.nc', mode='r')
21 field_v = f.variables['v'][:,:,0,0]
22 field_vv = f.variables['v'].units
23 f.close()
24
25 # convert numbers to dates (num2date)
26 tdates = num2date(time, units=timeu, calendar=timec)
27 print(tdates[240], time[240])
28 print(tdates[480], time[480])
Python - Creating Plots 211

29
30 # compute absolute windspeed
31 wspd = np.sqrt(np.square(field_u) + np.square(field_v))
32 print(np.min(wspd), np.max(wspd))
33
34 # transpose field
35 wspdnew = np.transpose(wspd)
36
37 # create lev/time grid
38 [timeall, levsall] = np.meshgrid(time, levs)
39
40 # open plot
41 fig, ax = plt.subplots(figsize=(5.5, 3.98))
42
43 # plot data
44 levels = np.arange(0, 25, 1)
45 myplot = ax.contourf(timeall, levsall, wspdnew, levels, cmap=plt.cm.rainbow,
46 extend='max')
47
48 #format x-xaxis
49 ax.set_xlim(time[240], time[480])
50 ax.xaxis.set_major_locator(MultipleLocator(12)) # major ticks every 12 hours
51 ax.xaxis.set_minor_locator(MultipleLocator(3)) # minor ticks every 3 hours
52 fig.canvas.draw() # creates plot in memory
53 ticks = ax.xaxis.get_major_ticks()
54 # create list with new labels
55 labels = []
56 for n in np.arange(len(ticks)):
57 val = ticks[n].get_loc()
58 dateobj = num2date(val, units=timeu, calendar=timec)
59 lab = dateobj.strftime("%-d %b %H:%M")
60 labels.append(lab)
61 ax.set_xticklabels(labels, fontsize=8) # set new labels
62 ax.set_xlabel('Time [UTC]', labelpad=5, fontsize=10)
63 fig.autofmt_xdate()
64
65 #format y-axis
66 ax.set_ylim(1000, 700)
67 ax.set_ylabel('Pressure [millibars]', labelpad=15, fontsize=10)
68
Python - Creating Plots 212

69 # axis labels and title


70 ax.set_title('ERA5 wind speed Dec 2018 - Bodele Depression (Chad)', fontsize=10)
71
72 # color bar
73 cbar = plt.colorbar(myplot, orientation='horizontal', shrink=0.7, pad=0.27)
74 cbar.set_label('wind speed [m/s]', rotation=0, fontsize=8)
75 cbar.ax.tick_params(labelsize=7, length=0)
76
77 plt.tight_layout()
78
79 # save plot to png file
80 plt.savefig('../images/7_python_time_height_contourf_plot_300dpi.png',
81 orientation='portrait', format='png', dpi=300)
82
83 # close file
84 plt.close()
Python - Creating Plots 213

Figure 8.9.1.1: Filled contour Hovmoller plot showing ERA5 wind speed at the Bodele depression
(Chad) highlighting the Bodele Low Level Jet (orange/red colours).

8.7 Vertical Cross-Section Plots

8.7.1 Meridional Cross-Section


Meridional cross-sections can be seen as vertical slices of the atmosphere with
latitude on the x-axis and a measure of altitude (elevation or pressure) on the y-axis.
The example Code 8.10.1.1 shows how to plot a meridional cross-section of zonal
mean wind speed averaged between the Equator and 10°E. In addition, different ways
of contouring data are shown in four subplots.
Python - Creating Plots 214

Code 8.10.1.1: Latitude-height cross-section of African Easterly Jet (AEJ).

1 import numpy as np
2 import matplotlib.pyplot as plt
3 from netCDF4 import Dataset
4 from matplotlib.ticker import MultipleLocator
5
6 # read u
7 f = Dataset('../data/era5_u_zonmean_1979_2018_AEJ.nc', mode='r')
8 levs = f.variables['level'][:]
9 lats = f.variables['lat'][:]
10 field = f.variables['u'][:]
11 f.close()
12
13 # get rid of dimensions of 1
14 print('Dimensions of field before np.squeeze:', field.shape)
15 field = np.squeeze(field)
16 print('Dimensions of field after np.squeeze:', field.shape)
17
18 # create 2d grid variables
19 [lats2d, levs2d] = np.meshgrid(lats, levs)
20
21 # open plot
22 fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex=True, sharey=True,
23 figsize=(5.5, 3.98))
24
25 # define countour levels
26 levels = np.arange(-14, 15, 1)
27
28 # format x-axis
29 ax1.set_xlim(0, 30)
30 ax1.xaxis.set_major_locator(MultipleLocator(5))
31
32 # format y-axis
33 ax1.set_ylim(1000, 500)
34 ax1.set_ylabel('Pressure [hPa]', fontsize=7)
35 ax1.tick_params(axis='y', which='major', labelsize=6)
36
37 # plot black line contour with lables (ax1) ==============================
38
Python - Creating Plots 215

39 cont1 = ax1.contour(lats2d, levs2d, field, levels, colors='black',


40 linewidths=0.5)
41 ax1.clabel(cont1, levels, fmt='%.0f', fontsize=4, inline=True,
42 inline_spacing=0.0)
43
44 # plot coloured line contour with lables (ax2) ==========================
45
46 cont2 = ax2.contour(lats2d, levs2d, field, levels, cmap=plt.cm.Spectral,
47 linewidths=0.5)
48 ax2.clabel(cont2, levels, fmt='%.0f', fontsize=4, inline=True,
49 inline_spacing=0.0)
50
51 # plot filled contours (ax3) =============================================
52 cont3 = ax3.contourf(lats2d, levs2d, field, levels, cmap=plt.cm.PiYG,
53 extend='both')
54
55 # format x-axis
56 ax3.set_xlabel('Latitude', fontsize=7)
57 ax3.tick_params(axis='x', which='major', labelsize=6)
58
59 # format y-axis
60 ax3.set_ylabel('Pressure [hPa]', fontsize=7)
61 ax3.tick_params(axis='y', which='major', labelsize=6)
62
63 # placing colorbar manually
64 cbaxes = fig.add_axes([0.1125, 0.075, 0.4, 0.02])
65 cbar = plt.colorbar(cont3, orientation='horizontal', cax = cbaxes, pad=0)
66 cbar.set_label('wind speed [m/s]', rotation=0, fontsize=5)
67 cbar.ax.tick_params(labelsize=5, length=0)
68
69 # annotation on plot with AEJ label
70 arrow_properties = dict(facecolor="black", arrowstyle='->')
71 ax3.annotate('AEJ', xy=(12.5, 620), xytext=(20, 550), fontsize=7,
72 verticalalignment='center', arrowprops=arrow_properties)
73
74 # plot filled contours and black line contours on top with lables (ax4) ===
75
76 cont41 = ax4.contourf(lats2d, levs2d, field, levels, cmap=plt.cm.PiYG,
77 extend='both')
78 cont42 = ax4.contour(lats2d, levs2d, field, levels, colors='black',
Python - Creating Plots 216

79 linewidths=0.5)
80 ax4.clabel(cont42, levels, fmt='%.0f', fontsize=4, inline=True,
81 inline_spacing=0.0)
82
83 # format x-axis
84 ax4.set_xlabel('Latitude', fontsize=7)
85 ax4.tick_params(axis='x', which='major', labelsize=6, length=3)
86
87 # optimise layout
88 plt.tight_layout(h_pad=1, w_pad=1, rect=[0,0.08,1,1])
89
90 # save plot to png file
91 plt.savefig('../images/7_python_meridional_xsection_contours_300dpi.png',
92 orientation='portrait', format='png', dpi=300)
93
94 # close file
95 plt.close()

All relevant packages and function are imported in lines1 to 4. The pre-calculated
longterm mean zonal (u) wind component averaged between 0 and 10°E are read in
from a netCDF file in lines7 to 11.
Some residual dimensions are removed by using the np.squeeze() function in line 15.
Compare the shape of the NumPy variable field before and after the function was
applied as displayed by the print() statements in lines14 an 16 in the terminal when
running the code.
The lats and lons variables are converted to two-dimensional fields using the
np.meshgrid() function in line 19.
A figure with four subplots is created in lines22 and 23. X-axis and y-axis sharing is
set to True.
Contour levels ranging from -14 to 14 in steps of 1 are saved in levels using the
np.arange() function in line 26.

The x-axis limits are set to range from 0 to 30 in line 29 representing a latitude range
from the Equator to 30°N. Y-axis limits are set to range from 1000 and 500 representing
the vertical depth of the atmosphere in hPa.
Python - Creating Plots 217

Note that the x-axis and y-axis limits are defined before any contour
commands are executed. This is important in this scenario as contour level
labels are messed up otherwise.

Black line contours are drawn into the top left subplot (ax1) using the ax1.contour()
function in lines39 and 40. Line colour is set to black and line widths is set to 0.5.
The handle cont1 created in line 39 and the contour levels variable levels is passed to
the ax1.clabel() function to draw contour labels. The label format is set to a floating
point value with one digit (%.0f) and fontsize is set to 4. Setting in the inline keyword
argument to True makes sure that no contour line is drawn under the label. The
keyword argument inline_spacing controls the distance to the contour line left and
right of the label.
A y-axis label is added in line 34.

Note that by default negative line contours are plotted as dashed lines while
positive line contours are plotted as solid lines (see Figure 8.10.1.1).

The plotting commands in lines 46 to 49 are for the top right subplot (ax2). They are
very similar to the plotting commands for ax1 in lines 39 to 42. The main difference
is that instead of setting the color keyword to black it is set to the colour map
plt.cm.Spectral (see Section 7.7.4 for colour maps). This will draw colour coded
contour lines as seen in the top right panel in Figure 8.10.1.1.
While the first two subplots ax1 and ax2 were line plots the third subplot ax3 uses
the ax3.contourf() to plot filled contours in lines 52 and 53 supplying the colour map
plt.cm.PiYG extended at both ends.

Axis labels are plotted for the y-axis and x-axis for ax3 in lines 56 and 60, respectively.
Next, a colour bar is placed manually onto the figure outside the four subplots in
lines 64 to 67. The position of the colour bar is controlled by adding a 5ʰ axis named
cbaxes to the figure using the fig.add_axes() function with the dimension [0.1125,
0.075, 0.4, 0.02] representing [left, bottom, width, height] rectangle coordinates given
as fractions of figure width and height.
In order to make space for the colour bar axis the figure borders are adjusted
using the plt.tight_layout() function in line 88 with the horizontal and vertical
Python - Creating Plots 218

padding between the figure edge and the subplots set to 1. The rect keyword here
controls the rectangular area for the normal subplots (ax1 to ax4) with the four values
corresponding to left, bottom, right and top in normalised figure coordinates (0 to 1).
Note that the value for bottom is set to 0.8 to create some space for the colour bar.

Note that the placement of the colour bar is controlled by a combination


of the plt.tight_layout() and fig.add_axes() function.

After the cbaxes is created the colour bar is plotted as usual using the plt.colorbar()
function in lines 65 to 67. It is connected to the filled contour plot in ax3 via the
handle cont3 and knows its plot position via the handle cbaxes which is passed to the
cax keyword in line 65.

In addition, a feature in the contour plot known as the African Easterly Jet (AEJ)
is labelled by using the ax3.annotate() function in lines 71 and 72. The label itself
is defined in the beginning as ‘AEJ’. The xy keyword defines the coordinates of
the feature to be labelled using the data coordinate system. The xytext keyword
defines the coordinates of the label position. Between the two points an arrow is
drawn. The arrow properties are defined in line 70 as a dictionary and passed via
the arrow_properties variable to the arrowprops keyword in line 72 (see Matplotlib
documentation²⁸ for more details about annotations).
In the bottom right subplot (ax4) both the black line contours as plotted in ax1 and
the filled colour contours as plotted in ax3 are combined to create a filled contour
plot with overlaying black line contours in lines 76 to 81.
An addition axis label is plotted for ax4 in line 84.

Note that axis labels were only added for the x-axes of ax3 and ax4 and for
the y-axes of ax1 and ax3.

The role of the plt.tight_layout() function in line 88 has been discussed earlier.
Finally, the plot is saved in lines 91 and 92 and closed in line 95.
²⁸https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.annotate.html?highlight=annotate#matplotlib.pyplot.
annotate
Python - Creating Plots 219

Figure 8.10.1.1: Latitude-height cross-section off longterm (1979-2018) July-September mean zonal
winds averaged over 0-10° longitude showing the African Easterly Jet (AEJ) over western Africa.
Four different ways of contouring the data are shown.

8.7.2 Vertical Cross-Section Between two Points


In this section another common type of vertical cross-section is discussed. Instead
of a vertical cross-section with either a zonal or meridional alignment the vertical
cross-section introduced here is between two arbitrary point locations on the Earth’s
surface defined by their geographical coordinates. The gridded three-dimensional
data field needs to be interpolated to the baseline between the two points. This
interpolation step is part of the example following example Code 8.10.2.1.
The code generates two subplots Figure 8.10.2.1. The first one is a map of showing
mean ERA5 10m wind speed and the baseline between points A and B. The
second plot shows the associated atmospheric wind speed vertical cross-section
corresponding to the baseline between point A and B.
Python - Creating Plots 220

Code 8.10.2.1: Plotting a vertical cross-section between two geographical points.

1 import numpy as np
2 import matplotlib.pyplot as plt
3 import matplotlib.ticker as mticker
4 from matplotlib import colors
5 from netCDF4 import Dataset
6 from scipy import interpolate
7 import cartopy.crs as ccrs
8 from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
9
10 # define cross section coordinates for end points A and B
11 lonA = 14.5
12 latA = 23
13 lonB = 21.5
14 latB = 16.5
15
16 # set up figure and map projection
17 fig = plt.figure(figsize=(3.98, 5.5))
18 ax1 = plt.subplot(211, projection=ccrs.PlateCarree())
19 ax2 = plt.subplot(212)
20
21 ### PANEL 1 (ax1)
22
23 # read 10m wind speed
24 f = Dataset('../data/era5_monthly_10m_wind_speed_1979_2018.nc', mode='r')
25 lons = f.variables['longitude'][:]
26 lats = f.variables['latitude'][:]
27 field = f.variables['si10'][0,:,:]
28 f.close()
29
30 # create 2D fields of lons and lats
31 [lons2d, lats2d] = np.meshgrid(lons, lats)
32
33 # define contour levels
34 levels = np.linspace(0, 11, 12)
35
36 # contour data
37 plot1 = ax1.contourf(lons2d, lats2d, field, levels,
38 transform=ccrs.PlateCarree(), cmap=plt.cm.rainbow,
Python - Creating Plots 221

39 extend='max')
40 ax1.set_extent([10, 25, 14, 27], crs=ccrs.PlateCarree())
41
42 # format gridlines and labels
43 gl = ax1.gridlines(draw_labels=True, linewidth=0.5, color='black', alpha=0.5,
44 linestyle=':')
45 gl.xlabels_top = False
46 gl.xlocator = mticker.FixedLocator(np.arange(-180,180,2))
47 gl.xformatter = LONGITUDE_FORMATTER
48 gl.xlabel_style = {'size':7, 'color':'black'}
49 gl.ylabels_right = False
50 gl.ylocator = mticker.FixedLocator(np.arange(-90,90,2))
51 gl.yformatter = LATITUDE_FORMATTER
52 gl.ylabel_style = {'size':7, 'color':'black'}
53
54 # add colorbar
55 cbar = fig.colorbar(plot1, ax=ax1, orientation='vertical')
56 cbar.set_label('10m wind speed [m/s]', rotation=90, fontsize=8)
57 cbar.ax.tick_params(labelsize=7, length=0)
58
59 # plot cross section line between A and B
60 ax1.plot([lonA, lonB], [latA, latB], marker='+', color='black', linewidth=1)
61 ax1.text(lonA, latA+0.3, 'A', horizontalalignment='center',
62 verticalalignment='bottom')
63 ax1.text(lonB, latB+0.3, 'B', horizontalalignment='center',
64 verticalalignment='bottom')
65
66 # add title and axis labels
67 ax1.set_title('ERA5 mean (1979-2018)', fontsize=8)
68
69 ### PANEL 2 (ax2)
70
71 # read u winds
72 f = Dataset('../data/era5_monthly_u_component_of_wind_1979_2018_ltm.nc',
73 mode='r')
74 lons = f.variables['longitude'][:]
75 lats = f.variables['latitude'][:]
76 levs = f.variables['level'][:]
77 field_u = f.variables['u'][0,:,:,:]
78 f.close()
Python - Creating Plots 222

79
80 # read v winds
81 f = Dataset('../data/era5_monthly_v_component_of_wind_1979_2018_ltm.nc',
82 mode='r')
83 field_v = f.variables['v'][0,:,:,:]
84 f.close()
85
86 # calculate wind speed
87 wspd3d = np.sqrt(np.square(field_u) + np.square(field_v))
88
89 # generate an empty array of the same dimensions as line
90 steps = 50
91 lonsnew = np.linspace(lonA, lonB, steps)
92 latsnew = np.linspace(latA, latB, steps)
93 wspd2d = np.zeros((len(lonsnew), len(levs)))
94
95 # do interpolation to 2d vertical plane
96 for i, a in enumerate(levs):
97 print('Interpolating level', i, 'representing', a, 'hPa')
98 inter = interpolate.interp2d(lons, lats, wspd3d[i,:,:])
99 wspd2d[:,i] = [inter(lonsnew[j], latsnew[j]) for j in range(steps)]
100
101 # create 2D variables
102 [levs2d, lons2d] = np.meshgrid(levs, lonsnew)
103
104 # define countour levels and plot filled contours
105 levels = np.linspace(0, 11, 12)
106
107 # contour data
108 plot2 = ax2.contourf(lons2d, levs2d, wspd2d, levels=levels, extend='max',
109 cmap=plt.cm.rainbow)
110
111 # add colorbar
112 cbar = fig.colorbar(plot2, ax=ax2, orientation='vertical')
113 cbar.set_label('wind speed [m/s]', rotation=90, fontsize=8)
114 cbar.ax.tick_params(labelsize=7, length=0)
115
116 # format x-axis
117 ax2.set_xlabel('Longitude', labelpad=7, fontsize=8)
118
Python - Creating Plots 223

119 # format y-axis


120 ax2.set_ylim(1000, 700)
121 ax2.set_ylabel('Pressure [hPa]', labelpad=7, fontsize=8)
122 ax2.tick_params(labelsize=7)
123
124 # add grid lines
125 ax2.grid(linewidth=0.5, color='black', alpha=0.5, linestyle=':')
126
127 # plot labels for A and B
128 ax2.text(lonsnew[0], 1010, 'A', horizontalalignment='center',
129 verticalalignment='top')
130 ax2.text(lonsnew[-1], 1010, 'B', horizontalalignment='center',
131 verticalalignment='top')
132
133 ### FINISH PLOT
134
135 # optimise layout
136 plt.tight_layout()
137
138 # save plot to png file
139 plt.savefig('../images/7_python_vertical_xsection_AB_300dpi.png',
140 orientation='portrait', format='png', dpi=300)
141
142 # close file
143 plt.close()

All relevant Python packages and functions are imported in lines 1 to 8.


The longitude/latitude coordinates of point A and point B are defined in lines 11 to
14 and saved in the variables lonA, latA, lonB and latB.
The figure (fig) and axes (ax1 and ax2) are set up in lines 17 to 19. Note that here the
usual plt.subplots() function is not used. Instead the figure object is set separately in
line 17 and the two axes objects ax1 and ax2 are set up in lines 18 and 19, respectively,
using the plt.subplot() function. The reason for this that the plt.subplots() function
does not allow the projection to be set for one subplot to a map projection and for
another subplot to the default rectangular projection (x-y plots). Therefore, separate
calls of the plt.subplot() function are used in line 18 to use the ccrs.PlateCarree()
map projection and in line 19 to use the default rectangular plot projection. See
Python - Creating Plots 224

Matplotlib for how to use the plt.subplot()²⁹ function.

The convenience function plt.subplots() does not allow the projection to


be set differently for individual subplots. This can be solved by setting up
axes separately using the plt.subplot() function.

The longterm mean 10m surface wind field is read in from a netCDF file in lines
24 to 28 and the longitude and latitude variables (lons and lats) are converted to
two-dimensional grids using the np.meshgrid() function in 31.
Contour levels ranging from 0 to 11 in steps of 1 are generated in line 34 using the
np.linspace() function.

The wind field is contoured onto the map projection in lines 37 to 39 in the normal
way (see Section 7.7).
The ax.set_extent() function is use in line 40 to define the plot domain ([left, right,
bottom, top]).
Grid lines and axis labels are formatted in lines 43 to 52 and a vertical colour bar is
added in lines 55 to 57 (ax1 handle passed to keyword argument ax in line 55).
Markers are plotted onto the map for point A and B in line 60 using the ax1.plot()
function providing the coordinates defined earlier in lines 11 to 14. The marker is a
‘black’ plus symbol (+) with a line width of 1. The labels A and B are added to the
markers in lines 61 to 64. The label positions are controlled by adding 0.3 degrees to
the latitude coordinates latA and latB as well as the horizontal and vertical alignment
of the label.
Finally, a title is added for the first subplot (ax1) in line 67.
Next, the three-dimensional longterm mean u and v wind components are read in
from netCDF files in lines 72 to 78 and lines 81 to 84, respectively (lons and lats are
read in only once because they are the same in both files). The absolute wind speed
is calculated from the u and v fields in line 87 yielding the three-dimensional wind
speed field wspd3d.
A sequence of new longitude (lonsnew) and latitude (latsnew) coordinate values is
generated in lines 91 and 92 that are positioned on the line between point A and point
²⁹https://matplotlib.org/3.2.1/api/_as_gen/matplotlib.pyplot.subplot.html
Python - Creating Plots 225

B. The distance between the two points is split up into 49 segments. It is suggested
that the length of each segment matches roughly the resolution of the input field.
A NumPy array (wspd2d) filled with zeros is created line 93 two dimension repre-
senting the surface along the baseline between point A and point B and the vertical
levels.
Next, the three-dimensional wind field is interpolated to the two-dimensional
vertical cross-section along the baseline between point A and B in lines 96 to 99.
The loop set up in line 96 iterates over each vertical level with the variable i being
and index starting with 0 and the variable a being the model level as retrieved from
the levs variable (e.g., in hPa).
To see the loop progress print() statement is added in line 97. With each loop iteration
the interpolation is performed in a two-stage process.
First, the interpolate.interp2d() function from the scipy package is used in line
98. One horizontal slice (level) of the wpsd3d variable and the associated longitude
and latitude variables lons and lats are passed to the function which returns
the interpolation function inter (spline interpolation method applied). Second, the
interpolation function inter is used on each individual new longitude and latitude
coordinate pair to get the associated wind speed values for that level along the points
between point A and point B (saved in wspd2d). After the loop completed, the wspd2d
variable will hold the data for the vertical cross-section associated with the baseline
between point A and point B.
The one-dimensional variables levs and lonsnew are turned into two-dimensional
fields in line 102 so that they can be used in the ax2.contourf() command in lines 108
and 109.
Contour levels ranging from 0 to 11 in steps of 1 are defined in line 105.
The filled contours of the vertical cross-section are produced in lines 108 and 109.
A colour bar is added in lines 112 to 114. Note that the axis handle ax2 is passed to
the keyword argument ax in the fig.colorbar() call in line 112.
Some further axis formatting is done in line 117 and lines 120 to 122 for the x-axis
and y-axis, respectively.
Labels for point A and Point B are added to the lower left and lower right corner of
the second subplot (see Figure 8.10.2.1) in lines 128 to 131 using ax2.text() function.
Python - Creating Plots 226

Finally, the plot is completed by optimising the layout in line 136, save everything to
a file in lines 139 to 140 and closing the plot in line 143.

Figure 8.10.2.1: ERA5 longterm 10m wind speed (top panel) and vertical cross-section of wind speed
(bottom panel). The vertical cross-section is representative for the baseline between point A and B
show in the top panel.

Increased surface wind speeds can be seen about 22°N/15.5°E and 19°N/19°E with the
Python - Creating Plots 227

later representing the Bodele low-level jet (LLJ) as it manifests itself at the surface (top
panel in Figure 8.10.2.1. The associated vertical cross-section shows the LLJ position
at about 920 hPa (bottom panel in Figure 8.10.2.1.

8.8 Multiple Panels

8.8.1 Multiple Line Plots (axes.flat method)

Code 8.12.1.1: Multiple line plots on a single page using the axes.flat method.

1 import matplotlib.pyplot as plt


2 import numpy as np
3 from matplotlib.ticker import MultipleLocator
4
5 # setup up figure and axes
6 fig, axes = plt.subplots(2, 3, sharex=True, sharey=True, figsize=(5.5, 3.98))
7
8 # flatten axes object
9 axesf = axes.flat
10
11 # get number of axes and number of plots
12 naxes = len(axesf)
13 nplots = 5
14
15 # define variable
16 x = np.arange(0, 10)
17
18 # loop through axes and create plots
19 for n in range(nplots):
20 axesf[n].plot(x, np.sin(x*n))
21 axesf[n].set_title('Plot '+str(n))
22
23 # remove remaining empty panels
24 for n in range(nplots, naxes):
25 axesf[n].set_axis_off()
26
27 # format axes
28 axesf[0].xaxis.set_minor_locator(MultipleLocator(1))
Python - Creating Plots 228

29 axesf[0].yaxis.set_minor_locator(MultipleLocator(0.1))
30
31 # optimise layout
32 plt.tight_layout()
33
34 # save plot
35 plt.savefig('../images/7_python_muliple_line_plots_1_300dpi.png', format='png',\
36 dpi=300)
37 plt.close()

Figure 8.12.1.1: Multiple line plots on a single page using the axes.flat method.

8.8.2 Multiple Line Plots (pop() function)


Python - Creating Plots 229

Code 8.12.1.2: Multiple line plots on a single page using a list and the Python pop() function.

1 import matplotlib.pyplot as plt


2 import numpy as np
3
4 # setup up figure and axes
5 fig, axs = plt.subplots(3, 4, sharey=True, figsize=(5.5, 7.6))
6 #axsf = axs.flat
7 axsl = [item for sublist in axs for item in sublist]
8
9 # get number of axes and number of plots
10 nplots = 9
11
12 # define variable
13 x = np.arange(0, 10)
14
15 # loop through axes
16 for n in range(nplots):
17 # set correct axis
18 ax = axsl.pop(0)
19
20 # create plot
21 ax.plot(x, np.sin(x*n))
22 ax.set_title('Plot '+str(n))
23
24 # remove remaining empty panels
25 for ax in axsl:
26 ax.remove()
27
28 # optimise layout
29 plt.tight_layout()
30
31 # save plot
32 plt.savefig('../images/7_python_muliple_line_plots_2_300dpi.png', format='png',\
33 dpi=300)
34 plt.close()
Python - Creating Plots 230

Figure 8.12.1.2: Multiple line plots on a single page using a list and the Python pop() function.

8.8.3 Multiple Map Plots (axes.flat method)


The following code example (Code 8.12.1.3) shows how to create figure with 12
subplots, each showing a map a longterm mean rainfall (Figure 8.12.1.3). The
Python - Creating Plots 231

challenge here is to set up map projections for all subplots. In addition, the axes.flat
method is used here to loop through the subplots.
Code 8.12.1.3: Multiple map plots on a single page using the the axes.flat method.
1 import numpy as np
2 import matplotlib.pyplot as plt
3 import matplotlib.ticker as mticker
4 import cartopy.crs as ccrs
5 from netCDF4 import Dataset
6 from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
7 import calendar
8
9 # reading in netCDF file
10 ifile = '../data/chirps-v2.0.1981-2018.monmean_p05_africa.nc'
11 f = Dataset(ifile, mode='r')
12 lons = f.variables['longitude'][:]
13 lats = f.variables['latitude'][:]
14 field = f.variables['precip'][:,:,:]
15 f.close()
16
17 # create 2D fields of lons and lats
18 [lons2d, lats2d] = np.meshgrid(lons, lats)
19
20 # set up figure and map projection
21 fig, ax = plt.subplots(3, 4, sharex=True, sharey=True, figsize=(5.5, 3.98),
22 subplot_kw={'projection':ccrs.PlateCarree()})
23
24 # flatten axes object
25 axflat = ax.flat
26
27 # define contour levels
28 levels = np.linspace(0, 800, 9)
29
30 # loop through months
31 for m in np.arange(12):
32 # extract one month data field
33 mfield = field[m,:,:]
34
35 # contour data
36 mymap = axflat[m].contourf(lons2d, lats2d, mfield, levels,
Python - Creating Plots 232

37 transform=ccrs.PlateCarree(),
38 cmap=plt.cm.rainbow, extend='max')
39
40 # format map
41 axflat[m].coastlines()
42 axflat[m].set_extent([-20, 55, -35, 40], crs=ccrs.PlateCarree())
43
44 # format gridlines and labels
45 gl = axflat[m].gridlines(draw_labels=True, linewidth=0.5, color='black',
46 alpha=0.5, linestyle=':')
47 gl.xlabels_top = False
48 if (m < 8): gl.xlabels_bottom = False
49 gl.xlocator = mticker.FixedLocator(np.arange(-180, 180, 20))
50 gl.xformatter = LONGITUDE_FORMATTER
51 gl.xlabel_style = {'size':5, 'color':'black'}
52 gl.ylabels_right = False
53 if (m not in [0,4,8]): gl.ylabels_left = False
54 gl.ylocator = mticker.FixedLocator(np.arange(-90, 90, 20))
55 gl.yformatter = LATITUDE_FORMATTER
56 gl.ylabel_style = {'size':5, 'color':'black'}
57
58 # add label for month
59 axflat[m].text(-16, -25, calendar.month_abbr[m+1], fontsize=5,
60 horizontalalignment='left', verticalalignment='center',
61 fontweight='bold')
62
63 # make plot look nice
64 plt.tight_layout(h_pad=0, w_pad=-5, rect=[0,0.1,1,1])
65
66 # add colorbar
67 cbarax = fig.add_axes([0.2, 0.07, 0.6, 0.02])
68 cbar = plt.colorbar(mymap, cax=cbarax, orientation='horizontal')
69 cbar.set_label('Precipitation [mm]', rotation=0, fontsize=7, labelpad=1)
70 cbar.ax.tick_params(labelsize=5, length=0)
71
72 # save figure to file
73 plt.savefig('../images/7_python_multiple_maps_300dpi.png', format='png',
74 dpi=300)
75
76 # close plot
Python - Creating Plots 233

77 plt.close()

All Python packages and functions are imported in lines 1 to 7.


The precipitation data fields is read in from a netCDF file in lines 10 to 15 in the usual
way. The variable field has three dimensions ([time, lat, lon]) and holds longterm
averages of global precipitation calculated from the CHIRPS v2.0 dataset.
The longtitude and latitude variables are converted from to two-dimensional fields
in line 18.
A figure (fig) and axes (ax) object are created in lines 21 and 22 by using the
plt.subplots() function. The ax object holds a 3 by 4 grid of subplots (12 subplots
in total) which share the x-axis and the y-axis. The map projection for all subplots is
set to ccrs.PlateCarree() via the subplot_kw keyword argument.
The ax object is flattened using the ax.flat method in line 25. This allows to easily
loop through the 12 subplots.
Contour levels ranging from 0 to 800 in steps of 100 are generated in line 28 using the
np.linspace() function.

A for-loop is initiated in line 31 that iterates over a sequence of numbers ranging


from 0 to 11.
Inside the loop, the precipitation field is extracted from the three-dimensional
variable field for that particular month by using the index m. The two-dimensional
output is saved in the variable mfield in line 33.
The precipitation variable mfield is then contoured in lines 36 to 38. Note that the
correct subplots is selected here by using the index m with the axflat object created
earlier in line 25. As the precipitation fields is on a regular grid the transform keyword
argument is set to ccrs.PlateCarree().
Coastlines are added to the map in line 41 and the map domain is defined in line
42 via the axflat[m].set_extent() method. The coordinate reference system keyword
crs is defined accordingly.

Grid lines and grid labels are added for both x-axis and y-axis in lines 41 to 56. Grid
labels for the y-axis are only plotted for the leftmost subplots and for the x-axis only
for the bottommost subplots (see Figure 8.12.1.3). All other grid labels are switched
of (set to False) for respective subplots by using if-conditions in lines 48 and 53.
Python - Creating Plots 234

A three-letter text label indicating the month is added to the bottom left corner
of each map in lines 59 to 61. The label is retrieved from the calendar.month_abbr
attribute by using the index m+1 to get the correct month. The indexed attribute
returns the three-letter abbreviation for the specified month. More details about the
calendar package can be found in the Python documentation³⁰.
Space is created at the bottom of the figure for the colour bar by setting the rect
keyword argument with the plt.tight_layout() function in line 64.
A colour bar is added in lines 67 to 70 by using the same method as in Code 8.10.1.1.
An additional plot axis cax object is created in line 67 into which the colour bar is
placed in line 68 by passing the cax handle to the cax keyword argument.
Finally, the figure is saved in lines 73 and 74 and closed in line 77.
³⁰https://docs.python.org/3.7/library/calendar.html#calendar
Python - Creating Plots 235

Figure 8.12.1.3: Multiple map plots on a single page using the the axes.flat method showing monthly
mean rainfall derived from CHIRPS v2.
9. Data Analysis with CDO
9.1 What is CDO?
Climate Data Operators (CDO) are a collection of command line operators that can
be used to manipulate climate data products and data from climate and Numerical
Weather Prediction (NWP) models. CDO was developed at the Max-Planck-Institute
for Meteorology, Hamburg (Germany), is freely available and is widely used in the
climate community.
More than 700 operators are available that can be used to combine, extract, compare,
modify, interpolate, regress, and transform climate data and to compute statistics.
Although CDO can also handle data in GRIB (Section 2.5.3) and binary (Section 2.5.2)
format in this book only the processing of data in netCDF format is discussed.
What makes CDO such a powerful tool is a combination of factors which are
summarised in the following list.

• Simple command line interface


• More than 700 operators
• Complex single command operations through piping of operators
• Fast processing of large datasets
• Support for different grid types

While not all 700+ CDO operators can be covered in this chapter an overview of the
most frequently used operators and their usage will be presented.

9.2 Useful CDO Resources


The CDO User’s Guide is a 200+ page document that details the usage of CDO and
each of the 700+ operators. The guide is available in HTML and PDF format on
Data Analysis with CDO 237

the CDO webpage¹. Also useful is the CDO Reference Card available on the same
webpage.
It is important to make sure that the version of the CDO User’s Guide and CDO
Reference Card matches the version of the CDO package installed on the system. In
order to identify the installed CDO version execute the following command on the
Unix command line. It will list all operators as well as the CDO version at the end of
the output.

cdo -h

The CDO community has an active forum² and responses are often posted within a
day or two. For most questions that a CDO beginner may have there is a good chance
that it has been discussed before and that potential solutions have been suggested.
Always search the forum for solutions first before starting a new thread.
The CDO documentation is also available on the command line. To see the docu-
mentation for a specific operator use cdo -h command followed by the operator.
For instance, in order to list the documentation for the operator seldate execute the
following command.

cdo -h seldate

9.3 Basic Syntax of CDO Commands


The generic format of the CDO syntax on the Unix command line is cdo followed
by one or more options (if required) followed by at least one operator followed by
the input file name(s) and output file name (if required). This generalised syntax is
summarised in the following line.

cdo [options] operator_N,par1,par2 [-operator_2 [-operator_1]] ifile(s) [ofile]

The following CDO syntax rules are important to remember.


¹https://code.zmaw.de/projects/cdo/wiki/Cdo#Documentation
²https://code.zmaw.de/projects/cdo/boards
Data Analysis with CDO 238

• Every CDO command starts with cdo.


• Options come before operators.
• Multiple operators will be executed in sequence from right to left.
• Single operators do not need hyphens.
• Multiple operators require hyphens (-) apart from the leftmost (last executed)
operator.
• An output filename is required for most operators.
• Parameters may be passed to operators using commas with no white spaces.

The individual parts of the CDO command syntax are discussed in more detail in the
following subsections.

9.3.1 CDO Options


First of all, options are only required for certain specific tasks. Most of the time they
are not needed when analysing climate data.
There are a total of 21 options. When options are included in a CDO command they
must be placed before (to the left of) the any CDO operator (operators are covered
in the following Section 8.3.2). Some more frequently used options are listed in Table
8.3.1.1. A complete list can be found in the CDO User’s Guide.

Table 8.3.1.1: Options used frequently with CDO commands.

Option <input> Description Input Examples


-a Convert from relative to absolute
time axis
-b <nbits> Set the number of bits for the output F32, F64
precision
-f <format> Define the output file format when grb, nc, srv, ext
converting
-g <gird> Define the output file grid r360x180, lon=20/lat=30, file.nc
-h <operator> Show help information for operators any operator
-m <missval> Set default missing value of -9e+33(default)
non-netCDF file
-r Convert from absolute to relative
time axis

You might also like