Skip to main content

PyWeatherEnriched

Real geocoding + real historical weather, wired into a Rust core with pandas/numpy climate feature engineering on top.

License: Apache 2.0 PyPI Python 3.10+ Tests Graphify

What it is

Given a location name and a timestamp, PyWeatherEnriched geocodes the location (OpenStreetMap Nominatim) and looks up the real, genuinely observed historical weather for that place and hour (Open-Meteo's free Archive API) — no formula-generated or fabricated numbers. The lookup/geocoding core is Rust (via PyO3) for speed and a small memory footprint; a Python layer on top adds pandas/numpy feature engineering (rolling aggregates, heating/cooling degree-days, cyclical time encoding, anomaly z-scores) useful for feeding weather into an ML pipeline.

Features

  • Real historical weather. Open-Meteo Archive API — observed data, not forecasts or formulas.
  • Real forward geocoding. OpenStreetMap Nominatim turns a location name into coordinates for you.
  • Historical backfill. enrich_range(location, start_date, end_date) geocodes once and fetches every hourly observation across the whole range in a single Open-Meteo request.
  • pandas/numpy feature engineering via standalone functions or the build_features convenience pipeline: degree-days (hdd/cdd), cyclical hour/day-of-week/day-of-year encodings, trailing rolling aggregates, and anomaly z-scores.
  • Two-tier caching. A built-in in-memory LRU cache, plus an opt-in SQLite-backed EnhancedCache with geospatial-proximity matching, TTL expiration, batch deduplication, and date-range queries.
  • Honest failure handling. Rows whose lookup fails get NaN rather than raising or fabricating a value; hours with no observation are omitted, not filled.

Who it's for

  • ML/data engineers enriching a training dataset with real historical weather for a location/timestamp pair, instead of a synthetic or averaged fill-in.
  • Teams building weather-sensitive models that need ready-made climate features (rolling aggregates, degree-days, cyclical encodings, anomaly z-scores).
  • Anyone who wants Open-Meteo data without writing the geocoding and feature math themselves.

What it is not

  • Not real-time or forecast weather. This is historical, observed data only.
  • Not a source of hyperlocal data beyond what Open-Meteo provides. The package layers geocoding and feature engineering on top of the same public archive; it does not produce new weather measurements.
  • Some Rust building blocks are not exposed to Python. Elevation/lapse-rate adjustment (real SRTM GeoTIFF parsing), urban-heat-island modeling (real OSM building-density analysis), and OSM-based reverse geocoding are independently unit-tested but not yet wired into the Python API. They live under src/geospatial/.
  • Some layers are deliberately unimplemented. Vegetation/NDVI, soil, and flood-risk layers, plus additional commercial reverse-geocoding provider backends, are framework stubs (src/geospatial/optional.rs) that return a clear "not yet implemented" error rather than fake data.
  • Not fully covered by cargo test. Plain cargo test (without --lib) currently fails to compile because a stale integration test references source files not declared as mods in src/lib.rs. Use cargo test --lib.

Project status

Beta. Version 0.7.0. The Python-facing geocoding, historical weather, caching, and feature-engineering paths are real and tested against live network data; the extra Rust building blocks above are implemented but not yet exposed. Changes are tracked in CHANGELOG.md; see also ROADMAP_HONEST.md.

Installation

pip install pyweatherenriched

Requires Python 3.10+. CI builds prebuilt wheels for Linux (x86_64/ARM64), macOS (Intel/Apple Silicon), and Windows (x86_64) on every release, but the CI→PyPI publish step is currently broken (PyPI rejects the upload with 403 Invalid or non-existent authentication information — a stale/invalid API token), so only a macOS ARM64 abi3 wheel plus an sdist is actually published on PyPI today. On any other platform, pip install falls back to building the sdist from source, which does need a Rust toolchain (see Requirements below).

Quick start

import pyweatherenriched as pwe

enricher = pwe.WeatherEnricher()
result = enricher.enrich_row("New York", "2024-06-15T12:00:00")

print(result)
# {'location': 'New York', 'latitude': 40.7127281, 'longitude': -74.0060152,
#  'temperature': 19.4, 'humidity': 74.0, 'condition': 'Clear',
#  'timestamp': '2024-06-15T12:00:00'}

enrich_row makes two real network calls the first time it sees a location/timestamp pair (one to Nominatim to geocode location, one to Open-Meteo for the historical weather at those coordinates); the in-process cache means repeating the same lookup doesn't hit the network again:

fresh = pwe.WeatherEnricher()
fresh.enrich_row("New York", "2024-06-15T12:00:00")  # network
fresh.enrich_row("New York", "2024-06-15T12:00:00")  # cache hit
print(fresh.cache_stats())  # {'hits': 1, 'misses': 1, 'size': 1}

Usage

Historical backfill

enrich_row/enrich_batch fetch one day of data per call. To backfill a date range for a location, enrich_range geocodes once and fetches every hourly observation across the whole range in a single Open-Meteo request:

enricher = pwe.WeatherEnricher()
rows = enricher.enrich_range("Chicago", "2024-06-01", "2024-06-07")
print(len(rows))  # ~168 (7 days x 24 hours, minus any hours Open-Meteo has no data for)

Hours Open-Meteo has no observation for (e.g. the tail of a range that runs past the latest available data) are omitted from the result rather than filled with a fabricated value.

Feature engineering on a DataFrame

enrich_dataframe bridges the Rust core to pandas — fetch real weather for every row of a DataFrame in one call — and the features functions build standard climate ML features on top of the result:

import pandas as pd
import pyweatherenriched as pwe

orders = pd.DataFrame({
    "order_id": ["A1", "A2", "A3"],
    "location": ["Chicago", "Miami", "Denver"],
    "timestamp": ["2024-01-15T12:00:00", "2024-06-15T12:00:00", "2024-03-15T12:00:00"],
})

enricher = pwe.WeatherEnricher()
enriched = pwe.enrich_dataframe(enricher, orders)     # + latitude/longitude/temperature/humidity/condition
features = pwe.build_features(enriched)                # + hdd/cdd, cyclical time, rolling stats, anomaly z-score

print(features[["order_id", "temperature", "hdd", "cdd", "temperature_zscore"]])

Rows whose lookup fails (unresolvable location, upstream error) get NaN in the new columns instead of raising or fabricating a value, so one bad row doesn't lose the batch.

build_features is a convenience pipeline; each step is also a standalone function:

Function What it adds
add_degree_days(df, temp_col, base_temp=18.0) hdd, cdd — heating/cooling degree-days (max(0, base - t) / max(0, t - base))
add_cyclical_time_features(df, timestamp_col) hour_sin/cos, day_of_week_sin/cos, day_of_year_sin/cos — sin/cos encoding so e.g. 23:59 and 00:00 stay numerically adjacent
add_rolling_features(df, value_col, window, group_col=None, stats=(...)) trailing rolling aggregates (mean/std/min/max/...), optionally computed independently per group
add_anomaly_features(df, value_col, group_col=None, baseline="expanding"|"rolling"|"global") a z-score measuring how unusual each reading is relative to a chosen baseline

Caching

WeatherEnricher has a small built-in LRU cache. For larger workloads, EnhancedCache adds a second, SQLite-backed persistent tier with geospatial-proximity matching, TTL expiration, batch deduplication, and date-range queries:

from pyweatherenriched import EnhancedCache

cache = EnhancedCache(cache_size=5000, db_path="weather_cache.db")
cache.set_proximity_radius(10.0)  # treat lookups within 10km as cache hits
cache.set_ttl(72)                 # hours before an entry expires

cache.put("New York", 40.7128, -74.0060, 15.2, 65.0, "Partly Cloudy", "2024-01-15T12:00:00Z")
result = cache.get("New York", 40.7128, -74.0060, "2024-01-15T12:00:00Z")

# Deduplicate a batch before making any API calls.
batch = [
    ("New York", 40.7128, -74.0060, "2024-01-15T12:00:00Z"),
    ("New York", 40.7128, -74.0060, "2024-01-15T12:00:00Z"),  # duplicate
]
missing_indices, cache_hits = cache.deduplicate_batch(batch)

# Every observation in a date range near a point (requires db_path).
rows = cache.get_range(40.7128, -74.0060, "2024-01-01T00:00:00Z", "2024-01-31T23:59:59Z")

stats = cache.stats()
print(f"hit ratio: {stats['hit_ratio']:.1%}")

get() checks the memory tier, then the persistent tier (if db_path was given), then falls back to a proximity search; get_range only queries the persistent tier, so it always returns [] without db_path.

vs raw Open-Meteo

PyWeatherEnriched is built directly on Open-Meteo's free Archive API, so the honest comparison isn't "who has better weather data" (same real source) — it's "what do you get for the extra network round-trip and processing time." Tested live against real data for 3 real location/timestamp pairs (Chicago/Jan, Miami/Jun, Denver/Mar 2024, noon local):

Raw Open-Meteo (openmeteo-requests, direct) PyWeatherEnriched (enrich_dataframe + build_features)
Input required Real lat/lon (you geocode yourself) Real location name — geocodes internally (Nominatim)
Fields returned temperature, humidity (2 raw fields) + condition, + hdd/cdd, cyclical hour/day/day-of-year encodings, rolling mean/std, anomaly z-score (11 engineered features on top)
Time, 3 locations, cold cache 1.26s 2.63s (geocoding is a second real network call raw Open-Meteo alone doesn't need)
Missing-data handling Raises/errors on a bad request Failed rows get NaN, not fabricated values

Correctness was verified by hand against real output: hdd/cdd matched max(0, 18-t)/max(0, t-18) exactly for all 3 real temperatures, and hour_sin/hour_cos matched sin/cos(2π·hour/24) exactly. Returned temperature/humidity also matched the raw Open-Meteo baseline for the same coordinates/timestamps (rounding only).

Requirements

  • Python 3.10+
  • Rust 1.75+ (only if building from source — not needed to pip install on a platform with a published wheel)
  • Network access to nominatim.openstreetmap.org and archive-api.open-meteo.com at call time (no API key needed for either)

Documentation

Contributing

git clone https://github.com/Mullassery/PyWeatherEnriched.git
cd PyWeatherEnriched

maturin develop --release   # build the Rust extension + install editable
cargo test --lib            # Rust unit tests
pip install -e ".[dev]"
pytest tests/ -v            # Python tests, incl. live-network ones

cargo run --bin pyweatherenriched-validate is a small smoke-test CLI that exercises real geocoding + weather fetch end to end — useful to confirm a build/environment can actually reach Nominatim and Open-Meteo.

Other tools from the same family, for adjacent parts of the data/ML stack:

  • PySynthData — synthetic data generation for ML training: realistic datasets for model development and testing.
  • StatGuardian — fast, declarative data quality framework (Rust): schema validation, drift detection, anomaly detection.
  • PyReverseETL — reverse ETL runtime: move data into CRMs and marketing tools with lineage and compliance enforcement.
  • PyDBTGuard — analyzes a dbt project's manifest.json to flag risky tests, map blast radius, and estimate test cost.
  • PyAirflowTester — dependency intelligence and quality assurance for Airflow and dbt pipelines.

Browse all projects →

License

This project is licensed under the Apache License 2.0.

Metadata

Release files for pyweatherenriched 0.7.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyweatherenriched 0.7.1
File Size Uploaded Provenance Actions
pyweatherenriched-0.7.1.tar.gz 116.0 kB Without provenance Details for pyweatherenriched-0.7.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pyweatherenriched 0.7.1
File Interpreter ABI Platform Actions
pyweatherenriched-0.7.1-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details for pyweatherenriched-0.7.1-cp310-abi3-macosx_11_0_arm64.whl

Total release size: 2.7 MB

Release files / pyweatherenriched-0.7.1.tar.gz

Download URL pyweatherenriched-0.7.1.tar.gz
Size 116.0 kB
Tags Source
SHA-256 checksum
How to use checksums
3e72dcd34281a25ee7f23bd07bc5c2275e43ca18aa8d0bdbef9ed7c57cb63535
BLAKE2b-256 checksum
How to use checksums
fd365f92d6fd6c55cbe7b4df176e12d406f3c2e41f04f47a809f774dc87d6b8e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / pyweatherenriched-0.7.1-cp310-abi3-macosx_11_0_arm64.whl

Download URL pyweatherenriched-0.7.1-cp310-abi3-macosx_11_0_arm64.whl
Size 2.5 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
6562fb45bf5e5c825faab8b8185ca55580f2387645a3e26827485f5d09bb8dcd
BLAKE2b-256 checksum
How to use checksums
05fe0fe4e6eac5ea665f4bfd05ad34e22129585fd3cf555c75aba6fb864fce39
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Security for pyweatherenriched 0.7.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page