Geographical Support

Time series data often originates from physical locations — weather stations, wind farms, grid nodes.
TimeDataModel provides first-class geographical primitives so that location and data travel together.

Class

Purpose

GeoLocation

A validated (latitude, longitude) point

GeoArea

A shapely Polygon region (optional shapely dependency)

Both can be attached to TimeSeries and TimeSeriesTable objects.

[1]:
from datetime import datetime, timedelta, timezone

import numpy as np

import timedatamodel as tdm

base = datetime(2024, 1, 15, tzinfo=timezone.utc)
timestamps = [base + timedelta(hours=i) for i in range(24)]
rng = np.random.default_rng(42)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 5
      1 from datetime import datetime, timedelta, timezone
      3 import numpy as np
----> 5 import timedatamodel as tdm
      7 base = datetime(2024, 1, 15, tzinfo=timezone.utc)
      8 timestamps = [base + timedelta(hours=i) for i in range(24)]

ModuleNotFoundError: No module named 'timedatamodel'

GeoLocation — point coordinates

GeoLocation is a frozen dataclass that validates latitude ∈ [−90, 90] and longitude ∈ [−180, 180].

[2]:
oslo = tdm.GeoLocation(latitude=59.91, longitude=10.75)
bergen = tdm.GeoLocation(latitude=60.39, longitude=5.32)
tromsoe = tdm.GeoLocation(latitude=69.65, longitude=18.96)

print(f"Oslo:    {oslo}")
print(f"Bergen:  {bergen}")
print(f"Tromsø:  {tromsoe}")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[2], line 1
----> 1 oslo = tdm.GeoLocation(latitude=59.91, longitude=10.75)
      2 bergen = tdm.GeoLocation(latitude=60.39, longitude=5.32)
      3 tromsoe = tdm.GeoLocation(latitude=69.65, longitude=18.96)

NameError: name 'tdm' is not defined
[3]:
try:
    bad = tdm.GeoLocation(latitude=200, longitude=0)
except ValueError as e:
    print(f"Validation error: {e}")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 2
      1 try:
----> 2     bad = tdm.GeoLocation(latitude=200, longitude=0)
      3 except ValueError as e:
      4     print(f"Validation error: {e}")

NameError: name 'tdm' is not defined

Distance, bearing, and midpoint

GeoLocation provides Haversine-based spatial methods — no external dependency required.

[4]:
d_km = oslo.distance_to(bergen)
d_mi = oslo.distance_to(bergen, unit="mi")
print(f"Oslo → Bergen: {d_km:.1f} km  ({d_mi:.1f} mi)")

d_tromsoe = oslo.distance_to(tromsoe)
print(f"Oslo → Tromsø: {d_tromsoe:.1f} km")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 d_km = oslo.distance_to(bergen)
      2 d_mi = oslo.distance_to(bergen, unit="mi")
      3 print(f"Oslo → Bergen: {d_km:.1f} km  ({d_mi:.1f} mi)")

NameError: name 'oslo' is not defined
[5]:
bearing = oslo.bearing_to(bergen)
print(f"Initial bearing Oslo → Bergen: {bearing:.1f}°")

mid = oslo.midpoint(bergen)
print(f"Geographic midpoint: {mid}")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 bearing = oslo.bearing_to(bergen)
      2 print(f"Initial bearing Oslo → Bergen: {bearing:.1f}°")
      4 mid = oslo.midpoint(bergen)

NameError: name 'oslo' is not defined

Offset — displace a point

offset(distance_km, bearing_deg) produces a new GeoLocation displaced from the original.

[6]:
east_of_oslo = oslo.offset(100, 90)  # 100 km due east
print(f"100 km east of Oslo: {east_of_oslo}")
print(f"Verify distance: {oslo.distance_to(east_of_oslo):.1f} km")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 east_of_oslo = oslo.offset(100, 90)  # 100 km due east
      2 print(f"100 km east of Oslo: {east_of_oslo}")
      3 print(f"Verify distance: {oslo.distance_to(east_of_oslo):.1f} km")

NameError: name 'oslo' is not defined

Attaching location to a TimeSeries

Pass a GeoLocation via the location parameter. It shows up in the rich repr.

[7]:
ts_oslo = tdm.TimeSeries(
    tdm.Frequency.PT1H,
    timezone="Europe/Oslo",
    timestamps=timestamps,
    values=(5.0 + rng.normal(0, 2, 24)).tolist(),
    name="temperature",
    unit="°C",
    description="Hourly temperature at Oslo Blindern",
    data_type=tdm.DataType.MEASUREMENT,
    location=oslo,
)
ts_oslo
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 ts_oslo = tdm.TimeSeries(
      2     tdm.Frequency.PT1H,
      3     timezone="Europe/Oslo",
      4     timestamps=timestamps,
      5     values=(5.0 + rng.normal(0, 2, 24)).tolist(),
      6     name="temperature",
      7     unit="°C",
      8     description="Hourly temperature at Oslo Blindern",
      9     data_type=tdm.DataType.MEASUREMENT,
     10     location=oslo,
     11 )
     12 ts_oslo

NameError: name 'tdm' is not defined
[8]:
print(f"Location: {ts_oslo.location}")
print(f"Lat: {ts_oslo.location.latitude}, Lon: {ts_oslo.location.longitude}")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 print(f"Location: {ts_oslo.location}")
      2 print(f"Lat: {ts_oslo.location.latitude}, Lon: {ts_oslo.location.longitude}")

NameError: name 'ts_oslo' is not defined

Per-column locations in a TimeSeriesTable

When columns represent different physical locations, attach one GeoLocation per column.

[9]:
table = tdm.TimeSeriesTable(
    tdm.Frequency.PT1H,
    timezone="Europe/Oslo",
    timestamps=timestamps,
    values=np.column_stack([
        5.0 + rng.normal(0, 2, 24),
        2.0 + rng.normal(0, 3, 24),
        -8.0 + rng.normal(0, 4, 24),
    ]),
    names=["Oslo", "Bergen", "Tromsø"],
    units=["°C", "°C", "°C"],
    locations=[oslo, bergen, tromsoe],
    data_types=[tdm.DataType.MEASUREMENT],
)
table
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 table = tdm.TimeSeriesTable(
      2     tdm.Frequency.PT1H,
      3     timezone="Europe/Oslo",
      4     timestamps=timestamps,
      5     values=np.column_stack([
      6         5.0 + rng.normal(0, 2, 24),
      7         2.0 + rng.normal(0, 3, 24),
      8         -8.0 + rng.normal(0, 4, 24),
      9     ]),
     10     names=["Oslo", "Bergen", "Tromsø"],
     11     units=["°C", "°C", "°C"],
     12     locations=[oslo, bergen, tromsoe],
     13     data_types=[tdm.DataType.MEASUREMENT],
     14 )
     15 table

NameError: name 'tdm' is not defined
[10]:
for i, name in enumerate(table.column_names):
    loc = table.locations[i] if len(table.locations) > 1 else table.locations[0]
    print(f"{name:8s}  {loc}")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[10], line 1
----> 1 for i, name in enumerate(table.column_names):
      2     loc = table.locations[i] if len(table.locations) > 1 else table.locations[0]
      3     print(f"{name:8s}  {loc}")

NameError: name 'table' is not defined

Computing distances between stations

[11]:
stations = {"Oslo": oslo, "Bergen": bergen, "Tromsø": tromsoe}

print("Pairwise distances (km):")
for a_name, a_loc in stations.items():
    for b_name, b_loc in stations.items():
        if a_name < b_name:
            print(f"  {a_name:8s}{b_name:8s}  {a_loc.distance_to(b_loc):7.1f} km")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[11], line 1
----> 1 stations = {"Oslo": oslo, "Bergen": bergen, "Tromsø": tromsoe}
      3 print("Pairwise distances (km):")
      4 for a_name, a_loc in stations.items():

NameError: name 'oslo' is not defined

GeoArea — polygon regions

GeoArea wraps a shapely Polygon for representing bidding zones, countries, or catchment areas.
It requires the optional shapely dependency:
pip install timedatamodel[geo]
[12]:
try:
    southern_norway = tdm.GeoArea.from_coordinates(
        [
            (57.5, 4.5),
            (57.5, 12.5),
            (63.0, 12.5),
            (63.0, 4.5),
            (57.5, 4.5),
        ],
        name="Southern Norway",
    )

    print(f"Area name: {southern_norway.name}")
    print(f"Bounds:    {southern_norway.bounds}")
    print(f"Centroid:  {southern_norway.centroid}")
except ImportError:
    southern_norway = None
    print("shapely not installed — skip this cell or: pip install timedatamodel[geo]")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[12], line 2
      1 try:
----> 2     southern_norway = tdm.GeoArea.from_coordinates(
      3         [
      4             (57.5, 4.5),
      5             (57.5, 12.5),
      6             (63.0, 12.5),
      7             (63.0, 4.5),
      8             (57.5, 4.5),
      9         ],
     10         name="Southern Norway",
     11     )
     13     print(f"Area name: {southern_norway.name}")
     14     print(f"Bounds:    {southern_norway.bounds}")

NameError: name 'tdm' is not defined

Bounding box — quick rectangular areas

GeoArea.bounding_box(center, radius_km) creates a rectangular area around a point.

[13]:
try:
    oslo_region = tdm.GeoArea.bounding_box(oslo, radius_km=50, name="Oslo 50km")
    print(f"Area: {oslo_region.name}")
    print(f"Bounds: {oslo_region.bounds}")
    print(f"Centroid: {oslo_region.centroid}")
except ImportError:
    oslo_region = None
    print("shapely not installed — skipping")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[13], line 2
      1 try:
----> 2     oslo_region = tdm.GeoArea.bounding_box(oslo, radius_km=50, name="Oslo 50km")
      3     print(f"Area: {oslo_region.name}")
      4     print(f"Bounds: {oslo_region.bounds}")

NameError: name 'tdm' is not defined

Spatial queries

GeoArea supports contains_point, contains_area, and overlaps.
GeoLocation has a matching is_within(area) method.
[14]:
try:
    if southern_norway is not None:
        for name, loc in stations.items():
            inside = southern_norway.contains_point(loc)
            also = loc.is_within(southern_norway)
            print(f"{name:8s} in Southern Norway? {inside}  (is_within: {also})")
except ImportError:
    print("shapely not installed — skipping")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[14], line 2
      1 try:
----> 2     if southern_norway is not None:
      3         for name, loc in stations.items():
      4             inside = southern_norway.contains_point(loc)

NameError: name 'southern_norway' is not defined
[15]:
try:
    if southern_norway is not None and oslo_region is not None:
        print(f"Southern Norway contains Oslo 50km region? {southern_norway.contains_area(oslo_region)}")
        print(f"Oslo 50km region overlaps Southern Norway?  {oslo_region.overlaps(southern_norway)}")
except ImportError:
    print("shapely not installed — skipping")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[15], line 2
      1 try:
----> 2     if southern_norway is not None and oslo_region is not None:
      3         print(f"Southern Norway contains Oslo 50km region? {southern_norway.contains_area(oslo_region)}")
      4         print(f"Oslo 50km region overlaps Southern Norway?  {oslo_region.overlaps(southern_norway)}")

NameError: name 'southern_norway' is not defined

Attaching a GeoArea to a TimeSeries

Both GeoLocation and GeoArea are accepted by the location parameter.

[16]:
try:
    if southern_norway is not None:
        ts_area = tdm.TimeSeries(
            tdm.Frequency.PT1H,
            timestamps=timestamps,
            values=(120 + rng.normal(0, 15, 24)).tolist(),
            name="wind_south",
            unit="MW",
            description="Aggregated wind production in southern Norway",
            location=southern_norway,
        )
        ts_area
except ImportError:
    print("shapely not installed — skipping")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[16], line 2
      1 try:
----> 2     if southern_norway is not None:
      3         ts_area = tdm.TimeSeries(
      4             tdm.Frequency.PT1H,
      5             timestamps=timestamps,
   (...)     10             location=southern_norway,
     11         )
     12         ts_area

NameError: name 'southern_norway' is not defined

Distance between areas and points

GeoArea.distance_to() accepts both GeoLocation and GeoArea.
It returns 0.0 if the point is contained (or areas overlap), otherwise uses centroid-based Haversine.
[17]:
try:
    if southern_norway is not None:
        print(f"Southern Norway → Oslo (contained):  {southern_norway.distance_to(oslo):.1f} km")
        print(f"Southern Norway → Tromsø (outside):  {southern_norway.distance_to(tromsoe):.1f} km")
except ImportError:
    print("shapely not installed — skipping")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[17], line 2
      1 try:
----> 2     if southern_norway is not None:
      3         print(f"Southern Norway → Oslo (contained):  {southern_norway.distance_to(oslo):.1f} km")
      4         print(f"Southern Norway → Tromsø (outside):  {southern_norway.distance_to(tromsoe):.1f} km")

NameError: name 'southern_norway' is not defined

Location survives serialization

GeoLocation round-trips through JSON.
(GeoArea requires re-attaching after deserialization.)
[18]:
json_str = ts_oslo.to_json()
ts_back = tdm.TimeSeries.from_json(json_str, location=oslo)

print(f"Name:     {ts_back.name}")
print(f"Location: {ts_back.location}")
print(f"Match:    {ts_back.location == oslo}")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[18], line 1
----> 1 json_str = ts_oslo.to_json()
      2 ts_back = tdm.TimeSeries.from_json(json_str, location=oslo)
      4 print(f"Name:     {ts_back.name}")

NameError: name 'ts_oslo' is not defined

Summary

Feature

API

Point coordinates

GeoLocation(lat, lon) — validated, frozen

Distance

loc.distance_to(other, unit="km") — Haversine

Bearing

loc.bearing_to(other) — initial bearing in degrees

Midpoint

loc.midpoint(other) — great-circle midpoint

Offset

loc.offset(distance_km, bearing_deg) — displace a point

Polygon regions

GeoArea.from_coordinates(coords) — requires shapely

Quick box

GeoArea.bounding_box(center, radius_km)

Spatial queries

area.contains_point(), area.overlaps(), loc.is_within()

Attach to series

TimeSeries(..., location=loc), TimeSeriesTable(..., locations=[...])

Next up: nb_10 demonstrates hierarchical time series — organising series into tree structures with bottom-up aggregation.