Fixing DMS Coordinates That Land in the Wrong Hemisphere

On this page

Problem statement

You converted a column of degrees-minutes-seconds coordinates, plotted them, and the Hawaiian Islands appeared in the western Pacific south-east of Japan, or your Brazilian sites turned up in the Sahara. The conversion arithmetic was right; the sign was lost. In DMS notation the hemisphere letter carries the sign, west and south being negative, and a parser that reads the digits but ignores the letter mirrors every point across the equator or the prime meridian. The error is easy to miss in a table, because the numbers look reasonable, and obvious on a map.

This page reproduces the mistake on the USGS Geographic Names Information System's features for Hawaii, measures how far the points move, detects it automatically with a bounding box and a country lookup, and fixes it, including the double negative that appears when a source writes both a minus sign and a W.

Quick answer

Apply the sign from the hemisphere letter, and check the result against where the data should be:

def dms_naive(s):
    digits = s[:-1]; n = 3 if s[-1] in "EW" else 2
    return int(digits[:n]) + int(digits[n:n + 2]) / 60 + float(digits[n + 2:]) / 3600       # ignores the letter

def dms(s):
    v = dms_naive(s)
    return -v if s[-1] in "SW" else v

G["lon_naive"] = G.prim_long_dms.map(dms_naive)
G["lon"] = G.prim_long_dms.map(dms)
print(f"features: {len(G):,}; naive longitudes from {G.lon_naive.min():.2f} to {G.lon_naive.max():.2f}; correct from {G.lon.min():.2f} to {G.lon.max():.2f}")
features: 7,333; naive longitudes from 154.81 to 178.33; correct from -178.33 to -154.81

Ignoring the letter gives longitudes from 154.81° to 178.33° east instead of west: every feature is mirrored across the prime meridian.

Converting a DMS longitude with and without its hemisphere letter. where the sign goes missing1575130WGNIS DMS157 + 51/60 +30/3600157.858333letter W?S and W negativeapply sign-157.858333check the boxHawaii: 100% insideSkipping the letter puts all 7,333 features at 155-178°E spatialworkflow.io
Where the sign goes missing.

How far points move, automatic detection, doubled signs

1. How far the points move

Dropping a W mirrors a point across the prime meridian. Measure the displacement along the Earth's surface:

G["lat"] = G.prim_lat_dms.map(dms)
d = geod.inv(G.lon.values, G.lat.values, G.lon_naive.values, G.lat.values)[2] / 1000
print(f"displacement: min {d.min():,.0f} km, median {np.median(d):,.0f} km, max {d.max():,.0f} km")
print(f"mirrored Hawaii lands near {G.lat.median():.1f}°N, {G.lon_naive.median():.1f}°E")
displacement: min 327 km, median 4,862 km, max 5,268 km
mirrored Hawaii lands near 20.9°N, 156.6°E

The median feature moves 4,862 km and the farthest 5,268 km; features near 178°W, in the north-western islands, move only 327 km, because they are close to the 180° meridian where the mirror image is nearby. Mirrored Hawaii lands at about 20.9°N, 156.6°E, in the open western Pacific.

2. Detect it automatically

A check against the data's expected extent catches a lost sign at once. Use a bounding box for the state, and a country lookup for a second opinion:

box = (-179.5, 18.0, -154.0, 29.0)                     # Hawaii, including the north-western islands
inside = lambda lon, lat: (lon >= box[0]) & (lon <= box[2]) & (lat >= box[1]) & (lat <= box[3])
print(f"naive points inside the Hawaii box: {inside(G.lon_naive, G.lat).mean():.0%}; correct points: {inside(G.lon, G.lat).mean():.0%}")
pts = gpd.GeoDataFrame(geometry=gpd.points_from_xy(G.lon_naive, G.lat), crs=4326)
hit = gpd.sjoin_nearest(pts.to_crs(8857), C[["NAME", "geometry"]].to_crs(8857), how="left")
print("nearest countries to the mirrored points:", hit.NAME.value_counts().head(4).to_dict())
naive points inside the Hawaii box: 0%; correct points: 100%
nearest countries to the mirrored points: {'Japan': 7300, 'U.S. Minor Outlying Is.': 33}

A bounding box catches the problem completely: no naive point and every corrected point is inside the box for Hawaii. The country check says the same thing differently: the nearest country to 7,300 of the mirrored points is Japan.

Map of the North Pacific in 0 to 360 degree longitudes with Hawaii's features at their correct position east of the 180 degree line in green and the mirrored copy produced by ignoring the W, south-east of Japan, in red. Map of the North Pacific in 0 to 360 degree longitudes with Hawaii's features at their correct position east of the 180 degree line in green and the mirrored copy produced by ignoring the W, south-east of Japan, in red.
Hawaii and its mirror image.

3. Signs written twice

Some sources write -157°51'30"W. Applying both the minus and the letter flips the sign back to positive. Decide on one rule and test it:

def parse_signed(text):
    neg = text.strip().startswith("-")
    d, m, s, h = re.match(r"-?(\d+)°(\d+)'([\d.]+)\"([NSEW])", text.strip()).groups()
    v = int(d) + int(m) / 60 + float(s) / 3600
    return -v if (h in "SW" or neg) else v              # either marker means negative, never both

def parse_both(text):                                  # the bug: applies the minus and the letter
    d, m, s, h = re.match(r"-?(\d+)°(\d+)'([\d.]+)\"([NSEW])", text.strip()).groups()
    v = (int(d) + int(m) / 60 + float(s) / 3600) * (-1 if text.strip().startswith("-") else 1)
    return -v if h in "SW" else v
for t in ["157°51'30\"W", "-157°51'30\"W", "21°18'25.5\"N"]:
    print(f"{t!r:>18}: either-marker rule {parse_signed(t):+.6f}; applying both {parse_both(t):+.6f}")
    '157°51\'30"W': either-marker rule -157.858333; applying both -157.858333
   '-157°51\'30"W': either-marker rule -157.858333; applying both +157.858333
   '21°18\'25.5"N': either-marker rule +21.307083; applying both +21.307083

With the either-marker rule, 157°51'30"W and -157°51'30"W both give -157.858333. Applying the minus and the letter turns the second into +157.858333, back in the eastern hemisphere.

A validation function for converted columns

The data used on this page

The USGS Geographic Names Information System domestic names for Hawaii, with compact DMS fields such as 1575130W, and Natural Earth's 1:10m countries for the country lookup.

import io
import re
import zipfile
from pathlib import Path

import geopandas as gpd
import numpy as np
import pandas as pd
import requests
from pyproj import Geod

Path("data").mkdir(exist_ok=True)
def cached(path, url):
    p = Path(path)
    if not p.exists():
        r = requests.get(url, timeout=600)
        r.raise_for_status()
        p.write_bytes(r.content)
    return p
with zipfile.ZipFile(cached("data/DomesticNames_HI_Text.zip", "https://prd-tnm.s3.amazonaws.com/StagedProducts/GeographicNames/DomesticNames/DomesticNames_HI_Text.zip")) as z:
    G = pd.read_csv(io.BytesIO(z.read("Text/DomesticNames_HI.txt")), sep="|", dtype={"prim_lat_dms": str, "prim_long_dms": str}, encoding="utf-8-sig")
G = G[G.state_name == "Hawaii"].dropna(subset=["prim_lat_dms", "prim_long_dms"]).reset_index(drop=True)
C = gpd.read_file(cached("data/ne_10m_admin_0_countries.zip", "https://naciscdn.org/naturalearth/10m/cultural/ne_10m_admin_0_countries.zip"))
geod = Geod(ellps="WGS84")
print(f"{len(G):,} Hawaii features; hemisphere letters: {G.prim_long_dms.str[-1].value_counts().to_dict()} {G.prim_lat_dms.str[-1].value_counts().to_dict()}")
7,333 Hawaii features; hemisphere letters: {'W': 7333} {'N': 7333}

All 7,333 features carry W longitudes and N latitudes; only the longitude sign is at stake here.

Example 1: a validation function for any converted column

Wrap the checks so every conversion is tested before use:

def validate(lat, lon, box, name):
    issues = {"latitude out of range": int((lat.abs() > 90).sum()), "longitude out of range": int((lon.abs() > 180).sum()),
              "outside expected box": int((~inside(lon, lat)).sum())}
    print(f"{name}: {issues}")
validate(G.lat, G.lon_naive, box, "naive")
validate(G.lat, G.lon, box, "correct")
naive: {'latitude out of range': 0, 'longitude out of range': 0, 'outside expected box': 7333}
correct: {'latitude out of range': 0, 'longitude out of range': 0, 'outside expected box': 0}

Range checks pass for both versions, because mirrored coordinates are still valid numbers; only the expected-location check separates them, failing all 7,333 naive points and none of the corrected ones.

Histogram of the distance between each feature's correct and mirrored position: almost all between 4,000 and 5,300 kilometres, median 4,862, with a few north-western islands much closer. Histogram of the distance between each feature's correct and mirrored position: almost all between 4,000 and 5,300 kilometres, median 4,862, with a few north-western islands much closer.
How far a lost sign moves points.

Signs against hemisphere letters

Nearest country to the mirrored Hawaii features. mirrored Hawaii: nearest countryJapan7,300U.S. Minor Outlying Is.330250050007500Correct positions: all 7,333 inside the Hawaii bounding box spatialworkflow.io
Detecting the mistake.

Decimal degrees put the hemisphere in the sign: negative latitudes are south of the equator, negative longitudes west of Greenwich. DMS puts it in a letter and keeps the numbers positive. A conversion that keeps the numbers but drops the letter therefore produces valid-looking coordinates, all in the northern and eastern hemisphere, and every western point is mirrored across the prime meridian, every southern one across the equator. For Hawaii, at about 157°W, that is a jump to about 157°E: only 46° of longitude across the 180° meridian, but nearly 5,000 km to the western Pacific.

Because the output is in range, value checks alone do not catch it; a check against the expected location does. A bounding box of where the data should be, a country lookup or simply a map before any analysis reveals the problem immediately. The double negative is the mirror image of the same bug: a source that writes the sign and the letter, read by code that applies both, flips western points back east. Pick one rule, either marker means negative, and test it on a known coordinate.

Points near zero and 180, and southern data

Checks after converting DMS coordinates. after converting DMShemisphere letters appliedW, S negativeexpected bounding boxcatches a lost signnever apply sign and letter twice-157 W must stay negativelook at a maprange checks pass anywaycase-insensitive lettersw and s occurGNIS Hawaii, 7,333 features spatialworkflow.io
After converting DMS.
  • Points near 0° or 180°: a lost sign moves them only slightly; the bounding-box check may miss them.
  • Southern data: the same bug mirrors southern points into the northern hemisphere; check latitude ranges too.
  • Mixed sources: if some rows have letters and others signs, normalise to one form before converting.
  • Lowercase letters: w and s occur; compare case-insensitively.

FAQ

Why are my converted coordinates in the wrong hemisphere?

The hemisphere letter was ignored: W and S must make the decimal value negative.

Why is my US data showing up in China or the Pacific?

Longitudes that should be negative, west, were converted as positive, east, which mirrors them across the prime meridian.

What if the coordinate has a minus sign and a W?

Treat either marker as negative, but never apply both, or the sign flips back.

How do I check converted coordinates?

Test them against a bounding box of the expected area and look at them on a map before any analysis.