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.frameInstallation
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 totalFAO-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
#> ...Related packages
| 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 frameSee 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.