Earthquake catalogues explained: magnitudes, completeness and declustering

On this page

Problem statement

An earthquake catalogue looks like the cleanest dataset in hazard work: a point, a time, a depth, a magnitude. It is not a sample of earthquakes, though. It is a record of what a particular network of instruments detected, with magnitudes computed by several different methods, over a period during which the network changed. Count events straight out of the download and you will measure the seismic network's history as much as the earth's.

Three properties decide whether a catalogue analysis means anything: the magnitude of completeness, below which small events are missed; the magnitude type, which can differ between events in the same file; and clustering, because aftershocks are not independent events and most statistical methods assume they are.

Quick answer

Download from a catalogue service, then check completeness, magnitude type and time span before counting anything:

import pandas as pd, requests

def usgs(bbox, start, end, minmag=2.0):
    r = requests.get("https://earthquake.usgs.gov/fdsnws/event/1/query",
                     params={"format": "geojson", "starttime": start, "endtime": end,
                             "minlatitude": bbox[1], "maxlatitude": bbox[3],
                             "minlongitude": bbox[0], "maxlongitude": bbox[2],
                             "minmagnitude": minmag, "limit": 20000}, timeout=120)
    feats = r.json()["features"]
    return pd.DataFrame([{
        "time": pd.to_datetime(f["properties"]["time"], unit="ms", utc=True),
        "mag": f["properties"]["mag"], "magtype": f["properties"]["magType"],
        "depth": f["geometry"]["coordinates"][2],
        "lon": f["geometry"]["coordinates"][0], "lat": f["geometry"]["coordinates"][1],
        "place": f["properties"]["place"]} for f in feats])

eq = usgs([-122.5, 35.5, -117.5, 38.5], "2000-01-01", "2024-12-31", 2.5)  # central California
print(f"{len(eq):,} events, {eq.time.min().date()} to {eq.time.max().date()}")
print(f"(the service caps a query at 20,000 events; at M2.0 this box hits the cap and "
      f"silently returns only the most recent decade)")
print("magnitude types:", eq.magtype.value_counts().head(5).to_dict())
print(f"magnitudes {eq.mag.min():.1f}–{eq.mag.max():.1f}, depths {eq.depth.min():.0f}–{eq.depth.max():.0f} km")
13,234 events, 2000-01-02 to 2024-12-29
(the service caps a query at 20,000 events; at M2.0 this box hits the cap and silently returns only the most recent decade)
magnitude types: {'ml': 6947, 'md': 5211, 'mw': 710, 'mlr': 185, 'mc': 106}
magnitudes 2.5–7.1, depths -3–45 km

Several magnitude types in one file is normal. They are not interchangeable: local magnitude, duration magnitude and moment magnitude are different measurements that happen to be scaled to agree roughly.

Fields in an earthquake catalogue and the trap in each. what each field is notfieldlooks likeactuallymagone scaleml, md, mw mixed in one filedeptha measurement10 km is often a defaulttimecomplete recorddetection changed over itplacea locationnearest named place, not theepicentrecountseismicitynetwork history + sequencescentral California 2000–2024, M2.5+: 13,234 events, four magnitude typesThe cleanest-looking dataset in hazard work has a caveat in every column spatialworkflow.io
Five fields, five things that are not quite what they look like.

Network, completeness, magnitude types, aftershocks

1. Know which network produced it

A global catalogue (USGS ComCat, ISC) is complete to a much higher magnitude than a dense regional network. Mixing them gives a catalogue whose completeness changes with location.

2. Estimate the magnitude of completeness

Below some magnitude, small events are missed. The usual quick estimate is the magnitude at which the frequency–magnitude curve peaks; fit anything only above it.

3. Check the magnitude types

magType may be ml, md, mw, mb. Analyses that assume one scale should filter to it or accept the mixture explicitly.

4. Decide about aftershocks

Aftershock sequences make events strongly dependent in time and space. Hazard models use declustered catalogues — mainshocks only — while operational aftershock forecasting needs the opposite.

5. Watch the time span

Apparent trends in event counts usually have one of two causes that are not seismicity: a network upgrade that lowered the detection threshold, or a single large sequence whose aftershocks fill a year or two.

6. Keep depth in mind

Depth is the least well constrained parameter, especially offshore, and "10 km" is often a fixed default rather than a measurement.

Event counts by magnitude threshold in a central California catalogue, showing the drop with magnitude. events above each magnitude, 2000–2024M ≥ 2.513,234M ≥ 3.04,076M ≥ 3.51,290M ≥ 4.0361M ≥ 5.0300700014kRoughly ten times fewer events per magnitude unit — the Gutenberg–Richter relation spatialworkflow.io
The peak is where detection fails, not where the earth stops producing earthquakes.

Completeness and a rise that is one sequence

The data used on this page

Earthquake catalogues from the USGS ComCat FDSN event service, requested by the page's own code. ComCat revises events as they are reviewed, so a later download of the same query can differ by an event or two.

import requests

reply = requests.get("https://earthquake.usgs.gov/fdsnws/event/1/version", timeout=60)
print("FDSN event service version", reply.text.strip())
FDSN event service version 2.8.1

Example 1 — magnitude of completeness, the quick way

import numpy as np

bins = np.arange(2.5, 6.0, 0.1)
counts, _ = np.histogram(eq.mag, bins=bins)
mc = bins[np.argmax(counts)]
print(f"peak of the frequency-magnitude histogram: M{mc:.1f}")
print(f"events at or above it: {(eq.mag >= mc).sum():,} of {len(eq):,}")
for m in (2.5, 3.0, 3.5, 4.0, 5.0):
    print(f"  M >= {m}: {(eq.mag >= m).sum():6,} events")
peak of the frequency-magnitude histogram: M2.5
events at or above it: 13,234 of 13,234
  M >= 2.5: 13,234 events
  M >= 3.0:  4,076 events
  M >= 3.5:  1,290 events
  M >= 4.0:    361 events
  M >= 5.0:     30 events

Example 2 — a "rise in seismicity" that is one sequence

per_year = eq.groupby(eq.time.dt.year).size()
print("events per year:", per_year.to_dict())
quiet = per_year.drop([2019, 2020])
print(f"mean per year 2000–2009: {per_year.loc[2000:2009].mean():.0f}")
print(f"mean per year 2015–2024: {per_year.loc[2015:2024].mean():.0f}")
print(f"mean per year 2015–2024 excluding 2019–20: {quiet.loc[2015:2024].mean():.0f}")
print("largest event:", eq.loc[eq.mag.idxmax(), ["time", "mag", "place"]].to_dict())
events per year: {2000: 369, 2001: 623, 2002: 296, 2003: 795, 2004: 929, 2005: 357, 2006: 318, 2007: 304, 2008: 271, 2009: 358, 2010: 291, 2011: 410, 2012: 247, 2013: 295, 2014: 381, 2015: 277, 2016: 340, 2017: 395, 2018: 254, 2019: 2434, 2020: 1848, 2021: 576, 2022: 331, 2023: 306, 2024: 229}
mean per year 2000–2009: 462
mean per year 2015–2024: 699
mean per year 2015–2024 excluding 2019–20: 338
largest event: {'time': Timestamp('2019-07-06 03:19:53.040000+0000', tz='UTC'), 'mag': 7.1, 'place': 'Ridgecrest Earthquake Sequence'}

Counting by decade says seismicity rose by half, from 462 events a year to 699. Counting by year says 2019 and 2020 happened: the M7.1 Ridgecrest sequence and its aftershocks. Drop those two years and the recent rate is 338 a year, below the 2000s — which had sequence years of their own, in 2003 and 2004. A rise caused by better detection looks much the same on a plot and has a different cause; the counts fix tells them apart.

Example 3 — how much of the catalogue is aftershocks

from scipy.spatial import cKDTree

eq_sorted = eq.sort_values("time").reset_index(drop=True)
main, xy = [], np.radians(eq_sorted[["lat", "lon"]].to_numpy())
R = 6371.0
coords = np.c_[R * np.cos(xy[:, 0]) * np.cos(xy[:, 1]), R * np.cos(xy[:, 0]) * np.sin(xy[:, 1]),
               R * np.sin(xy[:, 0])]
is_aftershock = np.zeros(len(eq_sorted), bool)
for i, row in eq_sorted.iterrows():
    if is_aftershock[i]:
        continue
    # Gardner-Knopoff style window: bigger events, bigger window
    days = 10 ** (0.032 * row.mag + 2.7389) if row.mag >= 6.5 else 10 ** (0.5409 * row.mag - 0.547)
    km = 10 ** (0.1238 * row.mag + 0.983)
    later = (eq_sorted.time > row.time) & (eq_sorted.time <= row.time + pd.Timedelta(days=days))
    near = np.linalg.norm(coords - coords[i], axis=1) <= km
    is_aftershock |= (later & near & (eq_sorted.mag <= row.mag)).to_numpy()
print(f"declustered: {(~is_aftershock).sum():,} mainshocks of {len(eq_sorted):,} events "
      f"({is_aftershock.mean():.0%} removed)")
declustered: 4,323 mainshocks of 13,234 events (67% removed)

Completeness is the first question

Why completeness is the first question

Every frequency–magnitude analysis — b-values, recurrence rates, hazard curves — assumes the catalogue contains all events above the magnitude being fitted. Below completeness the counts fall not because the earth produces fewer small earthquakes but because the network misses them, which bends the curve and biases every estimate taken from it.

Why magnitude types are not interchangeable

Local magnitude is measured from the amplitude of a seismogram, duration magnitude from how long the shaking lasts, moment magnitude from the physical moment of the rupture. They are calibrated to agree in the middle of their ranges and diverge at the ends, particularly for the largest events, where local magnitude saturates.

Why declustering changes the answer

Aftershocks are physically real and matter enormously for response. They are also not independent samples, so leaving them in a statistical model inflates the apparent rate of events and distorts the estimated tail. Standard hazard practice removes them with a declustering window and states which algorithm was used, because different algorithms remove different fractions.

An earthquake sequence: mainshock and aftershocks inside a time and distance window. one sequence, thousands of eventsbackground rate~300 events a yearM7.1 mainshock6 July 2019aftershocks2,434 events in 2019decaying1,848 in 2020background again229 in 2024Declustering removed 67% of this catalogue; hazard rates are fitted to what is left spatialworkflow.io
One sequence, dozens of events: independent samples they are not.

Blasts, preliminary magnitudes and fixed depths

  • Quarry blasts and other non-tectonic events are flagged in some catalogues and not others.
  • Preliminary magnitudes are revised; a downloaded catalogue is a snapshot.
  • Fixed depths of 5, 10 or 33 km are defaults where depth could not be resolved.
  • Induced seismicity near wastewater injection breaks stationarity assumptions.
  • Catalogue completeness varies in space, not just in time.
  • ISC-GEM is the standard homogenised global catalogue for long-term work.

FAQ

What is the magnitude of completeness?

The lowest magnitude above which a catalogue contains essentially all events. Below it, detection fails.

Are ml, md and mw the same?

No. They are different measurements calibrated to agree approximately; they diverge at the extremes.

Should I remove aftershocks?

For hazard rates and frequency–magnitude fits, yes. For response and forecasting, no.

Why do older years have fewer small events?

Because the network was sparser. That is detection, not seismicity.

Is depth reliable?

Less than location. Fixed values such as 10 km are defaults, not measurements.

Which catalogue should I use?

A regional network catalogue for local work, ISC-GEM or USGS ComCat for global or long-term analysis.