Sea Anchor
~20× a popularity baseline
Implicit ALS on two years of AIS tracks: vessels × ocean cells, fishing hours as confidence, not ratings.
01 Problem
Two years of Global Fishing Watch AIS (~190k vessels) is a recommender with the wrong nouns: vessel as user, 0.1° ocean cell as item, fishing hours as the signal. There are no negatives. Zero hours in a cell might mean out of range, wrong season, a protected area, or simply never tried — so treating absence as dislike would poison the model. A cell with long presence and almost no fishing is a transit corridor, not a ground.
02 Approach
Hours became a confidence weight on a binary preference (Hu, Koren & Volinsky), transit noise was filtered out of the interaction matrix, and implicit ALS (32 factors, α=10) was trained against popularity and popularity-by-gear baselines. Evaluation used a temporal split — train through Q3 2024, test Q4 — plus a leak check; a random split would have inflated every number. At ~750 MB compressed per year, DuckDB over partitioned Parquet with streaming ingest beat reaching for Spark. Serving is a 10-endpoint FastAPI with typed Pydantic contracts and a React map.
03 Result
On 177 warm vessels with new Q4 2024 grounds in the Australian EEZ, ALS hit Precision@10 0.0814 against 0.0040 for popularity-by-gear — about 20× on the same protocol — and Recall@10 0.1149 vs 0.0043. For cold-start vessels, content fold-in reached P@10 0.0556 where gear popularity scored 0. Hybrid serving uses ALS for warm vessels and fold-in for unseen ones. The same factors feed MPA-legal recommendations, IUU-like anomaly flags and seasonal effort climatology.
04 What didn’t work
Content fold-in was weaker for warm vessels (P@10 0.0192 vs 0.0814), so it is a fallback, not the main model. A 10 GB CSV dump on disk was the first thing that broke; the zips are now streamed and never extracted, keeping peak memory bounded.