Skip to contents

Reviews each district-year in a spi_compare_npafp() result using the five STEPS components (strength, timeliness, extent, persistence, stool adequacy) and one of the field guide's three review labels (Priority for review, Monitor, No SPI indication). STEPS is applied only to districts with an SPI below spi_cut (1 by default, as in the field guide).

SPI compares reported NPAFP cases with expected counts. STEPS adds specimen transport and stool adequacy, and checks whether a low SPI also appears elsewhere in the area or in the previous year. The five components and what each asks:

  • S: Strength – how large is the shortfall, and how uncertain is the estimate? Read from the SPI value and whether its 90% credible interval lies entirely below 1.

  • T: Timeliness – are specimens reaching the laboratory within 3 days? Needs process.

  • E: Extent – is a low SPI more common in the same region (such as a province or state) than nationally? Needs an extent_col in the comparison.

  • P: Persistence – was the SPI also below the cut in the previous year?

  • S: Stool adequacy – are stool specimens adequate? Needs process.

The five STEPS components are reported separately. The review label depends on strength, extent, and persistence. Timeliness and stool adequacy describe specimen handling and do not change the label. These labels are not validated measures of surveillance adequacy or automatic recommendations for field action.

Trend, neighbouring-district context, seasonal patterns, and poliovirus detections through AFP or environmental surveillance are reported separately as supporting context when inputs are supplied. They do not change the generated review label.

Usage

spi_field_guide(
  comparison,
  process = NULL,
  extent_col = NULL,
  process_target = 80,
  process_min_cases = 5L,
  adjacency = NULL,
  spi_month = NULL,
  detections = NULL,
  detection_col = NULL,
  es = NULL,
  es_col = NULL,
  serotype_col = NULL,
  detection_serotypes = NULL,
  read_year = NULL,
  spi_cut = NULL,
  spi_rule = NULL,
  traj_window = 5L,
  traj_tol = 0.01,
  traj_alpha = NULL,
  noise_alpha = NULL,
  id_col = NULL,
  verbose = TRUE
)

Arguments

comparison

A spi_compare_npafp() result (class spi_compare_npafp). Supplies SPI and NPAFP results for every district-year.

process

Optional district-year tibble of AFP process counts, keyed by the district id column and year, with columns n_cases (AFP cases), n_adequate (cases with adequate stool specimens), n_transport (cases with both the second stool collection and laboratory receipt dates, in order) and n_transport_timely (of those, received by the laboratory within 3 days of the second stool collection). Enables timeliness (T) and stool adequacy (S). District-years missing from process count as zero cases. Default: NULL.

extent_col

Name of the admin-1 column in comparison$district_year that defines the area for extent (E). Default: NULL ("adm1_name" when present, as it is when spi_compare_npafp() was given boundaries).

process_target

Numeric target, in percent, for timeliness and stool adequacy. Default: 80.

process_min_cases

Integer. Fewest cases for timeliness or stool adequacy to be assessed. Default: 5.

adjacency

Optional spatial neighbour object from spi_adjacency() (class spi_nb). Enables the neighbour contrast, reported as context. Default: NULL.

spi_month

Optional spi_index() result at district_month level. Enables assessment of seasonal reporting patterns, reported as context. Default: NULL.

detections

Optional tibble of case-based (AFP) poliovirus detections with the district id column and year. Reported as context. Rows are detections; pass detection_col to filter on a 0/1 flag column. Default: NULL.

detection_col

Optional name of a logical / 0-1 column in detections; only TRUE or 1 rows count as detections. Default: NULL (every row counts).

es

Optional tibble of environmental-surveillance (ES) detections with the district id column and year. Reported as context. Rows are positives; pass es_col to filter on a count / flag column. Default: NULL.

es_col

Optional name of a count / logical column in es; only rows with a positive count (or TRUE) count as detections. Default: NULL (every row counts).

serotype_col

Optional name of a serotype column in detections and / or es, e.g. holding "cVDPV2", "WPV1". When given, the distinct serotypes seen up to each year are recorded as orphan_serotypes / es_serotypes, so the report can name the serotypes found. Without it those columns are NA, which downstream reads as "not recorded" rather than "none found". Default: NULL.

detection_serotypes

Optional character vector of the serotypes that count as a detection, matched against serotype_col, e.g. c("WPV1", "cVDPV1", "cVDPV2", "cVDPV3"). Rows outside the set are dropped before the detection years, flags and serotypes are computed. Requires serotype_col. Default: NULL (whatever the input contains counts).

read_year

Integer year shown by print() and by default in tables and worked examples. Default: NULL (latest year present).

spi_cut

Numeric SPI cutoff below which STEPS is applied. Lower values such as 0.80, 0.60, or 0.40 focus review on larger reporting shortfalls. The same cutoff is used for persistence and extent; the credible-interval check still compares with 1. A district above a lower cutoff may still need review based on other surveillance indicators. Default: NULL, which is 1 for the model-based SPI and 0.5 for the simple SPI.

spi_rule

Character, "median" or "interval". "median" labels every district-year with an SPI below spi_cut. "interval" also requires the 90% upper bound (spi_q95) to be below 1, so a district-year whose interval includes 1 is labelled No SPI indication. Extent and persistence still use the median. Default: NULL (the spi_rule given to spi_compare_npafp()).

traj_window

Integer. Trend regression window in years, ending at each year (context). Default: 5.

traj_tol

Numeric tolerance for the annual trend slope. A slope whose absolute value is below this tolerance is labelled "flat". Default: 0.01.

traj_alpha

Optional numeric significance level for the trend. When set, a trajectory is only "falling" or "rising" if its OLS slope differs from zero at this two-sided level (needing at least three points in the window); otherwise it is "flat". Default: NULL (slope sign and tolerance only).

noise_alpha

Optional numeric threshold for a sampling-noise check on strength (S). The SPI credible interval is posterior uncertainty in the expected count with the observed count held fixed, so it says nothing about sampling variation in the count itself. At observed = 0 the ratio is identically zero in every draw, the interval collapses to (0, 0), and cri_excludes_1 is TRUE whatever the expected count. When noise_alpha is set, gate_pass additionally requires the Poisson reference tail \(P(X \le observed \mid \lambda = expected)\) to be at or below it. cri_excludes_1 keeps its own meaning either way. The tail is always reported (noise_tail, noise_plausible). Default: NULL (no check). A value such as 0.05 adds this check; it is not a validated field-action threshold.

id_col

Character district id column. Default: NULL (take comparison$id_col).

verbose

Logical. Progress and missing-input messages via cli. Default: TRUE.

Value

An object of class spi_field_guide. A list with:

district_year

Tibble, one row per district-year, carrying the inputs plus every component: strength (spi_below, cri_excludes_1, gate_pass, noise_tail, noise_plausible), timeliness (n_transport, n_transport_timely, pct_transport_timely, transport_assessed, timeliness_concern), extent (extent_area, extent_others, extent_others_below, extent_pct, national_pct, extent_concern), persistence (spi_previous, persistence_concern), stool adequacy (n_cases, n_adequate, pct_adequate, adequacy_assessed, adequacy_concern), the context signals (s1_discordance, longest_run_below, trailing_run_below, trajectory, neighbour_spi, neighbour_discordant, neighbourhood_shortfall, seasonal, orphan_years, genomic_orphan, orphan_serotypes, es_years, es_detected, es_serotypes), and verdict (factor).

focal

The read_year slice of district_year.

reference

The STEPS reference tibble (what each component asks and how to interpret it).

read_year, thresholds, params, signals_active, id_col, call

Metadata: focal year, spi_cut / npafp_target / spi_rule, the tuning parameters, which optional components and context signals were computed, the id column, and the matched call.

Details

Each district-year receives one of three review labels, using the field guide's descriptions:

  • Priority for review – the SPI is below spi_cut, its 90% credible interval lies entirely below 1 (and, when noise_alpha is set, sampling noise alone is unlikely to produce the shortfall), and extent or persistence also raises concern. The label identifies districts to consider for closer review.

  • Monitor – the SPI is below spi_cut, but the interval includes 1, the optional sampling-noise check fails, or neither extent nor persistence supports it. Uncertainty or limited supporting information favours reassessment as new data become available.

  • No SPI indication – the SPI is at or above spi_cut. There is no SPI indication for additional review; routine surveillance and review based on other indicators continue.

With spi_rule = "interval", a district-year whose 90% credible interval includes 1 is labelled No SPI indication rather than Monitor, so only shortfalls whose interval lies entirely below 1 are labelled.

Simple SPI. A comparison built from spi_simple() has no credible interval. The strength component uses the SPI value alone. The default cutoff is spi_cut = 0.5, which focuses review on larger relative shortfalls. This cutoff does not measure uncertainty. A district-year with an SPI below 0.5 and support from extent or persistence is labelled Priority for review; one below 0.5 without that support is labelled Monitor. spi_rule = "interval" is not available.

Extent compares the share of the other districts in the same extent_col area with an SPI below the cut against the national share in the same year, and raises concern when the area share is higher. Timeliness and stool adequacy are assessed on at least process_min_cases cases against a target of process_target percent; below that they are NA (not assessed).

Without process or an extent_col the corresponding component is NA. signals_active records which components and additional findings could be calculated.

See also

spi_field_guide_table() to render it, spi_field_guide_help() to learn to read it, spi_compare_npafp() for the input, and the one-page infographic at system.file("field-guide", "npafp_spi_steps_infographic.html", package = "spi").

Examples

# a precomputed field guide on the synthetic bundle ships with the package
fg <- synth_field_guide
print(fg)
#> 
#> ── SPI field guide ─────────────────────────────────────────────────────────────
#> Review year: 2024 | STEPS applied below SPI 1 (median rule) | priority for
#> review: 90% CrI below 1, with extent or persistence
#> Available components and additional findings: "timeliness", "extent",
#> "adequacy", "surroundings", "seasonal", "detect_afp", and "detect_es"
#> 
#> ── Review labels for 2024 ──
#> 
#> # A tibble: 3 × 3
#>   verdict                 n   pct
#>   <chr>               <int> <dbl>
#> 1 Priority for review    60  25.4
#> 2 Monitor                71  30.1
#> 3 No SPI indication     105  44.5
#> ── Districts with priority for review (top 10 by size of shortfall) ──
#> 
#> # A tibble: 60 × 11
#>    district   obs   exp   spi cri       npafp extent persist transport adequacy
#>    <chr>    <int> <dbl> <dbl> <chr>     <dbl> <lgl>  <lgl>       <dbl>    <dbl>
#>  1 Khandor      0   2.4  0    0.00-0.00   0   TRUE   TRUE           NA       NA
#>  2 Arddor       0   1.7  0    0.00-0.00   0   TRUE   TRUE           NA       NA
#>  3 Ardor        0   1.2  0    0.00-0.00   0   TRUE   TRUE           NA       NA
#>  4 Doloth       0   1.1  0    0.00-0.00   0   TRUE   TRUE           NA       NA
#>  5 Raenan       0   1    0    0.00-0.00   0   TRUE   TRUE           NA       NA
#>  6 Chakis       0   1.9  0    0.00-0.00   0   TRUE   TRUE           NA       NA
#>  7 Kiroth       0   1.5  0    0.00-0.00   0   TRUE   FALSE          NA       NA
#>  8 Vashoth      0   2.3  0    0.00-0.00   0   TRUE   TRUE           NA       NA
#>  9 Suzil        1   8.1  0.13 0.10-0.17   1.6 FALSE  TRUE          100      100
#> 10 Nentha       2  14.5  0.14 0.11-0.18   2   TRUE   FALSE         100      100
#> # ℹ 50 more rows
#> # ℹ 1 more variable: verdict <chr>
#> ℹ See `spi_field_guide_help()` for worked examples, `spi_field_guide_table()` to render it.
head(as_tibble(fg))
#> # A tibble: 6 × 60
#>   adm0_name adm1_name adm2_name adm2_guid           year observed expected_total
#>   <chr>     <chr>     <chr>     <chr>              <dbl>    <int>          <dbl>
#> 1 Harad     Gartha    Khandor   {01325AA0-BEA1-66…  2015        1           1.55
#> 2 Harad     Gartha    Khandor   {01325AA0-BEA1-66…  2016        2           1.87
#> 3 Harad     Gartha    Khandor   {01325AA0-BEA1-66…  2017        0           1.90
#> 4 Harad     Gartha    Khandor   {01325AA0-BEA1-66…  2018        2           2.11
#> 5 Harad     Gartha    Khandor   {01325AA0-BEA1-66…  2019        5           2.49
#> 6 Harad     Gartha    Khandor   {01325AA0-BEA1-66…  2020        2           1.39
#> # ℹ 53 more variables: spi_median <dbl>, spi_q05 <dbl>, spi_q95 <dbl>,
#> #   count_annual <int>, pop_u15 <dbl>, npafp_rate <dbl>, npafp_adequate <lgl>,
#> #   spi_below_threshold <lgl>, spi_pass <lgl>, category <fct>,
#> #   conventional_pass <lgl>, spi_below <lgl>, noise_tail <dbl>,
#> #   noise_plausible <lgl>, cri_excludes_1 <lgl>, gate_pass <lgl>,
#> #   s1_discordance <lgl>, spi_previous <dbl>, persistence_concern <lgl>,
#> #   national_pct <dbl>, extent_others <int>, extent_others_below <int>, …
if (FALSE) { # \dontrun{
# build one from scratch (needs INLA for the upstream fit)
adj  <- spi_adjacency(synth_surveillance$boundaries, id_col = "adm2_guid")
cy   <- spi_index(
  synth_surveillance$cases, synth_surveillance$population, adj,
  first_assessment = 2018, id_col = "adm2_guid"
)
cm   <- spi_index(
  synth_surveillance$cases, synth_surveillance$population, adj,
  first_assessment = 2018, level = "district_month", id_col = "adm2_guid"
)
comparison <- spi_compare_npafp(
  cy, synth_surveillance$cases, synth_surveillance$population,
  boundaries = synth_surveillance$boundaries
)
fg <- spi_field_guide(
  comparison,
  process = synth_surveillance$afp_process,
  adjacency = adj,
  spi_month = cm,
  detections = synth_surveillance$virus_outcome |>
    dplyr::filter(any_virus == 1) |>
    dplyr::select(adm2_guid, year),
  es = synth_surveillance$es_district_year,
  es_col = "n_positive"
)
} # }