From API to Decision-Ready Meteograms: Unlocking Global 15-Day Hourly Weather Forecasts with Python

In consumer weather apps, a brief summary like "light rain over the next three days, temperatures between 12°C and 18°C" compresses highly complex atmospheric physics into a single sentence. While sufficient for a daily commute, such coarse daily overviews fall short in rigorous data analysis and engineering applications. Whether you are forecasting solar photovoltaic (PV) power output at noon tomorrow, evaluating sudden wind gusts along an autonomous drone inspection route, or analyzing the exact onset and cessation of a precipitation event alongside shifting wind fields and cloud cover, you need continuously sampled hourly meteorological time-series data.

In this technical walkthrough, we explore the Global City Forecast API from Global Weather Data. Beyond demonstrating how to query the API in Python, we will focus on using Matplotlib to organize up to 23 meteorological variables into a clean, reproducible multi-panel meteogram—while systematically avoiding common engineering pitfalls such as timezone mismatches, mixed physical units, improper missing-value handling, and toponymic ambiguity.
Global Forecasts and Historical Weather: A Unified Spatiotemporal Platform
Before writing any code, let's examine the data architecture provided by Global Weather Data. The platform organizes global meteorological resources into three core product lines, available both via RESTful APIs for automated data pipelines and as downloadable flat or multidimensional files:
| Data Product | Spatial Coverage | Temporal Horizon & Update Frequency | Variables | Delivery Format |
|---|---|---|---|---|
| Global City Forecast | Any global location | 15-day hourly, updated 4 times daily | Up to 23 | API or CSV |
| Global Gridded Forecast | Global 0.1° grid | 15-day hourly, updated 4 times daily | Up to 23 | API or NetCDF |
| Global City Historical Weather | 12,000+ urban areas | 10+ years hourly, ~10-day latency | Up to 24 | API or CSV |
These three products correspond to three distinct analytical perspectives in operational workflows:
- Global City Forecast provides a point time-series view, focusing on a specific city or geographic coordinate pair to extract a continuous temporal profile. It is ideal for single-site operational forecasting.
- Global Gridded Forecast offers a spatially continuous grid view, covering the globe at a 0.1° x 0.1° spatial resolution (approximately 10 km grid spacing at mid-latitudes). It captures the spatial distribution of atmospheric fields and is well-suited for regional weather comparisons and spatial interpolation modeling.
- Global City Historical Weather serves as a long-term historical baseline, providing over a decade of hourly observations and reanalysis data (continuous datasets generated by assimilating historical observations into numerical models). It forms the foundation for training machine learning models, conducting historical backtesting, and performing climatological analysis.
Regarding delivery formats, CSV files integrate seamlessly into spreadsheet tools and standard tabular data workflows. Gridded datasets are delivered in NetCDF (Network Common Data Form), the industry-standard self-describing multidimensional array format in atmospheric and oceanic sciences, which pairs naturally with Python's xarray library for spatiotemporal slicing and scientific computing.
Understanding the "4 Daily Rolling Updates" NWP Mechanism
As noted in the product table above, forecast data is updated four times daily on a rolling basis. This schedule aligns with the four standard forecast cycles (or model runs) initiated daily at 00:00, 06:00, 12:00, and 18:00 Coordinated Universal Time (UTC) by major global Numerical Weather Prediction (NWP) models, such as ECMWF and GFS.
Consequently, a 15-day forecast for a given location is not a static table. Every few hours, as the latest global observations from meteorological satellites, weather radars, and surface stations undergo data assimilation (the process of fusing multi-source observations into an NWP model's initial state to minimize initial condition errors), a new model run computes an updated forecast trajectory. In production data engineering, we strongly recommend logging data provenance metadata—such as the request timestamp, query parameters, and API version—whenever persisting forecast data to disk. Without this metadata, conducting accurate model backtesting or post-event error analysis becomes nearly impossible, as you cannot verify which forecast cycle generated a given prediction.
Additionally, note that API rate limits and quotas vary by account tier; always review the official documentation before deploying batch ingestion scripts (basic accounts typically include up to 1,000 free API calls). Keep in mind that both city-level and gridded forecasts represent mesoscale background atmospheric conditions (spanning a few kilometers to several hundred kilometers horizontally) rather than street-level microclimates. Local topography, urban canopies, and the urban canyon effect in high-density built environments can all introduce noticeable deviations between localized in-situ measurements and grid-cell values.
Defining Operational Boundaries: A 15-day forecast is a numerical estimate of medium-range atmospheric evolution. Due to the nonlinear, chaotic nature of atmospheric dynamics, forecast uncertainty grows with lead time, and medium-range forecasts must not be conflated with long-term climate projections. For safety-critical applications—such as aviation, maritime navigation, severe weather mitigation, or industrial safety—never rely solely on a single API chart. Always cross-reference official meteorological warnings, in-situ sensor observations, and industry safety protocols.
Dissecting the Global City Forecast API: From Request Parameters to Columnar JSON
The Global City Forecast API uses a straightforward RESTful URL structure with path parameters:
https://global-weather-data.com/api/forecast/{location}/{api_key} Each request requires two core path parameters:
-
location(Target Location Identifier): The endpoint accepts an English city name, a coordinate pair, or a GeoNames ID. When querying by coordinates, pass a comma-separated string such as-118.24368,34.05223. Note a critical convention in meteorological and GIS systems: coordinates must be specified in Longitude,Latitude order, corresponding to (x, y) on a Cartesian plane—the reverse of the everyday Latitude,Longitude convention. -
api_key(Access Credential): Your unique authentication token obtained from the user dashboard. Treat this token with the same security posture as a database password. Before committing code to public repositories like GitHub, always externalize the key into environment variables or a secrets manager rather than hardcoding it in your scripts.
Among the three location formats, GeoNames ID (e.g., 5368361 for Los Angeles) is the most reliable choice for automated data pipelines. First, countless cities share identical names worldwide (there are dozens of settlements named Paris or Springfield), and a globally unique numeric ID eliminates toponymic ambiguity entirely. Second, city names containing spaces or special characters (like Los Angeles) require URL percent-encoding, whereas numeric IDs keep request URLs clean and predictable. If you pass coordinate pairs, ensure that the comma separating longitude and latitude is preserved unescaped during URL encoding.
Why Columnar Arrays Instead of an Array of Hourly Objects?
Developers new to meteorological APIs often notice that the JSON payload is not structured as a conventional row-oriented list (i.e., an array of 361 objects, each containing all 23 keys for a single hour). Instead, the response uses the following structure:
{ "status": "success", "location_forecast_api_used_count": "...", "location_forecast_api_count_limit": "...", "location_info": { "geonameid": "5368361", "name": "Los Angeles", "admin1": "California", "country": "United States", "longitude": -118.24368, "latitude": 34.05223 }, "forecast_count": 361, "forecast_data": { "time": ["2026-11-03 06:00:00", "..."], "t2m": [16.2, "..."], "d2m": [9.4, "..."], "tp": [0.0, "..."] } } This payload design reflects several deliberate engineering choices:
- Location Metadata Verification (
location_info): When querying by city name, an HTTP 200 status only confirms a successful network round-trip, not necessarily the intended geographic match. Production code should inspectcountry,admin1(first-level administrative division), and coordinates insidelocation_infoto verify that the resolved location matches expectations. - Compact Columnar Layout (
forecast_data): Insideforecast_data,timeis an array of timestamps, whilet2m,tp, and other meteorological variables are parallel numeric arrays of equal length. Elements at the same indexicorrespond to the same forecast validity time (e.g.,forecast_data["tp"][i]is the precipitation atforecast_data["time"][i]). This columnar layout offers two major advantages for time-series workloads: it eliminates the payload overhead of repeating 23 field names 361 times over the wire, and it maps directly to NumPy vectorized operations and Pandas memory structures. Callingpd.DataFrame(payload["forecast_data"])constructs a clean tabular DataFrame in a single line without row-by-row iteration. - Timezone Normalization: Notice that the timestamp strings in
time(e.g.,"2026-11-03 06:00:00") omit a trailingZor UTC offset—making them naive datetime strings. However, per the API specification, all timestamps are strictly in UTC. Never parse these strings using host-local datetime functions; otherwise, scripts running on servers in different timezones will shift the diurnal cycle and misalign sunrise, sunset, and peak heating hours. Always attach explicit UTC timezone metadata during parsing (converting them into timezone-aware datetime objects) to establish a consistent baseline for cross-regional comparisons or local timezone conversions. - Why Does a 15-Day Forecast Return 361 Records? While 15 * 24 = 360 hours,
forecast_countreturns361. This follows the classic fencepost principle (inclusive endpoints): a 360-hour horizon sampled hourly at the top of each hour—including both the initial boundary (hour 0) and the final boundary (hour 360)—yields 360 + 1 = 361 discrete timestamps.
All 23 Meteorological Variables Explained: Physical Definitions and Domain Applications
Our test query for Los Angeles returns 23 meteorological variables. The table below groups them by physical category (note that available fields may vary across product tiers; always rely on the keys returned in your live response):
| Category | Field Keys | Physical Meaning | Unit |
|---|---|---|---|
| Temperature | t2m, d2m, skt | 2 m air temperature, 2 m dew point, skin temperature | °C |
| Wind | u10, v10, u100, v100, fg10 | 10 m & 100 m zonal (u) and meridional (v) wind components, 10 m wind gust | m/s |
| Pressure | msl, sp | Mean sea-level pressure, surface pressure | hPa |
| Precipitation & Snow | tp, sf, sd, ro | Total precipitation, snowfall, snow depth, runoff | mm |
| Cloud Cover | tcc | Total cloud cover | 0--1 |
| Radiation | ssrd, strd, ttr, ssr, str | Surface downward/net solar & thermal radiation, top net thermal radiation | W/m² |
| Convection | mucape | Most-unstable convective available potential energy | J/kg |
| Column Water | tcw, tcwv | Total column water, total column water vapor | kg/m² |
To extract maximum operational value from these parameters, let's examine how meteorologists and domain engineers interpret key variable combinations:
- The Temperature Trio (
t2m,d2m,skt):t2mrepresents standard shelter-level air temperature at 2 meters above ground.d2m(2 m dew point temperature) is the temperature to which air must be cooled—at constant pressure and water vapor content—for saturation and condensation to occur. In meteorological diagnostics, when thed2mcurve converges toward or touchest2m(i.e., the dew point depression approaches zero), near-surface relative humidity is approaching 100%, signaling conditions conducive to fog, dew formation, or oppressive humidity. Meanwhile,skt(skin temperature) measures the radiative equilibrium temperature right at the land-atmosphere interface. Under intense daytime solar heating or clear-sky nocturnal radiative cooling,sktresponds faster and exhibits larger swings than 2 m air temperature, making it a critical indicator for road icing and agricultural frost risk. - Orthogonal Wind Components and 100 m Hub-Height Winds (
u10,v10,u100,v100,fg10): NWP datasets standardly decompose horizontal wind vectors into orthogonal components: zonal velocityu(eastward flow is positive, i.e., a westerly wind) and meridional velocityv(northward flow is positive, i.e., a southerly wind), rather than scalar speed and compass direction. Mathematically, wind direction is a circular variable (0° and 360° are identical), meaning linear arithmetic averaging or interpolation fails across the north boundary (averaging 1° and 359° northerly winds yields a 180° southerly wind). Fromuandvat the same height, scalar wind speed is easily derived via the Euclidean norm sqrt(u² + v²). Notably, the API provides 100 m wind components (u100,v100). Because modern utility-scale wind turbines typically have hub heights between 80 and 120 meters, these two fields serve as direct inputs for wind power forecasting. Finally,fg10(10 m wind gust) captures the peak instantaneous wind speed over the interval, essential for structural wind-load and safety assessments. - Mean Sea-Level Pressure vs. Surface Pressure (
msl,sp): Atmospheric pressure decays exponentially with altitude.spmeasures the actual atmospheric pressure at local terrain elevation; comparingspdirectly between a high-altitude city and a coastal city primarily reflects topography rather than weather patterns. To remove elevation bias and track synoptic-scale systems (such as migrating high-pressure ridges, cold fronts, or deepening low-pressure troughs), meteorologists reduce surface pressure to sea level using the hydrostatic equation, yieldingmsl(mean sea-level pressure). - Solar Shortwave and Thermal Longwave Radiation (
ssrd,ssr, etc.): For solar resource assessment and PV power forecasting,ssrd(surface solar radiation downwards, widely known in the solar industry as Global Horizontal Irradiance, or GHI) is the primary driver. It quantifies the total shortwave solar irradiance (W/m²) reaching a horizontal surface after atmospheric attenuation and cloud scattering, correlating directly with PV array output. - Convective Instability and Column Moisture (
mucape,tcw,tcwv):mucape(most-unstable convective available potential energy) quantifies buoyant thermal instability in the atmosphere. Spikes inmucapeindicate a highly unstable stratification capable of fueling thunderstorms and severe convective updrafts (note that CAPE measures thermodynamic potential rather than precipitation probability). Concurrently,tcwv(total column water vapor, or precipitable water) andtcw(total column water, integrating vapor, cloud liquid water, and cloud ice) measure the total mass of moisture contained within a vertical column of unit cross-section extending from the surface to the top of the atmosphere. Heavy convective precipitation requires both ample moisture supply (hightcwv) and a thermodynamic trigger (highmucape).
Engineering Implementation: Building a 23-Variable Publication-Grade Meteogram in Python
Plotting 23 variables with vastly different physical units and magnitudes on a single vertical axis is impractical—atmospheric pressure (~10³ hPa) would flatten cloud cover (0--1) into an unreadable line along the bottom axis. The standard scientific visualization approach is to group variables by physical dimension and stack them vertically in a shared-time-axis multi-panel figure (sharex=True).
The complete Python script below is ready to run out of the box and incorporates several production-grade engineering practices:
- Network Timeout Protection: Uses
requestswith an explicit timeout to prevent the process from hanging indefinitely during network stalls. - Two-Tier Error Handling: Separates HTTP transport errors from API-level business errors (an HTTP 200 response confirms connectivity, but the JSON
statusfield must still be checked for invalid keys or exhausted quotas). - Explicit UTC Normalization: Parses all timestamps into timezone-aware UTC
datetimeobjects, eliminating any dependency on the host machine's local timezone setting. - Array Alignment & Missing-Value Semantics: Validates that all variable arrays match the timestamp array length, and converts JSON
null(None) values intonp.nan. This distinction is vital: coercing missing data to0misrepresents sensor/model gaps as "zero precipitation, calm winds, or 0°C," whereasNaNinstructs Matplotlib to cleanly break the line and leave a visual gap. - Domain-Appropriate Visual Encoding: Organizes the 23 variables into 8 vertically aligned panels. Continuous state variables (temperature, wind, pressure, radiation) are rendered as line or filled-area plots, while hourly accumulation fluxes (precipitation, snowfall, runoff) are rendered as grouped bar charts alongside a dashed line for snow depth.
First, install the required dependencies in your Python environment:
python -m pip install requests numpy matplotlib Save the following script as plot_weather_forecast.py. Replace API_KEY at the top with your personal credential, and modify LOCATION if you wish to query a different city. Running the script generates both a high-resolution PNG and a vector PDF in the same directory:
from datetime import datetime, timezone from pathlib import Path from urllib.parse import quote import re import matplotlib matplotlib.use("Agg") import matplotlib.dates as mdates import matplotlib.pyplot as plt import numpy as np import requests # Replace with your personal API key from the user dashboard; never commit real keys to public repos. API_KEY = "PASTE_YOUR_API_KEY_HERE" LOCATION = "5368361" # Los Angeles GeoNames ID or city name or coordinations API_URL = "https://global-weather-data.com/api/forecast" OUTPUT_DIR = Path(__file__).parent def fetch_forecast(): if API_KEY == "PASTE_YOUR_API_KEY_HERE": raise ValueError("Please set your API_KEY at the top of the script first.") # quote() encodes spaces and special characters in city names while preserving commas in coordinate pairs. url = f"{API_URL}/{quote(LOCATION, safe=',')}/{API_KEY}" requested_at = datetime.now(timezone.utc) response = requests.get(url, timeout=60) # Check HTTP transport errors (401, 404, 429, 5xx) separately from JSON business status. response.raise_for_status() payload = response.json() if payload.get("status") != "success": raise RuntimeError(payload.get("message", "Forecast API request failed")) return payload, requested_at def as_utc(value): # API timestamp strings may omit timezone suffixes, but the specification guarantees UTC. parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=timezone.utc) return parsed.astimezone(timezone.utc) def draw(payload, requested_at): location = payload["location_info"] forecast = payload["forecast_data"] times = [as_utc(value) for value in forecast["time"]] if not times: raise ValueError("No timestamp records found in forecast_data") if any(len(values) != len(times) for values in forecast.values()): raise ValueError("Length mismatch between time array and meteorological variable arrays") def y(name): # Convert None to NaN to preserve missing-data semantics rather than plotting false zeros. return np.array( [np.nan if value is None else float(value) for value in forecast[name]], dtype=float, ) colors = { "navy": "#17324d", "blue": "#2878b5", "teal": "#168c8c", "orange": "#e07a2d", "coral": "#cf5c5c", "purple": "#8064a2", "gold": "#c49a28", "dark": "#34495e", "cyan": "#43a6b8", } plt.rcParams.update({ "font.family": "DejaVu Sans", "font.size": 8.5, "axes.titlesize": 10, "axes.labelsize": 8.5, "xtick.labelsize": 7.5, "ytick.labelsize": 7.5, "legend.fontsize": 7.1, "axes.edgecolor": "#b8c2cc", "axes.linewidth": 0.7, "grid.color": "#dbe2e8", "grid.linewidth": 0.55, "grid.alpha": 0.75, "savefig.bbox": "tight", }) fig, axes = plt.subplots( 8, 1, figsize=(16, 20.5), sharex=True, gridspec_kw={"height_ratios": [1.05, 1.2, .82, 1, .72, 1.25, .78, .8]}, ) fig.subplots_adjust(left=.075, right=.985, top=.932, bottom=.065, hspace=.24) fig.text( .075, .975, f"{location['name'].upper()} | 15-DAY HOURLY CITY FORECAST", color=colors["navy"], fontsize=16, fontweight="bold", ha="left", va="top", ) fig.text( .075, .953, f"{location.get('admin1', '')}, {location.get('country', '')} | " f"{len(times)} hourly records | ALL TIMES UTC | " f"{times[0]:%d %b %Y %H:%M} - {times[-1]:%d %b %Y %H:%M} UTC", color="#536273", fontsize=9, ha="left", va="top", ) for ax in axes: ax.grid(True, axis="y") ax.grid(True, axis="x", alpha=.38) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) ax.axvline(requested_at, color="#596a7b", lw=1, ls=(0, (4, 3)), zorder=5) # 1. Temperature: three variables sharing a common Celsius axis. for key, label, color, style in [ ("t2m", "2 m air temperature | t2m", colors["navy"], "-"), ("d2m", "2 m dew point | d2m", colors["teal"], "<...>
