```
library(dplyr)
library(sf)
#Import Cambodia country border
<- st_read("data_cambodia/cambodia.gpkg", layer = "country", quiet = TRUE)
country #Import provincial administrative border of Cambodia
<- st_read("data_cambodia/cambodia.gpkg", layer = "education", quiet = TRUE)
education #Import district administrative border of Cambodia
<- st_read("data_cambodia/cambodia.gpkg", layer = "district", quiet = TRUE)
district
# Import locations of cases from an imaginary disease
<- st_read("data_cambodia/cambodia.gpkg", layer = "cases", quiet = TRUE)
cases <- subset(cases, Disease == "W fever") cases
```

# 6 Basic statistics for spatial analysis

This section aims at providing some basic statistical tools to study the spatial distribution of epidemiological data. If you wish to go further into spatial statistics applied to epidemiology and their limitations you can consult the tutorial “Spatial Epidemiology” from M. Kramer from which the statistical analysis of this section was adapted.

## 6.1 Import and visualize epidemiological data

In this section, we load data that reference the cases of an imaginary disease, the W fever, throughout Cambodia. Each point corresponds to the geo-localization of a case.

The first step of any statistical analysis always consists on visualizing the data to check they were correctly loaded and to observe general pattern of the cases.

```
# View the cases object
head(cases)
```

```
Simple feature collection with 6 features and 2 fields
Geometry type: MULTIPOINT
Dimension: XY
Bounding box: xmin: 255891 ymin: 1179092 xmax: 506647.4 ymax: 1467441
Projected CRS: WGS 84 / UTM zone 48N
id Disease geom
1 0 W fever MULTIPOINT ((280036.2 12841...
2 1 W fever MULTIPOINT ((451859.5 11790...
3 2 W fever MULTIPOINT ((255891 1467441))
4 5 W fever MULTIPOINT ((506647.4 12322...
5 6 W fever MULTIPOINT ((440668 1197958))
6 7 W fever MULTIPOINT ((481594.5 12714...
```

```
# Map the cases
library(mapsf)
mf_map(x = district, border = "white")
mf_map(x = country,lwd = 2, col = NA, add = TRUE)
mf_map(x = cases, lwd = .5, col = "#990000", pch = 20, add = TRUE)
mf_layout(title = "W Fever infections in Cambodia")
```

In epidemiology, the true meaning of point is very questionable. If it usually gives the location of an observation, we cannot precisely tell if this observation represents an event of interest (e.g., illness, death, …) or a person at risk (e.g., a participant that may or may not experience the disease). If you can consider that the population at risk is uniformly distributed in small area (within a city for example), this is likely not the case at a country scale. Considering a ratio of event compared to a population at risk is often more informative than just considering cases. Administrative divisions of countries appear as great areal units for cases aggregation since they make available data on population count and structures. In this study, we will use the district as the areal unit of the study.

```
# Aggregate cases over districts
$cases <- lengths(st_intersects(district, cases))
district
# Plot number of cases using proportional symbol
mf_map(x = district)
mf_map(
x = district,
var = "cases",
val_max = 50,
type = "prop",
col = "#990000",
leg_title = "Cases")
mf_layout(title = "Number of cases of W Fever")
```

The incidence (\(\frac{cases}{population}\)) expressed per 100,000 population is commonly use to represent cases distribution related to population density but other indicators exists. As example, the standardized incidence ratios (SIRs) represent the deviation of observed and expected number of cases and is expressed as \(SIR = \frac{Y_i}{E_i}\) with \(Y_i\), the observed number of cases and \(E_i\), the expected number of cases. In this study, we computed the expected number of cases in each district by assuming infections are homogeneously distributed across Cambodia, i.e., the incidence is the same in each district. The SIR therefore represents the deviation of incidence compared to the average incidence across Cambodia.

```
# Compute incidence in each district (per 100 000 population)
$incidence <- district$cases/district$T_POP * 100000
district
# Compute the global risk
<- sum(district$cases)/sum(district$T_POP)
rate
# Compute expected number of cases
$expected <- district$T_POP * rate
district
# Compute SIR
$SIR <- district$cases / district$expected district
```

```
par(mfrow = c(1, 2))
# Plot incidence
mf_map(x = district)
mf_map(x = district,
var = c("T_POP", "incidence"),
type = "prop_choro",
pal = "Reds",
inches = .1,
breaks = exp(mf_get_breaks(log(district$incidence+1), breaks = "pretty"))-1,
leg_title = c("Population", "Incidence \n(per 100 000)"))
mf_layout(title = "Incidence of W Fever")
# Plot SIRs
# create breaks and associated color palette
<- c(0,exp(mf_get_breaks(log(district$SIR), nbreaks = 8, breaks = "pretty")))
break_SIR <- c("#273871", "#3267AD", "#6496C8", "#9BBFDD", "#CDE3F0", "#FFCEBC", "#FF967E", "#F64D41", "#B90E36")
col_pal mf_map(x = district)
mf_map(x = district,
var = c("T_POP", "SIR"),
type = "prop_choro",
breaks = break_SIR,
pal = col_pal,
inches = .1,
#cex = 2,
leg_title = c("Population", "SIR"))
mf_layout(title = "Standardized Incidence Ratio of W Fever")
```

These maps illustrate the spatial heterogeneity of the cases. The incidence shows how the disease vary from one district to another while the SIR highlight districts that have:

higher risk than average (SIR > 1) when standardized for population

lower risk than average (SIR < 1) when standardized for population

average risk (SIR ~ 1) when standardized for population

## 6.2 Cluster analysis

### 6.2.1 General introduction

Why studying clusters in epidemiology? Cluster analysis help identifying unusual patterns that occurs during a given period of time. The underlying ultimate goal of such analysis is to explain the observation of such patterns. In epidemiology, we can distinguish two types of process that would explain heterogeneity in case distribution:

The

**1st order effects**are the spatial variations of cases distribution caused by underlying properties of environment or the population structure itself. In such process individual get infected independently from the rest of the population. Such process includes the infection through an environment at risk as, for example, air pollution, contaminated waters or soils and UV exposition. This effect assume that the observed pattern is caused by a difference in risk intensity.The

**2nd order effects**describes process of spread, contagion and diffusion of diseases caused by interactions between individuals. This includes transmission of infectious disease by proximity, but also the transmission of non-infectious disease, for example, with the diffusion of social norms within networks. This effect assume that the observed pattern is caused by correlations or co-variations.

No statistical methods could distinguish between these competing processes since their outcome results in similar pattern of points. The cluster analysis help describing the magnitude and the location of pattern but in no way could answer the question of why such patterns occurs. It is therefore a step that help detecting cluster for description and surveillance purpose and rising hypothesis on the underlying process that will lead further investigations.

Knowledge about the disease and its transmission process could orientate the choice of the methods of study. We presented in this brief tutorial two methods of cluster detection, the Moran’s I test that test for spatial independence (likely related to 2nd order effects) and the scan statistics that test for homogeneous distribution (likely related 1st order effects). It relies on epidemiologist to select the tools that best serve the studied question.

### 6.2.2 Test for spatial autocorrelation (Moran’s I test)

#### 6.2.2.1 The global Moran’s I test

A popular test for spatial autocorrelation is the Moran’s test. This test tells us whether nearby units tend to exhibit similar incidences. It ranges from -1 to +1. A value of -1 denote that units with low rates are located near other units with high rates, while a Moran’s I value of +1 indicates a concentration of spatial units exhibiting similar rates.

We will compute the Moran’s statistics using `spdep`

(R. Bivand et al. 2015) and `Dcluster`

(Gómez-Rubio et al. 2015) packages. `spdep`

package provides a collection of functions to analyze spatial correlations of polygons and works with sp objects. In this example, we use `poly2nb()`

and `nb2listw()`

. These functions respectively detect the neighboring polygons and assign weight corresponding to \(1/\#\ of\ neighbors\). `Dcluster`

package provides a set of functions for the detection of spatial clusters of disease using count data.

```
#install.packages("spdep")
#install.packages("DCluster")
library(spdep) # Functions for creating spatial weight, spatial analysis
library(DCluster) # Package with functions for spatial cluster analysis
set.seed(345) # remove random sampling for reproducibility
<- poly2nb(district) # Neighbors according to queen case
queen_nb <- nb2listw(queen_nb, style = 'W') # row-standardized weights
q_listw
# Moran's I test
<- moranI.test(cases ~ offset(log(expected)),
m_test data = district,
model = 'poisson',
R = 499,
listw = q_listw,
n = length(district$cases), # number of regions
S0 = Szero(q_listw)) # Global sum of weights
print(m_test)
```

```
Moran's I test of spatial autocorrelation
Type of boots.: parametric
Model used when sampling: Poisson
Number of simulations: 499
Statistic: 0.1566449
p-value : 0.006
```

`plot(m_test)`

The Moran’s statistics is here \(I =\) 0.16. When comparing its value to the H0 distribution (built under 499 simulations), the probability of observing such a I value under the null hypothesis, i.e. the distribution of cases is spatially independent, is \(p_{value} =\) 0.006. We therefore reject H0 with error risk of \(\alpha = 5\%\). The distribution of cases is therefore autocorrelated across districts in Cambodia.

#### 6.2.2.2 The Local Moran’s I LISA test

The global Moran’s test provides us a global statistical value informing whether autocorrelation occurs over the territory but does not inform on where does these correlations occurs, i.e., what is the locations of the clusters. To identify such cluster, we can decompose the Moran’s I statistic to extract local information of the level of correlation of each district and its neighbors. This is called the Local Moran’s I LISA statistic. Because the Local Moran’s I LISA statistic test each district for autocorrelation independently, concern is raised about multiple testing limitations that increase the Type I error (\(\alpha\)) of the statistical tests. The use of local test should therefore be study in light of explore and describes clusters once the global test has detected autocorrelation.

The `localmoran()`

function from the package `spdep`

treats the variable of interest as if it was normally distributed. In some cases, this assumption could be reasonable for incidence rate, especially when the areal units of analysis have sufficiently large population count suggesting that the values have similar level of variances. Unfortunately, the local Moran’s test has not been implemented for Poisson distribution (population not large enough in some districts) in `spdep`

package. However, Bivand *et al.* (R. S. Bivand et al. 2008) provided some code to manually perform the analysis using Poisson distribution and this code was further implemented in the course “Spatial Epidemiology”.

```
# Step 1 - Create the standardized deviation of observed from expected
<- (district$cases - district$expected) / sqrt(district$expected)
sd_lm
# Step 2 - Create a spatially lagged version of standardized deviation of neighbors
<- lag.listw(q_listw, sd_lm)
wsd_lm
# Step 3 - the local Moran's I is the product of step 1 and step 2
$I_lm <- sd_lm * wsd_lm
district
# Step 4 - setup parameters for simulation of the null distribution
# Specify number of simulations to run
<- 499
nsim
# Specify dimensions of result based on number of regions
<- length(district$expected)
N
# Create a matrix of zeros to hold results, with a row for each county, and a column for each simulation
<- matrix(0, ncol = nsim, nrow = N)
sims
# Step 5 - Start a for-loop to iterate over simulation columns
for(i in 1:nsim){
<- rpois(N, lambda = district$expected) # generate a random event count, given expected
y <- (y - district$expected) / sqrt(district$expected) # standardized local measure
sd_lmi <- lag.listw(q_listw, sd_lmi) # standardized spatially lagged measure
wsd_lmi <- sd_lmi * wsd_lmi # this is the I(i) statistic under this iteration of null
sims[, i]
}
# Step 6 - For each county, test where the observed value ranks with respect to the null simulations
<- apply(cbind(district$I_lm, sims), 1, function(x) rank(x)[1])
xrank
# Step 7 - Calculate the difference between observed rank and total possible (nsim)
<- nsim - xrank
diff <- ifelse(diff > 0, diff, 0)
diff
# Step 8 - Assuming a uniform distribution of ranks, calculate p-value for observed
# given the null distribution generate from simulations
$pval_lm <- punif((diff + 1) / (nsim + 1)) district
```

Briefly, the process consist on 1) computing the I statistics for the observed data, 2) estimating the null distribution of the I statistics by performing random sampling into a poisson distribution and 3) comparing the observed I statistic with the null distribution to determine the probability to observe such value if the number of cases were spatially independent. For each district, we obtain a p-value based on the comparison of the observed value and the null distribution.

A conventional way of plotting these results is to classify the districts into 5 classes based on local Moran’s I output. The classification of cluster that are significantly autocorrelated to their neighbors is performed based on a comparison of the scaled incidence in the district compared to the scaled weighted averaged incidence of it neighboring districts (computed with `lag.listw()`

):

Districts that have higher-than-average rates in both index regions and their neighbors and showing statistically significant positive values for the local \(I_i\) statistic are defined as

**High-High**(hotspot of the disease)Districts that have lower-than-average rates in both index regions and their neighbors and showing statistically significant positive values for the local \(I_i\) statistic are defined as

**Low-Low**(cold spot of the disease).Districts that have higher-than-average rates in the index regions and lower-than-average rates in their neighbors, and showing statistically significant negative values for the local \(I_i\) statistic are defined as

**High-Low**(outlier with high incidence in an area with low incidence).Districts that have lower-than-average rates in the index regions and higher-than-average rates in their neighbors, and showing statistically significant negative values for the local \(I_i\) statistic are defined as

**Low-High**(outlier of low incidence in area with high incidence).Districts with non-significant values for the \(I_i\) statistic are defined as

**Non-significant**.

```
# create lagged local raw_rate - in other words the average of the queen neighbors value
# values are scaled (centered and reduced) to be compared to average
$lag_std <- scale(lag.listw(q_listw, var = district$incidence))
district$incidence_std <- scale(district$incidence)
district
# extract pvalues
# district$lm_pv <- lm_test[,5]
# Classify local moran's outputs
$lm_class <- NA
district$lm_class[district$incidence_std >=0 & district$lag_std >=0] <- 'High-High'
district$lm_class[district$incidence_std <=0 & district$lag_std <=0] <- 'Low-Low'
district$lm_class[district$incidence_std <=0 & district$lag_std >=0] <- 'Low-High'
district$lm_class[district$incidence_std >=0 & district$lag_std <=0] <- 'High-Low'
district$lm_class[district$pval_lm >= 0.05] <- 'Non-significant'
district
$lm_class <- factor(district$lm_class, levels=c("High-High", "Low-Low", "High-Low", "Low-High", "Non-significant") )
district
# create map
mf_map(x = district,
var = "lm_class",
type = "typo",
cex = 2,
col_na = "white",
#val_order = c("High-High", "Low-Low", "High-Low", "Low-High", "Non-significant") ,
pal = c("#6D0026" , "blue", "white") , # "#FF755F","#7FABD3" ,
leg_title = "Clusters")
mf_layout(title = "Cluster using Local Moran's I statistic")
```

### 6.2.3 Spatial scan statistics

While Moran’s indices focus on testing for autocorrelation between neighboring polygons (under the null assumption of spatial independence), the spatial scan statistic aims at identifying an abnormal higher risk in a given region compared to the risk outside of this region (under the null assumption of homogeneous distribution). The conception of a cluster is therefore different between the two methods.

The function `kulldorff`

from the package `SpatialEpi`

(Kim and Wakefield 2010) is a simple tool to implement spatial-only scan statistics.

Briefly, the kulldorff scan statistics scan the area for clusters using several steps:

It create a circular window of observation by defining a single location and an associated radius of the windows varying from 0 to a large number that depends on population distribution (largest radius could include 50% of the population).

It aggregates the count of events and the population at risk (or an expected count of events) inside and outside the window of observation.

Finally, it computes the likelihood ratio and test whether the risk is equal inside versus outside the windows (H0) or greater inside the observed window (H1). The H0 distribution is estimated by simulating the distribution of counts under the null hypothesis (homogeneous risk).

These 3 steps are repeated for each location and each possible windows-radii.

While we test the significance of a large number of observation windows, one can raise concern about multiple testing and Type I error. This approach however suggest that we are not interest in a set of signifiant cluster but only in a most-likely cluster. This *a priori* restriction eliminate concern for multpile comparison since the test is simplified to a statistically significance of one single most-likely cluster.

Because we tested all-possible locations and window-radius, we can also choose to look at secondary clusters. In this case, you should keep in mind that increasing the number of secondary cluster you select, increases the risk for Type I error.

```
#install.packages("SpatialEpi")
library("SpatialEpi")
```

The use of R spatial object is not implements in `kulldorff()`

function. It uses instead matrix of xy coordinates that represents the centroids of the districts. A given district is included into the observed circular window if its centroids fall into the circle.

```
<- st_centroid(district) %>%
district_xy st_coordinates()
head(district_xy)
```

```
X Y
1 330823.3 1464560
2 749758.3 1541787
3 468384.0 1277007
4 494548.2 1215261
5 459644.2 1194615
6 360528.3 1516339
```

We can then call kulldorff function (you are strongly encouraged to call `?kulldorff`

to properly call the function). The `alpha.level`

threshold filter for the secondary clusters that will be retained. The most-likely cluster will be saved whatever its significance.

```
<- kulldorff(district_xy,
kd_Wfever cases = district$cases,
population = district$T_POP,
expected.cases = district$expected,
pop.upper.bound = 0.5, # include maximum 50% of the population in a windows
n.simulations = 499,
alpha.level = 0.2)
```

The function plot the histogram of the distribution of log-likelihood ratio simulated under the null hypothesis that is estimated based on Monte Carlo simulations. The observed value of the most significant cluster identified from all possible scans is compared to the distribution to determine significance. All outputs are saved into an R object, here called `kd_Wfever`

. Unfortunately, the package did not develop any summary and visualization of the results but we can explore the output object.

`names(kd_Wfever)`

```
[1] "most.likely.cluster" "secondary.clusters" "type"
[4] "log.lkhd" "simulated.log.lkhd"
```

First, we can focus on the most likely cluster and explore its characteristics.

```
# We can see which districts (r number) belong to this cluster
$most.likely.cluster$location.IDs.included kd_Wfever
```

` [1] 48 93 66 180 133 29 194 118 50 144 31 141 3 117 22 43 142`

```
# standardized incidence ratio
$most.likely.cluster$SMR kd_Wfever
```

`[1] 2.303106`

```
# number of observed and expected cases in this cluster
$most.likely.cluster$number.of.cases kd_Wfever
```

`[1] 122`

`$most.likely.cluster$expected.cases kd_Wfever`

`[1] 52.97195`

17 districts belong to the cluster and its number of cases is 2.3 times higher than the expected number of cases.

Similarly, we could study the secondary clusters. Results are saved in a list.

```
# We can see which districts (r number) belong to this cluster
length(kd_Wfever$secondary.clusters)
```

`[1] 1`

```
# retrieve data for all secondary clusters into a table
<- data.frame(SMR = sapply(kd_Wfever$secondary.clusters, '[[', 5),
df_secondary_clusters number.of.cases = sapply(kd_Wfever$secondary.clusters, '[[', 3),
expected.cases = sapply(kd_Wfever$secondary.clusters, '[[', 4),
p.value = sapply(kd_Wfever$secondary.clusters, '[[', 8))
print(df_secondary_clusters)
```

```
SMR number.of.cases expected.cases p.value
1 3.767698 16 4.246625 0.008
```

We only have one secondary cluster composed of one district.

```
# create empty column to store cluster informations
$k_cluster <- NA
district
# save cluster information from kulldorff outputs
$k_cluster[kd_Wfever$most.likely.cluster$location.IDs.included] <- 'Most likely cluster'
district
for(i in 1:length(kd_Wfever$secondary.clusters)){
$k_cluster[kd_Wfever$secondary.clusters[[i]]$location.IDs.included] <- paste(
district'Secondary cluster', i, sep = '')
}
#district$k_cluster[is.na(district$k_cluster)] <- "No cluster"
# create map
mf_map(x = district,
var = "k_cluster",
type = "typo",
cex = 2,
col_na = "white",
pal = mf_get_pal(palette = "Reds", n = 3)[1:2],
leg_title = "Clusters")
mf_layout(title = "Cluster using kulldorf scan statistic")
```

## 6.3 Conclusion

```
par(mfrow = c(1, 2))
# create map
mf_map(x = district,
var = "lm_class",
type = "typo",
cex = 2,
col_na = "white",
pal = c("#6D0026" , "blue", "white") , # "#FF755F","#7FABD3" ,
leg_title = "Clusters")
mf_layout(title = "Cluster using Local Moran's I statistic")
# create map
mf_map(x = district,
var = "k_cluster",
type = "typo",
cex = 2,
col_na = "white",
pal = mf_get_pal(palette = "Reds", n = 3)[1:2],
leg_title = "Clusters")
mf_layout(title = "Cluster using kulldorf scan statistic")
```

Both methods identified significant clusters. The two methods could identify a cluster around Phnom Penh after standardization for population counts. However, the identified clusters does not rely on the same assumption. While the Moran’s test wonder whether their is any autocorrelation between clusters (i.e. second order effects of infection), the Kulldorff scan statistics wonder whether their is any heterogeneity in the case distribution. None of these test can inform on the infection processes (first or second order) for the studied disease and previous knowledge on the disease will help selecting the most accurate test.