Fixing Weather Data in the Wrong Time Zone
On this page
Problem statement
Daily weather values that are off by a day, overnight lows that look too high, rain landing on the wrong date, or a daily series that does not line up with a station's official record are almost always a time-zone problem. Weather data is stored and served in UTC unless you ask otherwise. In the eastern United States the local day starts four or five hours later, so a UTC day mixes the evening of one local day with the night of the next.
This page reproduces the problem with Open-Meteo's archive for Washington, D.C. It requests the same year of daily values with and without a time zone, measures the differences, and checks both against the official daily record at Reagan National Airport. It then fixes hourly station data the same way.
Quick answer
Ask the API for local days with timezone, or convert hourly UTC data to local time before resampling:
utc = archive(daily="temperature_2m_min,temperature_2m_max,precipitation_sum")
loc = archive(daily="temperature_2m_min,temperature_2m_max,precipitation_sum", timezone="America/New_York")
diff = (utc - loc).abs()
print(f"days where the daily minimum differs by more than 1 °C: {(diff.temperature_2m_min > 1).sum()} of {len(diff)}")
print(f"days where precipitation differs by more than 1 mm: {(diff.precipitation_sum > 1).sum()}")
days where the daily minimum differs by more than 1 °C: 40 of 366
days where precipitation differs by more than 1 mm: 88
Asked for UTC days and for local days, the same year differs by more than 1 °C in the daily minimum on 40 of 366 days, and by more than 1 mm of precipitation on 88.
Match the official record, find when days start, convert first
1. Which version matches the official record
off = G.loc["2024"]
def mae(a, b):
j = pd.concat([a, b], axis=1, join="inner").dropna()
return (j.iloc[:, 0] - j.iloc[:, 1]).abs().mean()
print(f"daily minimum vs station: UTC days {mae(utc.temperature_2m_min, off.TMIN):.2f} °C, local days {mae(loc.temperature_2m_min, off.TMIN):.2f} °C")
print(f"daily precipitation vs station: UTC days {mae(utc.precipitation_sum, off.PRCP):.2f} mm, local days {mae(loc.precipitation_sum, off.PRCP):.2f} mm")
daily minimum vs station: UTC days 2.13 °C, local days 2.14 °C
daily precipitation vs station: UTC days 2.62 mm, local days 2.07 mm
Against the station's official record, local days cut the precipitation error from 2.62 to 2.07 mm a day. For the daily minimum, the time zone makes no difference here, 2.13 against 2.14 °C, because the grid cell's own difference from this riverside, urban station is larger than the time-zone effect. With the station's own hourly data, where that bias is absent, local days do cut the error, as the hourly-to-daily guide shows.
2. Find the hour the day starts
The response says which offset it used. Compare:
print(f"without timezone: utc_offset_seconds = {META['utc'].get('utc_offset_seconds')}, timezone {META['utc'].get('timezone')}")
print(f"with timezone: utc_offset_seconds = {META['loc'].get('utc_offset_seconds')}, timezone {META['loc'].get('timezone')} ({META['loc'].get('timezone_abbreviation')})")
without timezone: utc_offset_seconds = 0, timezone GMT
with timezone: utc_offset_seconds = -14400, timezone America/New_York (GMT-4)
Without timezone, the response says utc_offset_seconds = 0 and GMT. With it, -14400 and America/New_York: the summer offset of four hours.
3. Hourly data: convert before resampling
For hourly data in UTC, localise and convert the index, then resample:
h = archive(hourly="temperature_2m")
naive = h.temperature_2m.resample("D").min()
fixed = h.temperature_2m.tz_localize("UTC").tz_convert("America/New_York").resample("D").min()
fixed.index = fixed.index.tz_localize(None)
j = pd.concat([naive, fixed], axis=1, keys=["naive", "fixed"]).dropna()
print(f"daily minimum from hourly data: naive and fixed differ by more than 1 °C on {((j.naive - j.fixed).abs() > 1).sum()} days")
daily minimum from hourly data: naive and fixed differ by more than 1 °C on 43 days
From the hourly series, resampling UTC timestamps and resampling local ones give daily minima more than 1 °C apart on 43 days.
The worst days
The data used on this page
Daily and hourly 2024 data for Washington Reagan National Airport from the Open-Meteo historical weather API (ERA5-based reanalysis), cached on disk; and the station's official daily record from GHCN-Daily (USW00013743), converted from tenths.
import hashlib
import json
import time
from pathlib import Path
import pandas as pd
import requests
Path("data").mkdir(exist_ok=True)
META = {}
def archive(daily=None, hourly=None, timezone=None):
params = {"latitude": 38.8472, "longitude": -77.0345, "start_date": "2024-01-01", "end_date": "2024-12-31"}
if daily: params["daily"] = daily
if hourly: params["hourly"] = hourly
if timezone: params["timezone"] = timezone
f = Path("data/om_" + hashlib.md5(json.dumps(params, sort_keys=True).encode()).hexdigest() + ".json")
if not f.exists():
for attempt in range(6):
r = requests.get("https://archive-api.open-meteo.com/v1/archive", params=params, timeout=300)
if r.status_code == 429:
time.sleep(60 * (attempt + 1)); continue
r.raise_for_status(); f.write_text(r.text); break
js = json.loads(f.read_text())
META["loc" if timezone else "utc"] = js
return pd.DataFrame(js["daily" if daily else "hourly"]).assign(time=lambda d: pd.to_datetime(d.time)).set_index("time")
CSV = Path("data/USW00013743.csv")
if not CSV.exists():
r = requests.get("https://www.ncei.noaa.gov/data/global-historical-climatology-network-daily/access/USW00013743.csv", timeout=600)
r.raise_for_status(); CSV.write_bytes(r.content)
G = pd.read_csv(CSV, usecols=["DATE", "TMAX", "TMIN", "PRCP"], parse_dates=["DATE"]).set_index("DATE") / 10
print("ready")
ready
The helpers are ready.
Example 1: the worst days
List the days where the UTC and local daily minimum differ most, with the official value:
w = pd.DataFrame({"UTC day": utc.temperature_2m_min, "local day": loc.temperature_2m_min, "station": off.TMIN})
w["gap"] = (w["UTC day"] - w["local day"]).abs()
print(w.nlargest(4, "gap").round(1).to_string())
UTC day local day station gap
2024-05-03 17.3 12.0 12.8 5.3
2024-02-28 13.0 8.1 5.6 4.9
2024-10-14 14.2 10.3 10.0 3.9
2024-01-25 7.7 11.4 6.7 3.7
On 3 May the UTC day's minimum is 17.3 °C, the local day's 12.0 °C, and the station recorded 12.8 °C. The UTC version took its lowest value from a warm evening hour that belongs to the next local day. On 25 January the error runs the other way.
UTC data, local days
Weather observations and model output use UTC because data from everywhere has to fit on one clock. A day, though, is local. Daily minima are usually reached around dawn and maxima in mid-afternoon, and a storm in the evening belongs to that evening's date. When the day is cut at midnight UTC, 7 or 8 pm in Washington, the last hours of each local day are assigned to the next one. Daily maxima are rarely affected, because the afternoon is well inside both definitions. Minima and precipitation shift often, and the error looks like noise unless it is compared with a reference.
The fix is to decide which day definition is wanted and apply it before aggregating. For APIs that aggregate on the server, such as Open-Meteo, pass timezone, or timezone=auto for the location's own zone. For hourly data, convert the index from UTC to the local zone, then resample. Official US station days are in local standard time all year, so matching them exactly during daylight saving time needs a fixed offset rather than the civil zone.
Naive times, daylight saving and climatological days
- Naive timestamps: a timestamp without a zone is ambiguous; establish whether it is UTC before doing anything else.
- Daylight saving: converting to a civil zone gives 23- and 25-hour days; resampling handles them, fixed-offset zones avoid them.
- Multi-location requests:
timezone=autogives each location its own local days. - Climatological days: some networks end the observing day at 7 am local, not midnight; check the metadata before comparing.
Internal links
- How to convert hourly weather data to daily in Python — resampling in detail
- How to get historical weather data in Python — the archive API
- Fixing daily fire detection counts on the wrong day — the same trap with satellite data
FAQ
Why are my daily weather values off by a day?
The days are probably counted in UTC; ask for local time (timezone= in Open-Meteo) or convert hourly data to local time before resampling.
What time zone is weather data in?
Usually UTC; check the documentation and the response metadata.
Which daily values are most affected by the time zone?
Daily minimum temperature and precipitation; daily maxima change less because afternoons fall inside both days.
How do I match official US daily weather records?
Use local standard time for the day boundary, all year round.