Skip to contents

CRAN status CRAN downloads Total Downloads Lifecycle: stable License: MIT

An R package for computing climate indices from daily weather observations. Takes vectors of temperature, precipitation, humidity, and wind data and returns tidy data frames: no file wrangling, no class coercion, no API calls.

What are climate indices?

Standardised summary statistics that compress daily weather observations into measures like frost-day counts, growing-season length, drought severity, and heat-index exceedances. The definitions come from international bodies (WMO ETCCDI 27, ET-SCI extensions) and from individual research communities (SPI / SPEI for drought, Huglin / Winkler for viticulture).

Getting started: where to get the data

You bring the data, climatekit does the maths. If you already have a CSV:

library(climatekit)
weather <- read.csv("my_weather_station.csv")
ck_frost_days(weather$tmin, weather$date)

If you don’t have data yet, pair with readnoaa for free NOAA daily observations from 100,000+ stations. No API key:

install.packages("readnoaa")  # or devtools::install_github("charlescoverdale/readnoaa")
library(readnoaa)
library(climatekit)

# Step 1: Find a station near you
noaa_nearby(lat = 51.5, lon = -0.1, radius_km = 25)
#>        station                    name latitude longitude distance_km
#>   UKE00105915     LONDON WEATHER CENTRE   51.517    -0.117        1.4

# Step 2: Download daily data
weather <- noaa_daily("UKE00105915", "2020-01-01", "2024-12-31",
                      datatypes = c("TMAX", "TMIN", "PRCP"))

# Step 3: Compute indices
ck_frost_days(weather$tmin, weather$date, period = "annual")
ck_spi(weather$prcp, weather$date, scale = 3)
ck_heating_degree_days((weather$tmax + weather$tmin) / 2, weather$date)

Common data sources

Region Source Coverage Access
Global NOAA GHCNd 100,000+ stations worldwide Free, no key. Use readnoaa
Global ERA5 reanalysis Gridded, 0.25° resolution, 1940–present Free, requires CDS account
UK Met Office MIDAS ~1,000 UK stations, daily Free via CEDA, requires registration
Europe ECA&D 20,000+ stations across Europe Free download
US ACIS (RCC) All US cooperative & ASOS stations Free, no key
Australia Bureau of Meteorology All BoM stations, daily Free download

Why does this package exist?

R has the methods scattered across packages with incompatible interfaces:

Package Coverage Limitation
ClimInd Multi-family climate indices Returns raw vectors with no metadata, dates, or units
climdex.pcic (archived from CRAN, 2023) 27 ETCCDI core indices Requires a custom climdexInput S4 object
SPEI SPI + SPEI drought indices Single-purpose
heatwaveR Marine + atmospheric heatwaves Single-purpose
weathermetrics Unit conversions + heat index No climate indices

climatekit replaces these with a single interface: vectors in, tidy data frames out, 50+ indices spanning temperature, precipitation, drought, agroclimatic, and comfort categories.

library(climatekit)
ck_frost_days(tmin, dates)                      # data.frame
ck_spi(precip, dates, scale = 3)                # data.frame
ck_growing_degree_days(tavg, dates, base = 10)  # data.frame
ck_huglin(tmin, tmax, dates, lat = 45)          # data.frame

Installation

install.packages("climatekit")

# Or install the development version from GitHub
# install.packages("devtools")
devtools::install_github("charlescoverdale/climatekit")

Functions

ETCCDI canonical 27

The full ETCCDI core set (Alexander et al. 2006; Zhang et al. 2011) is implemented. ck_etccdi_27() returns an audit table mapping every code to its climatekit function.

Code Function Description
FD ck_frost_days() Days where Tmin < 0 °C
ID ck_ice_days() Days where Tmax < 0 °C
SU ck_summer_days() Days where Tmax > 25 °C
TR ck_tropical_nights() Days where Tmin > 20 °C
TXx ck_txx() Annual / monthly max of Tmax
TNx ck_tnx() Annual / monthly max of Tmin (warmest night)
TXn ck_txn() Annual / monthly min of Tmax (coldest day)
TNn ck_tnn() Annual / monthly min of Tmin (coldest night)
DTR ck_diurnal_range() Mean daily temperature range
GSL ck_growing_season() Growing season length
TX10p ck_tx10p() % cool days (calendar-day base, optional Zhang 2005 bootstrap)
TN10p ck_tn10p() % cool nights (calendar-day base, optional bootstrap)
TX90p ck_tx90p() % warm days (calendar-day base, optional bootstrap)
TN90p ck_tn90p() % warm nights (calendar-day base, optional bootstrap)
WSDI ck_wsdi() Warm spell duration index
CSDI ck_csdi() Cold spell duration index
RX1day ck_max_1day_precip() Max 1-day precipitation
RX5day ck_max_5day_precip() Max 5-day precipitation
SDII ck_precip_intensity() Simple daily intensity index
R10mm ck_heavy_precip() (default 10) Days with precip >= 10 mm
R20mm ck_very_heavy_precip() (default 20) Days with precip >= 20 mm
Rnnmm ck_heavy_precip(threshold = nn) Days with precip >= nn mm
CDD ck_dry_days() Max consecutive dry days
CWD ck_wet_days() Max consecutive wet days
R95p ck_r95p() Total precip on very-wet days
R99p ck_r99p() Total precip on extremely-wet days
PRCPTOT ck_total_precip() Annual wet-day precip total

ET-SCI heatwave family

Period of >= 3 consecutive days with TX above the calendar-day 90th percentile, plus cold-wave duals (TN below 10th percentile).

Code Function Description
HWN ck_hwn() Number of distinct heatwave events
HWF ck_hwf() Total days inside heatwave events
HWD ck_hwd() Longest heatwave duration
HWM ck_hwm(mode = "excess" / "absolute") Mean magnitude across event days
HWA ck_hwa(mode = "excess" / "absolute") Peak magnitude across event days
CWN ck_cwn() Cold-wave number
CWF ck_cwf() Cold-wave frequency
CWD ck_cwd() Cold-wave duration (note: ETCCDI also uses “CWD” for consecutive wet days, which is ck_wet_days)
CWM ck_cwm() Cold-wave magnitude
CWA ck_cwa() Cold-wave amplitude
EHF ck_ehf() Excess Heat Factor (Nairn & Fawcett 2013)

Drought, evapotranspiration

Function Description
ck_spi(distribution = "gamma" / "pearsonIII") Standardized Precipitation Index
ck_spei(distribution = "log-logistic" / "gev") Standardized Precipitation-Evapotranspiration Index
ck_pet() Reference evapotranspiration (Hargreaves)
ck_pet_pm() Reference evapotranspiration (FAO-56 Penman-Monteith)

Agroclimatic, comfort, energy

Function Description
ck_huglin(lat) Huglin heliothermal index (viticulture)
ck_winkler() Winkler index (wine region classification)
ck_branas(lat) Branas hydrothermal index (disease pressure)
ck_first_frost(lat) First autumn frost date (NH or SH)
ck_last_frost(lat) Last spring frost date (NH or SH)
ck_growing_degree_days() Accumulated GDD above base
ck_heating_degree_days() Heating degree days
ck_cooling_degree_days() Cooling degree days
ck_warm_spell() Warm-spell days (series-quantile, simpler variant of WSDI)
ck_wind_chill() Wind chill (Environment Canada / NWS)
ck_heat_index() Heat index (Rothfusz / NWS)
ck_humidex() Canadian humidex
ck_fire_danger() Simplified fire-danger proxy (use cffdrs for full FWI)

Discovery, dispatch, gridded

Function Description
ck_etccdi_27() Canonical ETCCDI 27 audit table
ck_catalogue() Full implementation catalogue
ck_browse(sector, standard, search) Filter the catalogue
ck_compute(data, index, ...) Dispatch any index by name
ck_available(), ck_metadata() Lightweight registry queries
ck_convert_temp() Celsius / Fahrenheit / Kelvin
ck_apply_grid(x, fun, dates, ...) Apply any function over a terra::SpatRaster
ck_from_netcdf(path, var) Thin reader for netCDF input
clear_cache() Clear the user-data cache

Examples

How many frost days does a location get?

library(climatekit)

# Daily minimum temperatures for a year
dates <- as.Date("2024-01-01") + 0:364
set.seed(42)
tmin <- sin(seq(0, 2 * pi, length.out = 365)) * 15 + 2

# Annual frost days
ck_frost_days(tmin, dates)
#>       period value      index unit
#>   2024-01-01   132 frost_days days

# Monthly breakdown
ck_frost_days(tmin, dates, period = "monthly")
#>       period value      index unit
#>   2024-01-01    25 frost_days days
#>   2024-02-01    17 frost_days days
#>   2024-03-01     4 frost_days days
#>   ...

How much heating energy does a building need?

# Heating degree days: cumulative warmth deficit below the base (default 18C).
tavg <- sin(seq(0, 2 * pi, length.out = 365)) * 12 + 10
ck_heating_degree_days(tavg, dates, period = "monthly")
#>       period  value                index        unit
#>   2024-01-01 481.10 heating_degree_days degree-days
#>   2024-02-01 378.49 heating_degree_days degree-days
#>   2024-03-01 244.53 heating_degree_days degree-days
#>   ...

# Cooling degree days for air conditioning demand
ck_cooling_degree_days(tavg, dates, base = 22)

Is a region in drought?

# SPI standardises rolling-window precipitation totals to N(0, 1).
# Values below -1 = moderate, -1.5 = severe, -2 = extreme drought.
dates_long <- seq(as.Date("2015-01-01"), as.Date("2024-12-31"), by = "day")
set.seed(42)
precip <- rgamma(length(dates_long), shape = 2, rate = 0.5)

spi <- ck_spi(precip, dates_long, scale = 3)
head(spi)
#>       period      value index        unit
#>   2015-03-01 -0.2891577   spi dimensionless
#>   2015-04-01  0.4458927   spi dimensionless
#>   ...

# SPEI adds evapotranspiration to capture temperature-driven drought
pet <- ck_pet(tmin, tmax, lat = 51.5, dates = dates)

What wine regions does a climate support?

# The Huglin heliothermal index classifies grape-growing potential:
# < 1500: too cool for viticulture
# 1500-1800: cool climate (Champagne, Mosel)
# 1800-2100: temperate (Burgundy, Oregon)
# 2100-2400: warm (Bordeaux, Napa)
# > 2400: hot (Barossa, Southern Spain)

dates_gs <- seq(as.Date("2024-04-01"), as.Date("2024-09-30"), by = "day")
set.seed(42)
tmin_gs <- rnorm(length(dates_gs), mean = 12, sd = 3)
tmax_gs <- tmin_gs + runif(length(dates_gs), 8, 15)

ck_huglin(tmin_gs, tmax_gs, dates_gs, lat = 45)
#>       period    value  index        unit
#>   2024-01-01 2129.284 huglin degree-days

# Winkler index (wine region classification)
tavg_gs <- (tmin_gs + tmax_gs) / 2
ck_winkler(tavg_gs, dates_gs)

When did frost season start and end?

dates_year <- as.Date("2024-01-01") + 0:364
set.seed(42)
tmin_year <- -10 + seq_along(dates_year) * 0.08 + rnorm(365, sd = 4)

ck_last_frost(tmin_year, dates_year)
#>       period value       date      index       unit
#>   2024-01-01   120 2024-04-29 last_frost day of year

ck_first_frost(tmin_year, dates_year)

How dangerous is a heatwave?

# Heat index = apparent temperature from T + RH. Above ~40C is dangerous.
ck_heat_index(tavg = c(30, 33, 36, 39), humidity = c(60, 65, 70, 75))
#>      value      index unit
#>   32.94844 heat_index   °C
#>   38.67052 heat_index   °C
#>   47.57163 heat_index   °C
#>   60.56858 heat_index   °C

# Wind chill for cold conditions
ck_wind_chill(tavg = c(-5, -10, -15), wind_speed = c(20, 30, 40))

# Fire weather risk
ck_fire_danger(tavg = 35, humidity = 15, wind_speed = 30, precip = 0)

Removing the in-base bias with the Zhang (2005) bootstrap

# Inside the reference period, each year biases its own threshold.
# bootstrap = TRUE applies Zhang (2005) leave-one-out resampling.
ck_tx10p(tmax, dates, ref_start = 1961L, ref_end = 1990L, bootstrap = TRUE)

Operational heatwave intensity (Excess Heat Factor)

# EHF: 3-day mean anomaly above the 95th percentile + acclimatisation.
# Australian BoM operational metric; positive EHF = heatwave conditions.
ck_ehf(tmax, tmin, dates, ref_start = 1961L, ref_end = 1990L, stat = "max")
ck_ehf(tmax, tmin, dates, stat = "n_positive")    # heatwave-condition day count
ck_ehf(tmax, tmin, dates, stat = "sum_positive")  # severity-weighted total

FAO-56 Penman-Monteith reference evapotranspiration

# ck_pet() is Hargreaves (Tmin / Tmax / lat). ck_pet_pm() is FAO-56
# Penman-Monteith with optional humidity, wind, Rs, and elevation.
ck_pet_pm(tmin, tmax, lat = 45, dates = dates,
          elev = 200, wind = 2.5,
          rh_min = rh_min, rh_max = rh_max)

Computing indices programmatically

# ck_compute() dispatches on a string index name. Useful in loops or apps.
weather <- data.frame(
  dates = as.Date("2024-01-01") + 0:364,
  tmin = sin(seq(0, 2 * pi, length.out = 365)) * 15 + 2,
  tmax = sin(seq(0, 2 * pi, length.out = 365)) * 15 + 12,
  precip = rgamma(365, shape = 0.5, rate = 0.2)
)

# Compute any index by name
ck_compute(weather, "frost_days")
ck_compute(weather, "total_precip", period = "monthly")

# See all available indices
ck_available()
#>                 index      category          unit
#>            frost_days   temperature          days
#>              ice_days   temperature          days
#>          summer_days    temperature          days
#>    tropical_nights     temperature          days
#>    ...

Package Description
readnoaa NOAA weather and climate data (pairs with climatekit for data acquisition)
carbondata Carbon market data (EU/UK ETS, voluntary registries)
cer Clean Energy Regulator data (Australia)
aemo Australian Energy Market Operator data

Migrating from climdex.pcic

climdex.pcic was the standard R implementation of the canonical ETCCDI 27 until it was archived from CRAN in 2023. climatekit covers the same set with a simpler interface:

# climdex.pcic
ci <- climdexInput.raw(tmax, tmin, prec, ..., base.range = c(1961, 1990))
fd <- climdex.fd(ci)        # named numeric vector

# climatekit
fd <- ck_frost_days(tmin, dates)   # tidy data frame

See vignette("climdex-migration", package = "climatekit") for the full function-name crosswalk and interface-shift notes.

Citation

citation("climatekit")

For academic use, also cite Alexander et al. (2006) and Zhang et al. (2011) (canonical ETCCDI), and Zhang et al. (2005) if you use the in-base bootstrap. inst/CITATION and CITATION.cff carry the bibentries.

Issues

Please report bugs or requests at https://github.com/charlescoverdale/climatekit/issues.