Meteogram#

Here we draw a surface meteogram, from data that we retrieve from our METAR archive.#

Imports#

import numpy as np
import pandas as pd

import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter, AutoDateLocator,YearLocator, HourLocator,DayLocator,MonthLocator

from metpy.units import units
from datetime import datetime, timedelta
import seaborn as sns

Set the desired start and end time (UTC), and create strings for the figure title.

startTime = datetime(2025,3,28,0)
endTime = datetime(2025,3,31,0)
sTimeStr = startTime.strftime(format="%x %H UTC")
eTimeStr = endTime.strftime(format="%x %H UTC")

Create an empty Dateframe to which we will concatenate the hourly CSV files in the desired time range.

df = pd.DataFrame()

Loop over each hour. Concatenate each hour’s METARs into the full Dataframe.

curTime = startTime
while (curTime <= endTime):
    print (curTime)
    timeStr = curTime.strftime("%y%m%d%H")
    metarCSV = f'/ktyle_rit/scripts/sflist2/complete/{timeStr}.csv'
    dfTemp = pd.read_csv(metarCSV, sep='\\s+')
    df = pd.concat([df, dfTemp], ignore_index=True)
    curTime = curTime + timedelta(hours=1)
2025-03-28 00:00:00
2025-03-28 01:00:00
2025-03-28 02:00:00
2025-03-28 03:00:00
2025-03-28 04:00:00
2025-03-28 05:00:00
2025-03-28 06:00:00
2025-03-28 07:00:00
2025-03-28 08:00:00
2025-03-28 09:00:00
2025-03-28 10:00:00
2025-03-28 11:00:00
2025-03-28 12:00:00
2025-03-28 13:00:00
2025-03-28 14:00:00
2025-03-28 15:00:00
2025-03-28 16:00:00
2025-03-28 17:00:00
2025-03-28 18:00:00
2025-03-28 19:00:00
2025-03-28 20:00:00
2025-03-28 21:00:00
2025-03-28 22:00:00
2025-03-28 23:00:00
2025-03-29 00:00:00
2025-03-29 01:00:00
2025-03-29 02:00:00
2025-03-29 03:00:00
2025-03-29 04:00:00
2025-03-29 05:00:00
2025-03-29 06:00:00
2025-03-29 07:00:00
2025-03-29 08:00:00
2025-03-29 09:00:00
2025-03-29 10:00:00
2025-03-29 11:00:00
2025-03-29 12:00:00
2025-03-29 13:00:00
2025-03-29 14:00:00
2025-03-29 15:00:00
2025-03-29 16:00:00
2025-03-29 17:00:00
2025-03-29 18:00:00
2025-03-29 19:00:00
2025-03-29 20:00:00
2025-03-29 21:00:00
2025-03-29 22:00:00
2025-03-29 23:00:00
2025-03-30 00:00:00
2025-03-30 01:00:00
2025-03-30 02:00:00
2025-03-30 03:00:00
2025-03-30 04:00:00
2025-03-30 05:00:00
2025-03-30 06:00:00
2025-03-30 07:00:00
2025-03-30 08:00:00
2025-03-30 09:00:00
2025-03-30 10:00:00
2025-03-30 11:00:00
2025-03-30 12:00:00
2025-03-30 13:00:00
2025-03-30 14:00:00
2025-03-30 15:00:00
2025-03-30 16:00:00
2025-03-30 17:00:00
2025-03-30 18:00:00
2025-03-30 19:00:00
2025-03-30 20:00:00
2025-03-30 21:00:00
2025-03-30 22:00:00
2025-03-30 23:00:00
2025-03-31 00:00:00

Examine the Dataframe

df
STN YYMMDD/HHMM SLAT SLON SELV PMSL ALTI TMPC DWPC SKNT ... P03C CTYL CTYM CTYH P06I T6XC T6NC CEIL P01I SNEW
0 NUW 250328/0000 48.35 -122.65 14.0 1001.0 29.55 13.3 5.6 12.0 ... -1.7 -9999.0 -9999.0 -9999.0 0.0 16.1 12.2 110.0 0.0 -9999.0
1 NYL 250328/0000 32.65 -114.62 65.0 1008.3 29.78 31.1 5.6 6.0 ... -2.5 -9999.0 -9999.0 -9999.0 -9999.0 32.8 26.1 -9999.0 -9999.0 -9999.0
2 PAGA 250328/0000 64.73 -156.93 46.0 1025.7 30.28 -7.8 -16.1 6.0 ... -0.7 -9999.0 -9999.0 -9999.0 -9999.0 -7.8 -14.4 -9999.0 -9999.0 -9999.0
3 PAKN 250328/0000 58.68 -156.65 15.0 1014.3 29.95 -0.6 -7.2 18.0 ... -1.1 -9999.0 -9999.0 -9999.0 -9999.0 -0.6 -8.3 -9999.0 -9999.0 -9999.0
4 CACQ 250328/0000 47.00 -65.45 34.0 1014.1 -9999.00 0.0 -1.0 1.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
324506 SKPD 250331/0000 -9999.00 -9999.00 -9999.0 -9999.0 29.80 26.0 23.0 10.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 31.0 -9999.0 -9999.0
324507 SKPP 250331/0000 -9999.00 -9999.00 -9999.0 -9999.0 30.09 17.0 17.0 2.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0
324508 SKPS 250331/0000 -9999.00 -9999.00 -9999.0 -9999.0 30.09 17.0 16.0 1.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 32.0 -9999.0 -9999.0
324509 SKPV 250331/0000 -9999.00 -9999.00 -9999.0 -9999.0 29.83 28.0 23.0 5.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0
324510 SKQU 250331/0000 -9999.00 -9999.00 -9999.0 -9999.0 29.80 25.0 24.0 1.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 150.0 -9999.0 -9999.0

324511 rows × 35 columns

Select the station from which to subset the Dataframe.

site = 'LGA'

Create a new Dataframe that contains only the rows for the desired site.

dfSub = df.query('STN == @site')

Create a new column that represents the date and time as a datetime64 object.

# Next line suppresses a warning message. See https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy. 
# This won't be necessary with Pandas version 3 and beyond.

pd.options.mode.copy_on_write = True 

dattim = pd.to_datetime(dfSub['YYMMDD/HHMM'],format="%y%m%d/%H%M", utc=True)
dfSub['DATETIME'] = dattim

Examine the subsetted Dataframe.

dfSub
STN YYMMDD/HHMM SLAT SLON SELV PMSL ALTI TMPC DWPC SKNT ... CTYL CTYM CTYH P06I T6XC T6NC CEIL P01I SNEW DATETIME
941 LGA 250328/0000 40.77 -73.9 9.0 1022.2 30.19 10.6 -12.2 11.0 ... -9999.0 -9999.0 -9999.0 -9999.0 11.7 8.9 -9999.0 -9999.0 -9999.0 2025-03-28 00:00:00+00:00
5234 LGA 250328/0100 40.77 -73.9 9.0 1022.9 30.21 10.6 -12.2 11.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 2025-03-28 01:00:00+00:00
9504 LGA 250328/0200 40.77 -73.9 9.0 1022.9 30.21 10.0 -11.7 8.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 2025-03-28 02:00:00+00:00
13737 LGA 250328/0300 40.77 -73.9 9.0 1022.9 30.21 9.4 -11.7 7.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 2025-03-28 03:00:00+00:00
17969 LGA 250328/0400 40.77 -73.9 9.0 1022.4 30.19 9.4 -10.6 9.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 2025-03-28 04:00:00+00:00
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
303216 LGA 250330/2000 40.77 -73.9 9.0 1018.4 30.08 7.2 3.9 13.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 9.0 -9999.0 -9999.0 2025-03-30 20:00:00+00:00
307672 LGA 250330/2100 40.77 -73.9 9.0 1017.7 30.06 7.2 4.4 11.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 9.0 -9999.0 -9999.0 2025-03-30 21:00:00+00:00
312138 LGA 250330/2200 40.77 -73.9 9.0 1017.5 30.05 7.8 4.4 11.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 9.0 -9999.0 -9999.0 2025-03-30 22:00:00+00:00
316597 LGA 250330/2300 40.77 -73.9 9.0 1017.3 30.04 7.2 4.4 10.0 ... -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 -9999.0 4.0 -9999.0 -9999.0 2025-03-30 23:00:00+00:00
321038 LGA 250331/0000 40.77 -73.9 9.0 1016.9 30.03 6.7 4.4 8.0 ... -9999.0 -9999.0 -9999.0 -9999.0 7.8 6.7 4.0 -9999.0 -9999.0 2025-03-31 00:00:00+00:00

73 rows × 36 columns

Select a couple of variables we wish to plot on the meteogram. In this case, 2m temperature and dewpoint.

var1 = dfSub['TMPC']
var2 = dfSub['DWPC']

Examine one of the variables … of course, it’s a Pandas Series.

var1
941       10.6
5234      10.6
9504      10.0
13737      9.4
17969      9.4
          ... 
303216     7.2
307672     7.2
312138     7.8
316597     7.2
321038     6.7
Name: TMPC, Length: 73, dtype: float64

Pandas Series objects don’t support units. We can, however, create Numpy arrays from the Series values attribute and then attach units to them.

tmpc = var1.values * units('degC')
dwpc = var2.values * units('degC')

Examine one of the units-aware arrays.

tmpc
Magnitude
[10.6 10.6 10.0 9.4 9.4 8.9 8.9 8.9 8.9 8.9 8.9 8.9 10.0 10.0 11.1 12.8 14.4 15.0 16.1 16.7 17.8 15.6 13.9 13.3 11.7 11.7 13.3 13.3 13.3 12.2 11.7 10.6 10.0 12.8 15.0 16.1 16.7 17.8 18.9 21.7 23.9 26.1 27.2 26.7 26.1 10.6 8.9 10.6 10.0 8.9 8.9 8.3 6.7 6.1 6.1 6.1 5.6 5.6 6.1 6.1 6.7 6.7 7.2 7.8 6.7 7.8 7.2 7.2 7.2 7.2 7.8 7.2 6.7]
Unitsdegree_Celsius

Now convert into degrees F.

tmpf = tmpc.to('degF')
dwpf = dwpc.to('degF')

Create the meteogram and save the figure to disk.#

sns.set()

fig, ax = plt.subplots(figsize=(15, 10))

# Improve on the default ticking

ax.xaxis.set_major_locator(HourLocator(interval=3))
hoursFmt = DateFormatter('%d/%H')
ax.xaxis.set_major_formatter(hoursFmt)

ax.plot(dfSub['DATETIME'], tmpf, color='tab:red', label='Temp')
ax.plot(dfSub['DATETIME'], dwpf, color='tab:green', label='Dwpt')

ax.set_title(f'Site: {site}      Date Range: {sTimeStr} - {eTimeStr}')

ax.set_xlabel('Hour (UTC)')
ax.set_ylabel('Temperature °F')
ax.set_xlim(startTime, endTime)
ax.legend(loc='best')

fig.savefig (f'{site}_mgram.png')
../../_images/c286405f21e7dd84050634b29b57c7ca84bc2284e8f77d247d03997276b3a809.png

Things to try:

  1. Basic Select a site and time range relevant for your case
  2. Basic Create a multi-panel figure, each displaying a variable of interest
  3. Advanced Plot two variables that have different units; create corresponding y-axis labels on the left and right sides of the plot