Introduction to ClimateAnomaly: Computing Climate Anomaly Indices

Pushkar Bora

2026-10-07

Overview

ClimateAnomaly is a general-purpose R package for computing standardised rainfall anomaly indices and temperature anomalies from any monthly (or annual) climate dataset. All indices are computed group-wise — typically per location × calendar month — so that each value reflects a departure from that location’s own long-term climatology for the same time of year.

Indices implemented

Index Full name Reference
RAI Rainfall Anomaly Index Van Rooy (1965)
MRAI Modified Rainfall Anomaly Index Ekpetere (2025)
SRAI Standardised Rainfall Anomaly Index Ekpetere (2025)
SSPI Simplified Standardised Precipitation Index Raziei (2021)
SPI Standardised Precipitation Index McKee et al. (1993)
TEMP Temperature anomaly (z-score) —

1 Quick start with the built-in dataset

library(ClimateAnomaly)

The package ships a sample dataset india_monthly — monthly climate records for six Indian states (1976–2026) spanning arid, tropical, Himalayan, semi-arid, high-rainfall, and peninsular climate zones.

data(india_monthly)
dim(india_monthly)
#> [1] 3654   12
head(india_monthly[, 1:8])
#>   state year month_num   month rainfall_mm rainy_days tmax_mean_c tmin_mean_c
#> 1 Assam 1976         1 1976-01        4.48          0       23.12        9.34
#> 2 Assam 1976         2 1976-02       44.01          6       24.23       12.75
#> 3 Assam 1976         3 1976-03       96.65         10       27.94       15.72
#> 4 Assam 1976         4 1976-04      153.34         20       30.94       19.29
#> 5 Assam 1976         5 1976-05      202.54         28       29.98       21.18
#> 6 Assam 1976         6 1976-06      543.10         30       29.43       22.80

Compute all six indices in one call:

result <- calc_all_indices(
  data       = india_monthly,
  group_cols = c("state", "month_num"),  # group = state × calendar month
  rain_col   = "rainfall_mm",
  tmax_col   = "tmax_mean_c",
  tmin_col   = "tmin_mean_c"
)

# New columns added
setdiff(names(result), names(india_monthly))
#>  [1] "RAI"             "MRAI"            "SRAI"            "SSPI"           
#>  [5] "SPI"             "tmax_anomaly"    "tmin_anomaly"    "tavg_c"         
#>  [9] "tavg_anomaly"    "RAI_class"       "MRAI_class"      "SRAI_class"     
#> [13] "SSPI_class"      "SPI_class"       "tmax_anom_class" "tmin_anom_class"
#> [17] "tavg_anom_class"

Inspect a single state:

mh <- subset(result, state == "Maharashtra" & year >= 2015,
             select = c("year", "month_num", "rainfall_mm",
                        "RAI", "RAI_class", "SPI", "SPI_class",
                        "tmax_anomaly", "tmax_anom_class"))
print(head(mh, 24))
#>      year month_num rainfall_mm        RAI      RAI_class         SPI
#> 1687 2015         1       13.15  2.1105769       Very Wet  1.20637108
#> 1688 2015         2        2.12 -1.2648664 Moderately Dry  0.05845866
#> 1689 2015         3       48.36  6.5600690  Extremely Wet  2.44813982
#> 1690 2015         4       24.46  4.9791757  Extremely Wet  1.91533192
#> 1691 2015         5       20.03  0.1202305    Near Normal  0.38649673
#> 1692 2015         6      235.64  1.1894571 Moderately Wet  0.58183296
#> 1693 2015         7      137.25 -4.7238113  Extremely Dry -2.23338892
#> 1694 2015         8      179.55 -3.0917360  Extremely Dry -1.54180289
#> 1695 2015         9      169.93 -0.5163190   Slightly Dry -0.08304929
#> 1696 2015        10       34.27 -2.0726764       Very Dry -0.73546146
#> 1697 2015        11        9.62 -1.5240139 Moderately Dry -0.02005225
#> 1698 2015        12        0.29 -2.8492516       Very Dry -0.56085411
#> 1699 2016         1        0.51 -2.7111029       Very Dry -0.55647884
#> 1700 2016         2        3.01 -0.5273933   Slightly Dry  0.27811408
#> 1701 2016         3       12.74  0.8548610   Slightly Wet  0.88006461
#> 1702 2016         4        4.05 -1.5708948 Moderately Dry -0.27284641
#> 1703 2016         5       10.98 -1.4898354 Moderately Dry -0.20440264
#> 1704 2016         6      184.20 -0.7139279   Slightly Dry -0.23515430
#> 1705 2016         7      445.82  1.4602419 Moderately Wet  0.80821239
#> 1706 2016         8      231.93 -1.4672839 Moderately Dry -0.62747762
#> 1707 2016         9      257.09  1.8058763 Moderately Wet  0.92420142
#> 1708 2016        10       77.57  0.2343224    Near Normal  0.33470186
#> 1709 2016        11        0.51 -3.1092807  Extremely Dry -1.35087100
#> 1710 2016        12        0.53 -2.7218585       Very Dry -0.41404214
#>           SPI_class tmax_anomaly tmax_anom_class
#> 1687 Moderately Wet   -1.3425321            Cool
#> 1688    Near Normal    0.5570989   Slightly Warm
#> 1689  Extremely Wet   -1.5107237            Cool
#> 1690   Severely Wet   -1.0334567            Cool
#> 1691    Near Normal    0.7705149   Slightly Warm
#> 1692     Mildly Wet   -0.3219821     Near Normal
#> 1693  Extremely Dry    2.3197181  Extremely Warm
#> 1694   Severely Dry    1.8101959            Warm
#> 1695    Near Normal    1.7703832            Warm
#> 1696     Mildly Dry    1.8843798            Warm
#> 1697    Near Normal    2.0208430  Extremely Warm
#> 1698     Mildly Dry    1.9528529            Warm
#> 1699     Mildly Dry    1.5347155            Warm
#> 1700    Near Normal    1.7306612            Warm
#> 1701     Mildly Wet    1.2458741            Warm
#> 1702    Near Normal    1.5172763            Warm
#> 1703    Near Normal    0.6883052   Slightly Warm
#> 1704    Near Normal    0.3800091     Near Normal
#> 1705     Mildly Wet   -0.8899401   Slightly Cool
#> 1706     Mildly Dry    0.6118260   Slightly Warm
#> 1707     Mildly Wet   -1.0188160            Cool
#> 1708    Near Normal   -0.8286269   Slightly Cool
#> 1709 Moderately Dry    0.6700497   Slightly Warm
#> 1710    Near Normal    1.1748701            Warm

2 Using your own data file

ClimateAnomaly imposes no fixed column names. Supply the actual column names from your file as arguments.

2.1 Loading a CSV

my_data <- read.csv("my_climate_data.csv", stringsAsFactors = FALSE)
head(my_data)

Assume the file has columns district, yr, mon, precip_mm, tmax_deg, tmin_deg.

my_result <- calc_all_indices(
  data       = my_data,
  group_cols = c("district", "mon"),   # group by district AND calendar month
  rain_col   = "precip_mm",
  tmax_col   = "tmax_deg",
  tmin_col   = "tmin_deg"
)

2.2 Loading an Excel file

# install.packages("readxl")   # if not yet installed
library(readxl)
my_xl <- as.data.frame(read_excel("my_data.xlsx", sheet = "Monthly"))

my_result <- calc_all_indices(
  data       = my_xl,
  group_cols = c("Station", "Month"),
  rain_col   = "Rainfall_mm",
  tmax_col   = "Tmax_C",
  tmin_col   = "Tmin_C"
)

2.3 Annual data (no month grouping)

For annual totals, group only by location:

annual_result <- calc_all_indices(
  data       = my_annual_data,
  group_cols = "station",   # or "state", "grid_id", …
  rain_col   = "annual_rain_mm",
  tmax_col   = NULL,   # skip temperature
  tmin_col   = NULL,
  indices    = c("RAI", "MRAI", "SPI")
)

2.4 Using the bundled full India dataset

src <- system.file("extdata", "state_weather_monthly.csv",
                   package = "ClimateAnomaly")
india_full <- read.csv(src, stringsAsFactors = FALSE)
full_result <- calc_all_indices(india_full,
                                group_cols = c("state","month_num"),
                                rain_col   = "rainfall_mm",
                                tmax_col   = "tmax_mean_c",
                                tmin_col   = "tmin_mean_c")

3 Running individual index functions

Each index can also be called directly on a single numeric vector (one group at a time).

3.1 RAI — Rainfall Anomaly Index

# Rainfall series for Kerala July (all available years)
kl_jul <- subset(india_monthly, state == "Kerala" & month_num == 7)$rainfall_mm
rai_vals <- calc_rai(kl_jul, n_extreme = 10L)
summary(rai_vals)
#>     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
#> -4.85774 -1.14463  0.17143  0.07083  1.39775  3.82819
classify_rai(rai_vals)
#>  [1] "Near Normal"    "Moderately Wet" "Moderately Wet" "Slightly Wet"  
#>  [5] "Very Wet"       "Near Normal"    "Slightly Dry"   "Near Normal"   
#>  [9] "Moderately Wet" "Very Dry"       "Very Dry"       "Extremely Dry" 
#> [13] "Slightly Dry"   "Near Normal"    "Slightly Dry"   "Very Wet"      
#> [17] "Moderately Wet" "Moderately Wet" "Extremely Wet"  "Slightly Wet"  
#> [21] "Slightly Wet"   "Extremely Wet"  "Slightly Wet"   "Moderately Wet"
#> [25] "Very Dry"       "Slightly Dry"   "Extremely Dry"  "Moderately Dry"
#> [29] "Very Dry"       "Slightly Wet"   "Near Normal"    "Extremely Wet" 
#> [33] "Near Normal"    "Extremely Wet"  "Near Normal"    "Moderately Dry"
#> [37] "Very Dry"       "Very Wet"       "Moderately Wet" "Very Dry"      
#> [41] "Moderately Dry" "Very Dry"       "Extremely Wet"  "Slightly Dry"  
#> [45] "Moderately Dry" "Near Normal"    "Moderately Wet" "Slightly Wet"  
#> [49] "Very Wet"       "Near Normal"    "Extremely Dry"

3.2 MRAI and SRAI

mrai_vals <- calc_mrai(kl_jul)
srai_vals <- calc_srai(kl_jul)
data.frame(year = subset(india_monthly, state=="Kerala" & month_num==7)$year,
           RAI  = round(rai_vals, 2),
           MRAI = round(mrai_vals, 2),
           SRAI = round(srai_vals, 2))
#>    year   RAI  MRAI  SRAI
#> 1  1976  0.33  0.26  1.75
#> 2  1977  1.30  1.02  2.10
#> 3  1978  1.68  1.31  2.23
#> 4  1979  0.80  0.63  1.92
#> 5  1980  2.37  1.85  2.48
#> 6  1981 -0.31 -0.19  3.00
#> 7  1982 -0.66 -0.41  3.00
#> 8  1983 -0.25 -0.15  3.00
#> 9  1984  1.40  1.10  2.14
#> 10 1985 -2.32 -1.43  3.00
#> 11 1986 -3.00 -1.85  3.00
#> 12 1987 -3.76 -2.32  2.15
#> 13 1988 -0.77 -0.47  3.00
#> 14 1989 -0.19 -0.12  3.00
#> 15 1990 -0.53 -0.33  3.00
#> 16 1991  2.81  2.20  2.64
#> 17 1992  1.52  1.19  2.18
#> 18 1993  1.02  0.80  2.00
#> 19 1994  3.20  2.51  2.78
#> 20 1995  0.89  0.70  1.95
#> 21 1996  0.60  0.47  1.85
#> 22 1997  3.51  2.75  2.89
#> 23 1998  0.67  0.53  1.88
#> 24 1999  1.39  1.09  2.13
#> 25 2000 -2.73 -1.69  3.00
#> 26 2001 -0.91 -0.56  3.00
#> 27 2002 -3.17 -1.96  3.00
#> 28 2003 -1.46 -0.90  3.00
#> 29 2004 -2.15 -1.33  3.00
#> 30 2005  0.52  0.41  1.82
#> 31 2006  0.17  0.13  1.70
#> 32 2007  3.83  3.00  3.00
#> 33 2008 -0.30 -0.18  3.00
#> 34 2009  3.76  2.95  2.98
#> 35 2010 -0.10 -0.06  3.00
#> 36 2011 -1.14 -0.70  3.00
#> 37 2012 -2.77 -1.71  3.00
#> 38 2013  2.50  1.96  2.53
#> 39 2014  1.26  0.99  2.08
#> 40 2015 -2.55 -1.58  3.00
#> 41 2016 -2.00 -1.23  3.00
#> 42 2017 -2.70 -1.67  3.00
#> 43 2018  3.30  2.58  2.81
#> 44 2019 -0.91 -0.56  3.00
#> 45 2020 -1.15 -0.71  3.00
#> 46 2021 -0.02 -0.01  3.00
#> 47 2022  1.78  1.39  2.27
#> 48 2023  0.54  0.43  1.83
#> 49 2024  2.94  2.30  2.68
#> 50 2025  0.19  0.15  1.71
#> 51 2026 -4.86 -3.00 -3.00

3.3 SPI — Standardised Precipitation Index

spi_vals <- calc_spi(kl_jul)
summary(spi_vals)
#>      Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
#> -3.296952 -0.434468  0.189060  0.003338  0.662968  1.479532

3.4 SSPI — Simplified SPI (skewness-based)

sspi_vals <- calc_sspi(kl_jul)
# Compare SPI vs SSPI
cor(spi_vals, sspi_vals, use = "complete.obs")
#> [1] 0.9822867

3.5 Temperature anomaly

kl_tmax <- subset(india_monthly, state == "Kerala" & month_num == 6)$tmax_mean_c
z <- calc_temp_anomaly(kl_tmax)
summary(z)
#>     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
#> -2.13290 -0.66293  0.05254  0.00000  0.65094  2.19897
classify_temp_anomaly(z)
#>  [1] "Warm"           "Slightly Cool"  "Extremely Cool" "Slightly Warm" 
#>  [5] "Slightly Cool"  "Extremely Cool" "Slightly Cool"  "Warm"          
#>  [9] "Cool"           "Cool"           "Near Normal"    "Warm"          
#> [13] "Near Normal"    "Slightly Cool"  "Near Normal"    "Cool"          
#> [17] "Near Normal"    "Near Normal"    "Slightly Cool"  "Near Normal"   
#> [21] "Near Normal"    "Warm"           "Near Normal"    "Slightly Cool" 
#> [25] "Slightly Cool"  "Cool"           "Near Normal"    "Warm"          
#> [29] "Slightly Cool"  "Near Normal"    "Near Normal"    "Slightly Cool" 
#> [33] "Near Normal"    "Near Normal"    "Slightly Warm"  "Near Normal"   
#> [37] "Slightly Warm"  "Cool"           "Warm"           "Near Normal"   
#> [41] "Near Normal"    "Near Normal"    "Slightly Cool"  "Extremely Warm"
#> [45] "Slightly Warm"  "Slightly Warm"  "Slightly Warm"  "Extremely Warm"
#> [49] "Slightly Warm"  "Near Normal"    "Near Normal"

4 Selecting a subset of indices

Use the indices argument to compute only what you need:

rain_only <- calc_all_indices(
  data       = india_monthly,
  group_cols = c("state", "month_num"),
  rain_col   = "rainfall_mm",
  tmax_col   = NULL,
  tmin_col   = NULL,
  indices    = c("RAI", "SPI"),
  classify   = FALSE   # skip classification labels
)
names(rain_only)
#>  [1] "state"       "year"        "month_num"   "month"       "rainfall_mm"
#>  [6] "rainy_days"  "tmax_mean_c" "tmin_mean_c" "tmax_peak_c" "tmin_low_c" 
#> [11] "complete"    "source"      "RAI"         "SPI"

5 Drought classification

# Frequency table of SPI classes across all states/months
tab <- table(result$SPI_class)
sort(tab, decreasing = TRUE)
#> 
#>    Near Normal     Mildly Dry     Mildly Wet Moderately Wet Moderately Dry 
#>           1458            543            512            347            341 
#>   Severely Dry   Severely Wet  Extremely Wet  Extremely Dry 
#>            156            149             83             65
# RAI class distribution per state
with(result, tapply(RAI_class, state, function(x) {
  sort(table(x), decreasing = TRUE)[1:3]
}))
#> $Assam
#> x
#>       Very Dry    Near Normal Moderately Dry 
#>             99             98             89 
#> 
#> $Kerala
#> x
#>       Very Dry Moderately Dry    Near Normal 
#>            119             96             92 
#> 
#> $Maharashtra
#> x
#>       Very Dry Moderately Dry    Near Normal 
#>            133             91             91 
#> 
#> $Rajasthan
#> x
#>       Very Dry    Near Normal Moderately Dry 
#>            171             85             83 
#> 
#> $`Tamil Nadu`
#> x
#> Moderately Dry       Very Dry    Near Normal 
#>            116            116             96 
#> 
#> $Uttarakhand
#> x
#> Moderately Dry       Very Dry    Near Normal 
#>            103            100             91

6 Simple time-series plot

kl_jun <- subset(result, state == "Kerala" & month_num == 6)
kl_jun <- kl_jun[order(kl_jun$year), ]

plot(kl_jun$year, kl_jun$SPI,
     type = "h", col = ifelse(kl_jun$SPI >= 0, "steelblue", "firebrick"),
     lwd  = 2,
     xlab = "Year", ylab = "SPI",
     main = "Kerala – June SPI (1976–2026)")
abline(h = c(-1, 1), lty = 2, col = "grey50")
abline(h = 0, lwd = 1)
SPI for Kerala June (1976–2026)
SPI for Kerala June (1976–2026)

7 Minimum data requirements

Index Min non-NA observations per group Notes
RAI 4 n_extreme capped at floor(n/2)
MRAI 4 Uses full-series min/max
SRAI 4 Uses full-series SD; output clamped to [−3, +3]
SSPI 4 Uses 5th/95th percentiles; PZP-branch for dry months
SPI 4 Gamma MLE; falls back to z-score if < 4 non-zero
TEMP 4 z-score; returns 0 if SD = 0

Groups with fewer than 4 non-missing values return NA.


References

Van Rooy, M.P. (1965). A rainfall anomaly index independent of time and space. Notos, 14, 97–104.

Ekpetere, K.O. (2025). Development and evaluation of the modified and standardized rainfall anomaly indices for extreme variability analysis. Journal of Environmental Management, 375, 124160. https://doi.org/10.1016/j.jenvman.2025.124160

Raziei, T. (2021). Revisiting the Rainfall Anomaly Index to serve as a Simplified Standardized Precipitation Index. Journal of Hydrology, 602, 126761. https://doi.org/10.1016/j.jhydrol.2021.126761

Raziei, T. (2022). Improving the normalization procedure of the simplified standardized precipitation index (SSPI) using Box-Cox transformation. Stochastic Environmental Research and Risk Assessment, 37, 925–951. https://doi.org/10.1007/s00477-022-02317-9

McKee, T.B., Doesken, N.J., & Kleist, J. (1993). The relationship of drought frequency and duration to time scales. Proceedings of the 8th Conference on Applied Climatology, AMS, pp. 179–184.