Climate station data homogenization implementing the six-step procedure from González-Rouco et al. (2001), with six pluggable breakpoint tests.
Beta: rucola is under active development. The API may change between minor versions before a stable 1.0 release.
About the name: rucola is the Italian word for the salad green known in English as arugula or rocket — and it's only one letter away from Rouco, the surname of J. Fidel González-Rouco, first author of the 2001 paper this library implements.
Long climate records from ground stations are frequently affected by non-climatic discontinuities — station relocations, instrument replacements, changes in observation practice. rucola detects and corrects these breakpoints using an iterative reference-station approach:
- Build a normalized Q-series for each candidate station relative to its neighbors
- Apply one or more statistical breakpoint tests to the Q-series
- Correct detected breaks and refine the reference pool across six steps
Six tests are available: SNHT (Alexandersson 1986), Buishand range (Buishand 1982), Pettitt (Pettitt 1979), Worsley likelihood ratio (Worsley 1979), Easterling–Peterson two-phase regression (Easterling & Peterson 1995), and STARS sequential regime-shift test (Rodionov 2004). Tests can be run individually or in consensus combinations.
Both ratio (multiplicative, for precipitation) and difference (additive, for temperature) correction modes are supported.
pip install rucola # core (Polars + CSV)
pip install rucola[duckdb] # with DuckDB supportimport rucola
r = rucola.Rucola.from_csv("values.csv", "stations.csv")
# Run the six-step procedure
detection = r.run(rucola.RunConfig(mode="ratio"))
# Apply corrections
result = detection.normalize()
print(result.summary)
print(result.corrections)| Table | Required columns |
|---|---|
stations |
station_id, latitude, longitude |
values |
station_id, date, value, parameter |
Values must be at annual resolution and pre-filtered to a single parameter.
# from Polars DataFrames
rucola.Rucola.from_polars(values_df, stations_df)
# from CSV files
rucola.Rucola.from_csv("values.csv", "stations.csv")
# from a DuckDB file (requires: pip install rucola[duckdb])
rucola.Rucola.from_duckdb("climate.duckdb")detection = r.run(
rucola.RunConfig(
tests=[
rucola.SNHTTest(),
rucola.BuishandTest(),
rucola.PettittTest(),
rucola.StarsTest(l=10), # sequential regime-shift test
],
mode="ratio",
)
)from rucola import NormalizationConfig
result = detection.normalize(
NormalizationConfig(
consensus="majority", # require >50 % of tests to agree
tiebreak="strongest_signal",
break_window_years=3,
min_correction_magnitude=0.02,
min_relative_signal=1.2, # require signal 1.2× the critical value
min_years_from_end=5, # reject edge-effect artefacts (Hawkins 1977)
)
)Use composable predicates to filter which detected breaks are applied. Combine them with &, |, and ~:
from rucola import (
NormalizationConfig,
YearBetween, StationIn, StepIn,
MagnitudeAbove, SignalAbove,
NSignificantAbove, NeighborCountAbove,
)
# Trusted year window + minimum correction size
result = detection.normalize(
NormalizationConfig(
predicate=YearBetween(min=1960, max=2010) & MagnitudeAbove(threshold=0.05)
)
)
# Only correct a specific set of stations
result = detection.normalize(
NormalizationConfig(predicate=StationIn({"S1", "S3", "S7"}))
)
# Require strong evidence: signal 1.5× critical value, at least 3 tests agree,
# detected from a solid reference pool, skip early unreliable steps
result = detection.normalize(
NormalizationConfig(
predicate=SignalAbove(1.5) & NSignificantAbove(3)
& NeighborCountAbove(4) & ~StepIn({1, 2})
)
)Predicates are fully serializable via to_dict() / BreakPredicate.from_dict().
detection.to_json("detection.json")
result.to_json("result.json")
detection = rucola.DetectionResult.from_json("detection.json")
result = rucola.HomogenizationResult.from_json("result.json")git clone https://github.com/earthobservations/rucola
cd rucola
uv sync --all-groups
# code quality
poe format # auto-format with ruff
poe lint # check formatting and linting
poe type # type-check with ty
# testing
poe test # fast unit + constructor tests (default)
poe test-unit # unit tests only, verbose
poe test-slow # full pipeline tests, verbose
poe test-integration # DWD integration tests (requires network)
poe test-all # everything, verbose
poe coverage # run tests and generate coverage.xml for Codecov
# docs
poe docs-serve # live preview at localhost:8000
poe docs-build # build static site
# security / hygiene
poe audit # scan dependencies for vulnerabilities
poe deptry # check for unused/missing dependencies
poe zizmor # audit GitHub Actions workflows
# all-in-one
poe check # lint + type + audit + test- González-Rouco et al. (2001), J. Climate 14(5):964–978. doi:10.1175/1520-0442(2001)014<0964:QCAHOP>2.0.CO;2
- Hawkins (1977), Biometrika 64(2):279–288. doi:10.1093/biomet/64.2.279
- Alexandersson (1986), Int. J. Climatol. 6(6):661–675. doi:10.1002/joc.3370060607
- Alexandersson & Moberg (1997), Int. J. Climatol. 17(1):25–34. doi:10.1002/(SICI)1097-0088(199701)17:1<25::AID-JOC103>3.0.CO;2-J
- Buishand (1982), J. Hydrol. 58(1–2):11–29. doi:10.1016/0022-1694(82)90066-X
- Pettitt (1979), Appl. Stat. 28(2):126–135. doi:10.2307/2346729
- Worsley (1979), J. Amer. Statist. Assoc. 74(366):365–367. doi:10.1080/01621459.1979.10482519
- Easterling & Peterson (1995), Int. J. Climatol. 15(4):369–377. doi:10.1002/joc.3370150403
- Rodionov (2004), Geophys. Res. Lett. 31, L09204. doi:10.1029/2004GL019448
rucola was created by Benjamin Gutzmann, with the majority of the implementation written by Claude (Anthropic).