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.
| 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) | — |
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.80Compute 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 WarmClimateAnomaly imposes no fixed column names. Supply the actual column names from your file as arguments.
Assume the file has columns district, yr,
mon, precip_mm, tmax_deg,
tmin_deg.
For annual totals, group only by location:
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")Each index can also be called directly on a single numeric vector (one group at a time).
# 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"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.00kl_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"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"# 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 91kl_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)| 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.
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.