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 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 columnas (es decir, variables) nuevas
  • summarise(): 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.