3.10: Programación Procesal
- Page ID
- 55107
Para muchos (quizás la mayoría) lenguajes, las estructuras de flujo de control como for-loops y sentencias if son bloques de construcción fundamentales para escribir programas útiles. Por el contrario, en R, podemos lograr mucho haciendo uso de operaciones vectorizadas, reciclaje vectorial, indexación lógica, split-apply-combine, etc. En general, R enfatiza fuertemente las operaciones vectorizadas y funcionales, y estos enfoques deben usarse cuando sea posible; también suelen estar optimizados para la velocidad. Aún así, R proporciona el repertorio estándar de bucles y estructuras condicionales (que forman la base del paradigma de la “programación procedimental”) que se puede utilizar cuando otros métodos son torpes o difíciles de diseñar. Ya hemos visto funciones, por supuesto, que son increíblemente importantes en la gran mayoría de los lenguajes de programación.
Bucle condicional con bucles While-Loops
Un bucle while ejecuta un bloque de código una y otra vez, probando una condición dada (que debería ser o devolver un vector lógico) antes de cada ejecución. En R, los bloques se delinean con un par de corchetes. La práctica estándar es colocar el primer corchete de apertura en la línea, definiendo el bucle y el corchete de cierre en una línea por sí mismo, con el bloque envolvente indentado por dos espacios.
En la ejecución de lo anterior, cuando se alcanza la línea while (count < 4)
, count < 4
se ejecuta y devuelve el vector lógico de un solo elemento TRUE
, dando como resultado que el bloque se ejecute e imprima “Count is:"
y 1
, y luego incrementando contar
por 1. Entonces el bucle vuelve a la comprobación; el conteo
siendo 2 es aún menor que 4, por lo que ocurre la impresión y el conteo
se incrementa nuevamente a 3. El bucle comienza de nuevo, se ejecuta la comprobación, se imprime la salida y el recuento
se incrementa a 4. El bucle vuelve a la parte superior, pero esta vez el recuento < 4
da como resultado FALSO
, por lo que se salta el bloque, y finalmente la ejecución pasa a imprimir “¡Hecho!”
.
Debido a que la comprobación ocurre al inicio, si el conteo
comenzara en algún número mayor como 5
, entonces el bloque de bucle se saltaría por completo y solo “¡Hecho!”
se imprimirían.
Debido a que no existen “datos desnudos” en R y los vectores son la unidad más básica, la comprobación lógica dentro del while ()
funciona con un vector lógico. En lo anterior, ese vector acaba de pasar a tener un solo elemento. ¿Y si la comparación devolvió un vector más largo? En ese caso, solo el primer elemento del vector lógico será considerado por el while ()
, y se emitirá una advertencia al efecto de condición tiene longitud > 1
. Esto puede tener algunas consecuencias extrañas. Consideremos si en lugar de contar <- 1
, teníamos count <- c (1, 100)
. Debido a la naturaleza vectorizada de la suma, la salida sería:
Dos funciones prácticas pueden proporcionar una medida de seguridad frente a tales errores cuando se usan con condicionales simples: any ()
y all ()
. Dado un vector lógico, estas funciones devuelven un vector lógico de un solo elemento que indica si alguno, o todos, de los elementos en el vector de entrada son VERDADEROS
. Por lo tanto, nuestro condicional while
-loop anterior podría codificarse mejor como while (any (count < 4))
. (¿Se puede decir la diferencia entre esto y while (any (count) < 4)
?)
Generación de Datos Aleatorios Truncados, Parte I
Las características estadísticas únicas de R se pueden combinar con estructuras de flujo de control como bucles while de maneras interesantes. R sobresale al generar datos aleatorios a partir de una distribución dada; por ejemplo, rnorm (1000, media = 20, sd = 10)
devuelve un vector numérico de 100 elementos con valores muestreados de una distribución normal con media 20 y desviación estándar 10.
Aunque cubriremos el trazado con más detalle más adelante, la función hist ()
nos permite producir un histograma básico a partir de un vector numérico. (Hacerlo requiere una interfaz gráfica como Rstudio para mostrar fácilmente la salida. Consulte el capítulo 37, “Datos de trazado y ggplot2
”, para obtener detalles sobre el trazado en R.)
¿Y si quisiéramos muestrear números originarios de esta distribución, pero limitados al rango de 0 a 30? Una forma de hacerlo es “remuestrear” cualquier valor que se encuentre fuera de este rango. Esto truncará efectivamente la distribución a estos valores, produciendo una nueva distribución con media y desviación estándar alteradas. (Este tipo de distribuciones “truncadas por remuestreo” a veces son necesarias para fines de simulación).
Idealmente, nos gustaría una función que encapsula esta idea, así podemos decir sample <- rnorm_trunc (0, 30, 1000, mean = 20, sd = 10)
. Comenzaremos definiendo nuestra función, y dentro de la función primero produciremos una muestra inicial.
Observe el uso de media = media
en la llamada a rnorm ()
. El lado derecho se refiere al parámetro pasado a la función rnorm_trunc ()
, el lado izquierdo se refiere al parámetro tomado por rnorm ()
, y el intérprete no tiene ningún problema con este uso.
Ahora, necesitaremos “arreglar” la muestra siempre y cuando cualquiera de los valores sea menor que menor
o mayor que superior
. Comprobaremos tantas veces como sea necesario usando un bucle while.
Si algún valor está fuera del rango deseado, no queremos simplemente probar un conjunto de muestras completamente nuevo, porque la probabilidad de generar solo la muestra correcta (dentro del rango) es increíblemente pequeña, y estaríamos haciendo un bucle durante bastante tiempo. Más bien, sólo remuestrearemos aquellos valores que lo necesiten, generando primero un vector lógico de elementos “malos”. Después de eso, podemos generar un remuestreo del tamaño necesario y usar reemplazo selectivo para reemplazar los elementos malos.
Vamos a probarlo:
El histograma trazado refleja la naturaleza truncada del conjunto de datos:
IF-Declaraciones
Una sentencia if ejecuta un bloque de código basado en un condicional (como un bucle while, pero el bloque solo se puede ejecutar una vez). Al igual que el check for while
, solo se verifica el primer elemento de un vector lógico, por lo que se sugiere usar any ()
y all ()
por seguridad a menos que estemos seguros de que la comparación dará como resultado un vector lógico de un solo elemento.
Al igual que las sentencias if en Python, cada condicional se verifica a su vez. Tan pronto como se evalúa como VERDADERO
, ese bloque se ejecuta y se salta todo el resto. Sólo se ejecutará un bloque de una cadena if/else, y tal vez ninguno lo será. Se requiere el bloque controlado si
para iniciar la cadena, pero uno o más bloques controlados si
están controlados y el bloque final controlado por otro
son opcionales.
En R, si queremos incluir uno o más bloques else if
o un solo bloque else
, las palabras clave else if ()
o bien
deben aparecer en la misma línea que el corchete de cierre anterior. Esto difiere ligeramente de otros idiomas y es resultado de la forma en que el intérprete R analiza el código.
Generación de Datos Aleatorios Truncados, Parte II
Uno de los problemas con nuestra función rnorm_trunc ()
es que si el rango deseado es pequeño, aún podría requerir muchos esfuerzos de remuestreo para producir un resultado. Por ejemplo, llamar a sample_trunc <- rnorm_trunc (15, 15.01, 1000, 20, 10)
tardará mucho en terminar, ya que es raro generar aleatoriamente valores entre 15 y 15.01 a partir de la distribución dada. En cambio, lo que nos podría gustar es que la función se dé por vencida después de algún número de remuestreos (digamos, 100,000) y devuelva un vector que contenga NA
, indicando un cálculo fallido. Posteriormente, podemos verificar el resultado devuelto con is.na ()
.
El ejemplo hasta ahora ilustra algunas cosas diferentes:
NA
puede actuar como marcador de posición en nuestras propias funciones, al igual quemean ()
devolveráNA
si alguno de los elementos de entrada esNA
en sí mismo.- Las declaraciones IF pueden usar
else si
yelse
, pero no tienen que hacerlo. - Las funciones pueden regresar desde más de un punto; cuando esto sucede, la ejecución de la función se detiene incluso si está dentro de un bucle. [1]
- Los bloques de código, como los utilizados por los bucles while, las instrucciones if y las funciones, pueden anidarse. Es importante aumentar el nivel de sangría para todas las líneas dentro de cada bloque anidado; de lo contrario, el código sería difícil de leer.
For-Loops
For-loops son la última estructura de flujo de control que estudiaremos para R. Al igual que los bucles while, repiten un bloque de código. For-loops, sin embargo, repiten un bloque de código una vez por cada elemento de un vector (o lista), estableciendo un nombre de variable dado a cada elemento del vector (o lista) a su vez.
Si bien los bucles for son importantes en muchos otros lenguajes, no son tan cruciales en R. La razón es que muchas operaciones están vectorizadas, y funciones como lapply ()
y do ()
proporcionan el cálculo repetido de una manera completamente diferente.
Hay un poco de historia que sostiene que los bucles for son más lentos en R que en otros idiomas. Una forma de determinar si esto es cierto (y en qué medida) es escribir un bucle rápido que itera un millón de veces, tal vez imprimiendo en cada iteración mil. (Logramos esto manteniendo un contador de bucle y usando el operador de módulo%%
para verificar si el resto del contador
dividido por 1000
es 0
.)
Cuando ejecutamos esta operación nos encontramos con que, aunque no del todo instantáneo, este bucle no tarda en absoluto. Aquí hay una comparación rápida que mide el tiempo para ejecutar código similar para algunos idiomas diferentes (sin imprimir, lo que a su vez lleva algún tiempo) en una MacBook Pro 2013.
Lenguaje | Loop 1 Millón | Loop 100 Millones |
---|---|---|
R (versión 3.1) | 0.39 segundos | ~30 segundos |
Python (versión 3.2) | 0.16 segundos | ~12 segundos |
Perl (versión 5.16) | 0.14 segundos | ~13 segundos |
C (g++ versión 4.2.1) | 0.0006 segundos | 0.2 segundos |
Si bien R es el más lento del grupo, es “meramente” el doble de lento que los otros lenguajes interpretados, Python y Perl.
El problema con R no es que los bucles for sean lentos, sino que uno de los usos más comunes para ellos, alargar iterativamente un vector con c ()
o un marco de datos con rbind ()
, es muy lento. Si quisiéramos ilustrar este problema, podríamos modificar el bucle anterior de tal manera que, en lugar de agregar uno a contador
, aumentemos la longitud del vector contador
en uno con contador <- c (contador, 1)
. También modificaremos la impresión para reflejar el interés en la longitud del vector contador en lugar de su contenido.
En este caso, encontramos que el bucle comienza tan rápido como antes, pero a medida que el vector contador
se alarga, el tiempo entre impresiones crece. Y sigue creciendo y creciendo, porque el tiempo que lleva hacer una sola iteración del bucle depende de la longitud del vector contador
(que está creciendo).
Para entender por qué este es el caso, tenemos que inspeccionar el contador de línea <- c (contador, 1)
, que agrega un nuevo elemento al vector contador
. Consideremos un conjunto relacionado de líneas, en donde se concatenan dos vectores para producir un tercero.
En lo anterior, los nombres
serán el vector esperado de cuatro elementos, pero los nombres_nombres y
no han sido eliminados o alterados por esta operación, persisten y aún pueden imprimirse o accederse de otra manera más tarde. Para eliminar o alterar elementos, el intérprete R copia la información de cada vector más pequeño en un nuevo vector que está asociado con los apellidos
nombres
de las variables.
Ahora, podemos volver al contador <- c (contador, 1)
. El lado derecho copia la información de ambas entradas para producir un nuevo vector; éste se asigna luego al contador
de variables. Hace poca diferencia para el intérprete que el nombre de la variable se esté reutilizando y el vector original ya no será accesible: la función c ()
(casi) siempre crea un nuevo vector en la RAM. La cantidad de tiempo que lleva hacer esta copia crece así junto con la longitud del vector contador
.
La cantidad total de tiempo que se tarda en agregar n elementos a un vector en un bucle for de esta manera es aproximadamente , lo que significa que el tiempo que se tarda en hacer crecer una lista de n elementos crece cuadráticamente en su longitud final! Este problema se agrava cuando se usa rbind ()
dentro de un for-loop para hacer crecer una trama de datos fila por fila (como en algo así como df <- rbind (df, c (val1, val2, val3)))
, ya que las columnas de marcos de datos suelen ser vectores, haciendo de rbind ()
una aplicación repetida de c ()
.
Una solución a este problema en R es “preasignar” un vector (o marco de datos) del tamaño apropiado, y usar el reemplazo para asignar a los elementos en orden usando un bucle cuidadosamente construido.
(Aquí simplemente estamos colocando valores de 1
en el vector, pero ciertamente son posibles ejemplos más sofisticados). Este código se ejecuta mucho más rápido pero tiene la desventaja de requerir que el programador sepa de antemano qué tan grande tendrá que ser el conjunto de datos. [2]
¿Significa esto que nunca debemos usar bucles en R? ¡Desde luego que no! A veces, el bucle es un ajuste natural para un problema, especialmente cuando no implica el crecimiento dinámico de un vector, una lista o un marco de datos.
Ejercicios
- Usando un bucle for y un vector preasignado, generar un vector de los primeros 100 números de Fibonacci, 1, 1, 2, 3, 5, 8, y así sucesivamente (los dos primeros números de Fibonacci son 1; de lo contrario, cada uno es la suma de los dos anteriores).
- Una línea como
resultado <- readline (prompt = “¿Cuál es tu nombre?”)
solicitará al usuario que ingrese su nombre y almacenará el resultado como un vector de caracteres en elresultado
. Usando esto, un bucle while y una declaración if, podemos crear un simple juego de adivinación de números.Comience estableciendo la
entrada <- 0
yrand
a un entero aleatorio entre 1 y 100 conrand <- muestra (seq (1,100), tamaño = 1)
. A continuación, ¡mientras que laentrada! = rand
: Lee una conjetura del usuario y conviértela en un entero, almacenando el resultado enentrada
. Si laentrada < rand
, imprima“¡Mayor!”
, de lo contrario siingresa > rand
, imprima“¡Inferior!”
, y de lo contrario reportar“¡Lo tienes!”
. - La prueba t de Student emparejada evalúa si dos vectores revelan una diferencia significativa en cuanto a elementos. Por ejemplo, podríamos tener una lista de puntuaciones de los estudiantes antes y después de una sesión de entrenamiento. Pero la prueba t pareada solo debe usarse cuando las diferencias (que en este caso podrían calcularse con
puntos_después - puntuaciones
) están normalmente distribuidas. Si no lo son, una mejor prueba es la prueba de rango firmado de Wilcoxon:
(Mientras que la prueba t verifica para determinar si la diferencia de medias es significativamente diferente de0
, la prueba de rango firmado de Wilcoxon verifica para determinar si la diferencia de mediana es significativamente diferente de0
.)El proceso de determinar si los datos se distribuyen normalmente no es fácil. Pero una función conocida como la prueba de Shapiro está disponible en R, y prueba la hipótesis nula de que un vector numérico no se distribuye normalmente. Por lo tanto, el valor p es pequeño cuando los datos no son normales. La desventaja de una prueba de Shapiro es que ésta y pruebas similares tienden a ser sobresensibles a la no normalidad cuando se dan muestras grandes. La función
shapiro.test ()
se puede explorar ejecutandoprint (shapiro.test (rnorm (100, mean = 10, sd = 4)))
eprint (shapiro.test (rexp (100, rate = 2.0)))
.Escribe una función llamada
wilcox_or_ttest ()
que toma dos vectores numéricos de igual longitud como parámetros y devuelve un valor p. Si elshapiro.test ()
reporta un valor p de menos de 0.05 sobre la diferencia de los vectores, el valor p devuelto debería ser el resultado de una prueba con signo de rango de Wilcoxon. De lo contrario, el valor p devuelto debería ser el resultado de unt.test ()
. La función también debe imprimir información sobre qué prueba se está ejecutando. Pon a prueba tu función con datos aleatorios generados a partir derexp ()
yrnorm ()
.
Una extensión funcional
Habiendo revisado las estructuras procesales de control-flujo si
, while
y for
, vamos a ampliar nuestro ejemplo de muestreo aleatorio truncado para explorar más características “funcionales” de R y cómo podrían ser utilizadas. La función que diseñamos, rnorm_trunc ()
, devuelve una muestra aleatoria que normalmente se distribuye pero se limita a un rango dado a través del remuestreo. La distribución de muestreo original se especifica por los parámetros mean =
y sd =
, los cuales se pasan a la llamada a rnorm ()
dentro de rnorm_trunc ()
.
¿Y si quisiéramos hacer lo mismo, pero para rexp ()
, qué muestras de una distribución exponencial tomando un parámetro rate =
?
La distribución normalmente va de 0 a infinito, pero es posible que queramos remuestrear, digamos, de 1 a 4.
Una posibilidad sería escribir una función rexp_trunc ()
que opere de manera similar a la función rnorm_trunc ()
, con cambios específicos para el muestreo a partir de la distribución exponencial.
Las dos funciones rnorm_trunc ()
y rexp_trunc ()
son increíblemente similares, solo difieren en la función de muestreo utilizada y los parámetros que se les pasan. ¿Podemos escribir una sola función para hacer ambos trabajos? Podemos, si recordamos dos hechos importantes que hemos aprendido sobre funciones y parámetros en R.
- Funciones como
rnorm ()
yrexp ()
son un tipo de datos como cualquier otro y así se pueden pasar como parámetros a otras funciones (como en la estrategia split-apply-combine). - El parámetro especial
...
“recopila” parámetros para que las funciones puedan tomar parámetros arbitrarios.
Aquí, usaremos...
para recopilar un conjunto arbitrario de parámetros y pasarlos a llamadas a funciones internas. Al definir una función a tomar...
, por lo general se especifica último. Entonces, escribiremos una función llamada sample_trunc ()
que toma cinco parámetros:
- El límite inferior,
inferior
. - El límite superior,
superior
. - El tamaño de muestra a generar,
contar
. - La función a llamar para generar muestras,
sample_func
. - Parámetros adicionales para pasar a
sample_func
,...
.
Podemos llamar a nuestra función sample_trunc ()
usando cualquier número de funciones de muestreo. Hemos visto rnorm ()
, que toma los parámetros mean =
y sd =
, y rexp ()
, que toma un parámetro rate =
, pero hay muchos otros, como dpois ()
, que genera distribuciones de Poisson y toma un parámetro lambda =
.
En el primer ejemplo anterior, la media = 20, sd = 10
se coteja en...
en la llamada a sample_trunc ()
, como es rate = 1.5
en el segundo ejemplo y lambda = 2
en el tercer ejemplo.
Ejercicios
- Como se discutió en un ejercicio anterior, las funciones
t.test ()
ywilcox.test ()
toman dos vectores numéricos como sus primeros parámetros, y devuelven una lista con una entrada$p.value
. Escribe una funciónpval_from_test ()
que tome cuatro parámetros: los dos primeros deben ser dos vectores numéricos (como en las funcionest.test ()
ywilcox.test ()
), el tercero debe ser una función de prueba (ya seat.test
owilcox.test
), y la cuarta debe ser cualquier parámetro opcional para transmitir (...
). Se debe devolver el valor p de la ejecución de prueba.Entonces deberíamos poder ejecutar la prueba así: Los cuatro valores
pval1
,pval2
,pval3
ypval4
deben contener valores p simples.
- Algunos programadores creen que las funciones deben tener solo un punto de retorno, y siempre debe ir al final de una función. Esto se puede lograr inicializando la variable para que regrese al inicio de la función, modificándola según sea necesario en el cuerpo, y finalmente regresando al final. Sin embargo, este patrón es más difícil de implementar en este tipo de función, donde la ejecución de la función necesita romperse de un bucle.
- Otra forma de alargar un vector o lista en R es asignarle al índice justo al final del mismo, como en
counter [length (counter) + 1] <- val
. Uno podría estar tentado a usar tal construcción en un bucle for, pensando que el resultado será que el vectorcontador
se modifique “en su lugar” y evitando la costosa copia. Uno estaría equivocado, al menos a partir del momento de escribir esto, porque esta línea es en realidad azúcar sintáctica para una llamada de función y asignación:counter <- `[<-` (counter, length (counter) + 1, val),
que sufre el mismo problema que la llamada a la funciónc ()
.