Eruption Years Before Year 1: Negative Years and Radiocarbon Dates

On this page

Problem statement

Sort the Smithsonian's eruptions by year and the oldest comes out at -55500; plot them on a date axis and pandas refuses everything before 1678. The year column holds years before the Common Era as negative numbers, and most eruptions of the Holocene were never seen by anyone: their years come from radiocarbon, ice cores, tree rings or layers of ash. Some of those radiocarbon years are calibrated to calendar years and some are not, and an uncalibrated radiocarbon year in the early Holocene can be off from the calendar by a thousand years or more. This page shows how the years are stored, which methods produced them, and how to keep them comparable.

Quick answer

Keep the year as an integer, never a timestamp; split it from the dating method and its uncertainty, and flag uncalibrated radiocarbon:

yr = eru[["Volcano_Name", "StartDateYear", "StartDateYearUncertainty", "StartEvidenceMethod"]].copy()
yr["era"] = np.where(yr.StartDateYear <= 0, "BCE", "CE")
yr["uncalibrated"] = yr.StartEvidenceMethod.eq("Isotopic: 14C (uncalibrated)")
print(yr.era.value_counts().to_string())
print(f"oldest {yr.StartDateYear.min()}, uncalibrated radiocarbon years: {yr.uncalibrated.sum():,}; year 0 used: {(yr.StartDateYear == 0).sum()}")
era
CE     8774
BCE    2315
oldest -55500, uncalibrated radiocarbon years: 984; year 0 used: 1

8,774 eruptions are dated in the Common Era and 2,315 before it, written as negative years, the oldest -55500. 984 years come from uncalibrated radiocarbon. One eruption is dated to year 0, which the BCE/CE calendar doesn't have (1 BCE is followed by 1 CE). The column is best read as a number line of approximate years, where an off-by-one at the boundary is far smaller than the uncertainty of any date that old.

Stacked histogram on a log scale of eruption years since 10,000 BCE by dating method: isotopic and correlation dates before about 1500, observations dominating after. Stacked histogram on a log scale of eruption years since 10,000 BCE by dating method: isotopic and correlation dates before about 1500, observations dominating after.
Eruption years by how they were dated.

Years, methods and uncertainty

1. How old, by method

print(yr.groupby("StartEvidenceMethod").StartDateYear.agg(["size", "median"]).sort_values("size", ascending=False).head(7).to_string())
                               size  median
StartEvidenceMethod                        
Observations: Reported         6468  1923.0
Correlation: Tephrochronology  1043 -2000.0
Isotopic: 14C (uncalibrated)    984 -1320.0
Isotopic: 14C (calibrated)      869 -1200.0
Sidereal: Varve Count           187   190.0
Observations: Hydrophonic        72  1982.5
Correlation: Magnetism           58   475.0

The method follows the age. Reported observations have a median year of 1923, and hydrophone detections 1982.5. Matched ash layers have a median of 2000 BCE, and radiocarbon dates of 1320 and 1200 BCE. Before written records, a year in this column is a laboratory measurement with an error bar, not an observation.

2. Uncertainty

has = yr.StartDateYearUncertainty.notna()
print(f"with a year uncertainty: {has.sum():,}; median ±{yr[has].StartDateYearUncertainty.median():.0f} years; before 1 CE: ±{yr[has & (yr.era == 'BCE')].StartDateYearUncertainty.median():.0f}")
with a year uncertainty: 2,047; median ±100 years; before 1 CE: ±150

2,047 eruptions carry a year uncertainty, with a median of ±100 years, and ±150 for those before the Common Era. Many of the others have none recorded rather than none in fact. Sorting or binning by decade over those centuries gives a precision the dates don't have.

Scatter on a log scale of year uncertainty against year: hundreds of years before the Common Era, falling to a few years or less after 1500. Scatter on a log scale of year uncertainty against year: hundreds of years before the Common Era, falling to a few years or less after 1500.
Year uncertainty, where the table gives one.

3. Why datetime fails

try:
    pd.to_datetime(yr.StartDateYear.astype(str), format="%Y")
except (ValueError, pd.errors.OutOfBoundsDatetime) as err:
    print(type(err).__name__, str(err)[:70])
ValueError time data "-104" doesn't match format "%Y". You might want to try:
   

%Y doesn't accept negative years, and pandas timestamps can't go back further than 1677 anyway. Keep prehistoric years as integers or floats, plot them on a numeric axis, and convert only recent, day-precise dates to timestamps.

Comparing ages

The data used on this page

The Smithsonian Institution Global Volcanism Program's Volcanoes of the World database (version 5.3 is cited as doi:10.5479/si.GVP.VOTW5-2025.5.3): its Holocene volcano and eruption tables, read from the program's WFS service on 19 October 2026 and saved on this site, because the database is edited as eruptions are reported.

from pathlib import Path
import zipfile

import geopandas as gpd
import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
# Smithsonian Global Volcanism Program, Volcanoes of the World: Holocene volcanoes, Holocene eruptions and Pleistocene
# volcanoes as GeoJSON from its WFS service, with Natural Earth's populated places and 1:110m countries.
# Saved 19 October 2026, because the database is edited as eruptions are reported.
# Live: https://webservices.volcano.si.edu/geoserver/GVP-VOTW/ows?service=WFS&version=2.0.0&request=GetFeature
#       &outputFormat=json&typeName=GVP-VOTW:Smithsonian_VOTW_Holocene_Volcanoes (and ..._Holocene_Eruptions)
ARCHIVE = "gvp-votw-2026-10-19.zip"
if not Path(f"data/{ARCHIVE}").exists():
    Path(f"data/{ARCHIVE}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{ARCHIVE}", timeout=600).content)
Z = zipfile.ZipFile(f"data/{ARCHIVE}")
vol = gpd.read_file(Z.open("Smithsonian_VOTW_Holocene_Volcanoes.json"))
eru = gpd.read_file(Z.open("Smithsonian_VOTW_Holocene_Eruptions.json"))
print(f"Volcanoes of the World: {len(vol):,} Holocene volcanoes, {len(eru):,} eruptions")
Volcanoes of the World: 1,214 Holocene volcanoes, 11,089 eruptions

Example 1: years before present

yr["bp"] = 1950 - yr.StartDateYear
print(yr.sort_values("StartDateYear").head(3)[["Volcano_Name", "StartDateYear", "bp", "StartEvidenceMethod"]].to_string(index=False))
      Volcano_Name  StartDateYear    bp           StartEvidenceMethod
Golden Trout Creek         -55500 57450 Isotopic: Cosmic Ray Exposure
      Quetrupillan         -11345 13295    Isotopic: 14C (calibrated)
       Yojoa, Lago         -11073 13023    Isotopic: 14C (calibrated)

The oldest eruption in the table is at Golden Trout Creek in California in 55,500 BCE, dated by cosmic-ray exposure and far older than the Holocene. The next two, Quetrupillán in Chile and Lago Yojoa in Honduras, fall right at the start of the Holocene.

Radiocarbon laboratories report ages as years before present, where present is 1950. The table converts them to calendar-style years, so 1950 minus the year gives the age back, which is the scale to plot prehistoric eruptions on.

Example 2: calibrated and uncalibrated side by side

r = yr[yr.StartEvidenceMethod.str.startswith("Isotopic: 14C", na=False)]
print(r.groupby("StartEvidenceMethod").StartDateYear.describe()[["count", "min", "50%", "max"]].to_string())
                              count      min     50%     max
StartEvidenceMethod                                         
Isotopic: 14C (calibrated)    869.0 -11345.0 -1200.0  1954.0
Isotopic: 14C (uncalibrated)  984.0 -10060.0 -1320.0  1820.0

Calibrated and uncalibrated radiocarbon years sit side by side with similar ranges, but they are not on the same scale. Radiocarbon in the atmosphere has varied, so a raw radiocarbon age drifts from the calendar, by a thousand years or more in the early Holocene. The table doesn't convert the uncalibrated ones. Comparing an uncalibrated 8000 BCE with a calibrated 8000 BCE, or an ice-core year, compares two different clocks. Keep the method beside the year, and compare like with like.

Two step histograms of calibrated and uncalibrated radiocarbon-dated eruptions by year, overlapping across the Holocene. Two step histograms of calibrated and uncalibrated radiocarbon-dated eruptions by year, overlapping across the Holocene.
Calibrated and uncalibrated radiocarbon years in one column.