Thunder Bay Isolation

From thunderbay.wiki
Jump to navigation Jump to search

The geographic isolation of Thunder Bay refers to the unusual distance separating the City of Thunder Bay, Ontario, from other cities of comparable size. By the standard nearest-neighbour measure applied to cities of at least 100,000 people, Thunder Bay is the most geographically isolated city in Canada: its nearest Canadian peer, Winnipeg, lies about 598 km away. Measured against cities of any country, its nearest neighbour is Green Bay, Wisconsin, about 440 km to the south, which places Thunder Bay third in mainland North America and 33rd worldwide.[1]

This article defines the isolation metric, presents the rankings at national, continental and global scope, examines several alternative definitions, and documents the methodology and source code so the results can be reproduced.

Defining isolation

A city's geographic isolation can be quantified as the great-circle distance from that city to the nearest other city above some population threshold:

iso(c)=mincC,ccd(c,c)

where C is the set of qualifying cities and d is the great-circle distance. Cities are then ranked by iso(c) in descending order.

The result depends strongly on the comparison pool — the set C. Restricting the pool to a single country, continent or region changes which neighbours "count" and therefore changes the ranking. Thunder Bay illustrates this clearly: it is only moderately isolated on a global list but rises to first place when the pool is restricted to Canada, because its two nearest same-size neighbours (Green Bay and Minneapolis–Saint Paul) lie across the international border in the United States.

Thunder Bay in Canada

Among the 64 Canadian cities with a population of at least 100,000, Thunder Bay is the most isolated. With American cities excluded from the pool, its nearest peer becomes Winnipeg, roughly 598 km west across the Canadian Shield, which edges out St. John's (590 km from Sydney, Nova Scotia).

Rank City Province Nearest other Canadian city >100k Distance
1 Thunder Bay Ontario Winnipeg 598 km
2 St. John's Newfoundland and Labrador Sydney 590 km
3 Winnipeg Manitoba Regina 536 km
4 Sydney Nova Scotia Dartmouth 311 km
5 Greater Sudbury Ontario Barrie 254 km
6 Regina Saskatchewan Saskatoon 235 km
7 Saskatoon Saskatchewan Regina 235 km
8 Saguenay Quebec Québec 179 km
9 Lethbridge Alberta Calgary 175 km
10 Windsor Ontario London 164 km

Nearer settlements such as Duluth, Minnesota (~278 km) and Sault Ste. Marie (~424 km) fall below the 100,000 threshold, which is why Thunder Bay's nearest qualifying neighbour lies several hundred kilometres away in every direction.

Wider context: North America and the world

Across mainland North America (excluding Alaska, Hawaii and offshore islands), Thunder Bay ranks third, behind St. John's and Billings, Montana. On the worldwide list of all cities over 100,000 it ranks 33rd of 5,783; with offshore-island cities removed it rises to 19th. The worldwide top of the list is dominated by ocean-isolated and high-latitude cities; islands are flagged below.[1]

Rank City Country Nearest city >100k Distance Island
1 Honolulu United States Daly City 3,848 km island
2 Anchorage United States Vancouver 2,132 km
3 Perth Australia Adelaide 2,131 km
4 Reykjavík Iceland Aberdeen 1,321 km island
5 Petropavlovsk-Kamchatsky Russia Yuzhno-Sakhalinsk 1,319 km
6 Yakutsk Russia Blagoveshchensk 1,314 km
7 Norilsk Russia Noyabrsk 888 km
8 Punta Arenas Chile Comodoro Rivadavia 848 km
9 Port Moresby Papua New Guinea Merauke 749 km island
10 Darwin Australia Dili 721 km
33 Thunder Bay Canada Green Bay 440 km

When offshore islands (Honolulu, Reykjavík, Hobart, St. John's and others) are removed, the leaders become Anchorage, Perth and Darwin, and Thunder Bay rises to 19th.

How the ranking depends on the definition

Because isolation depends on the comparison pool and on what counts as a "city," Thunder Bay's rank ranges widely. Restricting the pool raises its standing; raising the population bar for a qualifying neighbour lowers it.

Definition (nearest-neighbour unless noted) Thunder Bay's rank
Cities >100k, Canada #1 of 64
Cities >100k, North America (excl. Alaska/Hawaii) #3 of 556
Cities >100k, North America (incl. Alaska/Hawaii) #5 of 558
Cities >100k, world (continental only) #19 of 4,934
Cities >100k, world (all) #33 of 5,783
Distance to nearest city >500k, world #93 of 5,783
Distance to nearest larger city, world #199 of 5,783

Alternative metrics considered

Several other definitions of isolation were tested. None ranks Thunder Bay higher than the nearest-neighbour measure does, because each captures a different kind of isolation than Thunder Bay possesses.

  • Distance to a large city. Measuring the distance to the nearest city over 500,000 lowers Thunder Bay's rank (to 93rd worldwide, 6th in Canada). Its nearest large city, Winnipeg (~750,000), is only 598 km away; cities such as Winnipeg itself and the cities of Atlantic Canada, whose nearest metropolis is more than 1,000 km off, rank far higher.
  • Directional isolation. Averaging the distance to the nearest city in each of eight compass sectors rewards cities on continental edges facing open ocean (Punta Arenas, Anchorage and the coastal cities of Australia top this list). Thunder Bay, with cities at moderate distance in most directions and emptiness only to the north, falls to roughly 346th.
  • Surrounding population. Summing the population living within a disc around the city — a measure of local density — does not favour Thunder Bay either. About 3.1 million people live within 500 km and some 42.4 million within 1,000 km, placing it 167th and 812th in the world respectively. Thunder Bay sits on the northern edge of the densely populated Great Lakes corridor: Minneapolis, Duluth and the cities of Wisconsin lie well within 500 km to the south, and Chicago, Toronto and Detroit within 1,000 km. A symmetric disc captures these southern millions and masks the genuine emptiness to the north.

Taken together, these results indicate that Thunder Bay's isolation is of the nearest-neighbour kind — a sparse ring of similarly sized cities, with the populated world ending abruptly to the north — rather than the remote-from-any-metropolis or low-surrounding-density kind found in Siberia, the Australian interior or the Sahara.

Methodology

Data source. City names, coordinates and populations are from GeoNames, via the <syntaxhighlight lang="text" class="" id="" style="" inline="1">geonamescache</syntaxhighlight> Python package, which bundles the GeoNames <syntaxhighlight lang="text" class="" id="" style="" inline="1">cities15000</syntaxhighlight> set (complete coverage of cities with population at least 15,000).[1][2]

Nearest-neighbour search. Each city's coordinates are mapped to a point on a three-dimensional unit sphere so that a Euclidean k-d tree returns the correct nearest neighbour; the resulting chord length is converted to a great-circle arc using d=2Rarcsin(chord/2) with R=6,371 km.

Island detection. To distinguish offshore islands from continents, a land/ocean grid (0.2° resolution) is flood-filled into connected landmasses, stitching the grid across the ±180° longitude seam. Landmasses of at least 3,000,000 km² are treated as continents (Afro-Eurasia, the Americas and Australia); a city on any smaller landmass — Iceland, Newfoundland, Hawaii, Tasmania, Japan, Great Britain and so on — is flagged as an island.

Surrounding population. For the disc metric, all GeoNames cities within a given radius are summed (excluding the city itself) using a ball-point query on the same spherical embedding.

Source code

The following self-contained Python script reproduces the rankings and the headline figures. It requires the <syntaxhighlight lang="text" class="" id="" style="" inline="1">geonamescache</syntaxhighlight>, <syntaxhighlight lang="text" class="" id="" style="" inline="1">scipy</syntaxhighlight>, <syntaxhighlight lang="text" class="" id="" style="" inline="1">numpy</syntaxhighlight> and <syntaxhighlight lang="text" class="" id="" style="" inline="1">global-land-mask</syntaxhighlight> packages.

<syntaxhighlight lang="python">

  1. !/usr/bin/env python3

""" thunder_bay_isolation.py -- Geographic-isolation metrics for world cities, with Thunder Bay, Ontario as the reference point.

Headline result: by the nearest-neighbour metric, Thunder Bay is the most geographically isolated city of >=100,000 people in Canada (598 km to its nearest Canadian peer, Winnipeg).

Data source: GeoNames (https://www.geonames.org/) via `geonamescache`.

   pip install geonamescache scipy numpy global-land-mask

""" import numpy as np import geonamescache from scipy.spatial import cKDTree from scipy import ndimage from global_land_mask import globe

R_KM = 6371.0088


def to_xyz(cities):

   lat = np.radians([c["latitude"] for c in cities])
   lon = np.radians([c["longitude"] for c in cities])
   return np.column_stack(
       [np.cos(lat) * np.cos(lon), np.cos(lat) * np.sin(lon), np.sin(lat)]
   )


def chord_to_km(chord):

   return 2 * R_KM * np.arcsin(np.clip(chord / 2, 0, 1))


def continental_keys(res=0.2, min_area_km2=3_000_000):

   """Keys (name, lat, lon) of cities sitting on a continental-scale landmass.
   A land/ocean grid is flood-filled into connected landmasses (8-connectivity,
   wrapping at the +-180 longitude seam). Landmasses with area >= min_area_km2
   are continents; everything else (Iceland, Newfoundland, Hawaii, ...) is an
   island.
   """
   lats = np.arange(-60, 75, res)
   lons = np.arange(-180, 180, res)
   LON, LAT = np.meshgrid(lons, lats)
   land = globe.is_land(LAT, LON)
   lbl, n = ndimage.label(land, structure=np.ones((3, 3)))
   parent = list(range(n + 1))
   def find(x):
       while parent[x] != x:
           parent[x] = parent[parent[x]]
           x = parent[x]
       return x
   for a, b in zip(lbl[:, 0], lbl[:, -1]):
       if a > 0 and b > 0:
           parent[find(a)] = find(b)
   root = np.array([find(i) for i in range(n + 1)])
   lbl = root[lbl]
   cell = 3080 * (res / 0.5) ** 2 * np.cos(np.radians(LAT))
   area = {lab: cell[lbl == lab].sum() for lab in np.unique(lbl) if lab}
   continents = {lab for lab, a in area.items() if a >= min_area_km2}
   def landmass(lat, lon):
       i = min(max(int(round((lat - lats[0]) / res)), 0), len(lats) - 1)
       j = int(round((lon - lons[0]) / res)) % len(lons)
       for r in range(7):
           for di in range(-r, r + 1):
               for dj in range(-r, r + 1):
                   ii, jj = i + di, (j + dj) % len(lons)
                   if 0 <= ii < len(lats) and lbl[ii, jj] > 0:
                       return lbl[ii, jj]
       return 0
   cities = geonamescache.GeonamesCache().get_cities().values()
   keys = set()
   for c in cities:
       if c["population"] >= 100000 and landmass(
           c["latitude"], c["longitude"]
       ) in continents:
           keys.add((c["name"], round(c["latitude"], 3),
                     round(c["longitude"], 3)))
   return keys


def nearest_neighbour(pool):

   P = to_xyz(pool)
   d, idx = cKDTree(P).query(P, k=2)            # k=2: self + nearest other
   return chord_to_km(d[:, 1]), idx[:, 1]


def surrounding_population(candidates, universe, radius_km):

   tree = cKDTree(to_xyz(universe))
   pops = np.array([c["population"] for c in universe])
   chord = 2 * np.sin(radius_km / (2 * R_KM))
   cand_pop = np.array([c["population"] for c in candidates])
   out = np.empty(len(candidates))
   for i, lst in enumerate(tree.query_ball_point(to_xyz(candidates), chord)):
       out[i] = max(pops[lst].sum() - cand_pop[i], 0)
   return out


def main():

   gc = geonamescache.GeonamesCache()
   allc = list(gc.get_cities().values())
   cand = [c for c in allc if c["population"] >= 100000]
   cont = continental_keys()
   def is_island(c):
       return (c["name"], round(c["latitude"], 3),
               round(c["longitude"], 3)) not in cont
   def rank(pool, label, show=10, focus="Thunder Bay"):
       dist, idx = nearest_neighbour(pool)
       order = np.argsort(-dist)
       print(f"\n=== {label} (n={len(pool)}) ===")
       for r, i in enumerate(order[:show], 1):
           flag = " [island]" if is_island(pool[i]) else ""
           print(f"  {r:>2}. {pool[i]['name']:<20}{pool[i]['countrycode']} "
                 f"{dist[i]:>5.0f} km -> {pool[idx[i]]['name']}{flag}")
       for r, i in enumerate(order, 1):
           if pool[i]["name"] == focus:
               print(f"  >>> {focus}: #{r} of {len(pool)} "
                     f"({dist[i]:.0f} km -> {pool[idx[i]]['name']})")
               break
   rank([c for c in cand if c["countrycode"] == "CA"],
        "Most isolated city >=100k in CANADA")
   rank([c for c in cand if c["countrycode"] in {"US", "CA", "MX"}],
        "Most isolated >=100k in NORTH AMERICA (islands flagged)", show=8)
   rank(cand, "Most isolated >=100k WORLDWIDE (islands flagged)", show=8)
   surr = surrounding_population(
       [c for c in cand if c["name"] == "Thunder Bay"], allc, 1000)
   print(f"\nThunder Bay: {surr[0]/1e6:.1f} M people within 1000 km.")


if __name__ == "__main__":

   main()

</syntaxhighlight>

Limitations

  • Population definitions are not uniform. GeoNames' population field mixes city-proper and urban-agglomeration figures, so the threshold is not applied to a perfectly consistent unit. This mainly affects borderline cases rather than the overall ranking.
  • Straight-line distance. Isolation is measured as great-circle distance, not travel distance; a road-distance variant would further separate land-isolated cities such as Thunder Bay from ocean-isolated ones.
  • Urban population only. The surrounding-population metric counts GeoNames towns of at least 15,000 people and omits dispersed rural population.
  • Resolution of island detection. The land-mask flood-fill is computed at 0.2°; very narrow straits or causeways can occasionally merge a near-shore island into its continent, or vice versa.
  • Snapshot in time. Figures reflect the GeoNames dataset at the time of computation and drift as populations change.

See also

References

  1. 1.0 1.1 1.2 GeoNames geographical database, <syntaxhighlight lang="text" class="" id="" style="" inline="1">cities15000</syntaxhighlight> dataset. https://www.geonames.org/
  2. <syntaxhighlight lang="text" class="" id="" style="" inline="1">geonamescache</syntaxhighlight> Python package (bundles GeoNames city data). https://github.com/yaph/geonamescache