Las pipes y cuando usarlas “%>%”

Las pipes son una herramienta muy útil para crear conexiones entre operaciones sin necesidad de crear variables intermedias.

Estan incluidas en un paquete llamado “magrittr”.

library(magrittr)

Explicación ventajas de uso de pipe

Habran dos tipos de funciones que no funcionaran el entorno de pipe:

Pipes alternativas

Mejor trabajar como máximo con 10/12 pipes, para facilitar el debugging, fácil comprobar los resultados intermedios. Para más de 10/12, lo mejor es crear objetos intermedios, con nombres significativos.

Las pipes estan pensadas para expresar relaciones complejas lineales, todo lo que este fuera de lo lineal, las pipes no podran ayudar.

También se puede echar un vistazo a otras pipes de “magrittr”.Consultar la información en RStudio documentation. Un ejemplo:

rnorm(1000) %>%
  matrix(ncol = 2) %T>%
  plot() %>%
  str()

##  num [1:500, 1:2] -0.0379 1.1666 0.0515 -0.1593 0.4549 ...
mtcars %$%
  cor(disp, mpg)
## [1] -0.8475514

Creación de funciones

Ventajas de la creación de las propias funciones:

¿Cuándo conviene crear una función?

Primero creemos un dataset con el que vamos a trabajar:

df  <- tibble::tibble(
  a = rnorm(20),
  b = rnorm(20),
  c = rnorm(20),
  d = rnorm(20),
  e = rnorm(20)
)
df
## # A tibble: 20 x 5
##          a        b      c       d       e
##      <dbl>    <dbl>  <dbl>   <dbl>   <dbl>
##  1  0.824  -0.784   -0.832  0.236   0.232 
##  2  1.34   -1.60    -0.305  0.495   0.945 
##  3  0.214  -0.260   -1.33  -0.369   0.123 
##  4  0.788   0.290    1.36   0.639  -1.93  
##  5 -1.58   -2.70    -0.431 -0.0411 -0.742 
##  6  1.26    0.564    1.59   0.0918  0.477 
##  7 -0.789   0.330   -1.46   1.07   -0.909 
##  8 -1.22    0.298   -1.38   0.729   2.04  
##  9 -0.354   0.00805 -0.670  0.0349  1.41  
## 10 -1.48   -0.326    1.16   0.435  -0.643 
## 11  0.680  -0.610   -0.887  0.249   0.816 
## 12  1.66    1.26    -1.43  -0.292  -0.141 
## 13  0.926   0.152   -0.235 -0.199   0.580 
## 14 -1.64   -1.73    -1.43  -0.171  -1.02  
## 15  0.393  -0.891   -0.203 -0.521  -1.27  
## 16  0.897   0.214    0.529 -1.08   -0.150 
## 17  0.0686 -1.96     0.280 -2.34   -0.0520
## 18  0.922   1.07     1.40  -0.756  -0.538 
## 19 -0.426  -0.284    0.138 -1.21    1.09  
## 20  0.130   0.400   -1.67  -1.38   -0.362

Ahora queremos normalizar estos datos entre 0 i 1. Ejemplo de como hacerlo uno por uno:

(df$a - min(df$a, na.rm = T))/(max(df$a) - min(df$a, na.rm = T))
##  [1] 0.74581363 0.90268671 0.56100906 0.73497762 0.01843150 0.87824453
##  [7] 0.25731934 0.12655814 0.38925812 0.04904639 0.70224750 1.00000000
## [13] 0.77658514 0.00000000 0.61515179 0.76797375 0.51708997 0.77555702
## [19] 0.36737296 0.53564503
(df$b - min(df$b, na.rm = T))/(max(df$b) - min(df$b, na.rm = T))
##  [1] 0.4834827 0.2779275 0.6156409 0.7543400 0.0000000 0.8232698 0.7643169
##  [8] 0.7562201 0.6831780 0.5989353 0.5272960 1.0000000 0.7193526 0.2440680
## [15] 0.4565242 0.7351776 0.1856852 0.9514275 0.6096399 0.7820852
# (...) para cada columna

¿Podemos intentar reescalar cada columna entre 0 i 1 sin ningún tipo de error? Creando una función:

# Para crear la función, primero debemos saber ¿quántos datos de entrada tiene?
# Respuesta: solo uno, si miramos para la primera  observación es "a" de df$a
# Creearemos un vector con el que trabajaremos:
x <- df$a
# Sustituimos en la función que hemos creado por variable:
(x - min(x, na.rm = T))/(max(x) - min(x, na.rm = T))
##  [1] 0.74581363 0.90268671 0.56100906 0.73497762 0.01843150 0.87824453
##  [7] 0.25731934 0.12655814 0.38925812 0.04904639 0.70224750 1.00000000
## [13] 0.77658514 0.00000000 0.61515179 0.76797375 0.51708997 0.77555702
## [19] 0.36737296 0.53564503
# Aún así, no queda del todo claro. Podríamos optimizaro más con la función range, para que nos devuelva un vector de dos componentes, en el primero esta el minimo en el segundo esta el máximo
(rng <- range(x, na.rm = T))
## [1] -1.639207  1.663596
# Podríamos aplicar este rango para simplificar los cálculos:
(x - rng[1])/(rng[2]-rng[1])
##  [1] 0.74581363 0.90268671 0.56100906 0.73497762 0.01843150 0.87824453
##  [7] 0.25731934 0.12655814 0.38925812 0.04904639 0.70224750 1.00000000
## [13] 0.77658514 0.00000000 0.61515179 0.76797375 0.51708997 0.77555702
## [19] 0.36737296 0.53564503

Ahora que hemos entendido la estructura, vamos a crear la función:

rescale_0_1 <- function(x){
  rng <- range(x, na.rm = T, finite = TRUE)
  (x - rng[1])/(rng[2] - rng[1])
}

Ahora simplemente podemos aplicar la función para cada una de las variables, simplificando así la sintaxis:

# Seleccionando una variable de dateframe
rescale_0_1(df$a)
##  [1] 0.74581363 0.90268671 0.56100906 0.73497762 0.01843150 0.87824453
##  [7] 0.25731934 0.12655814 0.38925812 0.04904639 0.70224750 1.00000000
## [13] 0.77658514 0.00000000 0.61515179 0.76797375 0.51708997 0.77555702
## [19] 0.36737296 0.53564503
rescale_0_1(df$b)
##  [1] 0.4834827 0.2779275 0.6156409 0.7543400 0.0000000 0.8232698 0.7643169
##  [8] 0.7562201 0.6831780 0.5989353 0.5272960 1.0000000 0.7193526 0.2440680
## [15] 0.4565242 0.7351776 0.1856852 0.9514275 0.6096399 0.7820852
# Para un vector de valores de 0 a 10 lo reescala entre 0 i 1
rescale_0_1(c(0,5,10))
## [1] 0.0 0.5 1.0
# Añadimos un NA
rescale_0_1(c(1,2,NA,4,5))
## [1] 0.00 0.25   NA 0.75 1.00

Hay tres partes muy importantes en una función:

  • Que tenga un nombre senzillo e informativo

  • listo de argumentos que tiene la función

  • cuerpor de la función (entre {})

Por lo tanto, para normalizar las columnas, ahora podemos hacerlo:

df$a <- rescale_0_1(df$a)
df$b <- rescale_0_1(df$b)
df$c <- rescale_0_1(df$c)
df$d <- rescale_0_1(df$d)
df$e <- rescale_0_1(df$e)

Sin embargo, esta función tiene un problema. Si incluimos el valor infinito, espta función, peta (los resultados son correctos porque se le ha cambiado el parámetro finite a range préviamente):

rescale_0_1(c(1:10, Inf))
##  [1] 0.0000000 0.1111111 0.2222222 0.3333333 0.4444444 0.5555556 0.6666667
##  [8] 0.7777778 0.8888889 1.0000000       Inf

Para eso debemos añadir el parámetro finite = TRUE dentro de la función range().

Funciones para humanos y ordenadores

El nombre de a función ha de ser corto y claro, que sea explicativo antes que corto. Ejemplos de como NO se debe escribir:

  • l() -> l que????

  • this_function_is_superguai() -> MAL, no sabemos que hace

Formas correctas de dar nombres:

  • impute_missing()

  • count_days()

  • collapse_hours()

Evitar sobreescribir:

  • T <- FALSE

  • c <- 5

  • mean <- function(x) median(x)

Comentar todo para que sea claro y usar herramientas para lectura fácil de R.

Condicionales y toma de decisiones

Ejemplo de una estructura de if:

# if(condicion){
    # código a ejecutar si la condicion es TRUE
# } else {
    # código a ejecutar si la condici?n es FALSE
# }

Ejemplo de función if(), con el objetivo de evaluar si cada element del vector tiene o no tiene nombre:

has_name <- function(x){
  nms <- names(x)
  if(is.null(nms)){ 
    # no tiene nombre asignado, no existe el objeto en question
    rep(FALSE, length(x))
  } else {
    # ausencia de NAs y de ""
    !is.na(nms) & nms != "" 
  }
}

Un ejemplo para aplicar la función:

# Enviamos un vector sin nombre de columnas
has_name(c(1,2,3))
## [1] FALSE FALSE FALSE
# Enviamos un data set que S? que contiene nombres de columnas
has_name(mtcars)
##  [1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
#  Generamos una tribble para evaluar
has_name(dplyr::tribble(
  ~x, ~y, ~' ',
   1,  2,   3
))
## [1] TRUE TRUE TRUE

Podéis usar operadores con if() como:

Ejemplos para entender el if() para vectores lógicos, con la función any() alguna condición verdadera o all() para saber si todas son verdaderas:

if(any(c(T,F))){
  "tenemos almenos un verdadero"
} else {
  "tenemos alguna condicion falsa"
}
## [1] "tenemos almenos un verdadero"
if (all(c(T,F))){
  "tenemos todas las condiciones verdaderas"
} else {
  "tenemos alguna condicion falsa"
}
## [1] "tenemos alguna condicion falsa"

Los if no solo tienen que dar resutados booleanos, pueden también dar diferentes opciones:

# if(condicion){
    # resultado 1
# } else if(condicion2) {
    # resultado 2
# } else if(condicion3) {
    # resultado 3
# } else {
    # resultado por defecto
# }

Para simplificar estos if() que pueden ser tan grandes, se usa la técnica de switch(). Vamos a hacer ejemplo de la calculadora:

calculate <- function(x,y,op){
  switch (op,
          suma  = x+y,
          resta = x-y,
          multiplicacion = x+y,
          division = x/y,
          stop("ERROR: no se puede ejecturar la funcion"))
}

Ejemplos de uso de la función:

calculate(2,3,"suma")
## [1] 5
calculate(2,3,"resta")
## [1] -1
calculate(2,3,"multiplicacion")
## [1] 5
calculate(2,3,"division")
## [1] 0.6666667

Los argumentos de las funciones

Las funciones estan divididas en dos partes:

  • Dato

  • Detalle del calculo

log(x = 8, base = 2)

mea(x = c(1,2,3), trim = 2, na.rm = TRUE)

Algunas ideas para creación de variables:

  • vectores: x, y, z

  • vector de pesos: w

  • data frame: df, data, d

  • subíndices: i, j, k

  • longitud de un vector, o número de filas: n

  • probabilidad: p

Punto, punto, punto y evaluación tardía

Ejemplos de funciones

Para funciones robustas, usar:

  • stop()

  • stopifnot()

Para añadir de forma ilimitadas, se usan los … , o función “commas”. Ejemplo:

#  commas <- function(...)

Valores de retorno y valores entorno

Acceso directo al video

Normalmente se usa return para devolver un valor: […]

La estructura de vector

Como haremos uso del paquete de purrr, cargaremos el fichero de tidyverse:

library(tidyverse)

Hay dos tipos de vectores con los que podemos trabajar:

  1. vector atómic: lógico, integer (numeric), double (numeric), character, complex, raw. Un vector será homogeneo, porque solo podrá tener un valor.

  2. listas o vectores recursivos. Se llaman vectores recursivos porque una lista puede contener otras listas, un vector en cambio no puede contener otros vectores. Una lista tabién será heterogenea.

Luego tendremos dos valores más en R:

Todo vector tiene dos propiedades, uno es el tipo de dato y la otra es su longitud.

Un vector pueden contener otros metadatos a través de los atributos. Se usan para crear vectores aumentados, hay 4 tipos:

Los vectores atómicos

Los 4 tipos más importantes de vectores atómicos son:

Lógico

Sólo puede tomar el valor TRUE, el valor FALSE y el valor NA. Se acostumbra a usar para comparación lógica Ejemplo:

1:10 %% 3 == 0 # que valores del rango de 1 a 10 son divisibles entre 3
##  [1] FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE

Entero & Double

Tanto el integer como double son numéricos. R los diferenciará entre un valor integer [1L] y un valor double [1]. Su diferencia está más relacionado en la memoria que ocupa en el sistema.

Atención para comparar números es mejor usar la función de dplyr near(), para evitar los problemas de la coma flotante. Ejemplo:

x <- sqrt(2)^2  # no es exactamente igual a 2 debido al efecto de la coma flotante
dplyr::near(x, 2) # para assegurarnos que el resultado es correcto
## [1] TRUE

Evitar la comparación con valores como -Inf, NaN, Inf

Character

Cada elemento de su vector puede contener un número indeterminado de strings. R almacena los strings únicos una sola vez, reduciendo así la memoria utlizada y cada vez que se hace uso a un determinado string, se accede a esa información única que préviamente se había guardado. Ejemplo:

x <- "Dábale arroz a la zorra el abad"
pryr::object_size(x) # vemos el peso en memoria del sistema del character
## 136 B
y <- rep(x, 10000) # replicamos el character 10.000 veces
pryr::object_size(y) # su tamaño no es 10.000 veces más grande que x
## 80.1 kB

Tipos especiales de NAs de los vectres atómicos

  • NA –> lógico

  • NA_character_ –> character

  • NA_complex_ –> complejo

  • NA_integer_ –> entero

  • NA_real_ –> double

El row y número complejo no se acostumbran a usar en el análisis de datos.

Transformaciones y castings

Hay diferentas formas:

Casting directo de un vector

Es la forma más sencilla de hacer la conversión y más directa:

as.logical(1,0,0,0,1)
## [1] TRUE
as.integer(c(T,F,F,F,T))
## [1] 1 0 0 0 1
as.double(c(1,2,3))
## [1] 1 2 3
as.character("Hellou it's me")
## [1] "Hellou it's me"

Comprovar si un vector tiene un valor u otro:

# is_logical(c(T,T,T))        # logical
# is_integer(c(1L,2L,3L,4L))  # integer
# is_double(c(1,2,3,4))       # double
# is_numeric(c(1,2,3,4L))     # integer, double
# is_character("a","b","c")   # character
# is_atomic(c(T,T,1,2L,"a"))  # logical, integer, double, character
# 
# is.vector(c(T,T)) # serà verdad tanto para vectores atomicos como listas
# is.atomic(c(T,T))

La regla de reciclaje de datos