Skip to content

rucola

rucola

Climate station data homogenization implementing the six-step procedure from González-Rouco et al. (2001), with six pluggable breakpoint tests.

CI status Docs status Code coverage PyPI version PyPI status PyPI downloads/month Python versions License

Documentation

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.

Overview

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:

  1. Build a normalized Q-series for each candidate station relative to its neighbors
  2. Apply one or more statistical breakpoint tests to the Q-series
  3. 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.

Installation

pip install rucola           # core (Polars + CSV)
pip install rucola[duckdb]   # with DuckDB support

Quick start

import 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)

Input format

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.

Loaders

# 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")

Multiple tests with consensus detection

detection = r.run(
    rucola.RunConfig(
        tests=[
            rucola.SNHTTest(),
            rucola.BuishandTest(),
            rucola.PettittTest(),
            rucola.StarsTest(l=10),   # sequential regime-shift test
        ],
        mode="ratio",
    )
)

Normalization options

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)
    )
)

Break predicates

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().

Saving and loading results

detection.to_json("detection.json")
result.to_json("result.json")

detection = rucola.DetectionResult.from_json("detection.json")
result    = rucola.HomogenizationResult.from_json("result.json")

Development

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

References

Authors

rucola was created by Benjamin Gutzmann, with the majority of the implementation written by Claude (Anthropic).