El análisis espacial comprende un conjunto de técnicas formales de análisis de datos que incluyen propiedades espaciales que permiten ubicarlos de alguna forma en el espacio. El conjunto de tecnologías de móviles e internet, han conseguido aumentar de forma exponencial los datos disponibles sobre los clientes, incluidos los datos espaciales, por lo tanto, este nuevo tipo de dato dan la cara a obtener nuevas oportunidades de negocio o desarrollar las actuales.
En concreto, nuestro análisis se va a centrar en los datos de clientes de una compañía en la ciudad de Londres, dicha compañia quiere realizar las siguientes acciones:
La empresa tiene actualmente 33 sucursales en Londres, una por distrito, y ante la falta de rentabilidad necesita cerrar tres de estas oficinas. Ha decidido que lo hará con aquellas que tengan menor volumen de negocio (suma del consumo de todos los productos) de clientela menor de 55 años (ya que considera que, con sus servicios de banca electrónica, este tipo de clientes estarían cubiertos).
Quiere saber, para cada uno de los distritos cerrados, si hay alguna oficina u oficinas ubicadas en un distrito cercano a las que pueda derivar a los clientes en caso de necesidad. Para ello, se considerará que las oficinas están geoposicionadas en el centro de los correspondientes distritos.
Comenzaremos con la carga de los datos espaciales de la región a estudiar de Londres, así como el .csv correspondiente a los datos de los clientes, posteriormente se realizarán todas las operaciones necesarias para limpiar los datos, transformarlos, crear mapas que aporten insights y finalmente tomar las decisiones pertinentes.
En primer lugar, cargamos todos los paquetes necesarios y limpiamos el espacio de trabajo. Con una sencilla función comprobamos si los paquetes necesarios están instalados.
rm(list=ls())
setwd("C:/Users/Jose/Desktop/RSpatialTutorial")
is.installed <- function(package) is.element(package, installed.packages())
if(!is.installed('rgdal'))
install.packages('rgdal')
library(rgdal)
if(!is.installed('rgeos'))
install.packages('rgeos')
library(rgeos)
if(!is.installed('tmap'))
install.packages('tmap')
library(tmap)
if(!is.installed('dplyr'))
install.packages('dplyr')
library(dplyr)
Para esta práctica utilizamos los datos de Londres en formato shapefile (SHP). Este es un formato de archivo informático propietario de datos espaciales desarrollado por la compañía ESRI, quien crea y comercializa software para Sistemas de Información Geográfica como Arc/Info o ArcGIS.
Además, cargamos el dataframe que contiene la información sobre los clientes de la empresa y varios datos los cuales nos van a ser útiles para nuestro propósito, como la edad de los clientes, el distrito donde se encuentra, y la venta de una serie de productos. Al cargar eldataframe solo realizamos unos cambios básicos antes de entrar en las transformaciones de los datos, como es cambiar a numérica la columna de población y cambiar a numérica una columna del dataframe de clientes.
i_data_gs <- readOGR(dsn = "data", layer = "london_sport")
i_data_gs$Pop_2001 <- as.numeric(as.character(i_data_gs$Pop_2001))
LonCos <- read.csv("C:/Users/Jose/Desktop/RSpatialTutorial/data/LondonCustomer.csv",
header = TRUE, sep = ";")
str(LonCos)
## 'data.frame': 4980 obs. of 14 variables:
## $ CONTACT_ID : int 395 396 397 398 399 400 401 402 403 404 ...
## $ AGE : int 25 45 39 35 35 37 53 50 35 34 ...
## $ FAMILYSIZE : int 4 3 1 1 4 4 2 1 3 1 ...
## $ YEAREXPERIENCE : int 1 19 15 9 8 13 27 24 10 9 ...
## $ ANNUALINCOME : int 49 34 11 100 45 29 72 22 81 180 ...
## $ EDUCATIONLEVEL_ID : int 1 1 1 2 2 2 2 3 2 3 ...
## $ NETPRICE_PRO11_AMT: int 0 0 0 0 0 155 0 0 104 0 ...
## $ NETPRICE_PRO12_AMT: int 1 1 0 0 0 0 0 0 0 0 ...
## $ NETPRICE_PRO13_AMT: int 0 0 0 0 0 0 0 0 0 0 ...
## $ NETPRICE_PRO14_AMT: chr "16" "15" "10" "27" ...
## $ NETPRICE_PRO15_AMT: int 0 0 0 0 1 0 0 1 0 0 ...
## $ NETPRICE_PRO16_AMT: int 0 0 0 0 0 1 1 0 1 0 ...
## $ NETPRICE_PRO17_AMT: int 0 0 0 0 0 0 0 0 0 1 ...
## $ name : chr "Lewisham" "Enfield" "Waltham Forest" "Barking and Dagenham" ...
Al observar el tipo de dato, vemos como NETPRICE_… corresponde a la venta de varios tipos de productos, en concreto NETPRICE_PRO14_AMT aparece como caracter, por lo que lo cambiamos a numérico.
LonCos$NETPRICE_PRO14_AMT <- as.integer(LonCos$NETPRICE_PRO14_AMT)
En esta fase vamos a transformar algunos datos para ordenar y obtener las columnas necesarias para llegar a tomar la decisión de que sucursales se deberían de cerrar. En este análisis, se cerrarán aquellos distritos cuyo beneficio sea muy pequeño y además sus clientes tenga una edad menor de 55 años.
Como se cerrarán las sucursales que provean menor beneficio, sumamos todas las columnas de ganancias y la nombramos NETPRICE_TOTAL, los valores nulos los sustituimos por 0. Después, con dplyr, realizamos un sumatorio de las ganancias obtenidas por distrito.
Posteriormente también nos será útil la masa de clientes por distrito, por lo que usando la función aggregate contamos el total de clientes y los agrupamos por distrito.
Otra variable que se tendrá en cuenta para el cierre, serán aquellos distritos con clientes menores de 55 años, lo que viene a ser, aquellos distritos más jóvenes, para una mejor compresión se realiza una media de edad de los distritos, por lo que aquellos distritos más jóvenes serán más candidatos a ser cerrados.
Todos estos cálculos los vemos a continuación:
attach(LonCos)
col <- c("NETPRICE_PRO11_AMT", "NETPRICE_PRO12_AMT", "NETPRICE_PRO13_AMT",
"NETPRICE_PRO14_AMT", "NETPRICE_PRO15_AMT", "NETPRICE_PRO16_AMT",
"NETPRICE_PRO17_AMT")
# Sumamos el total de ganancias
LonCos$NETPRICE_TOTAL <- rowSums(LonCos[,col])
# Sustituimos los NA por 0 en la columna de ganancias totales
LonCos$NETPRICE_TOTAL[is.na(LonCos$NETPRICE_TOTAL)] <- 0
# Agrupamos todos los clientes por la provincia
ag_clien <- aggregate(LonCos$CONTACT_ID, by=list(LonCos$name), FUN = length)
ag_clien <- ag_clien %>% rename(clientes = x)
ag_clien <- ag_clien %>% rename(name = Group.1)
# Sumamos las ganancias totales por distrito
ag_net <- LonCos %>% group_by(name) %>% summarise(beneficio = sum(NETPRICE_TOTAL))
# Ahora calculamos la media de edad de los clientes de cada distrito
ag_edad <- LonCos %>% group_by(name) %>% summarise(media_edad = mean(AGE))
Una vez tenemos las transformaciones hecha, tenemos que unir las columnas creadas con los datos espaciales. Para esto utilizamos la función merge y seleccionamos las 3 columnas creadas. Después, construimos un objeto espacial nuevo que tenga los mismos datos espaciales que el original (municipios de Londres) con los nuevos datos cuantitativos referidos a los clientes por distrito, beneficio por distrito y edad media de los clientes por distrito. Con los datos geográficos y no geográficos almacenados en data_gs podemos comenzar a trabajar en la creación de mapas para la posterior toma de decisiones.
# Unimos las columnas creadas a los datos espaciales
data <- merge(i_data_gs@data, c(ag_edad, ag_net, ag_clien), by = "name", all.x = T )
data <- select(data, -c(6,8))
# Ahora vamos a construir un objeto espacial nuevo que tenga los mismos datos espaciales
data_gs<-merge(i_data_gs, data[,c(1,5:7)], by = "name", all.x=TRUE)
A continuación, vemos una muestra de data_gs@data para observar que los datos están ordenados correctamente y se han incluido las columnas:
as_tibble(head(data_gs@data))
## # A tibble: 6 x 7
## name ons_label Partic_Per Pop_2001 media_edad beneficio clientes
## <chr> <chr> <dbl> <dbl> <dbl> <dbl> <int>
## 1 Bromley 00AF 21.7 295535 45.1 791 19
## 2 Richmond upon Tha~ 00BD 26.6 172330 44.6 9459 105
## 3 Hillingdon 00AS 21.5 243006 45.1 22729 273
## 4 Havering 00AR 17.9 224262 44.4 13868 157
## 5 Kingston upon Tha~ 00AX 24.4 147271 45.0 19776 247
## 6 Sutton 00BF 19.3 179767 44.4 8148 98
Ya tenemos nuestro dataframe listo para trabajar. En primer lugar, vamos a tomar la decisión de cerrar 3 sucursales, para ello nos vamos a apoyar en varios mapas y filtraremos los resultados para una mejor interpretación y facilidad de decisión. La segunda tarea consistirá en asignar a los clientes de las sucursales cerradas en una sucursal cercana.
A continuación, mostramos dos mapas, el primero viene referido al beneficio obtenido por zonas geográficas, la leyenda nos muestra que las zonas menos coloreadas reportan menos beneficios. El segundo mapa nos muestra la media de edad de las zonas, solo que esta vez las zonas menos coloreadas vienen referidas a una media de edad más pequeña, es decir, que son clientes más jóvenes. Tenemos que evitar cerrar aquellas zonas con una media de edad más alta, ya que este tipo de usuario se maneja peor con las tecnologías actuales.
tm_shape(data_gs) +
tm_borders(col = 'black', lwd = 0.3) +
tm_fill(col = 'beneficio', title = 'Beneficios', legend.hist = TRUE, palette = "Blues") +
tm_legend(legend.outside = TRUE) +
tm_layout(frame = FALSE)
tm_shape(data_gs) +
tm_borders(col = 'black', lwd = 0.3) +
tm_fill(col = 'media_edad', title = 'Media de edad', legend.hist = TRUE, palette = "Blues") +
tm_legend(legend.outside = TRUE) +
tm_layout(frame = FALSE)
Aplicando un filtro, donde se coloreen las zonas cuyo beneficio sea menor a 9000 y la media de edad inferior a 46 años, las posibles zonas a cerrar son las que aparecen en el gráfico de abajo. En el apartado siguiente, será donde tomaremos finalmente la decisión de que zonas deben cerrarse.
Para añadir las etiquetas a las zonas que vamos a colorear a continución tenemos que calcular sus centros geograficos, este es el paso previo y se muestra a continución:
sel1 <- data_gs$name == "Haringey"
sel2 <- data_gs$name == "Wandsworth"
sel3 <- data_gs$name == "Hammersmith and Fulham"
sel4 <- data_gs$name == "Bromley"
sel5 <- data_gs$name == "Sutton"
cent_data_gs_ha <- gCentroid(data_gs[sel1,])
cent_data_gs_wan <- gCentroid(data_gs[sel2,])
cent_data_gs_hf <- gCentroid(data_gs[sel3,])
cent_data_gs_bro <- gCentroid(data_gs[sel4,])
cent_data_gs_sut <- gCentroid(data_gs[sel5,])
# Filtramos y coloremos las regiones que vamos a eliminar
filtro <- data_gs$beneficio < 9000 & data_gs$media_edad < 46
plot(i_data_gs, col = "azure1")
plot(i_data_gs[filtro, ], col = "darkgoldenrod1", add = TRUE)
text(coordinates(cent_data_gs_ha), "Haringey")
text(coordinates(cent_data_gs_wan), "Wandsworth")
text(coordinates(cent_data_gs_hf), "Hammersmith and Hulham")
text(coordinates(cent_data_gs_bro), "Bromley")
text(coordinates(cent_data_gs_sut), "Sutton")
Vemos como existen 5 zonas candidatas, la región de Bromley, la de Sutton, la de Wandsworth, la de Hammersmith and Hulham y la de Haringey, recordemos que buscamos cerrar solamente 3 sucursales de tal forma que causemos el menor impacto en los clientes habituales de esas zonas.
Por último, mostramos un gráfico con la densidad de clientes por zonas, vemos como también aquellas zonas que reportan pocos beneficios se debe a que tienen pocos clientes.
tm_shape(data_gs) +
tm_borders(col = 'black', lwd = 0.3) +
tm_fill(col = 'clientes', title = 'Nº de clientes', legend.hist = TRUE, palette = "Blues") +
tm_legend(legend.outside = TRUE) +
tm_layout(frame = FALSE) +
tm_text("name", size = 0.7)
En este último apartado, vamos a tomar la decisión de que 3 sucursales de las 5 candidatas se van a cerrar en su respectivo distrito. Para ello, filtramos el mapa para solo mostrarnos las zonas candidatas a ser cerradas, por su beneficio y media de edad:
tm_shape(data_gs[filtro,]) +
tm_polygons("beneficio", title = "Beneficio", palette = "Blues") + tm_text("name", size = 0.7) +
tm_legend(legend.position = c("right", "top"), frame = TRUE, legend.text.size = 0.9)
tm_shape(data_gs[filtro,]) +
tm_polygons("media_edad", title = "Media de edad", palette = "Blues") + tm_text("name", size = 0.7) +
tm_legend(legend.position = c("right", "top"), frame = TRUE, legend.text.size = 0.9)
¿Qué criterio deberíamos seguir? Pues sencillamente observando aquellas zonas con una color más claro en ambas partes del gráfico, por lo que a simple vista ya tenemos dos sucursales que van a ser cerradas, el del distrito de Wandsworth y el distrito de Haringey. Simplemente porque de las 5 zonas, son las que menos reportan beneficio y las que tienen unos clientes más jóvenes.
De las tres zonas restantes, es decir, Sutton, Bromley y Hammersmith and Fulham, claramente descartamos a Sutton, ya que reporta un beneficio mucho más grande que las dos zonas anteriores (de 8000 a 9000, mientras que las otras dos están por debajo de 2000), y la media, aunque más alta, estamos hablando de una diferencia de tan solo 1 año.
Entre Bromley y Hammersmith and Fulham, podríamos seguir un criterio de distancia ya que en beneficio y media de edad son prácticamente iguales. Bromley es una zona muy amplia, y si queremos localizar a sus clientes en una sucursal cercana, seguramente tendrán que andar muchos más kilómetros a que si cerramos la sucursal de Hammersmith and Fulham, cuya próxima sucursal estaría más cerca. Por lo tanto, la tercera sucursal cerrada es Hammersmith and Fulham.
Por lo tanto, hemos cerrados las zonas de Haringey, Wandsworth y Hammersmith and Fulham. Vemos como estas zonas tienen pocos clientes. Los datos los vemos a continuación:
as_tibble(data_gs@data[filtro, c(1,5:7)])
## # A tibble: 5 x 4
## name media_edad beneficio clientes
## <chr> <dbl> <dbl> <int>
## 1 Bromley 45.1 791 19
## 2 Sutton 44.4 8148 98
## 3 Wandsworth 42.8 517 5
## 4 Hammersmith and Fulham 45.2 1326 21
## 5 Haringey 42.5 1262 16
Por último, los clientes de las sucursales cerradas van a ser localizamos en otras sucursales, las cuales se van a encontrar a un máximo de 2 kilómetros de su respectivo centroide geográfico. Si exploramos el mapa de debajo de arriba abajo:
# Vamos a localizar a los clientes de las zonas clausuradas a un distrito cercano
# Seleccionamos las zonas geograficas que esten a 2km del centroide geografico
data_gs_buffer_ha <- gBuffer(spgeom = cent_data_gs_ha, width = 2000)
data_gs_buffer_wan <- gBuffer(spgeom = cent_data_gs_wan, width = 2000)
data_gs_buffer_hf <- gBuffer(spgeom = cent_data_gs_hf, width = 2000)
# Seleccionamos las provincias dentro de esta zona
data_gs_central_ha = data_gs[data_gs_buffer_ha,]
data_gs_central_wan = data_gs[data_gs_buffer_wan,]
data_gs_central_hf = data_gs[data_gs_buffer_hf,]
# Pintamos los graficos con sus respectivas zonas
# Zona de Haringey
plot(data_gs, col = "azure2")
plot(data_gs_central_ha, col = "coral", add = T)
plot(data_gs_buffer_ha, add = T)
text(coordinates(cent_data_gs_ha), "Haringey", cex=0.8)
# Zona de Wandsworth
plot(data_gs_central_wan, col = "coral", add = T)
plot(data_gs_buffer_wan, add = T)
text(coordinates(cent_data_gs_wan), "Wandsworth", cex=0.8)
# Zona de Hammersmith and Fulham
plot(data_gs_central_hf, col = "coral", add = T)
plot(data_gs_buffer_hf, add = T)
text(coordinates(cent_data_gs_hf), "Hammersmith", cex=0.8)
# Por último, pintamos el nombre de las regiones donde serán localizados
sel6 <- data_gs$name == "Islington"
sel7 <- data_gs$name == "Kensington and Chelsea"
sel8 <- data_gs$name == "Merton"
cent_data_gs_is <- gCentroid(data_gs[sel6,])
cent_data_gs_ho <- gCentroid(data_gs[sel7,])
cent_data_gs_mer <- gCentroid(data_gs[sel8,])
text(coordinates(cent_data_gs_is), "Islington", cex=0.8)
text(coordinates(cent_data_gs_ho), "Kensington", cex=0.8,pos=3)
text(coordinates(cent_data_gs_mer), "Merton", cex=0.8)
Analysis of Big Spatial Data with PostgreSQL/PostGIS and R – Case Studies in OpenStreetMap and Interactive Web Mapping from R. : Zielstra, D. y Tonini, F. (2015) North Carolina State University, https://pdfs.semanticscholar.org/presentation/3549/c1c57740608fcca097f36f49fdcef55c52b1.pdf
Introduction to visualising spatial data in R . : Lovelace, R. y Cheshire, J. (2014). NCRM Working Paper. EloGeo.
Wikipedia, “Shapefile” : https://es.wikipedia.org/wiki/Shapefile
EPSG Geodetic Parameter Registry : http://www.epsg-registry.org/