<- iris[1:10,] mi.iris
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:
- Programación imperativa: variables, bucles, etc. Es la habitual en lenguajes como C, Fortran o Matlab.
- Programación funcional, donde las funciones son ciudadanos de primera clase. Lisp fue el lenguaje pionero en programación funcional y, actualmente, Haskell o Scala son lenguajes casi puramente funcionales; otros como Python, Java o C++, aunque imperativos, incorporan cada vez más elementos funcionales.
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 <-
.
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:
<- iris[1:10,]
mi.iris class(mi.iris)
[1] "data.frame"
is.data.frame(mi.iris)
[1] TRUE
<- 1:10
x 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í:
<- function(capital, anyos, interes){
calcular.cuota.hipoteca <- interes / 12 / 100
interes.mensual <- 1:(anyos*12)
meses 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.:
<- calcular.cuota.hipoteca
calculadora.hipotecas 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,
<- function(x){
cuadrado return(x^2)
}
y
<- function(x) return(x^2) cuadrado
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
<- function(x) x^2 cuadrado
En R, además, las funciones pueden tener argumentos con valores por defecto. Por ejemplo,
<- function(x, exponente = 2) x^exponente
potencia 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:
<- function(x){
xln return(-x * log(x))
}
<- 1:10000 / 10000
x 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:
<- function(x){
xln 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
:
<- function(x){
xln 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:
<- function(n){
mi.factorial <- 1
factorial for (i in 1:n){
<- factorial * i
factorial
}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,
<- function(n){
mi.factorial <- n
factorial while (n > 1){
<- n - 1
n <- factorial * n
factorial
}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:
<- 1:5
x 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:
<- 1:10
x <- 0
suma.x for (i in x){
<- suma.x + i
suma.x
} 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:
<- x |> f() |> g() |> h() res
<- h(g(f(x))) res
<- x
res <- f(res)
res <- g(res)
res <- h(res) 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
:
<- function(x) if(x < 5) x^2 else -x^2
cuadrado.raro 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
ylapply
(que son casi la misma)tapply
apply
ymapply
- Las funciones
ddply
,ldply
, etc. del paqueteplyr
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:
<- function(n, lambda = 4, mean = 5){
simula <- sum(rpois(n, lambda))
n.visitas sum(rnorm(n.visitas, mean = mean))
}
<- replicate(1000, simula(10, lambda = 7)) res
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
<- 1:20
x %% 3 == 0] x[x
[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
):
<- lm(dist ~ speed, data = cars)
mi.modelo 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)
<- as_tibble(iris)
tmp 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.
Se las conoce como funciones de orden superior.↩︎
En otros lenguajes de programación se las conoce también como funciones lambda.↩︎
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.↩︎
Aunque no se recomienda: el corchete es más sucinto.↩︎