Geostatistics in practice using R | by Arthur Mello | Towards Data Science

In this article, you will understand what is geostatistics, and how to use kriging, an interpolation method, to make estimates using geolocation data. In our example, we will take prices of real estate sales in New York and create a model that gives us a price estimate for any location within the city.
Before we start, let’s look at some context of how geostatistics is inserted within the field of spatial statistics.
Spatial statistics is a relatively new domain within statistics and it is based on the idea of spatial dependence between data: the closer two observations are in space, the stronger their correlation is. Time dependence has been studied for a long time, but it is not possible to directly apply time series models to spatial data, since time dependence hypotheses usually use a serial correlation structure, while spatial correlation happens in two or three dimensions. There are, however, spatio-temporal models that combine both at the same time.
Within spatial statistics, there are four categories of data: geostatistical, lattice, point and flow. We will only be talking about geostatistics here, but knowing what are the others helps us understand what geostatistics is not.
In geostatistical data, a random variable can be measured at any of the infinite points of our set (a city or a country, for example) — even though, in practice, it is only measured at some points. This is the case of the example we will be using: you can have real estate prices (our random variable) at any point in New York (our set), and we only measure it at some of them (the points where a sale actually occurred). Our interest will then be to estimate that same variable for points where a sale did not happen.
In lattice data, a random variable is measured in a finite set, represented by a matrix, that can be regular or not. An example is when we split a country in states and measure average income in each of these states.
In point data, the randomness comes not necessarily from the variable, but from its location. An example would be if we wanted to understand or predict crime locations in a city: we would have a series of crime locations and would be interested in finding clusters of high density (dangerous zones).
In flow data, we have a random variable attached not only to one location, but to two of them. An example is migration data: we know the number of people that go from one location to the other, for any pairwise combination of locations.
Now we know what is geostatistics (and what it is not). Before we look at kriging, we have one last topic to cover: variograms.
If spatial statistics is based on the idea that “the closer two observations are in space, the stronger their correlation is”, a variogram shows how that correlation behaves depending on the distance between those observations.
A variogram is defined as “a function that, given a distance between two observations, returns the variance of the difference between those observations”.
Here is an example of an empirical variogram for real estate prices in New York.
As we can see, the further away two buildings are, the more their prices can differ, and the less correlated they are.
Based on those points, we can estimate a function that fits them well, which we then use for kriging.
Kriging is an interpolation method: given some data points, it tries to find a function that goes through these points so one can make estimations for intermediate values.
While linear regression and inverse distance weighted interpolation are based on a hypothetical pre-defined model, kriging is basically built empirically. One of its strengths is that it is not a deterministic model, but a probabilistic one, meaning that not only it gives you an estimate, it also tells you how sure you can be of those estimates.
The equation for making those estimations is the following:
The weights are estimated from the variogram, and depend exclusively on the distance between the observations.
We will be using a dataset available on Kaggle, which has property sales data on New York city for the year of 2016: location, price, area, tax class, etc. Given our goal here, we will ignore all information on the property except for its location, price and area. The reason we use area is that we want to calculate price per square feet, and not just sale price. From now on, we’ll only talk about price per square feet, and refer to it just as ‘price’.
The full code is available here. In this article, we’ll just go through its main parts.
After doing some data cleaning, we start by taking a look at how prices behave on a map:
IN:# Plotting pricesmapnyc <- data.frame(latitude = db$lat,                     longitude = db$lon,                     price = db$PRICE.SQUARE.FEET)Token_map_box = #<mapbox API token>palette_rev <- rev(brewer.pal(10, "RdYlBu"))pal <- colorNumeric(palette = palette_rev, domain = c(max(mapnyc$price):min(mapnyc$price)))leaflet() %>%  addTiles(urlTemplate = Token_map_box) %>%  addCircleMarkers(data = mapnyc, color = ~pal(price), radius = 2)
As we can see, there were a lot less sales on Manhattan, specially on the lower part. Additionally, prices seem a lot higher there than in Queens (the part on the east) for instance. Also, we can see that property that is right next to water is more expensive too, which makes sense, since they probably have a nice view.
Now, let’s see if prices follow a normal distribution:
IN:hist(db$PRICE.SQUARE.FEET)OUT:
As we can see, our data is not normally distributed, so let’s use its log instead. Please note that this step is not absolutely necessary: data following a normal distribution is not one of kriging’s assumptions. However, slightly improved prediction power has been reported when this is the case.
IN:db$PRICE.SQUARE.FEET.log = log(db$PRICE.SQUARE.FEET)hist(db$PRICE.SQUARE.FEET.log)OUT:
For now, all we did was use a regular dataframe. Spatial data, however, behaves in a totally different way in R. We can convert our dataframe into a SpatialPointsDataFrame, which stores point data as locations, together with additional information on them (such as the price). Before doing that, we will also split our dataframe into train and test sets, in order to calculate some error metrics at the end.
IN:# Train/test splitsmp_size <- floor(0.75 * nrow(db))set.seed(123)train_ind <- sample(seq_len(nrow(db)), size = smp_size)train <- db[train_ind, ]test <- db[-train_ind, ]# Converting to spatialpointsdataframeprojection = "+proj=longlat +ellps=WGS84 +no_defs"train_spdf = SpatialPointsDataFrame(train[28:29], train[-c(28,29)], proj4string = CRS(projection))test_spdf = SpatialPointsDataFrame(test[28:29], test[-c(28,29)], proj4string = CRS(projection))db_spdf = SpatialPointsDataFrame(db[28:29], db[-c(28,29)], proj4string = CRS(projection))
Notice the function SpatialPointsDataFrame, from the ‘sp’ package. It takes the location columns as its first argument, the rest of the data as the second argument, and a projection as the third argument. A projection is a function that turns spherical coordinates into planar coordinates. There are many different ways of doing this, and we used the WGS84 system.
We then remove any duplicate locations, since kriging takes only one value (price) per location.
IN:# Removing duplicate locationstrain_spdf = train_spdf[-zerodist(train_spdf)[,1],]test_spdf = test_spdf[-zerodist(test_spdf)[,1],]db_spdf = db_spdf[-zerodist(db_spdf)[,1],]
Now, let’s see if there seems to be any spatial correlation, by looking at the general empirical variogram. We can also, at the same time, find the best fit for the variogram, and plot both together.
IN:# Choosing the best variogramvario.fit = autofitVariogram(PRICE.SQUARE.FEET.log~1,                             train_spdf,                             model = c("Exp", "Sph"),                             kappa = c(0, 0.01, 0.05, seq(0.2, 2, 0.1), 5, 10),                             fix.values = c(NA),                             start_vals = c(NA),                             verbose = T)fit.v = vario.fit$var_modellzn.vgm <- variogram(PRICE.SQUARE.FEET.log~1, train_spdf) # calculates sample variogram values lzn.fit <- fit.variogram(lzn.vgm, model=fit.v, fit.kappa = TRUE) # fit modelplot(lzn.vgm, lzn.fit) # plot the sample values, along with the fit modelOUT:
The empirical variogram here is represented by the circles, while our fit is represented by the line. The package ‘automap’ has a function called ‘autofitVariogram’ that finds the best fit for the empirical variogram out of a combination of parameters given by the user, such as the shape of the function. The spatial correlation seems evident and our line fits the data reasonably, so we move on with it.
Now that we got our variogram function from the available data, we want to plot our estimations for new data, in order to create a heatmap of prices in NY. To do so, we need a grid of NY, which is essentially a set of small squares within the city boundaries. This is maybe a good time to talk about the different types of spatial data in R. Spatial coordinates can give you either points, lines or polygons. Our price data is given for points and NY city boundaries are given by a polygon, that can be downloaded here as a shapefile (.shp).
IN:# Getting NY polygonny_polygon = st_read('data/borough_boundaries/geo_export_a88ff5b0-7fb1-479d-a22b-224328f6d976.shp')ny_polygon = st_transform(ny_polygon, projection)spd <- sf::as_Spatial(st_geometry(ny_polygon), IDs = as.character(1:nrow(ny_polygon)))spd_data = ny_polygonspd_data$geometry = NULLspd_data <- as.data.frame(spd_data)spd <- sp::SpatialPolygonsDataFrame(spd, data = spd_data)grid <- makegrid(spd, cellsize = 0.001)grid <- SpatialPoints(grid, proj4string = CRS(projection))grid <- grid[spd, ]
What we did here was get the NY polygon using the same projection as our original data, turn it into a SpatialPolygonsDataFrame and then use it to make a grid, where each cell is of size equal to the variable ‘cellsize’. The smaller this number, the more detailed (or less “pixelated”) will be your map, but the longer it will take to build it.
We then move to the kriging part itself, where we apply it to our grid:
IN:# Krigingheat = krige(PRICE.SQUARE.FEET.log ~ 1, locations = train_spdf, newdata = grid, model = fit.v)heat %>% as.data.frame %>%  ggplot(aes(x=x1, y=x2)) + geom_tile(aes(fill=var1.pred)) + coord_equal() +  scale_fill_gradient(low = "yellow", high="red") +  scale_x_continuous(labels=comma) + scale_y_continuous(labels=comma) +  theme_bw()OUT:
Here, we used the function ‘krige’, where we tell it what model to use (‘fit.v’ is where we fit our variogram function) and ‘newdata’ is our grid. We then plot it as a heatmap, where we can clearly see the high price areas as a red zone around Lower Manhattan, even though we didn’t have much data there.
Let’s then evaluate the performance of our model in the test data, by comparing it to another, simpler method: K-nearest neighbors (KNN). In short, what KNN will do is, for each building on the test set, take the price for the K nearest buildings (in our case, the best value for K was 14) and use their average to estimate a price. I will not get into the details for our KNN model here, since it is not the scope of this article, but you can find it in the full code available on GitHub.
IN:kriging_error = rmse(test_spdf$PRICE.SQUARE.FEET, exp(kriging_test_prediction$var1.pred))knn_error = rmse(test$PRICE.SQUARE.FEET, exp(knn_model$pred))# final resultscat(paste('Kriging error:', kriging_error, '\nKNN error:', knn_error))OUT:Kriging error: 159.360604969667 KNN error: 162.557634288862
Here, we used the Root Mean Square Error (RMSE) to compare the two models, which takes the square root of the average squared difference between predicted and actual values. As we can see, kriging error was slightly smaller, but for a very small margin. Depending on the application, it might be better to go for the KNN, since it is faster and easier to calculate. The goal here, however, was to show how kriging works and how to do it using R.
If you want to try more advanced methods using this data, it might be interesting to check out kriging regression, which does kriging combined with regression using other explanatory variables (such as the building’s year of construction, for example).
Additionally, if you liked this article, you might also be interested on these two other ones:
towardsdatascience.com
towardsdatascience.com
Feel free to reach out to me on LinkedIn if you would like to discuss further, it would be a pleasure (honestly).