TGNs with TorchGeometric
Turn one day of NOAA AIS data into a temporal transit graph with AISdb and train a Temporal Graph Network on it with PyTorch Geometric.
Maritime traffic is relational. A port is busy because ships arrive from many origins, and a strait is congested because dozens of routes funnel through it. This tutorial turns one day of open NOAA AIS data into a stream of timestamped zone-to-zone transits with AISdb, then trains a Temporal Graph Network (TGN) in PyTorch Geometric to predict which transit happens next. You walk away able to convert raw AIS into a TemporalData event stream and run an event-driven link-prediction loop on it.
What you will learn
Building zone-to-zone transit edges from raw AIS with
aisdb.network_graph.graphLoading a transit CSV into a PyTorch Geometric
TemporalDataevent streamAssembling a TGN from
TGNMemory, an attention embedding, and a link predictorTraining and evaluating future-link prediction on chronological splits
Prerequisites
pip install aisdb torch torch-geometric scikit-learn pandasEvery number and figure on this page comes from one open dataset, the NOAA AIS day file for 2020-01-01 (about 200 MB zipped, from coast.noaa.gov), so anyone with AISdb installed can reproduce the whole run.
Step 1. Decode the NOAA day file
Decode the zipped archive straight into SQLite, then query the Gulf of Mexico window and vectorize the rows into per-vessel tracks. One day keeps the download small and is enough to see the pipeline work end to end.
from datetime import datetime
from aisdb import DBQuery, SQLiteDBConn, TrackGen, decode_msgs
from aisdb.database import sqlfcn_callbacks
DB_PATH = "./noaa_20200101.db"
ZIP_PATH = "./AIS_2020_01_01.zip"
with SQLiteDBConn(dbpath=DB_PATH) as dbconn:
decode_msgs(filepaths=[ZIP_PATH], dbconn=dbconn, source="NOAA", verbose=True)
qry = DBQuery(
dbconn=dbconn,
start=datetime(2020, 1, 1), end=datetime(2020, 1, 2),
xmin=-98, xmax=-80, ymin=24, ymax=31,
callback=sqlfcn_callbacks.in_bbox_time_validmmsi,
)
tracks = list(TrackGen(qry.gen_qry(), decimate=False))
print(len(tracks), "vessel tracks")Step 2. Build the transit graph
The nodes come from a DomainFromPoints, three zones a few kilometers apart along the Houston Ship Channel, spaced so a single day of data actually contains same-day transits between them. Two AISdb behaviors matter here. graph derives each zone's integer id by stripping non-digits from its name, so every zone name must contain a number, and with a SQLiteDBConn the trafficDBpath argument must be a real string path (a fresh, empty SQLite file is fine). Passing data_dir=None skips the optional multi-gigabyte shore, port, and bathymetry rasters.
graph vectorizes the query, splits long gaps, denoises, geofences every track against the domain, and writes one CSV row per transit. Positions outside all zones are labeled Z0, an implicit open-water node that shows up as a genuine hub in the graph. We use src_zone and rcv_zone as endpoints, last_seen_in_zone as the event time, and total_distance_meters, velocity_knots_avg, and minutes_spent_in_zone as edge features. The final segment of every track has no onward transit, so AISdb writes its rcv_zone as the string NULL and we drop those rows.
Step 3. Load the edges into TemporalData
TemporalData holds the event stream as parallel src, dst, t, and msg tensors. Two pandas details are load-bearing. read_csv turns the NULL string into NaN, so filter with dropna, and the minute-resolution timestamps parse to datetime64[ms], which silently breaks the usual .astype("int64") // 10**9 idiom, so convert through pd.Timestamp.timestamp instead. A chronological split holds out the last 30% of events so training never peeks at the future.
On the NOAA day file this leaves 358 true transit events spanning 16.3 hours, over the three channel zones plus the Z0 open-water node.

Step 4. Define the TGN
A TGN (Rossi et al., 2020, arXiv:2006.10637) keeps a memory vector per node that summarizes every event the node has taken part in. Each new edge updates the memory of both endpoints, a TransformerConv embedding attends over recent neighbors while folding in the elapsed time since each interaction, and a small MLP scores candidate edges.
Step 5. Train and evaluate
Future-link prediction trains as binary classification. For every real transit we sample one negative destination, push real scores up and fake scores down, replay events in time order, and reset the memory at the start of each epoch. Evaluation reuses the memory left behind by training and keeps replaying events chronologically, without gradients.
Results

With 358 events split three ways, the validation average precision is noisy and bounces between roughly 0.54 and 0.77 rather than converging cleanly. That is expected at demonstration scale. The loss drifts down as the memory learns the channel's rhythm and the pipeline is correct end to end, but a few hundred edges cannot pin down a stable metric. Treat these numbers as a sanity check that training runs, not as a benchmark to reproduce. Once trained at real scale, the same model ranks likely destinations for a vessel leaving a zone, predicts inflow per zone, and flags surprising transits for anomaly detection.
Takeaway
A temporal graph keeps the timeline a static adjacency matrix throws away, and AISdb's
graphbuilds the event stream directly from raw AIS.Zone names must embed a numeric id, and the
Z0open-water label is a real hub node, not a bug.The TGN memory updates per event, so the model reacts to new AIS without a full retrain.
Scale is the main lever. More zones (or an H3 tessellation, see Hexagon Discretization) and more days turn the noisy demo metric into a real benchmark.
Next, Building a RAG Chatbot turns this documentation itself into a question-answering system.
Last updated