11  Programación

R, entre otras cosas, es también un lenguaje de programación, aunque, generalmente, no se utiliza para programar; más bien, se utiliza interactivamente: el usuario lee datos, los revisa, los manipula y genera gráficos, modelos, ficheros Rmd, etc. Además, típicamente, en este proceso hay ciclos: la revisión de los datos conduce a reescribir su lectura, la modelización a modificar su manipulación, etc. Es inhabitual usar R para desarrollar programas largos con R al estilo de los que se crean con Java, C++ u otros lenguajes.

Así que en R, normalmente, programar no consiste tanto en crear programas como en empaquetar código útil en bloques, i.e., funciones reutilizables.

Muchas de estas funciones son de usar y tirar, es decir, solo son útiles en un contexto determinado: pueden crearse para ser utilizadas en un único proyecto o en una parte muy concreta o pequeña del mismo. Otras son más generales y los usuarios pueden querer guardarlas para reutilizarlas en otros proyectos. No es infrecuente que los usuarios identifiquen determinadas necesidades, desarrollen funciones destinadas a satisfacerlas y acaben creando sus propios paquetes de funciones de R y redistribuyéndolas entre sus colegas. No obstante, la creación de paquetes, aunque no es complicada, queda fuera del alcance de este libro.

Existen dos grandes paradigmas de programación:

R permite combinar ambos. Y los combina, además, con la programación orientada a objetos. El objetivo de la sección será el de familiarizarnos con los aspectos imperativos y funcionales de la programación en R. Esta sección, de todos modos, no es una introducción a la programación. Se limita a mostrar la sintaxis que utiliza R para las construcciones (expresiones condicionales, bucles, definición de funciones, maps, etc.) habituales en otros lenguajes de programación, con alguno de los cuales se espera que esté familiarizado el lector.

11.1 Programación imperativa en R

La programación imperativa es aquella en la que el estado de un programa, definido por el valor de sus variables, se modifica mediante la ejecución de comandos. El énfasis recae, además, en cómo se modifica el estado del programa y es frecuente el uso de bucles y expresiones condicionales. Es el tipo de programación a la que están acostumbrados los programadores en C, BASIC, mucho de C++, Python o Java, o MatLab.

11.1.1 Variables

Las variables ya son conocidas. Se crean con el operador de asignación <-.

mi.iris <- iris[1:10,]

Las variables existentes en la memoria de R se pueden listar, borrar, etc.

ls()
rm(mi.iris)
ls()

Al programar, en algunas ocasiones, resulta necesario conocer el tipo de las variables. Existen lenguajes tipados, como Java o Scala, donde al declarar una variable es obligatorio especificar la naturaleza (número, texto, etc.) del valor que contendrá. En R (al igual que Python), sin embargo, no lo es. Pero eso no elimina la necesidad de conocer el tipo de las variables en algunas ocasiones: durante el curso hemos visto cómo determinadas funciones operan de manera distinta (¡o fallan!) dependiendo del tipo de la variable subyacente. Por eso, frecuentemente, es necesario comprobar que los datos con los que trabajamos son del tipo adecuado.

En el código que aparece a continuación inquiremos el tipo de las variables implicadas:

mi.iris <- iris[1:10,]
class(mi.iris)
[1] "data.frame"
is.data.frame(mi.iris)
[1] TRUE
x <- 1:10
is.vector(x)
[1] TRUE
class(x)
[1] "integer"
typeof(x)
[1] "integer"

Este tipo de comprobaciones son importantes tras la lectura o la manipulación de datos para ver si se han procesado correctamente. También lo son cuando se crean funciones de propósito general y se quiere, por ejemplo, comprobar que el usuario proporciona a las funciones argumentos adecuados.

11.1.2 Funciones

Un análisis de datos consiste generalmente en una secuencia de comandos de R (posiblemente insertados dentro de un fichero .Rmd) que se ejecutan secuencialmente. En ocasiones, sin embargo, es conveniente crear funciones. Por ejemplo, cuando hay operaciones comunes que se realizan reiteradamente (incluso en análisis distintos).

Una función se define, por ejemplo, así:

calcular.cuota.hipoteca <- function(capital, anyos, interes){
  interes.mensual <- interes / 12 / 100
  meses <- 1:(anyos*12)
  return(capital / sum(1 / (1+interes.mensual)^meses))
}

Es decir, con function, seguido de la lista de argumentos y de un bloque de código (encerrado en llaves) que contiene el cuerpo de la función. Una función, típicamente, se asigna a una variable con <- (aunque veremos casos en que pueden usarse funciones anónimas). La función así creada puede invocarse:

calcular.cuota.hipoteca(100000, 20, 3)
[1] 554.5976

La función, además, se convierte en un objeto normal de R; es decir, aparece en los listados de ls, que se puede borrar con rm, etc.:

calculadora.hipotecas <- calcular.cuota.hipoteca
calculadora.hipotecas(100000, 20, 3)
[1] 554.5976
ls()
[1] "calculadora.hipotecas"   "calcular.cuota.hipoteca"
rm(calculadora.hipotecas)

Ejercicio 11.1 Crea una función que, dado un número n calcule la suma de los n primeros términos de la serie de Leibniz para aproximar \(\pi\). Nota: ya hemos realizado previamente este ejercicio; lo que se pide aquí es incluir aquel código dentro de una función.

Un bloque de código es un conjunto de líneas que se ejecutan secuencialmente. Para acotar los bloques de código se usan las llaves { }. Sin embargo, no son necesarias cuando el bloque consiste en una sola línea. Es decir,

cuadrado <- function(x){
  return(x^2)
}

y

cuadrado <- function(x) return(x^2)

son ambas definiciones válidas de la función cuadrado.

Hemos usado return para que la función devuelva un valor. En algunos lenguajes de programación es obligatorio el uso de return; sin embargo, en R no: una función de R devuelve el último valor calculado dentro de su cuerpo. Así que una tercera opción equivalente y más sucinta para definir la función cuadrado es

cuadrado <- function(x) x^2

En R, además, las funciones pueden tener argumentos con valores por defecto. Por ejemplo,

potencia <- function(x, exponente = 2) x^exponente
c(potencia(2), potencia(2, 3), potencia(2, exponente = 3))

Los valores por defecto permiten, por ejemplo, llamar a funciones como read.table, que admite muchos argumentos, especificando solo algunos de ellos.

Además de los valores por defecto, muchas funciones admiten el argumento especial .... Esos tres puntos significan que la función admite parámetros adicionales que o bien utilizará de cierta forma o que pasará a otras funciones. Véanse, por ejemplo, ?tapply, ?ddply, o incluso, ?head. De hecho, son esos tres puntos los que permiten realizar operaciones —ya tratadas previamente— como

tapply(iris$Petal.Length, iris$Species, mean, na.rm = TRUE)

En dicha expresión, na.rm = TRUE es recogido por el argumento especial ... de tapply y pasado posteriormente a la función mean.

11.1.3 Expresiones condicionales

Las expresiones condicionales permiten ejecutar un código u otro en función de un criterio. Para ilustrar su uso en R, Comenzaremos definiendo y representando gráficamente la función xln, que aparece frecuentemente en estadística:

xln <- function(x){
  return(-x * log(x))
}

x <- 1:10000 / 10000
plot(x, xln(x), type = "l", xlab = "", ylab = "",
     main = "Función -x * log(x)")

Diríase que su valor en 0 es cero; sin embargo, en contra de nuestra intuición,

xln(0)       # Nan cuando queremos cero!
[1] NaN

Podemos, por lo tanto, arreglarla con una expresión condicional:

xln <- function(x){
  if (x == 0)
    return(0)
  return(-x * log(x))
}

Y ahora sí,

xln(0)
[1] 0

Ejercicio 11.2 Modifica la función anterior para que dé un error cuando x sea menor que 0 o mayor que 1. Pista: la función stop() lanza un error. El argumento de stop es el texto que aparece en el mensaje de error.

Ejercicio 11.3 En la definición anterior hay dos return. Uno sobra y el otro no. ¿Cuál es cuál?

Como ilustra el ejercicio anterior, es muy común que una función resuelva al principio uno o más casos particulares (y salga de ellos mediante returns dentro de expresiones condicionales) y que, una vez solventados, se plantee al final el caso general y más complejo. La salida de este último, típicamente, no necesita return.

Frecuentemente, if va acompañado de else:

xln <- function(x){
  if (x == 0)
    return(0)
  else
    return(-x * log(x))
}

Como antes, cuando el bloque de código que sigue a if (o else) contiene una única línea, se pueden ignorar las llaves. Si no, hay que usarlas.

Ejercicio 11.4 ¿Cuántos return sobran en la última definición de xln? ¿Por qué?

Ejercicio 11.5 Existe una función en R, ifelse, que permite escribir la función anterior de una forma más compacta. Búscala en la ayuda y reescribe la función xln.

Ejercicio 11.6 Crea una función que tome como argumento un vector de texto (o un factor) cuyas entradas sean números de la forma "1.234,56" y devuelva el correspondiente número subyacente, es decir, que elimine el separador de miles, etc. Ten en cuenta que cuando el vector de entrada sea del tipo factor tendrás que convertirlo previamente en otro de tipo character.

Ejercicio 11.7 Modifica la función del ejemplo anterior de forma que si el usuario pasa como argumento un vector númerico devuelva ese número; si pasa un argumento del tipo character o factor, aplique la lógica descrita en ese ejercicio y, finalmente, si pasa un argumento de otro tipo, devuelva un error (usando stop) con un mensaje informativo.

11.1.4 Bucles

En la programación imperativa es habitual construir bucles dentro de los cuales se va modificando el valor de una expresión. Los bucles más habituales en R son los for. Su sintaxis es

for (var in vector){
  # expresión que se repite
}

Lo comentado más arriba sobre las llaves aplica también a los bucles: solo son obligatorias cuando el bloque de código contiene más de una línea.

Un ejemplo de libro para ilustrar el uso de los bucles es el del cálculo del factorial de un número:

mi.factorial <- function(n){
  factorial <- 1
  for (i in 1:n){
    factorial <- factorial * i
  }
  return(factorial)
}

mi.factorial(7)
[1] 5040

Ejercicio 11.8 Modifica la función anterior para que devuelva explícitamente un error siempre que el argumento de la función no sea un entero positivo.

También existen (aunque se usan menos), los bucles while:

while (condicion){
  # expresión que se repite
}

Por ejemplo,

mi.factorial <- function(n){
  factorial <- n
  while (n > 1){
    n <- n - 1
    factorial <- factorial * n
  }
  return(factorial)
}

mi.factorial(7)

Los bucles se usan poco en R por varios motivos. Uno de ellos es que muchas funciones están vectorizadas, es decir, no es neceario explicitar el bucle:

x <- 1:5
sqrt(x)
[1] 1.000000 1.414214 1.732051 2.000000 2.236068
sum(x)
[1] 15

Para sumar un vector de números con bucles explícitos habría que hacer:

x <- 1:10
suma.x <- 0
for (i in x){
  suma.x <- suma.x + i
}
suma.x

Es evidente cómo la vectorización permite crear código más breve y, sobre todo, expresivo.

Ejercicio 11.9 Crea una función que calcule la raíz cuadrada de los elementos de un vector usando un bucle explícito.

Ejercicio 11.10 Crea una función que, dado un número n devuelva la lista de sus divisores.

Ejercicio 11.11 Modifica la función construida en un ejercicio anterior, la que devolvía los divisores de un número n para que compruebe previamente que el argumento es un número entero positivo y devuelva un error en caso contrario.

11.1.5 Tuberías (pipes)

En la consolas de Mac y Linux existe el comando ls que imprime los contenidos de un directorio y wc -l, que cuenta el número de líneas de un fichero. Además, es posible hacer

ls | wc -l

que muestra el número de ficheros contenidos en un directorio. Se trata de la concatenación o composición de los dos comandos gracias al uso del operador | conocido como pipe o tubería. La concatenación de operaciones simples permite realizar acciones complejas.

En R existe una variante de ese operador, |>, gracias al cual se puede hacer

ls() |> length()

y que, en realidad, no es otra cosa que una manera distinta de escribir

length(ls())

En general, los tres bloques de código siguientes son equivalentes:

res <- x |> f() |> g() |> h()
res <- h(g(f(x)))
res <- x
res <- f(res)
res <- g(res)
res <- h(res)

Ejercicio 11.12 Comprueba que log(exp(2)) da el mismo resultado que 2 |> exp |> log.

Además, si f admite dos argumentos, entonces x |> f(b) es equivalente a f(x, b). Se puede decir que el pipe coloca lo que está a su izquierda como primer argumento de la función que está a su derecha.

El uso de pipes no aporta nada en el orden funcional, pero sí que puede resultar práctico para hacer más legibles ciertas transformaciones. Por ejemplo, aun sin saber nada —todavía— de dplyr es muy sencillo interpretar el siguiente bloque de código:

surveys |>
  filter(weight < 5) |>
  select(species_id, sex, weight)

En código avanzado no es infrecuente encontrar concatenaciones de diez o más funciones como las anteriores.

Verás que, frecuentemente, en lugar de |>, se usa %>% como operador pipe. El motivo es histórico: en R no existían tuberías hasta que fueron implementadas en el paquete magrittr mediante el operador %>%. Posteriormente, R adoptó el operador nativo |> con —prácticamente— el mismo funcionamiento.

11.2 Programación funcional en R

La programación funcional, en un sentido amplio, es aquella en que determinadas funciones1 admiten otras como argumento. Por ejemplo, la función sapply:

cuadrado.raro <- function(x) if(x < 5) x^2 else -x^2
sapply(1:10, cuadrado.raro)
 [1]    1    4    9   16  -25  -36  -49  -64  -81 -100

La programación funcional es sumamente poderosa y sugiere permite programar de la siguiente manera:

  • Crear funciones pequeñas y simples que resuelven un problema pequeño y acotado
  • Aplicar esas funciones a grupos homogéneos de valores.

En el ejemplo de más arriba hemos construido una función, cuadrado.raro, y con la función sapply (lapply, como hemos visto previamente, es una alternativa) se las hemos aplicado a una lista de valores homogéneos, los números del 1 al 10.

Hay muchas funciones en R, algunas de las cuales son ya conocidas, que admiten otras como argumento. Algunas de las más corrientes son:

  • sapply y lapply (que son casi la misma)
  • tapply
  • apply y mapply
  • Las funciones ddply, ldply, etc. del paquete plyr

Dos ejemplos de usos muy habituales de estas funciones son

lapply(iris, class)
sapply(iris, length)

que permiten inspeccionar el tipo de columnas de una tabla. Aprovechan, precisamente, que una tabla es una lista de columnas y las recorren una a una.

Generalmente, el código que usa este tipo de funciones es más breve y legible.

Es conveniente recordar aquí que si consultas la ayuda de las fucniones listadas más arriba verás que suelen incluir un argumento especial, ... que permite pasar argumentos adicionales a la función a la que llaman. En la sección en que introdujimos la función tapply discutimos un caso de uso.

11.2.1 Funciones anónimas

Las funciones que hemos usado son de dos tipos: o exiten en R o las hemos definido previamente. Pero en ocasiones es conveniente usar funciones anónimas2 de esta manera:

sapply(1:10, function(x) if(x < 5) x^2 else -x^2)

Conviene particularmente cuando la función solo se usa una única vez. Las funciones anónimas, debidamente usadas, confieren brevedad y expresividad al código.

Ejercicio 11.13 Crea el vector de nombres de ficheros de data usando dir; luego, aplícale una función que lea las líneas (readLines) y las cuente.

Ejercicio 11.14 Usa nchar para contar el número de caracteres de esos ficheros.

Ejercicio 11.15 Haz lo mismo usando la función ldply de plyr.

11.2.2 Map, reduce y más

Existen dos operaciones fundamentales en programación funcional. La primera es map y consiste en aplicar una función a todos los elementos de una lista o vector. Es, de hecho, la operación que hemos realizado más arriba:

sapply(1:10, function(x) if(x < 5) x^2 else -x^2)

Ese código aplica al vector 1:10 la función anónima que se le pasa a sapply como segundo argumento. Aunque en muchos lenguajes de programación existe una función map explícita (con ese nombre), en R hay varias: además de sapply, también están lapply o apply. La vectorización que hemos discutido previamente es un mecanismo implícito para realizar maps; p.e.,

sqrt(1:10)

aplica la función sqrt a cada elemento de su argumento.

La otra gran operación de la programación funcional es reduce. Consiste en aplicar una operacion binaria (p.e., la que suma dos números) a una lista de valores iterativamente. En R se pueden realizar reduces explícitamente:

Reduce(function(a, b) a + b, 1:10)

La función anónima anterior admite dos argumentos, a y b y los suma. Dentro de Reduce, la función anónima suma los dos primeros elementos, al resultado le suma el tercero, al resultado el cuarto, etc., hasta proporcionarnos la suma total. De nuevo, es frecuente poder realizar estas operaciones implícitamente. Por ejemplo, usando sum:

sum(1:10)

Ejercicio 11.16 Vamos a crear el objeto x <- split(iris, iris$Species), que es una lista de tres tablas. Usa lapply o sapply para examinarlas: dimensión, nombres de columnas, etc.

Ejercicio 11.17 Usa Reduce con la función rbind para apilar las tres tablas contenidas en x (véase el ejercicio anterior).

Este ejercicio tiene aplicaciones prácticas importantes. Por ejemplo, cuando se leen tablas del mismo formato de ficheros distintos y es necesario juntarlas todas, i.e., apilarlas, en una única tabla final.

Sin embargo, para operaciones como la anterior, en R no se usa tanto Reduce como do.call. Es muy típico escribir (y encontrar) código tal como

do.call(rbind, x)

donde x es una lista de tablas. El resultado de esa expresión es una tabla que contiene las tablas contenidas en x apiladas, igual que en el ejercicio anterior. Funciona porque rbind admite un número arbitrario de argumentos (objetos que aplilar) y lo que hace do.call es llamar a rbind con los valores de x como argumento. De esta manera, si x contuviese tres tablas,

do.call(rbind, x)

sería una forma abreviada y cómoda de escribir

rbind(x[[1]], x[[2]], x[[3]])

Operaciones tales como

sum(sqrt(1:10))

son, por lo tanto, los famosos mapreduces3 popularizados por las herramientas de big data.

Una operación relacionada con map y muy frecuente en R es replicate. Esta función permite llamar repetidamente a una función (o bloque de código) para, muy frecuentemente, realizar simulaciones:

simula <- function(n, lambda = 4, mean = 5){
  n.visitas <- sum(rpois(n, lambda))
  sum(rnorm(n.visitas, mean = mean))
}

res <- replicate(1000, simula(10, lambda = 7))

La diferencia fundamental con map es que no opera sobre un vector: crea uno de la nada.

Otra metaoperación básica en programación funcional es filter. Un filtro permite seleccionar aquellas observaciones (p.e., en una lista) que cumplen cierta condición. En R sabemos implementar esa operación usando los corchetes. Por ejemplo, para seleccionar los múltiplos de tres en un vector, podemos hacer

x <- 1:20
x[x %% 3 == 0]
[1]  3  6  9 12 15 18

Pero en R también se puede usar4 la función Filter:

Filter(function(i) i %%3 == 0, x)
[1]  3  6  9 12 15 18

Muy importantes en R debido a lo habitual de operar con tablas son las operaciones basadas en otra operación funcional, el groupby. El groupby permite partir un objeto en trozos de acuerdo con un determinado criterio para operar a continuación sobre los subloques obtenidos. tapply es una función que implementa una versión básica del groupby. Mucho más potente y versátil que ella es la función ddply del paquete plyr. Esta función, como ya sabemos, realiza tres operaciones:

  • Parte una tabla convenientemente (groupby).
  • Aplica (map) una función sobre cada subtabla.
  • Recompone (reduce) una tabla resultante a partir de los trozos devueltos en el paso anterior.

11.3 Orientación a objetos

R está orientado a objetos. Los objetos son estructuras que combinan datos y funciones que operan sobre ellos y son muy útiles en un entorno, como R, pensado para el análisis estadístico de datos. Por ejemplo, el resultado de una regresión lineal es un objeto (de clase lm):

mi.modelo <- lm(dist ~ speed, data = cars)
class(mi.modelo)
[1] "lm"

El objeto mi.modelo contiene información relevante acerca del modelo lineal: coeficientes, resíduos, p-valores, etc. Se puede consultar esta información haciendo, por ejemplo,

str(mi.modelo)

Pero, además, existen funciones como summary, plot o predict que saben cómo operar sobre un objeto de la clase lm como el anterior proporcionando los resultados esperados.

11.3.1 Polimorfismo

El polimorfismo es una característica del lenguaje que se logra en R gracias a la orientación a objetos. Permite que una única función, p.e., summary, opere de manera distinta dependiendo de su argumento. La siguiente discusión pone de manifiesto la utilidad del polimorfismo y cómo la orientación a objetos de R es fundamental para conseguirla.

Nótese que el objeto mi.modelo construido más arriba es también una lista:

is.list(mi.modelo)
[1] TRUE

En realidad, nuestro modelo es una lista a la que se ha añadido un atributo class que lo identifica como un modelo lineal, i.e., lm. Por contra, iris es una tabla, es decir, otro tipo especial de lista, una con atributo class data.frame. En el fondo, ambos son listas; sin embargo, la función summary proporciona información distinta sobre ellos:

summary(iris)
  Sepal.Length    Sepal.Width     Petal.Length    Petal.Width   
 Min.   :4.300   Min.   :2.000   Min.   :1.000   Min.   :0.100  
 1st Qu.:5.100   1st Qu.:2.800   1st Qu.:1.600   1st Qu.:0.300  
 Median :5.800   Median :3.000   Median :4.350   Median :1.300  
 Mean   :5.843   Mean   :3.057   Mean   :3.758   Mean   :1.199  
 3rd Qu.:6.400   3rd Qu.:3.300   3rd Qu.:5.100   3rd Qu.:1.800  
 Max.   :7.900   Max.   :4.400   Max.   :6.900   Max.   :2.500  
       Species  
 setosa    :50  
 versicolor:50  
 virginica :50  
                
                
                
summary(mi.modelo)

Call:
lm(formula = dist ~ speed, data = cars)

Residuals:
    Min      1Q  Median      3Q     Max 
-29.069  -9.525  -2.272   9.215  43.201 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) -17.5791     6.7584  -2.601   0.0123 *  
speed         3.9324     0.4155   9.464 1.49e-12 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 15.38 on 48 degrees of freedom
Multiple R-squared:  0.6511,    Adjusted R-squared:  0.6438 
F-statistic: 89.57 on 1 and 48 DF,  p-value: 1.49e-12

Es precisamente a través del atributo class que R implementa el polimorfismo, i.e, que una misma función opere de manera distinta sobre objetos distintos. Lo mismo ocurre con otras funciones genéricas como plot, print o predict.

En realidad, detrás de summary existen muchas funciones que son versiones específicas (o métodos) suyas. Las disponibles para summary, son

methods(summary)
 [1] summary.aov                         summary.aovlist*                   
 [3] summary.aspell*                     summary.check_packages_in_dir*     
 [5] summary.connection                  summary.data.frame                 
 [7] summary.Date                        summary.default                    
 [9] summary.ecdf*                       summary.factor                     
[11] summary.glm                         summary.infl*                      
[13] summary.lm                          summary.loess*                     
[15] summary.manova                      summary.matrix                     
[17] summary.mlm*                        summary.nls*                       
[19] summary.packageStatus*              summary.POSIXct                    
[21] summary.POSIXlt                     summary.ppr*                       
[23] summary.prcomp*                     summary.princomp*                  
[25] summary.proc_time                   summary.rlang_error*               
[27] summary.rlang_message*              summary.rlang_trace*               
[29] summary.rlang_warning*              summary.rlang:::list_of_conditions*
[31] summary.srcfile                     summary.srcref                     
[33] summary.stepfun                     summary.stl*                       
[35] summary.table                       summary.tukeysmooth*               
[37] summary.vctrs_sclr*                 summary.vctrs_vctr*                
[39] summary.warnings                   
see '?methods' for accessing help and source code

Cuando summary se aplica sobre un objeto de la clase lm, la versión de summary que se le aplica es summary.lm; cuando se aplica sobre iris, se utiza summary.data.frame, etc. Obviamente, es responsabilidad de los autores de lm definir las funciones summary.lm, print.lm, plot.lm, etc.

11.3.2 Herencia (y tibbles)

Otra de las posibilidades que abre la orientación a objetos es la herencia de clases. Es decir, a partir de una clase determinada es posible crear otras clases que la extiende. Por ejemplo, dentro del tidyverse —discutido más adelante— existe un paquete, tibble, que extiende las tablas habituales de R, de clase data.frame:

library(tibble)
tmp <- as_tibble(iris)
class(tmp)
[1] "tbl_df"     "tbl"        "data.frame"

tmp sigue siendo un data.frame, pero es, además, un objeto de la clase tbl_df y tbl. Eso significa que, en esencia y para casi todos los efectos, sigue siendo una tabla pero con ciertas particularidades. Por ejemplo, print(tmp) (o, simplemente, como ya sabemos, tmp), tiene un aspecto distinto:

tmp
# A tibble: 150 × 5
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
          <dbl>       <dbl>        <dbl>       <dbl> <fct>  
 1          5.1         3.5          1.4         0.2 setosa 
 2          4.9         3            1.4         0.2 setosa 
 3          4.7         3.2          1.3         0.2 setosa 
 4          4.6         3.1          1.5         0.2 setosa 
 5          5           3.6          1.4         0.2 setosa 
 6          5.4         3.9          1.7         0.4 setosa 
 7          4.6         3.4          1.4         0.3 setosa 
 8          5           3.4          1.5         0.2 setosa 
 9          4.4         2.9          1.4         0.2 setosa 
10          4.9         3.1          1.5         0.1 setosa 
# … with 140 more rows

Los tibbles son, de hecho, una especie de tabla mejorada propuesta por los autores del tidyverse.

Existen otras extensiones de las tablas habituales. Por ejemplo, el paquete data.table ha extendido los data.frames habituales para crear tablas con índices útiles para procesar grandes volúmenes de datos muy eficientemente en memoria.

11.4 Resumen y referencias

R no es propiamente un lenguaje de programación. Sus mismos creadores lo definieron como un entorno en/con el que realizar análisis estadístico y gráfico de datos. Puede decirse que contiene un lenguaje de programación o que permite la programación. Ese es el aspecto de R que se ha tratado en este capítulo.

Sin embargo, R, como lenguaje de programación en sí mismo, deja bastante que desear. Es muy difícil crear programas complejos sólidos en R. Esta es una de las razones por las que mucha gente lo evita en los llamados entornos productivos. Python, en ese sentido, es un lenguaje mucho más adecuado. Eso explica parcialmente su creciente popularidad en el mundo del análisis de datos a expensas, entre otros, de R.

La programación funcional proporciona una serie de operaciones genéricas, map, reduce, filter, groupby y algunas otras más que permiten modelar conceptualmente a los programas. Los programadores experimentados identifican frecuentemente un determinado algoritmo como, por ejemplo, un map seguido de un filter y un reduce final. Eso les facilita, por ejemplo, el poliglotismo: al final, desarrollar en cualquier lenguaje se reduce a expresar esas operaciones genéricas en la sintaxis específica. La programación funcional, además, abre la puerta de muchas aplicaciones big data, donde impera desde sus inicios la programación funcional. El mismo Hadoop se concibió alrededor del concepto del MapReduce y la más popular de las herramientas actuales de big data, Spark, es una extensión del lenguaje funcional Scala.

Para saber más sobre clases y orientación a objetos en R, puedes leer Desarrollo de paquetes con R (IV): funciones genéricas. Ahí se discute cómo asignar atributos de clase a objetos y cómo crear métodos y, si procede, nuevas funciones genéricas.

En R existen varios mecanismos para dotarlo de la orientación a objetos. En esta sección hemos explorado superficialmente la más simple, la conocida como S3 (que corresponde con la tercera especificación del lenguaje). Existen otros mecanismos más formales de clases (p.e., las de las clases S4), que utilizan algunos paquetes de R. Por ejemplo, gran parte de los que se usan en geoestadística.

11.5 Ejercicios adicionales

Ejercicio 11.18 En R nunca implementaríamos el factorial de la manera en que lo hemos hecho en esta sección, i.e., con bucles for o while. Reescribe la función mi.factorial teniendo en cuenta que el factorial de 7, por ejemplo, puede calcularse haciendo prod(2:7).

Ejercicio 11.19 En este ejercicio y el siguiente, vamos a realizar una simulación para estimar la probabilidad del evento que se describe a continuación. En un avión viajan n personas. Cada una de ellas tiene asignado su asiento y entran al aparato en cualquier orden. El vuelo, además, está lleno: no hay plazas libres. Sin embargo, la primera persona tiene necesidades especiales y le conceden el primer asiento. El resto de los pasajeros ocupa los suyos así:

  • Si su asiento está libre, se sientan en él.
  • Si está ocupado, se sientan en cualquiera de los que está libre.

La probabilidad que se pide estimar es la de que el último pasajero encuentre libre su asiento.

Para ello, crea una función que tome como parámetro el número de asientos en el avión y devuelva TRUE o FALSE según si el último pasajero tiene disponible o no su asiento.

Ejercicio 11.20 Una vez terminado el ejercicio anterior, usa la función replicate (consulta su ayuda) para ejecutar la función anterior muchas veces. El promedio del número de valores TRUE obtenidos será la estimación de la probabilidad del evento descrito arriba.


  1. Se las conoce como funciones de orden superior.↩︎

  2. En otros lenguajes de programación se las conoce también como funciones lambda.↩︎

  3. Mapreduce es una operación genérica que consiste en un map seguido de un reduce; muchas manipulaciones de datos se reducen en última instancia a un mapreduce o a una concatenación de ellos.↩︎

  4. Aunque no se recomienda: el corchete es más sucinto.↩︎