Capítulo 3 Poniendo los datos en forma
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.
3.1 Primeros pasos al examinar un conjunto de datos nuevo
Si no lo hicimos aún en la sesión en la que estamos trabajando, cargamos tidyverse
.
library(tidyverse)
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://cdn.buenosaires.gob.ar/datosabiertos/datasets/sistema-unico-de-atencion-ciudadana/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)
## 'data.frame': 57431 obs. of 5 variables:
## $ PERIODO : int 201301 201301 201301 201301 201301 201301 201301 201301 201301 201301 ...
## $ RUBRO : Factor w/ 346 levels "ACCESOS","ACERAS",..: 2 2 2 2 2 2 2 2 2 2 ...
## $ TIPO_PRESTACION: Factor w/ 5 levels "DENUNCIA","QUEJA",..: 3 3 3 3 3 3 3 3 3 3 ...
## $ BARRIO : Factor w/ 51 levels " ","AGRONOMIA",..: 2 3 4 5 6 7 8 9 10 11 ...
## $ total : int 6 172 92 45 79 10 38 109 20 45 ...
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 PERIODO, 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 integers 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 columnas 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)
## PERIODO RUBRO TIPO_PRESTACION
## Min. :201301 SANEAMIENTO URBANO : 4589 DENUNCIA :21606
## 1st Qu.:201309 TRANSPORTE Y TRANSITO: 4580 QUEJA : 3914
## Median :201404 ARBOLADO : 3122 RECLAMO :21038
## Mean :201401 ALUMBRADO : 2918 SOLICITUD: 9662
## 3rd Qu.:201503 PAVIMENTO : 2411 TRAMITE : 1211
## Max. :201512 ESPACIO PUBLICO : 1918
## (Other) :37893
## BARRIO total
## PALERMO : 2154 Min. : 1.00
## BALVANERA : 1961 1st Qu.: 1.00
## FLORES : 1959 Median : 4.00
## CABALLITO : 1872 Mean : 34.85
## SAN NICOLAS: 1748 3rd Qu.: 16.00
## RECOLETA : 1729 Max. :19221.00
## (Other) :46008
Las categorías posibles para un factor son llamadas “niveles” (levels). Para ver todos los niveles del factor BARRIO, es decir todos los barrios representados en la columna con la variable BARRIO, podemos usar la función levels()
levels(atencion_ciudadano$BARRIO)
## [1] " " "AGRONOMIA" "ALMAGRO"
## [4] "BALVANERA" "BARRACAS" "BELGRANO"
## [7] "BOCA" "BOEDO" "CABALLITO"
## [10] "CHACARITA" "COGHLAN" "COLEGIALES"
## [13] "CONSTITUCION" "ERRORNOHAYRESULTA" "ERRORNOHAYRESULTADOS"
## [16] "FLORES" "FLORESTA" "LINIERS"
## [19] "MATADEROS" "MONSERRAT" "MONTE CASTRO"
## [22] "NUEVA POMPEYA" "NUÑEZ" "PALERMO"
## [25] "PARQUE AVELLANEDA" "PARQUE CHACABUCO" "PARQUE CHAS"
## [28] "PARQUE PATRICIOS" "PATERNAL" "PUERTO MADERO"
## [31] "RECOLETA" "RETIRO" "SAAVEDRA"
## [34] "SAN CRISTOBAL" "SAN NICOLAS" "SAN TELMO"
## [37] "VELEZ SARSFIELD" "VERSALLES" "VILLA CRESPO"
## [40] "VILLA DEL PARQUE" "VILLA DEVOTO" "VILLA GRAL. MITRE"
## [43] "VILLA LUGANO" "VILLA LURO" "VILLA ORTUZAR"
## [46] "VILLA PUEYRREDON" "VILLA REAL" "VILLA RIACHUELO"
## [49] "VILLA SANTA RITA" "VILLA SOLDATI" "VILLA URQUIZA"
Para acceder en forma rápida al contenido de la columna BARRIO, hemos utilizado por primera vez un “truco” muy práctico. Para obtener el contenido de cualquier columna en particular, basta con el nombre del dataframe seguido del símbolo $
y el nombre de la columna a extraer: atencion_ciudadano$BARRIO
, o atencion_ciudadano$total
, etc.
3.2 Cruzando variables: la operación join
Al realizar un análisis “en la vida real”, es decir, usando datos salvajes en lugar de los prolijos datasets de práctica, es muy habitual encontrar que nos falta una variable que necesitamos. Si tenemos suerte, la información que necesitamos también está disponible en forma de tabla, con algún campo en común, y podemos llevar el cabo un cruce de datos para traérnosla.
Para expresarlo con un ejemplo concreto: hemos visto que los registros de atención al ciudadano incluyen una columna con el barrio, que es la única variable relacionada con la geografía. Si nuestra unidad de análisis fuera la columna en lugar del barrio, necesitaríamos agrega la columna correspondiente. En este caso, estamos de suerte porque una tabla con los barrios de la Ciudad de Buenos Aires y la comuna a la que pertenecen es fácil de conseguir. Con esa tabla en nuestro poder, ya tenemos las piezas necesarias para el cruce de datos. En cada registro en el dataframe de atención al ciudadano, tenemos un barrio; podemos buscarlo en la tabla de barrios y comunas, tomar nota de la comuna asociada, y copiarla en nuestro dataset original. Por supuesto, hacerlo a mano para cada uno de las 57.432 filas en nuestro dataframe tardaría una eternidad, amén de que quizás perderíamos la cordura antes de terminar. ¡Nada de eso! Vamos a resolverlo en meros instantes escriviendo unas pocas líneas de código. Antes de continuar hagamos una pausa para conmiserar a los investigadores de eras pasadas, antes de la popularización de la computadora personal, que realizaban tareas de esta escala con lápiz, papel y paciencia.
Existe una gran variedad de funciones que permiten combinar tablas relacionadas entre sí por una o varias variables en común. Para nuestro propósito, alcanza con conocer una: left_join()
. La funcion toma como parámetros dos dataframes (que son tablas al fin y al cabo) busca las variables que tengan el mismo nombre y usandolas como referencia completa la primera de ellas, la de la izquierda, con los datos nuevos que aporta la segunda. left_join
devuelve un dataframe nuevo con los datos combinados.
Manos a la obra. Descargamos el dataframe con barrios y comunas,
barrios_comunas <- read.csv("http://cdn.buenosaires.gob.ar/datosabiertos/datasets/barrios/barrios_comunas_p_Ciencia_de_Datos_y_PP.csv")
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
## 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)
Admiremos nuestra obra:
head(atencion_ciudadano)
## PERIODO RUBRO TIPO_PRESTACION BARRIO total COMUNA
## 1 201301 ACERAS RECLAMO AGRONOMIA 6 15
## 2 201301 ACERAS RECLAMO ALMAGRO 172 5
## 3 201301 ACERAS RECLAMO BALVANERA 92 3
## 4 201301 ACERAS RECLAMO BARRACAS 45 4
## 5 201301 ACERAS RECLAMO BELGRANO 79 13
## 6 201301 ACERAS RECLAMO BOCA 10 4
Es así de fácil. Bueno, no tanto… este fue un caso sencillo, pero hay todo tipo de datos y cruces allí afuera, y a veces se necesitan operaciones más complehas. Por eso hay toda una familia de funciones de join - right_join()
, inner_join()
, full_join
, anti_join()
, y alguna más. Pero podemos dejarlas en paz; para nuestras necesidades, con left_join()
podemos areglarnos muy bien.
Satisfechos con la mejora, si queremos guardar el dataframe “mejorado” para usarlo en otra ocasión, podemos hacerlo con write.csv()
, que lo convierte en un archivo de texto que queda en nuestra PC.
write.csv(atencion_ciudadano, "atencion_ciudadano.csv", row.names = FALSE)
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()
## [1] "/home/havb/Dropbox/Work/GCBA/curso_DS/Ciencia de Datos y Políticas Públicas"
El resultado será la dirección (la ubicacion de la la carpeta), donde estamos trabajando y hemos guardado los datos; por ejemplo /home/antonio/Practicando R/
.
3.3 Transformando los datos
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 nombrefilter()
: filtrar, es decir quedarse sólo con las filas que cumplan cierta condiciónarrange()
: ordenar las filas de acuerdo a su contenido o algún otro índicemutate()
: mutar -cambiar- un dataframe, modificando el contenido de sus columnas o creando columnas (es decir, variables) nuevassummarise()
: producir sumarios -un valor extraído de muchos, por ejemplo el promedio- con el contenido 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 componentes de la familia de paquetes Tidyverse. Ya tenemos disponible todo lo necesario, activado cuando invocamos library(tidiverse)
al comienzo.
Manos a la obra.
3.3.1 Seleccionar columnas con select()
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)
## [1] "PERIODO" "RUBRO" "TIPO_PRESTACION" "BARRIO"
## [5] "total" "COMUNA"
Si quisiéramos sólo las que contienen el período y el total, las seleccionamos por nombre, a continuación del nombre del dataframe:
seleccion <- select(atencion_ciudadano, PERIODO, total)
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)
## RUBRO TIPO_PRESTACION BARRIO
## 1 ACERAS RECLAMO AGRONOMIA
## 2 ACERAS RECLAMO ALMAGRO
## 3 ACERAS RECLAMO BALVANERA
## 4 ACERAS RECLAMO BARRACAS
## 5 ACERAS RECLAMO BELGRANO
## 6 ACERAS RECLAMO BOCA
Y podemos seleccionar por omisión. Si nos interesara todo el contenido del dataset menos la variable RUBRO, usaríamos
seleccion <- select(atencion_ciudadano, -RUBRO)
head(seleccion)
## PERIODO TIPO_PRESTACION BARRIO total COMUNA
## 1 201301 RECLAMO AGRONOMIA 6 15
## 2 201301 RECLAMO ALMAGRO 172 5
## 3 201301 RECLAMO BALVANERA 92 3
## 4 201301 RECLAMO BARRACAS 45 4
## 5 201301 RECLAMO BELGRANO 79 13
## 6 201301 RECLAMO BOCA 10 4
Al igual que con las selección por inclusión, podemos seleccionar por omisión de un rango de columnas contiguas (escritas entre paréntesis), o de varias columnas nombradas:
seleccion <- select(atencion_ciudadano, -(TIPO_PRESTACION:total))
head(seleccion)
## PERIODO RUBRO COMUNA
## 1 201301 ACERAS 15
## 2 201301 ACERAS 5
## 3 201301 ACERAS 3
## 4 201301 ACERAS 4
## 5 201301 ACERAS 13
## 6 201301 ACERAS 4
seleccion <- select(atencion_ciudadano, -RUBRO, -BARRIO)
head(seleccion)
## PERIODO TIPO_PRESTACION total COMUNA
## 1 201301 RECLAMO 6 15
## 2 201301 RECLAMO 172 5
## 3 201301 RECLAMO 92 3
## 4 201301 RECLAMO 45 4
## 5 201301 RECLAMO 79 13
## 6 201301 RECLAMO 10 4
3.3.2 Filtrar filas con filter()
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)
## PERIODO RUBRO TIPO_PRESTACION BARRIO total COMUNA
## 1 201401 ACERAS RECLAMO RETIRO 10 1
## 2 201401 ALUMBRADO RECLAMO RETIRO 34 1
## 3 201401 ALUMBRADO SOLICITUD RETIRO 2 1
## 4 201401 ARBOLADO RECLAMO RETIRO 10 1
## 5 201401 ARBOLADO SOLICITUD RETIRO 3 1
## 6 201401 ATENCION AL PUBLICO QUEJA RETIRO 3 1
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
* > 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)
## PERIODO RUBRO TIPO_PRESTACION BARRIO total COMUNA
## 1 201301 ACERAS RECLAMO ALMAGRO 172 5
## 2 201301 ACERAS RECLAMO CABALLITO 109 6
## 3 201301 ACERAS RECLAMO FLORES 111 7
## 4 201301 ACERAS RECLAMO PALERMO 113 14
## 5 201301 ALUMBRADO RECLAMO ALMAGRO 130 5
## 6 201301 ALUMBRADO RECLAMO BARRACAS 118 4
3.3.2.2 Operadores lógicos
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)
## PERIODO RUBRO TIPO_PRESTACION BARRIO total COMUNA
## 1 201508 SALUD QUEJA BARRACAS 1 4
## 2 201508 SALUD QUEJA CABALLITO 1 6
## 3 201508 SALUD QUEJA COGHLAN 1 12
## 4 201508 SALUD QUEJA RECOLETA 1 2
obtenemos todos los registros cuyo rubro es “SALUD”, y cuyo período es 20108, agosto de 2015.
Siguiendo el mismo formato, si intentamos
seleccion <- filter(atencion_ciudadano, BARRIO == "RETIRO", BARRIO == "PALERMO")
head(seleccion)
## [1] PERIODO RUBRO TIPO_PRESTACION BARRIO
## [5] total COMUNA
## <0 rows> (or 0-length row.names)
obtenemos un conjunto vacío. ¿Por qué? Es debido a que ninguna observación cumple con todas las condiciones; el ningún registro el barrio es Retiro y es Palermo. ¡Suena razonable!. Para obtener registros ocurrido en Retiro ó en Palermo, usamos el operador lógico |
que significa… “ó”.
seleccion <- filter(atencion_ciudadano, BARRIO == "RETIRO" | BARRIO == "PALERMO")
head(seleccion)
## PERIODO RUBRO TIPO_PRESTACION BARRIO total COMUNA
## 1 201301 ACERAS RECLAMO PALERMO 113 14
## 2 201301 ACERAS RECLAMO RETIRO 15 1
## 3 201301 ACERAS SOLICITUD PALERMO 2 14
## 4 201301 ACTOS DE CORRUPCION DENUNCIA PALERMO 4 14
## 5 201301 ALUMBRADO RECLAMO PALERMO 74 14
## 6 201301 ALUMBRADO RECLAMO RETIRO 15 1
Se trata de la lógica de conjuntos, o lógica booleana, que con un poco de suerte recordamos de nuestra época de escolares. Los símbolos importantes son &
, |
, y !
: “y”, “ó”, y la negación que invierte preposiciones:
* a & b a y b
* a | b a ó b
* a & !b a, y no b
* !a & b no a, y b
* !(a & b) no (a y b)
Hemos visto ejemplos de a & b
(PERIODO == 201508, RUBRO == "SALUD"
, que filter toma como un &
) y de a | b
(BARRIO == "RETIRO" | BARRIO == "PALERMO"
)
Un ejemplo de a & !b
, filas en las que el tipo de prestación sea “TRAMITE”, y en las que el rubro no sea “REGISTRO CIVIL”:
filter(atencion_ciudadano, TIPO_PRESTACION == "TRAMITE" & !(RUBRO == "REGISTRO CIVIL"))
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)
## PERIODO RUBRO TIPO_PRESTACION BARRIO total COMUNA
## 1 201301 ACERAS RECLAMO AGRONOMIA 6 15
## 2 201301 ACERAS RECLAMO ALMAGRO 172 5
## 3 201301 ACERAS RECLAMO BALVANERA 92 3
## 4 201301 ACERAS RECLAMO BARRACAS 45 4
## 5 201301 ACERAS RECLAMO BELGRANO 79 13
## 6 201301 ACERAS RECLAMO BOCA 10 4
3.3.3 Ordenar filas con arrange()
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)
## PERIODO RUBRO TIPO_PRESTACION BARRIO total COMUNA
## 1 201301 ACERAS RECLAMO PUERTO MADERO 1 1
## 2 201301 ACERAS SOLICITUD BARRACAS 1 4
## 3 201301 ACERAS SOLICITUD BOCA 1 4
## 4 201301 ACERAS SOLICITUD BOEDO 1 5
## 5 201301 ACERAS SOLICITUD COGHLAN 1 12
## 6 201301 ACERAS SOLICITUD CONSTITUCION 1 1
Si agregamos más columnas, se usan en orden para “desempatar”. Por ejemplo, si queremos que las filas con el mismo valor en total aparezcan en el orden alfabético del barrio que les corresponde, sólo necesitamos agregar esa columna:
ordenado <- arrange(atencion_ciudadano, total, BARRIO)
head(ordenado)
## PERIODO RUBRO TIPO_PRESTACION BARRIO total COMUNA
## 1 201301 ALUMBRADO SOLICITUD AGRONOMIA 1 15
## 2 201301 ATENCION SOCIAL RECLAMO AGRONOMIA 1 15
## 3 201301 ESPACIO PUBLICO RECLAMO AGRONOMIA 1 15
## 4 201301 QUEJA QUEJA AGRONOMIA 1 15
## 5 201301 RECUPERADORES RECLAMO AGRONOMIA 1 15
## 6 201301 SEGURIDAD RECLAMO AGRONOMIA 1 15
Si no se aclara lo contrario, el orden siempre es ascendente (de menor a mayor). Si quisiéramos orden de mayor a menor, usamos desc()
:
ordenado <- arrange(atencion_ciudadano, desc(total))
head(ordenado)
## PERIODO RUBRO TIPO_PRESTACION BARRIO total COMUNA
## 1 201502 REGISTRO CIVIL TRAMITE MONSERRAT 19221 1
## 2 201403 REGISTRO CIVIL TRAMITE SAN NICOLAS 19209 1
## 3 201402 REGISTRO CIVIL TRAMITE SAN NICOLAS 17032 1
## 4 201504 REGISTRO CIVIL TRAMITE MONSERRAT 16746 1
## 5 201503 REGISTRO CIVIL TRAMITE MONSERRAT 16730 1
## 6 201506 REGISTRO CIVIL TRAMITE MONSERRAT 14674 1
3.3.3.1 Valores faltantes
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?
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.
3.3.4 Agregar nuevas variables con mutate()
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
## nombre tamaño radio
## 1 Círculo 1 Pequeño 1
## 2 Círculo 2 Mediano 3
## 3 Círculo 3 Grande 5
Podemos agregar una columna con el área de cada círculo con mutate():
mutate(circulos, area = 3.1416 * radio^2)
## nombre tamaño radio area
## 1 Círculo 1 Pequeño 1 3.1416
## 2 Círculo 2 Mediano 3 28.2744
## 3 Círculo 3 Grande 5 78.5400
Usando mutate(), definimos la columna “area”, indicando que su contenido será el valor de la columna “radio” en cada registro puesto en la fórmula del área de un círculo. Los operadores aritméticos (+
, -
, *
, /
, ^
) son con frecuencia útiles para usar en conjunto con mutate().
Volvamos ahora a nuestro dataframe con datos de reclamos. Supongamos que nos interesa agregar columnas con el mes y el año de cada registro. La columna período, con valores del tipo “201301”, contiene la información necesaria para derivar estas dos nuevas variables. Para separar la parte del año de la parte del mes, la función substr()
, que extrae porciones de una variable de texto, nos va a dar una mano. La usamos así: el primer parámetro es una secuencia de caracteres, y los dos siguientes indican donde queremos que empiece y termine la porción a extraer.
atencion_ciudadano <- mutate(atencion_ciudadano,
AÑO = substr(PERIODO, 1, 4),
MES = substr(PERIODO, 5, 6))
head(atencion_ciudadano)
## PERIODO RUBRO TIPO_PRESTACION BARRIO total COMUNA AÑO MES
## 1 201301 ACERAS RECLAMO AGRONOMIA 6 15 2013 01
## 2 201301 ACERAS RECLAMO ALMAGRO 172 5 2013 01
## 3 201301 ACERAS RECLAMO BALVANERA 92 3 2013 01
## 4 201301 ACERAS RECLAMO BARRACAS 45 4 2013 01
## 5 201301 ACERAS RECLAMO BELGRANO 79 13 2013 01
## 6 201301 ACERAS RECLAMO BOCA 10 4 2013 01
3.3.5 Extraer sumarios con summarise()
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)
summarise(agrupado, promedio_totales = mean(total))
## # 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)
sumario <- summarise(agrupado, promedio = mean(total))
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
## 6 2013 06 28.9
… o por año, mes y barrio:
agrupado <- group_by(atencion_ciudadano, AÑO, MES, BARRIO)
sumario <- summarise(agrupado, promedio = mean(total))
head(sumario)
## # A tibble: 6 x 4
## # Groups: AÑO, MES [1]
## AÑO MES BARRIO promedio
## <chr> <chr> <chr> <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
3.3.6 ¡BONUS! El operador “pipe”: %>%
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 barrio;
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 queremos ver)
En código:
solo2014 <- filter(atencion_ciudadano, AÑO == 2014)
solo2014_agrupado_barrio <- group_by(solo2014, BARRIO)
total_por_barrio_2014 <- summarise(solo2014_agrupado_barrio, total = sum(total))
total_por_barrio_2014_ordenado <- arrange(total_por_barrio_2014, desc(total))
head(total_por_barrio_2014_ordenado, 5)
## # A tibble: 5 x 2
## BARRIO total
## <chr> <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
## <chr> <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 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.