Volcanic Eruption Dates That Will Not Parse: Month 0, Day 0 and 2 July

On this page

Problem statement

Build a date from the Smithsonian eruption table's year, month and day columns and pandas raises month must be in 1..12, or, with errors="coerce", silently turns a third of the eruptions into NaT. The table uses 0 for a month or day that is not known. It also has a second, quieter code: an eruption known only to its year is often given 2 July with an uncertainty of 182 days, the middle of the year plus or minus half a year. That one parses without complaint, and it piles more than a thousand eruptions onto one summer day. This page finds both codes and builds dates that say how much is known.

Quick answer

Treat month or day 0 as unknown, and a day uncertainty of 182 days as "known to the year"; keep the precision in its own column:

y, m, d, u = eru.StartDateYear, eru.StartDateMonth, eru.StartDateDay, eru.StartDateDayUncertainty
eru["precision"] = np.select([m.isna() | (m == 0), u.fillna(0) >= 182, d == 0], ["year", "year", "month"], "day")
ok = eru.precision == "day"
eru["start"] = pd.to_datetime(pd.DataFrame({"year": y, "month": m, "day": d}).where(ok & (y >= 1678)), errors="coerce")
print(eru.precision.value_counts().to_string())
print(f"exact start dates built: {eru.start.notna().sum():,}")
precision
day      5678
year     5232
month     179
exact start dates built: 5,377

5,678 eruptions have a start known to the day, 179 to the month and 5,232 only to the year, counting the 2 July placeholder as a year. Of the day-precise ones, 5,377 fall on or after 1678 and become timestamps. The precision column travels with the date, so a later analysis can choose what it needs: durations only from day-precise dates, a count by year from all of them.

Stacked bars of start-date precision by period: all BCE eruptions known only to the year, most since 1950 known to the day. Stacked bars of start-date precision by period: all BCE eruptions known only to the year, most since 1950 known to the day.
How precisely eruption starts are known.

The two codes

1. The error

try:
    pd.to_datetime(pd.DataFrame({"year": y, "month": m, "day": d})[y >= 1700])
except ValueError as err:
    print(type(err).__name__, str(err)[:60])
print(f"month 0: {(m == 0).sum():,}; day 0 with a month: {((d == 0) & (m > 0)).sum():,}")
ValueError cannot assemble the datetimes: time data "17960100" doesn't 
month 0: 3,818; day 0 with a month: 179

pandas assembles each row into a string such as 17960100, year, month, day, and stops at the first row with a 0, here an eruption of 1796 known only to the year. Month 0 appears on 3,818 eruptions and day 0 on 179 more whose month is known. With errors="coerce", all of them become NaT without a word.

2. The mid-year placeholder

print(eru[(m > 0) & (d > 0)].groupby([m, d]).size().nlargest(4).to_string())
mid = (m == 7) & (d == 2)
print(f"2 July with ±182 days: {(mid & (u == 182)).sum():,}; 2 July otherwise: {(mid & (u != 182)).sum()}")
print("day uncertainty on the 16th:", u[d == 16].value_counts(dropna=False).head(3).to_dict())
StartDateMonth  StartDateDay
7.0             2.0             1140
1.0             16.0             125
2.0             16.0              96
4.0             16.0              94
2 July with ±182 days: 1,123; 2 July otherwise: 17
day uncertainty on the 16th: {15.0: 938, nan: 137, 60.0: 1}

The most common start date by far is 2 July: 1,140 eruptions, 1,123 of them with a day uncertainty of 182. That is the program's mid-year date for an eruption known only to its year, 2 July being the middle day of the year. A smaller version of the same idea is the 16th of each month with ±15 days, an eruption known only to its month: 938 of the 16th-of-the-month dates carry that uncertainty. The 17 eruptions on 2 July without it really did start on 2 July, or are dated some other way.

Bars of eruption starts by day of the year, flat except a spike of 1,140 on 2 July and small spikes on the 16th of each month. Bars of eruption starts by day of the year, flat except a spike of 1,140 on 2 July and small spikes on the 16th of each month.
Start dates by day of the year: the placeholders.

3. What the naive month count shows

print(eru[(m > 0)].StartDateMonth.astype(int).value_counts().sort_index().loc[[5, 6, 7, 8, 9]].to_string())
print(eru[eru.precision != "year"].StartDateMonth.astype(int).value_counts().sort_index().loc[[5, 6, 7, 8, 9]].to_string())
StartDateMonth
5     494
6     483
7    1615
8     466
9     457
StartDateMonth
5    494
6    482
7    491
8    464
9    457

Counted naively by month, July has 1,615 eruption starts, more than three times any other month. Without the year-only placeholders it has 491, in line with the rest. A seasonal pattern in eruptions found this way is an artefact of the code.

End dates and years before 1678

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: the same codes on the end date

em, ed, eu = eru.EndDateMonth, eru.EndDateDay, eru.EndDateDayUncertainty
print(f"end month 0: {(em == 0).sum()}; end 2 July ±182: {((em == 7) & (ed == 2) & (eu == 182)).sum()}; no end year: {eru.EndDateYear.isna().sum():,}")
end month 0: 105; end 2 July ±182: 198; no end year: 6,602

End dates use the same codes, 105 with month 0 and 198 on the 2 July placeholder, and 6,602 eruptions, most of them prehistoric, have no end year at all. A continuing eruption has an end date too, the date of the last report, marked with > in EndDateYearModifier.

Example 2: dates pandas cannot hold

print(pd.Timestamp.min.year, f"eruptions with a full date before 1678: {((y < 1678) & (m > 0) & (d > 0)).sum()}")
print(pd.Period(year=1631, month=12, day=16, freq="D"))
1677 eruptions with a full date before 1678: 312
1631-12-16

pd.Timestamp stores nanoseconds since 1970 in 64 bits, which reaches back only to 1677. Vesuvius's eruption of December 1631 needs a Period, a plain year-month-day tuple, or a datetime64 of coarser resolution.

Paired bars by month: July has 1,615 starts with every month value counted and 491 with year-only placeholders removed, in line with other months. Paired bars by month: July has 1,615 starts with every month value counted and 491 with year-only placeholders removed, in line with other months.
The July peak is a code.