Functional programming combines the flexibility and power of abstract mathematics with the intuitive clarity of abstract mathematics. Randall Munroe

Introducción.

El objeto de este documento.

La manipulación de datos es una de las actividades que más tiempo insumen en la investigación cuantitiva. Manipular datos implica darles la forma necesaria o aplicarles las transformaciones que nos permiten analizarlos del modo en queremos. Este documento se limita a un sólo ámbito de la manipulación que podríamos dar el nombre de transformación. Otros dos ámbitos deben ser tenidos en cuenta: la adquisición de datos y su limpieza, que serán cubiertos en sendas guías. La adquisición es forzozamente previa a cualquier manipulación, sin algún tipo de datos no tenemos nada que manipular. En R podemos adquirir datos de diversas formas: capturándolos directamente en nuestra consola, importándolos desde archivos en diversos formatos o extrayéndolos desde fuentes en línea.2 La limpieza de datos puede hacerse de manera simultanea con la manipulación, pero hacerlo previamente facilita muchísimo la manipulación.
Para analizar datos es necesario disponer de ellos en nuestro entorno de trabajo R y dotarlos de una estructura que comprendamos tanto nosotros como las funciones de R que utilizaremos. Como veremos, en términos técnicos esto significa que trataremos siempre manejarlos como una estructura de la clase data.frame y en formato tidy o prolijo. Esos también deben estar limpios, es decir, estar claramente especificados en su tipo, codificados de manera consistente y libre ambigüedades. En términos prácticos eso significa que las columnas tendrán nombres claros y comprensibles3 y especificado el tipo de datos que contienen –numérico, factor, caracter, fecha, etc-; que el conjunto de datos estará libre de ambigüedades de codificación y errores, incluídos los ortográficos y que los casos perdidos estarán explícitamente señalados como valores NA. La tranformación de datos será mucho más fluida si trabajamos con conjunto de datos limpio, en este sentido de la palabra. Transformar los datos no significa en modo alguno alterarlos, significa estrictamente cambiarles la forma: modificar su estructura, pero no la información que contienen. Es muy probable que una transformación implique también pérdida de información, pero a cambio obtendremos beneficios de inteligibilidad.

En apretada síntesis la manipulación de datos se resume en cuatro operaciones:

  • Extraer subconjuntos de datos. Ya sea subconjuntos de filas o columnas. En los casos prácticos nunca haremos un análisis sobre la totalidad de los datos.
  • Transformarlos. Realizar operaciones aritméticas o lógicas sobre los datos. En general cuando una variable es contínua realizamos operaciones aritméticas, cuando es categórica, lógicas.
  • Obtener sumarios del conjunto de los datos o por grupos. Con frecuencia el análisis no se hace sobre todos los datos sino sobre medidas sumarias: conteos, medias, desviaciones estandar. -Unirlos con otros datos. La proliferación de bases de datos hace cada vez más útil combinar múltiples fuentes para enriquecer el análisis.

Este documento se basea en un paradigma de trabajo –la programación funcional basada en la inmutabilidad de los datos y la transparencia referencial–, un conjunto de herramientas disponibles en R –reunidas en las librerías tidyr:: y dplyr:: entre otras– y un formato preferido para datos –tidy data– compatible con ese paradigma y herramientas. Con estos tres pilares podermos llevar a cabo las operaciones resumidas previamente.

Manipulación de datos con dplyr:: y magrittr::

Las tareas de operaciones de manipulación de datos que hemos mencionado pueden llevarse a cabo de distintas maneras. La librería base:: de R contiene operadores y funciones para hacerlo, por ejemplo [,] para extraer subconjuntos, subset() para operaciones agrupadas, merge(), cbind() o rbind() para combinar múltiples fuentes de datos. A pesar de ser funciones sumamente flexibles y aplicables a múltiples estructuras de datos no son las más convenientes. Están pensadas para usarlas en en un paradigma procedimental, más que con uno funcional, además generan un código dificil de leer y nos obligan a repetir los nombres de las estructuras de datos en cada llamada. La librería dplyr:: ofrece un conjunto unificado de herramientas de manipulación de datos que induce a la programación funcional, produce código legible cuya comprensión es intuitiva y, al estar implementada en lenguaje C, ofrece tiempos de ejecución muy cortos. El conjunto de herramientas de dplyr:: funciona exclusivamente con la estructura de datos data.frame. Esta estrutcuta no es tan rápida como una matríz para operaciones aritméticas ni tan flexibles como una lista, pero es mucho más fácil de manejar: las columnas son las variables y las filas las observaciones. Una ventaja aún mayor es que un data.frame siempre es un data.frame. Es mucho más fácil organizar los procesos si sabemos exactamente qué entra y qué sale. Las funciones de dplyr:: sólo admiten como imput un data.frame y siempre producen como output un data.frame o un mensaje de error. Esta estabilidad de estructura hace que nuestro código se comporte de una forma predecible y los datos nunca pierdan sus atributos. El output de las funciones es completamente predecible y podemos pasarlo con confianza como input de la siguiente, sin tener que registrarle un nombre en el entorno. Las capacidades de dplyr:: están ampliadas por algunas librerías cómplices. Una importante es magrittr::, que introduce a R el operador binario %>%,4 una tubería a la que leemos en voz alta como “después”. Con %>% enlazamos los pasos de la manipulación de datos de las funciones de dplyr:: en el orden natural de ejecución. Con %>% organizamos una secuencia de análisis que culminará en un gráfico así: importar_datos %>% recodificar %>% hacer_conteos %>% hacer_grafico. Es decir, leer datos de un archivo, después recodificar esos datos, después hacer conteos sobre los datos recodificados y después graficar esos conteos.
dplyr:: importa el operador %>% de magrittr::, así que lo tendremos siempre disponible. Si queremos usar los restantes operadores deberemos cargar la librería.5
Otra librería hermana de dplyr:: es tidyr::, que incluye funciones para corregir algunos errores frecuentes en bases de datos y cambiarles la estrctura. tidyr:: además introduce el concepto datos prolijos o tidy data, una manera consistente de organizar la información contenida en estructura de la clase data.frame que facilita su manipulación. Ya veremos de qué se trata y por qué son importantes. Dado que dplyr:: funciona mejor con datos que tienen cierto formato el primer paso será identificar cuál es ese formato de datos.

La estructura de los datos.

¿Sumarios de datos o base de datos?

Con frecuencia los datos que encotramos en internet no son bases de datos en sentido estricto, sino sumarios o informes que condensan información. Una tabla en la que se registran cada una de las ventas de una frutería, con montos, cantidades, fecha y hora de la transacción, es una base de datos en bruto. Una tabla que resume las ventas mensuales es un sumario. Es muy probable que ese sumario se haya originado en una base de datos, pero esta o no es de acceso público o debemos seguir buscando para encontrarla. Siempre que resulte posible trabajaremos con bases de datos tan desagregadas como nos resulte posible conseguir, ya que tienen mayor cantidad de información. Si es necesario un sumario podemos realizarlo, pero no a la inversa. Es decir, a partir de una base de datos podemos crear los sumarios, pero desde los sumarios no podemos recrear la base de datos.

Base datos cruda
Fecha Producto Precio Cantidad
20/02/2017 Manzana 10 1.0
20/02/2017 Pera 15 2.0
21/03/2017 Ciruela 13 0.5
22/03/2017 Naranja 9 3.0
Sumario de datos
Mes Ventas
Febrero 40.0
Marzo 33.5

En R y con dplyr:: podemos manejar información en versión sumario, cosa que haremos cuando no podemos acceder a los datos crudos. Sin embargo como dplyr:: funciona solamente con data.frames es necesario que esos datos tengan una estructura rectangular.

Datos rectangulares.

COn dplyr:: podemos manipular datos provenientes importados desde casi cualquier formato de archivo, pero si como trabajamos con la estructura data.frame es necesario que cumplan una condición: los datos deben ser rectangulares. Por rectangulares se entiende que están organizados en filas y columnas contíguas y que cada filas tiene el mismo largo que las demás, así como todas las columnas.6 Cuando la intersección de una fila y una columna no tiene un dato válido debe codificarse como NA, not available o información perdida.

Es frecuente que cuando descargamos datos capturados o procesados con Excel los datos no sean rectangulares. Es un práctica tan mala como ampliamente difundida entre los usuarios de Excel usar la misma hoja para alojar los datos y el procesamiento de datos.7 o aprovechar la flexibilidad de esa aplicación para poner datos que corresponden a diferentes temáticas o indicadores en la misma columna. El proceso de importar esos datos mal formateados a R es penoso.

Veamos un ejemplo de datos no rectangulares y rectangulares.

Datos no rectangulares
Ventas
Frutería “La desprolija” NA NA NA NA
Primer Trimestre NA NA NA NA
Manzanas 100 NA NA NA
Peras 120 NA Ventas de fruta de carozo: 614
Ciruelas 85 NA NA NA
Segundo Trimestre NA NA Ventas de frutas de pepita: 166
Manzanas 110 NA NA NA
Peras 40 NA NA NA
Ciruelas 25 NA NA NA
Tercer Trimestre NA NA NA NA
Manzanas 60 NA NA NA
Peras 45 NA NA NA
Ciruelas 22 NA NA NA
Cuatro Trimestre NA NA NA NA
Manzanas 48 NA NA NA
Peras 91 NA NA NA
Ciruelas 34 NA NA NA
Ventas totales en el año: NA 780 NA NA
NA NA NA NA NA
Datos rectangulares
Fruta Primer trimestre Segundo Trimestre Tercer Trimestre Cuatro Trimestre
Manzanas 100 110 60 48
Peras 120 40 45 91
Ciruelas 85 25 22 34

Los datos no rectangulares fueron creados en Excel y R los fuerza a formato rectangular agregando NA allí donde es necesario. Sería muy dificultoso manipular esos datos desde R. Como en cada columna puede haber más de un tipo de dato (ejp. la columna 1 tiene información sobre frutas y sobre trimestres) o ninguno no hay forma de llamarlas por nombre. Si eso fuera poco un sumario de datos (ventas de frutas de carozo y pepita) comparte el espacio con datos en bruto.

En la estructura rectangular están solamente los datos y podemos manipularlos con facilidad. La sumatoria de la columna 2 nos da las ventas totales del primer trimestre. La sumatoria de la fila 1 las ventas anuales de Manzanas. No tiene sumarios, pero podríamos generarlos con mucha gran facilidad.

Si importamos datos desde Excel debemos prestar mucha atención a que estén en formato rectangular. Si el formato fuera irregular la mejor alternativa es pasarlos a formato rectangular usando el mismo Excel, antes de imporarlos a R. Cargue todos los datos, si es necesario en varias estructuras pequeñas, cada una de las cuales deberá ser rectangular. No se preocupe por los sumarios que dependan únicamente de los datos incluídos en la base o bases, con datos bien estructurados reconstruirlos lleva segundos.

Guarde su información en formato rectangular siempre, aún cuando no tenga planeado usar R. Antes de ingresar datos a una planilla de cálculos piense primero: ¿Qué operaciones voy a realizar sobre estos datos? ¿Cuál es la mejor forma de organizarlos?

Datos prolijos (tidy data)

Los datos prolijos son aquellos en los que se cumplen simultaneamente dos condiciones: cada columna es una variable y cada fila una observación. Quizás un ejemplo nos aclare el problema. Veamos primero un conjunto de datos que parece muy razonalbe, pero que de acuerdo con nuestra definición no está en un formato prolijo:

Datos desprolijos
Fruta Indicador 2015 2016
Mandarina Precio 10 12
Toronja Precio 14 15

¿Por qué son desprolijos estos datos? Porque Precio no es una observación sino una variable y los años no son variables, son puntos de datos u observaciones. En formato prolijo deberíamos tener las columnas Fruta, Año y Precio, con una observación por cada año y fruta.8

En versión prolija sería así:

Datos prolijos
Fruta Año Precio
Mandarina 2015 10
Mandarina 2016 12
Toronja 2016 14
Toronja 2016 15

Siempre que generemos datos –o estemos en condiciones de especificar el formato que tendrán– elegiremos datos prolijos. Si no podemos obtenerles en ese formato entonces los reformatearemos hasta llevarlos a datos prolijos. La librería tidyr:: incluye funciones que solucionan muchos de los problemas frecuentes para pasar datos desprolijos a prolijos.

Reformateo de datos.

De ningún modo vamos a hacer ese reformateado de datos manualmente, un proceso tedioso y propenso a errores. tidyr:: tiene la función gather() que se especializa en solucionar este problema.9 gather() toma un conjunto de datos con las observaciones en las colunas, convierte a cada una de esas columnas en una clave y crea otra nueva que registra el valor correspondiente a cada. Es decir, nos regresa la versión prolija de nuestros datos.

tribble(
        ~Fruta,      ~Indicador,  ~`2015`, ~`2016`, 
        "Mandarina", "Precio",     10,     12,
        "Toronja",   "Precio",     14,     15 
       ) ->desprolijo      #Asigno el nombre desprolijo a este data.frame. 
print(desprolijo)          #Y lo paso a la consola con print.
## # A tibble: 2 × 4
##       Fruta Indicador `2015` `2016`
##       <chr>     <chr>  <dbl>  <dbl>
## 1 Mandarina    Precio     10     12
## 2   Toronja    Precio     14     15
#Reformateado de datos I
#=======================

desprolijo %>% 
  gather () #Paso desprolijo a gather() sin argumentos.
## # A tibble: 8 × 2
##         key     value
##       <chr>     <chr>
## 1     Fruta Mandarina
## 2     Fruta   Toronja
## 3 Indicador    Precio
## 4 Indicador    Precio
## 5      2015        10
## 6      2015        14
## 7      2016        12
## 8      2016        15
#El resultado que obtuve no es el que busco, gather() pasó todas las columnas anterioes a filas, pero lo que buscamos es que sólo algunas columnas pasen a filas. 

#Reformateado de datos II
#========================

desprolijo %>% 
  gather(key, value,    #Los dos primeros argumentos son los nombres de las columnas que se crerán.
         `2015`, `2016`) #Los siguientes las columnas que pasaremos a filas. Estos nombres de columna se repetirán en key y las filas llenarán value.  
## # A tibble: 4 × 4
##       Fruta Indicador   key value
##       <chr>     <chr> <chr> <dbl>
## 1 Mandarina    Precio  2015    10
## 2   Toronja    Precio  2015    14
## 3 Mandarina    Precio  2016    12
## 4   Toronja    Precio  2016    15
#Reformateado de datos III
#=========================

desprolijo %>% 
  gather(Año, Precio,            #Doy nombres útiles a key y value: quiero los años y los precios. 
         -Fruta, -Indicador) %>% #Especifico que columnas NO voy a pasar a filas usando "-".
  select(-Indicador)             #Quito la columna redundante.  
## # A tibble: 4 × 3
##       Fruta   Año Precio
##       <chr> <chr>  <dbl>
## 1 Mandarina  2015     10
## 2   Toronja  2015     14
## 3 Mandarina  2016     12
## 4   Toronja  2016     15
#Crear una función ad hoc (Avanzado, no es necesario, es recomendable)
#=====================================================================

arreglar <- function(datos) {   #Asigno nombre a la función.
  select(gather(datos, Año, Precio, -Fruta, -Indicador), -Indicador)  #Código en ejecución.
}      #Cierro llave de función. 

#Esta función ad hoc sólo sirve para estos datos u otros una estructura idéntica. Pero nos ayuda a ahorrar tiempo tipeando si tenemos que arreglar una y otra vez el mismo error. 

desprolijo %>% 
  arreglar() #Menos código, no me repito y expongo a errores de dedazo.
## # A tibble: 4 × 3
##       Fruta   Año Precio
##       <chr> <chr>  <dbl>
## 1 Mandarina  2015     10
## 2   Toronja  2015     14
## 3 Mandarina  2016     12
## 4   Toronja  2016     15

Manejo de datos con dplyr::

Con los datos cargados y en formato prolijo manipularlos con dplyr() es muy fácil. Quizás lo más dificil sea figurarnos el estado de los datos al que queremos llegar. Una vez que tenemos una idea clara del punto de partida y el punto de llegada debemos dividir el trayecto en pasos simples, tan simples como para ser comprensibles por R y menlazar esos pasos con el operador %>%. Si partimos de los datos en la tabla “Datos prolijos” y queremos obtener un promedio de ventas anuales de los dos productos primero deberíamos cargar los datos, luego agruparlos por año y obtener la media por grupos e imprimirla para hacerla visible. Con dplyr lo haríamos así: datos_prolijos %>% group_by(Año) %>% summarise(mean(Precio)). Con esta simple estructura de sintaxis podemos hacer prácticamente cualquier manipulación de datos.

Verbos básicos de dplyr::

La manipulación de datos con dplyr:: se basa en 5 verbos básicos.

  • select(), para seleccionar columnas. Si el formato es prolijo columna y variables son equivalentes.
  • filter(), para filtrar filas de acuerdo a una o más condiciones.
  • group_by(), para generar grupos. Admite más de un nivel de agrupamiento.
  • mutate(), para hacer transformaciones y recodificación de variables. Regresa un resultado por fila.
  • summarise(), para generar sumarios de variables. Regresa un resultado por variable o por cada grupo previamente definido con group_by().
  • El operador %>% nos permite encadenar operaciones. %>% pasa el output de una función como primer argumento de la siguiente. Normalmente el primer argumento de una función son los datos, con lo que nos ahorraremos tipear una y otra vez a que datos nos referimos.

A estos verbos básicos se agregan otros de uso menos frecuente, pero de gran utilidad en ciertas ocasiones.

  • arrange(), para ordenar resultados en orden numérico o alfabético.
  • recode(), para recodificar variables. Se lleva muy bien con mutate().
  • mutate_all(), mutate_if() y mutate_at(), para hacer transformaciones a varias columnas de una vez. Técnicamente vectoriza una función de transformación.
  • spread() y gather() para reformatear nuestra estructura de datos y pasar de formato largo a ancho y de ancho a largo. Pertenecen a tidyr(), una librería hermana de dplyr::
  • rename() para cambiar nombres de columna.
  • La familia de funciones _join para combinar diferentes datos en una misma estuctura. La sintaxis es algo compleja, pero tiene muchísima flexibilidad.

Uso de la de sintaxis de dplyr::

Repasaremos algunas funciones básicas de dplyr::. Produciremos intencionalmente algunos errores que aclararán el proceso. Se presenta la sintaxis paso por paso, con la salida en consola correspondiente a cada paso. Buena parte del código es redundante, sin embargo se lo incluye como ejemplo del flujo de trabajo típico con dplyr:: y magrittr::: hacemos una operación, verificamos el resultado, luego encadenamos la siguiente, verificamos y así hasta obtener el resultado esperado.

library(tidyverse)               #Carga varias librerías, entre ellas dplyr y tidyr.
#Crear un data.frame
#===================
tribble(                         #Creo los datos con la función tribble, muy útil por su legibilidad al organizar la entrada en columnas. 
  ~Col1,    ~Col2,        ~Col3, #Nombres de columna, se antepone ~
  "Pepita", "Manzana",    48,    #Llenamos las columnas una a una. Mucha atención a las comas!!
  "Pepita", "Pera"   ,    52,
  "Cítrico", "Naranja",   12, 
  "Cítrico", "Mandarina", 8,
  "Carozo",  "Durazno",   60,
  "Carozo",  "Ciruela",   55     #En la última columna NO usamos la coma. 
        )                        #Cierro el paréntesis de la función tribble. Se genera el data.frame. Silenciosamente invoca a la función print() y saca el resultado en consola. 
## # A tibble: 6 × 3
##      Col1      Col2  Col3
##     <chr>     <chr> <dbl>
## 1  Pepita   Manzana    48
## 2  Pepita      Pera    52
## 3 Cítrico   Naranja    12
## 4 Cítrico Mandarina     8
## 5  Carozo   Durazno    60
## 6  Carozo   Ciruela    55
#Renombrar columnas
#==================

tribble(
  ~Col1,    ~Col2,        ~Col3, 
  "Pepita", "Manzana",    48,     
  "Pepita", "Pera"   ,    52,
  "Cítrico", "Naranja",   12, 
  "Cítrico", "Mandarina", 8,
  "Carozo",  "Durazno",   60,
  "Carozo",  "Ciruela",   55
        )  %>%                   #Paso el data.frame a la siguiente función.
  rename(Tipo=Col1,              #Cambio los nombres a unos más fáciles de recordar.
         Fruta=Col2,             #Hago un cambio en cada línea para facilitar la legibilidad. 
         Precio=Col3) 
## # A tibble: 6 × 3
##      Tipo     Fruta Precio
##     <chr>     <chr>  <dbl>
## 1  Pepita   Manzana     48
## 2  Pepita      Pera     52
## 3 Cítrico   Naranja     12
## 4 Cítrico Mandarina      8
## 5  Carozo   Durazno     60
## 6  Carozo   Ciruela     55
#Filtrar filas (error)
#============================

tribble(
  ~Col1,    ~Col2,        ~Col3, 
  "Pepita", "Manzana",    48,     
  "Pepita", "Pera"   ,    52,
  "Cítrico", "Naranja",   12, 
  "Cítrico", "Mandarina", 8,
  "Carozo",  "Durazno",   60,
  "Carozo",  "Ciruela",   55
        )  %>%                   
  rename(Tipo=Col1,              
         Fruta=Col2,             
         Precio=Col3) %>% 
  filter(Col2=="Cítrico")        #filter() regresa un error: Col2 no existe. ¿Por qué? Porque la renombramos en el paso anterior 
## Error in filter_impl(.data, dots): objeto 'Col2' no encontrado
#Filtrar filas
#=============
tribble(
  ~Col1,    ~Col2,        ~Col3, 
  "Pepita", "Manzana",    48,     
  "Pepita", "Pera"   ,    52,
  "Cítrico", "Naranja",   12, 
  "Cítrico", "Mandarina", 8,
  "Carozo",  "Durazno",   60,
  "Carozo",  "Ciruela",   55
        )  %>%                   
  rename(Tipo=Col1,              
         Fruta=Col2,             
         Precio=Col3) %>% 
  filter(Tipo=="Cítrico")        #Conserva solamente las filas en las que Tipo es igual a "Cítrico" 
## # A tibble: 2 × 3
##      Tipo     Fruta Precio
##     <chr>     <chr>  <dbl>
## 1 Cítrico   Naranja     12
## 2 Cítrico Mandarina      8
#Filtrado negativo.
#==================
tribble(
  ~Col1,    ~Col2,        ~Col3, 
  "Pepita", "Manzana",    48,     
  "Pepita", "Pera"   ,    52,
  "Cítrico", "Naranja",   12, 
  "Cítrico", "Mandarina", 8,
  "Carozo",  "Durazno",   60,
  "Carozo",  "Ciruela",   55
        )  %>%                   
  rename(Tipo=Col1,              
         Fruta=Col2,             
         Precio=Col3) %>% 
  filter(Tipo!="Cítrico")    #Usando != conserva solamente las filas en las que Tipo NO es igual a "Cítrico"
## # A tibble: 4 × 3
##     Tipo   Fruta Precio
##    <chr>   <chr>  <dbl>
## 1 Pepita Manzana     48
## 2 Pepita    Pera     52
## 3 Carozo Durazno     60
## 4 Carozo Ciruela     55
#Filtrado para datos númericos.
#==============================

tribble(
  ~Col1,    ~Col2,        ~Col3, 
  "Pepita", "Manzana",    48,     
  "Pepita", "Pera"   ,    52,
  "Cítrico", "Naranja",   12, 
  "Cítrico", "Mandarina", 8,
  "Carozo",  "Durazno",   60,
  "Carozo",  "Ciruela",   55
        )  %>%                   
  rename(Tipo=Col1,              
         Fruta=Col2,             
         Precio=Col3) %>% 
  filter(Precio>=12)    #Usando >= conservo solamente las filas en las que el precio es mayor a igual a 12. <, >, y <= también son válidos.
## # A tibble: 5 × 3
##      Tipo   Fruta Precio
##     <chr>   <chr>  <dbl>
## 1  Pepita Manzana     48
## 2  Pepita    Pera     52
## 3 Cítrico Naranja     12
## 4  Carozo Durazno     60
## 5  Carozo Ciruela     55
#Seleccionar columnas
#====================

tribble(
  ~Col1,    ~Col2,       ~Col3, 
  "Pepita", "Manzana",   48,     
  "Pepita", "Pera"   ,   52,
  "Cítrico","Naranja",   12, 
  "Cítrico","Mandarina", 8,
  "Carozo", "Durazno",   60,
  "Carozo", "Ciruela",   55
        )  %>%                   
  rename(Tipo=Col1,              
         Fruta=Col2,             
         Precio=Col3) %>% 
  filter(Tipo!="Cítrico")  %>%   #Filtro los que NO son cítricos usando != en lugar de ==
  select(Precio, Fruta)          #Selecciono las columnas Precio y Fruta, saldrán en ese orden. Nótese que cada función opera sobre el output de la anterior. 
## # A tibble: 4 × 2
##   Precio   Fruta
##    <dbl>   <chr>
## 1     48 Manzana
## 2     52    Pera
## 3     60 Durazno
## 4     55 Ciruela
#Cálculos agrupados 
#==================

tribble(
  ~Col1,    ~Col2,        ~Col3, 
  "Pepita", "Manzana",    48,     
  "Pepita", "Pera"   ,    52,
  "Cítrico", "Naranja",   12, 
  "Cítrico", "Mandarina", 8,
  "Carozo",  "Durazno",   60,
  "Carozo",  "Ciruela",   55
        )  %>%                   
  rename(Tipo=Col1,              
         Fruta=Col2,             
         Precio=Col3) %>%        #Retomo los datos originales, renombradas las columnas. 
  group_by(Tipo) %>%             #Agrupo por tipo. Las siguientes operaciones se harán por este grupo.
  summarise(promedio=            #Nombre de la columna que se creará con el sumario. Opcional, pero recomendable. 
              mean(              #Función que utilizaremos. mean regresa la media. 
                Precio)          #Columna sobre la que opera la función.
            )  %>%               #Cierro el paréntesis que abrí en summarise()
  arrange(promedio)              #Ordeno de menor a mayor.  
## # A tibble: 3 × 2
##      Tipo promedio
##     <chr>    <dbl>
## 1 Cítrico     10.0
## 2  Pepita     50.0
## 3  Carozo     57.5
#Transformaciones de columnas 
#============================
tribble(
  ~Col1,    ~Col2,        ~Col3, ~Col4, #Creé una nuevo columna. 
  "Pepita", "Manzana",    48,     12,   
  "Pepita", "Pera"   ,    52,     7, 
  "Cítrico", "Naranja",   12,     50,
  "Cítrico", "Mandarina", 8,      40,
  "Carozo",  "Durazno",   60,     3, 
  "Carozo",  "Ciruela",   55,     10
        )  %>%                  
rename(Tipo=Col1,             
         Fruta=Col2,            
         Precio=Col3,
         Kilos=Col4) %>%            #La nueva columna son los kilos en el inventario.
mutate(Fruta=recode(Fruta,          #Recodifico una variable categórica. 
  Manzana="Manzana Verde")) %>%     #categoría_vieja="categoría_nueva". ¡Atención a las comillas!
mutate(Capital=Precio*Kilos)        #Creo una nueva columna numérica a partir de otras dos.Uso * para multiplicación. También podria usar +, -, / y ^ para potecia.
## # A tibble: 6 × 5
##      Tipo         Fruta Precio Kilos Capital
##     <chr>         <chr>  <dbl> <dbl>   <dbl>
## 1  Pepita Manzana Verde     48    12     576
## 2  Pepita          Pera     52     7     364
## 3 Cítrico       Naranja     12    50     600
## 4 Cítrico     Mandarina      8    40     320
## 5  Carozo       Durazno     60     3     180
## 6  Carozo       Ciruela     55    10     550
#Asigno nombre al resultado de una función
#=========================================

tribble(
  ~Tipo,    ~Fruta,      ~Precio, ~Kilos, #Creé una nuevo columna. 
  "Pepita", "Manzana",    48,      12,   
  "Pepita",  "Pera",      52,      7, 
  "Cítrico", "Naranja",   12,      50,
  "Cítrico", "Mandarina", 8,       40,    #Mandarina no está en frutería2.
  "Carozo",  "Durazno",   60,      3, 
  "Carozo",  "Ciruela",   55,      10
        ) -> frutería1                    #Asigno el nombre frutería1 al final de la cadena con -> ¡El resultado no saldrá en la consola pero quedará disponible!

frutería2 <- tribble(
                     ~Fruta,      ~precio_futuro,  #Alternativamente asigno al principio de la cadena con <-. De todos modos se asignará el resultado final de la cadena.
                     "Naranja",   12,    
                     "Manzana",   48,    
                     "Pera"   ,   52,    
                     "Durazno",   60,    
                     "Toronja",   8,               #Toronja no está en frutería1    
                     "Ciruela",   55    
                             )

Uso avanzado de dplyr::

Alternar entre formato largo y ancho.

El formato de datos largo es el que llamamos prolijo. Sin embargo en ocasiones puede ser necesario hacer el proceso inverso y pasar datos de formato largo a formato ancho. Por ejemplo, para hacer operaciones matemáticas entre columnas. Siempre es mejor partir del formato largo y prolijo y pasar a formato ancho allí donde es necesario. La función spread() hace este cambio.

#Formato largo a formato ancho
#=============================
desprolijo %>% 
  arreglar %>% 
  spread(Año, Precio)  #Año será nombre de columna, Precio los valores de cada año.  
## # A tibble: 2 × 3
##       Fruta `2015` `2016`
## *     <chr>  <dbl>  <dbl>
## 1 Mandarina     10     12
## 2   Toronja     14     15
desprolijo %>% 
  arreglar %>% 
  spread(Año, Precio) %>% 
  mutate(variación_precio=`2016`-`2015`) #Puedo hacer el cálculo. 
## # A tibble: 2 × 4
##       Fruta `2015` `2016` variación_precio
##       <chr>  <dbl>  <dbl>            <dbl>
## 1 Mandarina     10     12                2
## 2   Toronja     14     15                1

Seleccionar las columnas que cumplen determinada condición.

Hasta ahora hemos visto como seleccionar columnas escribiendo sus nombres separados por comas o usando - para señalar cuales no queremos que permanezcan. select() tiene algunas funciones ayudantes que permiten seleccionar columnas de acuerdo con alguna condición lógica y pueden ser de gran ayuda cuando trabajamos con bases de datos con gran número de columna.

tribble(~frutamanzana, ~frutamandarina, ~vegetalcalabacin, ~vegetalzanahoria, 
        10,             8,                15,                 10) -> fruteria_verdulería

select(fruteria_verdulería, starts_with("vegetal"))       #Selecciona todas las columnas cuyo nombre comienza con la cadena "vegetal"
## # A tibble: 1 × 2
##   vegetalcalabacin vegetalzanahoria
##              <dbl>            <dbl>
## 1               15               10
select(fruteria_verdulería, ends_with("na"))              #Selecciona todas las columnas cuyo nombre termina con la cadena "na"
## # A tibble: 1 × 2
##   frutamanzana frutamandarina
##          <dbl>          <dbl>
## 1           10              8
select(fruteria_verdulería, contains("na"))               #Selecciona todas las columnas cuyo nombres contiene la cadena "na"
## # A tibble: 1 × 3
##   frutamanzana frutamandarina vegetalzanahoria
##          <dbl>          <dbl>            <dbl>
## 1           10              8               10
select(fruteria_verdulería, 2:4)                          #Selecciona las columnas con las ubicaciones 2 a la 4.
## # A tibble: 1 × 3
##   frutamandarina vegetalcalabacin vegetalzanahoria
##            <dbl>            <dbl>            <dbl>
## 1              8               15               10

Filtrar por más de una condición o dentro de un conjunto.

Filter es una herramienta muy potente para hacer subconjuntos de datos. Para sacar el mayor provecho vale la pena reflexionar sobre la forma en que opera y la sintaxis de la función nos da una pista. Entre los paréntesis posteriores a filter ubicamos una condición lógica que especificamos a través de operadores binarios. Los operadores binarios más conocidos son los que usamos frecuentemente para definir operaciones aritméticas: + para sumar o - para restar. Se los llama binarios porque se ubican entre dos expresiones y establecen una relación entre ellas, de lo contrario sería operadores unarios, otra clase también definida por R. Consiguientemente si no ubicamos al operador binario entre dos expresiones R nos regresará un error. A las dos expresiones que envuelven a un operador binario las llamados lado derecho y lado izquierdo. Esto es de la mayor importancia, ya que la ubicación en cada uno de los lados hace que la operación sea diferente. La aritmética nos ayuda una vez más a comprenderlo, si queremos hacer una resta entre los números 10 y 2 no obtenemos el mismo resultado alternando el lado en que los ubicamos. 10-2 no es lo mismo que 2-10. a %in% x, que se lee en voz alta como ‘a pertenece a x’ no es lo mismo que ‘x pertenece a a’. Filter utiliza todos los operadores binarios de R, que se detallan en tabla a continuación. Con esta especificación R evalúa el cumplimiento de una condición y regresa TRUE cuando se cumple y FALSE cuando no. Dado que R opera por defecto de manera vectorizada, cuando especificamos un vector evalúa todo el vector y regresa un vector de TRUEy FALSE del mismo largo del que introduducimos. Posteriormente filter() conseva solamente aquellas filas en las que el resultado de la evaluación fue TRUE. Esta evaluación aplica tanto para cadenas de caracteres como para datos del tipo numérico. En el caso de las cadenas de caracteres algunos operadores utilizarán el orden alfabético, así "pera">"manzana" regresa TRUE, porque p ocupa el lugar 16 en el alfabeto y m el 13. En caso de empate R evaluará la segunda letra y así hasta desempatar.
Table: Operadores binarios de R disponibles para filter()

Operador Uso
== Igual que
> Mayor que
>= Mayor o igual que
< Menor que
<= Menor o igual que
%in% | Pertenece a un vector|Vectores numéricos o de caracteres&| Y lógica, para especificar una segunda condición a cumplirse | Entre dos expresiones binarias | Ó lógica, para especificar una condición alternativa | Entre dos expresiones binarias!| No, invierte el sentido de cada operador, regresa TRUE cuando *no* se cumple la condición | Detrás de cualquiera de los operadores%like%`

Usando estos operadores y sus combinaciones podemos hacer prácticamente cualquier operación de filtrado de filas, empleando información de una o más columnas, siempre que estén dentro del data.frame al que estamos manipulando. Si bien es posible hacer filtrados sucesivos encadenando operaciones de filter() por cuestiones de desempeño –sobre todo en bases de datos muy grandes– y compacitud del código es preferible cargar tantas condiciones como es posible en la misma cadena. Recuerde que cada vez que invocamos la función R debe recorrer todo el vector evaluando, es mejor que lo haga sólo una vez para un conjunto de condiciones complejas que muchas veces para condiciones simples.

library(data.table)                                                      #Importa el operador %like%

#Datos
#=====
         
tribble(~Estado,            ~Municipio,          ~Población,
        "Chihuahua",        "Juárez",            1391180,
        "Nuevo León",       "Juárez",            333481,
        "Chiapas",          "Juárez",            21222, 
        "Chiapas",          "Unión Juárez",      15350, 
        "Guerrero",         "Acapulco de Juárez",810669, 
        "Ciudad de México", "Milpa Alta",        137927, 
        "Chihuahua",        "Batopilas",         9751, 
        "Nuevo León",       "Apodaca",           115913,
        "Nuevo León",       "Carmen",            4906 
        ) -> municipios

#Dos condiciones. 
filter(municipios, Municipio=="Juárez" & Población <400000)              #Municipios llamados Juárez con población menor a 400000.
## # A tibble: 2 × 3
##       Estado Municipio Población
##        <chr>     <chr>     <dbl>
## 1 Nuevo León    Juárez    333481
## 2    Chiapas    Juárez     21222
#Cadenas similares
filter(municipios, Municipio %like% "Juárez" & Población <400000)        #Incluye Unión Juárez, similar a "Juárez", no Acapulco o CD Juáres por la 2da condición. 
## # A tibble: 3 × 3
##       Estado    Municipio Población
##        <chr>        <chr>     <dbl>
## 1 Nuevo León       Juárez    333481
## 2    Chiapas       Juárez     21222
## 3    Chiapas Unión Juárez     15350
#Una u otra condición.
filter(municipios, Municipio =="Juárez" | Población==137927)             #Municipios llamados Juárez o con una población de 137927 (Milpa Alta)
## # A tibble: 4 × 3
##             Estado  Municipio Población
##              <chr>      <chr>     <dbl>
## 1        Chihuahua     Juárez   1391180
## 2       Nuevo León     Juárez    333481
## 3          Chiapas     Juárez     21222
## 4 Ciudad de México Milpa Alta    137927
#No es igual.
filter(municipios, Estado!="Chihuahua" & Población>400000)               #Población superir a 40000 habitantes, NO en Chihuahua. 
## # A tibble: 1 × 3
##     Estado          Municipio Población
##      <chr>              <chr>     <dbl>
## 1 Guerrero Acapulco de Juárez    810669
#Pertenencia a un conjunto.
filtro <- c("Acapulco de Juárez", "Milpa Alta")                          #Defino el conjunto como un vector. 
filter(municipios, Municipio %in% filtro)                                #Pertenece a un vector.
## # A tibble: 2 × 3
##             Estado          Municipio Población
##              <chr>              <chr>     <dbl>
## 1         Guerrero Acapulco de Juárez    810669
## 2 Ciudad de México         Milpa Alta    137927

Transformacion avanzada de datos.

Usándolo con cuidado mutate() es una herramienta muy potente para hacer transformaciones de datos. En términos muy prácticos mutate() une un vector resultado de una función al data.frame con el que estamos trabajando. Es decir, cualquier función que tome uno o más vectores y nos regrese otro puede utilizarse dentro de mutate(), incluyendo algunas básicas como ifelse() para operaciones condicionales vectoriales, cumsum() para sumas acumuladas. lag() y lead() para mover un vector una línea abajo o hacia arriba respectivamente, permitiéndonos computar la diferencia entre una fila y la anterior o posterior. También es posible pasar varias transformaciones al mismo tiempo separándolas con comas. La familia ampliada de mutate() incluye mutate_all(), para aplicar la misma transformación a todas las variables, mutate_if() que aplica una transformación a las variables que cumplen con una condición.

#Dicotomiza una variable continua. ifelse()
municipios %>%
  mutate (tamaño= ifelse(Población >400000, "Muy poblado", "Poco poblado"))
## # A tibble: 9 × 4
##             Estado          Municipio Población       tamaño
##              <chr>              <chr>     <dbl>        <chr>
## 1        Chihuahua             Juárez   1391180  Muy poblado
## 2       Nuevo León             Juárez    333481 Poco poblado
## 3          Chiapas             Juárez     21222 Poco poblado
## 4          Chiapas       Unión Juárez     15350 Poco poblado
## 5         Guerrero Acapulco de Juárez    810669  Muy poblado
## 6 Ciudad de México         Milpa Alta    137927 Poco poblado
## 7        Chihuahua          Batopilas      9751 Poco poblado
## 8       Nuevo León            Apodaca    115913 Poco poblado
## 9       Nuevo León             Carmen      4906 Poco poblado
#Calcular la diferencia entre filas con lag()
municipios %>% 
  mutate (acumulado = cumsum(Población), diferencia=Población-lag(Población)) 
## # A tibble: 9 × 5
##             Estado          Municipio Población acumulado diferencia
##              <chr>              <chr>     <dbl>     <dbl>      <dbl>
## 1        Chihuahua             Juárez   1391180   1391180         NA
## 2       Nuevo León             Juárez    333481   1724661   -1057699
## 3          Chiapas             Juárez     21222   1745883    -312259
## 4          Chiapas       Unión Juárez     15350   1761233      -5872
## 5         Guerrero Acapulco de Juárez    810669   2571902     795319
## 6 Ciudad de México         Milpa Alta    137927   2709829    -672742
## 7        Chihuahua          Batopilas      9751   2719580    -128176
## 8       Nuevo León            Apodaca    115913   2835493     106162
## 9       Nuevo León             Carmen      4906   2840399    -111007
#Aplicae la misma transformación a todas las variables numéricas con mutate_if()
municipios %>%
  mutate (acumulado = cumsum(Población), diferencia=Población-lag(Población)) %>% 
  mutate_if(is.numeric, funs(paste("Son", . , "personas")))                        
## # A tibble: 9 × 5
##             Estado          Municipio            Población
##              <chr>              <chr>                <chr>
## 1        Chihuahua             Juárez Son 1391180 personas
## 2       Nuevo León             Juárez  Son 333481 personas
## 3          Chiapas             Juárez   Son 21222 personas
## 4          Chiapas       Unión Juárez   Son 15350 personas
## 5         Guerrero Acapulco de Juárez  Son 810669 personas
## 6 Ciudad de México         Milpa Alta  Son 137927 personas
## 7        Chihuahua          Batopilas    Son 9751 personas
## 8       Nuevo León            Apodaca  Son 115913 personas
## 9       Nuevo León             Carmen    Son 4906 personas
## # ... with 2 more variables: acumulado <chr>, diferencia <chr>
#Transformae a todas las colunas al tipo caracter con mutate_all()
municipios %>% mutate_all(as.character)                                            
## # A tibble: 9 × 3
##             Estado          Municipio Población
##              <chr>              <chr>     <chr>
## 1        Chihuahua             Juárez   1391180
## 2       Nuevo León             Juárez    333481
## 3          Chiapas             Juárez     21222
## 4          Chiapas       Unión Juárez     15350
## 5         Guerrero Acapulco de Juárez    810669
## 6 Ciudad de México         Milpa Alta    137927
## 7        Chihuahua          Batopilas      9751
## 8       Nuevo León            Apodaca    115913
## 9       Nuevo León             Carmen      4906

Sumarios avanzados.

summarise() nos permite crear sumarios y es especialmente útil con datos agrupados. Sin embargo es posible que no exista una función que produzca el sumario que necesitamos. En ese caso podemos pasar a summarise() una función personalizada, ya sea una función que hemos definido previamente y hemos asignado nombre o una función anónima o función lambda. En este caso estimaremos la media de población para nuestra muestra, el desvío estandar y el error estandar de la media. Crearemos una función para calcular el error estándar traduciendo a R la notación matemática del error estándar: \(SE\hat{x}=\frac{\sigma}{\sqrt{n}}\), el error estándar de la media es igual al desvío estándar entre la raíz cuadrada de n.

#Con una función ad hoc, creada en la misma sintaxis.
summarise(municipios, n=n(), media=mean(Población), desvio=sd(Población), error_estandar=sd(Población)/sqrt(n()))
## # A tibble: 1 × 4
##       n    media   desvio error_estandar
##   <int>    <dbl>    <dbl>          <dbl>
## 1     9 315599.9 479662.7       159887.6
#Con una función predefinida. 
#Defino de una función para calcular el error estandar de la media. La llamo es_media
es_media <- function(x) {
  sd(x)/sqrt(length(x))
}

#La utilizo como cualquier otra función de R. 
summarise(municipios, n=n(), media=mean(Población), desvio=sd(Población), error_estandar=es_media(Población))
## # A tibble: 1 × 4
##       n    media   desvio error_estandar
##   <int>    <dbl>    <dbl>          <dbl>
## 1     9 315599.9 479662.7       159887.6
#Versión robusta de la función es_media: si n=1 regresa NaN, Not a Number. No se puede estimar el error estandar con n=1
#Sobreescribe la función anterior. 

es_media <- function(x) {
  ifelse(length(x)==1, NaN, sd(x)/sqrt(length(x)))}

municipios %>% 
  group_by(Estado) %>% 
  summarise(n=n(), 
            media=mean(Población), 
            desvio=sd(Población), 
            error_estandar=es_media(Población))  
## # A tibble: 5 × 5
##             Estado     n    media     desvio error_estandar
##              <chr> <int>    <dbl>      <dbl>          <dbl>
## 1          Chiapas     2  18286.0   4152.131        2936.00
## 2        Chihuahua     2 700465.5 976817.814      690714.50
## 3 Ciudad de México     1 137927.0        NaN            NaN
## 4         Guerrero     1 810669.0        NaN            NaN
## 5       Nuevo León     3 151433.3 167142.613       96499.83

Funciones avanzadas de combinación de bases de datos.

dplyr:: nos permite tiene algunos verbos muy usuales en el software de manejo de bases de datos que sirven para combinar múltiples fuentes de datos: los joins o uniones. Sin la sintaxis complicada de un lengujae como SQL podemos combinar bases de datos de manera flexible y aprovechar múltiples fuentes de datos, siempre que tengan al menos una columna que coincida. Podríamos usarlo para unir dos bases de datos con la misma unidad de análisis territorial o para combinar estructuras de datos asimétricas en cuanto al largo.

#Unir varias estructuras de datos 
#================================

#Une las filas en las que coinicen frutería1 y frutería2, usando Fruta como clave. Sólo conserva las coincidencias, mandarina desaparece porque no tiene precio futuro.
inner_join(frutería1, frutería2, by = "Fruta")  
## # A tibble: 5 × 5
##      Tipo   Fruta Precio Kilos precio_futuro
##     <chr>   <chr>  <dbl> <dbl>         <dbl>
## 1  Pepita Manzana     48    12            48
## 2  Pepita    Pera     52     7            52
## 3 Cítrico Naranja     12    50            12
## 4  Carozo Durazno     60     3            60
## 5  Carozo Ciruela     55    10            55
#Une todas las filas, imputa NA cuando hay información faltante. No precio futuro para Mandarina y falta información para Toronja.
full_join(frutería1, frutería2, by = "Fruta")   
## # A tibble: 7 × 5
##      Tipo     Fruta Precio Kilos precio_futuro
##     <chr>     <chr>  <dbl> <dbl>         <dbl>
## 1  Pepita   Manzana     48    12            48
## 2  Pepita      Pera     52     7            52
## 3 Cítrico   Naranja     12    50            12
## 4 Cítrico Mandarina      8    40            NA
## 5  Carozo   Durazno     60     3            60
## 6  Carozo   Ciruela     55    10            55
## 7    <NA>   Toronja     NA    NA             8
#Conserva sólamente las filas frutería1 que no coinciden con frutería2. 
anti_join(frutería1, frutería2, by = "Fruta")   
## # A tibble: 1 × 4
##      Tipo     Fruta Precio Kilos
##     <chr>     <chr>  <dbl> <dbl>
## 1 Cítrico Mandarina      8    40
#Conserva las filas de frutería2 que no cinciden con frutería1.
anti_join(frutería2, frutería1, by = "Fruta")  
## # A tibble: 1 × 2
##     Fruta precio_futuro
##     <chr>         <dbl>
## 1 Toronja             8
#Conserva sólo las filas de frutería1 y agregas las columnas de frutería2, imputa NA si no hay coincidencia. Hay mandarina, pero no toronja...
left_join(frutería1, frutería2, by = "Fruta")   
## # A tibble: 6 × 5
##      Tipo     Fruta Precio Kilos precio_futuro
##     <chr>     <chr>  <dbl> <dbl>         <dbl>
## 1  Pepita   Manzana     48    12            48
## 2  Pepita      Pera     52     7            52
## 3 Cítrico   Naranja     12    50            12
## 4 Cítrico Mandarina      8    40            NA
## 5  Carozo   Durazno     60     3            60
## 6  Carozo   Ciruela     55    10            55

Operadores avanzados de magrittr::

Ya hemos utilizado el principal operador de magrittr::, la tubería %>% que estádisponible directamente en dplyr::. Entender el funcionamiento de %>% puede ayudarnos a conocer sus límites y buscar alternativas para solucionar algunos problemas. %>% es un operador binario, es decir, relaciona un lado izquierdo y un lado derecho. La expresión que está del lado derecho pasa su output a la del lado izquierdo. R no contempla un método específico para pasar los datos de una función a la siguiente, pero el autor de magrittr:: encontró una manera ingeniosa. Dado que la mayoría de las funciones toma a los datos como primera argumento %>% se encarga de “reescribir” la función del lado izquierdo, de manera el primer argumento sean los datos que produjo el lado derecho de la formula. Esto funciona en la mayoría de los casos, pero podemos tener problemas de la función que estamos utilizando en el lado izquierdo espera algo diferente a los datos como primer argumento. Asi con los modelos lineales que especificamos con lm(), esperan como primer argumento de la función la formula del modelo, no los datos. Para solucionar estos problemas magrittr:: utiliza al signo . (un punto) como “comodín” para los datos y nos permite ubicarlos allí donde la sintaxis de la función del lado izquierdo los requiera. Otro operador avanzado es %T>%, que nos permite hacer una –breve– bifurcación. Cuando lo ubicamos detrás de una función %T>%% hace que esta regrese el lado izquierdo y no el derecho. Así, si queremos hacer visible un resultado intermedio usanda la función print(), que luego pasará a la siguiente función no su output sino el que está a su izquierda. %T>% funciona solamente para dar un salto, no más que eso. A veces no es suficiente, podríamos estar interesados en dar algo de formato a los datos intermedios que queremos imprimir y eso implicaría al menos dos saltos. En ese caso podemos envolver las funciones intermedias con {} y poner el operador unario . al final, de modo que pase los datos de la izquierda de {} a la función siguiente. Dentro de los límites de {} puedo hacer cualquier manipulación y generar cualquier outupt.

Por último el operador %$% pasa al lado derecho una serie de vectores en lugar de la estructura original de los datos –que con frecuencia es un data podría serlo un data.frame–. Esto es especialmente útil cuando del lado derecho tenemos una función que espera vectores, ya que nos ahorra la permanente llamada con $ para especificar el data.frame del que tiene que extraer el vector.

library(magrittr)       #Para disponer de todos los operadores. 

#Bifurcación con %T>%
municipios %T>%         #Marco la bifurcación
  print() %>%           #print() imprime los datos y pasa a la derecha los datos crudos, no su impresión.  
  group_by(Estado) %>% 
  tally() 
## # A tibble: 9 × 3
##             Estado          Municipio Población
##              <chr>              <chr>     <dbl>
## 1        Chihuahua             Juárez   1391180
## 2       Nuevo León             Juárez    333481
## 3          Chiapas             Juárez     21222
## 4          Chiapas       Unión Juárez     15350
## 5         Guerrero Acapulco de Juárez    810669
## 6 Ciudad de México         Milpa Alta    137927
## 7        Chihuahua          Batopilas      9751
## 8       Nuevo León            Apodaca    115913
## 9       Nuevo León             Carmen      4906
## # A tibble: 5 × 2
##             Estado     n
##              <chr> <int>
## 1          Chiapas     2
## 2        Chihuahua     2
## 3 Ciudad de México     1
## 4         Guerrero     1
## 5       Nuevo León     3
#Uso de . en lugar de los datos.
municipios %>% 
  mutate(Pob2 = Población*2) %>%         #Creo una nueva variable numérica, el doble de la población. 
  lm(Pob2~Población, data=.) %>%         #Genero el modelo, en el argumento data= va el . Lo paso a la derecha.
  summary()                              #Obtengo el sumario del modelo. R^2=1
## 
## Call:
## lm(formula = Pob2 ~ Población, data = .)
## 
## Residuals:
##        Min         1Q     Median         3Q        Max 
## -9.879e-11 -8.515e-11 -5.616e-11 -5.252e-11  5.379e-10 
## 
## Coefficients:
##              Estimate Std. Error   t value Pr(>|t|)    
## (Intercept) 2.328e-10  8.808e-11 2.644e+00   0.0333 *  
## Población   2.000e+00  1.597e-16 1.252e+16   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 2.167e-10 on 7 degrees of freedom
## Multiple R-squared:      1,  Adjusted R-squared:      1 
## F-statistic: 1.568e+32 on 1 and 7 DF,  p-value: < 2.2e-16
#Bloque de código separado con {}.
municipios %>% {                                                   #Abro una llamada      
  foo = rename(., "Est"=Estado, "Mun"=Municipio, "Pob"=Población)  #Cambios los nombres de colunma y los asigno a foo.
  print(foo)                                                       #Imprimo foo
  . } %>%                                                          #Regreso los datos originales "." a la función de la derecha
  print()                                                          #Imprimo los datos originales. 
## # A tibble: 9 × 3
##                Est                Mun     Pob
##              <chr>              <chr>   <dbl>
## 1        Chihuahua             Juárez 1391180
## 2       Nuevo León             Juárez  333481
## 3          Chiapas             Juárez   21222
## 4          Chiapas       Unión Juárez   15350
## 5         Guerrero Acapulco de Juárez  810669
## 6 Ciudad de México         Milpa Alta  137927
## 7        Chihuahua          Batopilas    9751
## 8       Nuevo León            Apodaca  115913
## 9       Nuevo León             Carmen    4906
## # A tibble: 9 × 3
##             Estado          Municipio Población
##              <chr>              <chr>     <dbl>
## 1        Chihuahua             Juárez   1391180
## 2       Nuevo León             Juárez    333481
## 3          Chiapas             Juárez     21222
## 4          Chiapas       Unión Juárez     15350
## 5         Guerrero Acapulco de Juárez    810669
## 6 Ciudad de México         Milpa Alta    137927
## 7        Chihuahua          Batopilas      9751
## 8       Nuevo León            Apodaca    115913
## 9       Nuevo León             Carmen      4906
#Pasar vectores al lado derecho con %$%
municipios %$% 
  table (Estado, Municipio)  %T>%       #table se lleva mejor con vectores que con data.frame
  print() %>%                           #Imprimo la tabla
  chisq.test()                          #Ajusto un modelo chi cuadrado y lo imprimo. 
##                   Municipio
## Estado             Acapulco de Juárez Apodaca Batopilas Carmen Juárez
##   Chiapas                           0       0         0      0      1
##   Chihuahua                         0       0         1      0      1
##   Ciudad de México                  0       0         0      0      0
##   Guerrero                          1       0         0      0      0
##   Nuevo León                        0       1         0      1      1
##                   Municipio
## Estado             Milpa Alta Unión Juárez
##   Chiapas                   0            1
##   Chihuahua                 0            0
##   Ciudad de México          1            0
##   Guerrero                  0            0
##   Nuevo León                0            0
## 
##  Pearson's Chi-squared test
## 
## data:  .
## X-squared = 28, df = 24, p-value = 0.26

Límites de dplyr::

La potencia y simplicidad del abordaje de dplyr:: para la manipulación de datos no hace que esté excento de limitaciones. El primer problema es conceptual: los datos aquí son una entidad abstracta. Lleva algún tiempo acotumbrarse a no tener un registro visual de nuestra materia de trabajo y todavía más a que ni siquiera tengan nombre. Sin embargo armando las cadenas paso a paso podremos ver en consola el output de cada función y tener claro que datos estamos pasando a la función siguiente. Y, en verdad, no tener que llevar registro de los múltiples nombres que nuestros datos pueden adquirir a lo largo del análisis es en realidad gran ventaja. Otros problemas son tecnicos y demandan mucha flexibilidad y creatividad al momento de escribir código. Para mantenernos en el paradigma de programción funcional debemos buscar formas diferentes de lograr los mismo. La elegancia de dplyr:: se debe en gran medida a que se concentra en una sola estructura de datos, el data.frame. Al hacerlo puede ocultar mucha de la complejidad de la manipulación de datos al usuario, conoce la estructura con la que trabaja y puede traducir los comandos simples que le introducimos a comandos mucho más complejos que son los que en realidad ejecuta. Con la elegancia viene una limitación: si necesitamos trabajar con una estructura de datos diferente al data.frame dplyr:: fracasa completamente. Valgan como ejemplo las dificultades para manejar dos versiones de los mismos datos en una misma cadena. dplyr:: está pensado para trabajar con sólo con un conjunto de datos con estructura data.frame –cuando usamos un join estamos conviertiendo a dos conjuntos de datos en uno solo– en busca de un único resultado. Existen maneras de producir y generar el output de resultados intermedios, por ejemplo, usando la tubería %T>% para salidas muy simples o generando una función lambda que encapsule el código de salida y regrese los datos al datos al final.

El mayor problema es lograr que las cadenas de dplyr:: se lleven bien con algunas funciones más antiguas de R, sobre todo aquellas que reciben vectores atómicos como imput o producen listas como output. Esto es especialmente grave porque buena parte del atractivo de R es usar esas funciones específicas e irremplazables. ¿Le interesa el análisis de clases latentes multigrupos con ponderador muestral complejo? La librería LCA lo hace, pero el primer argumento de la función es un objeto de la clase fórmula y el resultado una lista de matrices de cuatro dimensiones, para las que dplyr:: no será de ninugna ayuda. En este y otros casos dplyr:: nos llevará hasta cierto punto, luego no habrá más opción que asignar un nombre al output de la cadena y seguir a pié10. Si esto ocurriera solamente con funciones muy especializadas no sería un problema tan grande, pero ocurre en casos tan comunes como una prueba \(\chi\)2.11 Conocer estos límites será siempre de gran ayuda.

Otro potencial problema es que los métodos de dplyr:: suelen fallar cuando la base de datos no está limpia y adecuadamente codificada, con los casos missing explícitamente señalados y sin errores de ortografía12. Personalmente no creo que esto sea un problema, más bien es una virtud. Dar soluciones ad hoc a los errores de nuestros datos en el momento en el que lo necesitamos es una pésima práctica. Nos garantiza volveremos a encontrar con ese error u otro similar más abajo en el código. Es mejor arreglarlo desde un princio. Todavía mejor es generar una función personalizada que hace los arreglos necesarios e invocarla cada vez que es necesario. De este modo garantizamos la inmutabilidad de los datos. ¿Encontramos un nuevo error que había pasado inadvertido? Solo cambiamos esta función y cuando volvamos a ejecutar el código que la incluye realizará los cambios pertinentes. Un pilar del paradigma de programación funcional es que su entorno de trabajo debería estar poblado de funciones, no de estructuras de datos.

Manipulación con dplyr::en la práctica. Flujo de trabajo.

En términos muy estilizados el flujo de trabajo del análisis exploratorio de datos sería el siguiente.13

library(DiagrammeR)
DiagrammeR("
  graph TB
    Z(Desde .csv, Excel, SPSS, Stata, dBase, wescrapping, etc. <br> con readr)==>A
    A((Importar datos))-->B((Limpiar la base <br> de errores))
    N[Formato tidy <br> tidyr, dplyr] --- B
    R[Nombres columna cortos<br>rename, colnames] ---B
    B==>C((Recodificar))
    K[Casos perdidos explícitos<br>amelia, is.na]---C
    M[Códigos homogéneos<br>recode, <- ]---C
    C==>D((Explorar con <br>gráficos y sumarios))
    O[Gráficos <br>base, ggplot]---D
    P[Sumarios numéricos <br>summary, summarise]---D
    D==>F((Modelar))
    Q[Modelos lineales <br> lm, glm]---F
    S[Clasificación<br>cluster, lca]---F
T[Reducción de dimensionalidad<br>ca, mca, pca, fa]---F
    D-->G
    F--Fortificar con modelos-->G>Salidas gráficas de alta calidad]
    F==>H>Salidas en tablas formateadas]
subgraph Integración con rmarkdown, knitr y pander
    I>Texto]--> J[Reporte]
    G-->J
    H-->J
end
    style A fill:#00FF00
    style B fill:#00FF00
    style C fill:#00FF00
    style D fill:#00FF00
    style F fill:#00FF00
    style J fill:#FF0000
    style Z fill:#FF007C
    style I fill:#EFFF00
    style H fill:#EFFF00
    style G fill:#EFFF00
")

Implementación en dplyr:: de un análisis exploratorio de la base de datos Índice de marginación del CONAPO.

#Importación de datos.  
#====================

library(tidyverse)      #Carga varias librerías. 
library(pander)         #Para formatear salidas en tablas. 
base_marginacion <-     #Nombre que daremos al objeto después de importado. read_csv siempre produce un data.frame.
  read_csv("C:/Users/mpaladino/Downloads/Base_Indice_de_marginacion_municipal_90-15.csv", col_types = cols(`AÑO` = col_character()), locale = locale(encoding = "UTF-8"))

#Limpieza de la base de datos. 
#============================

#Generamos una función que limpia los datos, NO unos datos nuevos limpios!! De este modo podemos limpiar la base ad hoc, cuando nos hace falta y sin efectos colaterales. 
#Agregamos cada línea necesaria para la limpieza. Al interior de la función asignamos con =, de modo de no asignar de objetos al entorno global. 

limpiar <- function(datos) {
  datos=select(datos, -`<U+FEFF>CVE_MUN`, -CVE_ENT, -CVEE_MUN, -IND0A100,  -LUGAR_NAC, -LUGAR_EST)
  datos=mutate(datos, POB_TOT=as.numeric(
                                   gsub(" ", "", POB_TOT)
                                   )
        )
  datos=mutate_if(datos, is.character, as.factor)
  datos [datos=="-"] <- NA
  return(datos)            #Para que pase el resultado a la función siguiente. 
}

#Recodificación. 
#==============

recodificar <- function(datos){
  datos=mutate(datos, ENT=recode(ENT, `Distrito Federal`="Ciudad de México")) 
  return(datos)
}

#Sumario de datos. 
#==================

#En tabla.

base_marginacion %>% 
  limpiar() %>% 
  recodificar() %>% 
  filter (AÑO=="2015" & ENT!="Nacional") %>% 
  summary() %>% 
  pander
Table continues below
ENT MUN POB_TOT VP
Oaxaca : 570 Benito Juárez : 7 Min. : 87 - : 0
Puebla : 217 Ocampo : 6 1st Qu.: 4250 1 001 : 0
Veracruz de Ignacio de la Llave: 212 Emiliano Zapata: 5 Median : 13292 1 002 : 0
Jalisco : 125 Hidalgo : 5 Mean : 48649 1 008 : 0
México : 125 Juárez : 5 3rd Qu.: 34415 1 012 : 0
Chiapas : 118 Morelos : 5 Max. :1827868 (Other): 0
(Other) :1090 (Other) :2424 NA NA’s :2457
Table continues below
ANALF SPRIM OVSDE OVSEE OVSAE
Min. : 0.67 Min. : 2.49 Min. : 0.000 Min. : 0.000 Min. : 0.000
1st Qu.: 5.24 1st Qu.:20.50 1st Qu.: 0.820 1st Qu.: 0.490 1st Qu.: 1.500
Median : 9.73 Median :29.42 Median : 1.910 Median : 1.170 Median : 4.100
Mean :11.75 Mean :29.27 Mean : 4.429 Mean : 2.209 Mean : 8.726
3rd Qu.:15.75 3rd Qu.:37.41 3rd Qu.: 4.670 3rd Qu.: 2.560 3rd Qu.:10.940
Max. :56.42 Max. :71.24 Max. :70.570 Max. :57.960 Max. :98.880
NA NA NA NA NA
Table continues below
VHAC OVPT PL<5000 PO2SM OVSD OVSDSE
Min. : 7.28 Min. : 0.000 Min. : 0.00 Min. : 8.25 - : 0 - : 0
1st Qu.:27.95 1st Qu.: 1.900 1st Qu.: 42.80 1st Qu.:42.93 0.05 : 0 0.00 : 0
Median :35.02 Median : 5.210 Median :100.00 Median :57.02 0.11 : 0 0.06 : 0
Mean :36.28 Mean : 8.305 Mean : 71.98 Mean :55.43 0.14 : 0 0.09 : 0
3rd Qu.:43.56 3rd Qu.:11.420 3rd Qu.:100.00 3rd Qu.:68.55 0.16 : 0 0.10 : 0
Max. :78.46 Max. :68.490 Max. :100.00 Max. :94.12 (Other): 0 (Other): 0
NA NA NA NA NA’s :2457 NA’s :2457
IM GM AÑO
Min. :-2.228000 - : 0 1990: 0
1st Qu.:-0.748000 Alto :817 1995: 0
Median :-0.073000 Bajo :498 2000: 0
Mean :-0.000009 Medio :514 2005: 0
3rd Qu.: 0.636000 Muy alto:283 2010: 0
Max. : 5.027000 Muy bajo:345 2015:2457
NA NA NA
#Gráfico condensado para variables numéricas. 

base_marginacion %>% 
  limpiar() %>% 
  recodificar() %>% 
  filter (AÑO=="2015" & ENT!="Nacional") %>% 
  keep (is.numeric) %>% 
  gather(Variable, valor) %>% 
  ggplot(aes(valor)) +
  geom_density() + 
  facet_wrap(~Variable,scales="free")

#Gráfico condensado para variables categóricas. (factores). 

base_marginacion %>% 
  limpiar() %>% 
  recodificar() %>% 
  filter (AÑO=="2015" & ENT!="Nacional") %>% 
  keep (is.factor) %>% 
  gather(Variable, valor) %>% 
  ggplot(aes(valor)) +
  geom_bar() + 
  facet_wrap(~Variable,scales="free") + 
  theme(axis.text = element_text(angle=90))

#Relaciones entre variables numéricas. 

library(corrplot)
base_marginacion %>% 
  limpiar %>% 
  recodificar %>% 
  filter (AÑO=="2015" & ENT!="Nacional") %>% 
  keep (is.numeric) %>% 
  cor %T>% 
  corrplot() %>%
  kable() 

POB_TOT ANALF SPRIM OVSDE OVSEE OVSAE VHAC OVPT PL<5000 PO2SM IM
POB_TOT 1.0000000 -0.2427804 -0.3623459 -0.1120049 -0.1213714 -0.1012738 -0.2246343 -0.1759881 -0.4363121 -0.3272347 -0.3414772
ANALF -0.2427804 1.0000000 0.8855004 0.3027855 0.3945155 0.3967766 0.6476452 0.6299663 0.4541327 0.6032629 0.8842891
SPRIM -0.3623459 0.8855004 1.0000000 0.2976330 0.3728836 0.3463071 0.5550436 0.5521924 0.5867959 0.6511697 0.8712843
OVSDE -0.1120049 0.3027855 0.2976330 1.0000000 0.3980229 0.1739115 0.3524537 0.1752402 0.1989427 0.1915885 0.4413033
OVSEE -0.1213714 0.3945155 0.3728836 0.3980229 1.0000000 0.4012359 0.3581071 0.4828759 0.2649543 0.1964478 0.5851631
OVSAE -0.1012738 0.3967766 0.3463071 0.1739115 0.4012359 1.0000000 0.3402834 0.4302062 0.2027068 0.2824145 0.5464042
VHAC -0.2246343 0.6476452 0.5550436 0.3524537 0.3581071 0.3402834 1.0000000 0.5695477 0.2872756 0.5701576 0.7644394
OVPT -0.1759881 0.6299663 0.5521924 0.1752402 0.4828759 0.4302062 0.5695477 1.0000000 0.3584945 0.4391999 0.7579000
PL<5000 -0.4363121 0.4541327 0.5867959 0.1989427 0.2649543 0.2027068 0.2872756 0.3584945 1.0000000 0.4709845 0.6106980
PO2SM -0.3272347 0.6032629 0.6511697 0.1915885 0.1964478 0.2824145 0.5701576 0.4391999 0.4709845 1.0000000 0.7297121
IM -0.3414772 0.8842891 0.8712843 0.4413033 0.5851631 0.5464042 0.7644394 0.7579000 0.6106980 0.7297121 1.0000000
#Relaciones jerárquicas.
#=======================

library(treemapify)   #
base_marginacion %>% 
    limpiar %>% 
  recodificar %>% 
  filter(AÑO=="2015" & ENT!="Nacional") %>%
    treemapify(area="POB_TOT", fill="GM", group="ENT") %>%
  ggplotify(., group.label.colour = "black", group.label.size.fixed = 4) 

  labs(title="Población por Municipios, Estados y Grado de Marginación",
       subtitle="Población proporcional a área",
       caption="Datos: CONAPO 2015") +
  theme(legend.position = "bottom")
## NULL
#Join de bases de datos. 
#=======================
  
#Pendiente. 

  1. mpaladino@mora.edu.mx

  2. Ya sea utilizando la API especificada por un proveedor de datos o programando a una función para que recorra en sitios web, redes sociales, etc., idetifique ciertos patrones de texto y los alamacene.

  3. Y preferiblemente cortos: en R no seleccionamos las columnas con un menú, introducimos sus nombres en la consola: elija bien esos nombres, los tipeará una y otra vez.

  4. Similar en concepto a | de UNIX.

  5. Los restantes operadores son %T>%, que hace una bifurcación y para dar output a un paso intermedio y %$>%, que regresa el output como un conjunto de vectores en lugar de un data.frame.

  6. Esto no significa que deba tener el mismo número de filas que de columnas, esos serían datos cuadrados, un caso especial de los datos rectangulares.

  7. R impide intrínsecamente llevar a caso semejante mala práctica.

  8. Si cree que esta desprolijidad se debe a las malas prácticas de la frutería poco sabia en cuestiones de manejo de dato considere que las bases de datos del Banco Mundial suelen tener esta estructura.

  9. La sintaxis de gather() puede ser algo complicada, sin embargo una interfaz gráfica llamada tidyshiny() nos puede asistir en el proceso dándonos retroalimentación inmediata. Dado que no está en el repositorio CRAN la instalación es algo más complicada que lo usual, pero puede hacerlo con esta línea que resolverá previamente las dependencia: if(!require(tidyr)) {install.packages("devtools")} ; library(devtools); install_github("MangoTheCat/tidyshiny"). Úsela desde Rstudio en el botón Addins, debajo del menú.

  10. Una recomendación en estos casos es remover del entorno el objeto de datos al que asignamos nombre después de utilizarlo. Ya ha cumplido su función, deberíamos descartarlo.

  11. chsiq.test() admite una tabla como imput, table() produce tablas, pero recibe vectores como imput.

  12. Los errores ortográficos o de estilo en la codificación de una base de datos son un tema menor. Para R “México”, “MEXICO”, “Mexico”, “méxico” son cuatro cadenas de caracteres completamente diferentes.

  13. Nótese el énfasis en exploratorio. Este flujo de trabajo no está organizado alrededor de una hipótesis y utiliza poco o nada del conocimiento disponible sobre un tema. Al carecer de estado del arte y de marco de hipótesis no es un proceso de investigación. Está orientado a familizarizarnos con los datos, identificar y visualizar patrones y relaciones y, en el mejor de los casos, generar nuevas hipótesis. Dicho sea de paso, ni R ni ninguna plataforma de software producen buena investigación o análisis lúcidos, use dplyr, R base o papel y lápiz. A lo sumo pueden facilitarnos los aspectos técnicos y dejarnos más tiempo disponible para investigar o analizar.