Escolar Documentos
Profissional Documentos
Cultura Documentos
Antes de empezar 7
4 Visualización 45
4.1 Una buena visualización para empezar: el scatterplot . . . . . . . . . . . . . . . . . . . . . . . 45
4.2 Ajustando color y tamaño . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
4.3 Facetado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.4 Gráficos de barras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.5 Histogramas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
4.6 Preparando una visualización para compartir . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.7 Otras visualizaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
5 Modelado estadístico 73
5.1 Regresión lineal simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5.2 Regresión con múltiples variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
3
4 CONTENTS
¿Para quién es esto?
Este libro fue escrito con una audiencia en mente formada por urbanistas, sociólogos, politólogas y otros
entusiastas que se acercan al tema desde las Ciencias Sociales. Aún así, y por supuesto, todas las personas
y algoritmos con capacidad de procesar lenguaje son bienvenidas.
Espero que el tono introductorio del texto, así como el esfuerzo puesto en explicar los conceptos con la mayor
simplicidad posible, resulten de interés para un público amplio.
No hace falta ningún conocimiento previo de programación; todas las herramientas necesarias serán expli-
cadas sobre la marcha.
5
6 CONTENTS
Antes de empezar
Para practicar los ejemplos que se explicarán a lo argo del libro, es necesario instalar el lenguaje de progra-
mación R, y la interfaz gráfica RStudio Desktop.
7
8 CONTENTS
Chapter 1
La Big Data ha llegado para quedarse, y asumimos que su efecto en la sociedad será permanente. Así
como pasó con la escritura, los medios de comunicación o tantos otros inventos humanos de inmenso im-
pacto cultural, el incremento en la producción y análisis computacional de grandes volúmenes de datos está
transformando cada una de nuestras actividades. Algunas profesiones se ven en crisis, otras se benefician, y
también se crean algunas nuevas.
Big data es un término impreciso, que se usa cuando queremos hablar de los datos que nuestra sociedad crea
y procesa en forma digital, con cada vez más creciente velocidad, volumen, y variedad.
En forma acorde, data scientist o “científico de datos” es también una profesión, o una actividad, que aún
no está definida con toda claridad. El término, que abarca a quienes en forma cotidiana aplican técnicas de
programación para analizar datos, no existía antes del 2008. Sólo cuatro años después la publicación Harvard
Business Review agitó las aguas al declarar que quienes se desempeñan como científicos de datos pueden
presumir de la profesión “más sexy del siglo XXI” [^1]. Títulos exagerados aparte, lo que es seguro es que
la discipina ofrece un conjunto cada vez más maduro de saberes orientados a explotar datos para extraer
conocimiento. Las técnicas y principios que la comunidad de la ciencia de datos ha desarrollado pueden ser
aprovechados en muchos ámbitos. Entre ellos, el de las ciencias sociales, que también están en una etapa de
transformación e incorporan la programación analítica como un recurso cada vez extendido.
Avanzar las fronteras de la ciencia de datos, crear los algoritmos y técnicas informáticas que abren nuevas
posibilidades de análisis es una tarea compleja, llevada a cabo por especialistas con profundos conocimientos
de matemática. Y sin embargo “usar” la ciencia de datos, aplicar sus principios para resolver problemas
complejos, es bastante más fácil. Para empezar sólo necesitamos paciencia para aprender algunos conceptos
fundamentales de programación y estadística, empleándolos para entender y comunicar con datos. De eso
se trata este libro.
9
10 CHAPTER 1. ¿QUÉ ES LA CIENCIA DE DATOS?
• Estadística. ¡Inescapable! También poderosa, a veces anti-intuitiva, cuando tenemos suerte reve-
ladora. La estadística es muchas cosas, pero -a pesar de su mala fama- aburrida jamás. Sólo es
cuestión de amigarse con ella. Vamos a necesitarla para extraer conocimiento de los datos. Es sor-
prendente lo mucho que puede lograrse con sólo unos rudimentos (media, mediana, desvío estándar y
cuartiles) y de allí en más sólo es cuestión de profundizar paso a paso.
• Comunicación. Un científico de datos combina habilidades “duras” con otras que requieren empatizar
con los demás: las que se relacionan con la comunicación y la colaboración interdisciplinaria. Encontrar
la forma de explicar procesos complejos, de llevar las revelaciones de un modelo estadístico a términos
que tengan sentido para un público amplio, crear visualizaciones que permitan a terceros “leer” los
datos y sacar conclusiones por su cuenta. Parte de hacer ciencia de datos es saber cómo discutir
los datos usados y los resultados obtenidos con un interlocutores muy diversos: audiencia general,
funcionarios públicos, colegas, especialistas de otras disciplinas, etcétera.
• Conocimiento de dominio. El conocimiento de dominio es la experiencia acumulada en un campo
particular de actividad humana: agricultura, relaciones públicas, física cuántica, crianza de niños.
Complementa de forma imprescindible a las habilidades analíticas. El conocimiento de dominio no sólo
ayuda a discernir si las respuestas obtenidas mediante un sofisticado análisis estadístico tienen sentido.
También es necesario para saber cuáles son las preguntas que deberíamos estar haciendo.
Las cuatros habilidades entran en acción en cada proyecto que involucra ciencia de datos, en mayor o menor
medida de acuerdo a la etapa de análisis. Hablando de etapas, Hadley Wickham, uno de los referentes
actuales en el campo, las define así:
Y todo ello llevado a cabo mediante la programación, por supuesto.
A lo largo de los capítulos de este libro vamos a aprender técnicas de programación que nos permitan atravesar
cada uno de los pasos del proceso, y al hacerlo estaremos ejercitando las cuatro habilidades que involucra la
ciencia de datos.
Allá vamos.
Chapter 2
A continuación reproduciremos un ejercicio paso a paso, para ilustrar la potencia de una herramienta de
análisis como R. Que nadie se preocupe si algunas de las operaciones parecen no tener sentido, o resultan
arbitrarias. ¡Es normal! Nadie aprende un lenguaje en 10 minutos, sea R o esperanto. La idea es tener
exposición temprana a un caso de uso interesante, usando datos reales. Y que nos sirva como motivación
para practicar luego ejercicios básicos que son muy necesarios pero, a veces, no tan emocionantes.
Buenos Aires es una ciudad que desde hace décadas presenta una marcada polarización entre sus barrios del
sur, relativamente menos desarrollados, y los del norte donde el nivel socioeconómico y la calidad de vida
son mayores.
Uno de los aspectos más lamentables de la disparidad norte-sur, y sin duda de los que más polémica y
acusaciones cruzadas ha generado, es la diferencia en la tasa de mortalidad infantil de acuerdo a la región
de la ciudad.
¿Qué tan grande es esa diferencia? ¿Cómo se distribuye geográficamente?
Vamos a utilizar R para responder esas preguntas y visualizar los resultados de nuestro análisis, utilizando
como fuente cifras oficiales publicada por la ciudad.
11
12 CHAPTER 2. UNA PRESENTACIÓN A TODA MARCHA DE R
El primer paso es ejecutar RStudio, que ya deberíamos tener disponible en nuestro sistema.
Una vez abierta la interfaz gráfica, creamos un proyecto nuevo, cliqueando en File -> New Project...
-> New Directory -> New Project. En la ventana que surge, elegir un nombre para el proyecto (por
ejemplo, “Practicando R”) y finalizar la operación cliqueando en Create project.
Utilizar proyectos nos permite continuar otro día desde donde dejamos la tarea al terminar una sesión. Es
sólo cuestión de recuperar el proyecto deseado la próxima vez que abrimos RStudio, cliqueando en File ->
Recent Projects -> "nombre de mi proyecto".
Por ahora, sigamos trabajando. Vamos a crear un “script”. Un script, como su nombre en inglés lo indica, es
un guión; una serie de pasos que escribimos para que nuestra computadora ejecute en secuencia. Cliqueamos
en File -> New File -> R Script. De inmediato se abre una ventana con un editor de texto. ¡Ahora
empieza la acción!
sqrt() es una función. En el mundo de la programación, las funciones son secuencias de código ya listas
para usar, que realizan tareas útiles. Por ejemplo, mostrar algo en pantalla. En nuestro caso, completamos
la función con algo más: un parámetro, pues así se le llama a los valores que una función espera de parte
del usuario para saber que hacer. La función print espera que le demos un número para el cual calcular
su raíz cuadrada (square root en inglés), y eso hicimos: le pasamos cómo parámetro 144, un número. Los
parámetros siempre se escriben entre paréntesis, a continuación del nombre de la función.
Ahora vamos a aprender la combinación de teclas más importante al usar RStudio: Ctrl + Enter. Presionar
Ctrl + Enter al terminar de escribir una instrucción hace que RStudio la ejecute de inmediato, y espere en
la siguiente instrucción, si la hubiera.
Cambien podemos buscar una línea que deseemos ejecutar, posicionando el cursor de texto (que luce como
una barra vertical que titila, en el panel de edición) sobre ella. Si a continuación pulsamos Ctrl + Enter,
la línea será ejecutada y el cursor se moverá sólo hasta la siguiente línea, listo para repetir el proceso.
La modalidad de ejecución línea por línea es muy útil para lo que se llama “análisis interactivo”. Uno ejecuta
un comando, observa el resultado, y en base a eso decide su próxima acción: cambiar parámetros e intentarlo
de nuevo, dar por buenos los resultados y usarlos para una tarea subsiguiente… etc.
Por ejemplo, si escribimos las siguientes líneas:
sqrt(144)
mensaje
…y posicionamos el cursor en cualquier posición de la primera línea, para luego pulsar Ctrl + Enter tres
veces, veremos que las instrucciones son ejecutadas línea a línea.
14 CHAPTER 2. UNA PRESENTACIÓN A TODA MARCHA DE R
sqrt(144)
## [1] 12
mensaje <- "Hola mundo"
mensaje
## [1] 48
… con
ancho_habitacion_m <- 8
profundiad_habitacion_m <- 6
superficie_habitacion_m2 <- ancho_habitacion_m * profundiad_habitacion_m
superficie_habitacion_m2
## [1] 48
En su resultado ambas expresiones son iguales, dado que producen lo mismo. Pero la segunda esta escrita de
una forma mucho más clara para un ser humano, que hace más fácil interpretar su lógica… ¡está calculando
la superficie en metros cuadrados de una habitación!. Es muy importante escribir nuestro código de la forma
más explícita posible, aunque requiera tipear un poco más. Con ello, le hacemos la vida más fácil a otras
personas que interpreten nuestros programas. Y también a nosotros mismos en el futuro, cuando debamos
lidiar con un programa que escribimos tiempo atrás y del que a duras penas recordamos su lógica.
A todo esto… ¿no se suponía que íbamos a investigar la mortalidad infantil en la Ciudad de Buenos Aires?.
Suficiente introducción… ¡allá vamos!
Vamos a cargar datos de mortalidad infantil, por comuna de la ciudad, en el año 2016, publicados por la
Dirección General de Estadística y Censos de Buenos Aires. El formato original de los datos es “.xls” (planilla
de hojas de cálculo). Yo lo he convertido a .csv (“comma separated values”) un formato muy popular en el
mundo de la ciencia de datos, ya que es muy fácil de manipular y compartir entre sistemas… es posible abrir
16 CHAPTER 2. UNA PRESENTACIÓN A TODA MARCHA DE R
un archivo .csv hasta con el humilde block de notas. Al igual que los archivos .xls, los .csv se utilizan para
guardar información tabular: un rectángulo con filas y columnas. R incluye una función que lee archivos
.csv, que se llama read.csv. La usamos así:
mortalidad <- read.csv('https://bitsandbricks.github.io/data/mortalidad_infantil_caba_2016.csv')
Obsérvese que los datos están alojados en un servidor de internet (accesibles vía https://bitsandbricks…).
Eso no es problema para la función read.csv, que con la misma soltura lee archivos guardados en nuestra PC
o publicados en un sitio web. Para ver el contenido de la variable donde guardamos el resultado de leer la
data, mortalidad, sólo hace falta escribir su nombre:
mortalidad
## Comuna Tasa2016
## 1 1 9.5
## 2 2 3.6
## 3 3 8.0
## 4 4 11.9
## 5 5 8.5
## 6 6 2.4
## 7 7 8.5
## 8 8 9.7
## 9 9 10.1
## 10 10 3.6
## 11 11 6.2
## 12 12 7.1
## 13 13 4.5
## 14 14 3.2
## 15 15 6.4
Vemos que la tabla tiene 15 columnas (una por cada comuna de la ciudad) y 7 columnas (una que indica la
comuna, y 6 con los valores de mortalidad para cada año entre 2010 y 2017).
En R, las tablas son llamadas dataframes. El dataframe es el objeto por excelencia del análisis de datos.
En concepto, es muy similar a una tabla de excel; al fin y al cabo, ambos formatos guardan información en
celdas identificadas por fila y columna.
Algunas funciones útiles para explorar un dataframe que no conocemos son dim(), que nos da las dimensiones
del dataframe (cantidad de filas y columnas), names() que nos dice como se llaman sus columnas (que en
general representan variables), y head() que nos permite echar un vistazo rápido al contenido, mostrando
sólo las seis primeras filas (ésto es útil porque con frecuencia trabajamos con dataframes que contienen miles
o millones de filas, con lo que no tiene sentido tratar de volcar todas en pantalla).
dim(mortalidad)
## [1] 15 2
names(mortalidad)
## Comuna Tasa2016
## 1 1 9.5
## 2 2 3.6
## 3 3 8.0
## 4 4 11.9
## 5 5 8.5
## 6 6 2.4
2.2. VISUALIZACIÓN: LA EXPLORACIÓN GRÁFICA DE LA INFORMACIÓN 17
Ahora es vamos a pisar el acelerador. Insisto: nadie debe preocuparse si algunos conceptos parecen ser
demasiado complejos. En las próximas secciones practicaremos de forma gradual las técnicas que vamos a
usar ahora, y todo tendrá sentido -¡lo prometo!. Pero antes, seamos un poquito irresponsables con el poder
de R y empleemos un arsenal sofisticado de herramientas para ver de que somos capaces.
Si acabamos de instalar R y RStudio, el paquete aún no estará disponible en nuestro sistema. Para instalarlo,
usamos la función install.packages() y le pasamos el nombre del paquete deseado, “tidyverse”, entre
comillas. Los dos comandos siguientes, que también debemos ejecutar, se aseguran de que dispongamos
de ciertas funciones del paquete que son “experimentales” (es decir, en fase de prueba) pero que queremos
agregar porque las vamos a usar en breve. La instalación puede tomar unos minutos, así que tengamos
paciencia.
install.packages("tidyverse")
install.packages("devtools")
devtools::install_github("tidyverse/ggplot2")
De aquí en más, podremos activar el conjunto de funciones que provee tidyverse cada vez que queramos.
Para eso, lo invocamos con la función library():
library(tidyverse)
… y listo para usar. La razón por la cual activamos tidyverse es que en este momento nos vienen bien dos
de sus funciones: mutate() para modificar valores, y ggplot() para hacer gráficos.
Bien, llega la hora de los gráficos. Vamos a llamar a la función ggplot(), una auténtica navaja suiza para
la visualización.
Por ejemplo, veamos a cuanto asciende la tasa de mortalidad infantil en cada comuna durante 2016:
ggplot(mortalidad) +
geom_col(aes(x = factor(Comuna), y = Tasa2016))
18 CHAPTER 2. UNA PRESENTACIÓN A TODA MARCHA DE R
10.0
7.5
Tasa2016
5.0
2.5
0.0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
factor(Comuna)
Para realizar una visualización con ésta herramienta, siempre se comienza con la función ggplot(), que crea
un eje de coordenadas sobre el cual se pueden agregar capas. El primer parámetro que recibe ggplot()
es el dataset que queremos usar para el gráfico; en nuestro caso, ggplot(mortalidad). Ejecutar sólo gg-
plot(mortalidad) nos devuelve un gráfico vacío; la gracia está en agregar una o más capas especificando
cómo queremos mostrar los datos. Estas capas se agregan con un signo +.
En nuestro ejemplo, geom_col() crea columnas cuya posición en el eje de las x depende de la variable
“Comuna”, mientas que la altura (posición en el eje de las y) depende del valor de la variable “Tasa2016”.
Existen muchas funciones de tipo “geom_XXX”, que agregan distintas clases de capas al gráfico: geom_point,
geom_polygon, geom_text y muchos, muchos más que iremos viendo más adelante.
Cada función “geom_” toma como parámetro un conjunto de definiciones “estéticas” que le indican una
variable a graficar (“mortalidad” en nuestro caso), cómo (color, tamaño, etc) y dónde (posición x, posición
y del eje). Estos parámetros van siempre dentro de una función auxiliar, aes(). En nuestro ejemplo,
“geom_line(aes(x = año, y = mortalidad, group = Comuna, color = factor(Comuna)))”.
No se preocupen que iremos practicando el uso de ggplot, y su uso se volverá familiar.
En cuanto al gráfico que hemos creado, podemos observar que entre las 15 comunas en la ciudad, la tasa de
mortalidad tiene un rango que va de un poco menos de 2,5 a un poco más de 12,5 (esto es, muertes antes
del año de vida por cada 10.000 nacimientos).
Pero no se distingue aquello que queríamos comprender: la diferencia entre el norte y el sur de la ciudad.
Necesitamos contexto geográfico.
Vamos a presentar un paquete más, el último para éste capítulo: sf. Quizás algunos tengan experiencia con
sistemas de información geográfica (GIS por sus siglas en inglés), al estilo de QGIS o ArcGIS, que permiten
2.2. VISUALIZACIÓN: LA EXPLORACIÓN GRÁFICA DE LA INFORMACIÓN 19
crear, manipular y combinar archivos con datos espaciales para producir mapas que pueden ser simples o en
extremo sofisticados. En R, el paquete sf brinda herramientas que permiten realizar tares similares.
Nuestro objetivo es obtener un mapa de la ciudad de Buenos Aires con sus comunas.
Primero, instalamos sf en caso de que aún no lo hayamos hecho. Sólo es necesario hacerlo una vez:
install.packages("sf")
Y cargamos un archivo georeferenciado con las comunas de la Ciudad Autónoma de Buenos Aires, disponible
online en formato geojson, un estándar de representación de datos geográficos que es fácil de usar:
comunas <- st_read('https://bitsandbricks.github.io/data/CABA_comunas.geojson')
## [1] 15 5
names(comunas)
Podemos ver que el dataframe contiene 15 filas y 5 columnas. Una fila por comuna (es razonable!) y
5 columnas: “barrios”, “perímetro”, “area”, “comunas” y “geometry”. Nuestro vistazo mediante head()
permite asumir que “barrios” informa los barrios que componen cada comuna, mientras que perímetro y área
informan sobre las dimensiones del polígono cubierto por cada comuna. La columna “geometry” aparece en
todos los dataframes de tipo espacial, y es la que contiene los datos con sus coordenadas geográficas.
Y hablando de coordenadas, generar un mapa a partir de un dataframe espacial creado por sf es muy fácil
con la ayuda de ggplot
ggplot(comunas) +
geom_sf()
34.55°S
34.6°S
34.65°S
34.7°S
Si queremos agregar una leyenda al mapa que identifique cada comuna con su número, usamos:
ggplot(comunas) +
geom_sf(aes(fill = comunas))
2.2. VISUALIZACIÓN: LA EXPLORACIÓN GRÁFICA DE LA INFORMACIÓN 21
comunas
1
34.55°S
10
11
12
13
34.6°S 14
15
2
3
4
34.65°S
5
6
7
8
34.7°S 9
Dentro de “aes()” usé el parámetro “fill” (relleno en inglés) para pedirle a ggplot que llene cada polígono
con un color distinto de acuerdo al campo “comunas”.
Aprovechando que tenemos un mapa, deberíamos clasificar las comunas entre las que pertenecen al norte
y las que pertenecen al sur de la ciudad. No hay una línea divisoria oficial, pero la traza de la Avenida
Rivadavia suele ser tomada como frontera: Rivadavia es la “divisoria simbólica del Norte y el Sur de la
Ciudad, con sus diferencias de desarrollo”
Por esas casualidades de la vida, tengo un archivo geográfico que contiene la línea que dibuja a avenida
Rivadavia al atravesar la ciudad. Lo bajamos:
rivadavia <- st_read('https://bitsandbricks.github.io/data/avenida_rivadavia.geojson')
Y lo proyectamos sobre el mapa, como una capa adicional del gráfico de ggplot que definimos antes:
ggplot(comunas) +
geom_sf(aes(fill = comunas)) +
geom_sf(data = rivadavia, color = "red")
22 CHAPTER 2. UNA PRESENTACIÓN A TODA MARCHA DE R
comunas
1
34.55°S
10
11
12
13
34.6°S 14
15
2
3
4
34.65°S
5
6
7
8
34.7°S 9
La identificación por colores no hace fácil reconocer con rapidez que número corresponde a cada comuna;
es un recurso que funciona mejor con menos categorías que nuestras 15. Podríamos arreglarlo, por ejemplo
evitando la codificación por color, y dibujando una etiqueta con número dibujada sobre cada comuna. ¡Pero
no en este momento! En aras de la sencillez, vamos a aguzar la vista y tomar nota de cuales comunas tienen
gran parte de su territorio al sur de la Avenida Rivadavia. Según mi interpretación, son las comunas 1, 3, 4,
5, 7, 8 y 9. (Hay que admitir que la comuna 1 parece estar repartida en partes más o menos iguales, pero
vamos a dejársela al sur en forma arbitraria para no complicar el ejercicio).
En este punto necesitamos una manera de “etiquetar” cada comuna con el punto cardinal que le toca “Norte”
o “Sur”. La forma más rápida es crear una lista con los atributos, y agregarla a nuestro dataframe como una
nueva columna.
Podemos armar una sucesión de 15 “etiquetas” según el punto cardinal que le toca a cada comuna. El
comando en R que “une” valores en conjunto se llama c() (viene de “combine”, “combinar”), y permite
definir una lista de valores. Mejor dicho, un “vector” de valores; en el mundo de la programación, se usa
la palabra vector cuando se combinan elementos del mismo tipo, y “lista” cuando se combina una variedad
de clases: en el mismo conjunto números, textos, y otros tipos de objeto más complejos. Por ahora, no nos
preocupemos por eso.
nueva_columna <- c("Sur", "Norte", "Sur", "Sur", "Sur", "Norte", "Sur", "Sur", "Sur", "Norte", "Norte",
nueva_columna
Ya podemos agregar nuestra nueva columna usando una función que ya vimos, mutate(). En el dataframe,
vamos a ponerle a nuestra nueva columna un nombre descriptivo, “ubicación” :
comunas <- mutate(comunas, ubicacion = nueva_columna)
Verifiquemos el resultado:
head(comunas)
Y en el mapa:
ggplot(comunas) +
geom_sf(aes(fill = ubicacion)) +
geom_sf(data = rivadavia, color = "red")
24 CHAPTER 2. UNA PRESENTACIÓN A TODA MARCHA DE R
34.55°S
34.6°S
ubicacion
Norte
Sur
34.65°S
34.7°S
Todo en orden. Ahora hagamos lo mismo con el dataframe de mortalidad, aprovechando que lista las comunas
en el mismo orden (del 1 al 15) y por lo tanto podemos “pegarle” el mismo vector de etiquetas con ubicación
que ya preparamos.
mortalidad <- mutate(mortalidad, ubicación = nueva_columna)
head(mortalidad)
depende del valor de la mortalidad. Le sumamos una capa con la traza de la Avenida Rivadavia, nuestra
referencia de posición, y por último definimos la paleta de colores a usar en el fill, eligiendo una llamada
“Spectral”, que va del azul al rojo y es muy usada cuando se quiere resaltar la divergencia de una variable.
ggplot(comunas) +
geom_sf(aes(fill = mortalidad$Tasa2016)) +
geom_sf(data = rivadavia, color = "red") +
scale_fill_distiller(palette = "Spectral")
34.55°S
mortalidad$Tasa2016
34.6°S
10.0
7.5
5.0
2.5
34.65°S
34.7°S
Para una comparación visual más precisa entre los valores de cada comuna, le pedimos a ggplot un gráfico
de barras, con la capa geom_col(). En las variables estéticas, definimos que la posición de las barras en el
eje de las x estará dada por el número de cada comuna, la altura de las barras (eje y) será dada por su tasa
de mortalidad, y su color de relleno (fill) dependerá de su ubicación geográfica.
ggplot(mortalidad) +
geom_col(aes(x = Comuna, y = Tasa2016, fill = ubicación)) +
labs(title = "Mortalidad infantil en la Ciudad Autónoma de Buenos Aires",
subtitle = "Año 2016",
y = "tasa")
26 CHAPTER 2. UNA PRESENTACIÓN A TODA MARCHA DE R
10.0
7.5
ubicación
tasa
Norte
5.0 Sur
2.5
0.0
0 5 10 15
Comuna
Por último, usamos a R de calculadora. Separamos las comunas en dos dataframes distintos según su
ubicación, con el comando filter()…
comunas_al_sur <- filter(mortalidad, ubicación == "Sur")
## [1] 2.044788
En los siguientes capítulos practicaremos varias técnicas que nos permitirán profundizar nuestros análisis,
en la nunca finalizada misión de entender un poco más.
28 CHAPTER 2. UNA PRESENTACIÓN A TODA MARCHA DE R
Chapter 3
Cómo ya hemos mencionado, es normal que la mayor parte del tiempo dedicado a un proyecto de análisis se
nos vaya en la limpieza y orden de los datos disponibles. Aún cuando nuestros datos provengan de fuentes
oficiales (un gobierno nacional, el Banco Mundial, etc) en muy rara ocasión podremos usarlos para nuestros
fines sin antes procesarlos. Y aún si los datos llegaran en perfectas condiciones, no tenemos forma de saberlo
hasta haber realizado una exploración para verificarlo.
Ésta inevitable etapa de preparación es llamada data wrangling en inglés, algo así como el proceso de “domar
los datos”. El término hace referencia, en clave de humor, al esfuerzo que requiere la puesta en orden cuando
los datos son cuantiosos, de muchas fuentes distintas, o en particular desprolijos. Para que la experiencia sea
lo menos tediosa posible, y podamos pasar rápido al momento de extraer conocimiento, vamos a practicar
algunas técnicas muy útiles de wrangling.
Vamos a practicar usando los registros del Sistema Único de Atención Ciudadana (SUACI) de la Ciudad
Autónoma de Buenos Aires. El SUACI es el repositorio donde se integran las solicitudes y reclamos que los
ciudadanos presentan a la ciudad por distintos canales: en persona, por teléfono o usando la aplicación BA
147. Vamos a trabajar con una versión de los datos que ha sido simplificada para hacer más ameno el trabajo
con ella. Quién quiera acceder a los datos en su esplendor de complejidad original, puede encontrarlos en el
portal de datos abiertos de la ciudad: https://data.buenosaires.gob.ar/
Comenzamos por acceder al archivo con los registros para cargarlo en R como un dataframe. Tendremos
que ejercitar un poco la paciencia porque es un archivo de varios megas, que podría tardar unos minutos en
ser descargado.
atencion_ciudadano <- read.csv("http://bitsandbricks.github.io/data/gcba_suaci_barrios.csv")
Lo primero que deberíamos hacer con un dataframe que no conocemos es usar la función str(), que nos
indica su estructura (por structure en inglés):
str(atencion_ciudadano)
29
30 CHAPTER 3. PONIENDO LOS DATOS EN FORMA
Para empezar, nos enteramos que el objeto que estamos analizando es un dataframe (“data.frame”). Eso
ya lo sabíamos, pero como str() puede usarse con cualquier clase de objeto en R, en ocasiones resultará que
estamos ante un vector, una lista u otra clase de criatura. A continuación aparecen las dimensiones del
dataframe: 57.432 observaciones (filas) con 5 variables (columnas). Los nombres de las columnas son PERI-
ODO, RUBRO, TIPO_PRESTACION, BARRIO y total. Con eso ya podemos inferir que cada observación
en el dataframe contiene la cantidad total de solicitudes según concepto, rubro y tipo de prestación (aunque
no sepamos bien de que se tratan esas variables), en un período dado y en cada barrio.
Con str() también obtenemos el tipo de datos representados pro cada variable, y un ejemplo de los valores
contenidos en las primeras filas. PERIODO y total son variables de tipo “int”, es decir, números enteros o in-
tegers en inglés. El resto de las variables son de tipo “Factor”; en R las variables categóricas reciben el nombre
de factores. ¿Y cómo sabe R que RUBRO o BARRIO son categorías? La culpable es la función read.csv()
que usamos al principio. Si no se le aclara lo contrario, read.csv() interpreta como factores a todas las colum-
nas que contienen texto. Para avisarle que no lo haga, hay que usar el parámetro stringsAsFactors, así:
misdatos <- read.csv("archivo_con_mis_datos", stringsAsFactors = FALSE). En general es buena
idea evitar que los campos de texto se asuman como factores, pero en éste caso está bien: todas las columnas
de texto, en efecto, contienen variables categóricas. (Ante la duda, una variable es categórica cuando es
razonable considerar que se elige entre un conjunto finito de variables posibles; por ejemplo, los barrios de
Buenos Aires son un conjunto finito y predeterminado).
La siguiente función a utilizar cuando estamos conociendo el contenido de un set de datos es summary(), que
nos dará un resumen en forma de estadísticas descriptivas para las variables numéricas (cuartiles y mediana)
y un vistazo a las categorías más representadas par los factores.
summary(atencion_ciudadano)
echamos un vistazo, comprobando que existe BARRIOS, una columna en común que lo relaciona con el
dataframe de atención al ciudadano,
barrios_comunas
## BARRIO COMUNA
## 1 AGRONOMIA 15
## 2 ALMAGRO 5
## 3 BALVANERA 3
## 4 BARRACAS 4
## 5 BELGRANO 13
32 CHAPTER 3. PONIENDO LOS DATOS EN FORMA
## 6 BOCA 4
## 7 BOEDO 5
## 8 CABALLITO 6
## 9 CHACARITA 15
## 10 COGHLAN 12
## 11 COLEGIALES 13
## 12 CONSTITUCION 1
## 13 FLORES 7
## 14 FLORESTA 10
## 15 LINIERS 9
## 16 MATADEROS 9
## 17 MONSERRAT 1
## 18 MONTE CASTRO 10
## 19 NUEVA POMPEYA 4
## 20 NUÑEZ 13
## 21 PALERMO 14
## 22 PARQUE AVELLANEDA 9
## 23 PARQUE CHACABUCO 7
## 24 PARQUE CHAS 15
## 25 PARQUE PATRICIOS 4
## 26 PATERNAL 15
## 27 PUERTO MADERO 1
## 28 RECOLETA 2
## 29 RETIRO 1
## 30 SAAVEDRA 12
## 31 SAN CRISTOBAL 3
## 32 SAN NICOLAS 1
## 33 SAN TELMO 1
## 34 VELEZ SARSFIELD 10
## 35 VERSALLES 10
## 36 VILLA CRESPO 15
## 37 VILLA DEL PARQUE 11
## 38 VILLA DEVOTO 11
## 39 VILLA GRAL. MITRE 11
## 40 VILLA LUGANO 8
## 41 VILLA LURO 10
## 42 VILLA ORTUZAR 15
## 43 VILLA PUEYRREDON 12
## 44 VILLA REAL 10
## 45 VILLA RIACHUELO 8
## 46 VILLA SANTA RITA 11
## 47 VILLA SOLDATI 8
## 48 VILLA URQUIZA 12
y lo unimos (de allí el término “join”, unir en inglés) a nuestra data:
atencion_ciudadano <- left_join(atencion_ciudadano, barrios_comunas)
Podemos seguir siempre ese formato para guardar nuestros datos. El primer parámetro es el dataframe que
vamos a guardar, el segundo -siempre entre comillas- es el nombre de archivo, y la opcion final, row.names
= FALSE sirve para evitar que R le agregue una columna al principio con numeros consecutivos (1, 2, 3, y
así), cosa que quizás fue útil alguna vez pero en general no necesitamos.
Para volver a leer los datos en otra ocasión, usamos read.csv() tal como ya hemos hecho.
atencion_ciudadano <- read.csv("atencion_ciudadano.csv")
Y si queremos saber exactamente dónde ha guardado R nuestros datos, por ejemplo para abrirlos con otro
programa, usamos la función getwd (por get working directory )
getwd()
Habiendo revisado el contenido de un dataframe (y agregado alguna variable si hiciera falta), comenzamos
a hacernos idea de los ajustes que necesita para que los datos tomen el formato que necesitamos. Estos
ajustes pueden ser correcciones (por ejemplo, de errores de tipeo cuando se cargaron los datos), la creación
de nuevas variables derivadas de las existentes, o un reordenamiento de los datos para simplificar nuestro
trabajo.
Para hacer todo esto, y mucho más, vamos a aprender funciones que representan cinco verbos básicos para
la transformación de datos:
• select(): seleccionar -elegir- columnas por su nombre
• filter(): filtrar, es decir quedarse sólo con las filas que cumplan cierta condición
• arrange(): ordenar las filas de acuerdo a su contenido o algún otro índice
• mutate(): mutar -cambiar- un dataframe, modificando el contenido de sus columnas o creando colum-
nas (es decir, variables) nuevas
• summarise(): producir sumarios -un valor extraído de muchos, por ejemplo el promedio- con el con-
tenido de las columnas
Estas funciones tienen una sintaxis, una forma de escribirse, uniforme. El primer argumento que toman
siempre es un dataframe; los siguientes indican qué hacer con los datos. El resultado siempre es un nuevo
dataframe.
Las funciones son parte de dplyr, uno de los paquetes de funciones de la familia Tidyverse. Si no lo hicimos
aún en la sesión en la que estamos trabajando, cargamos tidyverse.
34 CHAPTER 3. PONIENDO LOS DATOS EN FORMA
library(tidyverse)
Muchas veces tendremos que lidiar con datasets con decenas de variables. Alguna que otra vez, con centenas.
En esos casos el primer problema es librarnos de semejante cantidad de columnas, reteniendo sólo aquellas
en las que estamos interesados. Para un dataset como el de reclamos de los ciudadanos, que tiene pocas
columnas, select() no es tan importante. Aún así, podemos usar select() con fines demostrativos.
Sabemos que el dataset tiene 5 columnas:
names(atencion_ciudadano)
head(seleccion)
## PERIODO total
## 1 201301 6
## 2 201301 172
## 3 201301 92
## 4 201301 45
## 5 201301 79
## 6 201301 10
También podemos seleccionar por contigüidad, por ejemplo “todas las columnas que van de RUBRO a
BARRIO”:
seleccion <- select(atencion_ciudadano, RUBRO:BARRIO)
head(seleccion)
head(seleccion)
head(seleccion)
head(seleccion)
Una de las tareas más frecuentes en el análisis de datos es la de identificar observaciones que cumplen con
determinada condición. filter() permite extraer subconjuntos del total en base a sus variables.
Por ejemplo, para seleccionar registros que correspondan a Retiro, ocurridos en el primer mes de 2014
(período 201401):
seleccion <- filter(atencion_ciudadano, BARRIO == "RETIRO", PERIODO == 201401)
head(seleccion)
3.3.2.1 Comparaciones
Aquí hemos usado un recurso nuevo, la comparación. R provee una serie de símbolos que permite comparar
valores entre sí:
* `==` igual a
* `!=` no igual a
36 CHAPTER 3. PONIENDO LOS DATOS EN FORMA
* `>` mayor a
* `>=` mayor o igual a
* `<` menor a
* `<=` menor o igual a
Atención especial merece el símbolo que compara igualdad, ==. Un error muy común es escribir BARRIO
= "RETIRO", (un sólo símbolo =) que le indica a R que guarde el valor “RETIRO” dentro de la variable
BARRIO, en lugar de verificar si son iguales. Para ésto último, lo correcto es BARRIO == "RETIRO", tal
como lo usamos en el ejemplo de filter().
También hay que tener en cuenta el uso de comillas. Para que R no se confunda, cuando queramos usar
valores de texto (de tipo character) los rodeamos con comillas para que quede claro que no nos referimos a
una variable con ese nombre, si la hubiera, sino en forma literal a esa palabra o secuencia de texto. En el
caso de los números, no hace falta el uso de comillas, ya que en R ningún nombre de variable puede comenzar
con o estar compuesta sólo por números.
Filtrando los registros de períodos para los cuales se registran más de 100 incidentes:
seleccion <- filter(atencion_ciudadano, total > 100)
head(seleccion)
Cuando le pasamos múltiples condiciones a filter(), la función devuelve las filas que cumplen con todas.
Por ejemplo, con
seleccion <- filter(atencion_ciudadano, PERIODO == 201508, RUBRO == "SALUD")
head(seleccion)
head(seleccion)
head(seleccion)
Y como ejemplo de !(a & b), todas las filas excepto las de tipo “DENUNCIA”, y rubro “SEGURIDAD E
HIGIENE”:
seleccion <- filter(atencion_ciudadano, !(TIPO_PRESTACION == "DENUNCIA" & RUBRO == "SEGURIDAD E HIGIENE"
head(seleccion)
La función arrange() cambia el orden en el que aparecen las filas de un dataframe. Como primer parámetro
toma un dataframe, al igual que el resto de los verbos de transformación que estamos aprendiendo. A
continuación, espera un set de columnas para definir el orden.
Por ejemplo, para ordenar por total de registros:
ordenado <- arrange(atencion_ciudadano, total)
head(ordenado)
head(ordenado)
head(ordenado)
En el último ejemplo, aparecen varias filas cuyo valor para la columna BARRIO es NA. R representa los
valores ausentes, desconocidos, con NA (“no disponible”, del inglés Not Available). Hay que tener cuidado
con los valores NA, porque la mayoría de las comparaciones y operaciones lógicas que los involucran resultan
indefinidas. En la práctica:
¿Es 10 mayor a un valor desconocido?
10 > NA
## [1] NA
R no sabe. (Nadie lo sabe, para ser justos)
¿A cuanto asciende la suma de 10 más un valor desconocido?
NA + 10
## [1] NA
Y en particular… ¿es un valor desconocido igual a otro valor desconocido?
3.3. TRANSFORMANDO LOS DATOS 39
NA == NA
## [1] NA
Por supuesto, la respuesta es desconocida también. La insistencia de R en no definir operaciones que
involucran NA’s podría parecer irritante a primera vista, pero en realidad nos hace un favor. Al evitar
extraer conclusiones cuando trata con datos faltantes, nos evita caer en errores garrafales en los casos en que
analizamos y comparamos datos incompletos. Además, podemos preguntar a R si un valor es desconocido,
y allí si contesta con seguridad. La función requerida es is.na().
desconocido <- NA
is.na(desconocido)
## [1] TRUE
Algo más a tener en cuenta con los valores desconocidos es cómo son interpretados cuando usamos funciones
de transformación de datos. Por ejemplo, filter() ignora las filas que contienen NA’s en la variable que
usa para filtrar. arrange() muestra las filas con NA’s en el campo por el que ordena, pero todas al final.
Recurrimos a la función mutate() cuando queremos agregarle columnas adicionales a nuestro dataframe,
en general en base a los valores de las columnas ya existentes. Vamos a ilustrarlo con un ejemplo sencillo.
Imaginemos que tenemos el siguiente dataset:
circulos <- data.frame(nombre = c("Círculo 1", "Círculo 2", "Círculo 3"),
tamaño = c("Pequeño", "Mediano", "Grande"),
radio = c(1, 3, 5))
circulos
head(atencion_ciudadano)
Llegamos al último de los verbos fundamentales para transformar datos. summarise() (por “resumir” en
inglés) toma un dataframe completo y lo resume un una sola fila, de acuerdo a la operación que indiquemos.
Por ejemplo, el promedio de la columna “total”:
summarise(atencion_ciudadano, promedio = mean(total))
## promedio
## 1 34.8478
Por si sola, summarise() no es de mucha ayuda. La gracia está en combinarla con group_by(), que cambia
la unidad de análisis del dataframe completo a grupos individuales. Usar summarise() sobre un dataframe
al que antes agrupamos con group_by resulta en resúmenes “por grupo”.
agrupado <- group_by(atencion_ciudadano, AÑO)
## # A tibble: 3 x 2
## AÑO promedio_totales
## <chr> <dbl>
## 1 2013 29.5
## 2 2014 30.2
## 3 2015 45.4
Podemos agrupar por múltiples columnas, generando más subgrupos; por ejemplo, promedios por por año y
mes…
agrupado <- group_by(atencion_ciudadano, AÑO, MES)
head(sumario)
## # A tibble: 6 x 3
## # Groups: AÑO [1]
## AÑO MES promedio
## <chr> <chr> <dbl>
## 1 2013 01 25.1
## 2 2013 02 26.1
## 3 2013 03 26.9
## 4 2013 04 29.5
## 5 2013 05 28.0
3.3. TRANSFORMANDO LOS DATOS 41
## 6 2013 06 28.9
… o por año, mes y barrio:
agrupado <- group_by(atencion_ciudadano, AÑO, MES, BARRIO)
head(sumario)
## # A tibble: 6 x 4
## # Groups: AÑO, MES [1]
## AÑO MES BARRIO promedio
## <chr> <chr> <fct> <dbl>
## 1 2013 01 AGRONOMIA 14.6
## 2 2013 01 ALMAGRO 29.5
## 3 2013 01 BALVANERA 23.6
## 4 2013 01 BARRACAS 19.4
## 5 2013 01 BELGRANO 24.4
## 6 2013 01 BOCA 9.97
Con summarise() podemos usar cualquier función que tome una lista de valores y devuelva un sólo resutado.
Para empezar, algunas de las que más podrian ayudarnos son:
* `mean()`: Obtiene el promedio de los valores
* `sum()`: Obtiene la suma
* `min()`: Obtiene el valor más bajo
* `max()`: Obtiene el valor más alto
Antes de terminar, vamos a presentar una herramienta más: el operador pipe (pronúnciese “paip”, es el
término en inglés que significa “tubo”).
El pipe es un operador: un símbolo que relaciona dos entidades. Dicho en forma más simple, el pipe de R,
cuyo símbolo es %>% está en familia con otros operadores más convencionales, como +, - o /. Y al igual que
los otros operadores, entrega un resultado en base a los operandos que recibe. Ahora bien… ¿Para qué sirve?
En resumidas cuentas, hace que el código necesario para realizar una serie de operaciones de transformación
de datos sea mucho más simple de escribir y de interpretar.
Por ejemplo, si quisiéramos obtener el top 5 de los barrios que más reclamos y denuncias de los ciudadanos
han registrado durante 2015, la forma de lograrlo en base a lo que ya sabemos sería así:
1. Filtramos los datos para aislar los registros del 2014;
2. agrupamos por Barrio;
3. hacemos un sumario, creando una variable resumen que contiene la suma de los registros para cada barr
4. los ordenamos en forma descendiente,
5. mostramos sólo los primeros 5 (esto se puede hacer con la función `head()`, aclarando cuantas filas q
En código:
solo2014 <- filter(atencion_ciudadano, AÑO == 2014)
head(total_por_barrio_2014_ordenado, 5)
## # A tibble: 5 x 2
## BARRIO total
## <fct> <int>
## 1 SAN NICOLAS 180956
## 2 PALERMO 22569
## 3 CABALLITO 19706
## 4 FLORES 15919
## 5 VILLA DEVOTO 15720
¡Funciona! Pero… el problema es que hemos generado un puñado de variables (“solo2014”, “solo2014_agrupado_barrio”,
etc) que, es probable, no volveremos a usar. Además de ser inútiles una vez obtenido el resultado buscado,
estas variables intermedias requieren que las nombremos. Decidir el nombre de estas variables que no nos
importan toma tiempo (sobre todo cuando producimos muchas), y nos distrae de lo importante, que es el
análisis.
El pipe, %>%, permite encadenar operaciones, conectando el resultado de una como el dato de entrada de la
siguiente. La misma secuencia que realizamos antes puede resolverse con pipes, quedando así:
atencion_ciudadano %>%
filter(AÑO == 2014) %>%
group_by(BARRIO) %>%
summarise(total = sum(total)) %>%
arrange(desc(total)) %>%
head(5)
## # A tibble: 5 x 2
## BARRIO total
## <fct> <int>
## 1 SAN NICOLAS 180956
## 2 PALERMO 22569
## 3 CABALLITO 19706
## 4 FLORES 15919
## 5 VILLA DEVOTO 15720
Una manera de pronunciar %>% cuando leemos código es “y luego…”. Algo así como “tomamos el
dataframe”atencion_ciudadano” y luego filtramos los registros del año 2014, y luego agrupamos por barrio,
y luego calculamos el total de registros para cada grupo, y luego los ordenamos en forma descendente por
total, y luego vemos los cinco primeros“.
El uso de pipes permite concentrarse en las operaciones de transformación, y no en lo que está siendo
transformado en cada paso. Esto hace al código mucho más sencillo de leer e interpretar. En el ejemplo con
pipe, sólo tuvimos que nombrar un dataframe con el cual trabajar un única vez, al principio.
Detrás de escena, x %>% f(y) se transforma en f(x, y). Por eso,
filter(atencion_ciudadano, AÑO == 2014)
es equivalente a
atencion_ciudadano %>% filter(AÑO == 2014)
Trabajar con pipes es una de las ventajas que hacen de R un lenguaje muy expresivo y cómodo para manipular
datos, y a partir de aquí lo usaremos de forma habitual.
Con esto cerramos la sección de transformación de datos. Las técnicas para examinar un dataframe, como
sumamry() nos permiten entender de forma rápida con que clase de variables vamos a trabajar. Los cinco
3.3. TRANSFORMANDO LOS DATOS 43
verbos de manipulación que aprendimos, usados en conjunto, brindan una enorme capacidad para adaptar
el formato de los datos a nuestras necesidades. Y el operador pipe nos ayuda a escribir nuestro código de
forma sucinta y fácil de interpretar.
A medida que vayamos progresando en nuestra familiaridad con las funciones -y agregando técnicas nuevas-
vamos a ser capaces de procesar grandes cantidades de datos con soltura. Y obtener en pocos minutos lo
que de otra forma, sin herramientas computacionales, tardaría días o sería inviable por lo tedioso.
44 CHAPTER 3. PONIENDO LOS DATOS EN FORMA
Chapter 4
Visualización
La visualización de información es una de las técnica más poderosas, y a la vez más accesibles, de las que
disponemos como analistas de datos. La visualización es el proceso de hacer visibles los contrastes, ritmos y
eventos que los datos expresan, que no podemos percibir cuando vienen en forma de áridas listas de números
y categorías.
Vamos a aprender a realizar las visualizaciones más usadas, y las opciones de ajuste con las que podemos
lograr que luzcan tal como queremos.
Usando los verbos de transformación que aprendimos, es fácil obtener un dataframe resumen con los totales
anuales por comuna. Vamos a expresar los totales en miles de contactos, para evitar trabajar con números
tan grandes.
contactos_por_comuna <- atencion_ciudadano %>%
group_by(COMUNA) %>%
summarise(miles_contactos = sum(total) / 1000 )
contactos_por_comuna
## # A tibble: 16 x 2
## COMUNA miles_contactos
## <int> <dbl>
## 1 1 685
## 2 2 48.8
45
46 CHAPTER 4. VISUALIZACIÓN
## 3 3 71.2
## 4 4 94.5
## 5 5 77.1
## 6 6 82.8
## 7 7 95.8
## 8 8 61.3
## 9 9 91.2
## 10 10 114
## 11 11 139
## 12 12 123
## 13 13 106
## 14 14 99.3
## 15 15 107
## 16 NA 5.78
Lo que nos falta ahora es la cantidad de habitantes en cada comuna. No problem. El dato es fácil de
conseguir, otra vez cortesía de la Dirección General de Estadística y Censos de la Ciudad de Buenos Aires.
Traemos la proyección al año 2017 de la cantidad de habitantes por comuna.
habitantes <- read.csv("http://bitsandbricks.github.io/data/gcba_pob_comunas_17.csv")
habitantes
## COMUNA POBLACION
## 1 1 253271
## 2 2 149720
## 3 3 192763
## 4 4 238809
## 5 5 186956
## 6 6 184846
## 7 7 240607
## 8 8 226649
## 9 9 170605
## 10 10 170282
## 11 11 189986
## 12 12 213914
## 13 13 235967
## 14 14 226944
## 15 15 182409
Por suerte, ya sabemos como combinar tablas usando left_join()
contactos_por_comuna <- contactos_por_comuna %>% left_join(habitantes)
contactos_por_comuna
## # A tibble: 16 x 3
## COMUNA miles_contactos POBLACION
## <int> <dbl> <int>
## 1 1 685 253271
## 2 2 48.8 149720
## 3 3 71.2 192763
## 4 4 94.5 238809
## 5 5 77.1 186956
## 6 6 82.8 184846
## 7 7 95.8 240607
## 8 8 61.3 226649
4.1. UNA BUENA VISUALIZACIÓN PARA EMPEZAR: EL SCATTERPLOT 47
## 9 9 91.2 170605
## 10 10 114 170282
## 11 11 139 189986
## 12 12 123 213914
## 13 13 106 235967
## 14 14 99.3 226944
## 15 15 107 182409
## 16 NA 5.78 NA
!Preparativos terminados! Hagamos por fin nuestro scatterplot. Tal como en el capítulo de introducción a
R, continuaremos usando ggplot() para visualizar:
ggplot(contactos_por_comuna)
¿Un gráfico vacío? Recordemos que ggplot funciona por capas. Primero uno declara el dataframe que va a
usar, y luego agrega una o más capas con representaciones de la información. La forma de agregar una capa
con un scatterplot, en la práctica dibujar puntos, es con geom_point:
ggplot(contactos_por_comuna) + geom_point(aes(x = POBLACION, y = miles_contactos))
48 CHAPTER 4. VISUALIZACIÓN
600
miles_contactos
400
200
Lo que hicimos fue pedirle a ggplot que dibuje un punto por cada fila (representando a cada comuna),
con la posición en el eje de las x según su población, y en el eje de las y según la cantidad de contactos
registrados. Estas referencias estéticas (aesthetics en inglés) son las que van dentro de la función aes() en
geom_point(aes(x = POBLACION, y = miles_contactos))
Primer sorpresa: ¡en el extremo superior derecho hay una comuna que se sale de la norma! Su relación
población/reclamos es muy diferente a la de todas las demás. Podemos identificarla, pidiendo a ggplot que
agregue una variable más a la visualización -la comuna. Siendo un gráfico en dos dimensiones, ya no podemos
usar la posición para representar un valor; tanto la posición horizontal como la vertical están siendo usadas
por población y total. Nuestras opciones son codificar la comuna por color, forma o tamaño del punto. A
pesar de que son identificadas con números, las comunas son una variable categórica: no tiene sentido decir
que la comuna 1 es “menor” que la comuna 7. Par las variables categóricas, el color suele ser una buena
opción de codificación.
Lo hacemos agregando un parámetro color dentro de aes(). Tal como hicimos en el capítulo 2, usamos
factor(COMUNA) en lugar de COMUNA a secas para indicarle a R que queremos que trate a la variable como
categórica:
ggplot(contactos_por_comuna) +
geom_point(aes(x = POBLACION, y = miles_contactos, color = factor(COMUNA)))
4.1. UNA BUENA VISUALIZACIÓN PARA EMPEZAR: EL SCATTERPLOT 49
factor(COMUNA)
1
2
600
3
4
5
miles_contactos
6
400 7
8
9
10
11
200
12
13
14
15
0 NA
150000 175000 200000 225000 250000
POBLACION
En ese caso, no es tan fácil discernir cuál es cuál, pero mirando con cuidado descubrimos que la comuna 1
es el outlier, el valor fuera de lo común. Lo que nos pasa aquí es que tenemos demasiadas categorías, con lo
cual cada una tiene su propio color pero el rango cromático no alcanza para darle a cada una un tono bien
distinto al de las demás.
Si necesitamos generar un gráfico que no deje lugar a dudas, lo resolvemos usando un método alternativo
para el scatterplot. En lugar de dibujar puntos, podemos poner etiquetas con el nombre de cada comuna.
En lugar de
ggplot(contactos_por_comuna) +
geom_point(aes(x = POBLACION, y = miles_contactos, color = factor(COMUNA)))
usamos
ggplot(contactos_por_comuna) +
geom_label(aes(x = POBLACION, y = miles_contactos, label = factor(COMUNA)))
50 CHAPTER 4. VISUALIZACIÓN
600
miles_contactos
400
200
11
10 12
15 14 13 4 7
9 65 3 8
2
0
Volvamos a nuestros puntos para practicar dos codificaciones estéticas que no hemos probado, color y tamaño.
Para dejar aún más clara la diferencia de reclamos entre comunas, podríamos usar el tamaño (size) de cada
punto para representar esa variable, además de su altura en el gráfico.
ggplot(contactos_por_comuna) +
geom_point(aes(x = POBLACION, y = miles_contactos, size = miles_contactos))
4.1. UNA BUENA VISUALIZACIÓN PARA EMPEZAR: EL SCATTERPLOT 51
600
miles_contactos
miles_contactos
400
200
400
600
200
Y para distinguir cuál es cuál, podemos pedirle a ggplot que cambie la forma (shape) de cada punto según
la comuna a la que corresponde.
ggplot(contactos_por_comuna) +
geom_point(aes(x = POBLACION, y = miles_contactos, shape = factor(COMUNA)))
52 CHAPTER 4. VISUALIZACIÓN
factor(COMUNA)
1
2
600
3
4
5
miles_contactos
6
400 7
8
9
10
11
200
12
13
14
15
0 NA
150000 175000 200000 225000 250000
POBLACION
¡Hey, sólo aparecen seis de las comunas! ggplot() usa cómo máximo 6 formas distintas, debido a que una
cantidad mayor sería de veras muy difícil de discernir para nuestros pobres ojos. Moraleja: la estética shape
sirve sólo cuando manejamos pocas categorías. De todas formas -en mi opinión- es el método de codificación
que menos gracia tiene, así que no es grave que su utilidad sea limitada.
Hemos visto que especificando atributos estéticos y las variables que representan dentro de aes() podemos
ajustas posición, tamaño, color y hasta la forma de los puntos de acuerdo a sus valores. Pero, ¿qué pasa si
queremos usar un tamaño o un color arbitrario para nuestros puntos? Es decir, si no nos gusta el color negro
y queremos que sean todos azules, o si nos parece que se ven pequeños y queremos que sean todos un poco
más grandes. Fácil: definimos el color o size que queremos por fuera de las función aes(), y será aplicado
a todos los puntos.
ggplot(contactos_por_comuna) +
geom_point(aes(x = POBLACION, y = miles_contactos), color = "blue")
4.2. AJUSTANDO COLOR Y TAMAÑO 53
600
miles_contactos
400
200
Obsérvese que color = "blue" está escrito por fuera de los paréntesis de aes(). De paso, hicimos uso
de una característica muy práctica de R: reconoce un montón de colores por su nombre, siempre que los
escribamos entre comillas. Si le decimos color = "blue", color = "red", color = "yellow", etc., sabe
de que hablamos. Una lista de todos los colores que R reconoce, ideal como referencia, se puede encontrar
en http://www.stat.columbia.edu/~tzheng/files/Rcolor.pdf ; ¡son más de 600!.
600
miles_contactos
400
200
Bellísimo.
600
miles_contactos
400
200
El valor de size se da en píxeles. Es una medida difícil de estimar antes de ver el resultado, pero es cuestión
de probar algunos valores distintos hasta encontrar el que nos va bien. Por supuesto, podemos ajustar varios,
o todos, los atributos a la vez
ggplot(contactos_por_comuna) +
geom_point(aes(x = POBLACION, y = miles_contactos),
size = 9, color = "chocolate3", shape = 0)
56 CHAPTER 4. VISUALIZACIÓN
600
miles_contactos
400
200
4.3 Facetado
Ya sabemos como representar variables usando atributos estéticos. Con esa técnica podemos mostrar con
claridad dos o tres variables en un plano bidimensional (nuestro gráfico). Pero cuando si queremos agregar
más atributos para codificar variables adicionales, la visualización pierde legibilidad de inmediato. Por suerte
existe otra técnica, que podemos usar en combinación con la estética, para agregar aún más variables: el
facetado.
Las facetas son múltiples gráficos contiguos, con cada uno mostrando un subconjunto de los datos. Son útiles
sobre todo para variables categóricas.
Practiquemos con un ejemplo. Sabemos que en la comuna 1 se registra una cantidad de contactos de la
ciudadanía mucho mayor que en las demás. ¿La diferencia será igual para todas las categorías de contacto,
o existe alguna en particular que es la que inclina la balanza?
En nuestro dataframe original, el tipo de contacto aparece en la columna “TIPO_PRESTACION”. El nombre
que eligieron no es del todo informativo, pero summary() (recuerden siempre lo usamos para explorar un
dataset que no conocemos) nos da una pista:
summary(atencion_ciudadano)
“TIPO_PRESTACION” es la categoría más general, con sólo cinco niveles - “DENUNCIA”, “QUEJA”,
“RECLAMO”, “SOLICITUD” y “TRAMITE”. Las otras variables categóricas, asumimos, representan subti-
pos.
Agrupamos entonces nuestra data por comuna y por tipo de contacto, sin olvidar agregar luego los datos de
población
contactos_por_comuna_y_tipo <- atencion_ciudadano %>%
group_by(COMUNA, TIPO_PRESTACION) %>%
summarise(miles_contactos = sum(total) / 1000 ) %>%
left_join(habitantes)
head(contactos_por_comuna_y_tipo)
## # A tibble: 6 x 4
## # Groups: COMUNA [2]
## COMUNA TIPO_PRESTACION miles_contactos POBLACION
## <int> <fct> <dbl> <int>
## 1 1 DENUNCIA 22.9 253271
## 2 1 QUEJA 17.4 253271
## 3 1 RECLAMO 55.7 253271
## 4 1 SOLICITUD 20.9 253271
## 5 1 TRAMITE 568 253271
## 6 2 DENUNCIA 10.2 149720
Listos para facetar. Producimos un scatterplot igual que antes, y le agregamos una capa adicional con
facet_wrap(). La variable a “facetar”, la que recibirá un gráfico por cada una de sus categorías, siempre se
escribe a continuación del signo ~; en nuestro caso, queda como ~TIPO_PRESTACION. El simbolillo en cuestión
denota lo que en R se denomina una fórmula y ya nos lo cruzaremos de nuevo, pero par ahora no le prestamos
más atención.
ggplot(contactos_por_comuna_y_tipo) +
geom_point(aes(x = POBLACION, y = miles_contactos)) +
facet_wrap(~TIPO_PRESTACION)
58 CHAPTER 4. VISUALIZACIÓN
400
200
miles_contactos
0
150000175000200000225000250000
SOLICITUD TRAMITE
400
200
0
150000175000200000225000250000
150000175000200000225000250000
POBLACION
Los culpables de la anomalía son los trámites; en ninguna otra categoría la comuna 1 se separa del resto.
Sabiendo que la base de donde provienen los datos combina información de distintos sistemas de atención,
mi interpretación es que una gran cantidad de “TRAMITES” proviene de algún sistema administrativo que
no guarda direcciones. Si ese fuera el caso, parece que a todos los trámites huérfanos de origen se les asigna
una dirección en la Comuna 1 (sede histórica del Gobierno de la Ciudad) y nosotros terminamos haciendo
estas elucubraciones.
Pero valga el ejemplo para mencionar algo fundamental: por más ciencia de datos que apliquemos, siempre
vamos a llegar a un punto en que nuestros hallazgos no tendrán sentido sin combinarlos con lo que se llama
“conocimiento de dominio”. El conocimiento de dominio es el saber especializado sobre el tema que estamos
tratando, sea el ciclo reproductivo de la gaviota austral o la organización administrativa del Gobierno de la
Ciudad Autónoma de Buenos Aires. Esto no debería desanimarnos, ¡al contrario!. El análisis de datos como
profesión conlleva un constante aprendizaje sobre los más variados temas. Y a la inversa: si somos expertos
en cualquier campo, aún con un puñado de técnicas básicas de R podemos extraer conocimiento de nuestros
datos que jamas encontraría un experto programador que no conoce el paño.
Si hay un tipo de visualización que compite en popularidad con el scatterplot, son los gráficos de barras (bar
charts en inglés). Solemos encontrarlos acompañando artículos en diarios y revistas, sin duda porque son
fáciles de leer de un vistazo. Los gráficos de barras se usan mucho para hacer comparaciones: quién tiene
más y quién tiene menos de alguna variable continua cómo ingresos, edad, altura o similares.
Comparemos la suma total de registros que alcanza cada barrio. Con geom_bar podemos agregar una capa
de visualizacón con gráficos de barras. Los parámetros a definir dentro de aes() son x, donde va una variable
categórica, y en forma opcional weight, que indica la variable a sumar para determinar la altura de cada
4.4. GRÁFICOS DE BARRAS 59
barra. Si no especificamos un weight, simplemente se cuenta cuantas veces aparece cada categoría en el
dataframe, en la práctica un conteo o frecuencia de aparición. En nuestro dataset cada fila incluye un período
y un total de contactos recibidos. Nosotros no estamos interesados en cuantas veces aparece cada barrio,
sino en la suma de la columna total para cada uno de ellos, así que vamos a usar weight = total.
ggplot(atencion_ciudadano) +
geom_bar(aes(x = BARRIO, weight = total))
4e+05
3e+05
count
2e+05
1e+05
0e+00
AGRONOMIA
ALMAGRO
BALVANERA
BARRACAS
BELGRANO
BOCA
CABALLITO
BOEDO
CHACARITA
ERRORNOHAYRESULTADOS
ERRORNOHAYRESULTA
COGHLAN
COLEGIALES
CONSTITUCION
FLORES
FLORESTA
MATADEROS
LINIERS
MONSERRAT
MONTE
NUEVA
PARQUE
PARQUE
CASTRO
NUÑEZ
POMPEYA
PALERMO
PARQUE
PARQUE
AVELLANEDA
CHACABUCO
PUERTO
PATERNAL
PATRICIOS
CHAS
RECOLETA
SAN
RETIRO
SAAVEDRA
MADERO
SAN
VELEZ
CRISTOBAL
SAN
NICOLAS
VERSALLES
VILLA
VILLA
TELMO
SARSFIELD
VILLA
VILLA
DEL
CRESPO
VILLA
GRAL.
DEVOTO
VILLA
PARQUE
VILLA
VILLA
LUGANO
VILLA
PUEYRREDON
MITRE
VILLA
ORTUZAR
LURO
VILLA
VILLA
RIACHUELO
VILLA
REAL
SANTA
SOLDATI
URQUIZA
RITA
BARRIO
Tenemos dos problemas. El primero es que los valores en el eje de las y son grandes, y R nos quiere hacer
un favor expresándolos en notación científica. La notación científica es práctica para ahorrar espacio, pero
no queda vien en visualizaciones. Para pedirle que no lo haga mas, usamos esta función
options(scipen = 999)
El segundo problema es que los nombres de los barrios resultan del todo ilegibles porque no tienen espacio.
En un gráfico, el eje horizonatal es un muy mal lugar para poner muchas categorías con nombre, ya que el
solapamiento se vuelve inevitable. Sería mejor tener los nombre en el eje vertical, donde se pueden escribir
uno encima del otro sin pisarse ¡La solución es invertir los ejes de de coordenadas! Sólo necesitamos agregar
coord_flip:
ggplot(atencion_ciudadano) +
geom_bar(aes(x = BARRIO, weight = total)) +
coord_flip()
60 CHAPTER 4. VISUALIZACIÓN
VILLA URQUIZA
VILLA SOLDATI
VILLA SANTA RITA
VILLA RIACHUELO
VILLA REAL
VILLA PUEYRREDON
VILLA ORTUZAR
VILLA LURO
VILLA LUGANO
VILLA GRAL. MITRE
VILLA DEVOTO
VILLA DEL PARQUE
VILLA CRESPO
VERSALLES
VELEZ SARSFIELD
SAN TELMO
SAN NICOLAS
SAN CRISTOBAL
SAAVEDRA
RETIRO
RECOLETA
PUERTO MADERO
PATERNAL
BARRIO
PARQUE PATRICIOS
PARQUE CHAS
PARQUE CHACABUCO
PARQUE AVELLANEDA
PALERMO
NUÑEZ
NUEVA POMPEYA
MONTE CASTRO
MONSERRAT
MATADEROS
LINIERS
FLORESTA
FLORES
ERRORNOHAYRESULTADOS
ERRORNOHAYRESULTA
CONSTITUCION
COLEGIALES
COGHLAN
CHACARITA
CABALLITO
BOEDO
BOCA
BELGRANO
BARRACAS
BALVANERA
ALMAGRO
AGRONOMIA
Ahora si podemos interpretar el gráfico. San Nicolás y Monserrat son los barrios a la cabeza, lo cual no
sorprende sabiendo que pertenecen a la ya legendaria comuna 1.
Los gráficos de barras, además de comparar, también son buenos para mostrar la composición interna de las
cosas: que “hay dentro”, que componentes contribuye a un determinado total. Vamos a mostrar entonces
cuanto contribuye cada tipo de trámite al toal por barrio, usando el parámetro estético fill (relleno).
geom_bar realiza un segmentado automático de cada barra, con la proporción que le corresponde a cada
subcategoría:
ggplot(atencion_ciudadano) +
geom_bar(aes(x = BARRIO, weight = total, fill = TIPO_PRESTACION)) +
coord_flip()
4.4. GRÁFICOS DE BARRAS 61
VILLA URQUIZA
VILLA SOLDATI
VILLA SANTA RITA
VILLA RIACHUELO
VILLA REAL
VILLA PUEYRREDON
VILLA ORTUZAR
VILLA LURO
VILLA LUGANO
VILLA GRAL. MITRE
VILLA DEVOTO
VILLA DEL PARQUE
VILLA CRESPO
VERSALLES
VELEZ SARSFIELD
SAN TELMO
SAN NICOLAS
SAN CRISTOBAL
SAAVEDRA
RETIRO TIPO_PRESTACION
RECOLETA
PUERTO MADERO DENUNCIA
PATERNAL
BARRIO
PARQUE PATRICIOS
PARQUE CHAS QUEJA
PARQUE CHACABUCO
PARQUE AVELLANEDA RECLAMO
PALERMO
NUÑEZ SOLICITUD
NUEVA POMPEYA
MONTE CASTRO TRAMITE
MONSERRAT
MATADEROS
LINIERS
FLORESTA
FLORES
ERRORNOHAYRESULTADOS
ERRORNOHAYRESULTA
CONSTITUCION
COLEGIALES
COGHLAN
CHACARITA
CABALLITO
BOEDO
BOCA
BELGRANO
BARRACAS
BALVANERA
ALMAGRO
AGRONOMIA
!Esos trámites otra vez! En cierto modo, estamos recorriendo las mismas conclusiones a las que arribamos
usando scatterplots, pero mostrando la información de otra manera. De más está decirlo, hay muchas
maneras de contar las cosas.
En lugar de relleno podríamos haber usado color, tal como hicimos con los puntos, pero los resultado es
un poco menos legible y no luce tan bien. La variable color modifica la silueta de las barras, pero no su
interior:
ggplot(atencion_ciudadano) +
geom_bar(aes(x = BARRIO, weight = total, color = TIPO_PRESTACION)) +
coord_flip()
62 CHAPTER 4. VISUALIZACIÓN
VILLA URQUIZA
VILLA SOLDATI
VILLA SANTA RITA
VILLA RIACHUELO
VILLA REAL
VILLA PUEYRREDON
VILLA ORTUZAR
VILLA LURO
VILLA LUGANO
VILLA GRAL. MITRE
VILLA DEVOTO
VILLA DEL PARQUE
VILLA CRESPO
VERSALLES
VELEZ SARSFIELD
SAN TELMO
SAN NICOLAS
SAN CRISTOBAL
SAAVEDRA
RETIRO TIPO_PRESTACION
RECOLETA
PUERTO MADERO DENUNCIA
PATERNAL
BARRIO
PARQUE PATRICIOS
PARQUE CHAS QUEJA
PARQUE CHACABUCO
PARQUE AVELLANEDA RECLAMO
PALERMO
NUÑEZ SOLICITUD
NUEVA POMPEYA
MONTE CASTRO TRAMITE
MONSERRAT
MATADEROS
LINIERS
FLORESTA
FLORES
ERRORNOHAYRESULTADOS
ERRORNOHAYRESULTA
CONSTITUCION
COLEGIALES
COGHLAN
CHACARITA
CABALLITO
BOEDO
BOCA
BELGRANO
BARRACAS
BALVANERA
ALMAGRO
AGRONOMIA
También podemos cambiar las categorías. Si qusiéramos ver el total de registros por cada tipo de trámite:
ggplot(atencion_ciudadano) +
geom_bar(aes(x = TIPO_PRESTACION, weight = total))
4.4. GRÁFICOS DE BARRAS 63
600000
400000
count
200000
Notamos que las quejas y denuncias son eventos poco frecuentes en comparación con las otras clases de
contacto entre ciudadanos y ciudad. En esta ocasión no recurrimos a coord_flip, ya que las categorías son
pocas y tienen espacio suficiente en el eje horizontal.
BARRIO
LINIERS SAN NICOLAS
AGRONOMIA MATADEROS SAN TELMO
ALMAGRO MONSERRAT VELEZ SARSFIELD
600000
BALVANERA MONTE CASTRO VERSALLES
BARRACAS NUEVA POMPEYA VILLA CRESPO
BELGRANO NUÑEZ VILLA DEL PARQUE
BOCA PALERMO VILLA DEVOTO
400000
BOEDO PARQUE AVELLANEDA VILLA GRAL. MITRE
count
Hemos obtenido una visualización indigerible. Quizás con un facetado por barrio…
ggplot(atencion_ciudadano) +
geom_bar(aes(x = TIPO_PRESTACION, weight = total)) +
facet_wrap(~BARRIO)
4.5. HISTOGRAMAS 65
300000
200000
100000
0
SAAVEDRA
SAN CRISTOBAL
SAN NICOLAS
SAN TELMO
VELEZ SARSFIELD
VERSALLES
VILLA CRESPO
VILLA DEL PARQUE
300000
200000
100000
0
VILLA DEVOTO
VILLA GRAL. MITRE
VILLA LUGANOVILLA LURO
VILLA ORTUZAR
VILLA PUEYRREDON
VILLA REAL
VILLA RIACHUELO
300000
200000
100000
0
DENUNCIA
QUEJA
RECLAMO
SOLICITUD
TRAMITE
DENUNCIA
QUEJA
RECLAMO
SOLICITUD
TRAMITE
DENUNCIA
QUEJA
RECLAMO
SOLICITUD
TRAMITE
DENUNCIA
QUEJA
RECLAMO
SOLICITUD
TRAMITE
DENUNCIA
QUEJA
RECLAMO
SOLICITUD
TRAMITE
VILLA SANTA RITA
VILLA SOLDATI
VILLA URQUIZA
300000
200000
100000
0
DENUNCIA
QUEJA
RECLAMO
SOLICITUD
TRAMITE
DENUNCIA
QUEJA
RECLAMO
SOLICITUD
TRAMITE
DENUNCIA
QUEJA
RECLAMO
SOLICITUD
TRAMITE
TIPO_PRESTACION
Esta opción es un poco mejor, ya que al menos permite identificar pronto los barrios salientes, y discernir
diferencias generales si se la mira con paciencia. Una visualización tan densa en información puede resultar
ideal para “uso personal”, explorando de forma rápida datos con los que estamos familiarizados, pero es poco
recomendable para compartir con otros.
En general, para evitar la confusión asociada a variables con docenas de categorías se busca simplificar
definiendo menos grupos. Por ejemplo, como hicimos al comienzo al separar por comunas, que son sólo
quince, en lugar de por barrios.
4.5 Histogramas
Los histogramas son usados para mostrar la distribución de una variable continua. El histograma permite
decir si los valores que toma cada observación se agrupan en torno a un valor “típico” o medio -como en el
caso de la llamada distribución normal-, o en torno a dos valores frecuentes (distribución bimodal), o con
dispersión sin picos ni valles, donde no hay valores típicos ni atípicos - distribución uniforme.
Por ejemplo, analicemos la distribución de resgistros mensuales (la columna PERIODO en nuestro datset
representa el lapso de un mes). Tenemos que agrupar por mes, y hacer un resumen (summarise()) que
extraiga el gran total:
contactos_por_mes <- atencion_ciudadano %>%
group_by(PERIODO) %>%
summarise(gran_total = sum(total))
head(contactos_por_mes)
## # A tibble: 6 x 2
66 CHAPTER 4. VISUALIZACIÓN
## PERIODO gran_total
## <int> <int>
## 1 201301 43826
## 2 201302 43666
## 3 201303 47405
## 4 201304 50768
## 5 201305 52761
## 6 201306 48344
Hacer un histograma es simple con geom_histogram(): sólo hay que elegir una variable y asignarla a las x.
ggplot(contactos_por_mes) +
geom_histogram(aes(x = gran_total))
3
count
geom_histogram() divide el rango de valores en una cantidad arbitraria de segmentos iguales (“bins” en
inglés) y cuenta cuantas observaciones caen en cada uno, cantidad que se representa con la altura de la
columna en el eje de las y.
En nuestro ejemplo, vemos que un mes mes en el que la cantidad de resgitros tiende a agurparse en torno a
un valor típico de poco más de 60.000 por mes. En apenas un caso hubo mennos de 40.000 o más de 80.000
No sería raro que la agregación qeu hicimos nos oculte patrones en los datos. Que pasa si contamos los
registros por mes y por tipo de contacto, y mostramso los histogramas mensuales en facetado por tipo?
Hacemos el agrupado y sumario de rigor
contactos_por_mes_y_tipo <- atencion_ciudadano %>%
group_by(PERIODO, TIPO_PRESTACION) %>%
summarise(gran_total = sum(total))
4.5. HISTOGRAMAS 67
head(contactos_por_mes_y_tipo)
## # A tibble: 6 x 3
## # Groups: PERIODO [2]
## PERIODO TIPO_PRESTACION gran_total
## <int> <fct> <int>
## 1 201301 DENUNCIA 2740
## 2 201301 QUEJA 889
## 3 201301 RECLAMO 16011
## 4 201301 SOLICITUD 15325
## 5 201301 TRAMITE 8861
## 6 201302 DENUNCIA 2463
20
10
0
count
20
10
0
0 10000 20000 30000 400000 10000 20000 30000 40000
gran_total
Aparecen las diferencias. Los reclamos tienen una dispersión mínima, con casi todas las observaciones
apiladas en torno a unos 1000 contactos mensuales; siempre son bajas. La cantidad mensual de denuncias,
reclamos y solicitudes muestra una dispersión mayor, pero aún así tendencia a rondar un valor típico. Los
trámites son el extremo opuesto a las qeujas, ya que muestran una gran dispersión, pudiendo tomar cualquier
valor de menos de 10.000 a más de 40.000 registros de forma bastante pareja.
68 CHAPTER 4. VISUALIZACIÓN
Lo último que nos queda por decir en este capítulo es que los gráficos que hemos producido hasta aquí
están pensandos para nuestro propio consumo. Son parte, y parte dundamental, de lo que llamamos análisis
exploratorio de datos. En el contexto de la exploración, lo importante es trabajar en forma rápida, probando
una u otra técnica de visuzalización y refinando nuestros resultados hasta hallar patrones interesantes, o
sacarnos dudas acerca de los datos. No necesitamos ponerle título a las visualizaciones, porque ya sabemos
de que tratan (¡acabamos de escribirlas!). No nos preocupa que los nombres de los ejes indiquen en forma
clara la variable representan, porque ya lo sabemos de antemano.
Pero cuando queremos guardar un gráfico para compartir con otros, sea publicándola en un paper, o envián-
dola por mail a un amigo, necesitamos tener más cuidado. Hemos pasado del ámbito de la exploración al de
la comunicación. Ahora si debe preocuparnos la claridad, porque no sabemos el grado de familiaridad que
tiene con los datos la eventual audiencia.
Si bien la comunicación clara es un arte cuyas reglas dependen del contexto, y además cada quien tiene su
estilo, podemos decretar al menos tres elementos que no deberían faltar en un gráfico destinado a comunicar
algo a los demás:
Con ggplot() podemos encargarnos de todo dentro de una sola función, labs() (por labels, etiquetas)
Tomemos un gráfico de los que hicimos antes para pulirlo un poco y que sirva de ejemplo. El original:
ggplot(atencion_ciudadano) +
geom_bar(aes(x = BARRIO, weight = total, fill = TIPO_PRESTACION)) +
coord_flip()
4.6. PREPARANDO UNA VISUALIZACIÓN PARA COMPARTIR 69
VILLA URQUIZA
VILLA SOLDATI
VILLA SANTA RITA
VILLA RIACHUELO
VILLA REAL
VILLA PUEYRREDON
VILLA ORTUZAR
VILLA LURO
VILLA LUGANO
VILLA GRAL. MITRE
VILLA DEVOTO
VILLA DEL PARQUE
VILLA CRESPO
VERSALLES
VELEZ SARSFIELD
SAN TELMO
SAN NICOLAS
SAN CRISTOBAL
SAAVEDRA
RETIRO TIPO_PRESTACION
RECOLETA
PUERTO MADERO DENUNCIA
PATERNAL
BARRIO
PARQUE PATRICIOS
PARQUE CHAS QUEJA
PARQUE CHACABUCO
PARQUE AVELLANEDA RECLAMO
PALERMO
NUÑEZ SOLICITUD
NUEVA POMPEYA
MONTE CASTRO TRAMITE
MONSERRAT
MATADEROS
LINIERS
FLORESTA
FLORES
ERRORNOHAYRESULTADOS
ERRORNOHAYRESULTA
CONSTITUCION
COLEGIALES
COGHLAN
CHACARITA
CABALLITO
BOEDO
BOCA
BELGRANO
BARRACAS
BALVANERA
ALMAGRO
AGRONOMIA
PARQUE CHAS
PARQUE CHACABUCO
PARQUE AVELLANEDA RECLAMO
PALERMO
NUÑEZ SOLICITUD
NUEVA POMPEYA
MONTE CASTRO TRAMITE
MONSERRAT
MATADEROS
LINIERS
FLORESTA
FLORES
ERRORNOHAYRESULTADOS
ERRORNOHAYRESULTA
CONSTITUCION
COLEGIALES
COGHLAN
CHACARITA
CABALLITO
BOEDO
BOCA
BELGRANO
BARRACAS
BALVANERA
ALMAGRO
AGRONOMIA
ocasiones encontraremos un paquete de R que extiende ggplot para realizar de forma fácil la visualización
que nos interesa.
Chapter 5
Modelado estadístico
Llegamos a un tema de gran interés para quienes realizan investigaciones formales. La posición central que
tiene el modelado en la investigación científica se debe a que cuantifica relaciones: permite pasar de decir “La
luz solar abundante mejora el crecimiento de las plantas” a “por cada hora adicional de exposición mensual
a la luz solar, los cultivos aumentaron su rinde en un 1%”. La cuantificación permite realizar comparaciones,
algo clave para entender un fenómeno estudiado: antes y después, con o sin tratamiento, en un lugar o en
otro.
Este capítulo le debe mucho a ModernDive: An Introduction to Statistical and Data Sciences via R por
Chester Ismay y Albert Y. Kim, disponible en forma gratuita http://http://moderndive.com/. ModernDive
es un recurso muy recomendable para quienes quieran continuar profundizando su conocimiento más allá de
los temas que veremos a continuación.
En términos matemáticos, se habla de “modelar” debido a que estamos creando un modelo, una reconstruc-
ción simplificada (¡simplificada en extremo!) de cómo funciona un proceso observado en el mundo real. En
un modelo de datos, siempre tenemos al menos
• Una variable resultante, siempre una sola, también llamada variable “dependiente”,
• Una o más variables predictoras, también llamadas “explicativas”
1. Predecir el valor de una variable resultante en base a valores conocidos de las variables predictoras.
Aquí no interesa tanto entender cómo es que las variables interactúan entre sí, o por qué lo hacen.
Mientras las predicciones sean acertadas, o se acerquen lo suficiente, el modelo cumple su cometido. Los
modelos predictivos se emplean en una enorme variedad de aplicaciones: inversión en bolsa, prevención
de fraude, publicidad online, fijación de primas en seguros de riesgo, etc.
2. Explicar la relación entre una variable dependiente y todas las demás (las explicativas), buscando de-
terminar si la relación es significativa. Los modelos explicativos son los que se favorecen en investigación
académica, ya que ayudan a entender el fenómeno modelado.
Existen muchísimas técnicas para modelar datos, algunas de ellas simples como la regresión lineal, y otras
mucho más complejas, como las redes neuronales. Por supuesto, vamos a practicar con las primeras.
La humilde regresión lineal, fácil de explicar y muy fácil de resolver con la ayuda de una computadora, es
el caballito de batalla del modelado estadístico. A pesar de que no es adecuada para ciertos tipo de datos,
y de que existen métodos más modernos que explotan con intensidad el potencial de las computadoras, la
regresión lineal sigue siendo la herramienta más común. Un poco por costumbre, y otro porque es el método
más fácil de interpretar, lo que favorece entender y comunicar sus resultados.
73
74 CHAPTER 5. MODELADO ESTADÍSTICO
La encarnación más sencilla de la regresión lineal es la simple o univariada. Tenemos nuestra variable y,
numérica, y una sola variable predictora x, que puede ser numérica o categórica.
Para poner en práctica los conceptos repasados en este capítulo, vamos a tomarnos un recreo de los datos
de Buenos Aires, haciendo un zoom out hacia las escalas de país, continente y el planeta entero. Contamos
con un dataset muy prolijo e interesante recopilado por Gapminder (www.gapminder.org), una organización
que busca “hacer comprensible al mundo en base a estadísticas confiables”.
Descarguemos el dataset y echémosle un vistazo como ya sabemos hacer:
data_mundial <- read.csv("https://bitsandbricks.github.io/data/gapminder.csv")
summary(data_mundial)
Hagamos nuestra pregunta: ¿Cómo ha se relaciona el paso del tiempo (variable explicativa) con la expectativa
de vida en la Argentina?
Para contestar, primero filtremos los datos que nos interesan:
data_arg <- data_mundial %>%
filter(pais == "Argentina")
data_arg
Como dijimos en el capítulo de visualización, los scatterplots son útiles para mostrar la relación entre dos
variables. Usemos uno para visualizar la relación entre año y expectativa de vida en Argentina, para intentar
anticipar los resultados de la regresión lineal.
ggplot(data = data_arg) +
geom_point(aes(x = año, y = expVida)) +
labs(title = "Correlación entre tiempo y expectativa de vida",
subtitle = "Argentina",
y = "expectativa de vida")
72
expectativa de vida
68
64
Obtener la correlación entre dos variables es fácil. La función cor() toma dos vectores dos secuencias
de valores, y los compara para determinar su grado de correlación. Recurriendo al truco que ya usamos
alguna vez, usamos el formato “dataframe$columna” para extraer las columnas de nuestro dataframe que
necesitamos:
cor(data_arg$año, data_arg$expVida)
## [1] 0.9977816
¿A partir de qué valor consideramos que existe una correlación apreciable? La verdad es que no hay una
regla a seguir, pero inventemos una. Si el valor absoluto de la correlación es..
- de 0,7 a 1: de fuerte a total
- de 0,5 a 0,7: de moderada a fuerte
- de 0,3 a 0,7: de débil a moderada
- menor a 0,3: de nula a débil
El valor que obtuvimos se acerca mucho a 1, la correlación casi total. OK, el paso de los años y la expectativa
de vida en la Argentina están correlacionados de forma intensa, pero aún desconocemos algo quizás más
importante: un valor preciso del “efecto” que el paso de cada año tiene sobre la expectativa de vida. Eso
es lo que vamos a determinar con la regresión lineal. Usamos la palabra “efecto” entre comillas para aclarar
una de las limitaciones del modelado estadístico: podemos probar correlación, pero no causalidad. Es decir,
no podemos probar que una variable causa a la otra; en todo caso, probamos que se mueven juntas y en base
a ello podríamos diseñar un experimento que permita comprobar causalidad.
Vamos a la regresión lineal entonces, para medir de una buena vez la correlación entre tiempo y expectativa
de vida. Usamos la función lm() (por “linear model”), así:
modelo_exp <- lm(expVida ~ año, data = data_arg)
¡Eso es todo! Hemos construido un modelo estadístico; ahora tenemos que aprender a usarlo. Obsérvese que
volvió aparecer el simbolillo que denota una fórmula, ~. Usado como primer argumento de lm(), significa
“expVida vs año”, es decir “estimar el efecto en la variable expVida cuando incrementa el valor de año”,
usando los datos contenidos en el dataframe data_arg.
El resultado de lm(), que hemos guardado dentro de la variable modelo_exp es un tipo de objecto con el
que no hemos trabajado hasta ahora. No es un dataframe, sino una lista que contiene distintos atributos del
modelo estadístico. No hace falta preocuparnos por eso ahora.
Retomando nuestra pregunta… ¿cuál es el efecto? Nos lo dice el modelo cuando lo escribimos.
modelo_exp
##
## Call:
## lm(formula = expVida ~ año, data = data_arg)
##
## Coefficients:
## (Intercept) año
## -389.6063 0.2317
Ahí está. En nuestro modelo, el coeficiente de la variable “año” es 0.2317. Significado: incrementando en
una unidad la variable año, la variable expectativa de vida se incrementa en 0.2317. Dicho de otra manera,
por cada año que pasa la expectativa de vida en la Argentina aumenta casi 3 meses.
El otro coeficiente que aparece, “(Intercept)” es la intersección. En términos de interpretado del modelo, la
intersección rara vez tiene utilidad. Para lo que sí sirve es para trazar la línea que permite “predecir” valores
para años en los que no tenemos observaciones. Recordemos la fórmula que define una línea recta:
y =a+b×x
5.1. REGRESIÓN LINEAL SIMPLE 77
72
expectativa de vida
68
64
Aquí no vemos más que los datos que ya teníamos. Pero proyectemos la línea hacia el futuro. Con xlim() e
ylim() podemos definir a mano los límites de nuestro gráfico, haciéndolo ir más allá del rango de los datos
que tenemos. La línea sigue siendo la misma, sólo que ahora podemos ver hacia donde va.
ggplot(data = data_arg) +
geom_point(aes(x = año, y = expVida)) +
labs(title = "Correlación entre tiempo y expectativa de vida",
subtitle = "Argentina",
y = "expectativa de vida",
78 CHAPTER 5. MODELADO ESTADÍSTICO
85
80
expectativa de vida
75
70
65
60
Ahí está la predicción. Según nuestro modelo, para el año 2030 la expectativa de vida en la Argentina habrá
superado los 80 años.
Es hora de dar una definición oficial para una regresión lineal, y es esta: es la línea que describe la ecuación:
ŷ = b0 + b1 × x
Obsérvese que se trata de la ecuación de una recta, y = a + b × x, con otros nombres. En voz alta, se
leería así “Cada predicción del valor de y, llamada ŷ, se obtiene multiplicando a la variable predictora x
por su coeficiente b1 y sumándole el valor de la intersección b0 ”. En otras palabras, a cada valor de x (las
observaciones de la variable explicativa) le corresponde un punto en la recta trazada por el modelo. La altura
sobre la recta de las y para ese punto es el valor predicho para la variable dependiente.
Ya que estamos, aprendamos otro truco. ggplot() puede agregar a nuestros scatterplots una capa con la
línea de la regresión lineal, en forma automática. La función geom_smooth() se usar para explicitar patrones
en los datos. Tal como otras de la familia ggplot, espera que se le diga que variables asignar a x e y, más
un parámetro method con el método solicitado para trazar una línea de tendencia. Aquí usamos method =
"lm" por linear model, el modelo lineal.
ggplot(data = data_arg) +
geom_point(aes(x = año, y = expVida)) +
labs(title = "Correlación entre tiempo y expectativa de vida",
subtitle = "Argentina",
5.1. REGRESIÓN LINEAL SIMPLE 79
y = "expectativa de vida",
caption = "con línea de regresión vía geom_smooth()") +
geom_smooth(aes(x = año, y = expVida), method = "lm")
72
expectativa de vida
68
64
Hacer una regresión lineal se trata de encontrar la línea que atraviesa nuestra nube de puntos de modo tal
que la suma de las distancias de cada punto a la línea sea la menor posible. Es un problema matemático
que puede resolverse con distintas técnicas (algebra lineal, geometría, etc) que no vamos a discutir aquí.
Confiaremos en R para hacer los cálculos.
En la relación año - expectativa de vida las distancias entre los puntos (las observaciones) y la línea (el
modelo) son muy pequeñas. Eso indica que el modelo describe con gran precisión la dinámica de la relación
entre las variables analizadas.
En general, es inusual encontrar una correlación tan nítida entre variables “en la vida real”, sobre todo cuando
estudiamos procesos complejos cuyo comportamiento describe patrones más complejos que una relación lineal
pura. No hace falta ir demasiado lejos para encontrar un ejemplo. Usando el mismo dataset, visualicemos
un scatterplot de PBI vs año, agregando la línea de regresión para:
ggplot(data = data_arg) +
geom_point(aes(x = año, y = PBI_PC)) +
labs(title = "Correlación entre PBI y expectativa de vida",
subtitle = "Argentina",
y = "PBI per cápita") +
geom_smooth(aes(x = año, y = PBI_PC), method = "lm")
80 CHAPTER 5. MODELADO ESTADÍSTICO
11000
PBI per cápita
9000
7000
5000
1950 1960 1970 1980 1990 2000
año
Sigue siendo evidente una fuerte tendencia lineal, pero las observaciones ya no se ciñen de forma tan estrecha
a la línea idealizada de la regresión.
Obtengamos el modelo del PBI per cápita de la Argentina en relación al paso del tiempo:
modelo_PBI <- lm(PBI_PC ~ año, data = data_arg)
modelo_PBI
##
## Call:
## lm(formula = PBI_PC ~ año, data = data_arg)
##
## Coefficients:
## (Intercept) año
## -162888.14 86.81
Tal como indicaba el scatterplot, obtuvimos un coeficiente positivo. Según el modelo, cada año que pasa
resulta en un incremento de 86 dólares en el PBI per cápita del país. Sin embargo, sabemos que no todas
las observaciones cumple al pie de la letra con ese incremento. ¿Debería preocuparnos eso? Una parte
importante del análisis basado en regresiones es revisar las desviaciones, y decidir si ameritan buscar una
explicación. Para ello, lo mejor es empezar por prestar atención a los residuos.
Los residuos, en la jerga estadística, no son otra cosa que las diferencias encontradas entre el valor que
predice un modelo y el valor observado para una variable. Es decir, el valor para cada punto de y − yb. Los
residuos representan el desvío de cada observación respecto al valor esperado por el modelo.
5.1. REGRESIÓN LINEAL SIMPLE 81
Cuando los desvíos son pequeños, es decir cuando los residuos son pequeños, decimos que nuestro modelo se
ajusta bien a los datos observados. Cuando los residuos son grandes ocurre lo contrario, y quizás deberíamos
buscar otros modelos para representar la relación entre las variables.
Prestemos atención a los residuos de nuestro modelo de PBV vs. tiempo. Podemos extraer los residuos
usando la función residuals(),
residuos <- residuals(modelo_PBI)
residuos
## 1 2 3 4 5 6
## -656.9180 -145.4351 -303.1836 182.5450 1138.5722 1340.5021
## 7 8 9 10 11 12
## -174.6855 -466.9699 -732.2809 492.5240 -2111.1755 1436.5051
agregarlos a nuestro dataframe,
data_arg <- data_arg %>% mutate(residuo_ml = residuos)
y visualizarlos comparados con una línea que indica el cero, trazada por geom_hline(). Los residuos cercanos
a ese valor son los que corresponden a observaciones a las que el modelo se ajusta bien.
ggplot(data_arg) +
geom_point(aes(x = año, y = residuo_ml)) +
geom_hline(yintercept = 0, col = "blue") +
labs(x = "año", y = "residuo del modelo lineal")
1000
residuo del modelo lineal
−1000
−2000
este es que los residuos se distribuyan al azar, sin indicios de patrones sistemáticos. Si así fuere, podemos
considerar que nuestro modelo es adecuado.
¿Cómo determinamos que no exhiben patrones sistemáticos? Una vez mas, se trata de una evaluación
bastante subjetiva, y cada quien estará conforme dependiendo del contexto y la experiencia previa. Aún así
podemos argumentar en favor de la adecuación del modelo cuando:
1. El promedio de los residuos se aproxima a cero; es decir, que los residuos positivos se cancelan con los
negativos, promediando cerca de cero.
2. El valor de los residuos no depende del valor de x; es decir, no se observa un crecimiento (o decrec-
imiento) sistemático de la magnitud de los residuos a medida quex crece
Por lo visto, nuestro modelo cumple con 1. pero no con 2, ya que la magnitud de los residuos parece crecer
con el paso de los años. Entre todos los puntos, los mayores transgresores son los últimos y corresponden
a los años 2002 y 2007. El valor del PBI per cápita observado en 2002 año resultó ser más de 2000 dólares
menor al esperado por el modelo, todo un derrumbe. ¿A qué se debe tanta discrepancia? Nuestro modelo
no tiene la culpa, es que la realidad tiene sus bemoles. A fines del 2001 la Argentina sufrió la peor crisis
financiera de su historia, factor que explica la brusca caída del PBI que revirtió la tendencia al crecimiento
de décadas anteriores. Una función que aún no habíamos usado, geom_line(), nos va a permitir trazar una
línea que siga el PBI a lo largo de los años, y otra novedad, geom_vline(), se encargará de agregar una
línea vertical que señale el año de la crisis:
ggplot(data_arg) +
geom_line(aes(x = año, y = PBI_PC)) +
geom_vline(aes(xintercept = 2001), color = "red") +
labs(title = "Evolución del PBI en la Argentina",
y = "PBI per cápita",
caption = "La línea roja indica la ocurrencia de la crisis del 2001")
12000
PBI per cápita
10000
8000
6000
Es claro que el modelo se beneficiaría de poder tener en cuenta la irrupción de las crisis en el país. Esto se
lograría agregando una variable categórica para cada año, que indique si se trata de un período de crisis. En
ese caso, seria un modelo de regresión lineal múltiple (con más de una variable explicativa), incorporando una
variable explicativa numérica y otra categórica. Que lástima que nuestro dataset no incluye la variable de las
crisis financieras. Si quisiéramos mejorar nuestro modelo con esa información, no nos quedaría mas remedio
que salir a buscar los datos. Con suerte, alguien los habrá recopilado por nosotros, y si no, tendríamos que
hacerlo por nuestra cuenta. ¡La investigación es un sacerdocio!
En aras de la simplicidad, sigamos practicando con los datos disponibles.
El dataset con datos del mundo provisto por Gapminder incluye dos variables categóricas: país y continente.
Con 142 países representados, podemos descartar a la primera como variable para realizar un modelo -
recordemos que para entender la interrelación de variables, cuantas menos involucremos mejor. Los cinco
continentes habitados representan un conjunto mucho más práctico, por lo que la pregunta será “¿Cuánto
incide el continente en la expectativa de vida de los países?”
Comencemos por explorar los datos tomando las observaciones más recientes, las de 2007.
data_mundial_2007 <- data_mundial %>% filter(año == 2007)
ggplot(data = data_mundial_2007) +
geom_point(aes(x = continente, y = expVida, color = continente)) +
labs(title = "Expectativa de vida por continente",
y = "expectativa de vida")
80
70
continente
expectativa de vida
Africa
Americas
60 Asia
Europe
Oceania
50
40
más bajos. La profusión de puntos hace que muchos terminen superpuestos, haciendo imposible determinar
cuántos ocupan cada posición (un problema llamado overplotting en inglés). Una variante de geom_point()
llamada geom_jitter() resuelve este problema al “sacudir” los puntos, sumando a cada uno un pequeño
valor al azar para que se separe de los que comparten su posición. Es un buen ejemplo de la paradoja por la
cual reducir la precisión de la información a veces permite entender mejor lo que está ocurriendo. Usamos
geom_jitter() igual que geom_point():
ggplot(data = data_mundial_2007) +
geom_jitter(aes(x = continente, y = expVida, color = continente)) +
labs(title = "Expectativa de vida por continente",
y = "expectativa de vida")
80
70
continente
expectativa de vida
Africa
Americas
60 Asia
Europe
Oceania
50
40
Algún ojo avizor habrá notado que la clasificación por color no es necesaria, ya el continente ya está señalado
por su posición en el exe de las x. El color cumple aquí una función más que nada cosmética, en pos de
hacer al gráfico maś atractivo a la vista.
También podemos visualizar la diferencia de distribución de expectactiva de vida de los países, con un
histograma facetado por continente:
ggplot(data = data_mundial_2007) +
geom_histogram(aes(x = expVida, fill = continente)) +
facet_wrap(~continente) +
labs(title = "Expectativa de vida por continente",
subtitle = "histogramas",
x = "expectativa de vida",
y = "cantidad")
5.1. REGRESIÓN LINEAL SIMPLE 85
7.5
5.0
2.5 continente
Africa
cantidad
0.0
Americas
40 50 60 70 80
Europe Oceania Asia
10.0
Europe
7.5 Oceania
5.0
2.5
0.0
40 50 60 70 80 40 50 60 70 80
expectativa de vida
Bien, estamos convencidos de que hay una relación entre continente y expectativa de vida, aunque no la
hemos cuantificado. Para eso, recurrimos a una regresión lineal con variable expicativa categórica. Se
obtiene de la misma manera que antes, no hay cambios en la forma de invocar lm() por el hecho de que la
variabe ahora sea categórica en vez de numérica.
modelo_exp_continente <- lm(expVida ~ continente, data = data_mundial_2007)
modelo_exp_continente
##
## Call:
## lm(formula = expVida ~ continente, data = data_mundial_2007)
##
## Coefficients:
## (Intercept) continenteAmericas continenteAsia
## 54.81 18.80 15.92
## continenteEurope continenteOceania
## 22.84 25.91
¿Qué ocurrió aquí? lm() inspeccionó el contenido de la variable “continente” y encontró cinco niveles o
categorías. Tomó el primero en orden alfabético, “Africa” como línea de base. El primer coeficiente de la
regresión (la intersección) es el promedio de la expectativa de vida en África. Para cada una de las categorías
restantes, el coeficiente representa la diferencia respecto a África de la expectativa de vida promedio en cada
uno de los otros continentes. He allí la cuantificación: para un país en las Américas, podemos esperar -en
promedio- una expectativa de vida que supera en 18.8 años la de los paises africanos. Para un país en Asia,
son 15.92 los años adicionales, y así.
Prestemos atención a los residuos. Agregamos al dataframe una columna con el residuo para cada obser-
86 CHAPTER 5. MODELADO ESTADÍSTICO
vación,
data_mundial_2007 <- data_mundial_2007 %>%
mutate(residuo_ml = residuals(modelo_exp_continente))
20
10
residuo del modelo lineal
−10
−20
ggplot(data_afganistan) +
geom_line(aes(x = año, y = expVida)) +
labs(title = "Expectativa de vida en Afganistán",
y = "expectativa de vida")
40
expectativa de vida
36
32
que más información podrían revelar. Existen tratados completos dedicados a la cuestión de como manejar
los outliers, pero el conocimiento de dominio es la principal herramienta para decidir que hacer ante casos
inusuales… como siempre.
Hasta aquí hemos usado la regresión lineal para hacer explícita la relación entre una variable resultante y
y una única variable predictiva o explicativa x. En algunos de nuestros resultados pudimos intuir que el
agregado de alguna variable explicativa adicional podría mejorar nuestras predicciones. De eso se trata la
regresión lineal múltiple: incorporar una cantidad arbitraria de variables al modelo, buscando representar
las múltiples dinámicas que inciden en el fenómeno estudiado.
Una buena noticia es que, en general, agregar variables a nuestro modelo estadístico no requiere mucho
esfuerzo adicional. En la época en que los cálculos matemáticos debían hacerse sin la ayuda de una com-
putadora, sumar variables sin ton ni son debía tener poca gracia, debido a la creciente cantidad de cálculos
a resolver. Para nosotros que dejamos la tarea en manos de software especializado, el problema es el opuesto.
Es tan fácil sumar variables al modelo, que debemos evitar la tentación de arrojar todo dentro de la fórmula
de regresión líneal y decidir luego que parece importante y que no.
Pasemos a la práctica. Vamos a modelar la expectativa como resultante de la población y del PBI per cápita
de los países, usando los datos más reciente (tomados en 2007). La única difrerencia respecto a una regresión
lineal simple es que usamos + para agregar variables en la fórmula de lm()
modelo_exp_multiple <- lm(expVida ~ pobl + PBI_PC, data = data_mundial_2007)
modelo_exp_multiple
##
## Call:
## lm(formula = expVida ~ pobl + PBI_PC, data = data_mundial_2007)
##
## Coefficients:
## (Intercept) pobl PBI_PC
## 59.205198140717 0.000000007001 0.000641608517
¿Cómo interpretamos esos resultados? Más o menos de la misma manera que con la regresión simple. Como
antes, tenemos un coeficiente para la intersección, al que no prestamos mucha atención porque no nos dice
nada de la relación entre las variables. Lo que cambia es que esta vez tenemos dos variables predictoras en
lugar a una, cada una con su coeficiente. Los coeficientes positivos indican que la relación de la población
con la expectativa de vida es de correlación positiva (cuando una crece la otra tiende a crecer también), y lo
mismo ocurre con el PBI. La magnitud de los coeficientes es pequeña (minúscula en el caso de la población),
lo cual dificulta “narrar” los resultados, pero podemos hacerlo así:
• Cuando las demás variables se mantienen constantes (es decir, en países con PBI similar) el incremento
de una unidad de población -un habitante- está asociado a un incremento de 0,000000007 años en la
expectativa de vida del país… unas dos décimas de segundo.
• Cuando las demás variables se mantienen constantes (es decir, en países con población similar) el
incremento de una unidad de PBI -un dólar per cápita- está asociado a un incremento de 0,00064 años
en la expectativa de vida del país… un poco más de cinco horas y media.
Pensemos un poco si los resultados tienen sentido. La correlación positiva entre PBI y longevidad es de lo
más razonable. No nos extraña que los países de mayores ingresos tiendan a ser aquellos cuyos habitantes
viven más tiempo. La correlación con la población es quizás inesperada. Si la longevidad se incrementa junto
a la cantidad de habitantes, ¿acaso no deberíamos encontrar a varios de los países más populosos entre los
más longevos?
5.2. REGRESIÓN CON MÚLTIPLES VARIABLES 89
## [1] 0.04755312
Recordemos que la intensidad de una correlación es su valor absoluto, que toma un máximo de 1, mientras
que el signo (positivo o negativo) indica si la relación entre variables es directa o inversa. Aquí obtuvimos
un valor bien bajo, cercano a cero: la correlación es nula. Entonces ¿Por qué aparece en nuestro modelo de
regresión lineal?
En resumidas cuentas, aparece porque nosotros le pedimos que aparezca. Es decir, instruimos en forma
específica a lm() para que incorpore a la población en el modelo. El caso es que población no es un buen
predictor de longevidad (la correlación es bajísima), pero si lo pedimos, lo tenemos: el coeficiente nos indica
el valor que minimiza las discrepancias entre valores observado y valores predichos trazando una línea recta.
Lo que no indica por si solo es el grado en el cual podemos confiar en esa variable para darnos buenas
predicciones o estimados.
Sería muy util que el resultado de lm() indique cuáles variables son buenas predictoras y cuáles no. Y
por suerte, lo hace cuando lo interrogamos con summary(), la misma función que hemos estado usando
90 CHAPTER 5. MODELADO ESTADÍSTICO
para obtener el resumen de un dataframe. Cuando la usamos con un objeto de R que contiene un modelo
estadístico, lo que obtenemos son sus detalles:
summary(modelo_exp_multiple)
##
## Call:
## lm(formula = expVida ~ pobl + PBI_PC, data = data_mundial_2007)
##
## Residuals:
## Min 1Q Median 3Q Max
## -22.496 -6.119 1.899 7.018 13.383
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 59.205198140717 1.040398672164 56.906 <0.0000000000000002
## pobl 0.000000007001 0.000000005068 1.381 0.169
## PBI_PC 0.000641608517 0.000058176209 11.029 <0.0000000000000002
##
## (Intercept) ***
## pobl
## PBI_PC ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 8.87 on 139 degrees of freedom
## Multiple R-squared: 0.4679, Adjusted R-squared: 0.4602
## F-statistic: 61.11 on 2 and 139 DF, p-value: < 0.00000000000000022
El resumen incluye los parámetros que definieron al modelo, los valores por cuartil de los residuos, y una
tabla con variables numéricas. En esa tabla, bajo la columna Estimate tenemos el “efecto” estimado de
cada variable explicativa sobre la dependiente. Es decir, los coeficientes que ya conocemos. Luego aparecen
tres columnas con atributos estadísticos: Std. Error, t value, y Pr(>|t|). En castellano las llamaríamos,
respectivamente, error estándar, valor t y valor p. Interpretar estos valores cae fuera de nuestros objetivos,
pero podemos señalar que el más famoso entre ellos es el valor p, porque se usa como medida: si vale menos
de 0,5, se considera que la capacidad de predicción de la variable asociada es significativa. Para interpretar
todo esto de manera sencilla, una vez más vamos a confiar en R para guiarnos. He aquí la curiosa forma de
determinar si una variable es buena predictora o no: contar estrellitas. Junto a cada fila aparecen, a veces,
de uno a tres asteriscos. Son la forma de R de decirnos cuales son las variables explicativas que muestran
una relación “estadísticamente significativa” con nuestra variable dependiente. Cuanto más bajo el valor p,
más significativa es la relación y más estrellitas aparecen:
• . o nada: No se encuentra una relación entre esta variable y la que queremos predecir.
• *: Es muy probable que esta variable tenga una relación con la que queremos predecir. Ya podemos
publicar estos resultados en un paper científico.
• **: Es muy, pero muy probable que esta variable tenga una relación con la que queremos predecir.
99% seguro.
• ***: Juramos que las variables estan relacionadas. Más no se puede pedir.
Lo de un asterisco/estrella (*) indicando que los resultados ya alcanzan rigor científico no es broma. El
asterisco solitario indica que, a nivel estadístico, se supera el 95% de confianza en que la relación existe
en la realidad y no es producto de una casualidad en los datos. Pasando ese umbral se considera que los
datos son “estadísticamente significativos”, y desde hace muchos años encontrar un valor p menor a 0,5 es la
meta dorada de los investigadores que emplean análisis estadístico. ¿Porqué un 95% de confianza alcanza?
¿Porqué no relajar el límite a 90%, o quizás mejor, exigir al menos un 99 o 99,9% de seguridad? La verdad
es que no hay ninguna razón trascendental. El 95% de certeza es tan sólo un valor arbitrario que en algún
5.2. REGRESIÓN CON MÚLTIPLES VARIABLES 91
momento se volvió estándar. Es importante aclara que en los últimos años ha crecido una reacción de rechazo
a esta norma arbitraria, dentro de la propia comunidad científica. Quienes siguen confiando en los valores
p son llamados “frecuentistas”; los que proponen cuantificar de otra forma nuestro grado de certeza son
llamados “bayesianos”. Google mediante, quien quiera saber más sobre la apasionante rivalidad tendrá horas
de diversión aseguradas.
En lo que a nosotros respecta, por ahora vamos a aceptar el enfoque frecuentista, y cuando veamos una
estrella diremos que la variable asociada es un buen predictor. O para ser más precisos, que su relación con
la variable dependiente es estadísticamente significativa.
Volvamos a nuestros modelos. Cuando hicimos regresiones simples no sabíamos aún de valores p, y no
revisamos la significancia de las variables predictoras. Hagamoslo ahora con el modelo de expectativa de
vida en Argentina vs. PBI :
summary(modelo_exp)
##
## Call:
## lm(formula = expVida ~ año, data = data_arg)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.53006 -0.13516 -0.01219 0.14228 0.55202
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -389.606345 9.677730 -40.26 0.000000000002140 ***
## año 0.231708 0.004889 47.40 0.000000000000422 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2923 on 10 degrees of freedom
## Multiple R-squared: 0.9956, Adjusted R-squared: 0.9951
## F-statistic: 2246 on 1 and 10 DF, p-value: 0.0000000000004216
Las tres estrellitas, distintión máxima, indican que sin dudas el año está relacionado con la expectativa
de vida. Esto no es una sopresa: la linea de la regresión lineal se ajusta con tanta precisión a los valores
observados, que no podía ser de otra manera.
Continuando con las regresiones múltiples, intentemos un modelo con tres variables predictoras. A población
y PBI, las que ya teníamos en cuenta, vamos a agregar una variable categórica: el continente.
modelo_exp_multiple <- lm(expVida ~ pobl + PBI_PC + continente, data = data_mundial_2007)
summary(modelo_exp_multiple)
##
## Call:
## lm(formula = expVida ~ pobl + PBI_PC + continente, data = data_mundial_2007)
##
## Residuals:
## Min 1Q Median 3Q Max
## -22.8199 -2.8905 0.1574 2.9046 20.0585
##
## Coefficients:
## Estimate Std. Error t value
## (Intercept) 53.7141900516204 0.9355709763972 57.413
92 CHAPTER 5. MODELADO ESTADÍSTICO
Hemos llegado al capítulo final, dedicado al análisis y visualización de información geográfica. Aquí incur-
sionaremos en el el dominio de los SIG (“Sistemas de Información Geográfica”) también conocidos como GIS
por sus siglas en inglés.
Hasta hace poco tiempo, labores como la producción de mapas y el análisis espacial estaban reservadas para
especialistas, debido a la complejidad de las tareas y al alto costo de producción y adquisición de datos
geográficos. Pero durante las dos últimas décadas la tecnología digital cambió el panorama. Una dramática
caída en el costo asociado a adquirir y procesar información geográfica (pensemos en satélites y computadoras
multiplicándose y bajando de precio) dio paso al mapa digital como herramienta universal. El consumo de
sofisticados mapas y otros productos geográficos se volvió masivo y cotidiano, con Google Maps como el
exponente más conocido. Apenas había pasado la novedad de disponer de mapas en alta resolución de todo
el mundo accesibles al instante desde nuestros escritorios, cuando la llegada de los smartphones popularizó
el acceso en cualquier momento y lugar.
El mismo proceso que nos convirtió a todos en consumidores constantes de información geográfica también
nos da la oportunidad de ser los productores. Sin dudas, hacer mapas se ha vuelto más fácil que nunca antes.
Existen cada vez más repositorios con información georreferenciada de acceso publico -datasets que incluyen
información precisa sobre su ubicación geográfica. Al mismo tiempo, maduran y se hacen más fáciles de usar
las herramientas para análisis y visualización espacial.
En los procesos sociales, el “dónde” suele ser un aspecto clave, central para quienes estudiamos, por ejemplo,
las ciudades o las dinámicas de la política, siempre tan arraigadas a lo territorial. Esto vuelve al mapa una
de las herramientas de visualización más importantes que podemos emplear.
En R contamos con varios paquete de funciones que permiten manipular información espacial con facilidad.
A continuación vamos a aprender a combinarlos con las herramientas que ya hemos aprendido, para hacer
análisis geográfico y crear nuestros propios mapas.
93
94 CHAPTER 6. INFORMACIÓN GEOGRÁFICA Y MAPAS
Figure 6.1: Distintos sistemas de proyección cartográfica (cortesía Daniel R. Strebe 2011)
Los CRS son un sistema de números que definen ubicaciones sobre la superficie de la Tierra; funcionan como
direcciones. El tipo de CRS más conocido es el que usa latitud y longitud, para definir posiciones en los ejes
norte-sur y este-oeste.
Las proyecciones cartográficas son instrucciones para traducir a un plano la disposición de puntos ubicados
en la esfera terrestre. Algo así como las instrucciones para dibujar en dos dimensiones las disposición de
fronteras, accidentes geográficos, calles o cualquier otro objeto que se extiende sobre la superficie curva del
planeta. Como en toda traducción, hay algo que se pierde en el proceso. Todo los mapas “mienten”, en el
sentido en que presentan una versión distorsionada de la superficie de terrestre. Esto es inevitable; no existe
forma de pasar de la esfera al plano sin distorsionar la forma, la superficie, la distancia o la dirección de los
rasgo geográficos. Existen muchísimas proyecciones distintas, cada una pensada para minimizar alguno de
los tipos de distorsión, o para encontrar una solución de compromiso que los balancee.
La proyección más famosa es la Mercator, diseñada para asistir la navegación marítima y en uso desde el
siglo XVI. Su fuerte es que no distorsiona las direcciones, por lo que permite fijar el rumbo de navegación
consultando el mapa. Su principal problema es que produce una distorsión notable en las áreas cercanas a los
polos: Groenlandia aparenta el mismo tamaño que toda África, cuando en realidad tiene sólo un quinceavo
de su superficie. Por esa razón perdió la proyección popularidad en el siglo XX cuando comenzaron a
preferirse proyecciones que respetan las áreas, como las de la Figura 6.1. Sin embargo, en el siglo XXI
la proyección Mercator recuperó protagonismo. Google la eligió para sus mapas en línea, y por razones
de compatibilidad otros proveedores de mapas digitales la adoptaron también. Así, y para inconsolable
irritación de los geógrafos, Mercator se convirtió en el estándar de facto para aplicaciones geográficas en la
web.
En la práctica, si trabajamos en forma frecuente con archivos georreferenciados vamos a sufrir tarde o
temprano de problemas de coordenadas o proyección. El más común de ellos: tener una fuentes de datos
geográficos que no podemos comparar con otras, porque desconocemos el sistema de coordenadas que se usó
6.1. LOS DATOS GEORREFERENCIADOS 95
para crearla; es decir, no podemos saber a que posición sobre el planeta corresponde cada observación en los
datos.
Otro problema asociado a trabajar con datos geográficos es el de los formatos de archivo. El formato más
común es el denominado “shapefile”, inventado por la empresa ESRI (los creadores del software ArcGIS). Es
un formato incómodo porque guarda la información en varios archivos distintos, que suelen ser combinados
en un archivo .zip para su distribución. Un inconveniente aún mayor es que los nombres de las variables en
un shapefile deben tener 10 caracteres o menos, lo que facilita el uso de abreviaturas ininteligibles. A pesar
de éstos y otros detrimentos, el formato es tan común que se ha vuelto sinónimo de archivo con información
geográfica, y resiste a pesar de los esfuerzos por reemplazarlo con alternativas más modernas. Una de ellas
es “GeoJSON”, un estándar abierto que corrige los dos inconvenientes mencionados antes. Para nuestros
ejercicios usaremos datos geográficos en esta último formato.
Como hemos hecho antes, practicaremos con datos tomados del portal de datos abiertos de la Ciudad de
Buenos Aires. En esta ocasión se trata de los radios censales de la ciudad. Los radios censales son particiones
de la superficie de la ciudad que contienen una cantidad similar de hogares. Fueron definidos por el Instituto
Nacional de Estadística y Censos (INDEC) para facilitar la labor durante la jornada del Censo Nacional
de Población que se realiza cada diez años. La idea es asignar a cada censista un radio censal, estimando
que puede recorrer todos los hogares incluidos durante el día. Los radios censales son la unidad de análisis
espacial por excelencia, debido a que combinan alta granularidad con abundante información asociada de
acceso público, producida como resultado del Censo.
A trabajar entonces. Si no lo hicimos aún, carguemos las librerías sf y tidyverse
library(sf)
library(tidyverse)
• bbox: xmin: -58.53092 ymin: -34.70574 xmax: -58.33455 ymax: -34.528: nos da
cuatro valores que forman una “caja” (bounding box), el rectángulo que contiene todos los datos. Estos
valores son la latitud mínima, la longitud mínima, la latitud máxima y la longitud máxima del conjunto
de datos. Sólo es útil cuando tenemos mucha práctica y ya reconocemos lugares por sus coordenadas.
• epsg (SRID): 4326 y proj4string: +proj=longlat +datum=WGS84 +no_defs significan lo
mismo, que nuestros datos usan el sistema de coordenadas WGS84, también conocido por su código
EPSG 4326 . Es el mismo que usan los sistemas GPS, Google Maps, y las aplicaciones de internet en
general. Es importante prestar atención al sistemas de coordenadas, o CRS, ya que para comparar
datos geográficos de distintas fuentes todas deben usar el mismo.
Como con cualquier otro dataset, comenzamos nuestra exploración pidiendo su resumen:
summary(radios)
Podemos sacar en limpio varias cosas. RADIO_ID, por su nombre, debe ser el código que identifica cada
radio censal. Tenemos columnas representando barrio y comuna de cada radio. Tenemos una columna para
la población, y vemos que así como algún radio está deshabitado, el más poblado alcanza los 3945 habitantes.
En cantidad de viviendas, el máximo es de 1405, y el de hogares 1093: eso significa que existe al menos
un radio censal donde hay viviendas desocupadas; tomamos nota para revisarlo luego. “HOGARES_NBI”
representa la cantidad de hogares donde se registró que al menos una de las necesidades básicas no estaba
satisfecha, con mínimo de 0 por radio, y máximo nada menos que de 403. También tenemos una columna
con el área en km^2, que muestra que en general los radios censales abarcan alrededor de medio kilómetro
cuadrado, pero existe alguno que es casi 8 veces mayor al promedio. Por último queda la columna geometry,
que contiene una serie de puntos que permiten trazar la silueta de cada radio (sus polígonos). Nosotros no
vamos a prestarle atención, pero para R es fundamental, ya que le permite proyectar mapas y hacer cálculos
geométricos cuando se lo pidamos.
98 CHAPTER 6. INFORMACIÓN GEOGRÁFICA Y MAPAS
34.55°S
34.6°S
34.65°S
34.7°S
Ademas de encontrarnos con la reconocible silueta de la ciudad, comprobamos lo que el resumen de la data
había sugerido: la mayoría de los radios censales tiene un tamaño similar, pero existe un puñado que es
considerablemente más extenso que el promedio. Los “mega radios” seguramente corresponden a zonas poco
habitadas, por lo que se asume que un censista puede terminar de encuestar a todos los residentes en un día.
34.55°S
VIVIENDAS
34.6°S
1000
500
34.65°S 0
34.7°S
EL grosor de la línea que traza las fronteras entre radios hace difícil determinar el color de relleno. Esto
suele pasar cuando se grafica información geográfica intrincada como la de los radios censales. Una solución
es definir el color de la línea como NA, que para ggplot significa “ninguno”. Lo hacemos así:
ggplot() + geom_sf(data = radios, aes(fill = POBLACION), color = NA)
100 CHAPTER 6. INFORMACIÓN GEOGRÁFICA Y MAPAS
34.55°S
POBLACION
34.6°S
3000
2000
1000
34.65°S 0
34.7°S
Así esta mejor. Nótese que definimos el color por fuera de aes(). Cuando queremos asignar un valor fijo a
alguno de los atributos estéticos (y no dependiente de una variable) siempre va fuera de la función aes().
En cuanto al gráfico, observamos que los radios censales más grandes tienden a ser poco poblados, con
algunas excepciones, en particular el gran radio censal al oeste. ¿A qué barrio corresponde?
ggplot() + geom_sf(data = radios, aes(fill = BARRIO), color = NA)
6.4. VISUALIZANDO INFORMACIÓN GEOGRÁFICA 101
BARRIO
AGRONOMIA MONSERRAT SAN TELMO
ALMAGRO MONTE CASTRO VELEZ SARSFIELD
BALVANERA NUEVA POMPEYA VERSALLES
BARRACAS NUÑEZ VILLA CRESPO
BELGRANO PALERMO VILLA DEL PARQUE
BOCA PARQUE AVELLANEDA VILLA DEVOTO
34.55°S
BOEDO PARQUE CHACABUCO VILLA GRAL. MITRE
34.6°S
CABALLITO PARQUE CHAS VILLA LUGANO
34.65°S CHACARITA PARQUE PATRICIOS VILLA LURO
COGHLAN PATERNAL VILLA ORTUZAR
34.7°S
58.5°W
58.45°W
58.4°W
58.35°W COLEGIALES PUERTO MADERO VILLA PUEYRREDON
CONSTITUCION RECOLETA VILLA REAL
FLORES RETIRO VILLA RIACHUELO
FLORESTA SAAVEDRA VILLA SANTA RITA
LINIERS SAN CRISTOBAL VILLA SOLDATI
MATADEROS SAN NICOLAS VILLA URQUIZA
Hemos logrado otro de nuestros gráficos ilegibles, intentando mostrar demasiadas variables categóricas a la
vez. Una forma de resolver el dilema es filtrando los datos para aislar los casos de interés. Del menú de
visualizaciones que aprendimos en el capítulo 2, podemos elegir el histograma para mostrar la distribución
de tamaños de nuestros radios censales.
ggplot() + geom_histogram(data = radios, aes(x = AREA_KM2))
102 CHAPTER 6. INFORMACIÓN GEOGRÁFICA Y MAPAS
2000
count
1000
0 1 2 3 4
AREA_KM2
Cómo había anticipado el resumen vía summary(), la gran mayoría de los radios tiene menos de medio km^2.
Unos pocos superan los 2 km^2, así que vamos a aislar esos para saber a que barrio corresponden.
filtrados <- radios %>%
filter(AREA_KM2 > 2)
ggplot() +
geom_sf(data = filtrados, aes(fill = BARRIO)) +
labs(title = "Radios censales de mayo tamaño")
6.4. VISUALIZANDO INFORMACIÓN GEOGRÁFICA 103
34.55°S
BARRIO
BELGRANO
34.6°S
BOCA
PALERMO
PUERTO MADERO
VILLA RIACHUELO
34.65°S VILLA SOLDATI
34.7°S
58.46°W
58.44°W
58.42°W
58.4°W
58.38°W
58.36°W
58.34°W Nuestro gran radio censal al
este, con población considerable, corresponde a Puerto Madero.
Llevemos ahora nuestra atención al tema de la cantidad de viviendas superando a la de hogares. Tal
situación implica que hay una tasa de vacancia alta en el radio censal. Podemos verla en el mapa graficando
la intensidad de la relación entre viviendas y hogares, expresándola como la división de una por otra.
ggplot() + geom_sf(data = radios, aes(fill = VIVIENDAS/HOGARES), color = NA)
104 CHAPTER 6. INFORMACIÓN GEOGRÁFICA Y MAPAS
34.55°S
VIVIENDAS/HOGARES
34.6°S 20
15
10
5
34.65°S
34.7°S
Hay un radio censal que parece brillar, destacándose entre los demás. ¿Dónde está? Esta vez lo resolvemos
en forma analítica en lugar de visual, usando los verbos de transformación de datos. Vamos a definir una
variable nueva, con la tasa entre viviendas y hogares que ya usamos para el gráfico. Luego vamos a ordenar
el dataframe por orden descendiente de la tasa, y usando head() nos quedamos sólo con los primeros valores,
que corresponden a los más altos:
radios %>%
mutate(viv_vs_hogares = VIVIENDAS / HOGARES) %>%
arrange(desc(viv_vs_hogares)) %>%
head()
Otra vez Puerto Madero, que contiene un radio censal con una vacancia notable, el segundo de la lista: con
473 viviendas disponibles, se asentaron allí sólo 20 hogares. El que se llevó el primer puesto, también en
Puerto Madero, obtuvo una tasa de “Inf”, o infinito. Esto ocurre porque allí tenemos 0 hogares, y al dividir
por esa cantidad no se obtiene un número. Conociendo al barrio, podemos sospechar que la especulación
inmobiliaria es la causa de las viviendas vacías. El resto de los radios censales del ranking corresponde a
San Nicolás, el barrio más céntrico de la ciudad, donde la gran cantidad de departamentos dedicados a uso
comercial o profesional explicaría la baja cantidad de hogares.
Algo importante que no hemos mencionado aún es la importancia de “normalizar” las variables antes de
mostrarlas en un mapa. Con esto me refiero a que, en general, no interesan tanto los valores absolutos sino
puestos en contexto. Ejemplos típicos:
• En lugar de mostrar “número de crímenes por barrio” es más instructivo mostrar el número de
crímenes per cápita; de lo contrario es de esperar que los lugares más poblados siempre estén a la
cabeza, lo cual no agrega demasiada información.
• En lugar de mostrar “cantidad de habitantes por radio censal”, suele preferirse mostrar la densidad de
población, es decir la cantidad de habitantes dividida por la extensión del área. Los mapas de densidad
muestran mucho mejor la distribución espacial de la población.
Densidad de población
Ciudad Autónoma de Buenos Aires
34.55°S
hab/km2
160000
34.6°S
120000
80000
40000
34.65°S 0
34.7°S
Este último gráfico representa de forma mucho mas precisa la distribución de habitantes en la ciudad,
haciendo saltar a la vista los núcleos con mayor densidad de población. De paso, aprendimos un truco nuevo:
agregando scale_fill_viridis_c() le pedimos a ggplot que utilice la escala de colores conocida como
“viridis”, diseñada por expertos en visualización para ser fácil de leer… y lucir bien.
En algunos casos, un archivo con información geográfica contiene todos los datos que necesitamos. Pero lo
habitual es que el archivo sólo brinde la ubicación y fronteras de nuestras unidades de análisis, de manera
que necesitamos agregarle los datos que hemos obtenido de otras fuentes y queremos proyectar en un mapa.
En el capítulo 2 aprendimos a usar la función left_join() para combinar tablas. Dado que los datos
espaciales cargados vía sf() son dataframes -tablas-, podemos usarla para agregar variables a nuestros
radios censales. Por ejemplo, las del dataset de interacciones de la ciudadanía con la ciudad.
Lo cargamos,
atencion_ciudadano <- read.csv("http://bitsandbricks.github.io/data/gcba_suaci_comunas.csv")
suerte para nosotros, el dataset con los radios censales incluye columnas con barrio y comuna, así que las
podemos usar para el cruce. Si no dispusiéramos de esa información, hubiéramos tenido que tomar el camino
largo. Este consiste en conseguir un archivo espacial que contenga los límites de los barrios (o comunas) y
hacer una operación llamada spatial join para cruzar los datos en base a sus coordenadas geográficas. La
“unión espacial” permite poner condiciones como “unir los datos X con los datos Y en caso de que X esté
adentro de Y”. Nosotros no vamos a necesitar recurrir a un spatial join, pero es bueno saber que la opción
existe (la función es st_join()) en caso de que la necesitemos en el futuro.
Para poder cruzar las tablas de atención ciudadana y la de datos espaciales, necesitamos que la unidad de
observación (la entidad que representa cada fila) sea la misma. Cómo el dataset de atención es el menos
detallado a nivel espacial, corresponde hacer un agregado de los radios censales para calcular sus datos a
nivel barrio o comuna. Vamos con los barrios, usando a nuestros viejos amigos group_by y summary.
Todas las unidades numéricas representan valores absolutos (no proporciones) así que es fácil pasarlas a un
agregado por barrio; basta con sumarlas.
barrios_geo <- radios %>%
group_by(BARRIO) %>%
summarise(POBLACION = sum(POBLACION),
VIVIENDAS = sum(VIVIENDAS),
HOGARES = sum(HOGARES),
HOGARES_NBI = sum(HOGARES_NBI),
AREA_KM2 = sum(AREA_KM2))
Y esto es lo lindo de trabajar con datos geográficos en forma de tabla: la columna geometry, la que guarda
la información espacial, se crea en forma automática al hacer el summarise, y contiene la fronteras de la
unidad de agregación - los barrios.
ggplot() + geom_sf(data = barrios_geo)
108 CHAPTER 6. INFORMACIÓN GEOGRÁFICA Y MAPAS
34.55°S
34.6°S
34.65°S
34.7°S
Como efecto secundario de la operación (que en la jerga del GIS se conoce como “disolver polígonos”)
podemos ver algunas líneas internas que han quedado como residuo de la unión de los radios censales. Es un
problema muy común al trabajar con datos geográficos, dependiendo de la calidad de la fuente. Por suerte,
en este caso el pequeño desperfecto no afecta nuestros planes. En pos de la prolijidad, podríamos realizar
un ajuste fino y eliminar esas líneas internas, ya que hay varias técnicas para ello. Pero la complejidad de la
tarea haría demasiado larga la explicación, así que vamos a dejarlo así… un recordatorio de que al trabajar
con datos “reales” pasan estas cosas.
Ahora hagamos también un agregado por barrio de los datos de atención al ciudadano,
atencion_por_barrio <- atencion_ciudadano %>%
group_by(BARRIO) %>%
summarise(total = sum(total))
head(atencion_por_barrio)
## # A tibble: 6 x 2
## BARRIO total
## <fct> <int>
## 1 " " 5722
## 2 AGRONOMIA 9604
## 3 ALMAGRO 54190
## 4 BALVANERA 49540
## 5 BARRACAS 31752
## 6 BELGRANO 56522
Ya tenemos las piezas necesarias: dos datasets con una columna en común que los relaciona (“BARRIO”)
permitiendo cruzar los datos. Queremos conservar todas las observaciones del dataset geográfico, agregando
los datos contenidos en el dataset de atención en donde la variable BARRIO sea la misma.
6.5. VOLCANDO EN EL MAPA INFORMACIÓN DE MÚLTIPLES FUENTES 109
34.55°S
total
34.6°S
300000
200000
100000
34.65°S
34.7°S
Tal como habíamos verificado cuando hicimos la exploración del dataset en el capítulo 2, en los barrios
céntricos se registra la mayoría de los contactos. Podemos mejorar un poco el mapa, normalizando los datos
para mostrar valores per cápita.
ggplot() +
geom_sf(data = barrios_geo, aes(fill = total/POBLACION)) +
labs(title = "Contactos a atención ciudadana per cápita",
subtitle = "Barrios de Ciudad Autónoma de Buenos Aires",
fill = "contactos/habitante")
110 CHAPTER 6. INFORMACIÓN GEOGRÁFICA Y MAPAS
34.55°S
contactos/habitante
34.6°S
10
34.65°S
34.7°S
Normalizar los datos hace evidente que los barrios son parejos en su grado de demanda por habitante,
exceptuando los casos salientes que mencionamos antes.
Hasta ahora hemos mostrado sobre un mapa variables numéricas, pero es igual de fácil representar variables
categóricas. Imaginemos que quisiéramos mostrar el principal rubro por el cual se comunican los ciudadanos
en cada barrio.
Usando los verbos de transformación que conocemos, la receta sería:
1. Agrupar los datos por barrio y por rubro
2. Crear un resumen con el total de contactos por rubro en cada barrio
3. Por cada grupo, filtrar los datos para conservar sólo el rubro que tiene la cantidad más grande de
contactos
atencion_por_barrio_principal_rubro <- atencion_ciudadano %>%
group_by(BARRIO, RUBRO) %>%
summarise(contactos = sum(total)) %>%
filter(contactos == max(contactos))
head(atencion_por_barrio_principal_rubro)
## # A tibble: 6 x 3
## # Groups: BARRIO [6]
## BARRIO RUBRO contactos
## <fct> <fct> <int>
## 1 " " REGISTRO CIVIL 5004
## 2 AGRONOMIA SANEAMIENTO URBANO 4691
## 3 ALMAGRO SANEAMIENTO URBANO 22429
## 4 BALVANERA SANEAMIENTO URBANO 16840
6.5. VOLCANDO EN EL MAPA INFORMACIÓN DE MÚLTIPLES FUENTES 111
1. group_by() agrupa los datos por barrio, y para cada barrio agrupa los datos por rubro.
2. summarise() “pela” una capa de agrupamiento, la más externa que es rubro, y deja para cada barrio
una sola fila por rubro, con la suma de sus totales.
34.55°S
34.6°S
RUBRO
REGISTRO CIVIL
SANEAMIENTO URBANO
34.65°S
34.7°S
Comentamos al principio de capítulo que los archivos con datos espaciales pueden representar áreas (polí-
gonos), líneas o puntos. Hasta ahora hemos hecho mapas con polígonos, pero según el caso podríamos querer
mostrar otros tipos de geometría.
Un ámbito donde es común utilizar toda la variedad de geometrías es el transporte: Polígonos para representar
distritos, líneas para el recorrido de un sistema de transporte, y puntos para la ubicación de las estaciones.
Una vez más, el portal de datos abiertos de la Ciudad de Buenos Aires nos provee los datos necesarios para
practicar. De allí he descargado, y preparado para su uso, datos espaciales con
Combinar capas mostrado distintas geometrías es simple usando sf() y ggplot(). Sólo es cuestión de sumar
capas de geom_sf() con cada fuente de datos:
ggplot() +
geom_sf(data = barrios_geo) +
geom_sf(data = subte_lineas, color = "yellow") +
geom_sf(data = subte_estaciones, color = "orange") +
labs(title = "Sistema de transporte subterráneo (SUBTE)",
subtitle = "Ciudad de Buenos Aires")
6.6. COMBINANDO CAPAS GEOGRÁFICAS 113
34.55°S
34.6°S
34.65°S
34.7°S
¡Voilà! Podríamos también aprovechar el lienzo en blanco de la superficie de los barrios para hacer un mapa
temático que muestre la cantidad de incidentes en la base de atención ciudadana relacionados con el SUBTE.
Para revisar todos los rubros de atención, podemos usar summary(atencion_ciudadano$RUBRO); en lugar
de pedir un resumen del dataframe completo, le estamos indicando a R que nos interesa el detalle de la
columna “RUBRO”. Entre las 346 categorías, una es “EMERGENCIAS EN SUBTE”. Suena interesante.
¿Dónde se han registrado más incidentes con emergencias? Lo responderemos con un mapa. A priori
asumimos que no vamos a encontrar casos en en los barrios por donde no pasa el SUBTE, lo cual también
podremos comprobar.
Primero creamos un subconjunto de los datos con la suma, por barrio, de emergencias reportadas
emergencias_en_subte <- atencion_ciudadano %>%
filter(RUBRO == "EMERGENCIAS EN SUBTE") %>%
group_by(BARRIO) %>%
summarise(emergencias = sum(total))
34.55°S
emergencias reportadas
40
34.6°S
30
20
10
34.65°S
34.7°S
Aquí se cumplen las expectativas: la cantidad de emergencias reportadas sigue en forma clara un patrón
relacionado con la cantidad de estaciones ubicadas en cada barrio. Y como era de esperarse, en barrios donde
el SUBTE no ofrece cobertura la cantidad de emergencias es nula.
Con eso cerramos el capítulo, y el libro. Hemos llegado al brusco final… al menos brusco por ahora. Contin-
uaré puliendo el texto, y más adelante quisiera agregar un breve capítulo de consideraciones finales.
Mientras tanto, mi mensaje final es fácil de resumir. Espero que “Ciencia de Datos para Gente Sociable”
haya sido una introducción satisfactoria al análisis, modelado y visualización de información, y tan sólo el
inicio de un largo y gratificante recorrido.
¡Gracias por haber leído hasta aquí!