Is Aruba really safer from hurricanes than St. Barts? NOAA keeps the record that answers it in one plain text file. HURDAT2 is the National Hurricane Center's best-track database, and it lists every known Atlantic storm since 1851 with its position and wind every six hours.

About 80 lines of standard-library Python turn that file into a count per island. Over 1966 to 2025, the script below finds one hurricane within 50 nautical miles of Aruba. For Virgin Gorda it finds twelve.

How a HURDAT2 file is laid out

The file stacks blocks. Each block opens with a header line, and a set number of track lines follow it. Here is the start of Hurricane Felix in 2007, from the release we used (hurdat2-1851-2025-092326.txt), cut after the wind and pressure columns and with some track lines skipped:

AL062007, FELIX, 28, 20070901, 0600, , TS, 12.1N, 61.1W, 40, 1005, ... 20070901, 0845, L, TS, 12.1N, 61.7W, 45, 1001, ... 20070901, 1200, , TS, 12.2N, 62.8W, 50, 1001, ... ... 20070902, 0600, , HU, 12.7N, 67.8W, 85, 985, ... 20070902, 1200, , HU, 13.0N, 69.4W, 90, 980, ... 20070902, 1800, , HU, 13.4N, 71.1W, 115, 962, ... 
  • Header: basin, storm number and year (AL062007), then the name, then how many track lines follow (28).
  • Track line: date, time in UTC, a record identifier (L means landfall), status (TD, TS, HU), latitude and longitude with a hemisphere letter, then maximum sustained wind in knots and minimum pressure. Wind radii fill the remaining columns, and -999 marks missing data.

Two details matter for parsing. Longitudes come as 61.1W and need a sign. And the six-hour rhythm breaks: the 08:45 landfall line above sits between two regular fixes.

The rules behind the count

  • Radius of 50 nautical miles (58 miles, 93 km). The National Hurricane Center uses this radius for its return periods.
  • 1966 to 2025 only. Satellites arrive in the mid-1960s. Earlier decades miss storms that stayed at sea, so older counts run low.
  • Hurricane if the storm's wind reached 64 kt (74 mph) while its center was inside the circle. Major if it reached 96 kt (111 mph, Category 3 and up).
  • One count per storm per island, even when a storm loops back.
  • Interpolation every 15 minutes between fixes.

The last rule is the one people skip, and Felix shows why it matters. Between 06:00 and 12:00 on September 2 it moved from 67.8W to 69.4W. At that latitude that is about 95 nautical miles in six hours, close to the width of the whole 100-mile circle. Check only the six-hour points and a fast storm can cross an island's circle between two fixes and never show up. At 15-minute steps, Felix moved about 4 nautical miles per step.

The code

Constants and one reference point per island:

import math import sys from datetime import datetime, timedelta RADIUS_NM = 50 # NHC's radius for return periods (58 mi / 93 km) HURRICANE_KT = 64 # hurricane-force sustained wind MAJOR_KT = 96 # Category 3+ START, END = 1966, 2025 # satellite era ISLANDS = { # one reference point per island (lat, lon) "Aruba": (12.52, -70.03), "Curacao": (12.17, -68.98), "Grenada": (12.05, -61.75), "Barbados": (13.10, -59.62), "St. Barts": (17.90, -62.83), "Virgin Gorda": (18.45, -64.40), } 

Parsing. The header tells us how many lines to read, so no guessing is needed:

def parse_coord(s): # "61.1W" -> -61.1, "12.1N" -> 12.1 v = float(s[:-1]) return -v if s[-1] in "SW" else v def read_hurdat2(path): """Yield (storm_id, name, [(time, lat, lon, wind_kt), ...]).""" with open(path) as f: lines = iter(f) for header in lines: sid, name, n = [x.strip() for x in header.split(",")[:3]] fixes = [] for _ in range(int(n)): p = [x.strip() for x in next(lines).split(",")] t = datetime.strptime(p[0] + p[1], "%Y%m%d%H%M") # columns 4-6: latitude, longitude, max sustained wind (kt) fixes.append((t, parse_coord(p[4]), parse_coord(p[5]), int(p[6]))) yield sid, name, fixes 

Interpolation and great-circle distance. Steps are computed from the real time gap, so irregular lines like the 08:45 landfall fix work too:

def interpolate(fixes, step_min=15): """Linear interpolation between fixes every `step_min` minutes.""" for (t0, la0, lo0, w0), (t1, la1, lo1, w1) in zip(fixes, fixes[1:]): steps = max(1, int((t1 - t0).total_seconds() // (step_min * 60))) for i in range(steps): f = i / steps yield (t0 + timedelta(minutes=step_min * i), la0 + f * (la1 - la0), lo0 + f * (lo1 - lo0), w0 + f * (w1 - w0)) yield fixes[-1] def distance_nm(lat1, lon1, lat2, lon2): # haversine r = 3440.065 # Earth radius in nautical miles p1, p2 = math.radians(lat1), math.radians(lat2) dp, dl = p2 - p1, math.radians(lon2 - lon1) a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 return 2 * r * math.asin(math.sqrt(a)) 

Counting. A storm is classified by the strongest wind it had while inside the circle:

def count_passes(path): result = {k: {"hurricanes": [], "major": 0} for k in ISLANDS} for sid, name, fixes in read_hurdat2(path): if not START <= fixes[0][0].year <= END or len(fixes) < 2: continue track = list(interpolate(fixes)) for island, (ilat, ilon) in ISLANDS.items(): inside = [(t, w) for t, la, lo, w in track if distance_nm(la, lo, ilat, ilon) <= RADIUS_NM] if not inside: continue peak = max(w for _, w in inside) if peak >= HURRICANE_KT: # counted once per storm per island result[island]["hurricanes"].append(f"{name.title()} {inside[0][0]:%Y}") result[island]["major"] += peak >= MAJOR_KT return result if __name__ == "__main__": years = END - START + 1 for island, r in count_passes(sys.argv[1]).items(): n = len(r["hurricanes"]) print(f"{island:13} {n:2} hurricanes ({r['major']} major) in {years} years" f" -> about 1 in {years / n:.0f} years" if n else f"{island:13} 0") print(" ", ", ".join(r["hurricanes"])) 

The real output

python hurricane_passes.py hurdat2-1851-2025-092326.txt 
Aruba 1 hurricanes (1 major) in 60 years -> about 1 in 60 years Felix 2007 Curacao 1 hurricanes (0 major) in 60 years -> about 1 in 60 years Felix 2007 Grenada 3 hurricanes (2 major) in 60 years -> about 1 in 20 years Ivan 2004, Emily 2005, Beryl 2024 Barbados 4 hurricanes (1 major) in 60 years -> about 1 in 15 years Allen 1980, Marilyn 1995, Tomas 2010, Elsa 2021 St. Barts 10 hurricanes (5 major) in 60 years -> about 1 in 6 years Luis 1995, Bertha 1996, Georges 1998, Jose 1999, Lenny 1999, Debby 2000, Earl 2010, Gonzalo 2014, Irma 2017, Tammy 2023 Virgin Gorda 12 hurricanes (5 major) in 60 years -> about 1 in 5 years Hugo 1989, Marilyn 1995, Bertha 1996, Georges 1998, Jose 1999, Lenny 1999, Debby 2000, Omar 2008, Earl 2010, Gonzalo 2014, Irma 2017, Dorian 2019 

The south and the northeast are far apart. Aruba, Curacao and Grenada sit low in the Caribbean and get few passes. St. Barts and Virgin Gorda sit near the usual tracks of storms coming in from the Atlantic. Felix counts as major at Aruba but not at Curacao: by the time it passed Aruba, its interpolated wind had climbed past 96 kt.

"About 1 in 60 years" is an average gap, not a schedule. With a single event, treat it as rough.

It matches the published table

We use this same method for the hurricane odds by island table on Gladori, which covers 21 destinations. The six islands here match that table exactly on both hurricanes and major hurricanes:

Island Hurricanes Major
Aruba 1 1
Curacao 1 0
Grenada 3 2
Barbados 4 1
St. Barts 10 5
Virgin Gorda 12 5

The last storm on each list also matches the table's "last hurricane" column: Tammy 2023 for St. Barts, Dorian 2019 for Virgin Gorda. Anyone can rerun the script on NOAA's file and get the same numbers.

Limits

  • One point per island. A long island differs along its coast, which is why the published table labels Jamaica as Montego Bay. A point and a radius is a simplification.
  • Storm wind, not island wind. HURDAT2 wind is the storm's maximum sustained wind. A hurricane whose center passed 45 miles away did not necessarily bring hurricane winds ashore.
  • Linear interpolation. Position and wind are assumed to change smoothly between fixes. Real storms curve and pulse.
  • Sixty years is a small sample, mostly for the quiet islands.
  • This is history, not a forecast. When a storm is on the way, follow the National Hurricane Center.

Taking it further

To run your own spot, add a line to ISLANDS and run the script again. The Gladori page adds the other 15 destinations, a week-by-week chart for each island, and the share of years with a hurricane in each month. The counts per island are also a CSV under CC BY 4.0, with the sources in the first lines.

Data credit: HURDAT2 is a US government work in the public domain. Cite it as Landsea, C. W., and J. L. Franklin (2013), Atlantic hurricane database uncertainty and presentation of a new database format, Mon. Wea. Rev., 141, 3576-3592.

Gladori data team