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.
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.
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.
Epochs counted in different units
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
- 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=Truethe 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.
Internal links
- How to download and map an earthquake catalogue in Python — the event service
- How to animate an earthquake sequence in Python — local time on a clock
- How to map focal mechanisms in Python — the same 2023 sequence
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.