Download code/fetch_weather.py from Elpriser/eu-price-forecast: direct link, hf CLI and curl.
- Browser
- Download file 6.34 kB
-
https://huggingface.co/Elpriser/eu-price-forecast/resolve/main/code/fetch_weather.py
- Command line
-
hf download hf://Elpriser/eu-price-forecast/code/fetch_weather.py
-
curl -L -o fetch_weather.py https://huggingface.co/Elpriser/eu-price-forecast/resolve/main/code/fetch_weather.py
6.34 kB
| #!/usr/bin/env python3 | |
| """Weather for the EU zones' points, as the forecast was ISSUED, from Open-Meteo's previous-runs API. | |
| previous_dayN is the forecast for an hour as it stood N days before, so a training row carries exactly the weather | |
| quality the live model gets at the same horizon (the Nordic and Danish models do the same). Only leads 2..7 are | |
| fetched: the model's horizon h = 2..7 uses lead h, and lead 1 would never be read. | |
| History is cached as one parquet per weather point (w_prev_<zone>_<i>.parquet, hours in UTC); a later run re-fetches | |
| the last three days and merges, because the newest previous-runs values are still filled in when first read. Open-Meteo | |
| weights a call by points x variables x days, so the first run is paced (RAMP pause) to stay under the hourly limit of | |
| the free tier; the daily top-up is a handful of calls. | |
| live() returns the CURRENT 10-day forecast for every point, for scoring. | |
| """ | |
| import json | |
| import os | |
| import sys | |
| import time | |
| import urllib.error | |
| import urllib.request | |
| from datetime import date, timedelta | |
| import pandas as pd | |
| DIR = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, DIR) | |
| from zones import all_points # noqa: E402 | |
| VARS = ["wind_speed_100m", "direct_radiation", "temperature_2m", "precipitation"] | |
| LEADS = range(2, 8) | |
| PREV_VARS = [f"{v}_previous_day{d}" for v in VARS for d in LEADS] | |
| PREV_START = date(2024, 3, 15) # the previous-runs archive starts here | |
| PER_CALL = 5 # points per request | |
| CHUNK_MONTHS = 3 | |
| RAMP_PAUSE = 55 # seconds between history requests (hourly limit 5,000 weighted calls) | |
| BASE = "https://previous-runs-api.open-meteo.com/v1/forecast" | |
| LIVE = "https://api.open-meteo.com/v1/forecast" | |
| def get(url, tries=14): | |
| for i in range(tries): | |
| try: | |
| with urllib.request.urlopen(url, timeout=240) as r: | |
| return json.load(r) | |
| except urllib.error.HTTPError as e: | |
| if i == tries - 1: | |
| raise | |
| wait = min(70 * (i + 1), 600) if e.code in (429, 503) else 20 * (i + 1) | |
| print(f" HTTP {e.code}, retry in {wait}s", flush=True) | |
| time.sleep(wait) | |
| except Exception as e: | |
| if i == tries - 1: | |
| raise | |
| print(f" {type(e).__name__}, retry in {min(60 * (i + 1), 300)}s", flush=True) | |
| time.sleep(min(60 * (i + 1), 300)) | |
| def months(start, end, n=CHUNK_MONTHS): | |
| cur = start | |
| while cur <= end: | |
| nxt = (cur.replace(day=1) + timedelta(days=31 * n)).replace(day=1) | |
| yield cur, min(nxt - timedelta(days=1), end) | |
| cur = nxt | |
| def path(zone, i, out_dir=DIR): | |
| return os.path.join(out_dir, f"w_prev_{zone}_{i}.parquet") | |
| def _frame(loc): | |
| df = pd.DataFrame(loc.get("hourly", {})) | |
| if len(df): | |
| df["time"] = pd.to_datetime(df["time"]) | |
| return df | |
| def _request(pts, s, e): | |
| lats = ",".join(str(p[2]) for p in pts) | |
| lons = ",".join(str(p[3]) for p in pts) | |
| res = get(f"{BASE}?latitude={lats}&longitude={lons}&hourly={','.join(PREV_VARS)}" | |
| f"&start_date={s}&end_date={e}&timezone=GMT") | |
| return res if isinstance(res, list) else [res] | |
| def fetch_history(out_dir=DIR, today=None, pause=RAMP_PAUSE, only=None): | |
| """Create or top up every point's cache. Points are grouped PER_CALL to a request; `only` is an optional set of group | |
| indexes (so a first run can be split over several processes, each its own groups).""" | |
| today = today or date.today() | |
| yesterday = today - timedelta(days=1) | |
| points = all_points() | |
| for g in range(0, len(points), PER_CALL): | |
| if only is not None and g // PER_CALL not in only: | |
| continue | |
| group = points[g:g + PER_CALL] | |
| have = [os.path.exists(path(p[0], p[1], out_dir)) for p in group] | |
| if all(have): | |
| last = min(pd.to_datetime(pd.read_parquet(path(p[0], p[1], out_dir), columns=["time"]).time).max().date() | |
| for p in group) | |
| lo = last - timedelta(days=2) | |
| if lo >= yesterday: | |
| continue | |
| ranges = [(lo, yesterday)] | |
| old = {(p[0], p[1]): pd.read_parquet(path(p[0], p[1], out_dir)) for p in group} | |
| else: | |
| ranges = list(months(PREV_START, yesterday)) | |
| old = {} | |
| frames = {(p[0], p[1]): [] for p in group} | |
| for s, e in ranges: | |
| print(f"history {group[0][0]}.. {s}..{e}", flush=True) | |
| for p, loc in zip(group, _request(group, s, e)): | |
| frames[(p[0], p[1])].append(_frame(loc)) | |
| time.sleep(3 if old else pause) | |
| for p in group: | |
| key = (p[0], p[1]) | |
| parts = ([old[key]] if key in old else []) + [f for f in frames[key] if len(f)] | |
| df = pd.concat(parts).drop_duplicates("time", keep="last").sort_values("time") | |
| df.to_parquet(path(p[0], p[1], out_dir), index=False) | |
| print(f" saved {p[0]}_{p[1]}: {len(df)} h to {df.time.max():%Y-%m-%d}", flush=True) | |
| def load_zone(zone, n_points, out_dir=DIR): | |
| """The zone's weather as ONE frame (UTC hours): the mean over its points, columns as PREV_VARS.""" | |
| frames = [] | |
| for i in range(n_points): | |
| df = pd.read_parquet(path(zone, i, out_dir)).set_index("time") | |
| frames.append(df[PREV_VARS]) | |
| return sum(frames) / len(frames) if len(frames) > 1 else frames[0] | |
| def live(horizon_days=10): | |
| """{zone: DataFrame(UTC hourly index; columns VARS)} - the current forecast, point-averaged per zone.""" | |
| points = all_points() | |
| per_zone = {} | |
| for g in range(0, len(points), 10): | |
| group = points[g:g + 10] | |
| lats = ",".join(str(p[2]) for p in group) | |
| lons = ",".join(str(p[3]) for p in group) | |
| res = get(f"{LIVE}?latitude={lats}&longitude={lons}&hourly={','.join(VARS)}" | |
| f"&forecast_days={horizon_days}&past_days=1&timezone=GMT") | |
| res = res if isinstance(res, list) else [res] | |
| for p, loc in zip(group, res): | |
| df = _frame(loc).set_index("time")[VARS] | |
| per_zone.setdefault(p[0], []).append(df) | |
| time.sleep(2) | |
| return {z: (sum(v) / len(v)) for z, v in per_zone.items()} | |
| if __name__ == "__main__": | |
| fetch_history(only={int(a) for a in sys.argv[1:]} or None) | |
| print("WEATHER DONE", flush=True) | |