Introducción

El objetivo de este curso es que aprendas a utilizar R como parte del flujo de tu trabajo cotidiano.

Esto con el propósito de permitirte trabajar de forma independiente y autónoma de principio a fin.

De esta manera, se reducen o eliminan tiempos de espera para completar solicitudes, permitiendo así un trabajo más eficiente y compacto. En ocasiones es más sencillo hacerlo tu mism@, que tener que explicar a un analista lo que necesitas, esperar a que complete la petición, revisarlo y repetir el proceso en caso de que la petición no haya sido satisfecha.

Otra gran ventaja de utilizar R es el mundo de la automatización propio de los lenguajes de programación. Si alguna vez te encuentras haciendo el mismo proceso en una tabla de Excel o base de SPSS una y otra vez, lo más probable es que puedas automatizarlo hasta cierto punto, permitiéndote ahorrar tiempo y reducir (incluso eliminar) la probabilidad de error humano.

No existirá un examen. La manera de evaluar el progreso en el curso será qué tan cómod@ te sientes para realizar tareas que son parte de tu trabajo diario en R.

Formato del curso

Este curso NO es el tradicional R for beginners que se encuentra en muchos lados. NO vamos a hacer énfasis en las distintas estructuras propias del lenguaje de R. Tampoco será exhaustivo. NO se busca que aprendas R desde el inicio y que llegues a un nivel avanzado.

Este curso es completamente enfocado a tareas muy específicas propias de los estudios con los que trabajamos normalmente. Por esta razón, desde el principio será basado en ejemplos, extraídos de proyectos anteriores.

La forma de trabajo será completamente práctica. Más que un curso, será un taller en el cual habrá entregables y cosas qué hacer desde el principio. No pasaré mucho tiempo hablando de teoría, ni con presentaciones ni compartiendo mi pantalla. Las sesiones serán, más bien, para resolver dudas de entregas anteriores, explorar ideas, y explicar el próximo entregable.

De esta manera, las sesiones serán breves pero frecuentes, buscando que te familiarices con los distintos conceptos de manera gradual pero constante.

Un poco todos los días.

La idea es que al terminar el curso, tengas los elementos necesarios para sentirte cómod@ resolviendo problemas utilizando R, pero también que tengas los recursos para investigar y profundizar el conocimiento adquirido, para seguir progresando en la curva de aprendizaje.

Set up inicial

R y RStudio

El primer paso es descargar R desde aquí. Abre el archivo ejecutable para instalar R.

Y RStudio desde aquí Abre el archivo ejecutable para instalar RStudio.

Una vez instalados, abre RStudio y en la consola escribe lo siguiente:

install.packages('tidyverse')

Building blocks: R y tidyverse

Aunque la idea general de el taller es que aprendas a usar R sobre la marcha, con ejemplos basados en nuestro trabajo cotidiano, es necesario establecer algunas bases, para lo cual utilizaremos ejemplos muy sencillos.

Esta sección está basada en este libro. No dudes en consultarlo frecuentemente.

¿Qué es Tidyverse?

Tidyverse es un conjunto de paquetes de R que comparten un paradigma de programación en común, y a su vez son fundamentalmente distintos al paradigma tradicional de programación básica en R. Tidyverse está desarrollado para el análisis y visualización de datos en formato tabular, o data frame, con el propósito principal que sea sencillo utilizarse sin necesidad de un background en lenguajes de programación. Esto sucede ya que tiene una sintaxis muy verbal, es decir, que el código es fácilmente entendible por un humano.

El primer paso es cargar el paquete que a su vez cargará todos los paquetes:

library(tidyverse)

Nótese que al imprimirse el output, se muestra una lista de todos los paquetes que se están utilizando. No te preocupes si aún no es claro qué es un paquete. Sólo es necesario saber que es necesario iniciar siempre con el comando anterior.

Para esta sección, utilizaremos el dataset diamonds, que viene precargado en tidyverse.

diamonds
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  6  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
##  7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
## 10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Observa que al imprimir los datos, se muestran varios pedazos de información importantes.

# A tibble: 53,940 x 10

Nos indica el número total de filas x columnas. Es útil tener presente esta información, ya que en ocasiones es útil verificar las dimensiones de nuestro tibble.

Después, tenemos los nombres de cada columna: carat, cuty, z.

Justo debajo de los nombres de las columnas, tenemos entre llaves ciertas palabras clave. Por ejemplo, Debajo de carat está <dbl> y debajo de cut está <ord>. Esto se refiere al tipo de columna que se tiene. Es importante tenerlo en mente, pues será útil más adelante.

A continuación, una lista de algunos tipos de datos y lo que significan:

  • dbl: DOUBLE - Número con precisión de varios decimales.
  • int: INTEGER - Número entero, es decir, sin decimales.
  • chr: CHARACTER - Un pedazo de texto, acepta cualquier tipo de caracteres.
  • fct: FACTOR - Pueden ser de tipo chr o numéricos. Cuando una columna es de tipo factor, se establece que sólo puede tomar valores dentro del conjunto de niveles definidos para la columna. Mejor conocidos como variables categóricas o nominales.
  • ord: ORDERED FACTOR - Similar a factor, pero se establece un ordenamiento, o jerarquía entre los distintos niveles de factores, es decir, variables ordinales.
  • date: DATE. Objeto de tipo fecha. Almacena datos temporales hasta nivel día.
  • dttm: DATETIME. Objeto de tipo fecha hora. A diferencia de date, con este formato se puede almacenar datos temporales hasta el nivel hora, minuto, segundo y milisegundo

No es necesario que memorices las diferencias entre los tipos de datos. Quedarán claras más adelante.

Una vez definidos estos conceptos clave, podemos ver algunas funciones útiles.

arrange(), head(), tail()

Una de las operaciones más comunes que se puede realizar a un tibble o dataframe es ordenarlo de acuerdo a los valores de una columna.

Para esto es es útil arrange().

Supongamos que queremos ver el rango de los valores que puede tomar la columna carat. Podemos ordenar de menor a mayor por medio de:

diamonds %>% 
  arrange(carat)
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1   0.2 Premium   E     SI2      60.2    62   345  3.79  3.75  2.27
##  2   0.2 Premium   E     VS2      59.8    62   367  3.79  3.77  2.26
##  3   0.2 Premium   E     VS2      59      60   367  3.81  3.78  2.24
##  4   0.2 Premium   E     VS2      61.1    59   367  3.81  3.78  2.32
##  5   0.2 Premium   E     VS2      59.7    62   367  3.84  3.8   2.28
##  6   0.2 Ideal     E     VS2      59.7    55   367  3.86  3.84  2.3 
##  7   0.2 Premium   F     VS2      62.6    59   367  3.73  3.71  2.33
##  8   0.2 Ideal     D     VS2      61.5    57   367  3.81  3.77  2.33
##  9   0.2 Very Good E     VS2      63.4    59   367  3.74  3.71  2.36
## 10   0.2 Ideal     E     VS2      62.2    57   367  3.76  3.73  2.33
## # ... with 53,930 more rows

Ordenó TODA la tabla utilizando carat como índice.

Y luego de mayor a menor:

diamonds %>% 
  arrange(-carat)
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  5.01 Fair      J     I1       65.5    59 18018 10.7  10.5   6.98
##  2  4.5  Fair      J     I1       65.8    58 18531 10.2  10.2   6.72
##  3  4.13 Fair      H     I1       64.8    61 17329 10     9.85  6.43
##  4  4.01 Premium   I     I1       61      61 15223 10.1  10.1   6.17
##  5  4.01 Premium   J     I1       62.5    62 15223 10.0   9.94  6.24
##  6  4    Very Good I     I1       63.3    58 15984 10.0   9.94  6.31
##  7  3.67 Premium   I     I1       62.4    56 16193  9.86  9.81  6.13
##  8  3.65 Fair      H     I1       67.1    53 11668  9.53  9.48  6.38
##  9  3.51 Premium   J     VS2      62.5    59 18701  9.66  9.63  6.03
## 10  3.5  Ideal     H     I1       62.8    57 12587  9.65  9.59  6.03
## # ... with 53,930 more rows

El signo - indica que debe ser en orden descendiente.

Podemos ordenar basado en varias columnas.

diamonds %>% 
  arrange(cut, -carat)
## # A tibble: 53,940 x 10
##    carat cut   color clarity depth table price     x     y     z
##    <dbl> <ord> <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  5.01 Fair  J     I1       65.5    59 18018 10.7  10.5   6.98
##  2  4.5  Fair  J     I1       65.8    58 18531 10.2  10.2   6.72
##  3  4.13 Fair  H     I1       64.8    61 17329 10     9.85  6.43
##  4  3.65 Fair  H     I1       67.1    53 11668  9.53  9.48  6.38
##  5  3.4  Fair  D     I1       66.8    52 15964  9.42  9.34  6.27
##  6  3.11 Fair  J     I1       65.9    57  9823  9.15  9.02  5.98
##  7  3.02 Fair  I     I1       65.2    56 10577  9.11  9.02  5.91
##  8  3.01 Fair  H     I1       56.1    62 10761  9.54  9.38  5.31
##  9  3.01 Fair  I     SI2      65.8    56 18242  8.99  8.94  5.9 
## 10  3.01 Fair  I     SI2      65.8    56 18242  8.99  8.94  5.9 
## # ... with 53,930 more rows

En este caso, ordena primero todas las filas por cut, pero cuando hay empates, re-ordena de forma descendiente por carat.

Podemos usar varias columnas:

diamonds %>% 
  arrange(cut, clarity, color, -carat)
## # A tibble: 53,940 x 10
##    carat cut   color clarity depth table price     x     y     z
##    <dbl> <ord> <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  3.4  Fair  D     I1       66.8    52 15964  9.42  9.34  6.27
##  2  1.7  Fair  D     I1       64.7    56  5617  7.46  7.37  4.8 
##  3  1.5  Fair  D     I1       64.7    62  5460  7.19  7.04  4.6 
##  4  0.91 Fair  D     I1       66.2    57  2491  6     5.94  3.95
##  5  1.3  Fair  E     I1       66.5    58  2571  6.79  6.75  4.5 
##  6  1.23 Fair  E     I1       67.4    56  3692  6.76  6.56  4.49
##  7  1.03 Fair  E     I1       78.2    54  1262  5.72  5.59  4.42
##  8  1.01 Fair  E     I1       64.5    58  2788  6.29  6.21  4.03
##  9  1.01 Fair  E     I1       64.5    54  2036  6.31  6.22  4.03
## 10  0.9  Fair  E     I1       59.9    66  2138  6.18  6.14  3.69
## # ... with 53,930 more rows

Si queremos obtener los últimos renglones, podemos utilizar tail():

diamonds %>% 
  arrange(cut, clarity, color, -carat) %>% 
  tail(15)
## # A tibble: 15 x 10
##    carat cut   color clarity depth table price     x     y     z
##    <dbl> <ord> <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.4  Ideal J     IF       62.5    56   758  4.7   4.74  2.95
##  2  0.4  Ideal J     IF       62.4    56   828  4.71  4.74  2.95
##  3  0.4  Ideal J     IF       62.6    56   828  4.71  4.75  2.96
##  4  0.4  Ideal J     IF       62.4    56  1063  4.74  4.71  2.95
##  5  0.4  Ideal J     IF       62.6    56  1063  4.75  4.71  2.96
##  6  0.37 Ideal J     IF       61.9    54   601  4.63  4.68  2.88
##  7  0.35 Ideal J     IF       61.8    55   569  4.54  4.56  2.81
##  8  0.35 Ideal J     IF       61.8    53   569  4.58  4.61  2.84
##  9  0.32 Ideal J     IF       62.2    55   684  4.44  4.4   2.75
## 10  0.32 Ideal J     IF       62.2    56   684  4.41  4.37  2.73
## 11  0.32 Ideal J     IF       61      57   521  4.42  4.46  2.71
## 12  0.32 Ideal J     IF       62.2    55   533  4.4   4.44  2.75
## 13  0.32 Ideal J     IF       62.2    56   533  4.37  4.41  2.73
## 14  0.3  Ideal J     IF       61.5    56   489  4.32  4.33  2.66
## 15  0.3  Ideal J     IF       61.5    57   489  4.29  4.36  2.66

Observa que se visualizan en la consola 15 renglones, y también que el tibble con el que trabajamos es de únicamente 15 renglones. Se ve en: a tibble: 15 x 10. A diferencia de print(), tail() sí toma un subconjunto de renglones. Es decir, sí realiza una operación al dataframe, por lo que sirve para recortar los datos y no sólo para visualizar.

head() es análogo a tail(), funciona para obtener los primeros n renglones de los datos.

diamonds %>% 
  arrange(cut, clarity, color, -carat) %>% 
  head(15)
## # A tibble: 15 x 10
##    carat cut   color clarity depth table price     x     y     z
##    <dbl> <ord> <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  3.4  Fair  D     I1       66.8    52 15964  9.42  9.34  6.27
##  2  1.7  Fair  D     I1       64.7    56  5617  7.46  7.37  4.8 
##  3  1.5  Fair  D     I1       64.7    62  5460  7.19  7.04  4.6 
##  4  0.91 Fair  D     I1       66.2    57  2491  6     5.94  3.95
##  5  1.3  Fair  E     I1       66.5    58  2571  6.79  6.75  4.5 
##  6  1.23 Fair  E     I1       67.4    56  3692  6.76  6.56  4.49
##  7  1.03 Fair  E     I1       78.2    54  1262  5.72  5.59  4.42
##  8  1.01 Fair  E     I1       64.5    58  2788  6.29  6.21  4.03
##  9  1.01 Fair  E     I1       64.5    54  2036  6.31  6.22  4.03
## 10  0.9  Fair  E     I1       59.9    66  2138  6.18  6.14  3.69
## 11  0.8  Fair  E     I1       65.5    54  1232  5.84  5.68  3.78
## 12  0.74 Fair  E     I1       58.2    65  1865  6.03  5.89  3.47
## 13  0.7  Fair  E     I1       66.1    58  1273  5.61  5.51  3.67
## 14  2.11 Fair  F     I1       67.6    57  7019  7.88  7.83  5.31
## 15  2.03 Fair  F     I1       65.6    56  6753  7.89  7.86  5.16

distinct()

En ocasiones queremos obtener los valores únicos de una columna. Para esto es útil distinct():

diamonds %>% 
  distinct(cut)
## # A tibble: 5 x 1
##   cut      
##   <ord>    
## 1 Ideal    
## 2 Premium  
## 3 Good     
## 4 Very Good
## 5 Fair

Si utilizamos 2 o más columnas dentro de dicha función, se imprimen las combinaciones distintas de todos los valores de dichas columnas.

diamonds %>% 
  distinct(cut,color) %>% 
  arrange(cut, color)
## # A tibble: 35 x 2
##    cut   color
##    <ord> <ord>
##  1 Fair  D    
##  2 Fair  E    
##  3 Fair  F    
##  4 Fair  G    
##  5 Fair  H    
##  6 Fair  I    
##  7 Fair  J    
##  8 Good  D    
##  9 Good  E    
## 10 Good  F    
## # ... with 25 more rows

select() y rename()

Otra operación común es eliminar columnas, o bien, conservar sólo un subconjunto de ellas.

select() funciona para seleccionar una o más columnas específicas:

diamonds %>% 
  select(carat, color, x, clarity)
## # A tibble: 53,940 x 4
##    carat color     x clarity
##    <dbl> <ord> <dbl> <ord>  
##  1  0.23 E      3.95 SI2    
##  2  0.21 E      3.89 SI1    
##  3  0.23 E      4.05 VS1    
##  4  0.29 I      4.2  VS2    
##  5  0.31 J      4.34 SI2    
##  6  0.24 J      3.94 VVS2   
##  7  0.24 I      3.95 VVS1   
##  8  0.26 H      4.07 SI1    
##  9  0.22 E      3.87 VS2    
## 10  0.23 H      4    VS1    
## # ... with 53,930 more rows

Hay formas alternativas de utilizar select.

Podemos quitar columnas en específico utilizando el signo -:

diamonds %>% 
  select(-carat, -cut)
## # A tibble: 53,940 x 8
##    color clarity depth table price     x     y     z
##    <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1 E     SI2      61.5    55   326  3.95  3.98  2.43
##  2 E     SI1      59.8    61   326  3.89  3.84  2.31
##  3 E     VS1      56.9    65   327  4.05  4.07  2.31
##  4 I     VS2      62.4    58   334  4.2   4.23  2.63
##  5 J     SI2      63.3    58   335  4.34  4.35  2.75
##  6 J     VVS2     62.8    57   336  3.94  3.96  2.48
##  7 I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8 H     SI1      61.9    55   337  4.07  4.11  2.53
##  9 E     VS2      65.1    61   337  3.87  3.78  2.49
## 10 H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Podemos referirnos a columnas por su posición, en lugar de su nombre.

diamonds %>% 
  select(1,3,5,7,9)
## # A tibble: 53,940 x 5
##    carat color depth price     y
##    <dbl> <ord> <dbl> <int> <dbl>
##  1  0.23 E      61.5   326  3.98
##  2  0.21 E      59.8   326  3.84
##  3  0.23 E      56.9   327  4.07
##  4  0.29 I      62.4   334  4.23
##  5  0.31 J      63.3   335  4.35
##  6  0.24 J      62.8   336  3.96
##  7  0.24 I      62.3   336  3.98
##  8  0.26 H      61.9   337  4.11
##  9  0.22 E      65.1   337  3.78
## 10  0.23 H      59.4   338  4.05
## # ... with 53,930 more rows

Es útil también para reordenar las columnas, sin quitar ninguna

diamonds %>% 
  select(10:1)
## # A tibble: 53,940 x 10
##        z     y     x price table depth clarity color cut       carat
##    <dbl> <dbl> <dbl> <int> <dbl> <dbl> <ord>   <ord> <ord>     <dbl>
##  1  2.43  3.98  3.95   326    55  61.5 SI2     E     Ideal      0.23
##  2  2.31  3.84  3.89   326    61  59.8 SI1     E     Premium    0.21
##  3  2.31  4.07  4.05   327    65  56.9 VS1     E     Good       0.23
##  4  2.63  4.23  4.2    334    58  62.4 VS2     I     Premium    0.29
##  5  2.75  4.35  4.34   335    58  63.3 SI2     J     Good       0.31
##  6  2.48  3.96  3.94   336    57  62.8 VVS2    J     Very Good  0.24
##  7  2.47  3.98  3.95   336    57  62.3 VVS1    I     Very Good  0.24
##  8  2.53  4.11  4.07   337    55  61.9 SI1     H     Very Good  0.26
##  9  2.49  3.78  3.87   337    61  65.1 VS2     E     Fair       0.22
## 10  2.39  4.05  4      338    61  59.4 VS1     H     Very Good  0.23
## # ... with 53,930 more rows

BONUS: El operador a:b sirve para crear una secuencia de 1 en 1 del número a al número b. Prueba poner 10:20 en tu consola.

Por último, podemos renombrar columnas al seleccionarlas con el comando nombre_nuevo = nombre_original.

diamonds %>% 
  select(kilates = carat, precio = price)
## # A tibble: 53,940 x 2
##    kilates precio
##      <dbl>  <int>
##  1    0.23    326
##  2    0.21    326
##  3    0.23    327
##  4    0.29    334
##  5    0.31    335
##  6    0.24    336
##  7    0.24    336
##  8    0.26    337
##  9    0.22    337
## 10    0.23    338
## # ... with 53,930 more rows

Nótese que se cambia el nombre y se seleccionan al mismo tiempo.

Si queremos conservar todas las columnas, y sólo cambiar el nombre de algunas, podemos utilizar rename.

diamonds %>% 
  rename(kilates = carat, precio = price)
## # A tibble: 53,940 x 10
##    kilates cut       color clarity depth table precio     x     y     z
##      <dbl> <ord>     <ord> <ord>   <dbl> <dbl>  <int> <dbl> <dbl> <dbl>
##  1    0.23 Ideal     E     SI2      61.5    55    326  3.95  3.98  2.43
##  2    0.21 Premium   E     SI1      59.8    61    326  3.89  3.84  2.31
##  3    0.23 Good      E     VS1      56.9    65    327  4.05  4.07  2.31
##  4    0.29 Premium   I     VS2      62.4    58    334  4.2   4.23  2.63
##  5    0.31 Good      J     SI2      63.3    58    335  4.34  4.35  2.75
##  6    0.24 Very Good J     VVS2     62.8    57    336  3.94  3.96  2.48
##  7    0.24 Very Good I     VVS1     62.3    57    336  3.95  3.98  2.47
##  8    0.26 Very Good H     SI1      61.9    55    337  4.07  4.11  2.53
##  9    0.22 Fair      E     VS2      65.1    61    337  3.87  3.78  2.49
## 10    0.23 Very Good H     VS1      59.4    61    338  4     4.05  2.39
## # ... with 53,930 more rows

Asignación: Crear y modificar objetos

Algo que es importante tener en mente es que correr estas funciones realiza cambios que no se conservan en la base original.

Por ejemplo, si ejecutas en R:

diamonds %>% 
  select(carat)
## # A tibble: 53,940 x 1
##    carat
##    <dbl>
##  1  0.23
##  2  0.21
##  3  0.23
##  4  0.29
##  5  0.31
##  6  0.24
##  7  0.24
##  8  0.26
##  9  0.22
## 10  0.23
## # ... with 53,930 more rows

E inmediatamente después ejecutas:

diamonds
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  6  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
##  7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
## 10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Observarás que el dataframe diamonds sigue intacto. Aunque aplicamos una operación en el dataframe, esa operación no cambió la forma original del dataframe.

Si deseamos conservar los cambios. Es necesario asignar el resultado de nuestras operaciones a un nuevo dataframe.

diamonds_carat <- diamonds %>% 
  select(carat)

El operador <- es el que se encarga de la asignación. Se interpreta como:

“Asigna al objeto llamado diamonds_carat el resultado de las operaciones del lado derecho.”

Al correr esto suceden 2 cosas:

  • No se imprime el resultado en tu consola. Esto es porque la operación que corriste es de asignación, no de visualización.

  • En tu environment, aparece un nuevo renglón.

Esto significa que has creado un nuevo objeto, y ahora puedes acceder a él con:

diamonds_carat
## # A tibble: 53,940 x 1
##    carat
##    <dbl>
##  1  0.23
##  2  0.21
##  3  0.23
##  4  0.29
##  5  0.31
##  6  0.24
##  7  0.24
##  8  0.26
##  9  0.22
## 10  0.23
## # ... with 53,930 more rows

IMPORTANTE:

Observa que el dataframe que creamos tiene un nombre distinto, se llama diamonds_caraty no diamonds. Esto es para no modificar el dataframe original, pues todas las operaciones que se hagan después asumen que diamonds tiene la estructura original.

Se pueden crear tantos objetos se desee, y los que ya se tienen se pueden modificar tantas veces sea necesario.

diamonds_carat <- diamonds %>% 
  select(carat, price, cut)

diamonds_carat
## # A tibble: 53,940 x 3
##    carat price cut      
##    <dbl> <int> <ord>    
##  1  0.23   326 Ideal    
##  2  0.21   326 Premium  
##  3  0.23   327 Good     
##  4  0.29   334 Premium  
##  5  0.31   335 Good     
##  6  0.24   336 Very Good
##  7  0.24   336 Very Good
##  8  0.26   337 Very Good
##  9  0.22   337 Fair     
## 10  0.23   338 Very Good
## # ... with 53,930 more rows
diamonds_cut <- diamonds %>% 
  select(cut)

diamonds_cut
## # A tibble: 53,940 x 1
##    cut      
##    <ord>    
##  1 Ideal    
##  2 Premium  
##  3 Good     
##  4 Premium  
##  5 Good     
##  6 Very Good
##  7 Very Good
##  8 Very Good
##  9 Fair     
## 10 Very Good
## # ... with 53,930 more rows

Una vez más, podemos corroborar estos cambios en nuestro Environment:

filter()

Sirve para filtrar renglones basado en una condición específica. Usualmente se utiliza en conjunto de otras funciones u operadores.

diamonds %>% 
  filter(carat > 3.4)
## # A tibble: 10 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  3.65 Fair      H     I1       67.1    53 11668  9.53  9.48  6.38
##  2  3.5  Ideal     H     I1       62.8    57 12587  9.65  9.59  6.03
##  3  4.01 Premium   I     I1       61      61 15223 10.1  10.1   6.17
##  4  4.01 Premium   J     I1       62.5    62 15223 10.0   9.94  6.24
##  5  4    Very Good I     I1       63.3    58 15984 10.0   9.94  6.31
##  6  3.67 Premium   I     I1       62.4    56 16193  9.86  9.81  6.13
##  7  4.13 Fair      H     I1       64.8    61 17329 10     9.85  6.43
##  8  5.01 Fair      J     I1       65.5    59 18018 10.7  10.5   6.98
##  9  4.5  Fair      J     I1       65.8    58 18531 10.2  10.2   6.72
## 10  3.51 Premium   J     VS2      62.5    59 18701  9.66  9.63  6.03

En este caso se utiliza en conjunto del operador > (mayor que).

La principal ventaja de tidyverse es que permite que los comandos puedan leerse fácilmente de manera humana, de izquiera a derecha, de arriba a abajo.

El comando anterior se lee facilmente como:

Del tibble diamonds, filtra los renglones en donde la columna carat tiene valores mayores a 3.4

Solo para contrastar (no es necesario detenerse mucho con el siguiente código) aquí se muestra este mismo comando sin utilizar tidyverse.

diamonds[diamonds$carat > 3.4, ]
## # A tibble: 10 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  3.65 Fair      H     I1       67.1    53 11668  9.53  9.48  6.38
##  2  3.5  Ideal     H     I1       62.8    57 12587  9.65  9.59  6.03
##  3  4.01 Premium   I     I1       61      61 15223 10.1  10.1   6.17
##  4  4.01 Premium   J     I1       62.5    62 15223 10.0   9.94  6.24
##  5  4    Very Good I     I1       63.3    58 15984 10.0   9.94  6.31
##  6  3.67 Premium   I     I1       62.4    56 16193  9.86  9.81  6.13
##  7  4.13 Fair      H     I1       64.8    61 17329 10     9.85  6.43
##  8  5.01 Fair      J     I1       65.5    59 18018 10.7  10.5   6.98
##  9  4.5  Fair      J     I1       65.8    58 18531 10.2  10.2   6.72
## 10  3.51 Premium   J     VS2      62.5    59 18701  9.66  9.63  6.03

Aquí comienza a ser relevante el tipo de datos que conforma cada columna.

Si corro lo siguiente:

diamonds %>% 
  filter(clarity == VS2)

Me marcará un error. ¿Por qué?

La forma de corregirlo es utilizando " " o ' '.

diamonds %>% 
  filter(clarity == "VS2")
## # A tibble: 12,258 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  2  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
##  3  0.23 Very Good E     VS2      63.8    55   352  3.85  3.92  2.48
##  4  0.3  Very Good J     VS2      62.2    57   357  4.28  4.3   2.67
##  5  0.23 Very Good D     VS2      60.5    61   357  3.96  3.97  2.4 
##  6  0.26 Very Good D     VS2      60.8    59   403  4.13  4.16  2.52
##  7  0.26 Good      D     VS2      65.2    56   403  3.99  4.02  2.61
##  8  0.25 Very Good E     VS2      63.3    60   404  4     4.03  2.54
##  9  0.22 Premium   E     VS2      61.6    58   404  3.93  3.89  2.41
## 10  0.22 Premium   D     VS2      59.3    62   404  3.91  3.88  2.31
## # ... with 12,248 more rows

Esto es porque las comillas indican que estoy usando texto. En este caso, es útil ver que la columna clarity tiene valores como texto.

BONUS: Nótese que para realizar comparaciones se utiliza ==. Esto es porque el operador = ya es utilizado para asignación. (Como se observó en el ejemplo de renombrar usando select.)

¿Recuerdas que ord representa a ordenado? Veamos qué significa eso:

diamonds %>% 
  filter(cut >= "Premium")
## # A tibble: 35,342 x 10
##    carat cut     color clarity depth table price     x     y     z
##    <dbl> <ord>   <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal   E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.29 Premium I     VS2      62.4    58   334  4.2   4.23  2.63
##  4  0.23 Ideal   J     VS1      62.8    56   340  3.93  3.9   2.46
##  5  0.22 Premium F     SI1      60.4    61   342  3.88  3.84  2.33
##  6  0.31 Ideal   J     SI2      62.2    54   344  4.35  4.37  2.71
##  7  0.2  Premium E     SI2      60.2    62   345  3.79  3.75  2.27
##  8  0.32 Premium E     I1       60.9    58   345  4.38  4.42  2.68
##  9  0.3  Ideal   I     SI2      62      54   348  4.31  4.34  2.68
## 10  0.24 Premium I     VS1      62.5    57   355  3.97  3.94  2.47
## # ... with 35,332 more rows

Encontramos los valores únicos de cut del tibble resultante

diamonds %>% 
  filter(cut >= "Premium") %>% 
  distinct(cut)
## # A tibble: 2 x 1
##   cut    
##   <ord>  
## 1 Ideal  
## 2 Premium

Aunque estamos trabajando con texto, R sabe que Ideal está por encima de Premium, y a su vez, estos son los niveles más altos. Esto lo sabe porque el ordenamiento está definido en la estructura de la variable. Lo verificamos por medio de la función attributes().

diamonds$cut %>% attributes()
## $class
## [1] "ordered" "factor" 
## 
## $levels
## [1] "Fair"      "Good"      "Very Good" "Premium"   "Ideal"

Si la columna cut fuera de formato chr, lo anterior no funcionaría como se espera. Veamos qué sucede:

diamonds_chr
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <chr>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  6  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
##  7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
## 10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Nótese que dice <chr> debajo de cut, en lugar de <ord>.

diamonds_chr %>% 
  filter(cut >= "Premium")%>% 
  distinct(cut)
## # A tibble: 2 x 1
##   cut      
##   <chr>    
## 1 Premium  
## 2 Very Good

Como R no tiene manera de saber qué valor es mayor que Premium toma un ordenamiento alfabético.

Hasta ahora hemos visto cómo obtener un subconjunto de, o recortar, un tibble por medio de filter() para renglones y select() para columnas.

Veamos ahora cómo modificar los datos con los que trabajamos.

mutate()

Una operación muy frecuente en el proceso exploratorio y de limpieza es la de agregar una columna a tus datos. Para eso se utiliza mutate().

Puede utilizarse de 3 formas.

  • Operación de una o más columnas existentes:
diamonds %>% 
  mutate(suma = x+y+z,
         producto = x*y*z,
         dos_x = 2*x) %>% 
  select(x:dos_x)
## # A tibble: 53,940 x 6
##        x     y     z  suma producto dos_x
##    <dbl> <dbl> <dbl> <dbl>    <dbl> <dbl>
##  1  3.95  3.98  2.43  10.4     38.2  7.9 
##  2  3.89  3.84  2.31  10.0     34.5  7.78
##  3  4.05  4.07  2.31  10.4     38.1  8.1 
##  4  4.2   4.23  2.63  11.1     46.7  8.4 
##  5  4.34  4.35  2.75  11.4     51.9  8.68
##  6  3.94  3.96  2.48  10.4     38.7  7.88
##  7  3.95  3.98  2.47  10.4     38.8  7.9 
##  8  4.07  4.11  2.53  10.7     42.3  8.14
##  9  3.87  3.78  2.49  10.1     36.4  7.74
## 10  4     4.05  2.39  10.4     38.7  8   
## # ... with 53,930 more rows

BONUS: ¿Notaste que el operador a:b tiene más formas de usarse?

En un mismo mutate() se pueden agregar todas las columnas que necesitemos.

  • Con un sólo valor fijo
diamonds %>% 
  mutate(cte = 2,
         cte_chr = 'constante') %>% 
  select(x:cte_chr)
## # A tibble: 53,940 x 5
##        x     y     z   cte cte_chr  
##    <dbl> <dbl> <dbl> <dbl> <chr>    
##  1  3.95  3.98  2.43     2 constante
##  2  3.89  3.84  2.31     2 constante
##  3  4.05  4.07  2.31     2 constante
##  4  4.2   4.23  2.63     2 constante
##  5  4.34  4.35  2.75     2 constante
##  6  3.94  3.96  2.48     2 constante
##  7  3.95  3.98  2.47     2 constante
##  8  4.07  4.11  2.53     2 constante
##  9  3.87  3.78  2.49     2 constante
## 10  4     4.05  2.39     2 constante
## # ... with 53,930 more rows

Observa que mutate() repite aútomaticamente el valor dado a lo largo de toda la tabla.

  • Un vector que tenga longitud igual al número de renglones.

Un vector es la unidad más básica de información en R. Se puede crear de varias maneras, usando c(), :, u otras funciones que regresan vectores, como seq() o rep().

En este caso, crearemos un vector que tenga 53,940 elementos, que es igual al número de filas en el tibble original.

vct <- 1:53940

Observamos los valores obtenidos con la función str().

str(vct)
##  int [1:53940] 1 2 3 4 5 6 7 8 9 10 ...

Así, podemos agregar dicho vector a una columna de nuestros datos.

diamonds %>% 
  mutate(vector = vct) %>% 
  select(price:vector)
## # A tibble: 53,940 x 5
##    price     x     y     z vector
##    <int> <dbl> <dbl> <dbl>  <int>
##  1   326  3.95  3.98  2.43      1
##  2   326  3.89  3.84  2.31      2
##  3   327  4.05  4.07  2.31      3
##  4   334  4.2   4.23  2.63      4
##  5   335  4.34  4.35  2.75      5
##  6   336  3.94  3.96  2.48      6
##  7   336  3.95  3.98  2.47      7
##  8   337  4.07  4.11  2.53      8
##  9   337  3.87  3.78  2.49      9
## 10   338  4     4.05  2.39     10
## # ... with 53,930 more rows

summarise()

En ocasiones, nos interesa calcular cifras como promedios, desviación estándar, percentiles, máximos, mínimos, etc. con los datos con los que estamos trabajando.

Veamos un ejemplo calculando el promedio del precio de todos los diamantes.

diamonds %>% 
  summarise(precio_prom = mean(price))
## # A tibble: 1 x 1
##   precio_prom
##         <dbl>
## 1       3933.

Observemos que el output se compactó. Pasamos de tener 53,940 filas y 10 columnas a tener un tibble de 1x1.

Summarise signficia resumir, y vemos aquí por qué tiene ese nombre. La idea de esta función es condensar la información de formas útiles, quitando todas las columnas que no se usan, y compactando toda la información a un sólo renglón.

group_by()

¿Qué pasa si queremos obtener el precio promedio de cada tipo de corte cut, en lugar de el precio promedio a lo largo de toda la base?

Para este tipo de preguntas, se utiliza group_by():

diamonds %>% 
  group_by(cut) %>% 
  summarise(precio_prom = mean(price))
## # A tibble: 5 x 2
##   cut       precio_prom
##   <ord>           <dbl>
## 1 Fair            4359.
## 2 Good            3929.
## 3 Very Good       3982.
## 4 Premium         4584.
## 5 Ideal           3458.

Ahora tenemos una cifra por cada nivel de cut.

Podemos utilizar varias columnas para agrupar, así como varias funciones para resumir distintas columnas.

diamonds %>% 
  group_by(cut, color) %>% 
  summarise(precio_prom = mean(price),
            carat_prom = mean(carat),
            precio_max = max(price))
## # A tibble: 35 x 5
##    cut   color precio_prom carat_prom precio_max
##    <ord> <ord>       <dbl>      <dbl>      <int>
##  1 Fair  D           4291.      0.920      16386
##  2 Fair  E           3682.      0.857      15584
##  3 Fair  F           3827.      0.905      17995
##  4 Fair  G           4239.      1.02       18574
##  5 Fair  H           5136.      1.22       18565
##  6 Fair  I           4685.      1.20       18242
##  7 Fair  J           4976.      1.34       18531
##  8 Good  D           3405.      0.745      18468
##  9 Good  E           3424.      0.745      18236
## 10 Good  F           3496.      0.776      18686
## # ... with 25 more rows

pivot_wider() y pivot_longer()

Si alguna vez has trabajado con tablas dinámicas, o pivot tables, en Excel notarás que muchas de estás funciones operan de manera similar, filtrando valores, quitando o agregando columnas y resumiendo con funciones.

Otra operación que a menudo se realiza es la de cambiar la forma que tienen los datos, sin modificar el contenido. Cambiando lo que está como renglon a columnas y vice versa.

Por ejemplo, tomemos los siguientes datos:

diamonds %>% 
  group_by(cut, color) %>% 
  summarise(precio_prom = mean(price))
## # A tibble: 35 x 3
##    cut   color precio_prom
##    <ord> <ord>       <dbl>
##  1 Fair  D           4291.
##  2 Fair  E           3682.
##  3 Fair  F           3827.
##  4 Fair  G           4239.
##  5 Fair  H           5136.
##  6 Fair  I           4685.
##  7 Fair  J           4976.
##  8 Good  D           3405.
##  9 Good  E           3424.
## 10 Good  F           3496.
## # ... with 25 more rows

Para visualizar la información más fácilmente, es lógico querer ver una tabla dónde las columnas sean los niveles de cut, y los renglones correspondan a los niveles de color.

Para esto utilizamos la función pivot_wider():

data_wide <- diamonds %>% 
  group_by(cut, color) %>% 
  summarise(precio_prom = mean(price)) %>% 
  pivot_wider(names_from = "cut", values_from = "precio_prom")
  

data_wide
## # A tibble: 7 x 6
##   color  Fair  Good `Very Good` Premium Ideal
##   <ord> <dbl> <dbl>       <dbl>   <dbl> <dbl>
## 1 D     4291. 3405.       3470.   3631. 2629.
## 2 E     3682. 3424.       3215.   3539. 2598.
## 3 F     3827. 3496.       3779.   4325. 3375.
## 4 G     4239. 4123.       3873.   4501. 3721.
## 5 H     5136. 4276.       4535.   5217. 3889.
## 6 I     4685. 5079.       5256.   5946. 4452.
## 7 J     4976. 4574.       5104.   6295. 4918.

De esta forma se ven los datos de manera tabular, haciendo más fácil la interpretación.

Observa el argumento names_from. Este argumento indica qué variable será la que aparecerá en las columnas, mientras que el argumento values_from indica con qué valores se rellenará la tabla.

Para regresarlo a su forma anterior, utilizamos pivot_longer().

data_wide %>% 
  pivot_longer(names_to = "cut", values_to = "precio_prom", cols = c(Fair, Good, `Very Good`, Premium, Ideal))
## # A tibble: 35 x 3
##    color cut       precio_prom
##    <ord> <chr>           <dbl>
##  1 D     Fair            4291.
##  2 D     Good            3405.
##  3 D     Very Good       3470.
##  4 D     Premium         3631.
##  5 D     Ideal           2629.
##  6 E     Fair            3682.
##  7 E     Good            3424.
##  8 E     Very Good       3215.
##  9 E     Premium         3539.
## 10 E     Ideal           2598.
## # ... with 25 more rows

Vemos que ahora los argumentos son names_to, que indica cómo se llamará la columna en dónde se juntarán las categorías de las variables, values_to que indica cómo se llamará la columna donde se juntarán los valores, y cols que indica qué columnas de la data anterior se utilizarán para llenar las columnas de la data nueva.

Podemos transponer la tabla con la ayuda de pivots.

data_wide %>% 
  pivot_longer(names_to = "cut", values_to = "precio_prom", cols = c(Fair, Good, `Very Good`, Premium, Ideal)) %>% 
  pivot_wider(names_from = "color", values_from = "precio_prom")
## # A tibble: 5 x 8
##   cut           D     E     F     G     H     I     J
##   <chr>     <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Fair      4291. 3682. 3827. 4239. 5136. 4685. 4976.
## 2 Good      3405. 3424. 3496. 4123. 4276. 5079. 4574.
## 3 Very Good 3470. 3215. 3779. 3873. 4535. 5256. 5104.
## 4 Premium   3631. 3539. 4325. 4501. 5217. 5946. 6295.
## 5 Ideal     2629. 2598. 3375. 3721. 3889. 4452. 4918.

Viendo los outputs, es fácil entender por qué las funciones tienen estos nombres. Wide significa ancho, y en este caso la data que sale de usar pivot_wider es más ancha en el sentido que tiene más columnas y menos renglones.

Su contraparte, long quiere decir largo y hace referencia a que el tibble resultante tendrá más renglones y menos columnas.

Funciones complementarias

Una vez que se establecieron las funciones base del manejo de datos, veremos otras funciones que suelen utilizarse en conjunto con las funciones básicas, que agregan funcionalidades o hacen el proceso más sencillo.

%in% y !

Supongamos que se quieren conservar los diamantes cuya clarity correspone a alguno de estos valores:

  • I1
  • S1
  • VS1
  • VVS1

Una forma (muy poco práctica) de resolverlo sería la siguiente:

diamonds %>% 
  filter(clarity == "I1" | clarity == "S1"  | clarity == "VS1"| clarity == "VVS1" )
## # A tibble: 12,567 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  2  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  3  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
##  4  0.23 Ideal     J     VS1      62.8    56   340  3.93  3.9   2.46
##  5  0.32 Premium   E     I1       60.9    58   345  4.38  4.42  2.68
##  6  0.23 Very Good H     VS1      61      57   353  3.94  3.96  2.41
##  7  0.24 Premium   I     VS1      62.5    57   355  3.97  3.94  2.47
##  8  0.23 Very Good F     VS1      60.9    57   357  3.96  3.99  2.42
##  9  0.23 Very Good F     VS1      60      57   402  4     4.03  2.41
## 10  0.23 Very Good F     VS1      59.8    57   402  4.04  4.06  2.42
## # ... with 12,557 more rows

Sin embargo, escribir tanto resulta no ser práctico.

Una forma más práctica es utilizando el operador %in%.

El primer paso es asignar los valores que sí queremos dentro de un vector:

clarities <- c("I1", "S1", "VS1", "VVS1")

clarities
## [1] "I1"   "S1"   "VS1"  "VVS1"

Y después:

diamonds %>% 
  filter(clarity %in% clarities)
## # A tibble: 12,567 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  2  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  3  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
##  4  0.23 Ideal     J     VS1      62.8    56   340  3.93  3.9   2.46
##  5  0.32 Premium   E     I1       60.9    58   345  4.38  4.42  2.68
##  6  0.23 Very Good H     VS1      61      57   353  3.94  3.96  2.41
##  7  0.24 Premium   I     VS1      62.5    57   355  3.97  3.94  2.47
##  8  0.23 Very Good F     VS1      60.9    57   357  3.96  3.99  2.42
##  9  0.23 Very Good F     VS1      60      57   402  4     4.03  2.41
## 10  0.23 Very Good F     VS1      59.8    57   402  4.04  4.06  2.42
## # ... with 12,557 more rows

Se lee cómo: De diamonds, conserva los renglones donde clarity está en el vector clarities.

Vemos que de esta manera el código utilizado es más compacto, más claro y más práctico. En esta ocasión, el vector clarities fue definido a mano porque eran pocos valores, sin embargo, en otras ocasiones ese vector puede tener muchísimos más valores y provenir de otro dataframe. Por ejemplo, es útil para ver qué ids de cierta base de datos están en otra.

Otra operación muy común es la negación de una expresión. Por ejemplo, si queremos obtener los diamantes cuya claridad NO pertenece al vector clarities, podemos utilizar el operador ! de la siguiente manera:

diamonds %>% 
  filter(!(clarity %in% clarities))
## # A tibble: 41,373 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  4  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  5  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
##  6  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  7  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
##  8  0.3  Good      J     SI1      64      55   339  4.25  4.28  2.73
##  9  0.22 Premium   F     SI1      60.4    61   342  3.88  3.84  2.33
## 10  0.31 Ideal     J     SI2      62.2    54   344  4.35  4.37  2.71
## # ... with 41,363 more rows
diamonds %>% 
  filter(!(clarity %in% clarities)) %>% 
  distinct(clarity)
## # A tibble: 5 x 1
##   clarity
##   <ord>  
## 1 SI2    
## 2 SI1    
## 3 VS2    
## 4 VVS2   
## 5 IF

Observa que se pone al principio de la expresión, y se lee como:

De diamonds, conserva los renglones donde clarity NO está en el vector clarities.

La negación se puede utilizar con otros operadores como |, & o ==.

Por ejemplo:

Tomamos los diamantes cuyo color es E y clarity es VS1:

diamonds %>% 
  filter(color == "E" & clarity == "VS1")
## # A tibble: 1,281 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  2  0.23 Very Good E     VS1      60.7    59   402  3.97  4.01  2.42
##  3  0.23 Very Good E     VS1      59.5    58   402  4.01  4.06  2.4 
##  4  0.23 Good      E     VS1      64.1    59   402  3.83  3.85  2.46
##  5  0.6  Ideal     E     VS1      61.7    55  2774  5.41  5.44  3.35
##  6  0.7  Good      E     VS1      57.2    62  2782  5.81  5.77  3.31
##  7  0.7  Premium   E     VS1      62.9    60  2782  5.62  5.54  3.51
##  8  0.7  Premium   E     VS1      62.2    58  2800  5.6   5.66  3.5 
##  9  0.29 Very Good E     VS1      61.9    55   555  4.28  4.33  2.66
## 10  0.29 Very Good E     VS1      62.4    55   555  4.2   4.25  2.63
## # ... with 1,271 more rows

Si queremos tomar el complemento del conjunto anterior, es decir, negar dicha expresión:

diamonds %>% 
  filter(!(color == "E" & clarity == "VS1"))
## # A tibble: 52,659 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  4  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  5  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
##  6  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  7  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  8  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
##  9  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
## 10  0.3  Good      J     SI1      64      55   339  4.25  4.28  2.73
## # ... with 52,649 more rows

Observa que fue necesario el uso de paréntesis después de !, para negar toda la expresión.

Una forma equivalente y sin utilizar paréntesis es:

diamonds %>% 
  filter(color != "E" | clarity != "VS1")
## # A tibble: 52,659 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  4  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  5  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
##  6  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  7  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  8  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
##  9  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
## 10  0.3  Good      J     SI1      64      55   339  4.25  4.28  2.73
## # ... with 52,649 more rows

Podemos corroborar que es el mismo resultado por el número de renglones.

Observa ahora que para negar la expresión fue necesario cambiar cada == por != y sustituir el & por |.

Esto sucede por las Leyes de DeMorgan. No es necesario que las aprendas todas, sin embargo es importante entender que al utilizar operadores lógicos (&, |, !, ==) es necesario tener cuidado y verificar que el resultado obtenido es el que esperábamos.

Las operaciones lógicas no suelen ser intuitivas, y en ocasiones lo que R hace y lo que queremos que haga pueden ser cosas muy distintas.

ifelse() y case_when()

Supongamos ahora que se quiere crear una clasificación de cut más sencilla, en la cuál sólo se tengan 2 valores:

  • top_quality si cut es Premium o Ideal
  • good_quality si cut es Fair, Good o Very Good.

Para esto es útil la función ifelse(), utilizada dentro de un mutate():

diamonds %>% 
  mutate(clasif_cut = ifelse(test = cut %in% c("Premium", "Ideal"), 
                              yes = "top_quality",
                              no = "good_quality")) %>% 
  select(cut, clasif_cut, everything())
## # A tibble: 53,940 x 11
##    cut       clasif_cut  carat color clarity depth table price     x     y     z
##    <ord>     <chr>       <dbl> <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1 Ideal     top_quality  0.23 E     SI2      61.5    55   326  3.95  3.98  2.43
##  2 Premium   top_quality  0.21 E     SI1      59.8    61   326  3.89  3.84  2.31
##  3 Good      good_quali~  0.23 E     VS1      56.9    65   327  4.05  4.07  2.31
##  4 Premium   top_quality  0.29 I     VS2      62.4    58   334  4.2   4.23  2.63
##  5 Good      good_quali~  0.31 J     SI2      63.3    58   335  4.34  4.35  2.75
##  6 Very Good good_quali~  0.24 J     VVS2     62.8    57   336  3.94  3.96  2.48
##  7 Very Good good_quali~  0.24 I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8 Very Good good_quali~  0.26 H     SI1      61.9    55   337  4.07  4.11  2.53
##  9 Fair      good_quali~  0.22 E     VS2      65.1    61   337  3.87  3.78  2.49
## 10 Very Good good_quali~  0.23 H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Como se observa en el ejemplo, la función if_else() tiene 3 argumentos:

  • test: La condición a evaluar
  • yes: El valor que se aplicará si la condición es cierta
  • no: El valor que se aplicará si la condición es falsa

¿Qué pasa si se desea clasificar en 3 o más categorías?

Supongamos que clasificaremos como:

  • top_quality: si cut es Premium o Ideal
  • good_quality: si cut es Very Good o Good
  • fair_quality: si cutes Fair

Una opción es anidar varias funciones ifelse(). Esto quiere decir que en el lugar del argumento no se establece una nueva condición.

Si alguna vez has realizado esta operación en Excel, reconocerás la semejanza.

diamonds %>% 
  mutate(clasif_cut = ifelse(test = cut %in% c("Premium", "Ideal"), 
                              yes = "top_quality",
                              no = ifelse(test = cut %in% c("Very Good", "Good"), 
                                              yes = "good_quality",
                                              no = "fair_quality"))) %>% 
  select(cut, clasif_cut, everything())
## # A tibble: 53,940 x 11
##    cut       clasif_cut  carat color clarity depth table price     x     y     z
##    <ord>     <chr>       <dbl> <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1 Ideal     top_quality  0.23 E     SI2      61.5    55   326  3.95  3.98  2.43
##  2 Premium   top_quality  0.21 E     SI1      59.8    61   326  3.89  3.84  2.31
##  3 Good      good_quali~  0.23 E     VS1      56.9    65   327  4.05  4.07  2.31
##  4 Premium   top_quality  0.29 I     VS2      62.4    58   334  4.2   4.23  2.63
##  5 Good      good_quali~  0.31 J     SI2      63.3    58   335  4.34  4.35  2.75
##  6 Very Good good_quali~  0.24 J     VVS2     62.8    57   336  3.94  3.96  2.48
##  7 Very Good good_quali~  0.24 I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8 Very Good good_quali~  0.26 H     SI1      61.9    55   337  4.07  4.11  2.53
##  9 Fair      fair_quali~  0.22 E     VS2      65.1    61   337  3.87  3.78  2.49
## 10 Very Good good_quali~  0.23 H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Podemos omitir el nombre de los argumentos para que sea un poco más compacta la sintaxis.

diamonds %>% 
  mutate(clasif_cut = ifelse(cut %in% c("Premium", "Ideal"), 
                              "top_quality",
                              ifelse(cut %in% c("Very Good", "Good"), 
                                      "good_quality",
                                      "fair_quality"))) %>% 
  select(cut, clasif_cut, everything())
## # A tibble: 53,940 x 11
##    cut       clasif_cut  carat color clarity depth table price     x     y     z
##    <ord>     <chr>       <dbl> <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1 Ideal     top_quality  0.23 E     SI2      61.5    55   326  3.95  3.98  2.43
##  2 Premium   top_quality  0.21 E     SI1      59.8    61   326  3.89  3.84  2.31
##  3 Good      good_quali~  0.23 E     VS1      56.9    65   327  4.05  4.07  2.31
##  4 Premium   top_quality  0.29 I     VS2      62.4    58   334  4.2   4.23  2.63
##  5 Good      good_quali~  0.31 J     SI2      63.3    58   335  4.34  4.35  2.75
##  6 Very Good good_quali~  0.24 J     VVS2     62.8    57   336  3.94  3.96  2.48
##  7 Very Good good_quali~  0.24 I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8 Very Good good_quali~  0.26 H     SI1      61.9    55   337  4.07  4.11  2.53
##  9 Fair      fair_quali~  0.22 E     VS2      65.1    61   337  3.87  3.78  2.49
## 10 Very Good good_quali~  0.23 H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Es fácil imaginar cómo la longitud del código aumenta y la facilidad de lectura disminuye al incrementar el número de categorías en las que se busca clasificar. Para evitar esto, utilizamos case_when(), una alternativa que hace el código más fácil de entender y mucho más compacto.

Realicemos el mismo ejemplo anterior pero ahora utilizando case_when().

diamonds %>% 
  mutate(clasif_cut = case_when(
    cut %in% c("Premium", "Ideal") ~ "top_quality",
    cut %in% c("Very Good", "Good") ~ "good_quality",
    cut == "Fair" ~ "fair_quality")) %>% 
  select(cut, clasif_cut, everything())
## # A tibble: 53,940 x 11
##    cut       clasif_cut  carat color clarity depth table price     x     y     z
##    <ord>     <chr>       <dbl> <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1 Ideal     top_quality  0.23 E     SI2      61.5    55   326  3.95  3.98  2.43
##  2 Premium   top_quality  0.21 E     SI1      59.8    61   326  3.89  3.84  2.31
##  3 Good      good_quali~  0.23 E     VS1      56.9    65   327  4.05  4.07  2.31
##  4 Premium   top_quality  0.29 I     VS2      62.4    58   334  4.2   4.23  2.63
##  5 Good      good_quali~  0.31 J     SI2      63.3    58   335  4.34  4.35  2.75
##  6 Very Good good_quali~  0.24 J     VVS2     62.8    57   336  3.94  3.96  2.48
##  7 Very Good good_quali~  0.24 I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8 Very Good good_quali~  0.26 H     SI1      61.9    55   337  4.07  4.11  2.53
##  9 Fair      fair_quali~  0.22 E     VS2      65.1    61   337  3.87  3.78  2.49
## 10 Very Good good_quali~  0.23 H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Aquí la sintaxis es más clara. Basta con definir la condición de lado izquierdo, el valor a aplicar en caso de que dicha condición sea cierta de lado derecho y separar ambas por medio de una tilde ~.

De esta manera podemos agregar cuantas condiciones sean necesarias sin aumentar la complejidad de lectura.

Viendo sólo el código, ¿puedes deducir qué hace la siguiente operación?:

diamonds %>% 
  mutate(ppk = price/carat) %>% 
  mutate(clasif_ppk = case_when(
    ppk <= 2500 ~ "bara_bara",
    ppk <= 5000 ~ "baraton",
    ppk <= 7500 ~ "normalito",
    ppk <= 10000 ~ "esta_carito",
    ppk <= 12500 ~ "pues_que_hace_o_que?",
    TRUE ~ "vendere_un_riñon"
  ))

Nota que las operaciones son evaluadas en orden, por esto no es necesario poner intervalos. Por ejemplo, los diamantes baratones son aquellos que tienen un precio por kilate entre $2501 y $5000, pero como se etiquetó primero a los diamantes cuyo ppk es menor o igual a $2500, no es necesario establecer el límite inferior de este nivel de precio, y lo mismo sucede en cada nivel de precio consecutivo.

OJO: ¿Qué hace la última línea?

    TRUE ~ "vendere_un_riñon"

left_join()

En ocasiones es necesario juntar dos tablas que comparten una o más columnas. Por ejemplo, se tiene un dataframe con las calificaciones que le dió un usuario a ciertos productos:

evaluaciones
## # A tibble: 434 x 5
##    folio prod1 prod2 prod3 prod4
##    <int> <int> <int> <int> <int>
##  1     1     3     3     6     5
##  2     2     5     5     4     8
##  3     3     6     7     6     4
##  4     4     5     5     4     5
##  5     5     4     8     7     6
##  6     6     3     5     6     8
##  7     7     5     8     5     6
##  8     8     5     8     4     6
##  9     9     5     4     7     6
## 10    10     4     7     6     7
## # ... with 424 more rows

Por otro lado, se tiene la información socioeconómica y demográfica de cada usuario:

demograficos
## # A tibble: 434 x 4
##    folio   NSE ciudad  sexo
##    <int> <dbl>  <dbl> <dbl>
##  1     1     5      3     2
##  2     2     5      4     2
##  3     3     4      1     2
##  4     4     4      4     1
##  5     5     2      2     1
##  6     6     5      2     2
##  7     7     2      1     1
##  8     8     1      4     1
##  9     9     3      1     1
## 10    10     5      3     1
## # ... with 424 more rows

Si quisiéramos analizar las evaluaciones de producto por nivel socioeconómico necesitaríamos unir o pegar ambas tablas. Para esto utilizamos left_join().

evaluaciones %>% 
  left_join(demograficos, by = "folio")
## # A tibble: 434 x 8
##    folio prod1 prod2 prod3 prod4   NSE ciudad  sexo
##    <int> <int> <int> <int> <int> <dbl>  <dbl> <dbl>
##  1     1     3     3     6     5     5      3     2
##  2     2     5     5     4     8     5      4     2
##  3     3     6     7     6     4     4      1     2
##  4     4     5     5     4     5     4      4     1
##  5     5     4     8     7     6     2      2     1
##  6     6     3     5     6     8     5      2     2
##  7     7     5     8     5     6     2      1     1
##  8     8     5     8     4     6     1      4     1
##  9     9     5     4     7     6     3      1     1
## 10    10     4     7     6     7     5      3     1
## # ... with 424 more rows

De esta manera tenemos todas las columnas en una misma tabla.

Observa el argumento by. En este argumento se especifica la(s) columna(s) que tienen ambos dataframes en común, y se usará para hacer coincidir todos los datos.

Observa que las variables demográficas tienen valores numéricos, lo cual puede no ser muy útil. Podemos pegar las etiquetas reales con left_join y la ayuda de un catálogo.

cat_nse
## # A tibble: 5 x 2
##     NSE nse_lbl
##   <int> <chr>  
## 1     1 A/B    
## 2     2 C+     
## 3     3 C      
## 4     4 C-     
## 5     5 D+/D
demograficos %>% 
  left_join(cat_nse, by = "NSE")
## # A tibble: 434 x 5
##    folio   NSE ciudad  sexo nse_lbl
##    <int> <dbl>  <dbl> <dbl> <chr>  
##  1     1     5      3     2 D+/D   
##  2     2     5      4     2 D+/D   
##  3     3     4      1     2 C-     
##  4     4     4      4     1 C-     
##  5     5     2      2     1 C+     
##  6     6     5      2     2 D+/D   
##  7     7     2      1     1 C+     
##  8     8     1      4     1 A/B    
##  9     9     3      1     1 C      
## 10    10     5      3     1 D+/D   
## # ... with 424 more rows

Observa que las dimensiones no tienen que coincidir. En el primer caso el número de renglones era el mismo en ambos dataframes. En este ejemplo, uno de los dataframes tenía sólo 5 renglones, uno por cada nivel de NSE. La función left_join hace coincidir los valores correspondientes, repitiendo los valores las veces que sea necesario.

Si alguna vez has realizado esta tarea en Excel, notarás que es similar a usar VLOOKUP o BUSCARV.

Podemos hacer lo mismo con ciudad y sexo, asumiendo que tengamos los respectivos catálogos.

cat_ciudad
## # A tibble: 4 x 2
##   ciudad ciudad_lbl 
##    <int> <chr>      
## 1      1 CDMX       
## 2      2 Guadalajara
## 3      3 Monterrey  
## 4      4 Queretaro
cat_sexo
## # A tibble: 2 x 2
##    sexo sexo_lbl
##   <int> <chr>   
## 1     1 Mujer   
## 2     2 Hombre
demograficos %>% 
  left_join(cat_nse, by = "NSE") %>% 
  left_join(cat_ciudad, by = "ciudad") %>% 
  left_join(cat_sexo, by = "sexo")
## # A tibble: 434 x 7
##    folio   NSE ciudad  sexo nse_lbl ciudad_lbl  sexo_lbl
##    <int> <dbl>  <dbl> <dbl> <chr>   <chr>       <chr>   
##  1     1     5      3     2 D+/D    Monterrey   Hombre  
##  2     2     5      4     2 D+/D    Queretaro   Hombre  
##  3     3     4      1     2 C-      CDMX        Hombre  
##  4     4     4      4     1 C-      Queretaro   Mujer   
##  5     5     2      2     1 C+      Guadalajara Mujer   
##  6     6     5      2     2 D+/D    Guadalajara Hombre  
##  7     7     2      1     1 C+      CDMX        Mujer   
##  8     8     1      4     1 A/B     Queretaro   Mujer   
##  9     9     3      1     1 C       CDMX        Mujer   
## 10    10     5      3     1 D+/D    Monterrey   Mujer   
## # ... with 424 more rows

Observa que se pueden encadenar varios left_joins por medio del %>%.

Una vez que se tienen las etiquetas para cada valor, se pueden eliminar las columnas numéricas, y unir este nuevo dataframe al de las evaluaciones de producto.

demograficos <-  demograficos %>% 
  left_join(cat_nse, by = "NSE") %>% 
  left_join(cat_ciudad, by = "ciudad") %>% 
  left_join(cat_sexo, by = "sexo") %>% 
  select(folio, nse = nse_lbl, ciudad = ciudad_lbl, sexo = sexo_lbl)

demograficos
## # A tibble: 434 x 4
##    folio nse   ciudad      sexo  
##    <int> <chr> <chr>       <chr> 
##  1     1 D+/D  Monterrey   Hombre
##  2     2 D+/D  Queretaro   Hombre
##  3     3 C-    CDMX        Hombre
##  4     4 C-    Queretaro   Mujer 
##  5     5 C+    Guadalajara Mujer 
##  6     6 D+/D  Guadalajara Hombre
##  7     7 C+    CDMX        Mujer 
##  8     8 A/B   Queretaro   Mujer 
##  9     9 C     CDMX        Mujer 
## 10    10 D+/D  Monterrey   Mujer 
## # ... with 424 more rows
evaluaciones %>% 
  left_join(demograficos)
## Joining, by = "folio"
## # A tibble: 434 x 8
##    folio prod1 prod2 prod3 prod4 nse   ciudad      sexo  
##    <int> <int> <int> <int> <int> <chr> <chr>       <chr> 
##  1     1     3     3     6     5 D+/D  Monterrey   Hombre
##  2     2     5     5     4     8 D+/D  Queretaro   Hombre
##  3     3     6     7     6     4 C-    CDMX        Hombre
##  4     4     5     5     4     5 C-    Queretaro   Mujer 
##  5     5     4     8     7     6 C+    Guadalajara Mujer 
##  6     6     3     5     6     8 D+/D  Guadalajara Hombre
##  7     7     5     8     5     6 C+    CDMX        Mujer 
##  8     8     5     8     4     6 A/B   Queretaro   Mujer 
##  9     9     5     4     7     6 C     CDMX        Mujer 
## 10    10     4     7     6     7 D+/D  Monterrey   Mujer 
## # ... with 424 more rows

Observa, por último, que si no se especifica la variable por medio de by =, se muestra un warning que avisa qué variable está utilizando para hacer el join. Por default R utiliza las columnas que tengan el mismo nombre, lo cual puede o no ser el comportamiento esperado. Siempre es recomendable especificar el argumento by para evitar ambigüedad.

Otro punto muy importante para poder realizar un join es que los tipos de dato de la columna en ambos dataframes debe coincidir. Por ejemplo, no se puede hacer un join cuando en un dataframe folio es numérico y en el otro es texto.

Importar y exportar datos.

Como se mencionó en la sección anterior, es muy común trabajar con datos externos, escritos por otros programas. R tiene diversos paquetes y funciones para trabajar con datos en otros formatos.

En general, todos los tipos de archivo siguen la misma lógica:

Una función para importar, en general con prefijo read, y una función para exportar, generalmente con prefijo write o save.

Para los siguientes ejemplos, utilizaremos archivos almacenados en una carpeta llamada data. Dicha carpeta la puedes descargar desde aquí (asegurate de haber iniciado sesión con tu cuenta de LasQuinceLetras).

En caso de que no funcione el link, la ubicación desde tu Google Drive de la empresa es Vox Populi > Cursos LQL > marketing leaRning.

Descarga, descomprime y almacena la carpeta dentro del directorio desde donde estás trabajando. Asegurate de estar trabajando dentro de un proyecto de R.

CSV

Los datos en formato csv, o comma separated values, son muy comunes al compartir datos, ya que son fácilmente importados por diversos programas.

Para leer un csv, basta con utilizar:

dta <- read_csv('data/mtcars.csv')
## 
## -- Column specification --------------------------------------------------------
## cols(
##   mpg = col_double(),
##   cyl = col_double(),
##   disp = col_double(),
##   hp = col_double(),
##   drat = col_double(),
##   wt = col_double(),
##   qsec = col_double(),
##   vs = col_double(),
##   am = col_double(),
##   gear = col_double(),
##   carb = col_double()
## )
dta
## # A tibble: 32 x 11
##      mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
##    <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
##  1  21       6  160    110  3.9   2.62  16.5     0     1     4     4
##  2  21       6  160    110  3.9   2.88  17.0     0     1     4     4
##  3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1
##  4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1
##  5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2
##  6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1
##  7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4
##  8  24.4     4  147.    62  3.69  3.19  20       1     0     4     2
##  9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4     2
## 10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4
## # ... with 22 more rows

Para exportar una tabla que tengamos directo a un archivo csv se utiliza:

diamonds %>% 
  write_csv('data/diamonds.csv')

Verifica que se ejecutó correctamente buscando manualmente el archivo en tu directorio data/.

Excel

Para leer desde excel utilizamos el paquete openxlsx. Si no lo tienes instalado, puedes instalarlo con:

install.packages('openxlsx', dependencies = TRUE)
library(openxlsx)
read.xlsx('data/CLIENTES.xlsx') %>% 
  as_tibble()
## # A tibble: 81 x 2
##    Nombre.de.cliente Abreviatura
##    <chr>             <chr>      
##  1 "Ach Food"        ACH        
##  2 "Alpura"          ALP        
##  3 "AT&T"            ATT        
##  4 "AT KERNEY "      ATK        
##  5 "Bayer"           BAY        
##  6 "Banco Azteca"    BAN        
##  7 "Be Grand "       BEG        
##  8 "BIG FOOT"        BIG        
##  9 "Bioderma "       BIO        
## 10 "CAMPBELLS"       CAM        
## # ... with 71 more rows

Exportar una tabla de datos a un archivo de Excel es un poco más complejo que hacerlo con un csv. Más adelante veremos en detalle cómo trabajar en archivos de Excel desde R. Un ejemplo muy sencillo es el siguiente:

wb <- createWorkbook()

wb %>% 
  addWorksheet('datos diamantes')

wb %>% 
  writeData(sheet = 'datos diamantes', x = diamonds)

wb %>% 
  saveWorkbook('data/diamonds.xlsx', overwrite = T)

Primero se crea un objeto Workbook en blanco con la función createWorkbook(). Esto crea todo un libro de Excel. Posteriormente se crea una hoja con addWorksheet, en este caso la llamamos diamonds. En esta hoja que creamos, escribimos los datos que queremos exportar con writeData(), en este caso utilizaremos el tibble diamonds con el que hemos estado trabajando. Por último, con saveWorkbook()guardamos en la carpeta de nuestra preferencia el archivo creado, en este caso será data/.

Portapapeles

Una manera mucho más sencilla de exportar un archivo a Excel, en lugar de crear dicho archivo desde R, es copiándolo en tu portapapeles, y pegándolo directo en un nuevo archivo de Excel.

Necesitarás el paquete clipr.

install.packages('clipr')
library(clipr)
## Welcome to clipr. See ?write_clip for advisories on writing to the clipboard in R.

Corre el siguiente comando en tu consola y posteriormente abre un archivo nuevo de Excel y presiona ctrl v o bien, click derecho > pegar:

diamonds %>% 
  write_clip()

SPSS o SAV o STATA o SAS

Para leer archivos creados por programas estadísticos y de análisis de datos, como pueden ser SPSS, STATA, SAS, etc. utilizaremos el paquete haven.

install.packages('haven')
library(haven)

Para este ejemplo contamos con una base en formato sav, para esto utilizamos la función read_sav(). En haven existen funciones read_ para varios tipos de archivo, los cuales puedes explorar escribiendo haven:: en tu consola y navegando con las flechas de tu teclado.

dta_sav <- read_sav('data/Vuala.sav')

dta_sav
## # A tibble: 360 x 105
##    REGISTRO  CIUDAD1    PERFIL       F1    F2 RANGO_EDAD      F3        F4  F5_1
##       <dbl> <dbl+lb> <dbl+lbl> <dbl+lb> <dbl>  <dbl+lbl> <dbl+l> <dbl+lbl> <dbl>
##  1       19 1 [CDMX] 3 [Mamá]  2 [Muje~    31 3 [6 a 12]  1 [Sí]  2 [2 hi~    11
##  2       20 1 [CDMX] 1 [Niño]  2 [Muje~    16 1 [16 a 1~ NA      NA           NA
##  3       21 1 [CDMX] 2 [Jóven~ 1 [Homb~    20 2 [20 a 2~ NA      NA           NA
##  4       22 1 [CDMX] 1 [Niño]  1 [Homb~    19 1 [16 a 1~ NA      NA           NA
##  5       23 1 [CDMX] 3 [Mamá]  2 [Muje~    43 3 [6 a 12]  1 [Sí]  3 [3 hi~    18
##  6       24 1 [CDMX] 3 [Mamá]  2 [Muje~    36 3 [6 a 12]  1 [Sí]  3 [3 hi~    15
##  7       25 1 [CDMX] 3 [Mamá]  2 [Muje~    38 3 [6 a 12]  1 [Sí]  2 [2 hi~    14
##  8       26 1 [CDMX] 1 [Niño]  1 [Homb~    17 1 [16 a 1~ NA      NA           NA
##  9       27 1 [CDMX] 2 [Jóven~ 1 [Homb~    23 2 [20 a 2~ NA      NA           NA
## 10       28 1 [CDMX] 3 [Mamá]  2 [Muje~    45 3 [6 a 12]  1 [Sí]  3 [3 hi~    18
## # ... with 350 more rows, and 96 more variables: F5_2 <dbl>, F5_3 <dbl>,
## #   CUOTA_NSE <dbl+lbl>, CUOTA_NSE_FINAL <dbl+lbl>, F6_1 <dbl+lbl>,
## #   F6_2 <dbl+lbl>, F6_3 <dbl+lbl>, F6_4 <dbl+lbl>, F6_5 <dbl+lbl>,
## #   F6_6 <dbl+lbl>, F6_7 <dbl+lbl>, F6_8 <dbl+lbl>, F7_1 <dbl+lbl>,
## #   F7_2 <dbl+lbl>, F7_3 <dbl+lbl>, F7_4 <dbl+lbl>, F7_5 <dbl+lbl>,
## #   F7_6 <dbl+lbl>, F7_7 <dbl+lbl>, F7_8 <dbl+lbl>, F7_9 <dbl+lbl>,
## #   F7_10 <dbl+lbl>, F7_11 <dbl+lbl>, RANGO_CUOTA <dbl+lbl>, F8_1 <dbl+lbl>,
## #   F8_2 <dbl+lbl>, F8_3 <dbl+lbl>, F8_4 <dbl+lbl>, F8_5 <dbl+lbl>,
## #   F8_6 <dbl+lbl>, F8_7 <dbl+lbl>, F8_8 <dbl+lbl>, F8_9 <dbl+lbl>,
## #   F8_10 <dbl+lbl>, F8_11 <dbl+lbl>, TIPO_CONSUMIDOR <dbl+lbl>, V1 <dbl+lbl>,
## #   V2 <dbl+lbl>, V3_1 <dbl+lbl>, V3_2 <dbl+lbl>, V3_3 <dbl+lbl>,
## #   V3_4 <dbl+lbl>, V4 <dbl+lbl>, V5_1 <dbl+lbl>, V5_2 <dbl+lbl>,
## #   V5_3 <dbl+lbl>, V5_4 <dbl+lbl>, V5_5 <dbl+lbl>, V5_6 <dbl+lbl>,
## #   V5_7 <dbl+lbl>, V6 <dbl+lbl>, V7_1 <dbl+lbl>, V7_2 <dbl+lbl>,
## #   V7_3 <dbl+lbl>, PRUEBA <dbl+lbl>, EVAL01 <dbl+lbl>, EVAL02 <dbl+lbl>,
## #   P1 <dbl+lbl>, P2 <dbl+lbl>, P3 <dbl+lbl>, P4 <dbl+lbl>, P5 <dbl+lbl>,
## #   P6 <dbl+lbl>, P7 <dbl+lbl>, P8 <dbl+lbl>, P9 <dbl+lbl>, P10 <dbl+lbl>,
## #   P11 <dbl+lbl>, P12 <dbl+lbl>, EVAL01_1 <dbl+lbl>, P13 <dbl+lbl>,
## #   P14 <dbl+lbl>, P15 <dbl+lbl>, P16 <dbl+lbl>, P17 <dbl+lbl>, P18 <dbl+lbl>,
## #   P19 <dbl+lbl>, P20 <dbl+lbl>, P21_1 <dbl+lbl>, P21_2 <dbl+lbl>,
## #   P21_3 <dbl+lbl>, P21_4 <dbl+lbl>, P22 <dbl+lbl>, P23_1 <dbl+lbl>,
## #   P23_2 <dbl+lbl>, P23_3 <dbl+lbl>, P23_4 <dbl+lbl>, P23_5 <dbl+lbl>,
## #   P23_6 <dbl+lbl>, P23_7 <dbl+lbl>, P24 <dbl+lbl>, P25_1 <dbl+lbl>,
## #   P25_2 <dbl+lbl>, P25_3 <dbl+lbl>, P26 <dbl+lbl>, P27 <dbl+lbl>

Si bien existen las funciones write_ para exportar archivos, no es recomendable utilizarlas, al menos de ser estrictamente necesario. Cualquier tipo de análisis que se vaya a realizar en SPSS o STATA, puede realizarse en R.

R

En ocasiones será útil guardar objetos generados por R (dataframes, listas, valores, modelos) para leerse en algún punto en el futuro, o bien, para ser compartidos para leerse por otro usuario de R.

Para exportar objetos creados para R, que sólo serán leídos en R, existe el tipo de datos rds, el cuál permite almacenar datos de una manera muy compacta (ocupa menos KBs que un CSV)

El uso más común es cuando se importa un conjunto de datos, generalmente de alguna fuente vista anteriormente, y después de realizar limpieza y preprocesamiento, se desea guardar el dataframe limpio para realizar el análisis posteriormente. De esta manera, no es necesario realizar la limpieza cada vez que se quiera acceder al dataframe.

Utilizamos la función saveRDS() para exportar un tibble:

diamonds %>% 
  saveRDS('data/diamonds.rds')

Para leerlo utilizamos readRDS():

diamonds_rds <- readRDS('data/diamonds.rds')
diamonds_rds
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  6  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
##  7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
## 10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Manejar datos faltantes

Variables categóricas

Al importar datos, es muy común que existan datos faltantes en algunos renglones de algunas columnas. De hecho, conjuntos de datos sin un solo dato faltante, son la excepción y no la regla.

En R, a los datos faltantes se les denota con NA. Es importante saberlos identificar y qué hacer con ellos, por las razones que veremos a continuación.

Tomemos el tibble diamonds, pero que ha sido modificado de manera que algunos diamantes tienen color faltante:

diamonds_na_color
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <chr> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  6  0.24 Very Good <NA>  VVS2     62.8    57   336  3.94  3.96  2.48
##  7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  9  0.22 Fair      <NA>  VS2      65.1    61   337  3.87  3.78  2.49
## 10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Nos interesa ver cuántos son y en dónde están todos los faltantes. Para esto, existe la función auxiliar is.na() que puede utilizarse dentro de un filter().

diamonds_na_color %>% 
  filter(is.na(color))
## # A tibble: 9,754 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <chr> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.24 Very Good <NA>  VVS2     62.8    57   336  3.94  3.96  2.48
##  2  0.22 Fair      <NA>  VS2      65.1    61   337  3.87  3.78  2.49
##  3  0.23 Ideal     <NA>  VS1      62.8    56   340  3.93  3.9   2.46
##  4  0.32 Premium   <NA>  I1       60.9    58   345  4.38  4.42  2.68
##  5  0.23 Very Good <NA>  VS2      63.8    55   352  3.85  3.92  2.48
##  6  0.23 Very Good <NA>  VS2      60.5    61   357  3.96  3.97  2.4 
##  7  0.23 Very Good <NA>  VS1      60.9    57   357  3.96  3.99  2.42
##  8  0.31 Good      <NA>  SI1      64      54   402  4.29  4.31  2.75
##  9  0.33 Ideal     <NA>  SI1      61.1    56   403  4.49  4.55  2.76
## 10  0.3  Very Good <NA>  SI1      62.6    57   405  4.25  4.28  2.67
## # ... with 9,744 more rows

Vemos que son 9,754 renglones en los que falta color.

Intentemos calcular el precio promedio agrupando por color:

diamonds_na_color %>% 
  group_by(color) %>% 
  summarise(mean_price =  mean(price))
## # A tibble: 8 x 2
##   color mean_price
##   <chr>      <dbl>
## 1 D          3149.
## 2 E          3089.
## 3 F          3740.
## 4 G          4031.
## 5 H          4451.
## 6 I          5052.
## 7 J          5381.
## 8 <NA>       3922.

Observa que ahora NA es una categoría nueva. Esto puede o no ser útil para nuestro análisis. Por ahora, la mejor opción es quitarlos:

diamonds_na_color %>% 
  filter(!is.na(color))
## # A tibble: 44,186 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <chr> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  6  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  7  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  8  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
##  9  0.3  Good      J     SI1      64      55   339  4.25  4.28  2.73
## 10  0.22 Premium   F     SI1      60.4    61   342  3.88  3.84  2.33
## # ... with 44,176 more rows

Observa el signo ! de negación. Lo que estamos diciendo es: Del tibble diamonds_na_color, conserva los renglones en los cuales color NO es NA. Es importante tener en mente que estamos perdiendo información. Si bien es un manera de resolver el problema, puede o no ser la más adecuada al contexto del problema.

Variables numéricas

Tomemos ahora el tibble diamonds, pero que ha sido modificado de manera que algunos diamantes tienen precio faltante:

diamonds_na_price
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  6  0.24 Very Good J     VVS2     62.8    57    NA  3.94  3.96  2.48
##  7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
## 10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
## # ... with 53,930 more rows

Podemos observar que el 6to de los primeros 10 no tiene precio.

diamonds_na_price %>% 
  filter(is.na(price))
## # A tibble: 5,254 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.24 Very Good J     VVS2     62.8    57    NA  3.94  3.96  2.48
##  2  0.3  Good      J     SI1      64      55    NA  4.25  4.28  2.73
##  3  0.3  Very Good J     SI1      62.7    59    NA  4.21  4.27  2.66
##  4  0.23 Very Good E     VS1      60.7    59    NA  3.97  4.01  2.42
##  5  0.22 Premium   E     VS2      61.6    58    NA  3.93  3.89  2.41
##  6  0.3  Very Good I     SI1      62.6    57    NA  4.25  4.28  2.67
##  7  0.35 Ideal     I     VS1      60.9    57    NA  4.54  4.59  2.78
##  8  0.3  Premium   H     SI1      62.5    57    NA  4.29  4.25  2.67
##  9  0.7  Good      E     VS2      57.5    58    NA  5.85  5.9   3.38
## 10  0.8  Ideal     I     VS1      62.9    56    NA  5.94  5.87  3.72
## # ... with 5,244 more rows

Se interpreta como: Del tibble diamonds_na_price, conserva todos los renglones en los cuales price tiene valor de NA.

Observamos que en total son 5254 diamantes cuyo precio es faltante. Es importante hacer algo al respecto, pues si seguimos adelante ignorando estos datos faltantes, muchas operaciones no pueden ser realizadas. Por ejemplo:

diamonds_na_price %>% 
  group_by(cut) %>% 
  summarise(mean_price = mean(price))
## # A tibble: 5 x 2
##   cut       mean_price
##   <ord>          <dbl>
## 1 Fair              NA
## 2 Good              NA
## 3 Very Good         NA
## 4 Premium           NA
## 5 Ideal             NA

Vemos que al calcular el precio promedio por cut, nos regresa como resultado NA para todos los niveles. Esto tiene sentido, pues estamos pidiendo que calcule el promedio de datos cuyo valor es desconocido. Es evidente que tenemos que solucionar esto antes de continuar con el análisis. La forma más sencilla de hacerlo es simplemente desechando estos renglones, utilizando filter() de nuevo.

diamonds_na_price %>% 
  filter(!is.na(price))
## # A tibble: 48,686 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
##  3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
##  4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
##  5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
##  6  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
##  7  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
##  8  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
##  9  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
## 10  0.23 Ideal     J     VS1      62.8    56   340  3.93  3.9   2.46
## # ... with 48,676 more rows

Otra solución es imputarlos, es decir, sustituir los valores faltantes por algún valor puntual, en este caso, podemos utilizar la medianade precio por kilate ppk de cada categoría de diamantes, según sus niveles de cut, color y clarity, es decir, todas nuestras variables categóricas.

Calculemos la mediana de precio por kilate ppk de los diamantes cuya información está completa, agrupando por cut, color y clarity.

ppk_mediana <- diamonds_na_price %>% 
  filter(!is.na(price)) %>% 
  mutate(ppk = price/carat) %>% 
  group_by(cut, color, clarity) %>% 
  summarise(median_ppk = median(ppk)) %>% 
  ungroup()
## `summarise()` has grouped output by 'cut', 'color'. You can override using the `.groups` argument.
ppk_mediana
## # A tibble: 276 x 4
##    cut   color clarity median_ppk
##    <ord> <ord> <ord>        <dbl>
##  1 Fair  D     I1           3472.
##  2 Fair  D     SI2          3634 
##  3 Fair  D     SI1          4099.
##  4 Fair  D     VS2          4368.
##  5 Fair  D     VS1          4174.
##  6 Fair  D     VVS2         3588.
##  7 Fair  D     VVS1         3584 
##  8 Fair  D     IF           4027.
##  9 Fair  E     I1           1997.
## 10 Fair  E     SI2          3541.
## # ... with 266 more rows

Una vez calculados, nos gustaría pegar estas medianas al tibble original, para saber qué precio le corresponde en caso de que tenga datos faltantes. Para esto es muy útil left_join(), debido a que busca cuándo coinciden cut, color y clarity de una tabla con las columnas respectivas de la otra tabla y pega el valor correspondiente de, en el caso de nuestro ejemplo, median_ppk.

diamonds_na_price %>% 
  left_join(ppk_mediana, by = c("cut", "color", "clarity"))
## # A tibble: 53,940 x 11
##    carat cut       color clarity depth table price     x     y     z median_ppk
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>      <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43      3842.
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31      3185.
##  3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31      3974.
##  4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63      4704.
##  5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75      3828.
##  6  0.24 Very Good J     VVS2     62.8    57    NA  3.94  3.96  2.48      5259.
##  7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47      2771.
##  8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53      4331.
##  9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49      3421.
## 10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39      3611.
## # ... with 53,930 more rows

De esta forma, ya tenemos una columna con la cual rellenar datos faltantes, sólo falta crear la regla de imputación con la formula \(price = median_ppk*carat\). Recuerda que dividimos \(\frac price carat\) para obtener ppk, por lo que debemos multiplicar \(ppk * carat\) para obtener price.

Esta fórmula la escribiremos dentro de ifelse() que a su vez irá dentro de un mutate().

diamonds_imp <- diamonds_na_price %>% 
  left_join(ppk_mediana, by = c("cut", "color", "clarity")) %>% 
  mutate(price_imp = ifelse(is.na(price), median_ppk*carat, price))

diamonds_imp
## # A tibble: 53,940 x 12
##    carat cut       color clarity depth table price     x     y     z median_ppk
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>      <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43      3842.
##  2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31      3185.
##  3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31      3974.
##  4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63      4704.
##  5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75      3828.
##  6  0.24 Very Good J     VVS2     62.8    57    NA  3.94  3.96  2.48      5259.
##  7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47      2771.
##  8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53      4331.
##  9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49      3421.
## 10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39      3611.
## # ... with 53,930 more rows, and 1 more variable: price_imp <dbl>

Por último, sustituimos la columna price con price_imp

diamonds_imp <-  diamonds_imp %>% 
  select(-price, -median_ppk) %>% 
  rename(price = price_imp)

diamonds_imp
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table     x     y     z price
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55  3.95  3.98  2.43  326 
##  2  0.21 Premium   E     SI1      59.8    61  3.89  3.84  2.31  326 
##  3  0.23 Good      E     VS1      56.9    65  4.05  4.07  2.31  327 
##  4  0.29 Premium   I     VS2      62.4    58  4.2   4.23  2.63  334 
##  5  0.31 Good      J     SI2      63.3    58  4.34  4.35  2.75  335 
##  6  0.24 Very Good J     VVS2     62.8    57  3.94  3.96  2.48 1262.
##  7  0.24 Very Good I     VVS1     62.3    57  3.95  3.98  2.47  336 
##  8  0.26 Very Good H     SI1      61.9    55  4.07  4.11  2.53  337 
##  9  0.22 Fair      E     VS2      65.1    61  3.87  3.78  2.49  337 
## 10  0.23 Very Good H     VS1      59.4    61  4     4.05  2.39  338 
## # ... with 53,930 more rows

Vemos que los valores imputados pueden ser distintos a los esperados. Es importante tener en mente las limitaciones de los métodos de imputación.

Otros métods incluyen utilizar fuentes de datos externas que puedan complementar o dar una idea de lo que la información faltante pueda ser.

Aunque la forma más frecuente de manejar datos faltantes es eliminar los renglones donde aparecen, es importante analizarlos primero, y entender por qué se están dando, pues el hecho de que falten datos puede ser información útil por sí misma.

Cambiar tipo de datos de una columna.

Para cambiar el tipo de datos de una columna, utilizamos las funciones auxiliares con prefijo as. dentro de un mutate().

Escribe as. en tu consola y presiona la tecla Tabulación para ver todas estas funciones auxiliares.

Las que se utilizarán más frecuentemente son:

  • as.character()
  • as.integer()
  • as.double()
  • as.Date()
  • factor(): Equivalente a as.factor() pero es más versatil
  • ordered(): Equivalente a as.ordered() pero es más versatil

Por ejemplo, regresando a nuestro dataframe de características demográficas:

demograficos
## # A tibble: 434 x 4
##    folio nse   ciudad      sexo  
##    <int> <chr> <chr>       <chr> 
##  1     1 D+/D  Monterrey   Hombre
##  2     2 D+/D  Queretaro   Hombre
##  3     3 C-    CDMX        Hombre
##  4     4 C-    Queretaro   Mujer 
##  5     5 C+    Guadalajara Mujer 
##  6     6 D+/D  Guadalajara Hombre
##  7     7 C+    CDMX        Mujer 
##  8     8 A/B   Queretaro   Mujer 
##  9     9 C     CDMX        Mujer 
## 10    10 D+/D  Monterrey   Mujer 
## # ... with 424 more rows

Dependiendo del análisis, puede ser más útil trabajar con estos datos como factores o como factores ordenados.

demog_fct <- demograficos %>% 
  mutate(ciudad = factor(ciudad),
         nse = ordered(nse, levels = c("D+/D", "C-", "C", "C+", "A/B")),
         sexo = factor(sexo, levels = c("Mujer", "Hombre")))  

demog_fct
## # A tibble: 434 x 4
##    folio nse   ciudad      sexo  
##    <int> <ord> <fct>       <fct> 
##  1     1 D+/D  Monterrey   Hombre
##  2     2 D+/D  Queretaro   Hombre
##  3     3 C-    CDMX        Hombre
##  4     4 C-    Queretaro   Mujer 
##  5     5 C+    Guadalajara Mujer 
##  6     6 D+/D  Guadalajara Hombre
##  7     7 C+    CDMX        Mujer 
##  8     8 A/B   Queretaro   Mujer 
##  9     9 C     CDMX        Mujer 
## 10    10 D+/D  Monterrey   Mujer 
## # ... with 424 more rows

Observa el uso del argumento levels dentro de las funciones ordered() y factor(). Es por medio de este argumento que especificamos la jerarquía u orden de los niveles de cada variable.

Podemos verificar el tipo de datos con str():

demog_fct %>% 
  str()
## tibble [434 x 4] (S3: tbl_df/tbl/data.frame)
##  $ folio : int [1:434] 1 2 3 4 5 6 7 8 9 10 ...
##  $ nse   : Ord.factor w/ 5 levels "D+/D"<"C-"<"C"<..: 1 1 2 2 4 1 4 5 3 1 ...
##  $ ciudad: Factor w/ 4 levels "CDMX","Guadalajara",..: 3 4 1 4 2 2 1 4 1 3 ...
##  $ sexo  : Factor w/ 2 levels "Mujer","Hombre": 2 2 2 1 1 2 1 1 1 1 ...

Regresando al tibble diamonds, podemos probar otras funciones:

diamonds_mod <- diamonds %>% 
  mutate(clarity = as.character(clarity),
         x = as.integer(x),
         y = as.character(y))

diamonds_mod
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table price     x y         z
##    <dbl> <ord>     <ord> <chr>   <dbl> <dbl> <int> <int> <chr> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326     3 3.98   2.43
##  2  0.21 Premium   E     SI1      59.8    61   326     3 3.84   2.31
##  3  0.23 Good      E     VS1      56.9    65   327     4 4.07   2.31
##  4  0.29 Premium   I     VS2      62.4    58   334     4 4.23   2.63
##  5  0.31 Good      J     SI2      63.3    58   335     4 4.35   2.75
##  6  0.24 Very Good J     VVS2     62.8    57   336     3 3.96   2.48
##  7  0.24 Very Good I     VVS1     62.3    57   336     3 3.98   2.47
##  8  0.26 Very Good H     SI1      61.9    55   337     4 4.11   2.53
##  9  0.22 Fair      E     VS2      65.1    61   337     3 3.78   2.49
## 10  0.23 Very Good H     VS1      59.4    61   338     4 4.05   2.39
## # ... with 53,930 more rows

Observa que cada transformación tuvo un efecto en los datos. Para clarity, se perdió la jerarquía entre niveles, y ahora sólo se tiene texto en esa columna:

diamonds_mod %>% 
  distinct(clarity) %>% 
  arrange(clarity)
## # A tibble: 8 x 1
##   clarity
##   <chr>  
## 1 I1     
## 2 IF     
## 3 SI1    
## 4 SI2    
## 5 VS1    
## 6 VS2    
## 7 VVS1   
## 8 VVS2

En el caso de x, al transformarse de dbl a int, se perdieron los decimales de cada valor. Observa que NO se redondea, sino que se trunca. El primer valor, por ejemplo, pasó de ser 3.95 a 3.

diamonds_mod
## # A tibble: 53,940 x 10
##    carat cut       color clarity depth table price     x y         z
##    <dbl> <ord>     <ord> <chr>   <dbl> <dbl> <int> <int> <chr> <dbl>
##  1  0.23 Ideal     E     SI2      61.5    55   326     3 3.98   2.43
##  2  0.21 Premium   E     SI1      59.8    61   326     3 3.84   2.31
##  3  0.23 Good      E     VS1      56.9    65   327     4 4.07   2.31
##  4  0.29 Premium   I     VS2      62.4    58   334     4 4.23   2.63
##  5  0.31 Good      J     SI2      63.3    58   335     4 4.35   2.75
##  6  0.24 Very Good J     VVS2     62.8    57   336     3 3.96   2.48
##  7  0.24 Very Good I     VVS1     62.3    57   336     3 3.98   2.47
##  8  0.26 Very Good H     SI1      61.9    55   337     4 4.11   2.53
##  9  0.22 Fair      E     VS2      65.1    61   337     3 3.78   2.49
## 10  0.23 Very Good H     VS1      59.4    61   338     4 4.05   2.39
## # ... with 53,930 more rows

En el caso de y, a simple vista puede parecer que no cambió nada, fuera de la etiqueta <chr> bajo el nombre de la variable. Sin embargo, si intentamos realizar alguna operación matemática, nos regresa un NA y un warning.

diamonds_mod %>% 
  summarise(mean_y = mean(y))
## Warning in mean.default(y): argument is not numeric or logical: returning NA
## # A tibble: 1 x 1
##   mean_y
##    <dbl>
## 1     NA

Esto se debe a que es imposible realizar operaciones matemáticas con pedazos de texto. Aunque a simple vista parezcan ser números, es importante no perder de vista cómo lo está interpretando R.

Fechas y as.Date()

Sin duda la transformación de tipo de datos más común, y también más compleja, es la de cambio a tipo fecha.

Partiendo del siguiente tibble:

air_passengers
## # A tibble: 144 x 2
##    date       AirPassengers
##    <chr>              <dbl>
##  1 1949-01-01           112
##  2 1949-02-01           118
##  3 1949-03-01           132
##  4 1949-04-01           129
##  5 1949-05-01           121
##  6 1949-06-01           135
##  7 1949-07-01           148
##  8 1949-08-01           148
##  9 1949-09-01           136
## 10 1949-10-01           119
## # ... with 134 more rows

Se observa que en la columna date se tienen fechas. Sin embargo, tenemos justo debajo del nombre de la columna el tipo de dato <chr>. Esto quiere decir que, aunque un humano vea que hay fechas en esa columna, una computadora lo interpreta como texto. Para corregir esto, utilizamos as.Date().

La prinicipal fuente de dolores de cabeza al trabajar con fechas, o texto que debe ser fecha, es determinar el formato correcto.

El primer paso está en identificar en qué formato están las fechas. En este ejemplo, tenemos un formato ‘yyyy-mm-dd’, es decir, 4 digitos para el año, 2 para el mes, 2 para el día, separados por guión. Esto lo podemos deducir tan sólo viendo las fechas, sin embargo, al utilizar as.Date() es necesario dar explícitamente el formato:

air_passengers %>% 
  mutate(date = as.Date(date, format = "%Y-%m-%d"))
## # A tibble: 144 x 2
##    date       AirPassengers
##    <date>             <dbl>
##  1 1949-01-01           112
##  2 1949-02-01           118
##  3 1949-03-01           132
##  4 1949-04-01           129
##  5 1949-05-01           121
##  6 1949-06-01           135
##  7 1949-07-01           148
##  8 1949-08-01           148
##  9 1949-09-01           136
## 10 1949-10-01           119
## # ... with 134 more rows

Obsérvese el argumento format =. Utilizamos una combinación de caracteres, usualmente que inician con %, para especificar el formato de la fecha.

  • %Y representa año expresado con 4 dígitos.
  • %m representa mes expresado con 2 dígitos.
  • %d representa día expresado con 2 dígitos.

En la práctica, no se suelen tener datos tan limpios ni en el formato óptimo. Es necesario hacer algunas transformaciones.

Veamos el siguiente tibble:

ventas
## # A tibble: 60 x 2
##    fecha    ventas
##    <chr>     <dbl>
##  1 Ene 2015   228.
##  2 Feb 2015   253.
##  3 Mar 2015   514.
##  4 Abr 2015   468.
##  5 May 2015   369.
##  6 Jun 2015   461.
##  7 Jul 2015   430.
##  8 Ago 2015   360.
##  9 Sep 2015   352.
## 10 Oct 2015   377.
## # ... with 50 more rows

Al importar un archivo de Excel, es probable que las columnas que contienen fechas se importen de esta o alguna manera similar. Identificando el formato, observamos que no tenemos días, los meses están abreviados y en español, y están separados por un espacio.

Todos estos detalles tenemos que tomarlos en cuenta antes de que podamos convertir a tipo de dato date.

El primer paso es agregar un día. Aunque la fecha sea mensual, es necesario que se especifique el día. En este caso escribiremos 01 para utilizar el primer día de cada mes:

ventas %>% 
  mutate(fecha_con_dia = paste("01", fecha))
## # A tibble: 60 x 3
##    fecha    ventas fecha_con_dia
##    <chr>     <dbl> <chr>        
##  1 Ene 2015   228. 01 Ene 2015  
##  2 Feb 2015   253. 01 Feb 2015  
##  3 Mar 2015   514. 01 Mar 2015  
##  4 Abr 2015   468. 01 Abr 2015  
##  5 May 2015   369. 01 May 2015  
##  6 Jun 2015   461. 01 Jun 2015  
##  7 Jul 2015   430. 01 Jul 2015  
##  8 Ago 2015   360. 01 Ago 2015  
##  9 Sep 2015   352. 01 Sep 2015  
## 10 Oct 2015   377. 01 Oct 2015  
## # ... with 50 more rows

BONUS: Utilizamos la función paste(), la cual une en una misma cadena cada pedazo de texto (string) que se le provea en los argumentos, separados por un espacio. Puedes ejecutar ?paste para más información.

Una vez que se le agregó el día, podemos utilizar as.Date() para convertirlo a tipo de datos temporal.

Antes de eso, recordemos que las fechas están abreviadas en español. Puede parecer algo irrelevante para nuestro ojo humano, sin embargo, para una computadora es necesario especificarlo.

Si usas Windows, puedes ejecutar:

Sys.setlocale(locale = 'Spanish')
## [1] "LC_COLLATE=Spanish_Spain.1252;LC_CTYPE=Spanish_Spain.1252;LC_MONETARY=Spanish_Spain.1252;LC_NUMERIC=C;LC_TIME=Spanish_Spain.1252"

Si estás en Mac o Linux:

Sys.setlocale(locale = "es_ES.UTF-8")

Una vez establecido el idioma, podemos proceder a usar as.Date() dentro de un mutate().

ventas %>% 
  mutate(fecha_con_dia = paste("01", fecha)) %>% 
  mutate(date = as.Date(fecha_con_dia, format = "%d %b %Y"))
## # A tibble: 60 x 4
##    fecha    ventas fecha_con_dia date      
##    <chr>     <dbl> <chr>         <date>    
##  1 Ene 2015   228. 01 Ene 2015   2015-01-01
##  2 Feb 2015   253. 01 Feb 2015   2015-02-01
##  3 Mar 2015   514. 01 Mar 2015   2015-03-01
##  4 Abr 2015   468. 01 Abr 2015   2015-04-01
##  5 May 2015   369. 01 May 2015   2015-05-01
##  6 Jun 2015   461. 01 Jun 2015   2015-06-01
##  7 Jul 2015   430. 01 Jul 2015   2015-07-01
##  8 Ago 2015   360. 01 Ago 2015   2015-08-01
##  9 Sep 2015   352. 01 Sep 2015   2015-09-01
## 10 Oct 2015   377. 01 Oct 2015   2015-10-01
## # ... with 50 more rows

Observa que en el argumento format utilizamos ahora %b en el lugar de los meses, ya que es ese el símbolo para especificar que los meses están como texto abreviado. Observa tambien que los símbolos están separados por un espacio, en lugar de un guión, ya que en nuestra columna fecha_con_dia cada parte de la fecha está separada por un espacio.

Ejecuta ?strptime() en tu consola para ver todos los caracteres para especificar formato que pueden ser usados dentro de as.Date() y similares, y qué representa cada uno.

operaciones con strings

Frecuentemente te econtrarás con el problema de trabajar con strings (pedazos de texto).

En muchas fuentes de datos, gran parte de las columnas serán importadas como texto y antes de poderlo convertir a número, fecha o factor, es necesario realizar transformaciones, usualmente dentro de mutate().

También puede ser útil para conservar o elminar ciertos renglones según una columna que contiene strings de texto cuando se utiliza dentro de filter().

tolower() y toupper()

Dos de las transformaciones más comunes son pasar a minúsculas lo que está en mayúsculas o vice versa.

Se usan de la siguiente manera:

diamonds %>% 
  select(cut) %>% 
  mutate(lower_cut =  tolower(cut),
         upper_cut = toupper(cut))
## # A tibble: 53,940 x 3
##    cut       lower_cut upper_cut
##    <ord>     <chr>     <chr>    
##  1 Ideal     ideal     IDEAL    
##  2 Premium   premium   PREMIUM  
##  3 Good      good      GOOD     
##  4 Premium   premium   PREMIUM  
##  5 Good      good      GOOD     
##  6 Very Good very good VERY GOOD
##  7 Very Good very good VERY GOOD
##  8 Very Good very good VERY GOOD
##  9 Fair      fair      FAIR     
## 10 Very Good very good VERY GOOD
## # ... with 53,930 more rows

str_detect()

Supongamos que queremos conservar todos los diamantes (renglones) en los cuales clarity es VVS1 o VVS2. Como vimos anteriormente, podríamos resolver esto con el operador %in%, sin embargo, una forma más versatil es utilizando str_detect().

diamonds %>% 
  filter(str_detect(clarity, "VVS"))
## # A tibble: 8,721 x 10
##    carat cut       color clarity depth table price     x     y     z
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
##  1  0.24 Very Good J     VVS2     62.8  57     336  3.94  3.96  2.48
##  2  0.24 Very Good I     VVS1     62.3  57     336  3.95  3.98  2.47
##  3  0.23 Very Good G     VVS2     60.4  58     354  3.97  4.01  2.41
##  4  0.28 Ideal     G     VVS2     61.4  56     553  4.19  4.22  2.58
##  5  0.32 Ideal     I     VVS1     62    55.3   553  4.39  4.42  2.73
##  6  0.24 Premium   E     VVS1     60.7  58     553  4.01  4.03  2.44
##  7  0.24 Very Good D     VVS1     61.5  60     553  3.97  4     2.45
##  8  0.26 Very Good F     VVS2     59.2  60     554  4.19  4.22  2.49
##  9  0.26 Very Good E     VVS2     59.9  58     554  4.15  4.23  2.51
## 10  0.26 Very Good D     VVS2     62.4  54     554  4.08  4.13  2.56
## # ... with 8,711 more rows
diamonds %>% 
  filter(str_detect(clarity, "VVS")) %>% 
  distinct(clarity)
## # A tibble: 2 x 1
##   clarity
##   <ord>  
## 1 VVS2   
## 2 VVS1

Esta operación se lee como: Del tibble diamonds filtra (conserva) los renglones en los que detectes el string “VVS” en la columna clarity.

Estas funciones (del paquete stringr) toman como primer argumento un vector si se utilizan independientemente, o una columna del dataframe si se utilizan dentro de filter(), de mutate() o de summarise().

El segundo argumento es el pattern (una subcadena de texto) que se buscará dentro del vector o columna.

Dependiendo de la función, el output será distinto.

Veamos qué sucede al usar str_detect() directamente en un vector.

Tomemos los primeros 50 renglones de la columna clarity y almacenémoslo como vector:

clarity_vct <-  diamonds %>% 
  head(50) %>% 
  pull(clarity)

clarity_vct
##  [1] SI2  SI1  VS1  VS2  SI2  VVS2 VVS1 SI1  VS2  VS1  SI1  VS1  SI1  SI2  SI2 
## [16] I1   SI2  SI1  SI1  SI1  SI2  VS2  VS1  SI1  SI1  VVS2 VS1  VS2  VS2  VS1 
## [31] VS1  VS1  VS1  VS1  VS1  VS1  VS1  SI1  VS2  SI2  SI2  SI1  VS2  VS1  SI2 
## [46] SI1  SI2  SI2  VS2  SI2 
## Levels: I1 < SI2 < SI1 < VS2 < VS1 < VVS2 < VVS1 < IF

BONUS: Observa la función pull(). Es la forma de tidyverse para extraer vectores. diamonds$clarity es equivalente a diamonds %>% pull(clarity).

Usemos str_detect() en dicho vector:

str_detect(clarity_vct, "VVS")
##  [1] FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE
## [13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [25] FALSE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [37] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [49] FALSE FALSE

Observamos que regresa un vector de TRUEs y FALSEs.

Lo que está haciendo R, es evaluar si detecta la expresión “VVS” en cada elemento del vector, regresando TRUE si lo encuentra, o FALSE en caso contrario.

Al utilizarse dentro de un filter(), la evaluación se hace directamente sobre los renglones del dataframe, de manera que para los elementos que regresa TRUE simplemente conserva los renglones correspondientes, y para los elementos en los que regresa FALSE descarta dichos renglones.

La detección del pattern se hace en cualquier parte del string evaluado.

diamonds %>% 
  filter(str_detect(clarity, "S")) %>% 
  distinct(clarity)
## # A tibble: 6 x 1
##   clarity
##   <ord>  
## 1 SI2    
## 2 SI1    
## 3 VS1    
## 4 VS2    
## 5 VVS2   
## 6 VVS1

Buscamos los renglones para los cuales detecta una “S” en la columna clarity. Nota que en algunos casos la “S” está al principio, en otros casos en medio y en otros casos más cerca del final.

str_extract()

Veamos el siguiente conjunto de datos (ficticios):

ventas_papitas <- readRDS('data/ventas_papitas.RDS')

ventas_papitas
## # A tibble: 192 x 3
##    fecha      submarca                  ventas
##    <date>     <chr>                      <dbl>
##  1 2018-01-01 Sabritas Adobadas           361.
##  2 2018-01-01 Sabritas Limon              295.
##  3 2018-01-01 Sabritas Flamin Hot         509.
##  4 2018-01-01 Sabritas Habanero           663.
##  5 2018-01-01 Sabritas Crema y especias   191.
##  6 2018-01-01 Doritos Nacho               373.
##  7 2018-01-01 Doritos Diablo              510.
##  8 2018-01-01 Doritos Flamin Hot          387.
##  9 2018-01-01 Doritos 3D                  282.
## 10 2018-01-01 Doritos Incognita           569.
## # ... with 182 more rows

Tenemos las ventas mensuales de distintas variedades de botanas saladas para el año 2018. a nivel submarca.

Queremos ver el total de venta mensual a nivel marca. Veamos los distintos valores que tenemos para submarca:

ventas_papitas %>% 
  distinct(submarca) %>% 
  print(n = 16)
## # A tibble: 16 x 1
##    submarca                 
##    <chr>                    
##  1 Sabritas Adobadas        
##  2 Sabritas Limon           
##  3 Sabritas Flamin Hot      
##  4 Sabritas Habanero        
##  5 Sabritas Crema y especias
##  6 Doritos Nacho            
##  7 Doritos Diablo           
##  8 Doritos Flamin Hot       
##  9 Doritos 3D               
## 10 Doritos Incognita        
## 11 Doritos Pizzerolas       
## 12 Cheetos Crunchy          
## 13 Cheetos Flamin Hot       
## 14 Cheetos Poffs            
## 15 Cheetos Torciditos       
## 16 Cheetos Colmillos

Contamos con 3 marcas principales: Sabritas, Doritos, Cheetos. Para calcular el nivel a total marca, necesitamos primero extraer la marca de los valores de submarca. Hacemos esto por medio de str_extract() dentro de un mutate().

ventas_papitas %>% 
  mutate(marca = str_extract(submarca,"Sabritas|Cheetos|Doritos"))
## # A tibble: 192 x 4
##    fecha      submarca                  ventas marca   
##    <date>     <chr>                      <dbl> <chr>   
##  1 2018-01-01 Sabritas Adobadas           361. Sabritas
##  2 2018-01-01 Sabritas Limon              295. Sabritas
##  3 2018-01-01 Sabritas Flamin Hot         509. Sabritas
##  4 2018-01-01 Sabritas Habanero           663. Sabritas
##  5 2018-01-01 Sabritas Crema y especias   191. Sabritas
##  6 2018-01-01 Doritos Nacho               373. Doritos 
##  7 2018-01-01 Doritos Diablo              510. Doritos 
##  8 2018-01-01 Doritos Flamin Hot          387. Doritos 
##  9 2018-01-01 Doritos 3D                  282. Doritos 
## 10 2018-01-01 Doritos Incognita           569. Doritos 
## # ... with 182 more rows
ventas_papitas %>% 
  mutate(marca = str_extract(submarca,"Sabritas|Cheetos|Doritos")) %>% 
  group_by(fecha, marca) %>% 
  summarise(ventas = sum(ventas)) %>% 
  ungroup()
## `summarise()` has grouped output by 'fecha'. You can override using the `.groups` argument.
## # A tibble: 36 x 3
##    fecha      marca    ventas
##    <date>     <chr>     <dbl>
##  1 2018-01-01 Cheetos   1702.
##  2 2018-01-01 Doritos   2556.
##  3 2018-01-01 Sabritas  2020.
##  4 2018-02-01 Cheetos   1888.
##  5 2018-02-01 Doritos   2572.
##  6 2018-02-01 Sabritas  1767.
##  7 2018-03-01 Cheetos   2085.
##  8 2018-03-01 Doritos   2013.
##  9 2018-03-01 Sabritas  1759.
## 10 2018-04-01 Cheetos   1557.
## # ... with 26 more rows

Observa el uso de | para separar los distintos patterns. La instrucción se lee como: Crear una nueva variable, en la cual se extraigan de la columna submarca el patrón "Sabritas" o "Cheetos" o "Doritos".

A diferencia de str_detect(), str_extract() regresa el pattern que pedimos que busque. Dicho de otra manera, extrae el pattern del string dado. Podemos verlo operando directamente sobre el vector:

str_extract(ventas_papitas$submarca,"Sabritas|Cheetos|Doritos")
##   [1] "Sabritas" "Sabritas" "Sabritas" "Sabritas" "Sabritas" "Doritos" 
##   [7] "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Cheetos" 
##  [13] "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos"  "Sabritas" "Sabritas"
##  [19] "Sabritas" "Sabritas" "Sabritas" "Doritos"  "Doritos"  "Doritos" 
##  [25] "Doritos"  "Doritos"  "Doritos"  "Cheetos"  "Cheetos"  "Cheetos" 
##  [31] "Cheetos"  "Cheetos"  "Sabritas" "Sabritas" "Sabritas" "Sabritas"
##  [37] "Sabritas" "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Doritos" 
##  [43] "Doritos"  "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos" 
##  [49] "Sabritas" "Sabritas" "Sabritas" "Sabritas" "Sabritas" "Doritos" 
##  [55] "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Cheetos" 
##  [61] "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos"  "Sabritas" "Sabritas"
##  [67] "Sabritas" "Sabritas" "Sabritas" "Doritos"  "Doritos"  "Doritos" 
##  [73] "Doritos"  "Doritos"  "Doritos"  "Cheetos"  "Cheetos"  "Cheetos" 
##  [79] "Cheetos"  "Cheetos"  "Sabritas" "Sabritas" "Sabritas" "Sabritas"
##  [85] "Sabritas" "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Doritos" 
##  [91] "Doritos"  "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos" 
##  [97] "Sabritas" "Sabritas" "Sabritas" "Sabritas" "Sabritas" "Doritos" 
## [103] "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Cheetos" 
## [109] "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos"  "Sabritas" "Sabritas"
## [115] "Sabritas" "Sabritas" "Sabritas" "Doritos"  "Doritos"  "Doritos" 
## [121] "Doritos"  "Doritos"  "Doritos"  "Cheetos"  "Cheetos"  "Cheetos" 
## [127] "Cheetos"  "Cheetos"  "Sabritas" "Sabritas" "Sabritas" "Sabritas"
## [133] "Sabritas" "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Doritos" 
## [139] "Doritos"  "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos" 
## [145] "Sabritas" "Sabritas" "Sabritas" "Sabritas" "Sabritas" "Doritos" 
## [151] "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Cheetos" 
## [157] "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos"  "Sabritas" "Sabritas"
## [163] "Sabritas" "Sabritas" "Sabritas" "Doritos"  "Doritos"  "Doritos" 
## [169] "Doritos"  "Doritos"  "Doritos"  "Cheetos"  "Cheetos"  "Cheetos" 
## [175] "Cheetos"  "Cheetos"  "Sabritas" "Sabritas" "Sabritas" "Sabritas"
## [181] "Sabritas" "Doritos"  "Doritos"  "Doritos"  "Doritos"  "Doritos" 
## [187] "Doritos"  "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos"  "Cheetos"

str_remove()

Supongamos ahora que el conjunto de datos viene a nivel SKU. Además de tener submarca, tiene el gramaje de la presentación. En este caso, buscaríamos calcular las ventas a nivel submarca, es decir, eliminando la presentación.

ventas_papitas_sku <- readRDS('data/ventas_papitas_sku.RDS')

ventas_papitas_sku
## # A tibble: 576 x 3
##    fecha      sku                           ventas
##    <date>     <chr>                          <dbl>
##  1 2018-01-01 Sabritas Adobadas (40 grs)      333.
##  2 2018-01-01 Sabritas Adobadas (100 grs)     143.
##  3 2018-01-01 Sabritas Adobadas (120 grs)     405.
##  4 2018-01-01 Sabritas Limon (40 grs)         298.
##  5 2018-01-01 Sabritas Limon (100 grs)        128.
##  6 2018-01-01 Sabritas Limon (120 grs)        389.
##  7 2018-01-01 Sabritas Flamin Hot (40 grs)    657.
##  8 2018-01-01 Sabritas Flamin Hot (100 grs)   394.
##  9 2018-01-01 Sabritas Flamin Hot (120 grs)   402.
## 10 2018-01-01 Sabritas Habanero (40 grs)      481.
## # ... with 566 more rows

La forma más sencilla es, para cada valor de sku, quitar la última parte que está entre paréntesis. De esa manera conservaríamos únicamente la marca y submarca.

Para esto, utilizamos str_remove() dentro de un mutate() de la siguiente manera:

ventas_papitas_sku %>% 
  mutate(submarca = str_remove(string = sku, pattern = " \\(.*\\)"))
## # A tibble: 576 x 4
##    fecha      sku                           ventas submarca           
##    <date>     <chr>                          <dbl> <chr>              
##  1 2018-01-01 Sabritas Adobadas (40 grs)      333. Sabritas Adobadas  
##  2 2018-01-01 Sabritas Adobadas (100 grs)     143. Sabritas Adobadas  
##  3 2018-01-01 Sabritas Adobadas (120 grs)     405. Sabritas Adobadas  
##  4 2018-01-01 Sabritas Limon (40 grs)         298. Sabritas Limon     
##  5 2018-01-01 Sabritas Limon (100 grs)        128. Sabritas Limon     
##  6 2018-01-01 Sabritas Limon (120 grs)        389. Sabritas Limon     
##  7 2018-01-01 Sabritas Flamin Hot (40 grs)    657. Sabritas Flamin Hot
##  8 2018-01-01 Sabritas Flamin Hot (100 grs)   394. Sabritas Flamin Hot
##  9 2018-01-01 Sabritas Flamin Hot (120 grs)   402. Sabritas Flamin Hot
## 10 2018-01-01 Sabritas Habanero (40 grs)      481. Sabritas Habanero  
## # ... with 566 more rows

Esta instrucción se lee como: Crea una nueva columna del resultado de remover todo lo que esté entre paréntesis de los valores de la columna sku.

¿Pero cómo es que ahí dice “todo lo que está entre paréntesis”, si sólo hay símbolos raros?

Buena pregunta.

REGEX

Todo lo que está escrito en el argumento pattern es una expresión regular o regular expression, abreviado como REGEX. Regex es una especie de lenguaje, una serie de símbolos con distintos signficados, que sirven para describir diversos patrones que se pueden encontrar en un texto. Como en el caso de nuestro ejemplo, " \\(.*\\)" significa un espacio, seguido de caracteres entre paréntesis.

A simple vista puede verse complicado, pero si analizamos cada parte de la expresión, verás que es más sencillo:

  • : Parece que no hay nada, pero ahí hay un espacio. Es importante especificar cada parte de la expresión.

  • \\(: Un paréntesis abierto. Las dos diagonales le dicen a R que busquen literalmente el paréntesis, pues en el lenguaje de Regex, un paréntesis solito (sin las diagonales) representan otra cosa.

  • .: Ese puntito es un comodín. Puede ser cualquier letra del abecedario, minúscula o mayúscula, o bien, cualquier dígito. A-Z, a-z, 0-9.

  • *: El asterisco es un multiplicador. Quiere decir que el símbolo que esté a su izquierda, en este caso el comodín, puede aparecer un número indefinido de veces.

  • \\): Paréntesis cerrado. De igual manera que con el abierto, las díagonales indican que se debe buscar un paréntesis literal.

Poniendo todo junto, vemos que el pattern buscado es un espacio, seguido de un paréntesis abierto, seguido de un número indefinido de caracteres alfanuméricos, (en este caso sirve para buscar los 40 grs, 100 grs, 120 grs), seguido de un paréntesis cerrado. Como dicho pattern está dentro de la función str_remove(), todo lo que coincida con ese patrón sera eliminado.

De nuevo, es fácil ver cómo funciona en un vector:

sku_vect <- ventas_papitas_sku %>% 
  distinct(sku) %>% 
  pull(sku)

sku_vect
##  [1] "Sabritas Adobadas (40 grs)"          "Sabritas Adobadas (100 grs)"        
##  [3] "Sabritas Adobadas (120 grs)"         "Sabritas Limon (40 grs)"            
##  [5] "Sabritas Limon (100 grs)"            "Sabritas Limon (120 grs)"           
##  [7] "Sabritas Flamin Hot (40 grs)"        "Sabritas Flamin Hot (100 grs)"      
##  [9] "Sabritas Flamin Hot (120 grs)"       "Sabritas Habanero (40 grs)"         
## [11] "Sabritas Habanero (100 grs)"         "Sabritas Habanero (120 grs)"        
## [13] "Sabritas Crema y especias (40 grs)"  "Sabritas Crema y especias (100 grs)"
## [15] "Sabritas Crema y especias (120 grs)" "Doritos Nacho (40 grs)"             
## [17] "Doritos Nacho (100 grs)"             "Doritos Nacho (120 grs)"            
## [19] "Doritos Diablo (40 grs)"             "Doritos Diablo (100 grs)"           
## [21] "Doritos Diablo (120 grs)"            "Doritos Flamin Hot (40 grs)"        
## [23] "Doritos Flamin Hot (100 grs)"        "Doritos Flamin Hot (120 grs)"       
## [25] "Doritos 3D (40 grs)"                 "Doritos 3D (100 grs)"               
## [27] "Doritos 3D (120 grs)"                "Doritos Incognita (40 grs)"         
## [29] "Doritos Incognita (100 grs)"         "Doritos Incognita (120 grs)"        
## [31] "Doritos Pizzerolas (40 grs)"         "Doritos Pizzerolas (100 grs)"       
## [33] "Doritos Pizzerolas (120 grs)"        "Cheetos Crunchy (40 grs)"           
## [35] "Cheetos Crunchy (100 grs)"           "Cheetos Crunchy (120 grs)"          
## [37] "Cheetos Flamin Hot (40 grs)"         "Cheetos Flamin Hot (100 grs)"       
## [39] "Cheetos Flamin Hot (120 grs)"        "Cheetos Poffs (40 grs)"             
## [41] "Cheetos Poffs (100 grs)"             "Cheetos Poffs (120 grs)"            
## [43] "Cheetos Torciditos (40 grs)"         "Cheetos Torciditos (100 grs)"       
## [45] "Cheetos Torciditos (120 grs)"        "Cheetos Colmillos (40 grs)"         
## [47] "Cheetos Colmillos (100 grs)"         "Cheetos Colmillos (120 grs)"
str_remove(sku_vect, " \\(.*\\)")
##  [1] "Sabritas Adobadas"         "Sabritas Adobadas"        
##  [3] "Sabritas Adobadas"         "Sabritas Limon"           
##  [5] "Sabritas Limon"            "Sabritas Limon"           
##  [7] "Sabritas Flamin Hot"       "Sabritas Flamin Hot"      
##  [9] "Sabritas Flamin Hot"       "Sabritas Habanero"        
## [11] "Sabritas Habanero"         "Sabritas Habanero"        
## [13] "Sabritas Crema y especias" "Sabritas Crema y especias"
## [15] "Sabritas Crema y especias" "Doritos Nacho"            
## [17] "Doritos Nacho"             "Doritos Nacho"            
## [19] "Doritos Diablo"            "Doritos Diablo"           
## [21] "Doritos Diablo"            "Doritos Flamin Hot"       
## [23] "Doritos Flamin Hot"        "Doritos Flamin Hot"       
## [25] "Doritos 3D"                "Doritos 3D"               
## [27] "Doritos 3D"                "Doritos Incognita"        
## [29] "Doritos Incognita"         "Doritos Incognita"        
## [31] "Doritos Pizzerolas"        "Doritos Pizzerolas"       
## [33] "Doritos Pizzerolas"        "Cheetos Crunchy"          
## [35] "Cheetos Crunchy"           "Cheetos Crunchy"          
## [37] "Cheetos Flamin Hot"        "Cheetos Flamin Hot"       
## [39] "Cheetos Flamin Hot"        "Cheetos Poffs"            
## [41] "Cheetos Poffs"             "Cheetos Poffs"            
## [43] "Cheetos Torciditos"        "Cheetos Torciditos"       
## [45] "Cheetos Torciditos"        "Cheetos Colmillos"        
## [47] "Cheetos Colmillos"         "Cheetos Colmillos"

Aunque la idea general de REGEX es muy sencilla, existen muchos símbolos y trucos que pueden hacer la vida más sencilla. No es necesario que aprendas todos, pero sí es muy útil saber que este recurso existe y está a tu disposición, si algún dia lo necesitas.