Saltar al contenido principal
LibreTexts Español

3.10: Programación Procesal

  • Page ID
    55107
  • \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \) \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)\(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\) \(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\)\(\newcommand{\AA}{\unicode[.8,0]{x212B}}\)

    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.

    III.10_1_R_216_MIENTRA-1

    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!” .

    III.10_2_R_217_While_1_out

    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:

    III.10_3_R_218_While_2_out

    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.

    III.10_4_R_219_RNORM_HIST

    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.)

    III.10_5_rnorm_muestra_hist

    ¿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.

    III.10_6_R_220_RNORM_TRUNC_1

    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.

    III.10_7_R_220_RNORM_TRUNC_2

    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.

    III.10_8_R_221_RNORM_TRUNC_2

    Vamos a probarlo:

    III.10_9_R_222_RNORM_TRUNC_2_Run

    El histograma trazado refleja la naturaleza truncada del conjunto de datos:

    III.10_10_RNORM_MUESTRA_HIST_TRUNC_OUT

    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.

    III.10_11_R_223_IF_Básica

    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 ().

    III.10_12_R_224_RNORM_TRUNC_NA

    El ejemplo hasta ahora ilustra algunas cosas diferentes:

    1. NA puede actuar como marcador de posición en nuestras propias funciones, al igual que mean () devolverá NA si alguno de los elementos de entrada es NA en sí mismo.
    2. Las declaraciones IF pueden usar else si y else, pero no tienen que hacerlo.
    3. 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]
    4. 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.

    III.10_13_R_225_for_loop_basic
    III.10_14_R_226_for_loop_basic_out

    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.)

    III.10_15_R_227_por_one_million

    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.

    III.10_16_R_228_for_one_millones_append

    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.

    III.10_17_R_229_Concatenate_lento

    En lo anterior, los nombres serán el vector esperado de cuatro elementos, pero los nombres_nombres y apellidos 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 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.

    alt

    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.

    III.10_19_R_230_Pre_alloc

    (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

    1. 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).
    2. 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 el resultado. 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 y rand a un entero aleatorio entre 1 y 100 con rand <- muestra (seq (1,100), tamaño = 1). A continuación, ¡mientras que la entrada! = rand: Lee una conjetura del usuario y conviértela en un entero, almacenando el resultado en entrada. Si la entrada < rand, imprima “¡Mayor!” , de lo contrario si ingresa > rand, imprima “¡Inferior!” , y de lo contrario reportar “¡Lo tienes!” .

    3. 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. III.10_20_R_230_2_TTEST_PAREADOPero 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:
      III.10_21_R_230_3_Wilcoxon_pareado (Mientras que la prueba t verifica para determinar si la diferencia de medias es significativamente diferente de 0, la prueba de rango firmado de Wilcoxon verifica para determinar si la diferencia de mediana es significativamente diferente de 0.)

      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 ejecutando print (shapiro.test (rnorm (100, mean = 10, sd = 4))) e print (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 el shapiro.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 un t.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 de rexp () y rnorm ().

    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 ().

    III.10_22_R_230_2_RNORM_TRUNC_REVISIÓN

    ¿Y si quisiéramos hacer lo mismo, pero para rexp (), qué muestras de una distribución exponencial tomando un parámetro rate =?

    III.10_23_R_231_REXP

    La distribución normalmente va de 0 a infinito, pero es posible que queramos remuestrear, digamos, de 1 a 4.

    III.10_24_REXP_DIST

    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.

    III.10_25_R_232_REXP_TRUNC

    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.

    1. Funciones como rnorm () y rexp () 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).
    2. 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:

    1. El límite inferior, inferior.
    2. El límite superior, superior.
    3. El tamaño de muestra a generar, contar.
    4. La función a llamar para generar muestras, sample_func.
    5. Parámetros adicionales para pasar a sample_func,... .
    III.10_26_R_233_Sample_trunc

    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 =.

    III.10_27_R_234_Sample_trunc_use

    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.

    III.10_28_muestra_trunco_hists

    Ejercicios

    1. Como se discutió en un ejercicio anterior, las funciones t.test () y wilcox.test () toman dos vectores numéricos como sus primeros parámetros, y devuelven una lista con una entrada $p.value. Escribe una función pval_from_test () que tome cuatro parámetros: los dos primeros deben ser dos vectores numéricos (como en las funciones t.test () y wilcox.test ()), el tercero debe ser una función de prueba (ya sea t.test o wilcox.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í:III.10_29_R_234_2_Funcional_TTEST_Wilcox_Test Los cuatro valores pval1, pval2, pval3 y pval4 deben contener valores p simples.


    1. 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.
    2. 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 vector contador 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ón c ().

    This page titled 3.10: Programación Procesal is shared under a CC BY-NC-SA license and was authored, remixed, and/or curated by Shawn T. O’Neil (OSU Press) .