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 columnasEstas 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(tidyverse)
al comienzo.
Manos a la obra.
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:
## [1] "PERIODO" "RUBRO" "TIPO_PRESTACION" "BARRIO" "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:
## 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”:
## 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
## 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:
## 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
## 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
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):
## 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
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:
## 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
Cuando le pasamos múltiples condiciones a filter(), la función devuelve las filas que cumplen con todas.
Por ejemplo, con
## 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
## [1] PERIODO RUBRO TIPO_PRESTACION BARRIO 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… “ó”.
## 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”:
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
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:
## 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:
## 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()
:
## 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
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?
## [1] NA
R no sabe. (Nadie lo sabe, para ser justos)
¿A cuanto asciende la suma de 10 más un valor desconocido?
## [1] NA
Y en particular… ¿es un valor desconocido igual a otro valor desconocido?
## [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()
.
## [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.
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():
## 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
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”:
## 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”.
## # A tibble: 3 × 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))
## `summarise()` has grouped output by 'AÑO'. You can override using the `.groups` argument.
## # A tibble: 6 × 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))
## `summarise()` has grouped output by 'AÑO', 'MES'. You can override using the `.groups` argument.
## # A tibble: 6 × 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 restado. Para empezar, algunas de las que más podrían 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 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 × 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 × 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,
es equivalente a
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.