What the index is
The Mosquito Weather Index reduces three weather variables to one
number in [0, 1]. It is the product of three response
functions, each of which returns a suitability multiplier for one
variable:
mwi_temperature(22)
#> [1] 1
mwi_humidity(70)
#> [1] 0.5454545
mwi_wind(10)
#> [1] 1
mwi_temperature(22) * mwi_humidity(70) * mwi_wind(10)
#> [1] 0.5454545
mwi(temperature = 22, humidity = 70, wind_speed = 10)
#> [1] 0.5454545The shapes are worth knowing, because they explain most of the index’s behaviour.
mwi_temperature() is a trapezoid, flat and maximal from
20 to 25 °C and zero outside 15–30 °C:
t <- seq(10, 35, by = 5)
data.frame(temperature = t, response = mwi_temperature(t))
#> temperature response
#> 1 10 0
#> 2 15 0
#> 3 20 1
#> 4 25 1
#> 5 30 0
#> 6 35 0mwi_humidity() ramps linearly from 40% to 95%, then
drops to zero as a step:
h <- c(35, 40, 60, 80, 95, 96)
data.frame(humidity = h, response = mwi_humidity(h))
#> humidity response
#> 1 35 0.0000000
#> 2 40 0.0000000
#> 3 60 0.3636364
#> 4 80 0.7272727
#> 5 95 1.0000000
#> 6 96 0.0000000That step at 95% is deliberate, not an artefact. It has a practical consequence: the index reaches 1 only when humidity is exactly 95%, so the top “very high activity” class is attained on rounded observational data but almost never on continuous data such as reanalysis output.
mwi_wind() is a switch rather than a gradient:
mwi_wind_threshold()
#> [1] 21.6
mwi_wind(c(21.5, 21.6, 21.7))
#> [1] 1 1 0Because the three responses are multiplied, any one of them hitting zero takes the whole index with it. Wind and humidity are therefore best thought of as gates, and temperature as the thing that grades the result.
Compute first, aggregate second
This is the part that most often goes wrong.
mwi() is non-linear, so it does not commute with
averaging. If you have hourly weather and you want a daily index, you
must evaluate the index hourly and then summarise the resulting index
values. Summarising the weather first and then indexing gives a
different answer.
The problem is worst with daily maxima, because the daily maximum of relative humidity and the daily maximum of temperature fall at opposite ends of the day — humidity peaks in the small hours, temperature in mid-afternoon. Combining them describes an hour that never occurred.
hours <- 0:23
temperature <- 18 + 8 * sin((hours - 6) / 24 * 2 * pi)
humidity <- 75 - 15 * sin((hours - 6) / 24 * 2 * pi)
wind_speed <- rep(5, 24)
# Note the phase difference: they peak twelve hours apart
c(warmest_hour = which.max(temperature) - 1,
most_humid_hour = which.max(humidity) - 1)
#> warmest_hour most_humid_hour
#> 12 0
# Right: index each hour, then reduce
mwi_summarise(temperature, humidity, wind_speed,
by = rep("2019-07-15", 24), statistic = "max")
#> by mwi n
#> 1 2019-07-15 0.5657766 24
# Wrong: reduce the weather, then index
mwi(max(temperature), max(humidity), max(wind_speed))
#> [1] 0.7272727mwi_summarise() enforces the correct order. Group with
by to get one row per day:
two_days <- rep(as.Date(c("2019-07-15", "2019-07-16")), each = 24)
mwi_summarise(rep(temperature, 2), rep(humidity, 2), rep(wind_speed, 2),
by = two_days)
#> by mwi n
#> 1 2019-07-15 0.5657766 24
#> 2 2019-07-16 0.5657766 24Maximum or mean?
Both are available through statistic. In the evaluation
study accompanying this package, the daily maximum consistently
predicted adult trap counts better than the daily mean, which is
consistent with catch responding to whether the day contained a window
of favourable conditions rather than to how favourable the day was on
average.
by <- rep("2019-07-15", 24)
rbind(
mwi_summarise(temperature, humidity, wind_speed, by, statistic = "max"),
mwi_summarise(temperature, humidity, wind_speed, by, statistic = "mean"),
mwi_summarise(temperature, humidity, wind_speed, by, statistic = "min")
)
#> by mwi n
#> 1 2019-07-15 0.5657766 24
#> 2 2019-07-15 0.2405692 24
#> 3 2019-07-15 0.0000000 24Activity classes
For communication the continuous index is reduced to five ordered classes:
x <- c(0, 0.15, 0.5, 0.85, 1)
data.frame(mwi = x, class = mwi_category(x))
#> mwi class
#> 1 0.00 No activity
#> 2 0.15 Low activity
#> 3 0.50 Moderate activity
#> 4 0.85 High activity
#> 5 1.00 Very high activityThe result is an ordered factor, so it can be compared and sorted:
mwi_category(0.85) > mwi_category(0.15)
#> [1] TRUEPass labels to translate them:
greek <- c("Καμία", "Χαμηλή", "Μέτρια", "Υψηλή", "Πολύ υψηλή")
mwi_category(c(0, 0.5, 1), labels = greek)
#> [1] Καμία Μέτρια Πολύ υψηλή
#> Levels: Καμία < Χαμηλή < Μέτρια < Υψηλή < Πολύ υψηλήFeeding the index from reanalysis data
Gridded products rarely give you the three variables in the form the index wants. ERA5-Land, for instance, distributes 2 m air temperature and 2 m dewpoint in Kelvin, and 10 m wind as eastward and northward components in m/s. The conversions are included:
t2m <- c(295.15, 299.15, 291.15)
d2m <- c(288.15, 289.15, 287.15)
u10 <- c(1.2, -3.4, 0.5)
v10 <- c(-0.8, 2.1, 1.1)
temperature <- celsius_from_kelvin(t2m)
dewpoint <- celsius_from_kelvin(d2m)
humidity <- relative_humidity_from_dewpoint(dewpoint, temperature)
wind_speed <- wind_speed_from_components(u10, v10)
data.frame(
temperature = round(temperature, 1),
humidity = round(humidity, 1),
wind_ms = round(wind_speed, 1),
mwi = round(mwi(temperature, humidity, wind_speed, units = "m/s"), 3)
)
#> temperature humidity wind_ms mwi
#> 1 22 64.5 1.4 0.446
#> 2 26 54.1 4.0 0.205
#> 3 18 77.5 1.2 0.409When wind is unavailable
Temperature and humidity loggers do not measure wind.
mwi_fhft() drops the wind term, which is equivalent to
assuming wind is always below the cut-off:
mwi_fhft(temperature = 22, humidity = 70)
#> [1] 0.5454545
mwi(temperature = 22, humidity = 70, wind_speed = 0)
#> [1] 0.5454545Treat this variant with care. The evaluation study found that substituting on-site logger readings for gridded reanalysis made the index a worse predictor of trap counts, partly because logger humidity ran systematically dry and the 40% threshold amplifies that bias.
Missing values
Every function propagates NA rather than guessing:
In mwi_summarise(), a group containing any missing index
value returns NA unless you ask otherwise:
gappy_temperature <- 18 + 8 * sin((hours - 6) / 24 * 2 * pi)
gappy_temperature[5] <- NA
gappy_humidity <- 75 - 15 * sin((hours - 6) / 24 * 2 * pi)
mwi_summarise(gappy_temperature, gappy_humidity, rep(5, 24), by)$mwi
#> [1] NA
mwi_summarise(gappy_temperature, gappy_humidity, rep(5, 24), by,
na.rm = TRUE)$mwi
#> [1] 0.5657766Note that arguments must be the same length or length 1. Partial recycling is an error rather than a silent surprise: