Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Import

Authors
Affiliations
Colorado State University
UCAR | UCP | NSF Unidata
CIRES University of Colorado Boulder
Boise State University
Montclair State University
NSF NCAR EOL
University at Albany (SUNY)
import matplotlib.pyplot as plt
import pandas as pd
#from cartopy import crs as ccrs
#from cartopy import feature as cfeature
from datetime import datetime,timedelta
#from dateutil.relativedelta import relativedelta

import geopandas as gpd
from lonboard import viz, Map, ScatterplotLayer, HeatmapLayer
import duckdb

By default, set the date and time to one hour prior to the current time. Or, specify a past date and hour.

Call time by querying

# Use the current time, or set your own for a past time.
# Set current to False if you want to specify a past time.

nowTime = datetime.now()

current = True
current = False
if (current):
    validTime = datetime.now()
    year = validTime.year
    month = validTime.month
    day = validTime.day
    hour = validTime.hour
    validTime = datetime(year, month, day, hour)
    offset = timedelta(hours = 1)
    validTime = validTime - offset
else:
    year = 2026
    month = 1
    day = 1
    hour = 0
    time_1 = datetime(year, month, day, hour)
time_0 = time_1 - timedelta(hours=1)
YYYY_0 = time_0.strftime("%Y")
YYYY_1 = time_1.strftime("%Y")
print(time_0, time_1)
# Handle edge case when the two hours straddle the end/beginning of a yearw
if (YYYY_0 == YYYY_1):
    URLs = [f'https://data.source.coop/dynamical/asos-parquet/year={YYYY_1}/data.parquet']
else:
    URLs = [f'https://data.source.coop/dynamical/asos-parquet/year={YYYY_0}/data.parquet',
            f'https://data.source.coop/dynamical/asos-parquet/year={YYYY_1}/data.parquet']
2025-12-31 23:00:00 2026-01-01 00:00:00
URLs
['https://data.source.coop/dynamical/asos-parquet/year=2025/data.parquet', 'https://data.source.coop/dynamical/asos-parquet/year=2026/data.parquet']
df = duckdb.execute("""
    SELECT *
    FROM read_parquet($1, hive_partitioning=true)
    WHERE 
---      country = 'FR' AND
    valid BETWEEN $2 AND $3
    ORDER BY country
""", [URLs, time_0, time_1]).fetchdf()
Loading...
df
Loading...

tmpc: Temperature in degrees Celsius

from numpy.random import default_rng
from pandas import Series, MultiIndex

rng = default_rng(0)

country = [ 'ZA', 'MX', 'CA', 'RU', 'KR', 'DE', 'BR', 'CN', 'GB', 'US', 'AU', 'IN', 'JP', 'FR']
years = (df['valid'])

index = MultiIndex.from_product([country, years], names=['country', 'valid'])
s = Series(rng.integers(20, 100, size=len(index)), index=index, name='count')

s
country valid ZA 2025-12-31 23:00:00+00:00 88 2025-12-31 23:30:00+00:00 70 2025-12-31 23:00:00+00:00 60 2025-12-31 23:26:00+00:00 41 2025-12-31 23:00:00+00:00 44 .. FR 2025-12-31 23:00:00+00:00 25 2025-12-31 23:00:00+00:00 84 2025-12-31 23:00:00+00:00 67 2025-12-31 23:00:00+00:00 59 2025-12-31 23:47:00+00:00 57 Name: count, Length: 109018, dtype: int64

The Data

from numpy.random import default_rng
from pandas import Series, MultiIndex

#rng = default_rng(0)

country = ['ZA', 'MX', 'CA', 'RU', 'KR', 'DE', 'BR', 'CN', 'GB', 'US', 'AU', 'IN', 'JP', 'FR']
valid = [2000, 2005, 2010, 2015, 2020, 2025]

index = MultiIndex.from_product([country, valid], names=['country', 'valid'])
s = Series(rng.integers(20, 100, size=len(index)), index=index, name='count')

s
country valid ZA 2000 80 2005 97 2010 38 2015 97 2020 31 .. FR 2005 39 2010 48 2015 75 2020 22 2025 53 Name: count, Length: 84, dtype: int64

Bar charts of counting the readings every 5 years from country between 2000 to 2025

from seaborn import barplot

fig, ax = plt.subplots(figsize=(12, 6))

barplot(
    data=s.reset_index(),
    x='country', y='count', hue='valid',
    hue_order=years, palette='viridis_r',
    ax=ax
)
#bars = ax.barh(df['country'], df['tmpc'], color='#4682b4')
ax.legend(ncol=3, title='Year', loc='lower right', bbox_to_anchor=(1, 1))

ax.spines[['top', 'right', 'left']].set_visible(False)
ax.yaxis.grid(color=ax.get_facecolor())
ax.set_xticklabels([t.get_text().title() for t in ax.get_xticklabels()]);
ax.set_xlabel('');
plt.tight_layout()
#plt.savefig('Temp2.pdf', bbox_inches='tight')
# 5. Show the plot
plt.show()
/tmp/ipykernel_4862/3797966388.py:16: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels([t.get_text().title() for t in ax.get_xticklabels()]);
<Figure size 1200x600 with 1 Axes>

Temperature reading between 2025-12-31 23:00:00 and 2026-01-01 00:00:00 across country stations

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

bars = ax.barh(df['country'], df['tmpc'], color='#4682b4')

ax.set_title('Temp in Celsius for 2025-12-31 23:10:00 to 2026-01-01 00:00:00', fontsize=14, pad=15)
ax.set_xlabel('Temp ($^\circ$C)', fontsize=12)
ax.set_xlim(-50, 50)
ax.set_ylabel('Country', fontsize=12)

plt.tight_layout()

# 5. Show the plot
plt.show()
<>:6: SyntaxWarning: "\c" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\c"? A raw string is also an option.
<>:6: SyntaxWarning: "\c" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\c"? A raw string is also an option.
/tmp/ipykernel_4862/3216971234.py:6: SyntaxWarning: "\c" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\c"? A raw string is also an option.
  ax.set_xlabel('Temp ($^\circ$C)', fontsize=12)
<Figure size 1000x600 with 1 Axes>
import pandas as pd
import fsspec
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature

# 1. Load Parquet Data from URL
# parquet_url = "https://data.source.coop/dynamical/asos-parquet/year=2025/data.parquet" # Replace with your actual URL
# with fsspec.open(parquet_url) as file:
#     df = pd.read_parquet(file)

# 2. Set up Matplotlib Figure with Plate Carree projection
fig = plt.figure(figsize=(14, 7))
ax = plt.axes(projection=ccrs.PlateCarree())

# 3. Plot the temperature data (assuming columns: 'longitude', 'latitude', 'temperature')
scatter = ax.scatter(
    df['longitude'], 
    df['latitude'], 
    c=df['tmpc'],  # The temperature parameter
    cmap='coolwarm',      # Colormap for temperature (blue = cold, red = hot)
    s=15,                 # Marker size
    alpha=0.8,
    transform=ccrs.PlateCarree()
)

# 4. Add map features for context
ax.coastlines(linewidth=0.8)
ax.add_feature(cfeature.BORDERS, linestyle=':', alpha=0.5)
ax.add_feature(cfeature.LAND, facecolor='lightgray', alpha=0.3)
ax.add_feature(cfeature.COASTLINE)
ax.add_feature(cfeature.OCEAN, color='lightblue')


# 4. Add geographic visual anchors
ax.coastlines(resolution="110m", color="black", linewidth=1)
ax.gridlines(draw_labels=True, dms=True, xlocs=[-180, -90, 0, 90, 180])

# 5. Add a colorbar and title
cbar = plt.colorbar(scatter, ax=ax, orientation='horizontal', pad=0.05, shrink=0.7)
cbar.set_label('Temperature (°C)', fontsize=12)
plt.title('Global distribution of stations', fontsize=16, pad=15)

# Display the map
plt.show()
/home/runner/micromamba/envs/METAR-archive-cookbook/lib/python3.14/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/110m_physical/ne_110m_land.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
/home/runner/micromamba/envs/METAR-archive-cookbook/lib/python3.14/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/110m_physical/ne_110m_ocean.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
/home/runner/micromamba/envs/METAR-archive-cookbook/lib/python3.14/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/110m_physical/ne_110m_coastline.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
/home/runner/micromamba/envs/METAR-archive-cookbook/lib/python3.14/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/110m_cultural/ne_110m_admin_0_boundary_lines_land.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
<Figure size 1400x700 with 2 Axes>
import pandas as pd
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import numpy as np

# 1. Load data from Parquet
# Ensure your Parquet file has columns named 'lat', 'lon', and 'temperature'
#df = pd.read_csv('global_temperature_data.parquet') 

# 2. Setup the figure and map projection (Flat Globe / Robinson)
fig = plt.figure(figsize=(12, 6))
ax = fig.add_subplot(1, 1, 1, projection=ccrs.Robinson(central_longitude=0))

# 3. Plot the temperature data
# We use a scatter plot, mapping longitude/latitude to the globe
scatter = ax.scatter(
    df['longitude'], df['latitude'], 
    c=df['tmpc'], 
    cmap='coolwarm', 
    s=5, # Marker size
    alpha=0.8,
    transform=ccrs.PlateCarree() # Specifies that the data's original coordinates are lat/lon
)

# 4. Add geographic context and formatting
ax.coastlines(color='black', linewidth=1)
ax.gridlines(draw_labels=True, linestyle='--', color='lightgray')
ax.gridlines(draw_labels=True, dms=True, xlocs=[-180, -120, -60, 0, 60, 120, 180])
ax.add_feature(cfeature.BORDERS, linestyle=':')
ax.add_feature(cfeature.LAND, facecolor='lightgray', alpha=0.5)
ax.add_feature(cfeature.OCEAN, color='lightblue')

# 5. Add a colorbar and title
cbar = plt.colorbar(scatter, ax=ax, orientation='horizontal', pad=0.05, shrink=0.7)
cbar.set_label('Temperature (°C)')
plt.title('Global distribution of stations')

# Show the plot
plt.show()
<Figure size 1200x600 with 2 Axes>