Fixing USGS Earthquake Times That Show Up as 1970

On this page

Problem statement

You download earthquakes from the USGS as GeoJSON, convert the time property with pd.to_datetime, and every event happened on 1 January 1970, within the first half hour. The data is fine. GeoJSON from the USGS event service gives time and updated as integers, milliseconds since 1 January 1970 UTC, and pandas reads a bare integer as nanoseconds unless told otherwise. A second, quieter problem follows the first: once the times are right they are in UTC, and grouping by day without converting to local time puts evening earthquakes on the wrong date.

This page downloads the first month of the February 2023 Kahramanmaraş, Türkiye, earthquake sequence as GeoJSON, shows the 1970 dates and why they appear, fixes them, compares with the CSV format, and counts earthquakes per day in UTC and in local time.

Quick answer

Tell pandas the unit, and that the epoch is UTC:

raw = props.time
print(f"raw value: {raw.iloc[0]} ({raw.dtype})")
print(f"pd.to_datetime(raw):                      {pd.to_datetime(raw).iloc[0]}")
print(f"pd.to_datetime(raw, unit='ms', utc=True): {pd.to_datetime(raw, unit='ms', utc=True).iloc[0]}")
raw value: 1675646254342 (int64)
pd.to_datetime(raw):                      1970-01-01 00:27:55.646254342
pd.to_datetime(raw, unit='ms', utc=True): 2023-02-06 01:17:34.342000+00:00

The first event's raw time is 1,675,646,254,342. Read as nanoseconds it is 00:27:55 on 1 January 1970; read as milliseconds since the epoch in UTC it is 01:17:34 on 6 February 2023, the M7.8 Kahramanmaraş earthquake.

From the integer in a USGS GeoJSON feed to a local date and time. from an integer to a local timeraw: 1675646254342no unit attachedunit="ms"milliseconds since 1970 UTCutc=True2023-02-06 01:17:34 UTCtz_convert("Europe/Istanbul")04:17 localWithout unit="ms": 1970-01-01 00:27:55 spatialworkflow.io
From an integer to a local time.

Why 1970, the CSV alternative, local days

1. Why 1970

An integer timestamp is a count from the Unix epoch, 1970-01-01 00:00 UTC. Read as nanoseconds, the 1.68 trillion milliseconds to February 2023 become 1.68 trillion nanoseconds, about 28 minutes:

wrong = pd.to_datetime(raw)
print(f"wrong range: {wrong.min()} to {wrong.max()}")
print(f"seconds after the epoch, read as nanoseconds: {raw.iloc[0] / 1e9:.0f} s = {raw.iloc[0] / 1e9 / 60:.1f} minutes")
print(f"the same value as milliseconds: {raw.iloc[0] / 1000 / 86400 / 365.25:.2f} years")
wrong range: 1970-01-01 00:27:55.646254342 to 1970-01-01 00:27:58.140897396
seconds after the epoch, read as nanoseconds: 1676 s = 27.9 minutes
the same value as milliseconds: 53.10 years

All 462 wrong timestamps fall within three seconds of each other, 28 minutes after the epoch, because a month of milliseconds is only 2.5 seconds of nanoseconds. The same number as milliseconds is 53.10 years after 1970.

2. The CSV format has no such problem

The same query as CSV returns ISO 8601 strings with a Z for UTC, which pandas parses directly. Check that both formats give the same times:

t_json = pd.to_datetime(props.time, unit="ms", utc=True)
t_csv = pd.to_datetime(csv.time, utc=True)
m = pd.DataFrame({"id": props.id_, "json": t_json}).merge(pd.DataFrame({"id": csv.id, "csv": t_csv}), on="id")
print(f"events in both: {len(m)}; largest difference {(m.json - m.csv).abs().max()}")
print(f"CSV time example: {csv.time.iloc[0]!r}")
events in both: 462; largest difference 0 days 00:00:00
CSV time example: '2023-02-06T01:17:34.342Z'

Matched by event ID, the GeoJSON times converted with unit="ms" and the CSV's ISO strings agree exactly for all 462 events. The CSV string ends in Z, for UTC, so utc=True keeps it timezone-aware.

How the USGS event service formats time in GeoJSON and CSV. the same event, two formatsGeoJSONCSVtime isinteger, ms since 1970ISO 8601 stringexample16756462543422023-02-06T01:17:34.342Zpandasto_datetime(unit="ms", utc=True)to_datetime(utc=True)462 events matched by ID: identical times spatialworkflow.io
The same event, two formats.

3. Days in local time

Türkiye uses UTC+3 all year. Count events per day in UTC and in Istanbul time; events between 21:00 and 24:00 UTC move to the next local day:

t = pd.to_datetime(props.time, unit="ms", utc=True)
local = t.dt.tz_convert("Europe/Istanbul")
days = pd.DataFrame({"UTC": t.dt.date.value_counts(), "local": local.dt.date.value_counts()}).fillna(0).astype(int).sort_index()
print(days.head(4).to_string())
print(f"events whose date differs between UTC and local time: {(t.dt.date != local.dt.date).sum()} of {len(t)}")
main = props.loc[props.mag.idxmax()]
print(f"largest: M{main.mag} at {pd.to_datetime(main.time, unit='ms', utc=True):%Y-%m-%d %H:%M} UTC = "
      f"{pd.to_datetime(main.time, unit='ms', utc=True).tz_convert('Europe/Istanbul'):%Y-%m-%d %H:%M} local")
            UTC  local
time                  
2023-02-06  222    211
2023-02-07   56     62
2023-02-08   31     35
2023-02-09   21     20
events whose date differs between UTC and local time: 32 of 462
largest: M7.8 at 2023-02-06 01:17 UTC = 2023-02-06 04:17 local

On 6 February, the day of the M7.8 and the M7.5 nine hours later, there were 222 M4+ events in UTC and 211 in local time: eleven late-evening events moved to 7 February locally. Over the month, 32 of the 462 events, 7%, fall on a different date in UTC and in local time. The M7.8 itself struck at 01:17 UTC, 04:17 local time, the same date in both.

Updated times and the deprecated tz field

The data used on this page

Earthquakes of magnitude 4 or more between 35° and 40°N and 35° and 40°E from 6 February to 6 March 2023, from the USGS FDSN event service, downloaded once as GeoJSON and once as CSV.

import json
from pathlib import Path

import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
Q = "https://earthquake.usgs.gov/fdsnws/event/1/query"
PARAMS = dict(starttime="2023-02-06", endtime="2023-03-07", minmagnitude=4, minlatitude=35, maxlatitude=40,
              minlongitude=35, maxlongitude=40, orderby="time-asc")
def get(fmt):
    p = Path(f"data/turkey_2023_m4.{fmt}")
    if not p.exists():
        r = requests.get(Q, params=dict(format="geojson" if fmt == "json" else fmt, **PARAMS), timeout=300)
        r.raise_for_status()
        p.write_text(r.text)
    return p
gj = json.loads(get("json").read_text())
props = pd.DataFrame([{**f["properties"], "id_": f["id"]} for f in gj["features"]])
csv = pd.read_csv(get("csv"))
print(f"GeoJSON: {len(props)} events; CSV: {len(csv)} rows; time column types {props.time.dtype} and {csv.time.dtype}")
GeoJSON: 462 events; CSV: 462 rows; time column types int64 and str

Both formats return the same 462 events. In the GeoJSON the time is an integer (int64); in the CSV it is a string.

Example 1: the updated field and the deprecated tz

updated, the time of the last revision, is in milliseconds too. Older feeds also carried tz, a local offset in minutes; check what it holds now:

upd = pd.to_datetime(props.updated, unit="ms", utc=True)
print(f"updated: {upd.min():%Y-%m-%d} to {upd.max():%Y-%m-%d}; median delay after the event {(upd - t).median().days} days")
print(f"tz values: {props.tz.value_counts(dropna=False).to_dict() if 'tz' in props else 'no tz field'}")
updated: 2023-04-15 to 2026-08-12; median delay after the event 68 days
tz values: {None: 462}

Every event has been revised since it happened: the last updates run from April 2023 to August 2026, with a median of 68 days after the earthquake. The tz field is present but empty for all 462 events.

Grouped bar chart of M4 and larger events per day from 6 to 15 February 2023, by UTC date and by local date: 222 against 211 on 6 February, 56 against 62 on 7 February, 31 against 35 on 8 February, and smaller differences after that. Grouped bar chart of M4 and larger events per day from 6 to 15 February 2023, by UTC date and by local date: 222 against 211 on 6 February, 56 against 62 on 7 February, 31 against 35 on 8 February, and smaller differences after that.
Events per day by time zone.

Epochs counted in different units

Map of the 462 M4 and larger earthquakes of the Kahramanmaras sequence from 6 February to 6 March 2023, coloured by days after the M7.8, with stars at the M7.8 and M7.5: a south-west to north-east line of events from Antakya past Kahramanmaras and Gaziantep, and an east-west line through the M7.5 towards Malatya. Map of the 462 M4 and larger earthquakes of the Kahramanmaras sequence from 6 February to 6 March 2023, coloured by days after the M7.8, with stars at the M7.8 and M7.5: a south-west to north-east line of events from Antakya past Kahramanmaras and Gaziantep, and an east-west line through the M7.5 towards Malatya.
The sequence on the map.

Unix time counts from 1970-01-01 00:00:00 UTC, and different systems count in different units: seconds in most Unix tools, milliseconds in JavaScript and in the USGS GeoJSON feeds, nanoseconds inside pandas. An integer carries no unit, so pd.to_datetime assumes its own, nanoseconds, and a 2023 timestamp in milliseconds lands half an hour after the epoch. The fix is one argument, unit="ms", and utc=True makes the result timezone-aware so later conversions are explicit.

Time zones are the second half. The USGS stores every origin time in UTC, which is right for science and wrong for anything human: the first Kahramanmaraş earthquake struck at 04:17 local time, 01:17 UTC, the same date either way, but evening events change date. Daily counts, "aftershocks on the first day" and any comparison with human activity need local time, from tz_convert with the region's IANA zone name.

Seconds, aware times and Excel

Checks when handling USGS earthquake times. before using event timeswhat unit is the integer?1.7e12 = ms, 1.7e9 = smake it timezone-awareutc=Trueconvert for local datesIANA zone name, not an offsetupdated is ms toomedian 68 days after the eventignore tzempty for all 462 eventsUSGS FDSN event service, GeoJSON spatialworkflow.io
Before using event times.
  • Seconds: other sources give epoch seconds; use unit="s". A value around 1.7 × 10⁹ is seconds, 1.7 × 10¹² milliseconds.
  • Naive versus aware: without utc=True the result is naive; mixing naive and aware timestamps raises errors in comparisons.
  • Daylight saving: use a zone name such as America/Los_Angeles, not a fixed offset, so summer and winter are both right.
  • Excel: spreadsheets convert ISO strings to local time on import; keep UTC in the file and convert in code.

FAQ

Why are my USGS earthquake dates in 1970?

The GeoJSON time property is milliseconds since 1970 and pd.to_datetime reads integers as nanoseconds; use pd.to_datetime(time, unit="ms", utc=True).

Are USGS earthquake times in UTC?

Yes, all origin times are UTC; convert with tz_convert and the local time zone name for local dates and hours.

Does the USGS CSV format have the same problem?

No: the CSV gives ISO 8601 strings ending in Z, which pandas parses to the correct UTC time.

What is the updated field?

The time of the event's most recent revision, also in epoch milliseconds.