5 Maps: coordinates, distances and neighbours

Spatial models need one of two things from your coordinates: distances between languages, or a list of who neighbours whom. Build both, and learn what each choice assumes.

Fitted with sf, spdepModel fitting about two minutes; the polygon section downloads 140 MB oncePackages sf spdep rnaturalearth ggplot2Download the R script

5.1 Set up

library(sf)
library(spdep)
library(rnaturalearth)
library(ggplot2)

base <- "https://rehan-muh.github.io/tutorials/data/"
d    <- read.csv(paste0(base, "languages.csv"))
d$elev_km <- d$elevation / 1000

5.2 Languages as points

The data give each language one longitude and one latitude. st_as_sf() turns those two columns into a spatial object, and crs = 4326 says they are ordinary degrees on the globe.

pts <- st_as_sf(d, coords = c("lon", "lat"), crs = 4326, remove = FALSE)
pts[1:3, c("name", "family", "geometry")]
## Simple feature collection with 3 features and 2 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: 36.7 ymin: 7.39 xmax: 39.3 ymax: 44
## Geodetic CRS:  WGS 84
##              name       family          geometry
## 1 West Circassian Abkhaz-Adyge   POINT (39.3 44)
## 2            Beja Afro-Asiatic POINT (36.7 17.2)
## 3  Alaba-K'abeena Afro-Asiatic POINT (38.2 7.39)

A point is a simplification. A language is spoken over an area, sometimes a discontinuous one, and Glottolog’s point is roughly its centre. For a world sample that is good enough. For a study of one region, speaker-area polygons are better if you can get them, and the last section of this chapter shows where they plug in.

5.3 Distances

Distance on a globe is measured along great circles. st_distance() does it for sf points and sp::spDists() returns a plain matrix in kilometres, which is the more convenient form.

coords <- as.matrix(d[, c("lon", "lat")])
km <- sp::spDists(coords, longlat = TRUE)
dimnames(km) <- list(d$name, d$name)

show <- c("Tigrinya", "Harari", "Zulu", "Central Aymara")
round(km[show, show])
##                Tigrinya Harari Zulu Central Aymara
## Tigrinya              0    753 4984          12318
## Harari              753      0 4417          12500
## Zulu               4984   4417    0           9929
## Central Aymara    12318  12500 9929              0

Next, find how far each language is from its nearest neighbour in the sample.

nearest <- apply(km + diag(Inf, nrow(km)), 1, min)
round(quantile(nearest, c(0, 0.25, 0.5, 0.75, 1)))
##   0%  25%  50%  75% 100% 
##    5   75  138  275 1508

The sample is very uneven: half the languages have another within about 140 km, and the most isolated is 1,500 km from anything. Every spatial model has to cope with that, and they cope in different ways.

5.4 Flat maps and their cost

Some models (the Gaussian process of Chapter 6) want coordinates in which ordinary straight-line distance means something. Degrees will not do: a degree of longitude is 111 km at the equator and 56 km at 60° north. The fix is to project. Equal Earth is a reasonable projection for the whole world.

xy <- st_coordinates(st_transform(pts, "+proj=eqearth")) / 1000   # km
flat_km <- as.matrix(dist(xy))

close <- km < 3000 & upper.tri(km)
round(quantile(flat_km[close] / km[close], c(0.05, 0.5, 0.95)), 2)
##   5%  50%  95% 
## 0.86 0.97 1.17

For pairs of languages within 3,000 km, the distance on the flat map is mostly within about 15% of the true one. The error comes mostly at high latitudes and across the cut at the 180th meridian, where Alaska and Chukotka end up on opposite edges.

Methods that work on the sphere need no projection. They take either degrees directly or the position of each language as a point on a unit sphere:

to_sphere <- function(lon, lat) {
  rad <- pi / 180
  cbind(x = cos(lat * rad) * cos(lon * rad),
        y = cos(lat * rad) * sin(lon * rad),
        z = sin(lat * rad))
}
head(round(to_sphere(d$lon, d$lat), 3), 3)
##          x     y     z
## [1,] 0.556 0.456 0.695
## [2,] 0.766 0.570 0.296
## [3,] 0.780 0.613 0.129

5.5 Neighbours

The other family of models (Chapter 7) never looks at a distance. It needs a graph that says which languages count as neighbours. For points there is no natural answer, so you choose a rule. Three common ones:

# 1. the k nearest languages, made mutual
knn <- make.sym.nb(knn2nb(knearneigh(coords, k = 5, longlat = TRUE)))

# 2. every language within a fixed distance
band <- dnearneigh(coords, 0, 500, longlat = TRUE)

# 3. natural neighbours: a triangulation of the points
tri <- tri2nb(coords)

compare <- function(nb) {
  c(links_per_language = round(mean(card(nb)), 1),
    without_neighbours = sum(card(nb) == 0),
    separate_pieces    = n.comp.nb(nb)$nc)
}
rbind("5 nearest" = compare(knn), "within 500 km" = compare(band),
      "triangulation" = compare(tri))
##               links_per_language without_neighbours separate_pieces
## 5 nearest                    6.4                  0               1
## within 500 km                9.1                 44              85
## triangulation                6.0                  0               1

Read the three columns as three things that can go wrong.

  • Nearest neighbours give every language about the same number of links, whether its neighbours are 20 km away or 2,000. The graph is in one piece.
  • A distance band is the most natural idea and the most troublesome in a world sample. Languages in dense regions get dozens of links, isolated ones get none, and the graph falls into many pieces. The models of Chapter 7 need one piece.
  • A triangulation always gives one piece and needs no tuning. It also draws long links across oceans, and because it works on raw degrees it treats the map as flat.
world <- ne_countries(scale = "small", returnclass = "sf")
wrap  <- function(nb) st_wrap_dateline(nb2lines(nb, coords = st_geometry(pts)),
                                       options = c("WRAPDATELINE=YES",
                                                   "DATELINEOFFSET=60"))
ggplot() +
  geom_sf(data = world, fill = "grey94", colour = NA) +
  geom_sf(data = wrap(knn), colour = "#2A78D6", linewidth = 0.25) +
  geom_sf(data = wrap(band), colour = "#EB6834", linewidth = 0.2, alpha = 0.6) +
  geom_sf(data = pts, size = 0.4) +
  coord_sf(crs = "+proj=eqearth")
Two neighbour graphs on the same languages: the five nearest (blue) and everything within 500 km (orange). The distance band leaves much of the world unconnected.

Figure 5.1: Two neighbour graphs on the same languages: the five nearest (blue) and everything within 500 km (orange). The distance band leaves much of the world unconnected.

Watch out. No rule knows any linguistics. A link across the Sahara counts the same as a link between neighbouring villages, and two languages on either side of a mountain range are neighbours if they are close on the map. You can edit a graph by hand with edit.nb(), or build it from a table of known contact. Whatever you do, draw it, and try the analysis with a second graph.

5.6 From a graph to weights

Models use the graph as a matrix. There are two conventions and they are not interchangeable.

W_binary <- nb2mat(knn, style = "B")   # 1 for a neighbour, 0 otherwise
W_rows   <- nb2mat(knn, style = "W")   # each row divided by its row sum

W_binary[1:4, 1:4]
##   1 2 3 4
## 1 0 0 0 0
## 2 0 0 0 0
## 3 0 0 0 1
## 4 0 0 1 0
round(rowSums(W_rows)[1:4], 2)
## 1 2 3 4 
## 1 1 1 1

The binary matrix is what the conditional autoregressive models of Chapter 7 take. The row-standardised one, usually stored as a list with nb2listw(), is what Moran’s I and the simultaneous autoregressive models of Chapter 8 take. With row standardisation a language’s neighbours always sum to one, so a language with two neighbours listens to each of them more than a language with ten.

5.7 How far does similarity reach?

Chapter 1 computed Moran’s I for immediate neighbours. A correlogram repeats the calculation for neighbours of neighbours, and theirs, and so on, which shows how quickly similarity fades with graph distance.

m0  <- glm(ejectives ~ elev_km, family = binomial, data = d)
res <- residuals(m0, type = "pearson")

cg <- sp.correlogram(knn, res, order = 6, method = "I", style = "W")
steps <- data.frame(step = 1:6, I = cg$res[, 1], se = sqrt(cg$res[, 3]))

ggplot(steps, aes(step, I)) +
  geom_hline(yintercept = 0, linetype = "22", colour = "grey35") +
  geom_linerange(aes(ymin = I - 2 * se, ymax = I + 2 * se),
                 colour = "#2A78D6", linewidth = 0.8) +
  geom_point(colour = "#2A78D6", size = 2.6) +
  scale_x_continuous(breaks = 1:6) +
  labs(x = "steps apart on the neighbour graph",
       y = "Moran's I of the residuals")
Moran's I of the naive model's residuals for neighbours one to six steps apart on the five-nearest-neighbour graph, with two standard errors either side. Similarity is strongest between direct neighbours and is still present several steps away.

Figure 5.2: Moran’s I of the naive model’s residuals for neighbours one to six steps apart on the five-nearest-neighbour graph, with two standard errors either side. Similarity is strongest between direct neighbours and is still present several steps away.

A correlogram that dies out within a step or two says that a neighbour model will capture the dependence. One that stays high for many steps, as here, says the patterns are regional, and a model with a smooth surface or a long reach is the better description.

With your own data. If your units are areas, read the polygons with st_read() and call poly2nb() to link areas that share a border; everything from nb onward is the same. If your sample covers one region, project to a local system (a UTM zone, or an equal-area projection centred on your region) and the flat-map error all but disappears.

5.8 Where locations come from

The coordinates in this book are Glottolog’s. They are the usual choice, and there are others.

Source What it gives How to get it
Glottolog (Hammarström et al., n.d.) One point per language languages.csv in its CLDF release; the lingtypology package (Moroz 2017)
WALS, Grambank, PHOIBLE and other CLDF datasets The same points, shipped with the data The languages.csv of each dataset
Glottography (Glottography, n.d.) Speaker-area polygons digitised from published atlases and matched to Glottocodes The Rglottography package, or the GeoJSON files directly
Ethnologue (Eberhard et al. 2023) Polygons for most languages Under licence only
Glottolog, AUTOTYP (Bickel et al. 2023), WALS Areas as categories: macroareas and continental areas A column in each database

Glottography is the open source of polygons. Its largest dataset digitises the Atlas of the World’s Languages (Asher and Moseley 2007) and comes in two layers, the areas where languages are spoken now and the areas where they were spoken traditionally. Its other datasets cover single regions and families in more detail.

5.9 Speaker areas as polygons

Read the contemporary layer of the atlas. The file is about 90 MB, so this takes a minute the first time.

src   <- paste0("https://raw.githubusercontent.com/Glottography/",
                "asher2007world/v2.0/cldf/")
areas <- st_read(paste0(src, "contemporary/languages.geojson"), quiet = TRUE)
names(areas)[names(areas) == "cldf.languageReference"] <- "glottocode"

sf_use_s2(FALSE)                  # repair the shapes on the flat map
areas <- st_make_valid(areas)
nrow(areas)
## [1] 4062

Each row is one language with its area, possibly in several pieces. Match the areas to the sample by Glottocode.

has  <- intersect(d$glottocode, areas$glottocode)
mine <- areas[match(has, areas$glottocode), ]
here <- pts[match(has, pts$glottocode), ]

c(sample = nrow(d), with_an_area = length(has))
##       sample with_an_area 
##          500          310
sf_use_s2(TRUE)
km2 <- as.numeric(st_area(mine)) / 1e6
round(quantile(km2, c(0, 0.25, 0.5, 0.75, 1)))
##      0%     25%     50%     75%    100% 
##      25     842    3712   17604 2467678

Now ask how far each Glottolog point is from the area the atlas gives the same language. A distance of zero means the point is inside.

gap <- as.numeric(st_distance(here, mine, by_element = TRUE)) / 1000
c(inside = sum(gap == 0), outside = sum(gap > 0))
##  inside outside 
##     172     138
round(quantile(gap[gap > 0], c(0.5, 0.9, 1)))
##  50%  90% 100% 
##   18  142 1063
sf_use_s2(FALSE)
box  <- st_bbox(c(xmin = 32, ymin = 2, xmax = 49, ymax = 19), crs = 4326)

ggplot() +
  geom_sf(data = st_crop(world, box), fill = "grey94", colour = NA) +
  geom_sf(data = st_crop(areas, box), fill = "#2A78D6", alpha = 0.1,
          colour = "#2A78D6", linewidth = 0.2) +
  geom_sf(data = st_crop(pts, box), aes(fill = factor(ejectives)),
          shape = 21, size = 2.2, stroke = 0.3) +
  scale_fill_manual(values = c("0" = "white", "1" = "#EB6834"),
                    labels = c("no ejectives", "ejectives"), name = NULL)
Speaker areas from the atlas (blue) around the Horn of Africa, with the Glottolog points of the sample languages on top. Filled points have ejectives. The areas differ enormously in size, some land (grey) has no area at all, and each language is still a single point.

Figure 5.3: Speaker areas from the atlas (blue) around the Horn of Africa, with the Glottolog points of the sample languages on top. Filled points have ejectives. The areas differ enormously in size, some land (grey) has no area at all, and each language is still a single point.

With polygons, neighbours can be defined as areas that touch. poly2nb() builds that graph.

touching <- poly2nb(mine, snap = 0.01)
c(links_per_language = round(mean(card(touching)), 1),
  without_neighbours = sum(card(touching) == 0),
  separate_pieces    = n.comp.nb(touching)$nc)
## links_per_language without_neighbours    separate_pieces 
##                0.9              161.0              194.0

The graph is nearly empty, and the reason is the sample. Two languages of the sample touch only if no unsampled language lies between them, and 500 languages out of several thousand rarely do. A contiguity graph is meaningful when you have every language of a region. For a sample, link by distance or by nearest neighbours, as above.

The atlas has a second layer with traditional areas. Compare the centre of each language’s area in the two layers.

past <- st_read(paste0(src, "traditional/languages.geojson"), quiet = TRUE)
names(past)[names(past) == "cldf.languageReference"] <- "glottocode"
past <- st_make_valid(past)

both <- intersect(mine$glottocode, past$glottocode)
centre_now  <- st_centroid(st_geometry(mine[match(both, mine$glottocode), ]))
centre_past <- st_centroid(st_geometry(past[match(both, past$glottocode), ]))

sf_use_s2(TRUE)
moved <- st_distance(centre_now, centre_past, by_element = TRUE)
moved <- as.numeric(moved) / 1000
c(languages = length(both), moved_over_100_km = sum(moved > 100),
  furthest_km = round(max(moved)))
##         languages moved_over_100_km       furthest_km 
##               294                26              4185

5.10 What a location leaves out

The numbers above are specific to this sample, and each points at a general problem.

A point stands for an area. The areas of the sample languages run from 25 to 2,467,678 square kilometres, and a single point represents each of them equally. Where two sources can be compared, they disagree: 138 of the 310 Glottolog points fall outside the area the atlas draws for the same language, half of those by more than 18 km and one by 1,063 km.

Coverage is incomplete. The atlas has a contemporary area for 310 of the 500 languages. A method that needs polygons would lose the rest, and the languages it loses are not a random subset: extinct and recently displaced languages are the ones without a present-day area.

A location has a date. 26 of the 294 languages with both layers moved by more than 100 km between their traditional and their present area, and the furthest by 4,185 km. If a feature arose before a language moved, the place where its speakers live now is the wrong place to look for its neighbours or its climate. Glottolog’s points do not say which period they describe.

A covariate measured at a point inherits all of this. The elevation in this book is the height of the ground at the Glottolog point. A language whose area runs from a coast to a plateau has no single elevation, and the point may sit at either end. With polygons you can take the mean or the range of elevation over the area, for example with terra::extract(), and with points you cannot.

Distance is not contact. Great-circle distance ignores mountain ranges, seas, rivers and trade routes, and every model in the next three chapters treats 300 km across a desert like 300 km along a coast. The barrier models cited in Chapter 8 are one response. Another is to compute distances that follow the terrain and to build the spatial matrix from those.

Drawn areas do not overlap, and languages do. An atlas gives each patch of ground to one language. Multilingual regions, cities, and languages of wider communication laid over local ones are not represented, though they are where much contact happens.

The map shows where linguists have worked. The sample is dense where description is dense. Two languages can be neighbours in the data because the languages between them have not been described, and a neighbour graph or a distance matrix will not show the difference.

Areas as categories have borders that somebody chose. Whether a language of the Caucasus belongs to Europe or to Asia changes which languages it is compared with, and the choice is a convention (Hammarström and Donohue 2014). Results that depend on where such a line is drawn are a known hazard of all analyses of areal units.

None of this argues against spatial models. It argues for saying where your locations come from and what period they describe, and for trying a second representation when one is available.

5.11 What to carry forward

  • Great-circle distances come from sp::spDists(coords, longlat = TRUE); projected coordinates from st_transform(); points on the sphere from three lines of trigonometry.
  • A neighbour graph depends on the rule you choose. Check n.comp.nb() for pieces and card() for languages left alone, and draw the graph.
  • Binary weights go to CAR models; row-standardised weights go to Moran’s I and SAR models.
  • A correlogram tells you how far the dependence reaches before you choose a model for it.

5.12 Further reading

How earlier studies represented space. For most of typology’s history, space meant a small number of large areas. Dryer (1989) divides the world into five (later six) continental areas and requires a pattern to hold in each; Jaeger et al. (2011) make the same idea a random intercept for area beside the one for family. The step from areas to coordinates came later. Wieling et al. (2011) fit a smooth function of longitude and latitude in a GAM for Dutch dialect data. Guzmán Naranjo and Becker (2022) use a Gaussian process over coordinates for world samples, and Skirgård et al. (2023) build a spatial covariance matrix from great-circle distances with a Matérn kernel. Ranacher et al. (2021) go the other way and infer discrete contact areas from the data.

  • Glottography (Glottography, n.d.) documents its datasets and the atlases behind them; the two layers of the world atlas come from Asher and Moseley (2007).
  • Hammarström and Donohue (2014) set out how macro-areas should and should not be used in typological comparison.
  • Bivand et al. (2013) is the standard account of spatial objects, neighbours and weights in R, by the authors of spdep.
  • Dormann et al. (2007) review how to account for spatial autocorrelation in regression, with a comparison of methods that maps closely onto the chapters that follow.
  • Banerjee et al. (2014) give the statistical theory behind both views of space, as a continuous surface and as a graph of areas.

References

Asher, R. E., and Christopher Moseley, eds. 2007. Atlas of the World’s Languages. 2nd ed. Routledge.
Banerjee, Sudipto, Bradley P. Carlin, and Alan E. Gelfand. 2014. Hierarchical Modeling and Analysis for Spatial Data. 2nd ed. Chapman; Hall/CRC.
Bickel, Balthasar, Johanna Nichols, Taras Zakharko, et al. 2023. The AUTOTYP Database (V1.1.1). Zenodo.
Bivand, Roger S., Edzer Pebesma, and Virgilio Gómez-Rubio. 2013. Applied Spatial Data Analysis with R. 2nd ed. Springer.
Dormann, Carsten F., Jana M. McPherson, Miguel B. Araújo, et al. 2007. “Methods to Account for Spatial Autocorrelation in the Analysis of Species Distributional Data: A Review.” Ecography 30 (5): 609–28.
Dryer, Matthew S. 1989. “Large Linguistic Areas and Language Sampling.” Studies in Language 13 (2): 257–92.
Eberhard, David M., Gary F. Simons, and Charles D. Fennig, eds. 2023. Ethnologue: Languages of the World. 26th ed. SIL International.
Glottography. n.d. Glottography: Speaker-Area Polygons for the World’s Languages. Https://github.com/Glottography.
Guzmán Naranjo, Matías, and Laura Becker. 2022. “Statistical Bias Control in Typology.” Linguistic Typology 26 (3): 605–70. https://doi.org/10.1515/lingty-2021-0002.
Hammarström, Harald, and Mark Donohue. 2014. “Some Principles on the Use of Macro-Areas in Typological Comparison.” Language Dynamics and Change 4 (1): 167–87.
Hammarström, Harald, Robert Forkel, Martin Haspelmath, and Sebastian Bank. n.d. Glottolog. Leipzig: Max Planck Institute for Evolutionary Anthropology. https://glottolog.org.
Jaeger, T. Florian, Peter Graff, William Croft, and Daniel Pontillo. 2011. “Mixed Effect Models for Genetic and Areal Dependencies in Linguistic Typology.” Linguistic Typology 15 (2): 281–319.
Moroz, George. 2017. Lingtypology: Easy Mapping for Linguistic Typology. R package, https://CRAN.R-project.org/package=lingtypology.
Ranacher, Peter, Nico Neureiter, Rik van Gijn, et al. 2021. “Contact-Tracing in Cultural Evolution: A Bayesian Mixture Model to Detect Geographic Areas of Language Contact.” Journal of the Royal Society Interface 18 (181): 20201031.
Skirgård, Hedvig, Hannah J. Haynie, Damián E. Blasi, et al. 2023. “Grambank Reveals the Importance of Genealogical Constraints on Linguistic Diversity and Highlights the Impact of Language Loss.” Science Advances 9 (16): eadg6175. https://doi.org/10.1126/sciadv.adg6175.
Wieling, Martijn, John Nerbonne, and R. Harald Baayen. 2011. “Quantitative Social Dialectology: Explaining Linguistic Variation Geographically and Socially.” PLoS ONE 6 (9): e23613.