# Herramientas de programación para la producción y difusión de estadísticas socioeconómicas - ASET

### Clase 5 - Funciones y Loops

# Introducción

En este material veremos algunas herramientas un poco más avanzadas que sirven para escalar procedimientos que vimos anteriormente y/o para automatizar (parte de) nuestro trabajo. La idea es un primer acercamiento a herramientas que evitan tener que constantemente copiar nuestro código y modificar algunos paramentos o variables, cada vez que queramos realizar procedimientos parecidos.

Trabajaremos con múltiples bases de la EPH - Argentina. Para no hacer demasiado pesada la descarga nos limitamos a 3 años (2021 a 2023). Importaremos también un archivo con una seria del Índices de Precios al Consumidor expresada en términos trimestrales descargado de CEPED-DATA

    library(tidyverse)
    library(openxlsx)
    library(eph)
    bases_2021_2023 <- eph::get_microdata(year = 2021:2023,period =1:4)

    probando la URL 'https://www.indec.gob.ar/ftp/cuadros/menusuperior/eph/EPH_usu_2_Trim_2023_txt.zip'
    Content type 'application/x-zip-compressed' length 2987879 bytes (2.8 MB)
    downloaded 2.8 MB

    probando la URL 'https://www.indec.gob.ar/ftp/cuadros/menusuperior/eph/EPH_usu_3_Trim_2023_txt.zip'
    Content type 'application/x-zip-compressed' length 2971224 bytes (2.8 MB)
    downloaded 2.8 MB

    probando la URL 'https://www.indec.gob.ar/ftp/cuadros/menusuperior/eph/EPH_usu_4_Trim_2023_txt.zip'
    Content type 'application/x-zip-compressed' length 2833417 bytes (2.7 MB)
    downloaded 2.7 MB

    ipc_trimestral <- read.xlsx(xlsxFile = "bases/ipc_trimestral.xlsx")

Si observamos rápidamente el objeto `ipc_trimestral` observaremos que la variable anio_trim contiene la identificación unificada del Año y Trimestre. A su vez, la base del índice está ubicada en el año 2017 (es decir, que el valor de referencia 100 está establecido como el promedio de los trimestres del 2017)

    glimpse(ipc_trimestral)

    Rows: 82
    Columns: 5
    $ Período   <chr> "2003", "2003", "2004", "2004",~
    $ Trimestre <chr> "T3", "T4", "T1", "T2", "T3", "~
    $ Serie     <chr> "Trimestres_EPH_2017", "Trimest~
    $ ipc       <dbl> 6.508173, 6.564211, 6.624051, 6~
    $ anio_trim <chr> "2003T3", "2003T4", "2004T1", "~

Replicaremos esta variable en la base de la EPH, para luego hacer un join de una estimación del salario promedio por trimestre con la inormación del índice de precios.

    bases_2021_2023 <- bases_2021_2023 %>%
      mutate(anio_trim  = paste0(ANO4,"T",TRIMESTRE))

    base_salarios <- bases_2021_2023 %>%
      filter(ESTADO == 1, CAT_OCUP ==3,P21>0) %>%
      group_by(anio_trim) %>%
      summarise(salario_promedio = weighted.mean(P21,PONDIIO)) %>%
      left_join(ipc_trimestral)

    Joining with `by = join_by(anio_trim)`

    base_salarios

# Estructuras Condicionales

## if

La función if se utilizará para correr código condicionalmente.

Su funcionamiento es el siguiente:
`if(condicion){codigo a ejecutar si se cumple la condición}`

    if( 2+2 == 4){
      print("Todo marcha bien")
    }

    [1] "Todo marcha bien"

    if( 2+2 == 148.24){
      print("R, tenemos un problema")
    }

## if + else

La función if también puede utilizarse para indicar la ejecución de uno u otro fragmentos de código según se cumpla o no una condición. Para ello, agregamos `else{}` y el código a ejecutar en caso de NO cumplimiento de la condición, de la siguiente manera:

`if(condicion){codigo a ejecutar si se cumple la condición} else {codigo a ejecutar si NO se cumple la condición}`

Veamos un ejemplo. Queremos que nuestro código chequee automáticamente si la base de salarios generada tiene efectivamente la cantidad de trimestres con la que decidimos trabajar (en nuestro caso, 12). Caso contrario, queremos que nos indique que hubo algun problema.

    if (nrow(base_salarios) == 12) {
        "Cantidad de trimestres ok"
    } else{
      paste("Chequear fallas en la descarga" )
    }

    [1] "Cantidad de trimestres ok"

# Funciones propias

La creación de funciones propias nos permite automatizar todas aquellas partes del código que se repiten mucho. Una vez diseñadas, funcionan igual que cualquier comando.

Arranquemos con un ejemplo básico. Podemos crear una función que al indicar dos valores, devuelva la suma de ellos

    suma <- function(valor1, valor2) {
      valor1+valor2
    }

    suma

    function(valor1, valor2) {
      valor1+valor2
    }

En el paso anterior simplemente creamos una función que se guardará en nuestro ambiente como cualquier otro objeto. Para utilizar la función, debemos hacerlo como con cualquier otra, específicandole los argumentos que queremos que tome.

    suma(valor1 = 5,valor2 = 6)

    [1] 11

Obviamente las funciones no son sólo para variables numéricas. Por ejemplo, podemos diseñar una función que “espere” un string como input.

    saludo <- function(nombre) {
      print(paste0("Hola, ",nombre,". ¿Qué te trae por acá?"))

    }

Ejecutamos la función.

    saludo(nombre = "Juan")

    [1] "Hola, Juan. ¿Qué te trae por acá?"

¿Qué pasa si llamamos a la función sin especificar un valor para el parámetro nombre?

    saludo()

    Error in paste0("Hola, ", nombre, ". ¿Qué te trae por acá?") :
      el argumento "nombre" está ausente, sin valor por omisión

Si quisieramos podemos asignar un valor por default para los parametros de nuestras funciones. Este es el valor que va a tomar el parametro en caso de que el usuario no lo defina al ejecutar la función.

    saludo_def <- function(nombre = "humano") {
      print(paste0("Hola, ",nombre,". ¿Qué te trae por acá?"))

    }

Veamos que pasa al ejecutar la función sin completar el parametro:

    saludo_def()

    [1] "Hola, humano. ¿Qué te trae por acá?"

Las funciones que creamos nosotros permanecen en el ambiente de R temporariamente. Cuando removemos los objetos del ambiente, la función deja de existir. Por ende, debemos incorporarla en cada uno de los scripts en la cual la necesitemos. Una buena práctica, es incorporar nuestras funciones útiles al comienzo de cada script junto a la carga de las librerías.

Vale mencionar que lo que ocurre en una función, queda en la función excepto que explícitamente estemos devolviendo el resultado “en consola”. Podemos imprimir pasos intermedios para que se visualicen al correr la función incluyendo el comando `print()`.

Las funciones siempre devuelven el último objeto que se crea en ellas, o si explicitamente se utiliza el comando `return()`.

Para ver esto, voy a diseñar una función que calcule el ratio entre el valor máximo de un vector y el valor mínimo:

    calcula_ratio <- function(vector) {

    vector.max  <-   max(vector)
    vector.min  <-   min(vector)

    return(vector.max/vector.min)
    }

Al correr una función definida de esta manera, en el ambiente solo se conserva en nuestro objeto ratio el valor devuelto con `return()`. vector.max y vector.min no quedan almacenados.

    ratio <- calcula_ratio(vector = c(1,2,3,4))

    ratio

    [1] 4

Ahora bien, cuando pensamos en funciones más complejas cuya operatoria no es tan obvia, pueden agregarse chequeos, y operatorias condicionales con alguna advertencia para que el usuario pueda comprender un error, o tenga mayor noción de qué está haciendo la función.

Que pasa si el usuario de la función no pasa un vector numérico?

    calcula_ratio(vector = c(1,2,3,4,"H"))

    Error in vector.max/vector.min :
      argumento no-numérico para operador binario

En este caso, podríamos haber seteado un mensaje más amigable que permita al usuario comprender su error. Lo podemos hacer con el paquete assertthat

    calcula_ratio <- function(vector) {

    assertthat::assert_that(is.numeric(vector),
                            msg = "Ingresá un vector numérico!")

    vector.max  <-   max(vector)
    vector.min  <-   min(vector)

    return(vector.max/vector.min)
    }

    calcula_ratio(vector = c(1,2,3,4,"H"))

    Error: Ingresá un vector numérico!

Una vuelta de tuerca más! Qué pasa si dentro del vector tenemos un 0?

    calcula_ratio(vector = c(1,2,3,4,0))

    [1] Inf

Supongamos que queremos que en ese caso nuestra función descarte ese valor, calcule igualmente el ratio, pero dando aviso de ello

    calcula_ratio <- function(vector) {

    assertthat::assert_that(is.numeric(vector),
                            msg = "Ingresa un vector numérico")

    if(any(vector==0)){

      warning("Mensaje: Hay un cero en tu vector, no lo tomo en cuenta para el calculo")
      vector <- vector[vector!=0]

    }
    vector.max  <-   max(vector)
    vector.min  <-   min(vector)

    return(vector.max/vector.min)
    }

Veamos que pasa

    calcula_ratio(vector = c(1,0,3,4))

    Warning: Mensaje: Hay un cero en tu vector, no lo tomo en cuenta para el calculo

    [1] 4

## Deflactando series

Veamos un ejemplo concerto de una función que nos permita transformar una serie nominal (como los salarios estimados en EPH) a valores reales. La idea de la función es pasar como imputs:

- Un dataframe que contenga

  - Una variable expresada en términos nominales

  - Un índice de precios

  - Una columna que identifique el período

- Un período hacia el cual reexpresar los valores nominales (es decir “¿a precios de que año queremos transformar los datos?”)

    deflacta_series <- function(df,valor_nominal,indice_precios,col_periodo,base_elegida){

    indice_base <- df[indice_precios]/df[indice_precios][df[col_periodo] == base_elegida]*100

    valor_real <- df[valor_nominal]/indice_base*100

    df["valor_real"]  <- valor_real
      return(df)
    }

Ejecutamos la función, específicando las columnas correspondientes (en este caso, la función está diseñada para recibir las columnas como strings) y el período más reciente (2023T4) como base elegida.

    deflacta_series(df = base_salarios,
                    valor_nominal = "salario_promedio",
                    col_periodo = "anio_trim",
                    indice_precios = "ipc",
                    base_elegida = "2023T4")

    NA

# Loops

Un loop es una estructura de código que nos permite aplicar iterativamente un mismo conjunto de comandos, variando el valor de una variable. Veremos aquí únicamente el loop for, cuya sintaxis es la siguiente:

`for (variable in vector) {conjunto de procedimientos a realizar}`

    for(i in 1:10){
       print(i^2)

    }

    [1] 1
    [1] 4
    [1] 9
    [1] 16
    [1] 25
    [1] 36
    [1] 49
    [1] 64
    [1] 81
    [1] 100

Esto se lee como : “Recorre cada uno de los valores (i) del vector numérico 1 a 10, y para cada uno de ellos imprimí el cuadrado (i^2)”.
Uno puede especificar la palabra que desee que tomé cada uno de los valores que debe tomar. En el ejemplo anterior fue i, pero bien podría ser la “Valores”

    for(Valores in 1:10){
       print(Valores^2)

    }

    [1] 1
    [1] 4
    [1] 9
    [1] 16
    [1] 25
    [1] 36
    [1] 49
    [1] 64
    [1] 81
    [1] 100

Un loop puede iterar sobre cualquier tipo de vector, independientemente de lo que contenga.

Una utilización típica de los loops: Recorrer las categorías de una variable y hacer el mismo gráfico para cada una de ellas.

Veamos un ejemplo. Vamos a armar un histograma de la edad, desagregando por la variable de calificación del puesto. Para simplificar, nos quedamos con un solo trimestre, y usamos el paquete eph para agregar la variable de calificación del puesto.

    base_con_cno <- bases_2021_2023 %>%
      filter(anio_trim == "2023T4") %>%
      eph::organize_cno()

Veamos que valores toma la variable de calificación del puesto…

    unique(base_con_cno$CALIFICACION)

    [1] "Operativos"        NA
    [3] "No calificados"    "Técnicos"
    [5] "Profesionales"     "falta informacion"
    [7] "Ns.Nc"             "otro"

Recomendación: Antes de hacer un loop (ya sea para gráficos u otros procesamientos), prueben primero obtener lo que quieren para una categoría, y luego lo extienden hacia el resto. En este sentido, armemos el gráfico para la categoría “Profesionales”

    base.profesionales <- base_con_cno %>%
        filter(CALIFICACION  == "Profesionales")

    hist(x = base.profesionales$CH06,
         main = "Profesionales",
         xlab = "Edad",
         xlim = c(0,100))

    NA
    NA

Una vez que tenemos el gráfico que nos gusta, ahora si armemos el loop.

¿Cómo operar con un loop sobre la base de datos? Lo más frecuente es que querramos realizar un mismo procedimiento sobre distintos valores de cierta variable de nuestra base. Para eso, puede ser util crear anticipadamente un vector que indique las cateogrías sobre las que queremos iterar. Ese vector, aqui llamado `vector_calificaciones` será el que utilizaremos para iterar (desechamos los valores NA, Ns.Nc, falta información)

    vector_calificaciones <- unique(base_salarios_calificacion$CALIFICACION)[c(1,3,4,5)]

    Error in unique(base_salarios_calificacion$CALIFICACION) :
      objeto 'base_salarios_calificacion' no encontrado

Miren las diferecias entre el código del gráfico anterior y el siguiente:

    for(categoria in vector_calificaciones){

    base.filtrada  <- base_salarios_calificacion %>%
        filter(CALIFICACION  == categoria)

    hist(x = base.filtrada$CH06,
         main = categoria,
         xlab = "Edad",
         xlim = c(0,100))

    }

# Exportar resultados a Excel

La función write.xlsx de la libreria openxlsx nos permite exportar uno o más dataframes a una o más pestañas de un archivo de Excel. Hay una infinidad de variantes para hacer la exportación. Podemos destacar algunas funcionalidades para:
- Agregar títulos a los cuadros
- Apilar múltiples dataframes en una misma solapa del excel
- Agregar colores y/o bordes a los cuadros
- Cambiar la fuente del texto exportado

Todas estas variantes, con ejemplos, se pueden consultar acá.

Supongamos que queremos un cuadro que nos muestre la cantidad de casos, según `clasificacion_resumen` y `provincia`.

    cuadro_por_calificaciones<- base_con_cno %>%
      group_by(CALIFICACION,CH04) %>%
      summarise(casos = sum(PONDERA,na.rm = T))

    `summarise()` has grouped output by 'CALIFICACION'. You can override using the `.groups` argument.

La forma más simple, como ya vimos sólo requiere indicar el dataframe a exportar, y la ruta completa, con extensión del archivo incluido.

    dir.create("Resultados")#Si quiero crear una nueva carpeta

    openxlsx::write.xlsx(x = cuadro_por_provincia,
                         file ="Resultados/miexportacion.xlsx")

La alternativa que me permite exportar datos con más chiches, tiene la siguiente forma:

-`createWorkbook()`: Creo un objeto Workbook en mi ambiente
-`addWorksheet()`: Le agrego a ese objeto una pestaña/hoja de excel
-`writeData()`: Le agrego la data que quiera a una determinada pestaña del excel (creada previamente). Acá puedo ajustar todo tipo de parámetros estéticos
-`saveWorkbook()`: Guardo el excel creado (lo creará en mi disco de la computadora)

    wb <- createWorkbook()

    addWorksheet(wb,sheetName =  "Casos Provincias")

    writeData(wb,
              sheet="Casos Provincias",
              x = "Cuadro 1. Cantidad de Casos por provincia", #Lo uso para crear un titulo
              startRow = 1)

    writeData(wb,
              sheet="Casos Provincias",
              x = cuadro_por_provincia,   #Acá va la base de datos
              borders = "rows" ,#Parametro de Estilo
              borderStyle = "dashed", #Parametro de Estilo
              startRow = 3) #Dejo un espacio entre titulo y datos

    saveWorkbook(wb, "Resultados/miexportacion_piola.xlsx")

# Loops + Exportacion de Excels

Un ejemplo típico es la necesidad de exportar a varias pestañas de un excel cada data.frame que surge como resultado de un loop. Iteremos sobre el cuadro anterior, para exportar en una pestaña distinta los resultados para cada provincia:

    provincias <- unique(cuadro_por_provincia$residencia_provincia_nombre)

    wb <- createWorkbook() #Creo nuevo libro

    for (prov in provincias) {

      #Este cuadro ira cambiando de provincia a medida que avanza el loop
    cuadro.prov <- cuadro_por_provincia %>%
        filter(residencia_provincia_nombre == prov)

    addWorksheet(wb, sheetName =  prov) #Al poner "prov" estoy agregando una pestaña con cada nombre de provincia

    writeData(wb,                #Especifico mi base de datos
              sheet=prov,
              x = cuadro.prov,
              startRow = 1)

    }

    saveWorkbook(wb, "Resultados/miexportacion_loop.xlsx")

[encoded data, 20740 characters]