Weather Data Dashboard: Pulling, Storing & Visualizing Live API Data

Data Science & ML Projects (Beginner)

Chapter 3 · Weather Data Dashboard: Pulling, Storing & Visualizing Live API Data

Every project so far has worked with data that already existed somewhere — a scraped page, a saved CSV. This chapter's data doesn't exist until the moment it's requested: a live weather API, parsed with ds1-3's own JSON-handling skills applied to a live response instead of a file on disk, then charted with ds1-7's and ds1-8's own tools.

What We're Building

A small tool that fetches an hourly weather forecast from Open-Meteo — a genuinely free, public weather API that needs no signup or API key, which is exactly why it's used here — turns the response into a DataFrame, appends it to a growing local dataset, and builds two small charts from it: a temperature-over-time line chart and a precipitation-by-hour bar chart.

Step 1: Making the API Request

import requests

params = {
    "latitude": 51.51,     # London, as an example — swap for any location
    "longitude": -0.13,
    "hourly": "temperature_2m,precipitation",
    "forecast_days": 2,
}
response = requests.get("https://api.open-meteo.com/v1/forecast", params=params)
print(response.status_code)   # 200 — success

Passing a params dict to requests.get() builds the full query-string URL automatically — no manual string concatenation needed. No API key here, unlike most weather services, which is exactly why this API was chosen for a beginner project: nothing to configure, sign up for, or accidentally commit to a public repository.

Step 2: Parsing JSON From a Live Response

data = response.json()
print(list(data["hourly"].keys()))
# ['time', 'temperature_2m', 'precipitation']
The same JSON, arriving a different way
ds1-3 read JSON with json.load(open(path)) — a file that already existed on disk. Here, response.json() does the identical parsing job, but on a response body that only came into existence the moment this script made the request. The structure once parsed is identical (nested dicts and lists); only its source differs.

Step 3: A Genuinely Different JSON Shape

import pandas as pd

hourly = data["hourly"]
df = pd.DataFrame({
    "time": pd.to_datetime(hourly["time"]),
    "temperature": hourly["temperature_2m"],
    "precipitation": hourly["precipitation"],
})
print(df.head())
An honest structural difference from earlier JSON
Open-Meteo returns parallel lists — one list of every timestamp, one list of every temperature, in matching order — rather than a list of individual row-like records the way most of this course's own earlier JSON examples looked. Building a DataFrame here means passing a dict of equal-length lists directly to pd.DataFrame(), not looping over a list of row dicts. Real APIs don't all agree on one shape; reading the actual response before assuming its structure is the real skill.

Step 4: Storing Results Locally Over Time

from pathlib import Path

def append_to_log(df, path="weather_log.csv"):
    if Path(path).exists():
        df.to_csv(path, mode="a", header=False, index=False)
    else:
        df.to_csv(path, index=False)
    print(f"Appended {len(df)} rows to {path}.")

Running this script once a day builds a genuinely growing local weather history out of nothing but repeated forecast pulls — mode="a" appends rather than overwriting, and skipping the header on every call after the first keeps the CSV valid.

Step 5: Building the Dashboard

import matplotlib.pyplot as plt

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6))

ax1.plot(df["time"], df["temperature"], color="#15803D")
ax1.set_title("Temperature Forecast")
ax1.set_ylabel("°C")

ax2.bar(df["time"], df["precipitation"], color="#EAB308")
ax2.set_title("Precipitation Forecast")
ax2.set_ylabel("mm")

plt.tight_layout()
plt.savefig("weather_dashboard.png")

ds1-7's own line-chart-for-a-trend and bar-chart-for-discrete-values choices, applied directly — temperature is a continuous value changing over time (a line), precipitation at each hour is a discrete quantity (a bar). Two subplots stacked in one figure make a small, genuine dashboard rather than two disconnected charts.

The Complete Script

import requests, pandas as pd, matplotlib.pyplot as plt
from pathlib import Path

params = {"latitude": 51.51, "longitude": -0.13,
          "hourly": "temperature_2m,precipitation", "forecast_days": 2}
data = requests.get("https://api.open-meteo.com/v1/forecast", params=params).json()

hourly = data["hourly"]
df = pd.DataFrame({
    "time": pd.to_datetime(hourly["time"]),
    "temperature": hourly["temperature_2m"],
    "precipitation": hourly["precipitation"],
})

log_path = "weather_log.csv"
df.to_csv(log_path, mode="a" if Path(log_path).exists() else "w",
          header=not Path(log_path).exists(), index=False)

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6))
ax1.plot(df["time"], df["temperature"], color="#15803D")
ax1.set_title("Temperature Forecast")
ax2.bar(df["time"], df["precipitation"], color="#EAB308")
ax2.set_title("Precipitation Forecast")
plt.tight_layout()
plt.savefig("weather_dashboard.png")
print("Dashboard updated.")
Being a good citizen of a free public API
Open-Meteo is free specifically for reasonable use — it isn't an invitation to poll it every second. Running this script on a schedule (once an hour, once a day) via a real scheduler is the appropriate pattern, not a tight loop. The same courtesy from Chapter 1's own scraping-ethics warn-box applies to APIs too: check a service's documented rate limits and terms before building anything that calls it repeatedly.

Query parameters as a dict

requests.get(url, params={...}) builds the URL automatically.

response.json()

Parses a live response body the same way ds1-3 parsed a saved file.

Parallel-list JSON

Some APIs return matching lists instead of row-like records — read before assuming.

Appending, not overwriting

mode="a" builds a real local history out of repeated small pulls.

Extend This Project

Try these on your own:

  • Add a second location and plot both on the same temperature chart, with a legend, to compare two cities at once.
  • Wrap the request in try/except so a network failure logs a message instead of crashing the whole script.
  • Once weather_log.csv has several days of real history, load it back in and plot a week-long trend instead of just the latest forecast.
  • Use a Seaborn heatmap (ds1-8) to show temperature by hour-of-day across several logged days at once.

What's Next

Chapter 4: Personal Finance Analyzer, Revisited With pandas — taking py4-8's own plain-Python expense tracker and rebuilding its analysis with the pandas/numpy toolkit this course has been using all along.

Chapter 3 Quick Reference

  • The first project pulling from a live API rather than a static file — data that doesn't exist until requested
  • requests.get(url, params={...}) builds a query-string URL automatically; .json() parses the live response, same job as ds1-3's own json.load()
  • Real APIs don't share one JSON shape — Open-Meteo's own parallel-list structure required a genuinely different DataFrame-building approach
  • Appending (mode="a") to a CSV over repeated runs builds a real local dataset out of small live pulls
  • ds1-7's line-vs-bar chart choice applied directly: continuous trend (temperature) as a line, discrete quantity (precipitation) as a bar
  • Free APIs still have limits — schedule pulls reasonably, don't poll in a tight loop