MetPy Intro: NYS Mesonet Map


Overview

In this notebook, we’ll use Cartopy, Matplotlib, and Pandas (with a bunch of help from MetPy) to read in, manipulate, and visualize current data from the New York State Mesonet.

Prerequisites

Concepts

Importance

Notes

Matplotlib

Necessary

Cartopy

Necessary

Pandas

Necessary

MetPy

Necessary

Intro

  • Time to learn: 30 minutes


Imports

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from datetime import datetime
from cartopy import crs as ccrs
from cartopy import feature as cfeature
from metpy.calc import wind_components, dewpoint_from_relative_humidity
from metpy.units import units
from metpy.plots import StationPlot, USCOUNTIES

Create a Pandas DataFrame object pointing to the most recent set of NYSM obs.

nysm_data = pd.read_csv('https://www.atmos.albany.edu/products/nysm/nysm_latest.csv')
nysm_data.columns
Index(['station', 'time', 'temp_2m [degC]', 'temp_9m [degC]',
       'relative_humidity [percent]', 'precip_incremental [mm]',
       'precip_local [mm]', 'precip_max_intensity [mm/min]',
       'avg_wind_speed_prop [m/s]', 'max_wind_speed_prop [m/s]',
       'wind_speed_stddev_prop [m/s]', 'wind_direction_prop [degrees]',
       'wind_direction_stddev_prop [degrees]', 'avg_wind_speed_sonic [m/s]',
       'max_wind_speed_sonic [m/s]', 'wind_speed_stddev_sonic [m/s]',
       'wind_direction_sonic [degrees]',
       'wind_direction_stddev_sonic [degrees]', 'solar_insolation [W/m^2]',
       'station_pressure [mbar]', 'snow_depth [cm]', 'frozen_soil_05cm [bit]',
       'frozen_soil_25cm [bit]', 'frozen_soil_50cm [bit]',
       'soil_temp_05cm [degC]', 'soil_temp_25cm [degC]',
       'soil_temp_50cm [degC]', 'soil_moisture_05cm [m^3/m^3]',
       'soil_moisture_25cm [m^3/m^3]', 'soil_moisture_50cm [m^3/m^3]', 'lat',
       'lon', 'elevation', 'name'],
      dtype='object')

Create several Series objects for some of the columns.

stid = nysm_data['station']
lats = nysm_data['lat']
lons = nysm_data['lon']

Our goal is to make a map of NYSM observations, which includes the wind velocity. The convention is to plot wind velocity using wind barbs. The MetPy library allows us to not only make such a map, but perform a variety of meteorologically-relevant calculations and diagnostics. Here, we will use such a calculation, which will determine the two scalar components of wind velocity (u and v), from wind speed and direction. We will use MetPy’s wind_components method.

This method requires us to do the following:

  1. Create Pandas Series objects for the variables of interest

  2. Extract the underlying Numpy array via the Seriesvalues attribute

  3. Attach units to these arrays using MetPy’s units class

Perform these three steps

wspd = nysm_data['max_wind_speed_prop [m/s]'].values * units['m/s']
drct = nysm_data['wind_direction_prop [degrees]'].values * units['degree']
Tip: The * operator might make you think that we're multiplying. However, in this case we use it to "attach" units to an array of values.

Examine these two units aware Series

wspd
Magnitude
[0.6 1.1 3.4 2.6 3.7 4.0 0.8 0.6 2.5 6.8 0.9 3.1 4.7 5.2 4.4 0.9 3.6 2.8
5.6 5.0 7.3 0.6 2.1 2.2 1.2 2.6 1.0 3.2 2.4 5.3 1.4 0.6 0.0 4.9 2.2 1.4
0.8 2.2 0.9 3.3 3.1 2.9 4.1 1.0 2.1 2.8 1.1 4.1 1.7 0.4 2.4 3.5 6.4 3.0
3.0 3.6 5.5 2.4 2.2 0.6 2.0 3.0 0.9 7.5 7.2 8.0 4.8 0.7 0.7 4.2 1.8 1.4
0.5 0.8 3.4 3.7 3.4 1.3 2.7 2.0 2.7 3.2 1.0 4.9 7.2 1.5 0.7 3.5 4.4 0.6
3.2 0.9 1.2 1.8 1.8 3.8 3.3 3.2 5.5 4.8 2.4 6.4 6.9 0.6 3.3 3.3 1.9 4.1
0.6 2.3 0.5 2.0 2.7 1.8 6.5 4.3 2.7 1.4 5.3 5.7 7.2 0.3 0.8 3.5 0.0 1.6]
Unitsmeter/second
Tip: This is a unique type of object: not only does it have values, aka magnitude, but it also has Units attached. This is a very nice property, since it will make calculations that require awareness of units much easier!
drct
Magnitude
[166 152  66  24  79  45 120 326 131  93 254 136  72 120  67  57  59  83
82 105 45 15 26 50 356 349 117 44 125 125 188 233 0 100 168 331
272 100 134 27 38 73 84 329 286 75 87 97 8 195 96 139 49 81
126 135 86 61 61 97 112 1 107 67 77 133 87 13 182 136 148 32
4 77 89 97 113 46 47 157 236 47 13 65 155 21 3 127 23 311
51 304 286 359 323 9 95 73 109 125 135 88 113 68 137 77 138 16
101 88 126 313 23 145 114 157 116 295 75 46 94 240 109 22 0 16]
Unitsdegree

Convert wind speed from m/s to knots

wspk = wspd.to('knots')
wspk
Magnitude
[1.1663066954643628 2.1382289416846656 6.609071274298056 5.053995680345573
7.192224622030238 7.7753779697624195 1.555075593952484 1.1663066954643628
4.859611231101512 13.218142548596113 1.7494600431965444 6.025917926565875
9.136069114470843 10.107991360691146 8.552915766738662 1.7494600431965444
6.997840172786177 5.442764578833693 10.885529157667387 9.719222462203025
14.190064794816415 1.1663066954643628 4.08207343412527 4.276457883369331
2.3326133909287257 5.053995680345573 1.9438444924406049 6.220302375809936
4.665226781857451 10.302375809935205 2.7213822894168467
1.1663066954643628 0.0 9.524838012958964 4.276457883369331
2.7213822894168467 1.555075593952484 4.276457883369331 1.7494600431965444
6.4146868250539955 6.025917926565875 5.637149028077754 7.9697624190064795
1.9438444924406049 4.08207343412527 5.442764578833693 2.1382289416846656
7.9697624190064795 3.304535637149028 0.777537796976242 4.665226781857451
6.803455723542117 12.440604751619873 5.831533477321814 5.831533477321814
6.997840172786177 10.691144708423327 4.665226781857451 4.276457883369331
1.1663066954643628 3.8876889848812097 5.831533477321814
1.7494600431965444 14.578833693304537 13.995680345572355
15.550755939524839 9.330453563714903 1.3606911447084233
1.3606911447084233 8.16414686825054 3.4989200863930887 2.7213822894168467
0.9719222462203024 1.555075593952484 6.609071274298056 7.192224622030238
6.609071274298056 2.5269978401727866 5.248380129589633 3.8876889848812097
5.248380129589633 6.220302375809936 1.9438444924406049 9.524838012958964
13.995680345572355 2.915766738660907 1.3606911447084233 6.803455723542117
8.552915766738662 1.1663066954643628 6.220302375809936 1.7494600431965444
2.3326133909287257 3.4989200863930887 3.4989200863930887
7.386609071274298 6.4146868250539955 6.220302375809936 10.691144708423327
9.330453563714903 4.665226781857451 12.440604751619873 13.412526997840175
1.1663066954643628 6.4146868250539955 6.4146868250539955
3.693304535637149 7.9697624190064795 1.1663066954643628 4.470842332613391
0.9719222462203024 3.8876889848812097 5.248380129589633
3.4989200863930887 12.63498920086393 8.3585313174946 5.248380129589633
2.7213822894168467 10.302375809935205 11.079913606911449
13.995680345572355 0.5831533477321814 1.555075593952484 6.803455723542117
0.0 3.110151187904968]
Unitsknot

Perform the vector decomposition

u, v = wind_components(wspk, drct)

Take a look at one of the output components:

u
Magnitude
[-0.28215512661732306 -1.003837682846721 -6.037687041871186
-2.0556452371433536 -7.060083198446483 -5.498022488707496
-1.346734969168026 0.6521904273740888 -3.6675951522704913
-13.200027543624174 1.6816889280994352 -4.185954327387909
-8.688918064640388 -8.753777299592167 -7.873000474452839
-1.467220648025472 -5.998319771651931 -5.402195037011083
-10.779591936193336 -9.38804798769073 -10.03389104189118
-0.30186238521676284 -1.789463212594722 -3.2759567977874227
0.1627148847595155 0.9643478384041352 -1.7319781247722055
-4.32098511214236 -3.8215300554302853 -8.43921220574188 0.378743212115945
0.931453942603754 -0.0 -9.380134321347382 -0.8891255892422538
1.3193523142772883 1.5541282839173847 -4.211488878972294
-1.25845623817345 -2.91220687737895 -3.7099255208608786
-5.3908324256231595 -7.926103226585159 1.001153925311121
3.9239408322320144 -5.257306873106808 -2.1352985732333223
-7.910357018480515 -0.4599024718550759 0.20124159014450865
-4.639670181415703 -4.4634685557669505 -9.389043589812458
-5.7597376233409765 -4.7178096864197805 -4.948220239836748
-10.665101617248661 -4.080299281687029 -3.740274341546445
-1.1576132222166606 -3.604602458359001 -0.10177429239884553
-1.673016959676153 -13.419887172362793 -13.63697196502878
-11.373102919347794 -9.317666501381769 -0.3060889076385204
0.047487436117873795 -5.671292959686845 -1.8541451577922072
-1.4421129005050501 -0.06779786864979781 -1.5152191072254202
-6.608064680731226 -7.138614870336076 -6.083682184804465
-1.8177701218060949 -3.83842223527988 -1.5190411042779544
4.351104322481105 -4.549241167739117 -0.4372698680550292 -8.6324348614074
-5.914830099524261 -1.0449173474862967 -0.0712130722096212
-5.433481331855232 -3.3418904294115 0.880222836544918 -4.834082870401893
1.4503681074937014 2.2422519041325795 0.06106457543930749
2.1057026727998234 -1.1555202385261114 -6.390277005037762
-5.94850474551521 -10.108675916234597 -7.643060110860571
-3.298813493224498 -12.433026271339077 -12.346296198573768
-1.0813807375077003 -4.374805895001341 -6.2502788173048565
-2.4713031033988075 -2.196764239664637 -1.1448783565048348
-4.46811881626248 -0.7863016144032967 2.8432757298369475
-2.0507054907752384 -2.0068981142304416 -11.542636991812559
-3.2659383741976016 -4.717212813190012 2.4664099604021144
-9.951330866952173 -7.970222841765184 -13.961587571670973
0.5050256134380094 -1.4703528605432143 -2.5486193720521015 -0.0
-0.8572738496252243]
Unitsknot
Exercise: Create a 2-meter temperature object; attach units of degrees Celsius (degC) to it, and then convert to Fahrenheit (degF).
# Write your code here
#%load /spare11/atm350/common/mar12/metpy1.py

Now, let’s plot several of the meteorological values on a map. We will use Matplotlib and Cartopy, as well as MetPy’s StationPlot method.

Create units-aware objects for relative humidity and station pressure

rh = nysm_data['relative_humidity [percent]'].values * units('percent')
pres = nysm_data['station_pressure [mbar]'].values * units('mbar')

Plot the map, centered over NYS, with add some geographic features, and the mesonet data.

Determine the current time, for use in the plot’s title and a version to be saved to disk.

timeString = nysm_data['time'][0]
timeObj = datetime.strptime(timeString,"%Y-%m-%d %H:%M:%S")
titleString = datetime.strftime(timeObj,"%B %d %Y, %H%M UTC")
figString = datetime.strftime(timeObj,"%Y%m%d_%H%M")

Previously, we used the ax.scatter and ax.text methods to plot markers for the stations and their site id’s. The latter method does not provide an intuitive means to orient several text stings relative to a central point as we typically do for a meteorological surface station plot. Let’s take advantage of the Metpy package, and its StationPlot method!

Be patient: this may take a minute or so to plot, if you chose the highest resolution for the shapefiles!

Specify two resolutions: one for the Natural Earth shapefiles, and the other for MetPy’s US County shapefiles.

res = '10m'
resCounty = '5m'
# Set the domain for defining the plot region.
latN = 45.2
latS = 40.2
lonW = -80.0
lonE = -72.0
cLat = (latN + latS)/2
cLon = (lonW + lonE )/2

proj = ccrs.LambertConformal(central_longitude=cLon, central_latitude=cLat)

fig = plt.figure(figsize=(18,12),dpi=150) # Increase the dots per inch from default 100 to make plot easier to read
ax = fig.add_subplot(1,1,1,projection=proj)
ax.set_extent ([lonW,lonE,latS,latN])
ax.set_facecolor(cfeature.COLORS['water'])
ax.add_feature (cfeature.STATES.with_scale(res))
ax.add_feature (cfeature.RIVERS.with_scale(res))
ax.add_feature (cfeature.LAND.with_scale(res))
ax.add_feature (cfeature.COASTLINE.with_scale(res))
ax.add_feature (cfeature.LAKES.with_scale(res))
ax.add_feature (cfeature.STATES.with_scale(res))

ax.add_feature(USCOUNTIES.with_scale(resCounty), linewidth=0.8, edgecolor='darkgray')

# Create a station plot pointing to an Axes to draw on as well as the location of points
stationplot = StationPlot(ax, lons, lats, transform=ccrs.PlateCarree(),
                          fontsize=8)

stationplot.plot_parameter('NW', tmpf, color='red')
stationplot.plot_parameter('SW', rh, color='green')
stationplot.plot_parameter('NE', pres, color='purple')
stationplot.plot_barb(u, v,zorder=2) # zorder value set so wind barbs will display over lake features

plotTitle = f'NYSM Temperature(°F), RH (%), Station Pressure (hPa), Peak 5-min Wind (kts), {titleString}'
ax.set_title (plotTitle);
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[17], line 28
     24 # Create a station plot pointing to an Axes to draw on as well as the location of points
     25 stationplot = StationPlot(ax, lons, lats, transform=ccrs.PlateCarree(),
     26                           fontsize=8)
---> 28 stationplot.plot_parameter('NW', tmpf, color='red')
     29 stationplot.plot_parameter('SW', rh, color='green')
     30 stationplot.plot_parameter('NE', pres, color='purple')

NameError: name 'tmpf' is not defined
../../_images/cbd69e83d4173c44ce5fe6a41c4aef43e29ee556ba01f9d4424fc526578da070.png
Tip: In the code cell above, the plotTitle string object, uses f-strings, which is a newer and better way of constructing more complex string objects in Python.

What if we wanted to plot sea-level pressure (SLP) instead of station pressure? In this case, we can apply what’s called a reduction to sea-level pressure formula. This formula requires station elevation (accounting for sensor height) in meters, temperature in Kelvin, and station pressure in hectopascals. We assume each NYSM station has its sensor height .5 meters above ground level.

Tip: At present, MetPy does not yet have a function that reduces station pressure to SLP, so we will do a units-unaware calculation. We can strip the units ... i.e., take only the magnitude of the units-aware variables via their .m attribute.
pres.m
array([ 954.03,  954.6 ,  980.01, 1004.79,  959.11,  996.04,  963.95,
        967.12,  951.81, 1011.28,  985.43,  994.17,  992.57, 1008.57,
        955.76, 1002.77,  990.27,  947.54, 1000.81,  996.25, 1004.09,
       1011.82,  978.33,  963.89,  948.08,  990.11,  959.92,  977.72,
        944.08,  964.51,  991.19,  985.  ,  965.58,  997.  ,  941.23,
        977.35, 1000.35,  967.51,  967.71,  978.45,  990.01,  974.  ,
        982.52,  973.71, 1012.14,  991.88,  983.  ,  954.11, 1005.82,
        984.31,  967.17,  942.64, 1001.76,  954.93,  956.64,  929.95,
        989.9 ,  993.06,  958.8 ,  989.6 ,  997.84, 1007.97,  957.17,
       1006.17,  988.48, 1003.99,  993.33,  971.82,     nan,  961.94,
        958.04,  982.67,  952.74,  959.44, 1001.3 ,  973.67,  975.  ,
       1001.96,  987.16,  960.92,  983.06,  998.85,  954.03, 1000.4 ,
       1008.83,  960.25,  952.7 ,  969.19, 1008.33,  949.46,  991.18,
        981.1 ,  971.91, 1005.  , 1004.05, 1013.58,  966.58,  972.53,
        993.98, 1016.35,  992.74,  962.82, 1010.68,  982.18, 1011.19,
        994.34,  934.3 , 1008.48,  968.97,  957.26,  970.05, 1003.17,
       1002.64,  949.69, 1014.8 ,  950.03,  995.19,  995.08,  965.06,
       1004.2 ,  990.51,  946.02,  962.93, 1013.85,  998.47,  991.56])
elev = nysm_data['elevation']
sensorHeight = .5
# Reduce station pressure to SLP. Source: https://www.sandhurstweather.org.uk/barometric.pdf 
slp = pres.m/np.exp(-1*(elev+sensorHeight)/((tmpc.m + 273.15) * 29.263))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[19], line 4
      2 sensorHeight = .5
      3 # Reduce station pressure to SLP. Source: https://www.sandhurstweather.org.uk/barometric.pdf 
----> 4 slp = pres.m/np.exp(-1*(elev+sensorHeight)/((tmpc.m + 273.15) * 29.263))

NameError: name 'tmpc' is not defined
slp
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[20], line 1
----> 1 slp

NameError: name 'slp' is not defined

Make a new map, substituting SLP for station pressure. We will also use the convention of the three least-significant digits to represent SLP in hectopascals.

Examples: 1018.4 hPa would be plotted as 184, while 977.2 hPa would be plotted as 772.
fig = plt.figure(figsize=(18,12),dpi=150) # Increase the dots per inch from default 100 to make plot easier to read
ax = fig.add_subplot(1,1,1,projection=proj)
ax.set_extent ([lonW,lonE,latS,latN])
ax.set_facecolor(cfeature.COLORS['water'])
ax.add_feature (cfeature.STATES.with_scale(res))
ax.add_feature (cfeature.RIVERS.with_scale(res))
ax.add_feature (cfeature.LAND.with_scale(res))
ax.add_feature (cfeature.COASTLINE.with_scale(res))
ax.add_feature (cfeature.LAKES.with_scale(res))
ax.add_feature (cfeature.STATES.with_scale(res))

ax.add_feature(USCOUNTIES.with_scale(resCounty), linewidth=0.8, edgecolor='darkgray')

stationplot = StationPlot(ax, lons, lats, transform=ccrs.PlateCarree(),
                          fontsize=8)

stationplot.plot_parameter('NW', tmpf, color='red')
stationplot.plot_parameter('SW', rh, color='green')
# A more complex example uses a custom formatter to control how the sea-level pressure
# values are plotted. This uses the standard trailing 3-digits of the pressure value
# in tenths of millibars.
stationplot.plot_parameter('NE', slp, color='purple', formatter=lambda v: format(10 * v, '.0f')[-3:])
stationplot.plot_barb(u, v,zorder=2)
plotTitle = f'NYSM Temperature(°F), RH (%), SLP(hPa), Peak 5-min Wind (kts), {titleString}'
ax.set_title (plotTitle);
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[21], line 17
     12 ax.add_feature(USCOUNTIES.with_scale(resCounty), linewidth=0.8, edgecolor='darkgray')
     14 stationplot = StationPlot(ax, lons, lats, transform=ccrs.PlateCarree(),
     15                           fontsize=8)
---> 17 stationplot.plot_parameter('NW', tmpf, color='red')
     18 stationplot.plot_parameter('SW', rh, color='green')
     19 # A more complex example uses a custom formatter to control how the sea-level pressure
     20 # values are plotted. This uses the standard trailing 3-digits of the pressure value
     21 # in tenths of millibars.

NameError: name 'tmpf' is not defined
../../_images/cbd69e83d4173c44ce5fe6a41c4aef43e29ee556ba01f9d4424fc526578da070.png

One last thing to do … plot dewpoint instead of RH. MetPy’s dewpoint_from_relative_humidity takes care of this!

dwpc = dewpoint_from_relative_humidity(tmpf, rh)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[22], line 1
----> 1 dwpc = dewpoint_from_relative_humidity(tmpf, rh)

NameError: name 'tmpf' is not defined
dwpc
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[23], line 1
----> 1 dwpc

NameError: name 'dwpc' is not defined

The dewpoint is returned in units of degrees Celsius, so convert to Fahrenheit.

dwpf = dwpc.to('degF')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[24], line 1
----> 1 dwpf = dwpc.to('degF')

NameError: name 'dwpc' is not defined

Plot the map

res = '10m'
fig = plt.figure(figsize=(18,12),dpi=150) # Increase the dots per inch from default 100 to make plot easier to read
ax = fig.add_subplot(1,1,1,projection=proj)
ax.set_extent ([lonW,lonE,latS,latN])
ax.set_facecolor(cfeature.COLORS['water'])
ax.add_feature(cfeature.STATES.with_scale(res))
ax.add_feature(cfeature.RIVERS.with_scale(res))
ax.add_feature (cfeature.LAND.with_scale(res))
ax.add_feature(cfeature.COASTLINE.with_scale(res))
ax.add_feature (cfeature.LAKES.with_scale(res))
ax.add_feature (cfeature.STATES.with_scale(res))

ax.add_feature(USCOUNTIES.with_scale(resCounty), linewidth=0.8, edgecolor='darkgray')

stationplot = StationPlot(ax, lons, lats, transform=ccrs.PlateCarree(),
                          fontsize=8)

stationplot.plot_parameter('NW', tmpf, color='red')
stationplot.plot_parameter('SW', dwpf, color='green')
# A more complex example uses a custom formatter to control how the sea-level pressure
# values are plotted. This uses the standard trailing 3-digits of the pressure value
# in tenths of millibars.
stationplot.plot_parameter('NE', slp, color='purple', formatter=lambda v: format(10 * v, '.0f')[-3:])
stationplot.plot_barb(u, v,zorder=2)
plotTitle = f'NYSM Temperature and Dewpoint(°F), SLP (hPa), Peak 5-min Wind (kts), {titleString}'
ax.set_title (plotTitle);
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[25], line 18
     13 ax.add_feature(USCOUNTIES.with_scale(resCounty), linewidth=0.8, edgecolor='darkgray')
     15 stationplot = StationPlot(ax, lons, lats, transform=ccrs.PlateCarree(),
     16                           fontsize=8)
---> 18 stationplot.plot_parameter('NW', tmpf, color='red')
     19 stationplot.plot_parameter('SW', dwpf, color='green')
     20 # A more complex example uses a custom formatter to control how the sea-level pressure
     21 # values are plotted. This uses the standard trailing 3-digits of the pressure value
     22 # in tenths of millibars.

NameError: name 'tmpf' is not defined
../../_images/cbd69e83d4173c44ce5fe6a41c4aef43e29ee556ba01f9d4424fc526578da070.png

Save the plot to the current directory.

figName = f'NYSM_{figString}.png'
figName
'NYSM_20240430_0200.png'
fig.savefig(figName)

Summary

  • The MetPy library provides methods to assign physical units to numerical arrays and perform units-aware calculations

  • MetPy’s StationPlot method offers a customized use of Matplotlib’s Pyplot library to plot several meteorologically-relevant parameters centered about several geo-referenced points.

What’s Next?

In the next notebook, we will plot METAR observations from sites across the world, using a different access method to retrieve the data.