[{"content":"Para adentrarte en el mundo del análisis de datos, la visualización de datos, y la programación, el primer paso es instalar R, el lenguaje, y RStudio, la interfaz que nos va a permitir trabajar con el lenguaje.\n1. Descargar e instalar R R es el lenguaje de programación y análisis estadístico base, el cual se caracteriza por operar mediante paquetes o librerías que expanden sus prestaciones.\nEl siguiente enlace es del sitio oficial de R, donde pueden descargarlo para el sistema operativo que utilicen: https://cran.r-project.org\nElige la descarga correspondiente a tu sistema operativo, e instálalo. Si usas Windows, es probable que tengas que instalar otro software llamado Rtools, cuyo enlace aparece en el mismo enlace anterior cuando eliges Windows.\nR y RStudio R funciona a través de la línea de comandos (la línea de comandos es el nivel más bajo de interacción con un computador), lo que significa que hay que escribirle instrucciones exactas para que trabaje, y retorna resultados en forma de texto. Como esto resulta un poco tedioso, se suele utilizar R a través de un entorno de desarrollo, RStudio, que facilita su uso.\n2. Descargar e instalar RStudio RStudio es el entorno de desarrollo integrado (IDE) que facilita el trabajo con R. También funciona con Python, y también hay otras alternativas de IDE, pero RStudio es la más usada.\nDescargar e instalar RStudio para su sistema operativo: https://posit.co/download/rstudio-desktop/#download\nPrincipalmente, lo que hace RStudio es presentarnos una consola de R junto a otros paneles: el panel de visualización, donde exploramos los gráficos, la ventana de entorno (environment) donde vemos los elementos con los que estamos trabajando, y la ventana de script o source, donde podemos ir elaborando uno o varios documentos con instrucciones para R. Además de estos paneles, RStudio facilita tareas como la importación de datos, la instalación de paquetes, la gestión de cambios, y otros.\nTeniendo ambas cosas instaladas, podemos abrir RStudio y verificar está funcionando escribiendo en la consola: “2+2” (sin comillas), y presionamos enter. Si obtenemos una respuesta, significa que la instalación funciona.\n","date":"2024-11-07T00:00:00Z","excerpt":"Instrucciones básicas para que descargues e instales R y RStudio, dirigidas a personas sin conocimientos previos o principiantes. ¡Es tu primer paso al mundo de la programación!","href":"https://bastianoleah.netlify.app/blog/r_introduccion/instalar_r/","tags":"básico","title":"Primer paso: instalar R"},{"content":"¿Por qué usar R para analizar datos?\nAnalizar datos mediante lenguajes de programación (como R u otros) puede sonar complicado, pero trae muchos beneficios para tu análisis.\nUsabilidad Facilidad de uso: A diferencia de otros lenguajes de programación, R está diseñado por y para personas que no se especializan en computación ni informática, sino que se enfoca en profesionales de distintas áreas que requieren aplicar técnicas computacionales a sus disciplinas. Por lo tanto, suele ser más sencillo de usar que otros lenguajes, y por sobre todo más \u0026ldquo;legible\u0026rdquo; que otros lenguajes, en el sentido de que la sintaxis de R es fácilmente interpretable como instrucciones en lenguaje natural, una vez que entiendes las reglas básicas para escribirlo y leerlo. Por lo demás, cada función del lenguaje está ampliamente documentada, y existe una gran comunidad de usuarios y usuarios con mucha disposición para ayudar y enseñar. Análisis estadísticos complejos: R es una herramienta poderosa para realizar análisis estadísticos complejos, como aplicar modelos estadísticos a tus datos, herramientas de machine learning, o análisis estadísticos sofisticados. Muchas de estas herramientas vienen por defecto en R, y muchísimas otras son fácilmente instalables mediante paquetes. Visualizaciones de datos: R cuenta con una amplia variedad de herramientas para visualizar datos. El paquete {ggplot2} permite crear gráficos personalizables y complejos con una sintaxis clara y concisa, basada en capas. Puedes explorar visualmente tus datos en cuestión de segundos, o producir visualizaciones complejas y atractivas si lo requieres. Velocidad: Con R puedes procesar bases de datos de millones o miles de millones de filas. Existen múltiples estrategias para trabajar con grandes volúmenes de datos en R, y puedes elegir la que más te convenga en cada situación para encontrar el equilibrio entre desempeño y conveniencia que desees. Flujos de trabajo Tener todo tu proyecto a la vista: En vez de pensar el procesamiento de datos y la obtención de resultados como un proceso continuo o una sucesión de pasos que realizamos uno después del otro, desde los datos iniciales hasta el producto final, vemos el proceso como una iteración de pasos que van aumentando su complejidad en cada etapa, y donde se puede ir optimizando cada fase del mismo. No vas a tener que repetir cosas que ya has hecho: tendrás flujos de trabajo que puedes reutilizar con distintas fuentes de datos, distintas versiones de los datos, o variaciones en los cálculos. Esto te permite experimentar e iterar. Tus proyectos tendrán entradas y salidas claras. Podrás cambiar las entradas de datos sin tener que deshacer el trabajo que ya has hecho. Como vas procesando datos por pasos, siempre puedes cambiar un paso anterior y aplicar los cambios a los pasos siguientes, o hacer cambios en pasos intermedios y volver a obtener los nuevos resultados. Flexibilidad en tecnologías y métodos: un mismo proceso puede ser fácilmente actualizado para usar una tecnología que mejore su desempeño; por ejemplo, pasar de leer un Excel a consultar una base de datos relacional como SQL. Podrás basarte en el progreso de otros: La comunidad de estadísticos, analistas de datos, científicos de datos, geógrafos, y otros profesionales que usan R, usualmente comparten sus proyectos, herramientas y código. Puedes basarte en eso para aplicarlo a tus necesidades. Procesos reutilizables Si lo haces una vez, puedes hacerlo 100 veces:\nAutomatización de tareas repetitivas, por ejemplo, realizar un mismo análisis sobre cientos de archivos. Pero también puedes automatizar la generación de reportes, informes, visualizaciones y resultados. Si realizas tareas repetitivas, puedes especificar el procedimiento una sola vez, y aplicarlo cientos de veces.\nSi redactas reportes basados en datos que se actualizan periódicamente, o con información repetitiva, puedes programar un solo reporte que se actualice con información nueva de forma automática.\nSi llevas a cabo un mismo procedimiento a muchos datos similares, por ejemplo a muchas planillas de Excel, puedes programar un proceso una sola vez y aplicarlo a cientos o miles de casos, ahorrándote tiempo para generar tus resultados.\nSi trabajas con múltiples unidades de información, como pueden ser empresas, clientes, países o comunas, puedes definir los resultados que requieres una sola vez, y obtener el resultado para cada una de las unidades; por ejemplo, una tabla para cada empresa, un reporte para cada cliente, un gráfico para cada comuna, etc.\nEjemplos: Si generas un reporte para varias empresas, programas un solo reporte y así puedes obtener los reportes que desees. Si recibes unos mismos datos cada cierto tiempo, puedes tener un flujo de trabajo listo para que solo lo apliques sobre el archivo nuevo y obtengas los resultados que esperas. Si estás haciendo gráficos sobre datos de una comuna, y tienes cientos de comunas, basta hacer un solo gráfico para aplicarlos a todas las comunas. Si recibes decenas de planillas de datos de clientes y tienes que hacer cambios a cada uno, creas una función que haga los cambios y la aplicas a todas las planillas que quieras. Reproducibilidad El análisis por medio de programación garantiza que otros usuarios pueden revisar todos los pasos de tu análisis y obtener los mismos resultados que tú.\nEn R existe una tendencia a incentivar la creación de flujos de trabajo reproducibles; es decir, que tus scripts abarquen desde la primera carga de los datos crudos u originales, avanzando por los pasos del análisis, hasta llegar a los resultados. En otras palabras, puedes volver a repetir tu proyecto desde cero, y llegarás siempre a los mismos resultados. Esto ocurre porque los pasos que vamos realizando en R son no destructivos, dado que nunca modificamos los datos originales, sino que vamos generando nuevos datos en cada paso, y podemos repetir esos pasos o devolvernos en ellos y modificarlos/corregirlos. Esto se contrapone a otras formas de trabajo, como por ejemplo editar los datos directamente en Excel: una mala práctica que te hace alterar los datos originales, volviendo invisibles para los demás los pasos que hiciste para llegar a tus resultados. En cambio, en R uno va dejando un registro de cada paso realizado, lo que permite que mantengamos los datos originales intactos, que otros puedan verificar y revisar nuestro trabajo, y que nosotres podamos también volver atrás en los pasos y revisar nuestro avance o corregir errores. Creo que la mayoría de los beneficios de R provienen de su origen: R es un lenguaje desarrollado por estadísticos y profesionales de fuera de la informática, y es dirigido también a personas fuera de la informática, por lo que es un len guaje enfocado en ser fácil de leer y aprender, y en obtener resultados. El hecho de que se dirija a usuarios variados hace que su dificultad de uso sea menor, y también a que su comunidad de usuarios sea diversa.\n¿Necesitas aprender a explorar tus datos? ¿Tienes datos pero no les puedes sacar provecho? ¿Quieres presentar tus resultados de forma atractiva y/o interactiva? ¡Considera aprender R!\n","date":"2024-11-07T00:00:00Z","excerpt":"Analizar datos mediante lenguajes de programación (como R u otros) puede sonar complicado, pero trae muchos beneficios para tu análisis. Aquí te dejo algunas de las razones principales.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/por_que_r/","tags":"blog","title":"¿Por qué usar R?"},{"content":" Introducción a R Esta guía contiene instrucciones paso a paso para aprender los aspectos más básicos del lenguaje R. Va dirigida a personas sin ningún conocimiento previo de R, así que si quieres aprender R desde cero, ¡sigue este tutorial!\nSi sigues estas instrucciones de principio a fin, aprenderás a: trabajar con el programa RStudio, a gestionar tus scripts para el análisis, a realizar las primeras operaciones matemáticas, a comprender los distintos tipos de datos que existen en R, y a manejar las operaciones fundamentales para todo análisis posterior, ya sea básico o avanzado: objetos, comparaciones, asignaciones, vectores, y funciones.\nEntender estos aspectos básicos del lenguaje es fundamental para que, en un futuro cercano, puedas utilizar R para analizar datos, crear visualizaciones, generar reportes, desarrollar aplicaciones, y mucho más.\nLa idea de esta guía es aproximarnos a los principios más básicos de R, en partes pequeñas y con ejemplos simples, para poder entenderlos más fácilmente. Una vez que entendamos estos principios básicos, veremos que son aplicables a bases de datos de cientos o millones de observaciones simultáneamente, sin demasiada diferencia.\nInstalación de R Para instalar R, el lenguaje de programación, y además RStudio, el programa que te ayuda a trabajar con el lenguaje, dirígete a este post donde doy todas las instrucciones: Primer paso: Instalar R\nRStudio Una vez que hayamos instalado R y R Studio, abrimos RStudio.\nEl programa debía haberse más o menos así:\nVemos que el estudio tiene cuatro paneles. De izquierda a derecha, y de arriba a abajo, los paneles principales son:\nPanel de scripts: aquí tenemos nuestros archivos de texto con nuestro código. Podemos tener varias pestañas de distintos archivos de texto. Panel de entorno: acá veremos los objetos que vayamos creando, que pueden ser números, texto, tablas de datos, funciones, gráficos y otros. Panel de consola: en la consola se imprimen los resultados que arroja R a partir del código que ejecutamos en los scripts. También podemos ejecutar código directamente en la consola. Panel de archivos: en este panel podemos navegar los archivos y carpetas de nuestro proyecto y/o computador. Scripts Al trabajar en R, realizamos nuestros análisis y pruebas mediante scripts. Los scripts son archivos de texto que terminan en .R. En estos archivos de texto escribimos nuestro código, intentando tener una instrucción por línea. Dentro del script, ejecutamos las instrucciones línea por línea, poniendo el cursor de texto en la línea que deseamos ejecutar, y presionando el botón \u0026ldquo;Run\u0026rdquo; (arriba a la derecha en el panel de scripts), o bien, presionando las teclas comando + enter en Mac y control + enter en Windows.\nAl ejecutar una línea o instrucción, el comando se va a enviar a la consola, que es el panel de abajo donde se muestran las operaciones realizadas, y la consola va a retornar la respuesta o resultado.\nLa consola es la forma directa de interactuar con el lenguaje R. En ella, las instrucciones se escriben, un comando a la vez, y se presiona enter para enviar los comandos. La consola va a responder un resultado efímero, que sirve para que lo puedas revisar de forma rápida, pero no queda registrado en ninguna parte.\nSi escribimos nuestros comandos en scripts, los comandos van a quedar guardados en texto para que los puedas volver a ejecutar, revisar, etc. Una particularidad de los scripts, es que si una línea contiene un signo gato #, todo lo que esté después del gato se transformará un comentario. Esto significa que lo que esté después del signo no se ejecuta, y por lo tanto, puedes escribir lo que quieras. Es muy recomendable que a lo largo de tu script vayas dejando comentarios, iniciando una línea con el signo gato, para ir dejando registro de las cosas que estás haciendo, ideas al respecto, correcciones, preguntas, y aclaraciones, debido a que es muy fácil confundirse o olvidarse de lo que se está haciendo.\nPrimeras operaciones en R Ahora que tenemos nuestro R y RStudio preparados, empezaremos a utilizar R para entender sus aspectos más básicos. La idea es que vayas copiando estos códigos y los vayas ejecutando uno a uno, para ver y entender cómo funciona.\nTipos de datos Un dato es una unidad mínima de información. Puede ser un número, una palabra, o incluso puede ser nada. Esto es lo que se denomina el tipo de un dato, y el tipo de un dato determina, en cierto modo, lo que podemos hacer con un dato.\nLos tipos principales son:\nNuméricos:\n1 2 3.1 Con los datos numéricos se pueden realizar todo tipo de operaciones matemáticas y transformaciones numéricas.\nCaracter o texto:\n\u0026#34;hola\u0026#34; Los textos van entre comillas, y pueden contener cualquier caracter dentro.\nLógicos:\nTRUE FALSE Los datos de tipo lógico son una forma de representar una condición verdadera o falsa.\nEn el lenguaje R, todos los datos son también denominados objetos, porque son elementos que contienen algo dentro de sí, aunque en lo que hemos visto hasta ahora, el objeto y su contenido son equivalentes (1 es igual a 1).\nOperaciones matemáticas Quizás lo más básico que podemos hacer con un lenguaje de programación es sacar cálculos matemáticos. En R, esto es tan sencillo como escribir la expresión matemática, y teniendo el cursor de texto en la línea que queremos ejecutar, presionamos el botón Run (arriba a la derecha del script) o presionamos las teclas comando + enter.\nA lo largo de este tutorial, vamos a ir viendo bloques de código, seguidos de su resultado.\n2 + 2 # suma [1] 4 En estos dos bloques, el primero es la operación que hicimos, y el segundo es el resultado que entrega a la consola de R; en este caso, el resultado de 2 + 2.\n50 * 100 # multiplicación [1] 5000 4556 - 1000 # resta [1] 3556 6565 / 89 # división [1] 73.76404 Si escribimos estas operaciones en un script, podemos ejecutar las instrucciones en cualquier orden y cuando deseemos. Sólo debemos ir poniendo el cursor de texto en la línea que queramos y ejecutarla. O bien, podemos seleccionar varias líneas y ejecutarlas todas al mismo tiempo, teniendo cuidado de seleccionar las operaciones completas1.\nComo el tipo de los datos determina las operaciones que podemos realizar, veremos que hay ciertas operaciones que no podemos hacer conciertos datos. Si intentamos sumar dos datos de tipo caracter (texto), obtenemos un error, que dice que estamos usando un dato que no es numérico.\n\u0026#34;1\u0026#34; + \u0026#34;1\u0026#34; Error in \u0026quot;1\u0026quot; + \u0026quot;1\u0026quot;: non-numeric argument to binary operator Es importante saber con qué tipo de datos estamos trabajando, para poder saber las herramientas que tenemos de nuestra disposición, o bien, para planificar qué hacer para poder hacer lo que necesitamos con los datos que tenemos.\nCon esta primera operaciones matemáticas podemos entender que en un script escribimos instrucciones y las vamos ejecutando. Al ir ejecutando operaciones consecutivas vamos estructurando nuestro análisis.\nComparaciones Las comparaciones son una de las operaciones computacionales más básicas, pero a su vez pueden ser muy poderosas.\nEn una comparación, se utilizan símbolos específicos que realizan operaciones, llamados operadores, para pedirle a R que compare distintos datos.\nAl comparar datos, la respuesta obtenida va a ser si la comparación es igual o es distinta a lo establecido.\nPor ejemplo: ¿es 1 igual a 1?\n1 == 1 [1] TRUE La igualdad se escribe ==, y se usa para evaluar si dos cifras son iguales. En este caso, la comparación entre 1 y 1 retorna TRUE (verdadero), porque 1 es igual a 1.\n1 == 2 [1] FALSE 1 no es igual a 2, por lo que la comparación retorna FALSE (falso).\nLa operación inversa la igualdad es la desigualdad: evaluar si un dato es distinto de otro.\n40 != 30 [1] TRUE 40 es distinto a 30, por lo que la comparación retorna FALSE. Si evaluamos si dos cifras iguales son distintas, lógicamente la respuesta va a ser verdadero (TRUE).\n40 != 40 [1] FALSE Los operadores mayor que (\u0026gt;) y menor que (\u0026lt;) retorno si una cifra es mayor o menor, respectivamente.\n31 \u0026gt; 18 [1] TRUE 40 \u0026lt; 80 [1] TRUE También existen los operadores mayor o igual, y menor o igual:\n40 \u0026gt;= 40 [1] TRUE 50 \u0026gt;= 40 [1] TRUE 4 \u0026lt;= 5 [1] TRUE Asignaciones Anteriormente, mencionamos que los datos son objetos. Un objeto puede contener información de cualquier tipo, y lo relevante es que podemos crear todos los objetos que queramos para realizar nuestros análisis.\n¿Cómo crear un objeto? Creamos un objeto asignando un dato o valor a un nombre. Es decir, tenemos un dato, y queremos que ese dato esté contenido en un nuevo objeto, el cual tendrá un nombre específico.\nPara crear un objeto usamos el operador de asignación: \u0026lt;-\nAl crear un objeto nuevo, el objeto va a tener un nombre: el símbolo que va a contener el dato, y por medio del cual vamos a llamar el dato contenido en el objeto.\nTodos los objetos tienen un nombre, el cual puede ser solo una palabra sin separar por espacios, y puede contener símbolos como guión bajo, tildes, eñes y números.\nCrear objeto:\naño \u0026lt;- 1993 De este modo, podemos guardar un dato en un objeto para usarlo más adelante.\nPara ver los contenidos del objeto, simplemente lo llamamos por su nombre:\naño [1] 1993 Al crear un objeto, aparece en el panel de entorno o Environment, en el panel superior derecho de RStudio.\nPodemos usar los objetos para realizar operaciones matemáticas, porque llamar a un objeto, es reutiliza el dato que está dentro de ese objeto:\naño + 10 [1] 2003 2024 - año [1] 31 Podemos crear un nuevo objeto a partir de un cálculo, escribiendo un cálculo y asignándolo a un nuevo objeto:\nedad \u0026lt;- 2024 - año edad [1] 31 Ejemplos Para practicar con la idea de que los objetos son símbolos que contienen datos, crearemos distintos objetos los usaremos para hacer distintos cálculos sencillos.\nEjemplo 1 En este ejemplo, definiremos unas cifras que representan el cálculo de un presupuesto.\nPrimero, definimos varios datos. Cuando un dato hace referencia a un aspecto real de algo, ya sea una propiedad o cualidad, se le denomina variable, debido a que estas cualidades no son fijas, sino que cambian en las distintas situaciones, contextos o realidades.\npresupuesto \u0026lt;- 100000 pizza \u0026lt;- 12000 bebida \u0026lt;- 2500 Restamos todos los objetos para ver cuánto queda de presupuesto:\npresupuesto - pizza - bebida [1] 85500 También podemos usar paréntesis para ordenar nuestras operaciones:\npresupuesto - (pizza + bebida) [1] 85500 Ejemplo 2 Repetimos la creación de un presupuesto al definir nuestras variables:\npresupuesto \u0026lt;- 100000 pizza \u0026lt;- 12000 bebida \u0026lt;- 2500 El objeto pizza representa el precio de una sola pizza. Pero en este ejemplo, necesitamos comprar varias pizzas, así que crearemos un objeto nuevo a partir de una operación:\npizzas_totales \u0026lt;- pizza * 4 pizzas_totales [1] 48000 También tenemos que considerar la compra de bebidas:\nbebidas_totales \u0026lt;- bebida * 2 bebidas_totales [1] 5000 Ahora hacemos el cálculo del presupuesto considerando los dos objetos nuevos que creamos, y guardamos el resultado en otro objeto:\ntotal \u0026lt;- pizzas_totales + bebidas_totales total [1] 53000 Restamos el total de las compras con el presupuesto disponible:\nrestante \u0026lt;- presupuesto - total Ahora revisamos el resultado:\nrestante [1] 47000 La gracia de hacerlo así es que facilita la posiblidad de ir modificando los precios de los objetos, sus cantidades, y luego volver a ejecutar todas las líneas para llegar al resultado actualizado.\nEjercicio:\nIntenta volver al principio del ejemplo, donde se definen las variables, y modifica el precio de alguna. Luego, vuelve a ejecutar los cálculos para llegar al nuevo monto restante. Intenta reescribir este último ejemplo, pero haciendo que las cantidades de pizzas y bebidas sean también variables que se definan al principio del cálculo, junto con el resto de las variables. Define estas nuevas variables con valores distintos, y llega al nuevo resultado. Si bien estos ejemplos pueden parecer muy básicos, la estructura de análisis más complejos sigue patrones bastante bastante similares a los que estamos viendo ahora.\nVectores Hasta este punto, hemos visto operaciones sobre un solo dato a la vez; es decir, sobre objetos que contienen un solo elemento. Pero en la vida real, probablemente queremos realizar cálculos sobre múltiples datos a la vez, ya sea desde hojas de cálculos, planillas o bases de datos.\nLos objetos en R pueden tener múltiples propiedades. Una de estas es su largo. Un objeto que contiene un solo dato es un objeto de largo 1. Los vectores en R son objetos que contienen una secuencia de datos, una cadena de valores.\nLos vectores tienen elementos, tienen un largo, y tienen un tipo.\nPodemos crear un vector de forma manual con la función c():\nc(1, 2, 3, 4, 5, 6, 7, 8) [1] 1 2 3 4 5 6 7 8 En este bloque de código hemos creado un vector, una secuencia de valores que tiene ocho elementos.\nEn un vector podemos almacenar datos arbitrarios, siempre y cuando sean todos del mismo tipo.\nc(40, 2, 7, 345) [1] 40 2 7 345 c(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;) [1] \u0026quot;a\u0026quot; \u0026quot;b\u0026quot; \u0026quot;c\u0026quot; \u0026quot;d\u0026quot; Si queremos crear un vector que contenga una secuencia continua de números, podemos usar el operador : entre el número de inicio y el número final de la secuencia:\n1:10 # números de 1 al 10 [1] 1 2 3 4 5 6 7 8 9 10 1990:2010 # años del 1990 al 2010 [1] 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 [16] 2005 2006 2007 2008 2009 2010 Podemos realizar operaciones sobre los vectores, dado que son secuencias de datos, y la operación se va a aplicar a todos sus elementos:\nc(1, 2, 4, 8) + 100 [1] 101 102 104 108 c(31, 32, 56, 74) - 2024 [1] -1993 -1992 -1968 -1950 1:10 * 20 [1] 20 40 60 80 100 120 140 160 180 200 También podemos hacer comparaciones sobre los elementos de un vector:\nc(3, 3, 4, 5, 4, 3) == 3 [1] TRUE TRUE FALSE FALSE FALSE TRUE c(\u0026#34;sí\u0026#34;, \u0026#34;no\u0026#34;, \u0026#34;sí\u0026#34;, \u0026#34;sí\u0026#34;) == \u0026#34;no\u0026#34; [1] FALSE TRUE FALSE FALSE Pero el potencial de los vectores es cuando están asignados a un objeto. Asignamos a un objeto el vector de números:\nedades \u0026lt;- c(25, 27, 30, 45, 37, 28, 46, 54) Teniendo un objeto que contiene una secuencia de datos, ahora podemos realizar operaciones escritas de una forma más abstracta o resumida, y que se empiezan a acercar a casos de uso reales.\nedades - 40 [1] -15 -13 -10 5 -3 -12 6 14 edades \u0026gt; 30 [1] FALSE FALSE FALSE TRUE TRUE FALSE TRUE TRUE edades == 30 [1] FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE Para acceder para acceder a uno de los valores específicos de un vector, usamos las sintaxis de corchetes. Dentro de corchetes ponemos el elemento o elementos del vector que queremos extraer.\nedades[3] [1] 30 edades[1:3] [1] 25 27 30 Tipos de datos en vectores Por definición, los valores de los vectores en R son todos de un mismo tipo. Esto se debe a que queremos realizar operaciones sobre los factores, y como las operaciones dependen del tipo de cada dato (no podrmos sumar letras), garantizar que los vectores sean de un mismo tipo permite que siempre podamos realizar operaciones en todas sus elementos.\nCuando creamos un vector de distintos tipos de datos, R va a reducirlo por defecto a un solo tipo:\nc(1, 2, 3, 4, \u0026#34;cinco\u0026#34;) [1] \u0026quot;1\u0026quot; \u0026quot;2\u0026quot; \u0026quot;3\u0026quot; \u0026quot;4\u0026quot; \u0026quot;cinco\u0026quot; Si notamos la salida de la consola, podemos ver que los números aparecen envueltos en comillas, porque la presencia de un elemento de tipo carácter dentro del vector hizo que todos los demás elementos fueran convertidos a caracter.\nEjemplos Ejemplo 1 Un vector con años de nacimiento, y luego restamos el año actual al vector, para obtener las edades actuales de las personas que nacieron en esos años.\naños \u0026lt;- c(1992, 1997, 1986, 1980, 1999) edades \u0026lt;- 2024 - años edades [1] 32 27 38 44 25 Si el objeto resultante le sumamos 10, podemos obtener las edades que tendrían estas personas 10 años después.\nedades_en_10_años \u0026lt;- edades + 10 edades_en_10_años [1] 42 37 48 54 35 Ejemplo 2 Tenemos un vector con precios de productos, y un objeto que contiene un determinado valor, que sería el presupuesto que tenemos. Queremos saber qué productos son más baratos que el presupuesto. Entonces, comparamos los elementos del vector precios con el presupuesto:\nprecios \u0026lt;- c(10500, 26990, 9900, 56700, 12350, 12000, 24990) presupuesto \u0026lt;- 15000 precios \u0026lt; presupuesto [1] TRUE FALSE TRUE FALSE TRUE TRUE FALSE El resultado es un vector de valores lógicos que indican si que el precio es menor o no que el presupuesto.\nPodemos usar ese resultado para filtrar el vector precios, porque cada valor verdadero o falso se corresponde con el valor de la misma posición en el vector original. Es decir, el primer elemento del vector precios es menor al presupuesto, el segundo no, el tercero sí, etc. Así podemos obtener exactamente los datos que cumplen la condición:\nprecios[precios \u0026lt; presupuesto] [1] 10500 9900 12350 12000 Esto funciona porque, como habíamos mencionado antes, se pueden seleccionar elementos específicos del vector usando corchetes, y en este caso, los elementos que están siendo seleccionados son los que resultan TRUE de la comparación precios \u0026lt; presupuesto.\nFunciones Las funciones son pequeños programas que se aplican a nuestros objetos o vectores.\nEl uso de funciones es la ventana que nos abre posibilidades a nuevas herramientas y análisis, porque dentro de ellas se pueden ejecutar operaciones e instrucciones mucho más complejas que las que hemos visto hasta el momento.\nUna función es simplemente un programa dentro del cual se entregan ciertos argumentos.\nfuncion(argumento_1, argumento_2, ...) Dependiendo de lo que haga la función, dentro de ella podemos poner uno o varios vectores, un vector con algunas opciones definidas, u objetos más complejos que veremos adelante.\nUna función sencilla es la función mean(), que calcula el promedio de los valores de un vector:\nmean(c(10, 20, 39)) [1] 23 cifras \u0026lt;- c(1, 5, 4, 7, 8, 4, 3, 2, 9, 0, 2) mean(cifras) [1] 4.090909 años \u0026lt;- c(1992, 1997, 1986, 1980, 1999, 2010, 1993, 1976) edades \u0026lt;- 2024 - años mean(edades) [1] 32.375 Como R es un lenguaje principalmente estadístico, hay varias funciones matemáticas que operan sobre vectores de números:\nmean(edades) # promedio [1] 32.375 min(edades) # mínimo [1] 14 max(edades) # máximo [1] 48 median(edades) # mediana [1] 31.5 sum(edades) # suma [1] 259 También hay funciones que no realizan cálculos sobre los datos, sino que nos dicen otra cosa sobre ellos:\nlength(edades) [1] 8 Algo que podría ser importante de confirmar, como hemos visto, sería el tipo de un vector. Para esto, existen varias funciones:\nclass(edades) [1] \u0026quot;numeric\u0026quot; La función permite saber la clase de un objeto, que es una propiedad de los objetos relacionada a su tipo. En este caso retorna numeric, porque se trata de números, pero en realidad el tipo es otro:\ntypeof(edades) [1] \u0026quot;double\u0026quot; Esto ocurre porque los números pueden ser de distintos tipos: hay números enteros (integer), números decimales (double), entre otros, y tienen características distintas. Pero es suficiente con saber si es o no numérico para la mayoría de los casos.\nOtras funciones permiten consultar si un objeto es de un tipo específico:\nis.numeric(edades) [1] TRUE is.character(edades) [1] FALSE Si tenemos un vector de un tipo que no nos sirve, podemos convertirlo a uno que si nos sirva:\nnumeros \u0026lt;- c(1, 2, 3, 4, \u0026#34;perro\u0026#34;) numeros_2 \u0026lt;- as.numeric(numeros) # convertir a números Warning: NAs introduced by coercion numeros_2 [1] 1 2 3 4 NA Notamos que R nos entrega una alerta! dice que hay NA introducidos por coerción. Los valores NA son también llamados valores perdidos, y significa que es un elemento de un vector que no contiene nada. En este caso, el último elemento del vector es convertido a un dato perdido, porque el texto perro no se corresponde realmente con ningún número.\nis.character(\u0026#34;texto\u0026#34;) # retorna TRUE si el objeto es un texto [1] TRUE is.character(11111) # retorna FALSE si el objeto no es un texto [1] FALSE is.numeric(4985984) # retorna TRUE si el objeto es un número [1] TRUE is.numeric(5555) [1] TRUE as.character(4444) [1] \u0026quot;4444\u0026quot; Si necesitamos saber qué hace una función, o cómo funciona, podemos escribir en la consola un ? seguido por el nombre de la función ?as.numeric, o bien, entrar al panel de ayuda de RStudio, que se encuentra en la parte inferior derecha de la pantalla, y buscar la función que queramos aclarar.\nEn la ayuda de R aparecen las funciones explicadas, con ejemplos, con detalles de cada uno de sus argumentos, y más.\nNúmeros al azar La función sample() permite obtener una cantidad de elementos al azar desde otro conjunto de elementos. Por ejemplo, obtengamos un número al azar entre uno y 10 millones:\nnumeros \u0026lt;- 1:10000000 sample(numeros, size = 1) [1] 4402724 La función recibe como primer argumento el vector de elementos, y como segundo argumento la cantidad de elementos que queremos obtener al azar.\nIntenta cambiando el segundo argumento para que te entregue cuatro números al azar.\nSecuencias Podemos utilizar la función seq() para crear una secuencia de números entre dos números, definiendo en el argumento from el número inicial, en to el número final, y en by el salto entre cada cifra:\nseq(from = 10, to = 100, by = 5) [1] 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 El resultado es un vector que empieza desde el 10 y va hasta el 100, de 5 en 5.\nCreemos un vector del 1 al 2, avanzando en 0,1:\nseq(from = 1, to = 2, by = 0.1) [1] 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 Recordemos que también podemos usar funciones para definir los argumentos de cualquier otra función. En este caso, en vez de escribir los números que queremos para el inicio del final de la secuencia, podemos usar números que se obtienen desde aplicar una función a un vector:\nseq(from = min(edades), to = max(edades), by = 10) [1] 14 24 34 44 Esta función puede ser útil, por ejemplo, para crear una secuencia de años:\naños \u0026lt;- seq(1900, 2020, 10) Redactar textos Otra función básica es paste(), que te permite unir varios elementos en un sólo texto:\ntexto_1 \u0026lt;- \u0026#34;Hola\u0026#34; texto_2 \u0026lt;- \u0026#34;amigues\u0026#34; paste(texto_1, texto_2) [1] \u0026quot;Hola amigues\u0026quot; Podemos unir varios elementos, sacados de vectores o calculados con funciones, para generar un texto más complejo:\naños \u0026lt;- c(1992, 1997, 1986, 1980, 1999, 2010, 1993, 1976) edades \u0026lt;- 2024 - años promedio \u0026lt;- mean(edades) paste(\u0026#34;Entre las\u0026#34;, length(edades), \u0026#34;edades analizadas,\u0026#34;, \u0026#34;el promedio fue de\u0026#34;, promedio, \u0026#34;y la edad máxima fue de\u0026#34;, max(edades), \u0026#34;años.\u0026#34; ) [1] \u0026quot;Entre las 8 edades analizadas, el promedio fue de 32.375 y la edad máxima fue de 48 años.\u0026quot; Podemos usar esta función combinada con sample() para obtener un muestreo aleatorio de cualquier tipo de elementos, lo que puede servir para hacer algunas cosas entretenidas:\nanimales \u0026lt;- c(\u0026#34;gato\u0026#34;, \u0026#34;mapache\u0026#34;, \u0026#34;castor\u0026#34;, \u0026#34;pollo\u0026#34;, \u0026#34;ratón\u0026#34;, \u0026#34;pudú\u0026#34;) paste(\u0026#34;el animal más lindo es el\u0026#34;, sample(animales, 1)) [1] \u0026quot;el animal más lindo es el pollo\u0026quot; Redondear datos En el ejemplo anterior, obtuvimos que el promedio de edades era 32.375. Si queremos redondear este número, podemos abusar la función round(), cuyo primer argumento es el número o números que queremos redondear, y el segundo es la cantidad de decimales:\nround(mean(edades), digits = 1) [1] 32.4 Condicionales Una función condicional permite hacer algo si se cumple o no una condición. Esto que puede sonar sencillo puede resultar tremendamente útil.\nEn la función ifelse(), el primer argumento es una comparación, y luego hay que especificar dos argumentos más: el primero es lo que queremos si la comparación es verdadera, y lo segundo es lo que queremos si la comparación es falsa. La expresión \u0026ldquo;if else\u0026rdquo; significa en castellano \u0026ldquo;si pasa esto, entonces haz esto\u0026rdquo;. En una sentencia if else, se define una o más condiciones que queremos que se cumplan para ejecutar un código y, si esa condición no se cumple, ejecutamos otro código (o no ejecutamos nada).\nPor ejemplo, evaluamos si un número es mayor a 5, y si es así, retornamos la palabra alto, y si no, retornamos la palabra bajo:\nnumero \u0026lt;- 10 ifelse(numero \u0026gt; 5, yes = \u0026#34;alto\u0026#34;, no = \u0026#34;bajo\u0026#34;) [1] \u0026quot;alto\u0026quot; numero \u0026lt;- 3 ifelse(numero \u0026gt; 5, yes = \u0026#34;alto\u0026#34;, no = \u0026#34;bajo\u0026#34;) [1] \u0026quot;bajo\u0026quot; Tenemos un vector de la cantidad de dulces que comieron unas personas. Si definimos que comer más de 4 dulces es mucho, aplicamos la función ifelse() para categorizar las cantidades de dulces que comieron las personas:\ndulces \u0026lt;- c(3, 2, 4, 3, 5, 6, 5, 7, 6, 2, 1, 2, 3, 2) dulces_2 \u0026lt;- ifelse(dulces \u0026gt;= 4, yes = \u0026#34;muchos\u0026#34;, no = \u0026#34;normal\u0026#34;) dulces_2 [1] \u0026quot;normal\u0026quot; \u0026quot;normal\u0026quot; \u0026quot;muchos\u0026quot; \u0026quot;normal\u0026quot; \u0026quot;muchos\u0026quot; \u0026quot;muchos\u0026quot; \u0026quot;muchos\u0026quot; \u0026quot;muchos\u0026quot; [9] \u0026quot;muchos\u0026quot; \u0026quot;normal\u0026quot; \u0026quot;normal\u0026quot; \u0026quot;normal\u0026quot; \u0026quot;normal\u0026quot; \u0026quot;normal\u0026quot; Luego, podemos usar una suma condicionada para saber cuántas personas comieron muchos dulces:\nsum(dulces_2 == \u0026#34;muchos\u0026#34;) # cantidad de personas que comieron \u0026#34;muchos\u0026#34; dulces [1] 6 Con este otro ejemplo, aprenderemos a usar el operador de inclusión, %in%, qué es lo que hace es determinar si un elemento está dentro de otro conjunto de elementos. Creamos un vector de elementos, y un segundo actor que contiene un subgrupo de los elementos. Luego, usamos el operador de inclusión %in% para saber cuál de los primeros elementos están dentro del segundo grupo de elementos. En este caso, tenemos un vector con los colores, y después los colores que yo considero que son más lindos, y queremos saber cuál es están dentro de este grupo de colores más lindos:\ncolores \u0026lt;- c(\u0026#34;rojo\u0026#34;, \u0026#34;verde\u0026#34;, \u0026#34;azul\u0026#34;, \u0026#34;rosado\u0026#34;, \u0026#34;amarillo\u0026#34;, \u0026#34;morado\u0026#34;, \u0026#34;naranjo\u0026#34;, \u0026#34;gris\u0026#34;, \u0026#34;lavanda\u0026#34;, \u0026#34;celeste\u0026#34;) lindos \u0026lt;- c(\u0026#34;morado\u0026#34;, \u0026#34;rosado\u0026#34;) colores %in% lindos [1] FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE Luego, aplicaremos el mismo principio, pero dentro de un if_else(), para asignarle la categoría lindo o feo a cada uno de los colores:\nopinión \u0026lt;- ifelse(colores %in% lindos, yes = \u0026#34;lindos\u0026#34;, no = \u0026#34;feos\u0026#34;) opinión [1] \u0026quot;feos\u0026quot; \u0026quot;feos\u0026quot; \u0026quot;feos\u0026quot; \u0026quot;lindos\u0026quot; \u0026quot;feos\u0026quot; \u0026quot;lindos\u0026quot; \u0026quot;feos\u0026quot; \u0026quot;feos\u0026quot; [9] \u0026quot;feos\u0026quot; \u0026quot;feos\u0026quot; Finalmente, podemos hacer una tabla con los resultados, o una suma condicionada, para saber cuántos colores caen en cada categoría:\ntable(opinión) opinión feos lindos 8 2 paste(\u0026#34;Hay\u0026#34;, sum(opinión == \u0026#34;feos\u0026#34;), \u0026#34;colores feos,\u0026#34;, \u0026#34;y\u0026#34;, sum(opinión == \u0026#34;lindos\u0026#34;), \u0026#34;colores lindos\u0026#34;) [1] \u0026quot;Hay 8 colores feos, y 2 colores lindos\u0026quot; Conclusión Si bien en estas instrucciones no aprendimos a analizar datos, si considero que comprender estos principios es algo fundamental para poder pasar al análisis de datos teniendo dudas recurrentes despejadas y una familiaridad con elementos básicos del lenguaje que es necesaria. Por ejemplo, saber lo que es un vector, o entender las particularidades de los tipos o clases, ahorra caer en los errores más recurrentes de personas que entran a R a trabajar directamente con tablas de datos sin entender qué está pasando internamente.\nEl entender cómo funcionan estas pequeñas herramientas, y familiarizarse con su uso nos facilitará bastante la aplicación del lenguaje al procesamiento de tablas, bases de datos, y otras situaciones en las que podemos aplicar R para ayudarnos y para producir resultados.\nSi entendiste este tutorial y quieres pasar al siguiente nivel, revisa Herramientas básicas para programar con R para aprender cómo aplicar lo que aprendiste dentro de flujos de procesamiento de datos más complejos. Luego podrías atreverte a intentar con este otro tutorial, en el cual se abarcan herramientas básicas de manipulación de datos: Introducción a {dplyr} con datos de población.\nSi este tutorial te sirvió, por favor considera hacerme una donación, al menos para poder tomarme un cafecito 🥺\nSi ejecutamos una línea incompleta, puede ser que la consola de R quede esperando que terminemos la expresión o que la completemos, y esto puede ser muy confuso para usuarios principiantes. Por ejemplo, si ejecutamos 1 +, la consola va a quedar esperando que le demos el número que está esperando que venga, y cualquier otra cosa que le entreguemos va a intentar sumársela a la operación anterior que quedó inconclusa.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-11-16T00:00:00Z","excerpt":"Instrucciones paso a paso para aprender los aspectos más básicos del lenguaje R. Dirigida a personas sin ningún conocimiento del lenguaje. Si quieres aprender R desde cero, esta publicación es para ti.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/r_basico/","tags":"funciones ; básico","title":"Introducción al lenguaje de programación R"},{"content":"Esta es la segunda guía introductoria para aprender el lenguaje de programación R orientado al análisis de datos. En la guía anterior vimos los principios más fundamentales del lenguaje R, para familiarizarnos con R y entender su funcionamiento básico. En esta segunda guía, seguiremos puliendo nuestros aprendizajes para aprender herramientas de programación poderosas que flexibilizarán mucho nuestras capacidades de análisis de datos, abriendo infinitas posibilidades.\nEn esta guía, aprenderemos herramientas que nos permitirán a:\nCrear herramientas que nos permitan hacer lo que necesitamos (crear funciones) Condicionar lo que queremos que ocurra en nuestros procesos (controlar el flujo) Hacer lo que necesitamos repetidamente (realizar iteraciones o loops) Crear funciones La creación de funciones nos abre las puertas a todo el mundo nuevo en el uso de R para analizar nuestros datos, dado que, una vez que te familiarizas con la creación de funciones, ya no depende solamente de las herramientas existentes, sino que empiezas a hacer tú quien crea nuevas herramientas adaptadas a tus propias necesidades.\nLa anterior tiene el enorme beneficio de permitirte personalizar las herramientas que utilices para analizar datos a tu forma de trabajo, a las especificidades de tus datos, y a las particularidades del proceso que estés llevando a cabo. Quizás el aspecto más satisfactorio de la creación de funciones sea poder englobar un proceso largo y complejo en una función llamada procesar_datos(), o poder generar un gráfico detallado y atractivo tan sólo con llamar graficar()!\nPara crear una función se usa, valga la redundancia, la función function(). Dentro de function() se definen los argumentos, que \u0026mdash;como sabemos\u0026mdash; son los elementos que pasamos dentro de la función para que ésta opere. Luego de function() se abren unos paréntesis de llave, dentro de los cuales definiremos qué hará la función con los argumentos que le entregamos, y que retornará. Con retornar nos referimos a el producto final de esta función; es decir, lo que nos entrega al ejecutarla.\nDefinamos el nombre, los argumentos, y el cuerpo de una nueva función:\nmultiplicar_mil \u0026lt;- function(cifra) { cifra * 1000 } En este ejemplo, estamos creando una función que se llamará multiplicar_mil(). La función tendrá un solo argumento, llamado cifra. Dentro del cuerpo de la función, vemos que el objeto cifra, que viene desde el argumento, se multiplica por 1000. Como esta es la única y última operación dentro del cuerpo de la función, su resultado será lo que se retorne al ejecutarla.\nProbemos nuestra primera función y veamos su comportamiento:\nmultiplicar_mil(4) [1] 4000 multiplicar_mil(\u0026#34;hola\u0026#34;) Error in cifra * 1000: non-numeric argument to binary operator multiplicar_mil(c(32, 65, 87)) [1] 32000 65000 87000 Hagamos otro ejemplo:\npersonas \u0026lt;- c(\u0026#34;Basti\u0026#34;, \u0026#34;Paula\u0026#34;, \u0026#34;Catherine\u0026#34;, \u0026#34;Luis\u0026#34;, \u0026#34;María José\u0026#34;) saludar \u0026lt;- function(persona) { paste(\u0026#34;¡Hola \u0026#34;, persona, \u0026#34;!\u0026#34;, sep = \u0026#34;\u0026#34;) } saludar(personas) [1] \u0026quot;¡Hola Basti!\u0026quot; \u0026quot;¡Hola Paula!\u0026quot; \u0026quot;¡Hola Catherine!\u0026quot; [4] \u0026quot;¡Hola Luis!\u0026quot; \u0026quot;¡Hola María José!\u0026quot; saludar(\u0026#34;Caro\u0026#34;) [1] \u0026quot;¡Hola Caro!\u0026quot; En esta función, el argumento persona expuesto dentro de una función paste() que genera un texto a partir de argumento. Como en el primer ejemplo se le entrega un vector a la función, paste() produce múltiples textos con cada elemento del vector. O sea que nuestra función funciona tanto para múltiples elementos como para uno solo.\nPasemos a una versión más compleja de la función anterior:\nsaludar \u0026lt;- function(persona) { # pasar el nombre a mayúsculas nombre_mayuscula \u0026lt;- toupper(persona) # crear un saludo uniendo el texto con el argumento saludo \u0026lt;- paste(\u0026#34;¡Hola \u0026#34;, nombre_mayuscula, \u0026#34;!\u0026#34;, sep = \u0026#34;\u0026#34;) # crear números al azar números \u0026lt;- sample(1:99, length(persona)) # uno por persona # agregar números de la suerte saludo2 \u0026lt;- paste(saludo, \u0026#34;Tu número de la suerte es\u0026#34;, números) saludo2 } saludar(personas) [1] \u0026quot;¡Hola BASTI! Tu número de la suerte es 47\u0026quot; [2] \u0026quot;¡Hola PAULA! Tu número de la suerte es 88\u0026quot; [3] \u0026quot;¡Hola CATHERINE! Tu número de la suerte es 82\u0026quot; [4] \u0026quot;¡Hola LUIS! Tu número de la suerte es 31\u0026quot; [5] \u0026quot;¡Hola MARÍA JOSÉ! Tu número de la suerte es 24\u0026quot; En este caso, dentro del cuerpo de la función estamos realizando varias operaciones. Dentro de una función podemos crear nuevos objetos, y usar esos nuevos objetos para realizar otras operaciones sobre ellos. De este modo podemos llevar a cabo acciones más complejas. En el ejemplo, el argumento de la función es transformado a mayúsculas con toupper(), luego se inserta dentro del texto con paste(). Después, se crea un vector de números al azar que contenga una cantidad de números igual a la cantidad de elementos del vector que viene del argumento de la función, para que haya un número por persona. Finalmente, se usa nuevamente paste() junto a sample() para agregar el número aleatorio, y se entrega el objeto final, que será lo que la función retorna.\nLas funciones pueden tener más de un argumento. Creemos una función que use dos argumentos para calcular una estimación de tiempo de viaje a partir de una velocidad y una distancia dados:\ncalcular_tiempo \u0026lt;- function(velocidad, distancia) { # el tiempo de viaje equivale a la distancia dividida por la velocidad tiempo \u0026lt;- distancia/(velocidad/60) # pasar km/h a km/minutos paste(\u0026#34;Tiempo de viaje:\u0026#34;, round(tiempo, 1), \u0026#34;minutos\u0026#34;) } calcular_tiempo(velocidad = 20, distancia = 5) [1] \u0026quot;Tiempo de viaje: 15 minutos\u0026quot; En este ejemplo, una función que calcula el tiempo de viaje requiere dos argumentos: la velocidad y la distancia. Dentro de function() simplemente se definen los nombres de los argumentos, y se usan dentro del cuerpo de la función para calcular lo necesario.\nAl momento de ejecutar una función, si ponemos los argumentos en el orden en que se especifican en la función, no es necesario que pongamos el nombre del argumento antes de el valor:\ncalcular_tiempo(velocidad = 22, distancia = 6) [1] \u0026quot;Tiempo de viaje: 16.4 minutos\u0026quot; calcular_tiempo(22, 6) [1] \u0026quot;Tiempo de viaje: 16.4 minutos\u0026quot; También podemos especificar valores por defecto para los argumentos al momento de crear la función. Éstos valores por defecto serán aplicados si es que el/la usuario/a no define los argumentos.\n# ejecutar función sin argumentos calcular_tiempo() Error in calcular_tiempo(): argument \u0026quot;distancia\u0026quot; is missing, with no default Como no definimos los argumentos, la función retorna un error. Modifiquemos la función para que tenga argumentos por defecto:\n# actualizar función con valores por defecto calcular_tiempo \u0026lt;- function(velocidad = 0, distancia = 1) { tiempo \u0026lt;- distancia/(velocidad/60) paste(\u0026#34;Tiempo de viaje:\u0026#34;, round(tiempo, 1), \u0026#34;minutos\u0026#34;) } # volver a ejecutar sin argumentos, por lo que usará los argumentos por defecto calcular_tiempo() [1] \u0026quot;Tiempo de viaje: Inf minutos\u0026quot; Ahora la función no tira un error si no se definen los argumentos, aunque el resultado que entrega en estos casos no sea muy útil 😅\n¿Cómo puede ayudarte la creación de funciones para el análisis de datos?\nSi realizas una misma tarea frecuentemente, puedes guardarla a una función para así reutilizar la operación en varios momentos de tu código. Si realizas una operación matemática o estadística sobre una columna, puedes guardarla como una función para simplificar la lectura del código Puedes simplificar los pasos de tu procesamiento de datos guardándolos como funciones, cuya entrada son los datos y la salida son los datos procesados. De este modo puedes ocultar la complejidad, lo que produce scripts más legibles. Puedes definir todas tus funciones en otro script, permitiéndote así tener scripts más ordenados y menos extensos. Control de flujo Las estructuras de control de flujo son el conjunto de reglas que hacen que, dentro de un proceso de análisis de datos, se realicen (o no) ciertas acciones si es que condiciones específicas se cumplen, o bien, que no ocurran ciertas cosas si es que las condiciones no lo permiten. En otras palabras, es una forma de hacer que tu proceso de análisis de datos adquiera fluidez, al definir momentos en los que el flujo del procesamiento es determinado por las condiciones que tu definas.\n¿En qué situaciones podría ser útil el control de flujo del código?\nSi un dato no cumple con un criterio específico, corregirlo o descartarlo. Si los datos llegan en cierto estado, o con ciertas particularidades, realizar un paso extra de limpieza. Si dentro de los datos existen observaciones de ciertas características, visualizar los datos de una manera específica. Si a la tabla de datos le falta una comuna columna, crearla. Si una columna viene en un tipo que no es el esperado, convertirla al apropiado. Si ocurre un error o el resultado no es aceptable, realizar la operación de nuevo de una forma alternativa. En la práctica, esta técnica permite crear condicionalidad en la ejecución del código usando una comparación, cuyo resultado (TRUE/FALSE) decide si el código siguiente se ejecutará o no. El código dentro de la condición sólo se ejecuta si la comparación retorna TRUE.\nCreemos un ejemplo donde definimos un vector de números, y si alguno de estos números cumple con un criterio específico, realizaremos una operación que corrija la situación:\nedades \u0026lt;- c(15, 17, 18, 24, 29, 31, 34, 36, 45, 49, 52) edades [1] 15 17 18 24 29 31 34 36 45 49 52 # si viene alguna edad que sea menor a 18 años, ejecutar lo siguiente if (any(edades \u0026lt; 18)) { # filtrar vector menores \u0026lt;- edades[edades \u0026lt; 18] message(paste(\u0026#34;Se detectaron registros menores de 18 años: eliminando\u0026#34;, length(menores), \u0026#34;registros\u0026#34;)) # dejar solamente las edades que son igual o mayor a 18 edades \u0026lt;- edades[edades \u0026gt;= 18] } Se detectaron registros menores de 18 años: eliminando 2 registros edades [1] 18 24 29 31 34 36 45 49 52 En este ejemplo, creamos un flujo de control donde un conjunto de datos se filtra sólo si se cumple un cierto criterio. Se usa la función any() para detectar si es que existe algún elemento dentro del vector que cumpla con el criterio de ser menor de 18 años. Si esto es así, se ejecuta el código dentro del if, que detecta las observaciones problemáticas, emite un mensaje, y finalmente filtra el vector para dejar solamente las observaciones válidas, y lo retorna sobreescribiendo el vector original.\nProbemos nuevamente cambiando el dato, agregando un mensaje que también confirma si la condición no se cumple:\nedades \u0026lt;- c(31, 34, 36, 45, 49, 52, 62, 63) edades [1] 31 34 36 45 49 52 62 63 # si viene alguna edad que sea menor a 18 años, ejecutar lo siguiente if (any(edades \u0026lt; 18)) { menores \u0026lt;- edades[edades \u0026lt; 18] message(paste(\u0026#34;Se detectaron registros menores de 18 años: eliminando\u0026#34;, length(menores), \u0026#34;registros\u0026#34;)) edades \u0026lt;- edades[edades \u0026gt;= 18] } else { # emitir un mensaje si la condición no se cumple message(\u0026#34;OK: todos los registros corresponden a mayores de 18 años\u0026#34;) } OK: todos los registros corresponden a mayores de 18 años En el apartado else podemos especificar el código que se ejecutará si la condición que especificamos en el if retorna FALSE, o bien, simplemente podemos omitir el apartado else para que en cuyo caso no ocurra nada, como en el ejemplo anterior.\nEl control de flujo resulta particularmente útil para crear funciones más complejas. Dentro de las funciones puedes crear condicionales que realizan operaciones específicas dependiendo de lo que se le entregue a la función.\nTambién podemos crear nuevos argumentos en la función que se usen en condicionales para alterar la operación de la función. Así puedes hacer que tu función trabaje de forma distinta dependiendo de lo que le especifiques.\nBucles Los bucles o loops son operaciones extremadamente útiles. Permiten generalizar tu código, en el sentido que una operación u operaciones que definas pueden aplicarse repetidas veces sobre distintos objetos, con un alto grado de libertad en el proceso.\nEn un bucle, se realiza una operación múltiples veces en base a un vector que entregues. Por cada elemento del vector que entregues al bucle, el código se ejecutará. En este sentido, el bucle tendrá tantos pasos como elementos tenga el vector. Cuando entregas un vector al bucle, también indicas cómo se va a llamar la variable que representa al paso; es decir, la variable que va a contener el valor del vector que se corresponde al paso que se está ejecutando.\nPor ejemplo:\nnumeros \u0026lt;- c(1, 2, 3, 4) for (paso in numeros) { print(paso) } [1] 1 [1] 2 [1] 3 [1] 4 En este ejemplo, creamos un vector numeros que contiene números del 1 al 4. Luego, definimos un bucle con el operador for, que en español se leería \u0026ldquo;por cada\u0026rdquo;. El primer argumento que definimos luego del for es el nombre que tendrá cada paso, luego in, y luego el vector. Entonces, se leería \u0026ldquo;por cada paso en numeros, ejecutar\u0026hellip;\u0026rdquo;. En el ejemplo, lo que se ejecutará es solamente retornar el valor de cada paso. Es decir, en el primer paso se retorna el valor 1 de numeros, en el segundo paso el valor 2, y así sucesivamente.\nOtro ejemplo más concreto:\npasos \u0026lt;- 10:20 for (i in pasos) { texto \u0026lt;- paste(\u0026#34;este es el paso:\u0026#34;, i) print(texto) } [1] \u0026quot;este es el paso: 10\u0026quot; [1] \u0026quot;este es el paso: 11\u0026quot; [1] \u0026quot;este es el paso: 12\u0026quot; [1] \u0026quot;este es el paso: 13\u0026quot; [1] \u0026quot;este es el paso: 14\u0026quot; [1] \u0026quot;este es el paso: 15\u0026quot; [1] \u0026quot;este es el paso: 16\u0026quot; [1] \u0026quot;este es el paso: 17\u0026quot; [1] \u0026quot;este es el paso: 18\u0026quot; [1] \u0026quot;este es el paso: 19\u0026quot; [1] \u0026quot;este es el paso: 20\u0026quot; En este caso, tenemos un vector de 10 números, por lo que el código especificado se aplica a cada uno de los números, usando el objeto i como si fuera el objeto que contiene el valor de cada paso (10, 11, 12, etc.).\nDentro de una iteración también podemos controlar el flujo del código con if else:\nfor (persona in personas) { saludo \u0026lt;- paste(\u0026#34;Hola\u0026#34;, persona) if (persona == \u0026#34;María José\u0026#34;) { saludo \u0026lt;- paste0(\u0026#34;¡Hola amiguita \u0026#34;, persona, \u0026#34;!\u0026#34;) } print(saludo) } [1] \u0026quot;Hola Basti\u0026quot; [1] \u0026quot;Hola Paula\u0026quot; [1] \u0026quot;Hola Catherine\u0026quot; [1] \u0026quot;Hola Luis\u0026quot; [1] \u0026quot;¡Hola amiguita María José!\u0026quot; En este caso, ponemos una excepción con un if para que, en un paso específico del loop, el comportamiento sea distinto.\nEn este ejemplo, controlamos el flujo del código para que hayan múltiples condiciones, y para que en cada condición se haga algo distinto. Podemos evaluar múltiples condiciones con else if, que es una forma de hacer que si la comparación anterior no coincidió con el valor, se intente se nuevo con la siguiente. Se pueden encadenar varios else if, y de este modo, se puede hacer que un solo valor vaya siendo evaluado en múltiples condiciones, a ver si coincide con alguna. Si coincide con alguna, se ejecuta el código correspondiente a ese paso, y se sale del flujo. Pero si no coincide con ninguna, no pasará nada a menos que al final pongas un else que capture todos los demás casos.\nfor (persona in personas) { if (persona == \u0026#34;Basti\u0026#34;) { saludo \u0026lt;- paste(\u0026#34;¡Hola a todes!\u0026#34;) } else if (persona == \u0026#34;Catherine\u0026#34;) { saludo \u0026lt;- paste(\u0026#34;Excelente pregunta, Catherine\u0026#34;) } else if (persona == \u0026#34;Luis\u0026#34;) { saludo \u0026lt;- sample(c(\u0026#34;Serpiente\u0026#34;, \u0026#34;Perro\u0026#34;, \u0026#34;Gato\u0026#34;, \u0026#34;Rata\u0026#34;, \u0026#34;Gallina\u0026#34;, \u0026#34;Pez\u0026#34;), 1) } else { saludo \u0026lt;- paste(\u0026#34;Hola\u0026#34;, persona) } print(saludo) } [1] \u0026quot;¡Hola a todes!\u0026quot; [1] \u0026quot;Hola Paula\u0026quot; [1] \u0026quot;Excelente pregunta, Catherine\u0026quot; [1] \u0026quot;Gato\u0026quot; [1] \u0026quot;Hola María José\u0026quot; Por cada paso, el objeto persona asume el valor del elemento correspondiente del vector personas. El código avanza dentro del loop probando si persona coincide con alguna de las condiciones dadas, y si no coincide, prueba si coincide la siguiente con else if, y al final, si el valor no coincidió con ninguna de las comparaciones específicas de los if, entonces se realiza una operación general para todos los demás casos en else.\n¿En qué casos sería útil usar bucles o loops?\nSi tienes múltiples objetos, observaciones, o tablas de datos, a las cuales quiera realizarles una misma operación a todas al mismo tiempo. Si realizaste un análisis, pero tienes que volver a realizar este mismo análisis sobre muchas versiones distintas de un conjunto de datos (por ejemplo, cuando los datos vienen separados en distintos archivos por mes o por año). Si tienes la ubicación de muchos archivos, y quieres cargarlos todos al mismo tiempo. Si tienes un conjunto de datos que puedes filtrar por una variable, y quieres generar múltiples visualizaciones a partir de el filtrado de cada una de las categorías de dicha variable. Por ejemplo, si tienes un conjunto de datos de 10 países, puedes enterar por los nombres de los países para generar 10 gráficos, uno para cada país. Si tienes un conjunto de datos con muchas variables, y a todas las variables quieres calcularle estadísticos descriptivos, defines el cálculo de los estadísticos descriptivos una vez, y lo pones dentro de una iteración que vaya por todas las columnas del dataframe. Cómo puedes ver, la creación de funciones, el control de flujo, y los bucles o iteraciones son herramientas complementarias. Muchas veces, lo que podemos hacer con una función queda mejor dentro de un bucle, o un bucle que diseñamos puede ser insertado en una función para nuestra conveniencia, y dentro de funciones o de bucle siempre vamos a estar usando el control de flujo para poder adaptarnos a todos los casos posibles que encontremos en nuestros datos.\nLuego de haber aprendido los contenidos de este tutorial, creo que podemos dar el siguiente paso, que es adentrarnos a los datos tabulares o dataframes, e introducirnos al uso de R para el análisis de datos!\n","date":"2025-02-14T00:00:00Z","excerpt":"En guía aprenderemos herramientas de programación poderosas que flexibilizarán mucho nuestras capacidades de análisis de datos, abriendo infinitas posibilidades. Con ellas, podremos crear nuevas herramientas que nos permitan hacer lo que necesitamos (funciones), condicionar lo que queremos que ocurra en nuestros procesos (controlar el flujo), y realizar operaciones repetidamente (iteraciones o loops).","href":"https://bastianoleah.netlify.app/blog/r_introduccion/r_intermedio/","tags":"básico ; funciones ; control de flujo ; loops ; programación","title":"Herramientas básicas para programar con R"},{"content":" Los conectores o pipes son símbolos que nos permiten encadenar varias operaciones. En inglés, pipe significa literalmente \u0026ldquo;tubería\u0026rdquo;, porque la idea es que los datos fluyan a través de una serie de pasos.\nEl uso de conectores facilita la lectura del código, porque nos muestra el flujo de las operaciones de forma ordenada y lógica, y también simplifica la escritura, porque nos hace escribir en el mismo orden lógico que pensamos las operaciones, y porque además nos permite reordenar, comentar, agregar o eliminar pasos.\nEn R, el conector nativo es |\u0026gt; y a continuación veremos cómo usarlo!\nDefinición Un conector conecta dos pasos del procesamiento de datos, ya sea conectando los datos a una acción, o el resultado de una acción con una nueva acción. Así que la lectura de un conector (|\u0026gt;) sería algo así como \u0026ldquo;luego\u0026rdquo; o \u0026ldquo;entonces\u0026rdquo;.\nPensemos en una idea de procesamiento:\nPara obtener resultado, empiezo con mis datos, y luego |\u0026gt; les aplico una acción(), y luego |\u0026gt; les hago otra_acción()\nTraduzcámosla a código:\nresultado \u0026lt;- datos |\u0026gt; acción() |\u0026gt; otra_acción() En otras palabras, se empieza con los datos, los datos se modifican una vez en el primer paso, y en el segundo paso se modifica el resultado del paso anterior. Es una cadena de operaciones con un inicio (datos) y un final (resultado).\nRecordemos que, en R, el resultado (la asignación) se pone al principio, porque es una forma de decir vamos a hacer esto de la siguiente manera, una especie de título de la operación.\nEscritura del conector El conector se escribe como |\u0026gt;, es decir, una barra vertical | seguida de un símbolo mayor que \u0026gt;.\nPero recomiendo aprender el atajo de teclado para insertarlo, ya que va a ser una operación de uso muy frecuente:\nAtajo de teclado para insertar el conector |\u0026gt; en Mac Atajo de teclado para insertar el conector |\u0026gt; en Linux y Windows Tipos de conectores Si intentaste el paso anterior, puede que te haya salido un símbolo distinto 😱\nEn R existen dos conectores: el |\u0026gt; (pipe nativo de R) y el %\u0026gt;% (pipe del paquete {magrittr}). Ambos hacen prácticamente lo mismo.\n|\u0026gt;\n%\u0026gt;%\nEl conector que aparezca va a depender de tu configuración de RStudio: en el menú Tools, entra a Global options, y en el menú Code elige si quieres o no usar el conector nativo de R (recomendado).\nEjemplos Veamos unos ejemplos de uso del conector en R, y cuándo resulta conveniente de usar:\nEmpecemos con un vector de números:\ndatos \u0026lt;- c(4, 6, 3, 7, 8, 6) Ejemplo sin conector 👎🏼 Si tenemos un vector de números y queremos calcular su promedio:\nmean(datos) Pero si además queremos redondear el resultado (dos operaciones), tenemos que empezar a anidar las funciones:\nround(mean(datos), digits = 2) Este código se leería, de izquierda a derecha, como redondear el promedio de datos, lo cual no está mal, pero te obliga a entender la operación desde adentro hacia afuera (los datos o el inicio del proceso están al medio del código). Además, anidar las funciones empieza a hacer que el código sea cada vez menos legible. Imagínate si en vez de 2 funciones necesitas 4 o 5? 🫤\nEjemplo con conector 👍🏼 Para hacer lo mismo pero usando conectores, encadenamos las operaciones empezando con los datos, en el orden de su ejecución:\ndatos |\u0026gt; mean() |\u0026gt; round(digits = 2) La lectura sería: a los datos les calculo el promedio y luego al resultado lo redondeo. La operación ahora es más legible (vemos que hay claramente 3 pasos) y sigue un orden secuencial (se leen en el orden que se aplican).\nCreemos una pequeña tabla de datos para el siguiente ejemplo:\n# crear tabla de datos de ejemplo con tribble library(dplyr) datos \u0026lt;- tibble(animal = c(\u0026#34;gato\u0026#34;, \u0026#34;ratón\u0026#34;, \u0026#34;perro\u0026#34;, \u0026#34;pez\u0026#34;, \u0026#34;paloma\u0026#34;), color = c(\u0026#34;gris\u0026#34;, \u0026#34;negro\u0026#34;, \u0026#34;blanco\u0026#34;, \u0026#34;azul\u0026#34;, \u0026#34;gris\u0026#34;), patas = c(4, 4, 4, 0, 2), edad = c(8, 2, 10, 1, 3)) Copia y pega el código para ejecutarlo en tu sesión de R animal color patas edad gato gris 4 8 ratón negro 4 2 perro blanco 4 10 pez azul 0 1 paloma gris 2 3 Ejemplo sin conector 👎🏼 Imagina que a estos datos queremos hacerle varias operaciones:\nSeleccionar algunas columnas, filtrar algunas filas, y ordenar los resultados Vamos agregando las funciones en orden: primero la selección\u0026hellip;\nselect(datos, animal, patas, edad) # 🙂 Ahora el filtro, pero adentro hay que ponerle los datos seleccionados\u0026hellip;\nfilter(select(datos, animal, patas, edad), patas \u0026gt;= 4) # 🤨 Si queremos ordenarlos tenemos que poner todo adentro de otra función\u0026hellip;\narrange(filter(select(datos, animal, patas, edad), patas \u0026gt;= 4), edad) # 😵‍💫 Terminamos con esta aberración de paréntesis y funciones que se leen algo así como ordeno el filtro de la selección de los datos 😣 y al final van apareciendo los argumentos de cada función. Imposible de leer! 😭\nUna forma de solucionarlo es ir asignando cada paso a un objeto\u0026hellip;\ndatos_2 \u0026lt;- select(datos, animal, patas, edad) datos_3 \u0026lt;- filter(datos_2, patas \u0026gt;= 4) datos_4 \u0026lt;- arrange(datos_3, edad) \u0026hellip;pero esto hace que tengamos que crear muchos objetos intermedios, que no nos interesan! Además se lee muy mal porque te interrumpe la constante creación de objetos.\nEjemplo con conector 👍🏼 Para hacer lo mismo pero usando los conectores, empezamos con los datos y vamos agregando los pasos del procesamiento: primero la selección, luego el filtro, y finalmente el ordenamiento:\ndatos |\u0026gt; select(animal, patas, edad) |\u0026gt; filter(patas \u0026gt;= 4) |\u0026gt; arrange(edad) El proceso queda más ordenado, se lee en orden (de arriba hacia abajo), no necesitamos crear objetos intermedios, y no hay ningún caos de paréntesis para desenredar! Mucho mejor 😊\nAdemás, se hace mucho más fácil reordenar los pasos o agregar pasos intermedios, o bien poder poner comentarios entre medio, para ir apoyándonos en la lectura y el aprendizaje:\ndatos |\u0026gt; # seleccionar sólo columnas útiles select(animal, patas, edad) |\u0026gt; # filtrar animales con 4 o más patitas filter(patas \u0026gt;= 4) |\u0026gt; # ordenarlos empeazndo por el más bebé arrange(edad) Solamente hay que acostumbrarse a fijarnos bien que los conectores conecten con algo: no dejar conectores apuntando a nada, porque hacen que R piense que el código conecta con lo que sea que tenga abajo, o peor, que R quede esperando que lo conectes con algo!1\nen cuyo caso hay que tirarle cualquier cosa a la consola para que retorne error y podamos seguir con nuestras vidas\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-11-24T00:00:00Z","excerpt":"Los conectores o _pipes_ son símbolos que nos permiten encadenar varias operaciones. En inglés, _pipe_ significa literalmente 'tubería', porque la idea es que los datos fluyan a través de una serie de pasos. En esta guía comprenderemos cómo se usan los conectores, y cómo pueden ayudarnos a escribir mejores análisis.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/conectores/","tags":"","title":"Aprendiendo a usar conectores o _pipes_"},{"content":" Proyectos Antes de hacer cualquier trabajo que involucre datos con R, se recomienda crear un Proyecto de RStudio. Un proyecto es una forma de definir la carpeta específica donde vamos a guardar todos los scripts y archivos que vamos a necesitar. Se caracteriza por un archivo que termina en .Rproj, que marca nuestro espacio de trabajo: una sola carpeta que reúne todas las piezas de nuestro análisis.\nExiste un post clásico en la comunidad de R que explica esto mucho mejor: Project-oriented workflow, mejor conocido como si usas setwd() voy a ir a tu oficina a incendiar tu computador! ¿Por qué trabajar con Proyectos? El objetivo de un proyecto es tener todos nuestros datos y código en un sólo lugar, por lo que se vuelven autcontenidos; es decir, funcionarán sin depender de nada que esté fuera del proyecto.\nUn efecto secundario de lo anterior es que nuestros proyectos serán portátiles: podremos cambiarlos de lugar en nuestro computador, abrirlos en otro computador, correrlos en la nube, o enviárselos a alguien más, y gracias a que son autocontenidos, seguirán funcionando.\nA través del tiempo vamos a trabajar con distintas fuentes de datos, para objetivos distintos, incluso para llevar a cabo ideas o trabajos completamente diferentes. Los proyectos de RStudio nos permiten separar distintos espacios de trabajo, sin que los resultados de un proyecto contagien a otro. Si tenemos múltiples proyectos en los que trabajamos con R, al delimitarlos por medio de proyectos de RStudio podremos cambiar entre proyectos de manera rápida y sencilla.\nTambién podremos tener distintas sesiones de R abiertas al mismo tiempo, en distintas ventanas de RStudio, cada una con proyectos distintos. Esto significa que cada proyecto de R va a tener un entorno distinto y scripts distintos. De esta forma, no mezclamos cosas ni nos confundimos.\nBeneficios de trabajar con Proyectos El beneficio principal de trabajar con proyectos en R es que simplifican muchísimo la gestión de las rutas de archivos. Todos los archivos con los que trabajamos nuestros computadores se encuentran en ubicaciones distintas, y estas ubicaciones varían entre computador y computador, y entre distintas personas. Por ejemplo, los datos con lo que yo trabajo están en una carpeta que tú no tienes en tu computador, y estas carpetas están a su vez dentro de una carpeta de usuario, que es distinto para cada persona. Peor aún, las estructuras de archivos son distintas en los diferentes sistemas operativos. Al crear un proyecto de RStudio, las rutas de todos los archivos dentro del proyecto empezarán desde la carpeta del proyecto. Esto significa que, si trabajamos en un proyecto de RStudio, podemos omitir completamente las rutas completas del archivo en nuestros computadores.\nEn términos prácticos, un archivo que está ubicado en una carpeta como /home/users/bastian/Documentos/Análisis de datos/Trabajo/Ejemplo/script.R podrá ser cargado tan solo con la ruta script.R si abrimos un proyecto de R e la carpeta Ejemplo. Esto es bueno por dos razones: porque no tendremos que escribir la ruta del archivo completa, y porque si yo envío este proyecto a otra persona, o lo subo a internet para compartirlo, cualquier persona podrá ejecutar los scripts en sus computadores, porque los archivos dentro del proyecto podrán estar en cualquier lugar de su computador, pero gracias al proyecto, R encontrará los archivos.\nCrear un proyecto Para crear un proyecto nuevo, elegimos la opción New Project\u0026hellip; en el menú File, o apretamos el ícono del cubo celeste ubicado en la esquina superior derecha de RStudio y elegimos la primera opción.\nUna vez que creamos el proyecto, podemos abrir nuestro proyecto haciendo doble clic en el archivo que termina en \u0026ldquo;.Rproj\u0026rdquo;, seleccionando en RStudio el menú File y el ítem Open Project\u0026hellip; o Recent Projects, o en la esquina superior derecha de RStudio, donde aparece un icono celeste que contiene los proyectos recientes.\nEs muy importante que, antes de empezar a trabajar con R, te asegures de que estás dentro del proyecto correcto!\n","date":"2025-01-04T00:00:00Z","excerpt":"Antes de hacer cualquier trabajo que involucre datos con R, es recomendable crear un _Proyecto_ de RStudio. Ésta es una forma de definir la carpeta específica donde vamos a guardar todos los scripts y archivos que vamos a necesitar, lo cual previene muchos problemas, simplifica la carga y guardado de datos, y ordena nuestro trabajo.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/proyectos/","tags":"consejos ; básico","title":"Organizar el trabajo en Proyectos de RStudio"},{"content":"Luego de haber aprendido las funcionalidades básicas del lenguaje R, y habernos familiarizado con herramientas un poco más avanzadas de la programación en este lenguaje, ahora podemos aplicar estos aprendizajes a los datos. Aprenderemos a explorar, comprender, y navegar tablas de datos, tanto en la forma nativa de trabajar con R, como con la ayuda del paquete {dplyr}.\n{dplyr} La herramienta que utilizaremos para explorar, manipular, y transformar datos será el paquete {dplyr}. Este paquete, parte central del conjunto Tidyverse de herramientas para el análisis de datos con R, es uno de los más usados por la comunidad de R por su facilidad de uso.\nSe caracteriza porque casi todas sus funciones son escritas por medio de verbos, lo que hace que su sintaxis sea muy legible, ya que cada función se corresponde con una acción que estamos realizando sobre nuestros datos.\nNota sobre el uso de {dplyr} Probablemente el 98% de las cosas que necesitemos hacer con tablas de datos en R puedan hacerse por medio de {dplyr} y otros paquetes adyacentes a él. En mi opinión esto es bueno, porque creo que {dplyr} hace todo más sencillo, ordenado y fácil de interpretar. Sin embargo, creo que también es importante entender cómo se realizan las operaciones básicas sobre los datos con R base. Esto porque un entendimiento \u0026mdash;aunque sea básico\u0026mdash; de R base nos va a ayudar a comprender mejor el funcionamiento del lenguaje, y nos puede sacar de posibles problemas que encontremos. Es por esto que en este tutorial iremos viendo cómo realizar las operaciones básicas de manipulación de datos primero con R base y luego con {dplyr}.\nInstalación Para usar el paquete {dplyr}, así como cualquier otro paquete de R que no venga instalado por defecto, tenemos que instalarlo desde internet. La instalación de los paquetes en R se facilita por la función install.packages(), que se conecta a un servidor centralizado donde todos los paquetes son revisados y verificados manualmente por revisores humanos, para garantizar que sean seguros de usar y funcionales.\nInstalamos {dplyr} con el siguiente comando:\ninstall.packages(\u0026#34;dplyr\u0026#34;) Así como cuando instalamos una aplicación en nuestro celular o computador, solamente necesitamos instalar el paquete una vez. Pero cuando queramos usar el paquete, debemos cargarlo, que sería equivalente a abrir una aplicación ya instalada en tu celular o computador.\nPara cargar un paquete se utiliza la función library():\nlibrary(\u0026#34;dplyr\u0026#34;) Attaching package: 'dplyr' The following objects are masked from 'package:stats': filter, lag The following objects are masked from 'package:base': intersect, setdiff, setequal, union Después de cargar un paquete puede ser que aparezcan mensajes en la consola, pero en general podemos ignorarlos.\nDatos Las tablas de datos, también conocidas como dataframes, son formas de almacenar información por medio de filas y columnas. Usualmente, las filas corresponden a observaciones, y las columnas corresponden a variables, pero no siempre esto se cumple. Otra característica de las tablas o dataframes es que son rectangulares: todas las columnas tienen la misma cantidad de filas.\nUsemos {dplyr} para tener nuestra primera aproximación a los datos en tablas construyendo una tabla. Cómo planteamos en el primer tutorial, las tablas de datos en R son construidas a partir de vectores, o dicho de otra forma, las columnas de las tablas son vectores.\nCrear una tabla Creemos unos vectores y luego hagamos una tabla a partir de ellos:\nanimal \u0026lt;- c(\u0026#34;gato\u0026#34;, \u0026#34;paloma\u0026#34;, \u0026#34;rana\u0026#34;, \u0026#34;pollo\u0026#34;, \u0026#34;mapache\u0026#34;, \u0026#34;serpiente\u0026#34;) tipo \u0026lt;- c(\u0026#34;mamífero\u0026#34;, \u0026#34;ave\u0026#34;, \u0026#34;anfibio\u0026#34;, \u0026#34;ave\u0026#34;, \u0026#34;mamífero\u0026#34;, \u0026#34;reptil\u0026#34;) patas \u0026lt;- c(4, 2, 4, 2, 4, 0) Creamos tres vectores, que serán las columnas de nuestra tabla. Confirmemos que tienen el mismo largo:\n# confirmar que los tres casos se cumplen all(length(animal) == 6, length(tipo) == 6, length(patas) == 6) [1] TRUE Ahora, usemos los vectores para crear una tabla con {dplyr}:\n# crear una tabla con los vectores tabla \u0026lt;- tibble(animal, tipo, patas) tabla # A tibble: 6 × 3 animal tipo patas \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 gato mamífero 4 2 paloma ave 2 3 rana anfibio 4 4 pollo ave 2 5 mapache mamífero 4 6 serpiente reptil 0 Esta es la salida en la consola que tienen los dataframes creados con {dplyr}. En ella, podemos ver en la primera fila de texto en largo y ancho de la tabla (número de filas y columnas). Luego vemos los nombres de las tres columnas, y debajo de ellos vemos el tipo de cada columna (caracter, caracter y numérico). Las siguientes filas corresponden a los datos mismos de la tabla.\nExplorar una tabla Exploremos un poco las características de una tabla o dataframe:\nclass(tabla) [1] \u0026quot;tbl_df\u0026quot; \u0026quot;tbl\u0026quot; \u0026quot;data.frame\u0026quot; Las tablas creadas con {dplyr} son de la clase \u0026quot;tbl_df\u0026quot;, que hace referencia a tibble, el tipo específico de tablas de datos de este paquete, que son más amigables y fáciles de leer.\nlength(tabla) [1] 3 Se consultamos el largo del objeto con length(), obtenemos el número de columnas. Si queremos saber el número de filas, usamos nrow():\nnrow(tabla) [1] 6 Si queremos saber los nombres de las columnas de una tabla, podemos usar names(), o bien, la función glimpse(), que nos entrega un conveniente vistazo de los datos de nuestra tabla:\nnames(tabla) [1] \u0026quot;animal\u0026quot; \u0026quot;tipo\u0026quot; \u0026quot;patas\u0026quot; glimpse(tabla) Rows: 6 Columns: 3 $ animal \u0026lt;chr\u0026gt; \u0026quot;gato\u0026quot;, \u0026quot;paloma\u0026quot;, \u0026quot;rana\u0026quot;, \u0026quot;pollo\u0026quot;, \u0026quot;mapache\u0026quot;, \u0026quot;serpiente\u0026quot; $ tipo \u0026lt;chr\u0026gt; \u0026quot;mamífero\u0026quot;, \u0026quot;ave\u0026quot;, \u0026quot;anfibio\u0026quot;, \u0026quot;ave\u0026quot;, \u0026quot;mamífero\u0026quot;, \u0026quot;reptil\u0026quot; $ patas \u0026lt;dbl\u0026gt; 4, 2, 4, 2, 4, 0 Seleccionar datos Recordemos que podemos extraer subconjuntos de los vectores usando los corchetes []:\nanimal[5] [1] \u0026quot;mapache\u0026quot; animal[4:5] [1] \u0026quot;pollo\u0026quot; \u0026quot;mapache\u0026quot; animal[c(1, 2, 4)] [1] \u0026quot;gato\u0026quot; \u0026quot;paloma\u0026quot; \u0026quot;pollo\u0026quot; Con los vectores es sencillo, porque un vector es una unidad de datos unidimensional, donde con un número podemos seleccionar cualquiera de los elementos contenidos en el vector.\nLas tablas de datos no son unidimensionales, sino bidimensionales, dado que tienen filas y columnas. Entonces, para poder extraer elementos de distintas posiciones de una tabla, dentro de los corchetes habrá que indicar sus filas y/o columnas. Tenemos que indicar ya no sólo uno, sino dos argumentos: el primero refiere a las filas, y el segundo a las columnas.\ntabla[2, 3] # A tibble: 1 × 1 patas \u0026lt;dbl\u0026gt; 1 2 En este caso, extrajimos la observación que se encuentra en la fila 2 y en la columna 3.\nFilas Para extraer una fila de una tabla, dentro de los corchetes debemos indicar la posición de la fila, separado por una coma, y dejar en blanco el segundo argumento, que sería la selección de columnas.\ntabla[1, ] # A tibble: 1 × 3 animal tipo patas \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 gato mamífero 4 Indicando sólo el número de fila 1 y dejando en blanco la ubicación de la columna, seleccionamos la fila 1 entera.\nTambién podemos usar la función slice() para extraer una fila de una tabla:\nslice(tabla, 5) # A tibble: 1 × 3 animal tipo patas \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 mapache mamífero 4 Si queremos extraer la última fila de una tabla, podemos combinar slice() con la función nrow() que nos entrega la cantidad de filas de una tabla, resultando en extraer la fila que corresponde con el número de filas; es decir, la última:\nslice(tabla, nrow(tabla)) # A tibble: 1 × 3 animal tipo patas \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 serpiente reptil 0 Columnas Para extraer una columna, debemos indicar su posición en el segundo argumento dentro de los corchetes, dejando el primero vacío:\ntabla[, 1] # A tibble: 6 × 1 animal \u0026lt;chr\u0026gt; 1 gato 2 paloma 3 rana 4 pollo 5 mapache 6 serpiente También podemos indicar el nombre de la columna, entre comillas, para seleccionarla:\ntabla[, \u0026#34;animal\u0026#34;] # A tibble: 6 × 1 animal \u0026lt;chr\u0026gt; 1 gato 2 paloma 3 rana 4 pollo 5 mapache 6 serpiente Puede para ser extraño dejar la coma por sí sola dentro del corchete, así que en su lugar podemos usar la función select() para seleccionar columnas:\nselect(tabla, 1) # A tibble: 6 × 1 animal \u0026lt;chr\u0026gt; 1 gato 2 paloma 3 rana 4 pollo 5 mapache 6 serpiente select(tabla, \u0026#34;tipo\u0026#34;) # A tibble: 6 × 1 tipo \u0026lt;chr\u0026gt; 1 mamífero 2 ave 3 anfibio 4 ave 5 mamífero 6 reptil Otra forma de extraer una columna de una tabla de datos es abrir la tabla usando el operador $. Al escribir el nombre de la tabla de datos seguido del operador $, RStudio sugerirá los nombres de las columnas para que puedas extraerlas:\ntabla$animal [1] \u0026quot;gato\u0026quot; \u0026quot;paloma\u0026quot; \u0026quot;rana\u0026quot; \u0026quot;pollo\u0026quot; \u0026quot;mapache\u0026quot; \u0026quot;serpiente\u0026quot; A usar el operador $ para extraer columnas, se obtiene el vector que se usó para crear la columna, así que recibimos los datos en forma de vector.\nLa misma extracción de una columna como vector puede hacerse con la función pull() de {dplyr}, donde se entrega la tabla y la columna, y se obtiene la columna como vector:\npull(tabla, animal) [1] \u0026quot;gato\u0026quot; \u0026quot;paloma\u0026quot; \u0026quot;rana\u0026quot; \u0026quot;pollo\u0026quot; \u0026quot;mapache\u0026quot; \u0026quot;serpiente\u0026quot; Crear columnas Si queremos agregar una nueva columna a nuestro dataframe, tenemos que hacer una mezcla entre la extracción de columnas con el operador $ y la asignación de nuevos objetos:\nSi intentamos extraer una columna que no existe, recibimos un error:\ntabla$habitat Warning: Unknown or uninitialised column: `habitat`. NULL Sin embargo, si creamos un vector encima de esta columna que aún no existe, ésta se crea:\ntabla$habitat \u0026lt;- c(\u0026#34;urbano\u0026#34;, \u0026#34;urbano\u0026#34;, \u0026#34;rural\u0026#34;, \u0026#34;rural\u0026#34;, \u0026#34;urbano\u0026#34;, \u0026#34;rural\u0026#34;) tabla # A tibble: 6 × 4 animal tipo patas habitat \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 gato mamífero 4 urbano 2 paloma ave 2 urbano 3 rana anfibio 4 rural 4 pollo ave 2 rural 5 mapache mamífero 4 urbano 6 serpiente reptil 0 rural En otras palabras, para crear una nueva columna simplemente especificamos su nombre como parte de la tabla existente, y asignamos los datos que queremos que se contengan en esta nueva columna.\nOtra forma de crear columnas es con la función mutate(), que nos permite crear o modificar columnas existentes:\nmutate(tabla, cola = c(\u0026#34;sí\u0026#34;, \u0026#34;sí\u0026#34;, \u0026#34;no\u0026#34;, \u0026#34;no\u0026#34;, \u0026#34;sí\u0026#34;, \u0026#34;toda\u0026#34;)) # A tibble: 6 × 5 animal tipo patas habitat cola \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 gato mamífero 4 urbano sí 2 paloma ave 2 urbano sí 3 rana anfibio 4 rural no 4 pollo ave 2 rural no 5 mapache mamífero 4 urbano sí 6 serpiente reptil 0 rural toda Revisa un tutorial completo y en profundida de mutate() en este post! La diferencia es que con mutate() solamente estamos previsualizando el cambio, dado que no hemos asignado nada. Si queremos que la columna realmente se guarde en el dataframe, debemos asignar el resultado a un objeto nuevo, o sobreescribir el actual:\n# sobreescribir la tabla con la columna nueva tabla \u0026lt;- mutate(tabla, cola = c(\u0026#34;sí\u0026#34;, \u0026#34;sí\u0026#34;, \u0026#34;no\u0026#34;, \u0026#34;no\u0026#34;, \u0026#34;sí\u0026#34;, \u0026#34;toda\u0026#34;)) Filtrar datos Una de las tareas más recurrentes que vamos a hacer con tablas de datos, sobre todo cuando son muy grandes, es filtrar la información. El filtrado de datos funciona a partir de comparaciones, dado que al filtrar estamos especificando un criterio que deben cumplir los valores para mantenerse en la tabla, y todos los valores que no cumplan este criterio serán removidos.\nEntonces, para filtrar datos, escribimos una comparación que coincida (retorne TRUE) con los valores que deseamos recibir.\nSupongamos que queremos ver solamente las observaciones donde el valor de la variable patas sea mayor que 2. Primero, planteemos la comparación:\n# extraer los valores de la columna `patas` tabla$patas [1] 4 2 4 2 4 0 # comparar los valores con el número 2 tabla$patas \u0026gt; 2 [1] TRUE FALSE TRUE FALSE TRUE FALSE A partir de las comparaciones, recibimos una secuencia de valores lógicos (TRUE/FALSE) que indican si cada elemento del vector cumplió o no con la comparación planteada. Entonces, en principio, queremos mantener solamente las observaciones que retornaron TRUE.\nRecordemos que para filtrar filas de una tabla, dentro de los corchetes especificamos en la primera posición las filas que queremos extraer.\ntabla[1, ] # A tibble: 1 × 5 animal tipo patas habitat cola \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 gato mamífero 4 urbano sí Esto, sumado a lo anterior, son los principios que usaremos para filtrar las filas de una tabla de datos:\ntabla[tabla$patas \u0026gt; 2, ] # A tibble: 3 × 5 animal tipo patas habitat cola \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 gato mamífero 4 urbano sí 2 rana anfibio 4 rural no 3 mapache mamífero 4 urbano sí Usamos la comparación que retorna verdaderos y falsos dentro de los corchetes para seleccionar las filas de la tabla. Entonces, como el vector de la comparación entre el mismo largo que las filas de la tabla, en cada posición de la comparación que se obtuvo TRUE, se muestra la fila correspondiente, y si se obtuvo FALSE, se omite.\nOtros ejemplos de filtrado:\ntabla$animal == \u0026#34;mapache\u0026#34; [1] FALSE FALSE FALSE FALSE TRUE FALSE tabla[tabla$animal == \u0026#34;mapache\u0026#34;, ] # A tibble: 1 × 5 animal tipo patas habitat cola \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 mapache mamífero 4 urbano sí Una segunda opción es usar la función subset() de R base para filtrar tablas:\nsubset(tabla, patas \u0026lt; 3) # A tibble: 3 × 5 animal tipo patas habitat cola \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 paloma ave 2 urbano sí 2 pollo ave 2 rural no 3 serpiente reptil 0 rural toda subset(tabla, animal == \u0026#34;pollo\u0026#34;) # A tibble: 1 × 5 animal tipo patas habitat cola \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 pollo ave 2 rural no La alternativa de {dplyr}, filter(), es similar a subset(), pero tiene otras conveniencias que aprenderemos más adelante:\nfilter(tabla, tipo != \u0026#34;ave\u0026#34;) # A tibble: 4 × 5 animal tipo patas habitat cola \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 gato mamífero 4 urbano sí 2 rana anfibio 4 rural no 3 mapache mamífero 4 urbano sí 4 serpiente reptil 0 rural toda filter(tabla, animal %in% c(\u0026#34;mapache\u0026#34;, \u0026#34;gato\u0026#34;)) # A tibble: 2 × 5 animal tipo patas habitat cola \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 gato mamífero 4 urbano sí 2 mapache mamífero 4 urbano sí Renombrar columnas Para cambiar el nombre de una de las columnas, hay varias formas.\nUna forma, quizás más rudimentaria, sería crear una columna nueva con el nombre que deseamos, a partir de una columna existente, para luego eliminar la columna original:\n# columna original tabla$habitat [1] \u0026quot;urbano\u0026quot; \u0026quot;urbano\u0026quot; \u0026quot;rural\u0026quot; \u0026quot;rural\u0026quot; \u0026quot;urbano\u0026quot; \u0026quot;rural\u0026quot; # crear columna nueva a partir de la original tabla$zona \u0026lt;- tabla$habitat # eliminar la columna original tabla$habitat \u0026lt;- NULL # revisar cambios names(tabla) [1] \u0026quot;animal\u0026quot; \u0026quot;tipo\u0026quot; \u0026quot;patas\u0026quot; \u0026quot;cola\u0026quot; \u0026quot;zona\u0026quot; Éste procedimiento es similar al que vimos para crear nuevas columnas, ya que implica crear una columna que no existe aún (zona), a partir de un valor, en este caso, una columna de la misma tabla (tabla$habitat). De esta forma, duplicamos la columna, para luego eliminar la columna original al asignarle el valor nulo, NULL.\nOtra forma de renombrar columnas, aunque un poco confusa, es usando la función names(), que nos entrega los nombres de las columnas:\n# obtener columnas names(tabla) [1] \u0026quot;animal\u0026quot; \u0026quot;tipo\u0026quot; \u0026quot;patas\u0026quot; \u0026quot;cola\u0026quot; \u0026quot;zona\u0026quot; Al obtener los nombres de las columnas, realizamos una selección de la columna que queremos, usando los corchetes. Luego habría que asignar el nombre nuevo a la columna que sleeccionamos:\n# seleccionar columna a renombrar names(tabla)[names(tabla) == \u0026#34;patas\u0026#34;] [1] \u0026quot;patas\u0026quot; # asignar nuevo nombre a la columna seleccionada names(tabla)[names(tabla) == \u0026#34;patas\u0026#34;] \u0026lt;- \u0026#34;piernas\u0026#34; # ver cambios names(tabla) [1] \u0026quot;animal\u0026quot; \u0026quot;tipo\u0026quot; \u0026quot;piernas\u0026quot; \u0026quot;cola\u0026quot; \u0026quot;zona\u0026quot; Finalmente, la alternativa que entrega {dplyr} para renombrar columnas es la función rename(), a la cual se le entrega la tabla, y luego se le entrega el nombre nuevo de la columna, el signo = y el nombre original:\ntabla \u0026lt;- rename(tabla, nombre = animal) # ver cambios names(tabla) [1] \u0026quot;nombre\u0026quot; \u0026quot;tipo\u0026quot; \u0026quot;piernas\u0026quot; \u0026quot;cola\u0026quot; \u0026quot;zona\u0026quot; Ordenar observaciones Otra operación común con tallas de datos es ordenar una tabla según una variable numérica, para que las observaciones estén en orden decreciente o ascendente.\nPrimero, si queremos ordenar un vector, podemos usar la función sort() para obtener los valores del vector de menor a mayor:\nsort(tabla$piernas) [1] 0 2 2 4 4 4 En cierto sentido, ordenar una tabla es la misma operación que extraer filas de una tabla, solamente extraemos las filas de la tabla en el orden que deseamos. Por ejemplo:\ntabla # A tibble: 6 × 5 nombre tipo piernas cola zona \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 gato mamífero 4 sí urbano 2 paloma ave 2 sí urbano 3 rana anfibio 4 no rural 4 pollo ave 2 no rural 5 mapache mamífero 4 sí urbano 6 serpiente reptil 0 toda rural # extraer filas de la tabla en un orden específico tabla[c(6, 4, 4), ] # A tibble: 3 × 5 nombre tipo piernas cola zona \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 serpiente reptil 0 toda rural 2 pollo ave 2 no rural 3 pollo ave 2 no rural En este ejemplo, extrajimos tres filas de la tabla, pero la extracción que hacemos es específicamente de la fila que tiene valor 0 en la variable piernas, y luego las dos filas que tienen valor 2.\nComo memos, en este caso no nos serviría la función sort(). Porque para ordenar una tabla no necesitamos los valores ordenados del vector, sino las posiciones de los valores del vector ordenadas de manera que podamos mover las filas de una tabla. Para eso nos sirve la función order():\n# posiciones de los elementos ordenados order(tabla$piernas) [1] 6 2 4 1 3 5 Esta función si no se entrega la posición de los elementos del vector o de la columna en el orden de menor a mayor, y con estas posiciones podemos reordenar las filas de la tabla:\n# ordenar de menor a mayor tabla[order(tabla$piernas), ] # A tibble: 6 × 5 nombre tipo piernas cola zona \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 serpiente reptil 0 toda rural 2 paloma ave 2 sí urbano 3 pollo ave 2 no rural 4 gato mamífero 4 sí urbano 5 rana anfibio 4 no rural 6 mapache mamífero 4 sí urbano # ordenar de mayor a menor tabla[order(tabla$piernas, decreasing = TRUE), ] # A tibble: 6 × 5 nombre tipo piernas cola zona \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 gato mamífero 4 sí urbano 2 rana anfibio 4 no rural 3 mapache mamífero 4 sí urbano 4 paloma ave 2 sí urbano 5 pollo ave 2 no rural 6 serpiente reptil 0 toda rural La función arrange() de {dplyr} nos permite ordenar una tabla tan sólo entregándole la columna por la cual queremos ordenar:\narrange(tabla, piernas) # A tibble: 6 × 5 nombre tipo piernas cola zona \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 serpiente reptil 0 toda rural 2 paloma ave 2 sí urbano 3 pollo ave 2 no rural 4 gato mamífero 4 sí urbano 5 rana anfibio 4 no rural 6 mapache mamífero 4 sí urbano ¡Mucho más sencillo! Si deseamos ordenar en orden descendiente (mayor a menor), debemos meter la columna dentro de la función desc() (por descending):\narrange(tabla, desc(piernas)) # A tibble: 6 × 5 nombre tipo piernas cola zona \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 gato mamífero 4 sí urbano 2 rana anfibio 4 no rural 3 mapache mamífero 4 sí urbano 4 paloma ave 2 sí urbano 5 pollo ave 2 no rural 6 serpiente reptil 0 toda rural Bonus Crear tablas manualmente Si queremos crear una tabla de datos de prueba, para explorar estos aprendizajes, o bien porque nuestros datos son muy poquitos, podemos usar la función tribble() para escribir la tabla como si la estuviéramos haciendo en una aplicación de planillas de cálculo:\ntribble(~días, ~n, ~caso, \u0026#34;jueves\u0026#34;, 0, 1, \u0026#34;viernes\u0026#34;, 4, 0, \u0026#34;sábado\u0026#34;, 2, 1, ) # A tibble: 3 × 3 días n caso \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 jueves 0 1 2 viernes 4 0 3 sábado 2 1 Simplemente escribe los nombres de las columnas con una colita de chancho antes (~) en la primera fila, y hacia abajo puedes ir rellenando los valores de cada columna.\nEn ese tutorial nuevamente aprendimos herramientas básicas de R, esta vez orientadas a trabajar con datos en forma de tabla. Para esto, combinamos varios de los aprendizajes anteriores que habíamos aplicado a vectores, para reutilizarlos en esta nueva forma de organizar la información. Con los conocimientos obtenidos hasta ahora, ya sería posible llevar a cabo la mayor parte de las operaciones necesarias para la exploración de datos!\nEl siguiente paso es aprender a transformar datos con {dplyr} y {tidyr}, o bien puedes pasar a los tutoriales aplicados, donde practicaremos estos aprendizajes en conjunto de datos reales: con datos de proyecciones de población provenientes del Censo, y con datos de un catastro de asentamientos de vivienda informales.\n","date":"2025-02-15T00:00:00Z","excerpt":"Luego de haber aprendido las funcionalidades básicas del lenguaje y R, y habernos familiarizado con herramientas un poco más avanzadas de la programación en este lenguaje, ahora podemos aplicar estos aprendizajes a los datos. Aprenderemos a explorar, comprender, y navegar tablas de datos, tanto en la forma nativa de trabajar con R, como con la ayuda del paquete {dplyr}.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/dplyr_intro/","tags":"dplyr ; datos ; básico","title":"Introducción al manejo de datos con {dplyr}"},{"content":"En esta ocasión veremos la función que nos permite crear variables nuevas, a partir de los aprendizajes sobre manipulación de datos que vimos en tutoriales anteriores.\nPara crear variables nuevas, o transformar variables existentes, usaremos la función mutate(), que forma parte del paquete {dplyr}.\nEsquema de la creación de una columna a partir de dos columnas existentes: la tercera columna es la suma de las dos anteriores La función mutate() nos va a permitir crear nuevas variables, ya sea usando datos nuevos, alguna función que genera datos, modificando los datos de una columna existente, o realizando un cálculo a partir de una o más columnas de la tabla.\nSi necesitas aprender {dplyr} desde el principio, revisa el tutorial de introducción a {dplyr}. Introducción Vamos a empezar con ejemplos básicos y luego pasaremos a datos reales.\nLo primero que haremos será cargar el paquete {dplyr}:\n# install.packages(\u0026#34;dplyr\u0026#34;) library(dplyr) Crear nuevas columnas Empezaremos con una tabla de datos de ejemplo, llamada creativamente ejemplo:\nlibrary(dplyr) ejemplo \u0026lt;- tibble(números = 1:3) ejemplo # A tibble: 3 × 1 números \u0026lt;int\u0026gt; 1 1 2 2 3 3 Podemos usar mutate() para crear una nueva variable o columna (¡son sinónimos! 🤓).\nPara crear usar mutate(), aplicamos esta función a un data frame conectando los datos a la función con un conector (|\u0026gt; o %\u0026gt;%).\nUn conector en R es un operador que nos permite conectar datos (o el resultado de una operación) con la siguiente operación. Para más información, revisa este tutorial ejemplo |\u0026gt; mutate(prueba = 1) # A tibble: 3 × 2 números prueba \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; 1 1 1 2 2 1 3 3 1 Creamos una nueva columna llamada prueba, que contiene el valor 1. Como le dimos un sólo valor (1), el valor se repite en todas las filas.\n¿Qué hicimos?\nPara crear la variable prueba, dentro de mutate():\nPrimero pusimos el nombre de la columna que queríamos crear (prueba), Luego un signo igual (=), que indica que ésto va a ser aquello, Finalmente el valor que queremos que contenga la nueva columna ( 1). Creemos otra columna, llamada saludo:\nejemplo |\u0026gt; mutate(saludo = \u0026#34;hola\u0026#34;) # A tibble: 3 × 2 números saludo \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; 1 1 hola 2 2 hola 3 3 hola De esta forma podemos crear variables que contengan un único valor.\nNotamos que en el ejemplo anterior no existe la primera columna que creamos. Ésto es porque nunca estamos sobreescribiendo ni editando los datos, a menos que hagamos una asignación (\u0026lt;-) para guardar el resultado como un nuevo objeto. También podemos usar funciones para crear los valores que va a tener una columna. Por ejemplo, creemos una columna llamada azar que contenga números aleatorios entre 1 y 100:\nejemplo |\u0026gt; mutate(azar = sample(1:100, size = 3)) # A tibble: 3 × 2 números azar \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; 1 1 5 2 2 70 3 3 98 En este ejemplo, los valores de la columna son definidos por la función que usamos.\nModificar variables existentes También podemos usar mutate() para modificar columnas que ya existen en el data frame. Esto se hace simplemente poniendo en mutate() como nombre de la columna que vamos a crear el nombre de una columna existente que queremos modificar.\nPor ejemplo, cambiemos los valores de números por unos distintos:\nejemplo |\u0026gt; mutate(números = 4:6) # A tibble: 3 × 1 números \u0026lt;int\u0026gt; 1 4 2 5 3 6 En este caso, sobreescribimos la columna con datos nuevos.\nVolvamos a ver los datos originales:\nejemplo # A tibble: 3 × 1 números \u0026lt;int\u0026gt; 1 1 2 2 3 3 En vez de cambiar completamente los datos de una columna, podemos editar una columna existente, usando los valores de la misma columna y ejecutando alguna operación sobre ellos, para obtener una versión editada de la misma columna:\nejemplo |\u0026gt; mutate(números = números * 100) # A tibble: 3 × 1 números \u0026lt;dbl\u0026gt; 1 100 2 200 3 300 En este ejemplo, editamos la columna números, que contenía los valores 1, 2 y 3, declarando que números = números * 100; es decir, la columna va a ser igual a ella misma pero multiplicada por 100. Por eso ahora contiene los valores 100, 200 y 300, que son el resultado de multiplicar cada valor original por 100.\nCrear una columna nueva en base a otra existente Siguiendo el ejemplo anterior, podemos crear una nueva columna llamada doble que contenga el doble de los valores de la columna números:\nejemplo |\u0026gt; mutate(doble = números * 2) # A tibble: 3 × 2 números doble \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; 1 1 2 2 2 4 3 3 6 Así creamos una nueva columna basada en datos de una columna existente.\nTambién podemos usar funciones para crear las variables nuevas:\nejemplo |\u0026gt; mutate(doble = números * 2) |\u0026gt; mutate(texto = paste(doble, \u0026#34;es el doble de\u0026#34;, números)) # A tibble: 3 × 3 números doble texto \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1 2 2 es el doble de 1 2 2 4 4 es el doble de 2 3 3 6 6 es el doble de 3 Estas son las piezas básicas para trabajar con variables o columnas. ¡Ahora veámoslo con datos!\nEjemplo con datos A continuación, usaremos datos sobre países de América Latina, que fueron obtenidos desde tablas de Wikipedia por medio de web scraping. Puedes revisar el script con el código del scraping aquí.\nEjecutaremos el siguiente código para obtener una tabla llamada datos en nuestro entorno de R:\ndatos \u0026lt;- tribble( ~pais, ~pobreza, ~esperanza, ~escolaridad, \u0026#34;Chile\u0026#34;, 5, 81.17, 11.29, \u0026#34;Uruguay\u0026#34;, 6, 78.14, 10.54, \u0026#34;Argentina\u0026#34;, 11, 77.69, 11.18, \u0026#34;Costa Rica\u0026#34;, 13, 80.8, 8.84, \u0026#34;Panamá\u0026#34;, 13, 79.59, 10.83, \u0026#34;Bolivia\u0026#34;, 15, 68.58, 10.02, \u0026#34;Paraguay\u0026#34;, 20, 73.84, 8.93, \u0026#34;México\u0026#34;, 22, 75.07, 9.35, \u0026#34;Brasil\u0026#34;, 24, 75.85, 8.43, \u0026#34;El Salvador\u0026#34;, 28, 72.1, 7.3 ) La tabla datos contiene información sobre porcentaje de pobreza, esperanza de vida promedio y años de escolaridad promedio en varios países de América Latina.\nCrear una variable nueva con mutate() Usemos mutate() para crear una nueva columna llamada escolaridad_redondeada, que contenga los valores de la columna escolaridad redondeados al número entero más cercano:\ndatos |\u0026gt; select(pais, escolaridad) |\u0026gt; mutate(escolaridad_redondeada = round(escolaridad)) # A tibble: 10 × 3 pais escolaridad escolaridad_redondeada \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Chile 11.3 11 2 Uruguay 10.5 11 3 Argentina 11.2 11 4 Costa Rica 8.84 9 5 Panamá 10.8 11 6 Bolivia 10.0 10 7 Paraguay 8.93 9 8 México 9.35 9 9 Brasil 8.43 8 10 El Salvador 7.3 7 En este caso aplicamos una función a una columna existente para obtener una columna nueva.\nCrear una variable condicional con if_else() Podemos usar mutate() junto con la función if_else() para crear una nueva columna basada en condiciones. Por ejemplo, creemos una columna llamada pobreza_nivel que indique si el porcentaje de pobreza alto (es mayor al 10%) o bajo:\ndatos |\u0026gt; select(pais, pobreza) |\u0026gt; mutate(pobreza_nivel = if_else(pobreza \u0026gt;= 10, \u0026#34;Alto\u0026#34;, \u0026#34;Bajo\u0026#34;)) # A tibble: 10 × 3 pais pobreza pobreza_nivel \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Chile 5 Bajo 2 Uruguay 6 Bajo 3 Argentina 11 Alto 4 Costa Rica 13 Alto 5 Panamá 13 Alto 6 Bolivia 15 Alto 7 Paraguay 20 Alto 8 México 22 Alto 9 Brasil 24 Alto 10 El Salvador 28 Alto La función if_else() permite crear variables condicionales. La condición que se defina será evaluada para cada valor, y se aplicará el resultado que corresponda si la condición es verdadera o falsa. Crear una variable con múltiples condiciones usando case_when() Podemos usar mutate() junto con la función case_when() para crear una nueva columna basada en múltiples condiciones. Por ejemplo, creemos una columna llamada esperanza_nivel que clasifique la esperanza de vida en \u0026ldquo;Alta\u0026rdquo;, \u0026ldquo;Media\u0026rdquo; y \u0026ldquo;Baja\u0026rdquo;:\ndatos |\u0026gt; select(pais, esperanza) |\u0026gt; mutate(esperanza_nivel = case_when( esperanza \u0026gt;= 80 ~ \u0026#34;Alta\u0026#34;, esperanza \u0026gt;= 70 ~ \u0026#34;Media\u0026#34;, esperanza \u0026lt; 70 ~ \u0026#34;Baja\u0026#34; )) # A tibble: 10 × 3 pais esperanza esperanza_nivel \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Chile 81.2 Alta 2 Uruguay 78.1 Media 3 Argentina 77.7 Media 4 Costa Rica 80.8 Alta 5 Panamá 79.6 Media 6 Bolivia 68.6 Baja 7 Paraguay 73.8 Media 8 México 75.1 Media 9 Brasil 75.8 Media 10 El Salvador 72.1 Media En el ejemplo anterior usamos números arbitrarios para recodificar una variable contínua en una variable categórica. En un caso real, deberíamos analizar la distribución de los datos para definir los puntos de corte adecuados. Una forma muy rudimentaria de hacer ésto es usando desviaciones estándar:\ndatos |\u0026gt; mutate(esperanza_nivel = case_when( esperanza \u0026gt;= mean(esperanza) + sd(esperanza) ~ \u0026#34;Alta\u0026#34;, esperanza \u0026lt;= mean(esperanza) - sd(esperanza) ~ \u0026#34;Baja\u0026#34;, TRUE ~ \u0026#34;Media\u0026#34; )) # A tibble: 10 × 5 pais pobreza esperanza escolaridad esperanza_nivel \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Chile 5 81.2 11.3 Alta 2 Uruguay 6 78.1 10.5 Media 3 Argentina 11 77.7 11.2 Media 4 Costa Rica 13 80.8 8.84 Alta 5 Panamá 13 79.6 10.8 Media 6 Bolivia 15 68.6 10.0 Baja 7 Paraguay 20 73.8 8.93 Media 8 México 22 75.1 9.35 Media 9 Brasil 24 75.8 8.43 Media 10 El Salvador 28 72.1 7.3 Baja La desviación estándar es una medida de dispersión que indica cuánto varían los datos respecto a su promedio. En este caso se usa para definir puntos de corte objetivos basados en la distribución de los datos. En este caso clasificamos la esperanza de vida en función de su distancia a la media, usando una desviación estándar como punto de corte: países que tengan valores que sean mayores al promedio más una desviación estándar serán altos, los que tengan valores menores al promedio menos una desviación estándar serán bajos, y el resto serán medios.\nCalcular variables por grupos con group_by() Podemos usar group_by() para crear variables nuevas basadas en grupos dentro de los datos; es decir, cuyo cálculo se haga dentro de cada grupo definido.\nPrimero creemos un grupo:\ndatos_grupo \u0026lt;- datos |\u0026gt; mutate(pobreza_nivel = if_else(pobreza \u0026gt;= 15, \u0026#34;Alto\u0026#34;, \u0026#34;Bajo\u0026#34;)) datos_grupo # A tibble: 10 × 5 pais pobreza esperanza escolaridad pobreza_nivel \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Chile 5 81.2 11.3 Bajo 2 Uruguay 6 78.1 10.5 Bajo 3 Argentina 11 77.7 11.2 Bajo 4 Costa Rica 13 80.8 8.84 Bajo 5 Panamá 13 79.6 10.8 Bajo 6 Bolivia 15 68.6 10.0 Alto 7 Paraguay 20 73.8 8.93 Alto 8 México 22 75.1 9.35 Alto 9 Brasil 24 75.8 8.43 Alto 10 El Salvador 28 72.1 7.3 Alto Ahora calculemos el promedio de esperanza de vida dentro de cada grupo de pobreza usando group_by() y mutate():\ndatos_grupo |\u0026gt; select(pais, pobreza_nivel, esperanza) |\u0026gt; group_by(pobreza_nivel) |\u0026gt; mutate(esperanza_promedio = mean(esperanza)) |\u0026gt; mutate(esperanza_diferencia = esperanza - esperanza_promedio) # A tibble: 10 × 5 # Groups: pobreza_nivel [2] pais pobreza_nivel esperanza esperanza_promedio esperanza_diferencia \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Chile Bajo 81.2 79.5 1.69 2 Uruguay Bajo 78.1 79.5 -1.34 3 Argentina Bajo 77.7 79.5 -1.79 4 Costa Rica Bajo 80.8 79.5 1.32 5 Panamá Bajo 79.6 79.5 0.112 6 Bolivia Alto 68.6 73.1 -4.51 7 Paraguay Alto 73.8 73.1 0.752 8 México Alto 75.1 73.1 1.98 9 Brasil Alto 75.8 73.1 2.76 10 El Salvador Alto 72.1 73.1 -0.988 En este ejemplo, primero agrupamos los datos por la variable pobreza_nivel usando group_by(), y luego usamos mutate() para crear una nueva columna llamada esperanza_promedio, que contiene el promedio de esperanza de vida dentro de cada grupo. Finalmente, creamos otra columna llamada esperanza_diferencia, que contiene la diferencia entre la esperanza de vida individual y el promedio del grupo.\nDe este modo obtuvimos una nueva variable que muestra la diferencia de cada país respecto al promedio de su grupo; una especie de indicador.\nCrear varias columnas al mismo tiempo con across() Finalmente, podemos modificar o crear varias columnas nuevas al mismo tiempo usando mutate() junto con la función across(). Con across(), especificamos las variables que queremos afectar, y luego la fórmula que les vamos a aplicar.\nPor ejemplo, podemos redondear todas las columnas al mismo tiempo:\ndatos |\u0026gt; mutate( across(c(pobreza, esperanza, escolaridad), ~round(.x) ) ) # A tibble: 10 × 4 pais pobreza esperanza escolaridad \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Chile 5 81 11 2 Uruguay 6 78 11 3 Argentina 11 78 11 4 Costa Rica 13 81 9 5 Panamá 13 80 11 6 Bolivia 15 69 10 7 Paraguay 20 74 9 8 México 22 75 9 9 Brasil 24 76 8 10 El Salvador 28 72 7 La función que aplicamos se escribe con colita de chancho (~) para indicar que todas las variables especificadas van a ser afectadas por la misma función, representadas dentro de la función como .x 🐷\nIncluso podemos hacerlo en menos código, al especificar que queremos afectar todas las columnas donde (where) sean numéricas (is.numeric):\ndatos |\u0026gt; mutate( across(where(is.numeric), ~round(.x) ) ) # A tibble: 10 × 4 pais pobreza esperanza escolaridad \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Chile 5 81 11 2 Uruguay 6 78 11 3 Argentina 11 78 11 4 Costa Rica 13 81 9 5 Panamá 13 80 11 6 Bolivia 15 69 10 7 Paraguay 20 74 9 8 México 22 75 9 9 Brasil 24 76 8 10 El Salvador 28 72 7 Esto trae el beneficio de no tener que referirnos a las columnas por sus nombres!\nOtro ejemplo sería aplicar una función que transforme todos los valores numéricos en texto, agregando una palabra antes del número:\ndatos |\u0026gt; mutate( across( where(is.numeric), ~paste(\u0026#34;Valor:\u0026#34;, .x) ) ) # A tibble: 10 × 4 pais pobreza esperanza escolaridad \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Chile Valor: 5 Valor: 81.17 Valor: 11.29 2 Uruguay Valor: 6 Valor: 78.14 Valor: 10.54 3 Argentina Valor: 11 Valor: 77.69 Valor: 11.18 4 Costa Rica Valor: 13 Valor: 80.8 Valor: 8.84 5 Panamá Valor: 13 Valor: 79.59 Valor: 10.83 6 Bolivia Valor: 15 Valor: 68.58 Valor: 10.02 7 Paraguay Valor: 20 Valor: 73.84 Valor: 8.93 8 México Valor: 22 Valor: 75.07 Valor: 9.35 9 Brasil Valor: 24 Valor: 75.85 Valor: 8.43 10 El Salvador Valor: 28 Valor: 72.1 Valor: 7.3 En este caso, el símbolo .x hace que el valor de cada observación se ponga en otro lugar de la función, en este caso al final de la función paste().\nPodemos combinar mutate() con uno o varios mutate(across()) para crear o modificar varias columnas al mismo tiempo. Por ejemplo, podemos agregarle el signo de porcentaje a pobreza, luego redondear las columnas esperanza y escolaridad, y al final agregar un mismo texto a dos columnas:\ndatos |\u0026gt; mutate( # redactar pobreza = paste(pobreza, \u0026#34;%\u0026#34;, sep = \u0026#34;\u0026#34;), # redondear across(c(esperanza, escolaridad), ~round(.x, 0)), # redactar across(c(esperanza, escolaridad), ~paste(.x, \u0026#34;años\u0026#34;)) ) # A tibble: 10 × 4 pais pobreza esperanza escolaridad \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Chile 5% 81 años 11 años 2 Uruguay 6% 78 años 11 años 3 Argentina 11% 78 años 11 años 4 Costa Rica 13% 81 años 9 años 5 Panamá 13% 80 años 11 años 6 Bolivia 15% 69 años 10 años 7 Paraguay 20% 74 años 9 años 8 México 22% 75 años 9 años 9 Brasil 24% 76 años 8 años 10 El Salvador 28% 72 años 7 años En los casos de across() que hemos visto, las variable son editadas o modificadas, y por ende sobreescritas. Pero también podemos hacer que se creen variables nuevas! Pero, lógicamente, como estamos editando varias columnas a la vez, no podemos darles a todas un nombre distinto. En este caso, usamos el argumento .names dentro de across() para definir un patrón de nombres para las nuevas columnas:\ndatos |\u0026gt; mutate( across(c(pobreza, esperanza, escolaridad), ~round(.x), .names = \u0026#34;{.col}_redondeada\u0026#34; ) ) # A tibble: 10 × 7 pais pobreza esperanza escolaridad pobreza_redondeada esperanza_redondeada \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Chile 5 81.2 11.3 5 81 2 Uruguay 6 78.1 10.5 6 78 3 Argent… 11 77.7 11.2 11 78 4 Costa … 13 80.8 8.84 13 81 5 Panamá 13 79.6 10.8 13 80 6 Bolivia 15 68.6 10.0 15 69 7 Paragu… 20 73.8 8.93 20 74 8 México 22 75.1 9.35 22 75 9 Brasil 24 75.8 8.43 24 76 10 El Sal… 28 72.1 7.3 28 72 # ℹ 1 more variable: escolaridad_redondeada \u0026lt;dbl\u0026gt; En este caso, usamos {.col} dentro de .names para indicar que el nombre de la nueva columna va a ser igual al nombre de la columna original (representada por .col), seguido del texto _redondeada. Esta sintaxis para crear textos se llama glue y es muy útil para crear textos dinámicos en R.\nEstas son las herramientas básicas que necesitas para crear y modificar variables en R! Al final se trata de usar mutate() para crear algo nuevo o editar algo existente, usando valores nuevos, otras columnas, o funciones, y combinar estas herramientas para llevar a cabo lo que necesitemos, ya sea limpieza de datos o análisis de datos más complejos.\n","date":"2025-12-11T00:00:00Z","excerpt":"La función `mutate()` es una de las principales en R, ya que te permite crear variables nuevas o transformar variables existentes en un _data frame_. Este tutorial presenta ejemplos muy básicos del uso de esta función, y va avanzando a casos más reales de su uso.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/dplyr_mutate/","tags":"dplyr ; básico","title":"Crear variables nuevas con `dplyr::mutate()`"},{"content":"En el tutorial de introducción a {dplyr} aprendimos a ordenar, seleccionar, y filtrar datos tabulares. Con estas operaciones básicas deberíamos poder desenvolvernos con tablas de datos.\nLuego aprendimos a crear y modificar variables con la función mutate() de {dplyr}.\nAhora veremos cómo calcular resúmenes de datos. Con esto nos referimos a tomar todas las observaciones o filas de una tabla, y aplicar algún cálculo para reducir todas las filas a un solo dato. Esto sirve, por ejemplo, para calcular la suma o el promedio de una columna, y para calcular estadísticos descriptivos.\nlibrary(dplyr) options(scipen = 999) # desactivar notación científica Si necesitas aprender {dplyr} desde el principio, revisa el tutorial de introducción a {dplyr}. Introducción Para este tutorial vamos a trabajar con datos de población de países de América del Sur, obtenidas desde una tabla de Wikipedia usando [web scraping](/tags/web-scraping/]. Puedes ver el código del web scraping aquí.\nPara cargar los datos en tu entorno de R, ejecuta el siguiente código:\ndatos \u0026lt;- tribble( ~pais, ~superficie, ~poblacion, ~idh, \u0026#34;Brasil\u0026#34;, 8515767, 2.08e+08, 0.765, \u0026#34;Argentina\u0026#34;, 2780400, 44500000, 0.845, \u0026#34;Perú\u0026#34;, 1286216, 32200000, 0.777, \u0026#34;Colombia\u0026#34;, 1141748, 5e+07, 0.767, \u0026#34;Bolivia\u0026#34;, 1098581, 11200000, 0.718, \u0026#34;Venezuela\u0026#34;, 916445, 31800000, 0.711, \u0026#34;Chile\u0026#34;, 756102, 17800000, 0.851, \u0026#34;Paraguay\u0026#34;, 406752, 7100000, 0.728, \u0026#34;Ecuador\u0026#34;, 256370, 16900000, 0.759, \u0026#34;Guyana\u0026#34;, 214970, 8e+05, 0.682, \u0026#34;Uruguay\u0026#34;, 176215, 3500000, 0.817, \u0026#34;Surinam\u0026#34;, 163820, 5e+05, 0.738, \u0026#34;Guayana Francesa\u0026#34;, 83846, 3e+05, 0.901, \u0026#34;Trinidad y Tobago\u0026#34;, 5128, 1300000, 0.796 ) Como hemos visto en el tutorial introductorio de R, para calcular una suma usamos la función sum():\ntotal \u0026lt;- sum(datos$poblacion) format(total, big.mark = \u0026#34;.\u0026#34;, scientific = FALSE) [1] \u0026quot;425.900.000\u0026quot; Mientras que para calcular un promedio usamos la función mean():\npromedio \u0026lt;- mean(datos$idh) round(promedio, 2) [1] 0.78 Cualquier función como las anteriores, que reciba varios datos y entregue uno solo, será una función que podemos usar dentro de summarize() para obtener resúmenes de datos.\nResumir datos con summarize() La función summarize() calcula resúmenes de las tablas de datos, a partir de una o varias funciones que van a resumir los datos a una sola fila.\nEsquema de un resumen de datos: se pasa de varias filas a una sola Para calcular el resumen de los valores de una variable, dentro de summarize() ubicamos la función necesaria, aplicada a la columna que nos interesa resumir.\nCalculemos la suma (sum()) de la columna poblacion:\ndatos |\u0026gt; summarize( suma = sum(poblacion) ) # A tibble: 1 × 1 suma \u0026lt;dbl\u0026gt; 1 425900000 Redujimos las 14 filas de nuestra tabla de datos y obtuvimos una sola fila, que resume todos los valores de la poblacion a partir de una suma.\nDentro de summarize() podemos calcular varios resúmenes al mismo tiempo.\nAgreguemos una segunda línea para calcular el promedio del índice de desarrollo humano (idh):\ndatos |\u0026gt; summarize( suma = sum(poblacion), promedio = mean(idh) ) # A tibble: 1 × 2 suma promedio \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 425900000 0.775 Estadísticos descriptivos De este modo podemos calcular datos que resumen las observaciones, por ejemplo, calcular estadísticas descriptivos relevantes a los datos que estamos trabajando:\ndatos |\u0026gt; # calcular estadísticos descriptivos summarize( poblacion_total = sum(poblacion), poblacion_promedio = mean(poblacion), poblacion_minima = min(poblacion), poblacion_maxima = max(poblacion) ) |\u0026gt; # agregar puntos de miles mutate( across( where(is.numeric), ~format(.x, big.mark = \u0026#34;.\u0026#34;) ) ) # A tibble: 1 × 4 poblacion_total poblacion_promedio poblacion_minima poblacion_maxima \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 425.900.000 30.421.429 300.000 208.000.000 Agregamos un mutate(across()) para aplicar una función a todas las columnas al mismo tiempo. Para más información, revisa este tutorial. Resúmenes por grupos Tal que como vimos con mutate(), podemos anteponer la función de agrupación group_by() para que los resúmenes de datos se calculen por grupos. Esto significa que, en vez de obtener una sola fila de resumen, obtendremos una fila por cada grupo de la variable que elijamos para agrupar los datos.\nPrimero creemos una variable que nos permita agrupar los datos:\ndatos_2 \u0026lt;- datos |\u0026gt; # crear grupos por índice de desarrollo humano mutate(grupo = case_when(poblacion \u0026gt;= 20000000 ~ \u0026#34;Alta\u0026#34;, poblacion \u0026gt;= 5000000 ~ \u0026#34;Media\u0026#34;, poblacion \u0026lt; 5000000 ~ \u0026#34;Baja\u0026#34;)) |\u0026gt; # ordenar factor mutate(grupo = factor(grupo, levels = c(\u0026#34;Baja\u0026#34;, \u0026#34;Media\u0026#34;, \u0026#34;Alta\u0026#34;))) datos_2 |\u0026gt; count(grupo) # A tibble: 3 × 2 grupo n \u0026lt;fct\u0026gt; \u0026lt;int\u0026gt; 1 Baja 5 2 Media 4 3 Alta 5 Ahora, usando group_by(), podemos calcular los resúmenes por cada grupo de población:\ndatos_2 |\u0026gt; group_by(grupo) |\u0026gt; summarize( poblacion_total = sum(poblacion), poblacion_promedio = mean(poblacion), idh_promedio = mean(idh), idh_maximo = max(idh) ) # A tibble: 3 × 5 grupo poblacion_total poblacion_promedio idh_promedio idh_maximo \u0026lt;fct\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Baja 6400000 1280000 0.787 0.901 2 Media 53000000 13250000 0.764 0.851 3 Alta 366500000 73300000 0.773 0.845 El uso de group_by() es muy conveniente, porque podemos tener un mismo código para calcular resúmenes estadísticos, y con tan solo cambiar la variable de agrupación obtendremos resultados actualizados.\nResúmenes de varias variables con across() En lugar de tener que estar especificando las columnas que queremos resumir, podemos calcular un resumen para varias columnas al mismo tiempo. Igual que con mutate(), esto lo logramos usando across() dentro de summarize(), que permite aplicar las operaciones a varias columnas a la vez.\nPor ejemplo, para calcular un resumen de todas las columnas numéricas, indicamos dentro de across() que queremos aplicar la operación donde (where()) las variables sean numéricas (is.numeric):\ndatos_2 |\u0026gt; summarize( # resumir los datos across( # donde las columnas where(is.numeric), # sean numéricas ~mean(.x) # calculando el promedio ) ) # A tibble: 1 × 3 superficie poblacion idh \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 1271597. 30421429. 0.775 Para controlar el nombre que tendrán las columnas resultantes se puede usar el argumento .names:\ndatos_2 |\u0026gt; summarise( across(where(is.numeric), ~mean(.x), .names = \u0026#34;resumen_{.col}_promedio\u0026#34; # cambiar el nombre de columnas ) ) # A tibble: 1 × 3 resumen_superficie_promedio resumen_poblacion_promedio resumen_idh_promedio \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 1271597. 30421429. 0.775 O bien, se pueden poner las funciones de resumen dentro de una lista, donde puedes entregarle el nombre que tendrá el resultado:\ndatos_2 |\u0026gt; summarise( across(where(is.numeric), list( \u0026#34;promedio\u0026#34; = ~mean(.x) ) ) ) # A tibble: 1 × 3 superficie_promedio poblacion_promedio idh_promedio \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 1271597. 30421429. 0.775 Múltiples resúmenes a la vez También es posible calcular resúmenes usando más de una función, por ejemplo para calcular los promedios y las sumas al mismo tiempo, entregando las funciones dentro de una lista:\ndatos_2 |\u0026gt; summarise( across(where(is.numeric), list( \u0026#34;promedio\u0026#34; = ~mean(.x), \u0026#34;suma\u0026#34; = ~sum(.x) ) ) ) |\u0026gt; glimpse() Rows: 1 Columns: 6 $ superficie_promedio \u0026lt;dbl\u0026gt; 1271597 $ superficie_suma \u0026lt;dbl\u0026gt; 17802360 $ poblacion_promedio \u0026lt;dbl\u0026gt; 30421429 $ poblacion_suma \u0026lt;dbl\u0026gt; 425900000 $ idh_promedio \u0026lt;dbl\u0026gt; 0.7753571 $ idh_suma \u0026lt;dbl\u0026gt; 10.855 O bien, podemos aprovechar across() para reducir el código que escribimos en combinación con resúmenes individuales:\ndatos_2 |\u0026gt; summarize( \u0026#34;idh_prom\u0026#34; = mean(idh), across(c(superficie, poblacion), list(\u0026#34;suma\u0026#34; = ~sum(.x)) ) ) # A tibble: 1 × 3 idh_prom superficie_suma poblacion_suma \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 0.775 17802360 425900000 Obviamente podemos combinar lo anterior con la agrupación para obtener un completo resumen de datos de la tabla:\ndatos_2 |\u0026gt; group_by(grupo) |\u0026gt; summarise( across(where(is.numeric), list( \u0026#34;promedio\u0026#34; = ~mean(.x), \u0026#34;suma\u0026#34; = ~sum(.x) ) ) ) # A tibble: 3 × 7 grupo superficie_promedio superficie_suma poblacion_promedio poblacion_suma \u0026lt;fct\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Baja 128796. 643979 1280000 6400000 2 Media 629451. 2517805 13250000 53000000 3 Alta 2928115. 14640576 73300000 366500000 # ℹ 2 more variables: idh_promedio \u0026lt;dbl\u0026gt;, idh_suma \u0026lt;dbl\u0026gt; ","date":"2025-12-14T00:00:00Z","excerpt":"En este tutorial veremos cómo calcular resúmenes de datos con la función `summarize()`, que nos permite tomar las filas de una tabla de datos, y aplicarles algún cálculo para reducir todas las filas a un solo dato; por ejemplo, para calcular la suma o el promedio de una columna.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/dplyr_summarize/","tags":"dplyr ; básico","title":"Calcular resúmenes de datos con `dplyr::summarize()`"},{"content":"Los datos pueden existir en distintos formatos o estructuras, y el poder transformar los datos entre distintas estructuras es una habilidad clave.\nEn este tutorial aprenderemos a usar las funciones pivot_longer() y pivot_wider() del paquete {tidyr} para cambiar entre formatos de datos ancho (wide) y largo (long).\nÍndice Formatos de datos Datos en formato ancho Datos en formato largo Transformación de formatos Desde ancho hacia largo Desde largo hacia ancho ¿Cuándo usar cada formato? Formatos de datos No existe una forma estandarizada de estructurar los datos. La estructura o formato que tengan los datos va a depender de muchas cosas: de dónde provienen los datos, para que se van a usar los datos, etc. Dependiendo de estas distintas necesidades los datos se pueden estructurar distintos.\nDos de estos formatos o estructuras de datos son los datos anchos y largos.\nDatos en formato ancho El formato ancho (wide) es cuando cada variable que describe las observaciones tiene su propia columna. Esto resulta en varias columnas, cada una describiendo un aspecto distinto de cada observación.\nPor ejemplo:\npais pobreza esperanza escolaridad Chile 5 81.17 11.29 Uruguay 6 78.14 10.54 Argentina 11 77.69 11.18 Costa Rica 13 80.80 8.84 Panamá 13 79.59 10.83 Bolivia 15 68.58 10.02 Tabla con datos en formato ancho, con las variables distribuidas en columnas Generalmente este formato se usa para presentar los datos, ya que es fácil de leer y comparar valores lado a lado.\nDatos en formato largo El formato largo (long) es cuando las variables se encuentran en una sola columna, y los valores de las variables en otra columna, lo que significa que cada observación puede ocupar más de una fila.\nPor ejemplo:\npais variable valor Chile pobreza 5.00 Chile esperanza 81.17 Chile escolaridad 11.29 Uruguay pobreza 6.00 Uruguay esperanza 78.14 Uruguay escolaridad 10.54 Argentina pobreza 11.00 Argentina esperanza 77.69 Argentina escolaridad 11.18 Tabla con datos en formato largo, con las variables distribuidas en filas Se trata de un formato de datos más conveniente para procesar información, o para organizar estructuras de datos más complejas.\nTransformación de formatos Ahora veremos cómo podemos pasar desde una estructura o formato de datos hacia la otra. Usaremos datos sobre países de América Latina, que fueron obtenidos desde tablas de Wikipedia usando web scraping. Revisa el script del scraping aquí.\nDesde ancho hacia largo Si tenemos nuestros datos cuyas variables están distribuidas en columnas, y queremos pasarlas hacia filas, necesitamos pivotar la tabla hacia lo largo.\nLa función pivot_longer() toma las columnas que contienen las variables, y las apila en dos nuevas columnas: una con los nombres de las variables, y otra con sus valores.\nlibrary(tidyr) library(dplyr) Veamos un ejemplo común. Cuando buscamos datos que muestran la evolución de una variable, en general los datos se estructuran con columnas que describen los valores de una variable separadas en columnas por mes o año.\ndesarrollo \u0026lt;- tribble( ~pais, ~\u0026#34;2023\u0026#34;, ~\u0026#34;2022\u0026#34;, ~\u0026#34;2021\u0026#34;, ~\u0026#34;2019\u0026#34;, \u0026#34;Chile\u0026#34;, 0.878, 0.869, 0.865, 0.867, \u0026#34;Argentina\u0026#34;, 0.865, 0.858, 0.847, 0.861, \u0026#34;Uruguay\u0026#34;, 0.862, 0.852, 0.837, 0.830, \u0026#34;Panamá\u0026#34;, 0.839, 0.835, 0.819, 0.824, \u0026#34;Costa Rica\u0026#34;, 0.833, 0.823, 0.817, 0.821, \u0026#34;Perú\u0026#34;, 0.794, 0.790, 0.764, 0.784, \u0026#34;México\u0026#34;, 0.789, 0.783, 0.761, 0.788, \u0026#34;Colombia\u0026#34;, 0.788, 0.782, 0.762, 0.777, \u0026#34;Brasil\u0026#34;, 0.786, 0.780, 0.768, 0.776, \u0026#34;Ecuador\u0026#34;, 0.777, 0.773, 0.753, 0.765 ) Ejecuta este código para cargar los datos en tu sesión de R Esta tabla contiene datos del índice de desarrollo humano para 10 países de la Latinoamérica:\npais 2023 2022 2021 2019 Chile 0.878 0.869 0.865 0.867 Argentina 0.865 0.858 0.847 0.861 Uruguay 0.862 0.852 0.837 0.830 Panamá 0.839 0.835 0.819 0.824 Costa Rica 0.833 0.823 0.817 0.821 Perú 0.794 0.790 0.764 0.784 México 0.789 0.783 0.761 0.788 Colombia 0.788 0.782 0.762 0.777 Brasil 0.786 0.780 0.768 0.776 Ecuador 0.777 0.773 0.753 0.765 Tabla de datos en formato ancho Tener los datos así puede ser útil para mirarlos y presentarlos, pero se vuelve difícil trabajar con ellos, por ejemplo, para calcular la variación entre cada año, o para graficar la evolución del índice de desarrollo humano a través del tiempo.\nPara convertirlos en formato largo usamos pivot_longer() indicando las columnas que queremos pivotar:\ndesarrollo_largo \u0026lt;- desarrollo |\u0026gt; pivot_longer(cols = c(\u0026#34;2023\u0026#34;, \u0026#34;2022\u0026#34;, \u0026#34;2021\u0026#34;, \u0026#34;2019\u0026#34;), names_to = \u0026#34;año\u0026#34;, values_to = \u0026#34;índice\u0026#34;) Pivotar una tabla al formato largo pais año índice Chile 2023 0.878 Chile 2022 0.869 Chile 2021 0.865 Chile 2019 0.867 Argentina 2023 0.865 Argentina 2022 0.858 Argentina 2021 0.847 Argentina 2019 0.861 Datos en formato largo Ahora cada país abarca varias filas, porque ahora las filas son también los años. Es decir, pasamos a tener una fila por cada país, tener una fila por cada año de cada país. De este modo, ahora todas las cifras se encuentran en una sola columna.\n¿Qué hicimos?\nCon cols = c(\u0026quot;2023\u0026quot;, \u0026quot;2022\u0026quot;, \u0026quot;2021\u0026quot;, \u0026quot;2019\u0026quot;) especificamos las columnas que queremos alargar.\nCon los argumentos names_to y values_to definimos el nombre de las nuevas columnas con los nombres y los valores de las columnas originales. Podemos dejar estos últimos dos argumentos vacíos para que se usen los nombres por defecto (name y value).\nEjemplos: ¿Para qué nos sirve ésto? Ahora podemos filtrar por año fácilmente:\ndesarrollo_largo |\u0026gt; filter(año == \u0026#34;2023\u0026#34;) # A tibble: 10 × 3 pais año índice \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Chile 2023 0.878 2 Argentina 2023 0.865 3 Uruguay 2023 0.862 4 Panamá 2023 0.839 5 Costa Rica 2023 0.833 6 Perú 2023 0.794 7 México 2023 0.789 8 Colombia 2023 0.788 9 Brasil 2023 0.786 10 Ecuador 2023 0.777 Podemos encontrar datos donde el índice cumpla algún criterio:\ndesarrollo_largo |\u0026gt; filter(índice \u0026gt; 0.86 \u0026amp; índice \u0026lt; 0.87) # A tibble: 6 × 3 pais año índice \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Chile 2022 0.869 2 Chile 2021 0.865 3 Chile 2019 0.867 4 Argentina 2023 0.865 5 Argentina 2019 0.861 6 Uruguay 2023 0.862 O podemos calcular la variación entre años:\ndesarrollo_largo |\u0026gt; filter(pais == \u0026#34;Chile\u0026#34;) |\u0026gt; arrange(año) |\u0026gt; mutate(variación = índice - lag(índice), aumento = if_else(variación \u0026gt; 0, \u0026#34;Sí\u0026#34;, \u0026#34;No\u0026#34;)) # A tibble: 4 × 5 pais año índice variación aumento \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Chile 2019 0.867 NA \u0026lt;NA\u0026gt; 2 Chile 2021 0.865 -0.00200 No 3 Chile 2022 0.869 0.00400 Sí 4 Chile 2023 0.878 0.00900 Sí Y lo mejor es que ahora resulta mucho más fácil poder hacer el gráficos con estos datos, dado que la información necesaria se encuentra agrupada en sólo dos columnas:\nlibrary(ggplot2) library(thematic) thematic_on(fg = \u0026#34;#553A74\u0026#34;, bg = \u0026#34;#EAD2FA\u0026#34;) desarrollo_largo |\u0026gt; filter(pais %in% c(\u0026#34;Chile\u0026#34;, \u0026#34;Argentina\u0026#34;, \u0026#34;Brasil\u0026#34;, \u0026#34;Perú\u0026#34;)) |\u0026gt; ggplot() + aes(año, índice, color = pais, group = pais) + geom_line(linewidth = 1, alpha = 0.8) + geom_point(size = 3) + labs(y = \u0026#34;Índice de desarrollo humano\u0026#34;, color = \u0026#34;País\u0026#34;, x = NULL) Veamos otro ejemplo: una tabla con tres variables distintas, que nos muestran datos de países latinoamericanos sobre pobreza y desarrollo.\npobreza \u0026lt;- tribble( ~pais, ~pobreza, ~esperanza, ~escolaridad, \u0026#34;Chile\u0026#34;, 5, 81.17, 11.29, \u0026#34;Uruguay\u0026#34;, 6, 78.14, 10.54, \u0026#34;Argentina\u0026#34;, 11, 77.69, 11.18, \u0026#34;Costa Rica\u0026#34;, 13, 80.8, 8.84, \u0026#34;Panamá\u0026#34;, 13, 79.59, 10.83, \u0026#34;Bolivia\u0026#34;, 15, 68.58, 10.02, \u0026#34;Paraguay\u0026#34;, 20, 73.84, 8.93, \u0026#34;México\u0026#34;, 22, 75.07, 9.35, \u0026#34;Brasil\u0026#34;, 24, 75.85, 8.43, \u0026#34;El Salvador\u0026#34;, 28, 72.1, 7.3 ) Ejecuta este código para cargar los datos en tu sesión de R pais pobreza esperanza escolaridad Chile 5 81.17 11.29 Uruguay 6 78.14 10.54 Argentina 11 77.69 11.18 Costa Rica 13 80.80 8.84 Panamá 13 79.59 10.83 Bolivia 15 68.58 10.02 Paraguay 20 73.84 8.93 México 22 75.07 9.35 Brasil 24 75.85 8.43 El Salvador 28 72.10 7.30 En las filas tenemos las observaciones (países), y en las columnas tenemos tres variables distintas: pobreza (medida por el Banco Mundial como porcentaje de la población que recibe menos de US$6,85 al día), esperanza de vida, y escolaridad promedio.\nSi convertimos esta tabla hacia el formato largo, vamos a tener la información estructurada de forma que cada variable ocupe una fila distinta:\npobreza_largo \u0026lt;- pobreza |\u0026gt; pivot_longer(cols = c(pobreza, esperanza, escolaridad), names_to = \u0026#34;variable\u0026#34;, values_to = \u0026#34;valor\u0026#34;) Pivotar una tabla al formato largo pais variable valor Chile pobreza 5.00 Chile esperanza 81.17 Chile escolaridad 11.29 Uruguay pobreza 6.00 Uruguay esperanza 78.14 Uruguay escolaridad 10.54 Argentina pobreza 11.00 Argentina esperanza 77.69 Argentina escolaridad 11.18 Ahora cada país ocupa tres filas, una por cada variable. Esto nos permite filtrar y analizar los datos de forma más flexible.\nEjemplos: ¿Para qué nos sirve ésto? pobreza_largo |\u0026gt; filter(variable == \u0026#34;escolaridad\u0026#34;) # A tibble: 10 × 3 pais variable valor \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Chile escolaridad 11.3 2 Uruguay escolaridad 10.5 3 Argentina escolaridad 11.2 4 Costa Rica escolaridad 8.84 5 Panamá escolaridad 10.8 6 Bolivia escolaridad 10.0 7 Paraguay escolaridad 8.93 8 México escolaridad 9.35 9 Brasil escolaridad 8.43 10 El Salvador escolaridad 7.3 library(ggplot2) library(thematic) thematic_on(fg = \u0026#34;#553A74\u0026#34;, bg = \u0026#34;#EAD2FA\u0026#34;) pobreza_largo |\u0026gt; mutate(variable = case_match(variable, \u0026#34;pobreza\u0026#34; ~ \u0026#34;Pobreza (%)\u0026#34;, \u0026#34;esperanza\u0026#34; ~ \u0026#34;Esperanza de vida\u0026#34;, \u0026#34;escolaridad\u0026#34; ~ \u0026#34;Escolaridad promedio\u0026#34;)) |\u0026gt; ggplot() + aes(y = pais, x = valor, fill = variable) + geom_col(width = 0.5) + facet_wrap(~variable, scales = \u0026#34;free_x\u0026#34;) + guides(fill = guide_none()) + labs(x = NULL, y = NULL) Selección de columnas Cuando tenemos muchas columnas para pivotar, es mejor usar técnicas o funciones que las abarquen todas en vez de escribirlas manualmente.\nSeleccionar todas excepto algunas Podemos usar el operador - para seleccionar todas las columnas excepto las que especifiquemos:\npobreza |\u0026gt; pivot_longer(cols = -pais, names_to = \u0026#34;variable\u0026#34;, values_to = \u0026#34;valor\u0026#34;) Esto es equivalente a escribir cols = c(pobreza, esperanza, escolaridad), pero mucho más corto cuando tenemos muchas columnas.\nSeleccionar por posición También podemos seleccionar columnas por su posición numérica:\ndesarrollo |\u0026gt; pivot_longer(cols = 2:last_col(), names_to = \u0026#34;año\u0026#34;, values_to = \u0026#34;índice\u0026#34;) Esto selecciona desde la segunda columna hasta la última (last_col()); es decir, los cuatro años de la tabla).\nSeleccionar con funciones auxiliares Se pueden usar todas las funciones de {tidyselect} para seleccionar columnas según su texto, formato, y más:\n# seleccionar columnas que empiezan con cierto texto desarrollo |\u0026gt; pivot_longer(cols = starts_with(\u0026#34;20\u0026#34;), names_to = \u0026#34;año\u0026#34;, values_to = \u0026#34;índice\u0026#34;) # seleccionar columnas que son numéricas desarrollo |\u0026gt; pivot_longer(cols = where(is.numeric), names_to = \u0026#34;año\u0026#34;, values_to = \u0026#34;índice\u0026#34;) # A tibble: 40 × 3 pais año índice \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Chile 2023 0.878 2 Chile 2022 0.869 3 Chile 2021 0.865 4 Chile 2019 0.867 5 Argentina 2023 0.865 6 Argentina 2022 0.858 7 Argentina 2021 0.847 8 Argentina 2019 0.861 9 Uruguay 2023 0.862 10 Uruguay 2022 0.852 # ℹ 30 more rows Desde largo hacia ancho pivot_wider() hace la operación inversa a pivot_longer(): toma valores que están apilados en filas y los distribuye en múltiples columnas nuevas.\nEs decir que pasamos de datos donde las variables de cada observación se encuentran en varias filas, a una tabla donde cada observación usará una fila y las variables estarán en varias columnas.\nPartamos desde la tabla pobreza_largo que creamos anteriormente:\npais variable valor Chile pobreza 5.00 Chile esperanza 81.17 Chile escolaridad 11.29 Uruguay pobreza 6.00 Uruguay esperanza 78.14 Uruguay escolaridad 10.54 Argentina pobreza 11.00 Argentina esperanza 77.69 Argentina escolaridad 11.18 Tabla con datos en formato largo Para volver a convertirla al formato ancho usamos pivot_wider():\npobreza_ancho \u0026lt;- pobreza_largo |\u0026gt; pivot_wider(names_from = variable, values_from = valor) Pivotar una tabla al formato ancho pais pobreza esperanza escolaridad Chile 5 81.17 11.29 Uruguay 6 78.14 10.54 Argentina 11 77.69 11.18 Costa Rica 13 80.80 8.84 Panamá 13 79.59 10.83 Bolivia 15 68.58 10.02 Paraguay 20 73.84 8.93 México 22 75.07 9.35 Brasil 24 75.85 8.43 El Salvador 28 72.10 7.30 Datos pivotados al formato ancho Ahora cada variable tiene su propia columna nuevamente.\n¿Qué hicimos?\nCon names_from indicamos desde cuál variable sacaremos los nombres de las nuevas columnas que queremos crear.\nCon values_from indicamos desde cuál columna sacaremos los valores que rellenarán las celdas de esas nuevas columnas.\nVeamos otro ejemplo usando la tabla desarrollo_largo:\npais año índice Chile 2023 0.878 Chile 2022 0.869 Chile 2021 0.865 Chile 2019 0.867 Argentina 2023 0.865 Argentina 2022 0.858 Argentina 2021 0.847 Argentina 2019 0.861 Tabla con datos en formato largo Podemos convertirla de vuelta al formato ancho para tener cada año como una columna separada:\ndesarrollo_ancho \u0026lt;- desarrollo_largo |\u0026gt; pivot_wider(names_from = año, values_from = índice) Pivotar a formato ancho pais 2023 2022 2021 2019 Chile 0.878 0.869 0.865 0.867 Argentina 0.865 0.858 0.847 0.861 Uruguay 0.862 0.852 0.837 0.830 Panamá 0.839 0.835 0.819 0.824 Costa Rica 0.833 0.823 0.817 0.821 Perú 0.794 0.790 0.764 0.784 México 0.789 0.783 0.761 0.788 Colombia 0.788 0.782 0.762 0.777 Brasil 0.786 0.780 0.768 0.776 Ecuador 0.777 0.773 0.753 0.765 Datos pivotados a formato ancho Ahora tenemos cada año como columna, lo que facilita comparar los valores entre años lado a lado.\nEjemplos: ¿Para qué nos sirve ésto? Con los datos en formato ancho podemos calcular fácilmente diferencias entre años específicos:\ndesarrollo_ancho |\u0026gt; mutate(cambio_2019_2023 = `2023` - `2019`) |\u0026gt; select(pais, `2019`, `2023`, cambio_2019_2023) |\u0026gt; arrange(desc(cambio_2019_2023)) # A tibble: 10 × 4 pais `2019` `2023` cambio_2019_2023 \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Uruguay 0.83 0.862 0.0320 2 Panamá 0.824 0.839 0.0150 3 Costa Rica 0.821 0.833 0.0120 4 Ecuador 0.765 0.777 0.0120 5 Chile 0.867 0.878 0.0110 6 Colombia 0.777 0.788 0.0110 7 Perú 0.784 0.794 0.0100 8 Brasil 0.776 0.786 0.0100 9 Argentina 0.861 0.865 0.00400 10 México 0.788 0.789 0.00100 Pero en general el formato ancho se usa para posteriormente guardar los datos en una tabla Excel y presentárselos a otras personas, o bien, generar tablas usando R destinadas a su lectura en informes o reportes.\n¿Cuándo usar cada formato? No existe un formato correcto qué pueda aplicarse a todas las tablas de datos. Esto debe depender de lo que necesites hacer.\nEn general, te vas a dar cuenta intuitivamente que el formato de tus datos te está haciendo más difícil hacer ciertas cosas, y en ese caso se recomienda transformar la estructura, ya sea temporalmente y luego deshacer la transformación, o permanentemente en función del tipo de datos que estás trabajando.\nFormato largo:\nCuando vayas a hacer gráficos con {ggplot2} Cuando necesites agrupar y resumir datos con group_by() y summarise() Cuando quieras aplicar filtros sobre los valores de las variables Formato ancho:\nCuando quieras presentar tablas para reportes Cuando necesites comparar valores lado a lado entre variables Cuando vayas a exportar datos a Excel para su lectura o consulta Cuando quieras calcular diferencias o razones entre columnas específicas Cuando necesites hacer operaciones matemáticas simples entre variables Saber transformar datos entre formato ancho y largo es una habilidad esencial para trabajar con datos en R. Dominar esta transformaciones de estructuras de datos te entrega mucha libertad al momento de manipular, limpiar, y trabajar con conjuntos de datos complejos.\n","date":"2025-11-29T00:00:00Z","excerpt":"Los datos pueden existir en distintos formatos o estructuras, y el poder transformar los datos entre distintas estructuras es una habilidad clave. En este tutorial aprenderemos a usar las funciones `pivot_longer()` y `pivot_wider()` del paquete `{tidyr}` para cambiar entre formatos de datos _ancho_ (_wide_) y _largo_ (_long_).","href":"https://bastianoleah.netlify.app/blog/r_introduccion/tidyr_pivotar/","tags":"dplyr ; tidyr ; procesamiento de datos ; datos ; básico","title":"Transformación de datos entre formato _ancho_ y _largo_ con {tidyr}"},{"content":"Este post es una introducción al paquete {dplyr} para la exploración y análisis de datos con R. Está dirigido a principiantes de R. Si es primera vez que usas R, te recomiendo revisar primero este breve tutorial inicial de R..\nEn este tutorial veremos:\ncarga de datos de Excel seleccionar columnas ordenar tablas de datos seleccionar filas de una tabla de datos filtrar datos Los datos usados en este tutorial son las Proyecciones de población para 2024 de Chile, calculadas por el Instituto Nacional de Estadísticas (INE). La obtención, procesamiento y visualización de estos datos puede encontrarse en este repositorio, junto a una aplicación web desarrollada en R para visualizar los datos.\nInstalación de paquetes Los paquetes son conjuntos de funciones, programas, datos y documentación que sirven para potenciar R. Para poder usarlos, primero hay que instalarlos en nuestro computador usando la función install.packages(). Luego de instalarlos, simplemente los cargamos usando library().\n# es necesario instalarlos una sola vez install.packages(\u0026#34;dplyr\u0026#34;) install.packages(\u0026#34;readxl\u0026#34;) Luego cargamos los paquetes:\nlibrary(dplyr) # manipulación de datos library(readxl) # carga de archivos Excel {dplyr} es un paquete parte del Tidyverse, que se usa para manipular datos a partir de funciones que emulan instrucciones sencillas, como seleccionar, filtrar, etc.\nCargar datos Antes que nada, debemos descargar el archivo que usaremos para el tutorial:\nDescargar datos en Excel Descargamos el archivo, y lo guardamos en una carpeta. Esta carpeta debe ser nuestro proyecto de RStudio, o bien, creamos un nuevo proyecto de RStudio apuntando a esa carpeta, para decirle a R que trabajaremos ahí.\nImportamos los datos que usaremos con la funcion read_excel(). Si el archivo que queremos cargar está dentro de nuestro proyecto de RStudio, entonces solamente necesitamos poner el nombre del archivo entre comillas, y si no, indicar la subcarpeta del proyecto en donde esté guardado, o si no estamos trabajando en un proyecto, indicar la ruta completa del archivo.\nAsignamos el resultado a un objeto, y así tenemos nuestros datos de Excel cargados en R:\ncenso \u0026lt;- read_excel(\u0026#34;censo_proyeccion_2024.xlsx\u0026#34;) # cargar censo # ver los datos de dataframe o tabla # A tibble: 346 × 7 cut_region region cut_provincia provincia cut_comuna comuna población \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 1 Tarapacá 11 Iquique 1101 Iquique 231962 2 1 Tarapacá 11 Iquique 1107 Alto H… 143294 3 1 Tarapacá 14 Tamarugal 1401 Pozo A… 18713 4 1 Tarapacá 14 Tamarugal 1402 Camiña 1374 5 1 Tarapacá 14 Tamarugal 1403 Colcha… 1558 6 1 Tarapacá 14 Tamarugal 1404 Huara 3095 7 1 Tarapacá 14 Tamarugal 1405 Pica 6291 8 2 Antofagasta 21 Antofagasta 2101 Antofa… 444276 9 2 Antofagasta 21 Antofagasta 2102 Mejill… 15877 10 2 Antofagasta 21 Antofagasta 2103 Sierra… 1800 # ℹ 336 more rows Para ver más filas del dataframe, usamos la función print() con el argumento n:\nprint(censo, n = 20) # A tibble: 346 × 7 cut_region region cut_provincia provincia cut_comuna comuna población \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 1 Tarapacá 11 Iquique 1101 Iquique 231962 2 1 Tarapacá 11 Iquique 1107 Alto H… 143294 3 1 Tarapacá 14 Tamarugal 1401 Pozo A… 18713 4 1 Tarapacá 14 Tamarugal 1402 Camiña 1374 5 1 Tarapacá 14 Tamarugal 1403 Colcha… 1558 6 1 Tarapacá 14 Tamarugal 1404 Huara 3095 7 1 Tarapacá 14 Tamarugal 1405 Pica 6291 8 2 Antofagasta 21 Antofagasta 2101 Antofa… 444276 9 2 Antofagasta 21 Antofagasta 2102 Mejill… 15877 10 2 Antofagasta 21 Antofagasta 2103 Sierra… 1800 11 2 Antofagasta 21 Antofagasta 2104 Taltal 13967 12 2 Antofagasta 22 El Loa 2201 Calama 196152 13 2 Antofagasta 22 El Loa 2202 Ollagüe 269 14 2 Antofagasta 22 El Loa 2203 San Pe… 11030 15 2 Antofagasta 23 Tocopilla 2301 Tocopi… 28354 16 2 Antofagasta 23 Tocopilla 2302 María … 6507 17 3 Atacama 31 Copiapó 3101 Copiapó 176100 18 3 Atacama 31 Copiapó 3102 Caldera 19964 19 3 Atacama 31 Copiapó 3103 Tierra… 14431 20 3 Atacama 32 Chañaral 3201 Chañar… 13017 # ℹ 326 more rows Ahora que tenemos nuestros datos cargados como un objeto en nuestro entorno de R, podemos empezar a manipularlo y explorarlo usando {dplyr}.\nSeleccionar columnas La función select() selecciona columnas del dataframe.\ncenso |\u0026gt; # comando + shift + M select(comuna, población) # A tibble: 346 × 2 comuna población \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Iquique 231962 2 Alto Hospicio 143294 3 Pozo Almonte 18713 4 Camiña 1374 5 Colchane 1558 6 Huara 3095 7 Pica 6291 8 Antofagasta 444276 9 Mejillones 15877 10 Sierra Gorda 1800 # ℹ 336 more rows El operador |\u0026gt; es un conector, y significa que a este objeto le hago esto otro; es decir, se lee como si dijera \u0026ldquo;luego\u0026rdquo; o \u0026ldquo;entonces\u0026rdquo;. En este caso: a censo le selecciono comuna y población.\nPodemos seleccionar negativamente; es decir, excluir ciertas columnas\ncenso |\u0026gt; select(-cut_provincia, -cut_comuna, -cut_region) # A tibble: 346 × 4 region provincia comuna población \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Tarapacá Iquique Iquique 231962 2 Tarapacá Iquique Alto Hospicio 143294 3 Tarapacá Tamarugal Pozo Almonte 18713 4 Tarapacá Tamarugal Camiña 1374 5 Tarapacá Tamarugal Colchane 1558 6 Tarapacá Tamarugal Huara 3095 7 Tarapacá Tamarugal Pica 6291 8 Antofagasta Antofagasta Antofagasta 444276 9 Antofagasta Antofagasta Mejillones 15877 10 Antofagasta Antofagasta Sierra Gorda 1800 # ℹ 336 more rows También podemos seleccionar columnas en base a sus nombres parciales:\ncenso |\u0026gt; select(-contains(\u0026#34;cut\u0026#34;)) # A tibble: 346 × 4 region provincia comuna población \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Tarapacá Iquique Iquique 231962 2 Tarapacá Iquique Alto Hospicio 143294 3 Tarapacá Tamarugal Pozo Almonte 18713 4 Tarapacá Tamarugal Camiña 1374 5 Tarapacá Tamarugal Colchane 1558 6 Tarapacá Tamarugal Huara 3095 7 Tarapacá Tamarugal Pica 6291 8 Antofagasta Antofagasta Antofagasta 444276 9 Antofagasta Antofagasta Mejillones 15877 10 Antofagasta Antofagasta Sierra Gorda 1800 # ℹ 336 more rows Selección de columnas por el numero de una columna (su posición):\ncenso |\u0026gt; select(1:3, población) # A tibble: 346 × 4 cut_region region cut_provincia población \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 1 Tarapacá 11 231962 2 1 Tarapacá 11 143294 3 1 Tarapacá 14 18713 4 1 Tarapacá 14 1374 5 1 Tarapacá 14 1558 6 1 Tarapacá 14 3095 7 1 Tarapacá 14 6291 8 2 Antofagasta 21 444276 9 2 Antofagasta 21 15877 10 2 Antofagasta 21 1800 # ℹ 336 more rows Ordenar filas Usamos la función arrange() para ordenar las filas de nuestros datos de acuerdo a otra variable:\ncenso |\u0026gt; arrange(población) |\u0026gt; select(comuna, población) # A tibble: 346 × 2 comuna población \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Antártica 151 2 Río Verde 205 3 Laguna Blanca 248 4 Ollagüe 269 5 Timaukel 276 6 Tortel 585 7 San Gregorio 651 8 Primavera 674 9 O'Higgins 675 10 General Lagos 801 # ℹ 336 more rows Ordenar de mayor a menor:\ncenso |\u0026gt; arrange(desc(población)) |\u0026gt; select(region, comuna, población) # A tibble: 346 × 3 region comuna población \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Metropolitana de Santiago Puente Alto 667904 2 Metropolitana de Santiago Maipú 586812 3 Metropolitana de Santiago Santiago 544388 4 Antofagasta Antofagasta 444276 5 Metropolitana de Santiago La Florida 407297 6 Valparaíso Viña del Mar 371490 7 Metropolitana de Santiago San Bernardo 348640 8 Metropolitana de Santiago Las Condes 343632 9 Valparaíso Valparaíso 320816 10 La Araucanía Temuco 309696 # ℹ 336 more rows Ordenar por dos variables a la vez\ncenso |\u0026gt; arrange(region, desc(población)) |\u0026gt; select(region, comuna, población) |\u0026gt; print(n = 20) # A tibble: 346 × 3 region comuna población \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Antofagasta Antofagasta 444276 2 Antofagasta Calama 196152 3 Antofagasta Tocopilla 28354 4 Antofagasta Mejillones 15877 5 Antofagasta Taltal 13967 6 Antofagasta San Pedro de Atacama 11030 7 Antofagasta María Elena 6507 8 Antofagasta Sierra Gorda 1800 9 Antofagasta Ollagüe 269 10 Arica y Parinacota Arica 257163 11 Arica y Parinacota Putre 2569 12 Arica y Parinacota Camarones 1246 13 Arica y Parinacota General Lagos 801 14 Atacama Copiapó 176100 15 Atacama Vallenar 57360 16 Atacama Caldera 19964 17 Atacama Tierra Amarilla 14431 18 Atacama Diego de Almagro 13909 19 Atacama Chañaral 13017 20 Atacama Huasco 11590 # ℹ 326 more rows Filtrar datos Con la función filter() podemos filtrar nuestro dataframe a partir de una comparación, dejando solamente las filas del dataframe que cumplan con la comparación.\nPor ejemplo, dejar sólo las filas donde la comuna sea \u0026ldquo;Providencia\u0026rdquo;:\ncenso |\u0026gt; filter(comuna == \u0026#34;Providencia\u0026#34;) # A tibble: 1 × 7 cut_region region cut_provincia provincia cut_comuna comuna población \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 13 Metropolitana … 131 Santiago 13123 Provi… 164009 Excluir las filas donde la columna sea \u0026ldquo;Alto Hospicio\u0026rdquo;:\ncenso |\u0026gt; filter(comuna != \u0026#34;Alto Hospicio\u0026#34;) # A tibble: 345 × 7 cut_region region cut_provincia provincia cut_comuna comuna población \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 1 Tarapacá 11 Iquique 1101 Iquique 231962 2 1 Tarapacá 14 Tamarugal 1401 Pozo A… 18713 3 1 Tarapacá 14 Tamarugal 1402 Camiña 1374 4 1 Tarapacá 14 Tamarugal 1403 Colcha… 1558 5 1 Tarapacá 14 Tamarugal 1404 Huara 3095 6 1 Tarapacá 14 Tamarugal 1405 Pica 6291 7 2 Antofagasta 21 Antofagasta 2101 Antofa… 444276 8 2 Antofagasta 21 Antofagasta 2102 Mejill… 15877 9 2 Antofagasta 21 Antofagasta 2103 Sierra… 1800 10 2 Antofagasta 21 Antofagasta 2104 Taltal 13967 # ℹ 335 more rows Dejar sólo las observaciones donde la población sea mayor a un valor:\ncenso |\u0026gt; filter(población \u0026gt; 300000) # A tibble: 10 × 7 cut_region region cut_provincia provincia cut_comuna comuna población \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 2 Antofagasta 21 Antofaga… 2101 Antof… 444276 2 5 Valparaíso 51 Valparaí… 5101 Valpa… 320816 3 5 Valparaíso 51 Valparaí… 5109 Viña … 371490 4 9 La Araucanía 91 Cautín 9101 Temuco 309696 5 13 Metropolitana… 131 Santiago 13101 Santi… 544388 6 13 Metropolitana… 131 Santiago 13110 La Fl… 407297 7 13 Metropolitana… 131 Santiago 13114 Las C… 343632 8 13 Metropolitana… 131 Santiago 13119 Maipú 586812 9 13 Metropolitana… 132 Cordille… 13201 Puent… 667904 10 13 Metropolitana… 134 Maipo 13401 San B… 348640 Población menor a 1000, sólo dejar comuna y población, y ordenar de menor a mayor:\ncenso |\u0026gt; filter(población \u0026lt; 1000) |\u0026gt; select(comuna, población) |\u0026gt; arrange(población) # A tibble: 11 × 2 comuna población \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Antártica 151 2 Río Verde 205 3 Laguna Blanca 248 4 Ollagüe 269 5 Timaukel 276 6 Tortel 585 7 San Gregorio 651 8 Primavera 674 9 O'Higgins 675 10 General Lagos 801 11 Lago Verde 914 Podemos hacer filtros usando funciones que operen sobre las columnas, por ejemplo, para filtrar las filas donde la población sea igual al mínimo de población:\ncenso |\u0026gt; filter(población == min(población)) |\u0026gt; select(region, comuna, provincia, población) # A tibble: 1 × 4 region comuna provincia población \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Magallanes y de la Antártica Chilena Antártica Antártica Chilena 151 Un caso más útil sería filtrar los casos donde la población sea mayor o igual al promedio de población:\ncenso |\u0026gt; filter(población \u0026gt;= mean(población)) |\u0026gt; select(region, comuna, provincia, población) # A tibble: 84 × 4 region comuna provincia población \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Tarapacá Iquique Iquique 231962 2 Tarapacá Alto Hospicio Iquique 143294 3 Antofagasta Antofagasta Antofagasta 444276 4 Antofagasta Calama El Loa 196152 5 Atacama Copiapó Copiapó 176100 6 Coquimbo La Serena Elqui 267400 7 Coquimbo Coquimbo Elqui 275644 8 Coquimbo Ovalle Limarí 124401 9 Valparaíso Valparaíso Valparaíso 320816 10 Valparaíso Viña del Mar Valparaíso 371490 # ℹ 74 more rows También es posible filtrar usando objetos que creamos con anterioridad:\nmin_pob \u0026lt;- 25000 max_pob \u0026lt;- 30000 censo |\u0026gt; filter(población \u0026gt; min_pob, población \u0026lt; max_pob) |\u0026gt; select(población, comuna, provincia, region) # A tibble: 16 × 4 población comuna provincia region \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 28354 Tocopilla Tocopilla Antofagasta 2 29916 Salamanca Choapa Coquimbo 3 27898 La Cruz Quillota Valparaíso 4 27065 Cartagena San Antonio Valparaíso 5 27286 Llaillay San Felipe Valparaíso 6 27749 Las Cabras Cachapoal Libertador General Bernardo O'Higgins 7 28552 Mostazal Cachapoal Libertador General Bernardo O'Higgins 8 26746 Hualqui Concepción Biobío 9 27152 Lebu Arauco Biobío 10 28028 Nacimiento Bíobío Biobío 11 25515 Carahue Cautín La Araucanía 12 25488 Freire Cautín La Araucanía 13 26617 Pitrufquen Cautín La Araucanía 14 26558 Collipulli Malleco La Araucanía 15 25218 Aysén Aysén Aysén del General Carlos Ibáñez del Campo 16 29067 Coihueco Punilla Ñuble Del mismo modo, podemos filtrar usando la cifra del promedio de población:\npromedio \u0026lt;- mean(censo$población) censo |\u0026gt; filter(población \u0026gt; promedio) # A tibble: 84 × 7 cut_region region cut_provincia provincia cut_comuna comuna población \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 1 Tarapacá 11 Iquique 1101 Iquique 231962 2 1 Tarapacá 11 Iquique 1107 Alto H… 143294 3 2 Antofagasta 21 Antofagasta 2101 Antofa… 444276 4 2 Antofagasta 22 El Loa 2201 Calama 196152 5 3 Atacama 31 Copiapó 3101 Copiapó 176100 6 4 Coquimbo 41 Elqui 4101 La Ser… 267400 7 4 Coquimbo 41 Elqui 4102 Coquim… 275644 8 4 Coquimbo 43 Limarí 4301 Ovalle 124401 9 5 Valparaíso 51 Valparaíso 5101 Valpar… 320816 10 5 Valparaíso 51 Valparaíso 5109 Viña d… 371490 # ℹ 74 more rows Seleccionar filas Usamos slice() para seleccionar filas específicas del dataframe:\ncenso |\u0026gt; slice(200:220) # filas del 200 al 220 # A tibble: 21 × 7 cut_region region cut_provincia provincia cut_comuna comuna población \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 9 La Araucanía 92 Malleco 9205 Lonqui… 11109 2 9 La Araucanía 92 Malleco 9206 Los Sa… 7468 3 9 La Araucanía 92 Malleco 9207 Lumaco 9916 4 9 La Araucanía 92 Malleco 9208 Purén 12103 5 9 La Araucanía 92 Malleco 9209 Renaico 11002 6 9 La Araucanía 92 Malleco 9210 Traigu… 19260 7 9 La Araucanía 92 Malleco 9211 Victor… 35554 8 10 Los Lagos 101 Llanquihue 10101 Puerto… 280955 9 10 Los Lagos 101 Llanquihue 10102 Calbuco 37626 10 10 Los Lagos 101 Llanquihue 10103 Cochamó 3947 # ℹ 11 more rows También puede servir para seleccionar la fila que tenga el mayor o menos valor en una columna:\ncenso |\u0026gt; slice_max(población) # A tibble: 1 × 7 cut_region region cut_provincia provincia cut_comuna comuna población \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 13 Metropolitana … 132 Cordille… 13201 Puent… 667904 censo |\u0026gt; slice_min(población) # A tibble: 1 × 7 cut_region region cut_provincia provincia cut_comuna comuna población \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 12 Magallanes y d… 122 Antártic… 12202 Antár… 151 Incluso nos puede servir para seleccionar una cantidad de filas elegidas al azar:\ncenso |\u0026gt; slice_sample(n = 5) |\u0026gt; select(comuna) # A tibble: 5 × 1 comuna \u0026lt;chr\u0026gt; 1 Panquehue 2 Independencia 3 Melipilla 4 Peñalolén 5 Chillán Viejo Selección de filas por grupos Haciendo uso de la función group_by() podemos realizar operaciones en base a grupos. Esto significa que si agrupamos por región, y luego usamos slice_max() para obtener las observaciones con mayor población, el filtro de slice_max() se realizará una vez por cada región. Entonces, en vez de solamente obtener la comuna de mayor población del país, obtendremos la comuna con mayor población para cada región.\ncenso |\u0026gt; group_by(region) |\u0026gt; slice_max(población) |\u0026gt; select(region, comuna, población) # A tibble: 16 × 3 # Groups: region [16] region comuna población \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Antofagasta Antofagasta 444276 2 Arica y Parinacota Arica 257163 3 Atacama Copiapó 176100 4 Aysén del General Carlos Ibáñez del Campo Coyhaique 62046 5 Biobío Concepción 239776 6 Coquimbo Coquimbo 275644 7 La Araucanía Temuco 309696 8 Libertador General Bernardo O'Higgins Rancagua 274407 9 Los Lagos Puerto Montt 280955 10 Los Ríos Valdivia 182086 11 Magallanes y de la Antártica Chilena Punta Arenas 145713 12 Maule Talca 242344 13 Metropolitana de Santiago Puente Alto 667904 14 Tarapacá Iquique 231962 15 Valparaíso Viña del Mar 371490 16 Ñuble Chillán 204091 Con esto concluye este tutorial inicial para manipular datos con el paquete {dplyr}. En siguientes tutoriales iremos usando funciones más complejas y avanzadas! 🫣\nSi este tutorial te sirvió, por favor considera hacerme una donación! Cualquier monto me ayuda al menos a poder tomarme un cafecito 🥺\n","date":"2024-11-08T00:00:00Z","excerpt":"Tutorial de introducción al paquete `{dplyr}` para la exploración y análisis de datos con R. Está dirigido a principiantes. En este tutorial veremos cómo explorar un conjunto de datos sobre población.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/tutorial_dplyr_censo/","tags":"dplyr","title":"Tutorial: introducción a {dplyr} con datos de población"},{"content":"Este post es una introducción al paquete {dplyr} para la exploración y análisis de datos con R. Está dirigido a principiantes de R. Si es primera vez que usas R, te recomiendo revisar primero este breve tutorial inicial de R..\nEn este tutorial veremos:\ncarga de datos de Excel revisar los datos seleccionar columnas ordenar tablas de datos contar frecuencias filtrar datos crear variables dicotómicas crear variables complejas Los datos usados en este tutorial corresponden al catastro de campamentos de Chile 2024, del Centro de Estudios del Ministerio de Vivienda y Urbanismo.. El código para obtener, procesar y visualizar los datos se encuentra en este repositorio de GitHub.\nPara empezar, instalamos los paquetes que usaremos en este tutorial (solo se necesita hacer una vez, y sólo si es que no los tienes instalados).\ninstall.packages(\u0026#34;readxl\u0026#34;) # cargar datos desde Excel install.packages(\u0026#34;dplyr\u0026#34;) # manipulación de datos Cargar datos Puedes descargar el archivo campamentos_chile_2024.xlsx que usaremos en este tutorial desde este enlace. Los datos se obtuvieron desde este repositorio.\nDescargar datos en Excel Cargamos los datos desde Excel usando el nombre del archivo como argumento a la función read_excel():\nlibrary(readxl) datos \u0026lt;- read_excel(\u0026#34;campamentos_chile_2024.xlsx\u0026#34;) datos # A tibble: 1,432 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Bellavista Valpa… Valparaí… Valpa… 05 051 5101 29 1.51 10617. 2 Buena Vis… Valpa… Valparaí… Valpa… 05 051 5101 32 1.67 11707. 3 Los Flete… Valpa… Valparaí… Valpa… 05 051 5101 56 2.49 17465. 4 Pueblo Hu… Valpa… Valparaí… Valpa… 05 051 5101 65 4.60 32261. 5 Villa Ita… Valpa… Valparaí… Valpa… 05 051 5101 33 0.867 6081. 6 Nueva Esp… Valpa… Valparaí… Valpa… 05 051 5101 17 0.399 2802. 7 Francisco… Valpa… Valparaí… Valpa… 05 051 5101 24 2.85 19955. 8 Manuel Ro… Valpa… Valparaí… Valpa… 05 051 5101 21 0.443 3103. 9 Sofia 35 Valpa… Valparaí… Valpa… 05 051 5101 69 2.68 18819. 10 Las Viñas… Valpa… Marga Ma… Villa… 05 058 5804 32 1.24 8704. # ℹ 1,422 more rows # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; Este tipo de objetos tabulares se llaman dataframes, específicamente tibble, que es un tipo de tabla. Los dataframes son tablas de datos hechas a partir de vectores, donde cada columna es un vector del mismo largo (misma cantidad de elementos). Los dataframes se caracterizan por tener distintas columnas que pueden de distinto tipo: numéricas, caracter (texto), lógico (TRUE/FALSE), fechas, entre otras. Todas las columnas tienen el mismo largo, que es equivalente a la cantidad de filas de la tabla.\nPara explorar los datos, usaremos el paquete {dplyr}, que nos permite manejar datos con mayor facilidad. {dplyr} sirve para manipular datos a partir de funciones que emulan instrucciones sencillas, como seleccionar, filtrar, etc.\nlibrary(dplyr) Una particularidad del uso de {dplyr} es el uso del operador |\u0026gt;, que es operador que nos permite conectar un objeto con una o múltiples operaciones que deseamos realizarle al objeto.\nEjemplo de dos operaciones encadenadas:\ndatos |\u0026gt; select(nombre, hogares, hectareas) |\u0026gt; filter(hogares \u0026gt; 60) # A tibble: 352 × 3 nombre hogares hectareas \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Pueblo Hundido 65 4.60 2 Sofia 35 69 2.68 3 Ampliación Prat-Lomas De Peñablanca 146 15.1 4 Violeta Parra 123 10.7 5 Mesana Alto 128 14.5 6 Pampa Ilusión (Sector Las Torres) 195 7.04 7 Parcela 11 520 44.0 8 Parcela 15 257 15.4 9 Campanillas 98 6.19 10 La Isla 116 4.51 # ℹ 342 more rows El conector significa que a este objeto le hago esto otro; es decir, se lee como si dijera \u0026ldquo;luego\u0026rdquo; o \u0026ldquo;entonces\u0026rdquo;. En este caso: a datos le selecciono columnas y le aplico un filtro.\nRevisar Ejecutar el nombre del objeto nos permite que sus contenidos aparezcan en la consola de R.\ndatos # A tibble: 1,432 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Bellavista Valpa… Valparaí… Valpa… 05 051 5101 29 1.51 10617. 2 Buena Vis… Valpa… Valparaí… Valpa… 05 051 5101 32 1.67 11707. 3 Los Flete… Valpa… Valparaí… Valpa… 05 051 5101 56 2.49 17465. 4 Pueblo Hu… Valpa… Valparaí… Valpa… 05 051 5101 65 4.60 32261. 5 Villa Ita… Valpa… Valparaí… Valpa… 05 051 5101 33 0.867 6081. 6 Nueva Esp… Valpa… Valparaí… Valpa… 05 051 5101 17 0.399 2802. 7 Francisco… Valpa… Valparaí… Valpa… 05 051 5101 24 2.85 19955. 8 Manuel Ro… Valpa… Valparaí… Valpa… 05 051 5101 21 0.443 3103. 9 Sofia 35 Valpa… Valparaí… Valpa… 05 051 5101 69 2.68 18819. 10 Las Viñas… Valpa… Marga Ma… Villa… 05 058 5804 32 1.24 8704. # ℹ 1,422 more rows # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; Por defecto, la consola nos muestra 10 filas de la tabla. Si queremos ver más filas podemos usar la función print()\ndatos |\u0026gt; print(n = 20) # A tibble: 1,432 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Bellavista Valpa… Valparaí… Valpa… 05 051 5101 29 1.51 1.06e4 2 Buena Vis… Valpa… Valparaí… Valpa… 05 051 5101 32 1.67 1.17e4 3 Los Flete… Valpa… Valparaí… Valpa… 05 051 5101 56 2.49 1.75e4 4 Pueblo Hu… Valpa… Valparaí… Valpa… 05 051 5101 65 4.60 3.23e4 5 Villa Ita… Valpa… Valparaí… Valpa… 05 051 5101 33 0.867 6.08e3 6 Nueva Esp… Valpa… Valparaí… Valpa… 05 051 5101 17 0.399 2.80e3 7 Francisco… Valpa… Valparaí… Valpa… 05 051 5101 24 2.85 2.00e4 8 Manuel Ro… Valpa… Valparaí… Valpa… 05 051 5101 21 0.443 3.10e3 9 Sofia 35 Valpa… Valparaí… Valpa… 05 051 5101 69 2.68 1.88e4 10 Las Viñas… Valpa… Marga Ma… Villa… 05 058 5804 32 1.24 8.70e3 11 Lomas De … Valpa… Marga Ma… Villa… 05 058 5804 42 3.97 2.78e4 12 Manzana 33 Valpa… Marga Ma… Villa… 05 058 5804 37 2.19 1.53e4 13 Ampliació… Valpa… Marga Ma… Villa… 05 058 5804 146 15.1 1.06e5 14 Los Artes… Valpa… Valparaí… Viña … 05 051 5109 35 2.00 1.40e4 15 La Unión … Valpa… Valparaí… Viña … 05 051 5109 20 1.33 9.30e3 16 John Kenn… Valpa… Valparaí… Valpa… 05 051 5101 48 0.858 6.01e3 17 Pezoa Vel… Valpa… Valparaí… Valpa… 05 051 5101 22 0.194 1.36e3 18 Violeta P… Valpa… Valparaí… Valpa… 05 051 5101 123 10.7 7.50e4 19 Cristo Re… Valpa… Valparaí… Valpa… 05 051 5101 53 0.862 6.04e3 20 Loma Vist… Valpa… Valparaí… Viña … 05 051 5109 47 1.19 8.34e3 # ℹ 1,412 more rows # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; Las funciones head() y tails() te muestran las primeras o últimas filas de la tabla, respectivamente. Si la combinas con print(), puedes consultar la cantidad que quieras de filas al principio o final de tu tabla.\n# ver últimas 20 filas datos |\u0026gt; tail(20) |\u0026gt; print(n=Inf) # A tibble: 20 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Longitudi… Valpa… Marga Ma… Villa… 05 058 5804 14 0.681 4.78e3 2 Comunidad… Valpa… Marga Ma… Villa… 05 058 5804 36 1.28 8.97e3 3 Nueva Tol… Valpa… Marga Ma… Villa… 05 058 5804 10 0.305 2.14e3 4 Toma Nuev… Metro… Cordille… Puent… 13 132 13201 0 0 0 5 Villa Res… Valpa… San Anto… Carta… 05 056 5601 48 6.15 4.26e4 6 Sector Lí… La Ar… Malleco Traig… 09 092 9210 15 2.56 1.58e4 7 Licantatay Antof… El Loa Calama 02 022 2201 41 6.27 5.33e4 8 Nuevo Ama… Antof… El Loa Calama 02 022 2201 41 47.4 4.03e5 9 Barrio Tr… Antof… El Loa Calama 02 022 2201 0 55.3 4.69e5 10 Calameños… Antof… El Loa Calama 02 022 2201 91 4.41 3.74e4 11 Tres Band… Antof… Antofaga… Taltal 02 021 2104 0 2.39 1.94e4 12 Sauce 3 Valpa… Valparaí… Viña … 05 051 5109 0 3.35 2.35e4 13 Campament… Antof… Antofaga… Antof… 02 021 2101 0 0.450 3.76e3 14 Campament… Antof… Tocopilla Tocop… 02 023 2301 27 5.52 4.72e4 15 Luz Divin… Antof… Antofaga… Antof… 02 021 2101 0 0.934 7.81e3 16 Villa Esp… Antof… Antofaga… Antof… 02 021 2101 0 0.623 5.20e3 17 Valle Her… Biobío Arauco Los A… 08 082 8206 71 8.18 5.14e4 18 Rucamanque Biobío Concepci… Talca… 08 081 8110 119 5.24 3.37e4 19 Caleta Co… Antof… Tocopilla Tocop… 02 023 2301 0 5.38 4.56e4 20 Caleta Co… Antof… Tocopilla Tocop… 02 023 2301 0 1.00 8.52e3 # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; Una función útil para poder revisar los datos rápidamente de una forma distinta es glimpse(), que nos muestra la tabla pero con las columnas hacia abajo y los valores hacia el lado. Es como ver la tabla girada, permitiéndonos ver todas sus columnas, sus formatos, y un ejemplo de las primeras observaciones de cada variable.\ndatos |\u0026gt; glimpse() Rows: 1,432 Columns: 12 $ nombre \u0026lt;chr\u0026gt; \u0026quot;Bellavista\u0026quot;, \u0026quot;Buena Vista\u0026quot;, \u0026quot;Los Fleteros\u0026quot;, \u0026quot;Pueblo Hun… $ region \u0026lt;chr\u0026gt; \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, … $ provincia \u0026lt;chr\u0026gt; \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, … $ comuna \u0026lt;chr\u0026gt; \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, … $ cut_r \u0026lt;chr\u0026gt; \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;0… $ cut_p \u0026lt;chr\u0026gt; \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, … $ cut \u0026lt;dbl\u0026gt; 5101, 5101, 5101, 5101, 5101, 5101, 5101, 5101, 5101, 58… $ hogares \u0026lt;dbl\u0026gt; 29, 32, 56, 65, 33, 17, 24, 21, 69, 32, 42, 37, 146, 35,… $ hectareas \u0026lt;dbl\u0026gt; 1.5138015, 1.6700459, 2.4914854, 4.6023230, 0.8673470, 0… $ area \u0026lt;dbl\u0026gt; 10617.233, 11707.446, 17465.450, 32261.178, 6080.654, 28… $ observaciones \u0026lt;chr\u0026gt; \u0026quot;CAMPAMENTO EN CATASTRO NACIONAL 2022. EXTENSIÓN DE LÍMI… $ año \u0026lt;chr\u0026gt; \u0026quot;CATASTRO_2011\u0026quot;, \u0026quot;CATASTRO_2011\u0026quot;, \u0026quot;CATASTRO_2011\u0026quot;, \u0026quot;CATA… La función slice() permite extraer filas específicas de la tabla, según su posición. Por ejemplo, aquí extraeremos las filas número 1000 al 1010:\ndatos |\u0026gt; slice(1000:1010) # A tibble: 11 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Batuco Metro… Chacabuco Lampa 13 133 13302 84 11.5 7.99e4 2 Jerusalén Metro… Chacabuco Lampa 13 133 13302 565 32.1 2.24e5 3 Borde Rio… Liber… Cachapoal Doñih… 06 061 6105 0 10.5 7.15e4 4 Toma Vist… Metro… Chacabuco Lampa 13 133 13302 539 14.5 1.01e5 5 Medialuna… Metro… Chacabuco Lampa 13 133 13302 104 2.23 1.56e4 6 Puente Li… Liber… Cachapoal Malloa 06 061 6109 0 1.75 1.18e4 7 Mirador Metro… Chacabuco Lampa 13 133 13302 117 5.37 3.74e4 8 Esperando… Liber… Cachapoal Rengo 06 061 6115 0 1.15 7.84e3 9 Central L… Metro… Chacabuco Lampa 13 133 13302 14 1.79 1.25e4 10 Los Tronc… Liber… Cachapoal Rengo 06 061 6115 0 0.970 6.59e3 11 Los Tubos Metro… Maipo Buin 13 134 13402 27 0.864 5.95e3 # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; Ordenar Ordenar observaciones en base a una o varias columnas\ndatos |\u0026gt; arrange(nombre) # A tibble: 1,432 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 05 De Abr… Ataca… Copiapó Copia… 03 031 3101 27 1.11 8.71e3 2 10 De Ago… Tarap… Iquique Alto … 01 011 1107 104 1.63 1.43e4 3 12 De Feb… Liber… Cachapoal Macha… 06 061 6108 47 12.7 8.64e4 4 12 De May… Metro… Chacabuco Colina 13 133 13301 148 2.16 1.51e4 5 12 De Oct… Ataca… Copiapó Copia… 03 031 3101 12 0.791 6.22e3 6 13 De Jun… Tarap… Tamarugal Pozo … 01 014 1401 0 0.224 1.96e3 7 14 de feb… La Ar… Cautín Temuco 09 091 9101 0 0.635 3.86e3 8 15 De Feb… Ataca… Copiapó Copia… 03 031 3101 16 0.499 3.92e3 9 17 De Mayo Metro… Santiago Cerro… 13 131 13103 158 15.7 1.09e5 10 18 De Oct… Valpa… Marga Ma… Villa… 05 058 5804 18 1.15 8.04e3 # ℹ 1,422 more rows # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; datos |\u0026gt; arrange(desc(hogares)) # A tibble: 1,432 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Alto Molle Tarap… Iquique Alto … 01 011 1107 3390 130. 1.14e6 2 Manuel Bu… Valpa… Valparaí… Viña … 05 051 5109 1647 90.2 6.33e5 3 América I… Metro… Santiago Cerri… 13 131 13102 1550 29.8 2.07e5 4 Dignidad Metro… Chacabuco Colina 13 133 13301 1004 15.8 1.10e5 5 Milla Ant… Metro… Cordille… Puent… 13 132 13201 1001 23.0 1.59e5 6 Comité de… Antof… Antofaga… Antof… 02 021 2101 1000 14.8 1.24e5 7 Centinela… Valpa… San Anto… San A… 05 056 5601 833 111. 7.69e5 8 Vista Her… Valpa… San Anto… Carta… 05 056 5603 789 64.6 4.48e5 9 Aguas Sal… Valpa… San Anto… San A… 05 056 5601 688 45.1 3.12e5 10 Felipe Ca… Valpa… Valparaí… Viña … 05 051 5109 669 37.0 2.59e5 # ℹ 1,422 more rows # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; datos |\u0026gt; arrange(comuna, desc(hogares)) |\u0026gt; select(nombre, comuna, hogares) # A tibble: 1,432 × 3 nombre comuna hogares \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Las Quilas II Aisén 83 2 Las Avutardas Aisén 16 3 CAMINO CEMENTERIO Aisén 8 4 Almirante Simpson Aisén 0 5 Pueblo Hundido Alto Biobío 30 6 Pueblo Hundido Alto Biobío 13 7 Sector Pangue Alto Biobío 10 8 Alto Molle Alto Hospicio 3390 9 Ex Vertedero Norte Alto Hospicio 394 10 La Pampa 2 Alto Hospicio 347 # ℹ 1,422 more rows datos |\u0026gt; glimpse() Rows: 1,432 Columns: 12 $ nombre \u0026lt;chr\u0026gt; \u0026quot;Bellavista\u0026quot;, \u0026quot;Buena Vista\u0026quot;, \u0026quot;Los Fleteros\u0026quot;, \u0026quot;Pueblo Hun… $ region \u0026lt;chr\u0026gt; \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, … $ provincia \u0026lt;chr\u0026gt; \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, … $ comuna \u0026lt;chr\u0026gt; \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, \u0026quot;Valparaíso\u0026quot;, … $ cut_r \u0026lt;chr\u0026gt; \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;0… $ cut_p \u0026lt;chr\u0026gt; \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, \u0026quot;051\u0026quot;, … $ cut \u0026lt;dbl\u0026gt; 5101, 5101, 5101, 5101, 5101, 5101, 5101, 5101, 5101, 58… $ hogares \u0026lt;dbl\u0026gt; 29, 32, 56, 65, 33, 17, 24, 21, 69, 32, 42, 37, 146, 35,… $ hectareas \u0026lt;dbl\u0026gt; 1.5138015, 1.6700459, 2.4914854, 4.6023230, 0.8673470, 0… $ area \u0026lt;dbl\u0026gt; 10617.233, 11707.446, 17465.450, 32261.178, 6080.654, 28… $ observaciones \u0026lt;chr\u0026gt; \u0026quot;CAMPAMENTO EN CATASTRO NACIONAL 2022. EXTENSIÓN DE LÍMI… $ año \u0026lt;chr\u0026gt; \u0026quot;CATASTRO_2011\u0026quot;, \u0026quot;CATASTRO_2011\u0026quot;, \u0026quot;CATASTRO_2011\u0026quot;, \u0026quot;CATA… datos2 \u0026lt;- datos |\u0026gt; select(1:4) |\u0026gt; slice_sample(n = 10) Seleccionar Podemos reducir la cantidad de columnas de nuestra tabla usando select(). Esto puede servirnos para acotar la exploración de los datos, cuando no necesitamos ver todas las columnas al mismo tiempo.\ndatos |\u0026gt; select(nombre, hectareas) # A tibble: 1,432 × 2 nombre hectareas \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Bellavista 1.51 2 Buena Vista 1.67 3 Los Fleteros 2.49 4 Pueblo Hundido 4.60 5 Villa Italia Turín 0.867 6 Nueva Esperanza 0.399 7 Francisco Vergara 2.85 8 Manuel Rodríguez 0.443 9 Sofia 35 2.68 10 Las Viñas De Irene Frei 1.24 # ℹ 1,422 more rows Ante poniendo un signo menos antes del nombre de una columna podemos excluirla de nuestra tabla.\ndatos |\u0026gt; select(-cut, -cut_r, -cut_p) # A tibble: 1,432 × 9 nombre region provincia comuna hogares hectareas area observaciones año \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Bellavi… Valpa… Valparaí… Valpa… 29 1.51 10617. CAMPAMENTO E… CATA… 2 Buena V… Valpa… Valparaí… Valpa… 32 1.67 11707. CAMPAMENTO E… CATA… 3 Los Fle… Valpa… Valparaí… Valpa… 56 2.49 17465. CAMPAMENTO E… CATA… 4 Pueblo … Valpa… Valparaí… Valpa… 65 4.60 32261. CAMPAMENTO E… CATA… 5 Villa I… Valpa… Valparaí… Valpa… 33 0.867 6081. CAMPAMENTO E… CATA… 6 Nueva E… Valpa… Valparaí… Valpa… 17 0.399 2802. CAMPAMENTO E… CATA… 7 Francis… Valpa… Valparaí… Valpa… 24 2.85 19955. CAMPAMENTO E… CATA… 8 Manuel … Valpa… Valparaí… Valpa… 21 0.443 3103. CAMPAMENTO E… CATA… 9 Sofia 35 Valpa… Valparaí… Valpa… 69 2.68 18819. CAMPAMENTO E… CATA… 10 Las Viñ… Valpa… Marga Ma… Villa… 32 1.24 8704. CAMPAMENTO E… CATA… # ℹ 1,422 more rows Recordemos que, hasta que no asignemos estas operaciones a un nuevo objeto, no estamos modificando nuestros datos realmente, sino que solamente estamos aplicando operaciones sobre los datos de una forma no destructiva, como exploración o a modo de prueba.\nLa función selecta acepta diversas formas de poder seleccionar o de seleccionar variables en base a sus nombres.\nExcluir columnas que incluyan cierto texto dentro de sus nombres:\ndatos |\u0026gt; select(-contains(\u0026#34;cut\u0026#34;)) # A tibble: 1,432 × 9 nombre region provincia comuna hogares hectareas area observaciones año \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Bellavi… Valpa… Valparaí… Valpa… 29 1.51 10617. CAMPAMENTO E… CATA… 2 Buena V… Valpa… Valparaí… Valpa… 32 1.67 11707. CAMPAMENTO E… CATA… 3 Los Fle… Valpa… Valparaí… Valpa… 56 2.49 17465. CAMPAMENTO E… CATA… 4 Pueblo … Valpa… Valparaí… Valpa… 65 4.60 32261. CAMPAMENTO E… CATA… 5 Villa I… Valpa… Valparaí… Valpa… 33 0.867 6081. CAMPAMENTO E… CATA… 6 Nueva E… Valpa… Valparaí… Valpa… 17 0.399 2802. CAMPAMENTO E… CATA… 7 Francis… Valpa… Valparaí… Valpa… 24 2.85 19955. CAMPAMENTO E… CATA… 8 Manuel … Valpa… Valparaí… Valpa… 21 0.443 3103. CAMPAMENTO E… CATA… 9 Sofia 35 Valpa… Valparaí… Valpa… 69 2.68 18819. CAMPAMENTO E… CATA… 10 Las Viñ… Valpa… Marga Ma… Villa… 32 1.24 8704. CAMPAMENTO E… CATA… # ℹ 1,422 more rows Seleccionar columnas que empiecen con cierto texto:\ndatos |\u0026gt; select(starts_with(\u0026#34;com\u0026#34;)) # A tibble: 1,432 × 1 comuna \u0026lt;chr\u0026gt; 1 Valparaíso 2 Valparaíso 3 Valparaíso 4 Valparaíso 5 Valparaíso 6 Valparaíso 7 Valparaíso 8 Valparaíso 9 Valparaíso 10 Villa Alemana # ℹ 1,422 more rows Seleccionar comunas que terminen con un texto determinado:\ndatos |\u0026gt; select(ends_with(\u0026#34;a\u0026#34;)) # A tibble: 1,432 × 3 provincia comuna area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Valparaíso Valparaíso 10617. 2 Valparaíso Valparaíso 11707. 3 Valparaíso Valparaíso 17465. 4 Valparaíso Valparaíso 32261. 5 Valparaíso Valparaíso 6081. 6 Valparaíso Valparaíso 2802. 7 Valparaíso Valparaíso 19955. 8 Valparaíso Valparaíso 3103. 9 Valparaíso Valparaíso 18819. 10 Marga Marga Villa Alemana 8704. # ℹ 1,422 more rows También podemos seleccionar columnas de un determinado tipo. En este caso, seleccionamos solamente las variables que sean numéricas:\ndatos |\u0026gt; select(where(is.numeric)) # A tibble: 1,432 × 4 cut hogares hectareas area \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 5101 29 1.51 10617. 2 5101 32 1.67 11707. 3 5101 56 2.49 17465. 4 5101 65 4.60 32261. 5 5101 33 0.867 6081. 6 5101 17 0.399 2802. 7 5101 24 2.85 19955. 8 5101 21 0.443 3103. 9 5101 69 2.68 18819. 10 5804 32 1.24 8704. # ℹ 1,422 more rows Conteos La función Count se aplica a una de las variables de nuestros datos, y cuenta la cantidad de observaciones que se corresponden con cada uno de los valores o niveles posibles de la variable. En otras palabras, cuenta la frecuencia de cada categoría de respuesta de la variable.\ndatos |\u0026gt; count(region) # A tibble: 16 × 2 region n \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 Antofagasta 116 2 Arica y Parinacota 14 3 Atacama 121 4 Aysén del General Carlos Ibáñez del Campo 10 5 Biobío 225 6 Coquimbo 52 7 La Araucanía 70 8 Libertador General Bernardo O'Higgins 59 9 Los Lagos 67 10 Los Ríos 40 11 Magallanes y de la Antártica Chilena 3 12 Maule 25 13 Metropolitana 168 14 Tarapacá 64 15 Valparaíso 374 16 Ñuble 24 Podemos combinar un conteo de una variable con el ordenamiento de las filas en base al conteo realizado, resultando una tabla ordenada de mayor a menor según las observaciones contadas:\ndatos |\u0026gt; count(region) |\u0026gt; arrange(desc(n)) # A tibble: 16 × 2 region n \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 Valparaíso 374 2 Biobío 225 3 Metropolitana 168 4 Atacama 121 5 Antofagasta 116 6 La Araucanía 70 7 Los Lagos 67 8 Tarapacá 64 9 Libertador General Bernardo O'Higgins 59 10 Coquimbo 52 11 Los Ríos 40 12 Maule 25 13 Ñuble 24 14 Arica y Parinacota 14 15 Aysén del General Carlos Ibáñez del Campo 10 16 Magallanes y de la Antártica Chilena 3 Naturalmente, es posible realizar conteos por más de una variable, mostrando las combinaciones entre ambas:\ndatos |\u0026gt; count(region, comuna) |\u0026gt; arrange(desc(n)) # A tibble: 195 × 3 region comuna n \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 Valparaíso Viña del Mar 114 2 Valparaíso Valparaíso 99 3 Antofagasta Antofagasta 87 4 Atacama Copiapó 82 5 Tarapacá Alto Hospicio 45 6 La Araucanía Temuco 41 7 Biobío Talcahuano 32 8 Valparaíso Quilpué 32 9 Biobío Lota 30 10 Valparaíso San Antonio 27 # ℹ 185 more rows Filtros Podemos realizar una comparación sobre una columna del dataframe para filtrar las observaciones (filas), dejando solamente las filas que cumplen con la condición.\nPor ejemplo, filtrar solamente los casos donde la cantidad de hogares sea mayor a 80:\ndatos |\u0026gt; filter(hogares \u0026gt; 80) # A tibble: 239 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Ampliació… Valpa… Marga Ma… Villa… 05 058 5804 146 15.1 1.06e5 2 Violeta P… Valpa… Valparaí… Valpa… 05 051 5101 123 10.7 7.50e4 3 Mesana Al… Valpa… Valparaí… Valpa… 05 051 5101 128 14.5 1.02e5 4 Pampa Ilu… Valpa… Valparaí… Valpa… 05 051 5101 195 7.04 4.93e4 5 Parcela 11 Valpa… Valparaí… Viña … 05 051 5109 520 44.0 3.08e5 6 Parcela 15 Valpa… Valparaí… Viña … 05 051 5109 257 15.4 1.08e5 7 Campanill… Valpa… Valparaí… Valpa… 05 051 5101 98 6.19 4.34e4 8 La Isla Valpa… Valparaí… Valpa… 05 051 5101 116 4.51 3.16e4 9 Nueva Aur… Valpa… Valparaí… Viña … 05 051 5109 91 7.47 5.24e4 10 Valle El … Valpa… Valparaí… Viña … 05 051 5109 107 4.04 2.84e4 # ℹ 229 more rows # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; datos |\u0026gt; filter(region == \u0026#34;Ñuble\u0026#34;) # A tibble: 24 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Los Corre… Ñuble Diguillín Chill… 16 161 16101 13 0.250 1.61e3 2 Línea Fér… Ñuble Diguillín Chill… 16 161 16101 35 3.21 2.07e4 3 Litral Co… Ñuble Diguillín Chill… 16 161 16101 26 5.33 3.43e4 4 Los Monte… Ñuble Diguillín Chill… 16 161 16101 47 5.57 3.59e4 5 Orilla It… Ñuble Itata Porte… 16 162 16205 11 1.41 9.07e3 6 Pablo Ner… Ñuble Itata Quiri… 16 162 16201 39 22.7 1.47e5 7 Esmeralda… Ñuble Punilla San N… 16 163 16305 13 0.232 1.50e3 8 Santa Lau… Ñuble Punilla San N… 16 163 16305 22 8.28 5.34e4 9 El Esfuer… Ñuble Diguillín Bulnes 16 161 16102 11 0.632 4.05e3 10 El Refugio Ñuble Diguillín Bulnes 16 161 16102 16 1.11 7.10e3 # ℹ 14 more rows # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; Es posible realizar dos o más filtros de forma consecutiva:\ndatos |\u0026gt; filter(hogares \u0026gt; 90, hectareas \u0026gt; 30) # A tibble: 18 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Parcela 11 Valpa… Valparaí… Viña … 05 051 5109 520 44.0 3.08e5 2 Manuel Bu… Valpa… Valparaí… Viña … 05 051 5109 1647 90.2 6.33e5 3 Reñaca Al… Valpa… Valparaí… Viña … 05 051 5109 663 39.1 2.75e5 4 Felipe Ca… Valpa… Valparaí… Viña … 05 051 5109 669 37.0 2.59e5 5 Aguas Sal… Valpa… San Anto… San A… 05 056 5601 688 45.1 3.12e5 6 Alto Mira… Valpa… San Anto… San A… 05 056 5601 560 47.7 3.30e5 7 Centinela… Valpa… San Anto… San A… 05 056 5601 833 111. 7.69e5 8 Rol 9034-1 Valpa… San Anto… San A… 05 056 5601 227 32.1 2.22e5 9 Vista Her… Valpa… San Anto… Carta… 05 056 5603 789 64.6 4.48e5 10 El Colora… Valpa… Marga Ma… Limac… 05 058 5802 235 59.1 4.15e5 11 Mirando l… Tarap… Iquique Alto … 01 011 1107 164 114. 9.96e5 12 Alto Molle Tarap… Iquique Alto … 01 011 1107 3390 130. 1.14e6 13 El Castaño Biobío Arauco Curan… 08 082 8205 92 56.2 3.55e5 14 Ribera De… Metro… Talagante Talag… 13 136 13601 115 37.8 2.61e5 15 Jerusalén Metro… Chacabuco Lampa 13 133 13302 565 32.1 2.24e5 16 Las Minas… Biobío Arauco Curan… 08 082 8205 100 88.3 5.58e5 17 Villa El … Biobío Arauco Curan… 08 082 8205 135 129. 8.16e5 18 Ex Inia Coqui… Choapa Los V… 04 042 4203 315 36.6 2.63e5 # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; También es posible usar otros operadores dentro de las comparaciones, Tales como el operador o |, que en este caso nos va a permitir filtrar las observaciones que cumplan con una o con otra condición:\ndatos |\u0026gt; filter(hogares \u0026gt; 300 | hectareas \u0026gt; 100) # A tibble: 33 × 12 nombre region provincia comuna cut_r cut_p cut hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Parcela 11 Valpa… Valparaí… Viña … 05 051 5109 520 44.0 3.08e5 2 Manuel Bu… Valpa… Valparaí… Viña … 05 051 5109 1647 90.2 6.33e5 3 Reñaca Al… Valpa… Valparaí… Viña … 05 051 5109 663 39.1 2.75e5 4 Felipe Ca… Valpa… Valparaí… Viña … 05 051 5109 669 37.0 2.59e5 5 Yevide Valpa… San Feli… San F… 05 057 5701 306 13.4 9.45e4 6 Aguas Sal… Valpa… San Anto… San A… 05 056 5601 688 45.1 3.12e5 7 Alto Mira… Valpa… San Anto… San A… 05 056 5601 560 47.7 3.30e5 8 Centinela… Valpa… San Anto… San A… 05 056 5601 833 111. 7.69e5 9 Fuerza Gu… Valpa… San Anto… Carta… 05 056 5603 342 16.5 1.14e5 10 Vista Her… Valpa… San Anto… Carta… 05 056 5603 789 64.6 4.48e5 # ℹ 23 more rows # ℹ 2 more variables: observaciones \u0026lt;chr\u0026gt;, año \u0026lt;chr\u0026gt; El filtro anterior deja las filas donde los hogares sean mayores a 300, o bien, las hectáreas sean mayores a 100, pudiendo darse el caso de que hayan filas con hogares mayores 300 pero hectáreas menores de 100, o con menos de 300 hogares pero con más de 100 hectáreas.\nAplicando lo aprendido hasta el momento, podemos combinar un filtro con un conteo y un orden, así obtenemos un conteo de campamentos por comuna bajo un primer criterio de filtro, ordenados de mayor a menor:\ndatos |\u0026gt; filter(hogares \u0026gt; 10) |\u0026gt; count(comuna) |\u0026gt; arrange(desc(n)) # A tibble: 177 × 2 comuna n \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 Viña del Mar 101 2 Valparaíso 89 3 Antofagasta 80 4 Copiapó 70 5 Alto Hospicio 42 6 Temuco 35 7 Lota 30 8 Talcahuano 26 9 San Antonio 22 10 Lampa 21 # ℹ 167 more rows Crear variables Hasta ahora, hemos explorado solamente con los datos que vienen directamente desde el archivo que cargamos. A continuación, crearemos nuevas variables a partir de los datos existentes, para potenciar nuestro análisis.\nLa función mutate() crea nuevas variables. Lo primero que se indica dentro de mutate() es el nombre de la nueva variable que vamos a crear, después de un signo igual (=), y después la operación que creará esta nueva variable.\nEn este primer ejemplo, crearemos la variable prueba, que contendrá un texto en todas las filas.\ndatos |\u0026gt; select(1:4, hogares) |\u0026gt; mutate(prueba = \u0026#34;hola\u0026#34;) # A tibble: 1,432 × 6 nombre region provincia comuna hogares prueba \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Bellavista Valparaíso Valparaíso Valparaíso 29 hola 2 Buena Vista Valparaíso Valparaíso Valparaíso 32 hola 3 Los Fleteros Valparaíso Valparaíso Valparaíso 56 hola 4 Pueblo Hundido Valparaíso Valparaíso Valparaíso 65 hola 5 Villa Italia Turín Valparaíso Valparaíso Valparaíso 33 hola 6 Nueva Esperanza Valparaíso Valparaíso Valparaíso 17 hola 7 Francisco Vergara Valparaíso Valparaíso Valparaíso 24 hola 8 Manuel Rodríguez Valparaíso Valparaíso Valparaíso 21 hola 9 Sofia 35 Valparaíso Valparaíso Valparaíso 69 hola 10 Las Viñas De Irene Frei Valparaíso Marga Marga Villa Alemana 32 hola # ℹ 1,422 more rows Usamos la función paste() para crear una nueva variable que contenga un texto, al cual le agregamos la cifra de otra variable. La operación se realizará para cada una de las filas de nuestra tabla, utilizando las cifras que corresponda a la fila en cuestión:\ndatos |\u0026gt; select(nombre, hogares) |\u0026gt; mutate(texto = paste(\u0026#34;Numero de hogares:\u0026#34;, hogares)) # A tibble: 1,432 × 3 nombre hogares texto \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Bellavista 29 Numero de hogares: 29 2 Buena Vista 32 Numero de hogares: 32 3 Los Fleteros 56 Numero de hogares: 56 4 Pueblo Hundido 65 Numero de hogares: 65 5 Villa Italia Turín 33 Numero de hogares: 33 6 Nueva Esperanza 17 Numero de hogares: 17 7 Francisco Vergara 24 Numero de hogares: 24 8 Manuel Rodríguez 21 Numero de hogares: 21 9 Sofia 35 69 Numero de hogares: 69 10 Las Viñas De Irene Frei 32 Numero de hogares: 32 # ℹ 1,422 more rows Es posible usar cualquier función que opere sobre una variable del tipo que corresponda. Por ejemplo, podemos redondear los valores de una variable numérica, y si a esta nueva variable creada le asignamos al mismo nombre de la variable original, la variable original será reemplazada por la versión nueva, con sus datos redondeados.\ndatos |\u0026gt; select(nombre, where(is.numeric), -cut) |\u0026gt; mutate(hectareas = round(hectareas, digits = 1), area = round(area, digits = 0)) # A tibble: 1,432 × 4 nombre hogares hectareas area \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Bellavista 29 1.5 10617 2 Buena Vista 32 1.7 11707 3 Los Fleteros 56 2.5 17465 4 Pueblo Hundido 65 4.6 32261 5 Villa Italia Turín 33 0.9 6081 6 Nueva Esperanza 17 0.4 2802 7 Francisco Vergara 24 2.8 19955 8 Manuel Rodríguez 21 0.4 3103 9 Sofia 35 69 2.7 18819 10 Las Viñas De Irene Frei 32 1.2 8704 # ℹ 1,422 more rows También podemos crear una nueva variable a partir de un cálculo matemático:\ndatos |\u0026gt; select(nombre, where(is.numeric), -cut) |\u0026gt; mutate(densidad = hectareas/hogares) # A tibble: 1,432 × 5 nombre hogares hectareas area densidad \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Bellavista 29 1.51 10617. 0.0522 2 Buena Vista 32 1.67 11707. 0.0522 3 Los Fleteros 56 2.49 17465. 0.0445 4 Pueblo Hundido 65 4.60 32261. 0.0708 5 Villa Italia Turín 33 0.867 6081. 0.0263 6 Nueva Esperanza 17 0.399 2802. 0.0235 7 Francisco Vergara 24 2.85 19955. 0.119 8 Manuel Rodríguez 21 0.443 3103. 0.0211 9 Sofia 35 69 2.68 18819. 0.0389 10 Las Viñas De Irene Frei 32 1.24 8704. 0.0388 # ℹ 1,422 more rows Si creamos una nueva variable en base a una comparación (en este caso, que las hectáreas sean mayores a 2), entonces la nueva variable será de tipo lógico, indicando las observaciones que cumplen con la comparación con TRUE, y las que no con FALSE.\ndatos |\u0026gt; select(nombre, hectareas) |\u0026gt; mutate(prioridad = hectareas \u0026gt; 2) # A tibble: 1,432 × 3 nombre hectareas prioridad \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;lgl\u0026gt; 1 Bellavista 1.51 FALSE 2 Buena Vista 1.67 FALSE 3 Los Fleteros 2.49 TRUE 4 Pueblo Hundido 4.60 TRUE 5 Villa Italia Turín 0.867 FALSE 6 Nueva Esperanza 0.399 FALSE 7 Francisco Vergara 2.85 TRUE 8 Manuel Rodríguez 0.443 FALSE 9 Sofia 35 2.68 TRUE 10 Las Viñas De Irene Frei 1.24 FALSE # ℹ 1,422 more rows Si lo pensamos, ésta es la misma forma mediante la cual funciona la función filter(): se establece una comparación, se evalúa si cada una de las filas cumple con la comparación, y finalmente se eliminan las que no cumplen (FALSE).\nVariables dicotómicas con if_else() Siguiendo la lógica del ejemplo anterior, podemos usar una función que nos ayude a crear variables en base a si las filas cumplen o no con una o varias condiciones que definamos. A esta operación se llama if else, que en español sería si se cumple, entonces esto, y si no, esto otro.\nEntonces, podemos usar if_else() para crear nuevas variables en base a una comparación, pero en vez de retornar verdadero o falso, puede retornar los valores que nosotros le pidamos.\nEjemplo:\nplata \u0026lt;- 1000000 plata \u0026lt; 300000 [1] FALSE # una comparación normal retorna TRUE o FALSE # pero en el ifelse, le especificamos primero la comparación, # luego lo que queremos que retorne si la comparación es TRUE, # y finalmente lo que queremos que retorne si es false if_else(plata \u0026lt; 300000, true = \u0026#34;poca\u0026#34;, false = \u0026#34;mucha\u0026#34;) [1] \u0026quot;mucha\u0026quot; # también se puede escribir obviando el \u0026#34;yes\u0026#34; y \u0026#34;no\u0026#34; if_else(plata \u0026lt; 300000, \u0026#34;poca\u0026#34;, \u0026#34;mucha\u0026#34;) [1] \u0026quot;mucha\u0026quot; Siguiendo el ejemplo, podemos usar la función dentro de mutate para crear nuestra nueva variable dicotómica:\ndatos |\u0026gt; select(nombre, hogares) |\u0026gt; mutate(tipo = if_else(hogares \u0026gt; 40, true = \u0026#34;grande\u0026#34;, false = \u0026#34;chico\u0026#34;) ) # A tibble: 1,432 × 3 nombre hogares tipo \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Bellavista 29 chico 2 Buena Vista 32 chico 3 Los Fleteros 56 grande 4 Pueblo Hundido 65 grande 5 Villa Italia Turín 33 chico 6 Nueva Esperanza 17 chico 7 Francisco Vergara 24 chico 8 Manuel Rodríguez 21 chico 9 Sofia 35 69 grande 10 Las Viñas De Irene Frei 32 chico # ℹ 1,422 more rows Otro ejemplo:\ndatos |\u0026gt; select(nombre, region, hectareas) |\u0026gt; mutate(prioridad = if_else(hectareas \u0026gt; 2, \u0026#34;alta\u0026#34;, \u0026#34;normal\u0026#34;)) |\u0026gt; count(region, prioridad) # A tibble: 31 × 3 region prioridad n \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 Antofagasta alta 45 2 Antofagasta normal 71 3 Arica y Parinacota alta 8 4 Arica y Parinacota normal 6 5 Atacama alta 45 6 Atacama normal 76 7 Aysén del General Carlos Ibáñez del Campo alta 3 8 Aysén del General Carlos Ibáñez del Campo normal 7 9 Biobío alta 128 10 Biobío normal 97 # ℹ 21 more rows Para los siguientes ejemplos, crearemos un nuevo dataframe en base al anterior, pero que contenga nuevas variables creadas mediante operaciones matemáticas:\ndatos_2 \u0026lt;- datos |\u0026gt; mutate(densidad_ha = hectareas / hogares, area_km = area/1000, densidad_km = area_km / hogares) Variables más complejas con case_when() La función case_when() es equivalente a usar varios if_else() seguidos, y por lo tanto nos permite crear variables más complejas, que tengan más de dos categorías. Se utiliza poniendo todas las evaluaciones junto al valor que queremos que adopten si es que estas comparaciones son TRUE.\nComo prueba, vamos a incluir una sola comparación, para ver que le otorga el valor solo a las observaciones correspondientes, y las demás las deja vacías:\ndatos_2 |\u0026gt; select(nombre, densidad_km) |\u0026gt; mutate(categoria = case_when(densidad_km \u0026lt; 0.3 ~ \u0026#34;baja\u0026#34;)) # A tibble: 1,432 × 3 nombre densidad_km categoria \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Bellavista 0.366 \u0026lt;NA\u0026gt; 2 Buena Vista 0.366 \u0026lt;NA\u0026gt; 3 Los Fleteros 0.312 \u0026lt;NA\u0026gt; 4 Pueblo Hundido 0.496 \u0026lt;NA\u0026gt; 5 Villa Italia Turín 0.184 baja 6 Nueva Esperanza 0.165 baja 7 Francisco Vergara 0.831 \u0026lt;NA\u0026gt; 8 Manuel Rodríguez 0.148 baja 9 Sofia 35 0.273 baja 10 Las Viñas De Irene Frei 0.272 baja # ℹ 1,422 more rows El orden en que ponemos las comparaciones será importante, porque se ejecutan en el orden que las escribas, así que una vez que una fila adquiere un valor, la fila deja de ser evaluada en las siguientes comparaciones.\nEl siguiente ejemplo usa tres comparaciones para abarcar todo el rango de números de la variable hogares:\ndatos_2 |\u0026gt; select(nombre, hogares) |\u0026gt; mutate(tamaño = case_when(hogares \u0026gt; 60 ~ \u0026#34;grande\u0026#34;, hogares \u0026gt; 30 ~ \u0026#34;mediano\u0026#34;, hogares \u0026lt;= 30 ~ \u0026#34;chico\u0026#34;)) # A tibble: 1,432 × 3 nombre hogares tamaño \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Bellavista 29 chico 2 Buena Vista 32 mediano 3 Los Fleteros 56 mediano 4 Pueblo Hundido 65 grande 5 Villa Italia Turín 33 mediano 6 Nueva Esperanza 17 chico 7 Francisco Vergara 24 chico 8 Manuel Rodríguez 21 chico 9 Sofia 35 69 grande 10 Las Viñas De Irene Frei 32 mediano # ℹ 1,422 more rows Otro ejemplo:\ndatos_2 |\u0026gt; select(nombre, densidad_km) |\u0026gt; mutate(categoria = case_when(densidad_km \u0026lt; 0.3 ~ \u0026#34;baja\u0026#34;, densidad_km \u0026lt; 0.6 ~ \u0026#34;media\u0026#34;, densidad_km \u0026lt; 0.9 ~ \u0026#34;alta\u0026#34;, densidad_km \u0026gt;= 0.9 ~ \u0026#34;muy alta\u0026#34; )) # A tibble: 1,432 × 3 nombre densidad_km categoria \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Bellavista 0.366 media 2 Buena Vista 0.366 media 3 Los Fleteros 0.312 media 4 Pueblo Hundido 0.496 media 5 Villa Italia Turín 0.184 baja 6 Nueva Esperanza 0.165 baja 7 Francisco Vergara 0.831 alta 8 Manuel Rodríguez 0.148 baja 9 Sofia 35 0.273 baja 10 Las Viñas De Irene Frei 0.272 baja # ℹ 1,422 more rows En el ejemplo anterior, usamos comparaciones que proponen un valor numérico, y coinciden con los valores inferiores al valor que pusimos. Entonces, el orden de asignación de los valores para la nueva variable va de menor a mayor: de 0.3 para abajo, de 0.6 para abajo, de 0.9 para abajo. Lo importante es entender que, una vez que una observación adquiere su etiqueta, las demás comparaciones no la van a sobreescribir. Por ejemplo, si tenemos el valor 0.2, va a ser etiquetado por la comparación \u0026ldquo;valores menores a 0.3\u0026rdquo;, porque es menor a 0.3. Pero cuando se aplique la siguiente comparación, que es \u0026ldquo;valores menores a 0.6\u0026rdquo;, a pesar de que 0.2 también es menor a 0.6, no va a recibir una nueva etiqueta, porque ya obtuvo una en la comparación anterior (menores a 0.3).\nLas comparaciones también pueden combinarse para volverse más específicas. En el siguiente ejemplo definiremos no solamente si son menores a un valor, sino que definimos un rango de valores:\nmayores a 0 y menores a 3 mayores o iguales a 0.3 y menores a 0.6 etc Podemos especificar el argumento .default para que le otorgue a un valor a \u0026ldquo;todos los demás\u0026rdquo;; es decir, a los valores que no coincidieron con ninguna de las condiciones dadas.\ndatos_2 |\u0026gt; mutate(categoria = case_when(densidad_km \u0026gt; 0 \u0026amp; densidad_km \u0026lt; 0.3 ~ \u0026#34;baja\u0026#34;, densidad_km \u0026gt;= 0.3 \u0026amp; densidad_km \u0026lt; 0.6 ~ \u0026#34;media\u0026#34;, densidad_km \u0026gt;= 0.6 \u0026amp; densidad_km \u0026lt; 0.9 ~ \u0026#34;alta\u0026#34;, .default = \u0026#34;desconocido\u0026#34;)) |\u0026gt; select(nombre, densidad_km, categoria) |\u0026gt; filter(categoria != \u0026#34;desconocido\u0026#34;) |\u0026gt; print(n = 15) # A tibble: 1,066 × 3 nombre densidad_km categoria \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Bellavista 0.366 media 2 Buena Vista 0.366 media 3 Los Fleteros 0.312 media 4 Pueblo Hundido 0.496 media 5 Villa Italia Turín 0.184 baja 6 Nueva Esperanza 0.165 baja 7 Francisco Vergara 0.831 alta 8 Manuel Rodríguez 0.148 baja 9 Sofia 35 0.273 baja 10 Las Viñas De Irene Frei 0.272 baja 11 Lomas De Bellavista 0.662 alta 12 Manzana 33 0.414 media 13 Ampliación Prat-Lomas De Peñablanca 0.723 alta 14 Los Artesanos 0.401 media 15 La Unión - Elías Lafferte 0.465 media # ℹ 1,051 more rows Finalmente, case_when() nos permite crear cualquier tipo de comparación arbitraria, usando cualquier columna de nuestro dataframe; por ejemplo, en este caso vamos a etiquetar una variable sobre densidad, pero podemos condicionar cierta densidad con una región. En este ejemplo (solo con fines ilustrativos), las observaciones que son de Valparaíso no pueden ser categorizadas como altas:\ndatos_2 |\u0026gt; mutate(categoria = case_when(densidad_km \u0026gt; 0 \u0026amp; densidad_km \u0026lt; 0.3 ~ \u0026#34;baja\u0026#34;, densidad_km \u0026gt;= 0.3 \u0026amp; densidad_km \u0026lt; 0.6 ~ \u0026#34;media\u0026#34;, region != \u0026#34;Valparaíso\u0026#34; \u0026amp; densidad_km \u0026gt;= 0.6 \u0026amp; densidad_km \u0026lt; 0.9 ~ \u0026#34;alta\u0026#34;)) |\u0026gt; select(nombre, region, densidad_km, categoria) # A tibble: 1,432 × 4 nombre region densidad_km categoria \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 Bellavista Valparaíso 0.366 media 2 Buena Vista Valparaíso 0.366 media 3 Los Fleteros Valparaíso 0.312 media 4 Pueblo Hundido Valparaíso 0.496 media 5 Villa Italia Turín Valparaíso 0.184 baja 6 Nueva Esperanza Valparaíso 0.165 baja 7 Francisco Vergara Valparaíso 0.831 \u0026lt;NA\u0026gt; 8 Manuel Rodríguez Valparaíso 0.148 baja 9 Sofia 35 Valparaíso 0.273 baja 10 Las Viñas De Irene Frei Valparaíso 0.272 baja # ℹ 1,422 more rows Pivotar Creamos una pequeña tabla de conteo de casos a partir de dos variables de agrupación: la región y el tamaño.\ntabla \u0026lt;- datos_2 |\u0026gt; mutate(tamaño = case_when(hogares \u0026gt; 60 ~ \u0026#34;grande\u0026#34;, hogares \u0026gt; 30 ~ \u0026#34;mediano\u0026#34;, .default = \u0026#34;chico\u0026#34;)) |\u0026gt; count(region, tamaño) tabla # A tibble: 47 × 3 region tamaño n \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 Antofagasta chico 42 2 Antofagasta grande 52 3 Antofagasta mediano 22 4 Arica y Parinacota chico 4 5 Arica y Parinacota grande 6 6 Arica y Parinacota mediano 4 7 Atacama chico 62 8 Atacama grande 29 9 Atacama mediano 30 10 Aysén del General Carlos Ibáñez del Campo chico 7 # ℹ 37 more rows En esta tabla, cada fila corresponde a una observación distinta; es decir, cada fila adquiere exactamente sólo un valor posible de cada una de las variables. Al mismo tiempo, cada columna solamente corresponde a una variable específica, y cada celda de la tabla solamente aloja un valor. Por ejemplo, en la primera fila, la región es \u0026ldquo;Antofagasta\u0026rdquo;, el tamaño es \u0026ldquo;chico\u0026rdquo;, y n es 42. A esto se le denomina formato largo o Tidy, y es un principio básico de mantener para el análisis de datos.\nSin embargo, al momento de compartir o publicar los datos, estas convenciones sobre ordenamiento de los datos dejan de ser prioridad. Usualmente, en las tablas destinadas para revisar o entregar resultados, las variables numéricas que están agrupadas por otras variables (región y tamaño, en nuestro ejemplo) suelen presentarse con cada una de las categorías de agrupación en una columna distinta. Entonces, Antofagasta ya no usaría tres filas, sino una sola fila, pero habrían tres columnas que correspondían al tamaño, chico mediano y grande, cada una de esas celdas es conteniendo el valor numérico correspondiente. Entonces, terminaríamos con un DataFramed donde cada región corresponda a una fila, y en cada fila habrán tres valores, cada uno correspondiendo a la categoría de la variable tamaño correspondiente.\nPara realizar esta transformación de la estructura de los datos, cargaremos el paquete {tidyr}:\nlibrary(tidyr) La operación que necesitamos realizar es pivotar la tabla a un formato ancho. Para esto, definimos que variable nos entregará los nombres de las nuevas columnas (en este ejemplo, el tamaño), y desde qué variable se sacarán los valores que se ubicarán en cada una de las columnas nuevas:\ntabla_ancha \u0026lt;- tabla |\u0026gt; pivot_wider(names_from = tamaño, values_from = n, values_fill = 0) |\u0026gt; relocate(mediano, .after = chico) tabla_ancha # A tibble: 16 × 4 region chico mediano grande \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; 1 Antofagasta 42 22 52 2 Arica y Parinacota 4 4 6 3 Atacama 62 30 29 4 Aysén del General Carlos Ibáñez del Campo 7 2 1 5 Biobío 116 61 48 6 Coquimbo 30 12 10 7 La Araucanía 48 19 3 8 Libertador General Bernardo O'Higgins 49 6 4 9 Los Lagos 35 17 15 10 Los Ríos 27 6 7 11 Magallanes y de la Antártica Chilena 0 2 1 12 Maule 22 2 1 13 Metropolitana 89 28 51 14 Tarapacá 13 14 37 15 Valparaíso 209 79 86 16 Ñuble 20 3 1 De esta forma, obtenemos una tabla más compacta, que nos permite visualizar los datos de una forma más sencilla.\nlibrary(gt) gt(tabla_ancha) |\u0026gt; gt::tab_header(title = \u0026#34;Campamentos según tamaño\u0026#34;) |\u0026gt; cols_label(region = \u0026#34;Región\u0026#34;) |\u0026gt; tab_style(locations = cells_body(region), style = cell_text(style = \u0026#34;italic\u0026#34;)) Campamentos según tamaño Región chico mediano grande Antofagasta 42 22 52 Arica y Parinacota 4 4 6 Atacama 62 30 29 Aysén del General Carlos Ibáñez del Campo 7 2 1 Biobío 116 61 48 Coquimbo 30 12 10 La Araucanía 48 19 3 Libertador General Bernardo O'Higgins 49 6 4 Los Lagos 35 17 15 Los Ríos 27 6 7 Magallanes y de la Antártica Chilena 0 2 1 Maule 22 2 1 Metropolitana 89 28 51 Tarapacá 13 14 37 Valparaíso 209 79 86 Ñuble 20 3 1 Guardar en Excel Guardar una tabla o dataframe de R en formato Excel es tan sencillo como utilizar la función write_xlsx() y entregarle el objeto y el nombre del archivo que queremos crear.\nlibrary(writexl) write_xlsx(tabla_ancha, \u0026#34;tabla_campamentos.xlsx\u0026#34;) Con esto concluye este tutorial inicial para manipular datos con el paquete {dplyr}. En siguientes tutoriales iremos usando funciones más complejas y avanzadas! 🫣\nSi este tutorial te sirvió, por favor considera hacerme una donación! Cualquier monto me ayuda al menos a poder tomarme un cafecito 🥺\n","date":"2024-11-09T00:00:00Z","excerpt":"Tutorial de introducción al paquete `{dplyr}` para la exploración y análisis de datos con R. Está dirigido a principiantes. En este tutorial veremos cómo explorar un conjunto de datos sociohabitacionales, y a crear nuevas variables.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/tutorial_dplyr_campamentos/","tags":"dplyr","title":"Tutorial: introducción a {dplyr} usando datos de campamentos"},{"content":" {ggplot2} es una librería de visualización de datos bastante popular en el mundo de la ciencia de datos. Sus principales características son su atractivo, su conveniencia para la exploración de datos, un gran potencial de personalización, y un extenso ecosistema de extensiones que nos permiten generar visualizaciones prácticamente de cualquier tipo.\nOtro beneficio de usarlo es propio de el uso de cualquier herramienta programática para generar resultados en el ámbito del análisis de datos: la reusabilidad del código, que nos permite especificar una sola vez el resultado que queremos y luego volver a aplicarlo infinitas veces con distintos datos, datos actualizados, o variaciones de un conjunto de datos, etc.\nEste tutorial está diseñado para empezar desde lo más básico, e ir avanzando de a poco por distintos tipos de visualizaciones para familiarizarse con el modo de uso de este paquete. A medida que tutorial avanza, se van introduciendo nuevos elementos es la medida que son relevantes y apropiados de introducir. También se van introduciendo distintos conjuntos de datos, y usando paquetes auxiliares que son introducidos secuencialmente. Por estas razones, se recomienda seguir el tutorial en orden.\n⚠️ Si aún no te manejas bien programando en R, te recomiendo revisar primero este tutorial sobre trabajar con datos usando {dplyr}, o bien, este tutorial y su segunda parte para aprender R desde cero. Introducción La librería {ggplot2} crea gráficos por medio de un sistema de capas. Mediante la suma de capas vamos a poder crear expresiones gráficas de nuestros datos, agregando tantas capas como sean necesarias para comunicar los resultados, así como también para afinar la visualización final.\nPara entender cómo funciona {ggplot2}, veamos primero los principales tipos de capas que vamos a poder ir agregando a nuestro gráfico. Cada capa va a tener un rol específico, y es importante saber cómo se llaman para poder aplicarlas cuando queramos modificar uno u otro aspecto de nuestra visualización.\ndatos: usualmente la primera capa, que entrega los datos a {ggplot2} para poder usar variables como elementos de la visualización estéticas: (aes()) capa donde se realiza el mapeo1 de variables a objetos geométricos, leyendas, ejes y escalas geometrías: (geom_x()) escalas: (scale_x_y()) definición de las dimensiones que adquirirán las estéticas, tales como rangos de valores, paletas de colores, límites de los ejes, etc. coordenadas: (coord_x()): configuración del plano de coordenadas donde se grafican los datos; por ejemplo, definir los límites del gráfico, la proyección de los datos si es que se trata de un mapa, etc. facetas: (facet_x()) distribución de los datos en matrices de gráficos; son una forma de especificar que queremos dividir la visualización en tantos gráficos como valores tenga una variable temas: (theme()) configura detalles visuales de todos los elementos del gráfico, tales como fondo, grilla, ejes, tipografía, tamaños, espaciados, bordes y más. Una vez que entendamos estas piezas básicas de la creación de gráficos con {ggplot2}, pasemos a generar nuestras primeras visualizaciones.\n# install.packages(\u0026#34;ggplot2\u0026#34;) library(ggplot2) Vamos a crear un gráfico vacío a partir de una primera capa de datos. Para iniciar la creación de un gráfico, usamos la función ggplot(), a la cual le pasamos el conjunto de datos que queremos usar.\niris |\u0026gt; ggplot() ggplot(data = iris) # equivalente a lo anterior Al llamar la función ggplot() se genera un gráfico completamente vacío, ya que aún no hemos definido las estéticas ni las geometrías de nuestro gráfico.\nA nuestra primera capa de datos le agregaremos una capa de estética, en la cual mapearemos variables a aspectos de la visualización. Los aspectos más básicos de una visualización de datos, y usualmente los que son obligatorios de definir, son las variables mapeadas a los ejes horizontales (x) y verticales (y) del gráfico.\niris |\u0026gt; ggplot() + # iniciar el gráfico # definir el mapeo de variables a características estéticas del gráfico aes(x = Sepal.Length, # eje x (horizontal) y = Sepal.Width) # eje y (vertical) Con el código anterior hicimos que el eje horizontal se corresponda con la variable Sepal.Length y el eje vertical con Sepal.Width. Podemos ver que ambos ejes adoptaron los nombres, rangos y valores de estas variables.\nSin embargo, aún no hay ninguna expresión gráfica directa de los datos en nuestro gráfico, debido a que aún no especificamos una capa de geometría.\nDispersión Como elegimos columnas numéricas de los datos iris para el gráfico, lo que representaremos son la ubicación de las observaciones con respecto a ambas variables. Cuando tenemos dos variables numéricas en los ejes, podemos crear un gráfico de dispersión.\nAgreguemos una capa geom_point() a nuestro gráfico para que los datos se expresen visualmente como puntos:\niris |\u0026gt; ggplot() + # iniciar el gráfico # definir el mapeo de variables a características estéticas del gráfico aes(x = Sepal.Length, # eje x (horizontal) y = Sepal.Width) + # eje y (vertical) # agregar una capa de geometría geom_point() # geometría de puntos Con geom_point(), cada observación adquiere una coordenada en el gráfico en base a los valores que tengan en las variables de los ejes vertical y horizontal.\nEste tipo de gráficos se llaman gráficos de dispersión, y son útiles para explorar datos que poseen varias variables numéricas que pueden estar relacionadas entre sí.\nComplementemos el ejemplo anterior, esta vez mapeando una variable más a la estética del color:\niris |\u0026gt; ggplot() + aes(x = Sepal.Length, y = Sepal.Width, color = Species) + # variable mapeada al color geom_point() Los puntos del gráfico adquieren colores en base a una tercera variable, Species, permitiéndonos diferenciar grupos de puntos, y en consiguiente extraer más información desde la visualización.\nAhora, en lugar del color, mapeemos una variable al tamaño de los puntos:\niris |\u0026gt; ggplot() + aes(x = Sepal.Length, y = Sepal.Width, size = Petal.Length) + # variable mapeada al tamaño geom_point(color = \u0026#34;purple2\u0026#34;, alpha = 0.5) Ahora los valores de la variable Petal.Length que mapeamos a la estética size harán que los puntos del gráfico cambien de tamaño en relación a los valores de Petal.Length.\nDentro de la geometría podemos definir manualmente el color, la transparencia, Y varios otros atributos visuales de todas las geometrías que usemos. En este caso, definimos un color para todos los puntos, y una transparencia que nos permitirá notar cuando existen puntos sobrepuestos (observaciones idénticas).\nNaturalmente, podemos usar variables para controlar tanto el color como el tamaño, y ajustar los argumentos de las geometrías para que se vean como queramos. Como toque final, cambiamos la paleta de colores por una de ColorBrewer:\niris |\u0026gt; ggplot() + aes(x = Sepal.Length, y = Sepal.Width, color = Species, size = Petal.Length) + geom_point(alpha = 0.6, shape = \u0026#34;diamond\u0026#34;) + # paleta de colores scale_color_brewer(palette = \u0026#34;Set2\u0026#34;) + # tema del gráfico theme_minimal() Histograma Los gráficos de histograma se usan comúnmente para explorar la distribución de los datos de forma rápida.\niris |\u0026gt; # datos ggplot() + # iniciar aes(x = Sepal.Length) + # variable horizontal geom_histogram() # histograma Ese tipo de gráficos solamente requiere de una variable. La variable se expresará en el eje horizontal, con la frecuencia de observaciones ascendiendo desde el plano horizontal en correspondencia con la cantidad de casos en cada valor de la variable.\nEn otras palabras, un histograma nos permite ver cuántas veces se repite cada valor de una variable, y de este modo podemos identificar cómo se distribuyen los datos.\nDensidad Al igual que los histogramas, los gráficos de densidad solo requieren de una variable. Los gráficos de densidad expresan la distribución de los datos como una curva, simplificando la visualización.\niris |\u0026gt; ggplot() + aes(x = Sepal.Length) + geom_density() Podemos cambiar los argumentos de geom_density(), o de cualquier otra función geom_x(), para personalizar su apariencia:\niris |\u0026gt; ggplot() + aes(x = Sepal.Length) + geom_density(fill = \u0026#34;black\u0026#34;, alpha = 0.6) Si mapeamos una variable al color del gráfico de densidad, obtendremos múltiples distribuciones de datos para cada grupo en la variable asignada al color.\niris |\u0026gt; ggplot() + aes(x = Sepal.Length, fill = Species, # relleno de la figura color = Species) + # bordes de la figura geom_density(alpha = 0.6) + # tema theme_classic() + scale_color_brewer(palette = \u0026#34;Dark2\u0026#34;) + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + coord_cartesian(expand = FALSE) Cuándo se visualizan figuras cerradas, como una curva, una barra, o un polígono, existen dos argumentos que corresponden con el color: el relleno (fill) de las figuras, y su borde (color). Por eso en este ejemplo asignamos la variable Species tanto al relleno como al borde. Como toque final, definimos un tema con theme_classic(), aplicamos una paleta de colores tanto al relleno como al color, y eliminamos el espaciado extra en los ejes ajustando el argumento expand en la capa de coordenadas, coord_cartesian().\nPara seguir aprendiendo veremos algunos ejemplos con conjuntos de datos reales.\nEl primer conjunto de datos que utilizaremos es una serie de temperaturas medidas en estaciones meteorológicas a lo largo de todo Chile.\nlibrary(dplyr) # para manipular datos library(readr) # para cargar datos Puedes descargar los datos desde este repositorio, o bien ejecutar el siguiente código que descargará los datos directamente y los cargará en tu sesión de R:\ntemp \u0026lt;- read_csv2(\u0026#34;https://github.com/bastianolea/temperaturas_chile/raw/main/datos/procesados/temperaturas_chile_unificadas.csv\u0026#34;) glimpse(temp) Rows: 159,073 Columns: 13 $ codigo_nacional \u0026lt;dbl\u0026gt; 180005, 180005, 180005, 180005, 180005, 180005, 180005… $ altura \u0026lt;dbl\u0026gt; 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50… $ año \u0026lt;dbl\u0026gt; 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, … $ mes \u0026lt;dbl\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, … $ dia \u0026lt;dbl\u0026gt; 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,… $ t_min \u0026lt;dbl\u0026gt; 16.7, 16.2, 20.1, 17.5, 19.8, 18.8, 17.5, 17.3, 19.2, … $ t_max \u0026lt;dbl\u0026gt; 23.9, 23.5, 24.3, 24.4, 23.9, 24.0, 23.6, 23.1, 24.9, … $ nombre \u0026lt;chr\u0026gt; \u0026quot;Chacalluta, Arica Ap.\u0026quot;, \u0026quot;Chacalluta, Arica Ap.\u0026quot;, \u0026quot;Cha… $ latitud \u0026lt;dbl\u0026gt; -18.35555, -18.35555, -18.35555, -18.35555, -18.35555,… $ longitud \u0026lt;dbl\u0026gt; -70.34028, -70.34028, -70.34028, -70.34028, -70.34028,… $ zona_geografica \u0026lt;chr\u0026gt; \u0026quot;Litoral\u0026quot;, \u0026quot;Litoral\u0026quot;, \u0026quot;Litoral\u0026quot;, \u0026quot;Litoral\u0026quot;, \u0026quot;Litoral\u0026quot;,… $ fecha \u0026lt;date\u0026gt; 2013-01-01, 2013-01-02, 2013-01-03, 2013-01-04, 2013-… $ t_med \u0026lt;dbl\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA… El conjunto de datos posee variables de identificación de las estaciones meteorológicas como su nombre y ubicación, tres columnas que identifican la fecha de la observación, y dos columnas con las temperaturas mínimas y máximas.\nExploremos las observaciones de una estación a través del tiempo con un gráfico de histograma, usando la geometría geom_histogram():\ntemp |\u0026gt; filter(nombre == \u0026#34;Chacalluta, Arica Ap.\u0026#34;) |\u0026gt; ggplot() + aes(fecha) + geom_histogram(color = \u0026#34;white\u0026#34;) + # tema theme_classic() Dentro de la función geom_histogram() especificamos que el borde de las barras sea blanco, y al final del gráfico agregamos una función theme_x() para darle una apariencia distinta a la visualización.\nDispersión Teniendo dos columnas numéricas, podemos crear un gráfico de dispersión que dibuje un puntos que relacionen las mediciones meteorológicas de cada día.\ntemp |\u0026gt; filter(nombre == \u0026#34;Carriel Sur, Concepción Ap.\u0026#34;, año \u0026gt; 1990) |\u0026gt; ggplot() + aes(t_min, t_max, color = mes) + geom_point(alpha = 0.6) + # tema theme_classic() Cada punto representa un día, y su ubicación corresponde a la temperatura máxima y mínima de cada día.\nCajas o boxplot Puedes crear un gráfico de caja o boxplot del total de las observaciones:\ntemp |\u0026gt; ggplot() + aes(y = t_max) + geom_boxplot(fill = \u0026#34;grey90\u0026#34;) + theme_classic() Solamente definiendo una variable para el eje vertical obtenemos un gráfico que nos presenta un resumen estadístico de las observaciones del conjunto de datos, con líneas que marcan la mediana (al medio de la caja) y los cuartiles del 25% y del 75% (los límites de la caja).\nFiltremos los datos para tener información de sólo tres estaciones meteorológicas:\ntemp_filt \u0026lt;- temp |\u0026gt; filter(nombre %in% c(\u0026#34;Chacalluta, Arica Ap.\u0026#34;, \u0026#34;General Freire, Curicó Ad.\u0026#34;, \u0026#34;Carlos Ibañez, Punta Arenas Ap.\u0026#34;)) Ahora podemos asignar la variable nombre al eje horizontal del gráfico para obtener tres cajas:\ntemp_filt |\u0026gt; ggplot() + aes(nombre, t_max) + geom_boxplot(fill = \u0026#34;grey90\u0026#34;) + # tema theme_classic() También podemos volver a mapear la variable del eje horizontal a la estética del color para distinguir aún más las cajas; sin embargo, mapear una variable al color hace que aparezca una leyenda, que en este caso es redundante, por lo que agregamos una capa de leyenda (guides()) para omitirla.\ntemp_filt |\u0026gt; ggplot() + aes(nombre, t_max, color = nombre, fill = nombre) + geom_boxplot(alpha = 0.5) + guides(fill = guide_none(), color = guide_none()) + # tema theme_classic() + # paletas de colores scale_fill_brewer(palette = \u0026#34;Set2\u0026#34;) + scale_color_brewer(palette = \u0026#34;Set2\u0026#34;) Dispersión por grupos Si queremos producir un punto por cada observación a través de las tres estaciones metodologías que filtramos, obtenemos un gráfico como el siguiente:\ntemp_filt |\u0026gt; ggplot() + aes(nombre, t_max) + geom_point(size = 10, alpha = 0.05) + theme_minimal() Podemos ver que resulta un gráfico muy poco legible, dado que estamos dibujando 15 mil puntos unos encima de otros. Para solucionarlo, podemos reemplazar geom_point() por geom_jitter(), que es una geometría que agrega dispersión a los puntos para hacerlos mas visibles:\ntemp_filt |\u0026gt; ggplot() + aes(nombre, t_max) + geom_jitter(size = 2, alpha = 0.05, height = 0) + theme_minimal() Dentro de los argumentos de geom_jitter() podemos especificar la dirección en la que queremos agregar la dispersión aleatoria. Si especificamos height = 0, entonces la dispersión no será vertical, y los puntos solamente se moverán horizontalmente, para que la posición vertical de los puntos sea certera y solamente se desplacen hacia los lados para poder verlos individualmente sin que se tapen unos a otros.\nViolín Los gráficos de violin son básicamente gráficos de densidad, pero espejados o duplicados para producir una silueta similar a la de un violín.\ntemp_filt |\u0026gt; ggplot() + aes(nombre, t_max) + geom_violin(alpha = 0.5, fill = \u0026#34;grey90\u0026#34;) + theme_minimal() Recordemos que siempre podemos agregar la cantidad de capas que deseemos a nuestros gráficos. Por ejemplo, agregar un boxplot sobre los violines:\ntemp_filt |\u0026gt; ggplot() + aes(nombre, t_max) + geom_violin(alpha = 0.4, fill = \u0026#34;purple3\u0026#34;, color = \u0026#34;purple3\u0026#34;) + geom_boxplot(width = 0.2, outliers = FALSE, alpha = 0.4, fill = \u0026#34;purple4\u0026#34;, color = \u0026#34;purple4\u0026#34;,) + theme_minimal() combinar violín con puntos\ntemp_filt |\u0026gt; ggplot() + aes(nombre, t_max) + geom_jitter(size = 1, alpha = 0.05, height = 0) + geom_violin(alpha = 0.4) + theme_minimal() Los gráficos que creemos no tienen por qué reducirse a un solo conjunto de datos. Podemos calcular un nuevo conjunto de datos para complementar las visualizaciones que queremos realizar.\nCalculemos una tabla que contenga los promedios y las medianas para cada uno de los tres grupos de nuestro conjunto de datos:\n# calcular medianas temp_median \u0026lt;- temp_filt |\u0026gt; group_by(nombre) |\u0026gt; summarise(t_max = median(t_max, na.rm = T)) Luego podemos hacer el gráfico, y en las capas que queremos utilizar conjuntos de datos distintos, usamos el argumento data para decirle a {ggplot2} que esas capas se basarán en datos distintos a los de la primera capa de datos (temp_filt):\ntemp_filt |\u0026gt; ggplot() + aes(nombre, t_max) + geom_violin(alpha = 0.4, color = \u0026#34;grey80\u0026#34;, fill = \u0026#34;grey80\u0026#34;) + # cuadrados de mediana geom_point(data = temp_median, size = 5, shape = \u0026#34;diamond\u0026#34;, color = \u0026#34;purple\u0026#34;, alpha = 0.6) + # tema theme_minimal() Como las columnas de el segundo conjunto de datos (temp_median) se llaman igual a las del primero, {ggplot2} detecta las variables mapeadas en la capa de estética y las reutiliza en la segunda capa de geometría, por lo que no es necesario especificar en la capa de geom_point() las variables a utilizar. Pero si las variables de un segundo conjunto de datos se llamaran distinto, podríamos especificar dentro de la geometría una nueva estética con la función aes() para que se aplique a esa capa en particular.\nLíneas Para visualizar los datos como líneas primero procesaremos los datos para obtener promedios mensuales de temperatura:\nlibrary(lubridate) # para trabajar con fechas temp_mensual \u0026lt;- temp |\u0026gt; filter(nombre == \u0026#34;Quinta Normal, Santiago\u0026#34;, año \u0026gt; 1980) |\u0026gt; # redondear fechas al mes mutate(fecha = lubridate::floor_date(fecha, \u0026#34;month\u0026#34;)) |\u0026gt; group_by(fecha) |\u0026gt; summarise(t_min = mean(t_min, na.rm = T), t_max = mean(t_max, na.rm = T)) Ahora podemos usar una variable en formato fecha como eje horizontal. {ggplot2} detectará que la variable es una fecha y mostrará un eje adaptado de acuerdo a la escala de los datos; en este caso, mostrando sólo los años.\ntemp_mensual |\u0026gt; ggplot() + aes(x = fecha, # usar fecha como eje horizontal y = t_max, color = t_max) + geom_line(linewidth = 1) + # gráfico de líneas # especificar colores de la escala scale_color_gradient(low = \u0026#34;blue4\u0026#34;, high = \u0026#34;red2\u0026#34;) + # leyenda abajo guides(color = guide_colorbar(position = \u0026#34;bottom\u0026#34;)) + # temas theme_classic() + theme(legend.key.width = unit(30, \u0026#34;pt\u0026#34;), legend.key.height = unit(4, \u0026#34;pt\u0026#34;)) Mosaico Del mismo modo que realizamos un gráfico de dispersión, podemos realizar un gráfico de puntos ordenados si es que las variables numéricas que usaremos en los ejes de coordenadas son consistentes, por ejemplo, en el caso de observaciones medidas diaria o mensualente.\nEn el siguiente gráfico mapearemos los valores de las temperaturas por año horizontalmente, y con los meses verticalmente:\ntemp |\u0026gt; filter(nombre == \u0026#34;General Freire, Curicó Ad.\u0026#34;) |\u0026gt; group_by(año, mes) |\u0026gt; summarize(t_max = mean(t_max, na.rm = T)) |\u0026gt; filter(año \u0026gt;= 1980) |\u0026gt; ggplot() + aes(año, mes, color = t_max) + geom_point(size = 3, alpha = .7) + scale_y_continuous(breaks = 1:12) + theme_minimal() Obtenemos una visualización que nos permite ver, al mismo tiempo, la evolución a través de los años de las temperaturas, teniendo la vista la variación mensual dentro de cada año. Podemos mejorar esta visualización utilizando la geometría geom_tile(), para generar mosaicos:\ntemp |\u0026gt; filter(nombre == \u0026#34;General Freire, Curicó Ad.\u0026#34;) |\u0026gt; group_by(año, mes) |\u0026gt; summarize(t_max = mean(t_max, na.rm = T)) |\u0026gt; filter(año \u0026gt;= 1980) |\u0026gt; ggplot() + aes(año, mes, fill = t_max) + geom_tile()+ scale_y_continuous(breaks = 1:12) + theme_minimal() En el clase anterior, cada observación es representada por uno de los mosaicos. Esta visualización podría mejorarse si es que cambiamos el sistema de coordenadas del gráfico para que la relación entre la variable vertical y la variable horizontal sea siempre cuadrada. Usamos la función coord_fixed() para fijar el sistema de coordenadas para cada intersección entre el eje x e y tengan un aspecto cuadrado:\ntemp |\u0026gt; filter(nombre == \u0026#34;General Freire, Curicó Ad.\u0026#34;) |\u0026gt; group_by(año, mes) |\u0026gt; summarize(t_max = mean(t_max, na.rm = T)) |\u0026gt; filter(año \u0026gt;= 1980) |\u0026gt; ggplot() + aes(año, mes, fill = t_max) + geom_tile() + coord_fixed(expand = FALSE) + viridis::scale_fill_viridis(option = \u0026#34;magma\u0026#34;, name = NULL) + theme_minimal() + scale_y_continuous(breaks = 1:12) + scale_x_continuous(breaks = c(seq(1980, 2020, 10), 2024)) + labs(title = \u0026#34;Temperatura máxima mensual\u0026#34;, subtitle = \u0026#34;Estación meteorológica General Freire, Curicó\u0026#34;) + theme(legend.key.height = unit(8, \u0026#34;mm\u0026#34;), legend.key.width = unit(2, \u0026#34;mm\u0026#34;)) + theme(panel.grid.major = element_blank()) Al especificar el argumento expand = FALSE en cualquier función coord_x(), hacemos que el gráfico elimine el espaciado interior entre los ejes y las geometrías, logrando así que se acerquen los mosaicos a los números de las escalas.\nA esta visualización también le agregamos una paleta de colores continua más apropiada para mostrar las diferencias entre los valores; en este caso, la paleta magma del conjunto de paletas de colores {viridis}. Además usamos scale_x_continuous() para especificar los quiebres (breaks) de los valores de la escala horizontal, incluyendo así el año 2024, ya agregamos títulos, subtítulos, y especificaciones del tema del gráfico (theme()) para que la leyenda se vea mejor.\nSeguiremos con los ejemplos de visualizaciones usando un conjunto de datos distintos. Se trata de conteo de población indígena u originaria, desagradados por género, pueblo indígena, y ubicación geográfica. Puedes obtener los datos desde este repositorio, o cargar los datos desde la siguiente función, que los descarga y lo agrega tu entorno de R directamente.\npueblos \u0026lt;- readr::read_csv2(\u0026#34;https://github.com/bastianolea/pueblos_indigenas_chile/raw/master/datos/pueblos_indigenas_chile.csv\u0026#34;) glimpse(pueblos) Rows: 7,612 Columns: 10 $ sexo \u0026lt;chr\u0026gt; \u0026quot;Hombres\u0026quot;, \u0026quot;Hombres\u0026quot;, \u0026quot;Hombres\u0026quot;, \u0026quot;Hombres\u0026quot;, \u0026quot;Hombres\u0026quot;,… $ pueblo \u0026lt;chr\u0026gt; \u0026quot;Mapuche\u0026quot;, \u0026quot;Aymara\u0026quot;, \u0026quot;Rapa Nui\u0026quot;, \u0026quot;Lican Antai\u0026quot;, \u0026quot;Quech… $ n \u0026lt;dbl\u0026gt; 4094, 27075, 19, 353, 1156, 152, 915, 14, 5, 2202, 540… $ codigo_comuna \u0026lt;dbl\u0026gt; 15101, 15101, 15101, 15101, 15101, 15101, 15101, 15101… $ nombre_comuna \u0026lt;chr\u0026gt; \u0026quot;Arica\u0026quot;, \u0026quot;Arica\u0026quot;, \u0026quot;Arica\u0026quot;, \u0026quot;Arica\u0026quot;, \u0026quot;Arica\u0026quot;, \u0026quot;Arica\u0026quot;, … $ codigo_region \u0026lt;dbl\u0026gt; 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15… $ nombre_region \u0026lt;chr\u0026gt; \u0026quot;Arica y Parinacota\u0026quot;, \u0026quot;Arica y Parinacota\u0026quot;, \u0026quot;Arica y P… $ año \u0026lt;dbl\u0026gt; 2017, 2017, 2017, 2017, 2017, 2017, 2017, 2017, 2017, … $ poblacion_total \u0026lt;dbl\u0026gt; 232628, 232628, 232628, 232628, 232628, 232628, 232628… $ orden_region \u0026lt;dbl\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, … Barras ordenado Realicemos una suma de los conteos agrupada por pueblo originario:\npueblos_n \u0026lt;- pueblos |\u0026gt; mutate(pueblo = ifelse(pueblo %in% c(\u0026#34;Mapuche\u0026#34;, \u0026#34;Aymara\u0026#34;, \u0026#34;Diaguita\u0026#34;), pueblo, \u0026#34;Otros\u0026#34;)) |\u0026gt; group_by(pueblo) |\u0026gt; summarize(total = sum(n)) pueblos_n # A tibble: 4 × 2 pueblo total \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Aymara 156754 2 Diaguita 88474 3 Mapuche 1745147 4 Otros 195417 Con esta información podemos generar un gráfico de barras:\npueblos_n |\u0026gt; ggplot() + aes(pueblo, total, fill = pueblo) + geom_col() + theme_minimal() + # paleta de colores scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) Si deseamos ordenar las barras de acuerdo a su valor, lo que tenemos que hacer es ordenar la variable del eje desde el que se originan las barras. En este caso, las barras salen desde el eje x de acuerdo al pueblo indígena. Entonces, la variable pueblo tendría que ser ordenada de acuerdo a la variable total. Para esto, necesitamos que la variable sea un factor, que son el tipo de variables categóricas en R que guardan información acerca de el orden de los niveles o categorías de la variable.\nLa función fct_reorder() del paquete {forcats} nos permite ordenar variables tipo caracter a partir de una segunda variable numérica:\ngrafico_pueblos_1 \u0026lt;- pueblos_n |\u0026gt; mutate(pueblo = forcats::fct_reorder(pueblo, total)) |\u0026gt; ggplot() + aes(pueblo, total, fill = pueblo) + geom_col() + theme_minimal() + # paleta de colores scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;)+ # escala vertical scale_y_continuous(labels = scales::label_comma(big.mark = \u0026#34;.\u0026#34;)) grafico_pueblos_1 También agregamos una capa scale_y_continuous() para modificar la escala vertical del gráfico y así mostrar correctamente los números grandes con un separador de miles, para mejorar la legibilidad de las cifras. Para lograrlo, usamos la función label_comma() del conveniente paquete {scales}.\nTextos Para agregar texto a nuestros gráficos debemos introducir la capa de geometría geom_text(). Esta geometría requiere de un mapeo específico, label, que corresponde a la etiqueta de texto que se mostrará. En label debes poner la variable cuyo valor quieres que aparezca como texto:\ngrafico_pueblos_1 + geom_text(aes(label = total)) En primera instancia, se ve terrible. Esto es porque los textos están apareciendo en la coordenada del eje vertical donde terminan las barras; es decir, justo encima del límite de las barras. Para mejorar esto, en la capa de geom_text() para especificar la justificación vertical del texto en vjust, para que el texto no aparezca centrado en la coordenada y, sino que la coordenada y sea la altura de base del texto, y el argumento nudge_y para sumarle un valor específico a la coordenada y del texto, cosa que aparezca levemente distanciado del borde de las barras:\ngrafico_pueblos_1 + geom_text(aes(label = format(total, big.mark = \u0026#34;.\u0026#34;)), vjust = 0, nudge_y = 15000) Cuando ponemos texto en un gráfico, usualmente tenemos que hacer algunos ajustes en consideración de que algunos elementos del gráfico van a ser muy grandes o muy pequeños, y por lo tanto el texto no siempre se va a ver bien. Para solucionar estos problemas podemos usar múltiples capas de texto en lugar de una sola, donde cada capa de texto dibuje los textos para algunos datos, con los ajustes necesarios para dichos casos.\nEn el ejemplo que estamos haciendo, vamos a graficar el texto en dos capas de geom_text(): una capa para las cifras que son mayores al promedio (barras grandes), y otra capa para las que son menores al promedio (barras pequeñas). De esta forma, podemos hacer que los números sobre las Barras grandes aparezcan dentro de la barra y en color blanco, y los números de las barras pequeñas aparezcan encima de las barras y en color negro:\ngrafico_pueblos_1 + # texto para barras chicas geom_text(data = ~filter(.x, total \u0026lt; mean(total)), aes(label = format(total, big.mark = \u0026#34;.\u0026#34;)), vjust = 0, nudge_y = 20000) + # texto para barras grandes geom_text(data = ~filter(.x, total \u0026gt;= mean(total)), aes(label = format(total, big.mark = \u0026#34;.\u0026#34;)), vjust = 1, nudge_y = -20000, color = \u0026#34;white\u0026#34;) Hicimos que cada capa dibuje textos distintos filtrando los datos que llegan a cada una de las capas en el argumento data de cada geometría. En los argumentos data usamos notación lambda para filtrar los datos del gráfico (~filter(.x)) sin tener que especificar el conjunto de datos específico. Pero si esto te parece complicado, también se puede lograr el mismo efecto sin data usando ifelse(total \u0026gt; mean(total), total, \u0026quot;\u0026quot;) en label para que cada capa escriba el texto que cumple con la condición, e imprima nada si es que no la cumple.\nAnotaciones Muchas veces queremos agregar un texto o anotación específico en alguna ubicación de nuestro gráfico. Para estos fines existe una capa ed geometría personalizada llamada annotate(), que a diferencia del resto de la geometría de {ggplot2}, no depende de la especificación estética (aes()), sino que sus parámetros son completamente manuales. Entonces, podemos usar annotate() para crear un evento puntual en nuestra visualización, y sea escribiendo el texto y sus coordenadas a mano, o sacándolas de un objeto. En este caso, filtraremos la tabla de datos que produce el gráfico para enfocarnos en una sola observación, y usaremos las coordenadas de esta tabla resultante para crear una anotación que añada un texto en un lugar específico del gráfico:\n# filtrar datos del gráfico cifra_pueblos \u0026lt;- pueblos_n |\u0026gt; filter(pueblo == \u0026#34;Aymara\u0026#34;) grafico_pueblos_1 + # agregar una geometría manual annotate(\u0026#34;text\u0026#34;, x = cifra_pueblos$pueblo, y = cifra_pueblos$total + 10000, # agregar separación entre la barra y el texto label = \u0026#34;***\u0026#34;, size = 6, fontface = \u0026#34;bold\u0026#34;) Un segundo ejemplo:\npueblos_n |\u0026gt; mutate(pueblo = forcats::fct_reorder(pueblo, total)) |\u0026gt; ggplot() + aes(pueblo, total, fill = pueblo) + geom_col() + theme_minimal() + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + scale_y_continuous(labels = scales::label_comma(big.mark = \u0026#34;.\u0026#34;)) + # geometría manual annotate(\u0026#34;text\u0026#34;, x = \u0026#34;Mapuche\u0026#34;, y = 1000000, label = \u0026#34;Mayoría mapuche\u0026#34;, size = 5, angle = 90, color = \u0026#34;white\u0026#34;) Barras apiladas En un gráfico de barras apiladas, lo que hacemos es separar los datos por colores, y generar una sola barra que esté compuesta por segmentos de colores del tamaño que represente cada grupo. Este tipo de visualización puede ser himnos para mostrar la proporción que ocupa cada sus grupos dentro del total, dado que la información se presenta como un solo todo.\nPara lograr que la información esté en una misma barra, simplemente tenemos que definir alguno de los dos ejes, horizontal o vertical, como un valor fijo, en este caso \u0026quot;grupo\u0026quot;:\ngrafico_pueblos_2 \u0026lt;- pueblos_n |\u0026gt; mutate(pueblo = forcats::fct_reorder(pueblo, total)) |\u0026gt; ggplot() + aes(total, \u0026#34;grupo\u0026#34;, fill = pueblo) + geom_col() + theme_minimal() + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + scale_x_continuous(labels = scales::label_comma(big.mark = \u0026#34;.\u0026#34;)) + coord_cartesian(expand = FALSE) + guides(fill = guide_legend(position = \u0026#34;top\u0026#34;)) grafico_pueblos_2 Podemos realizar algunos ajustes al tema del gráfico para ocultar el truco que hicimos para que el gráfico tuviera una sola barra, y aprovechamos de ponerle texto a los valores de cada segmento. El texto lo agregamos con la función geom_text(), pero explicitando que la posición de las etiquetas tiene que ser apilada, igual que nuestro gráfico, con position = position_stack(). El número 0.5 dentro de position_stack() indica que queremos que las cifras aparezcan en medio de su correspondiente segmento, y no al final del mismo.\ngrafico_pueblos_2 + geom_text(aes(label = format(total, big.mark = \u0026#34;.\u0026#34;)), color = \u0026#34;white\u0026#34;, position = position_stack(0.5), angle = -90) + theme(axis.title.y = element_blank(), axis.text.y = element_blank()) Torta Muchas personas dedicadas a las estadísticas o a la ciencia de datos han planteado (buenas) críticas en contra de los gráficos de torta. Para fines de este tutorial, ignoraremos un poco estas críticas, debido a que, a pesar de ser inapropiados para muchas situaciones, siguen siendo gráficos atractivos y muy solicitados en la práctica. Por lo tanto, aprenderemos de todas formas a realizarlos en {ggplot2}.\nEn {ggplot2}, un gráfico de torta no es un tipo de visualización completamente distinto a las que ya hemos visto, ya que, técnicamente, un gráfico de torta no es más que una visualización radial o circular de lo mismo que expresa un gráfico de barras. En otras palabras, un gráfico de torta es un gráfico de barras apilado, y luego enrollado desde una de sus esquinas.\nEn este sentido, todo gráfico de tortas empieza siendo un gráfico de barras apilado:\npueblos_n |\u0026gt; ggplot() + aes(1, total, fill = pueblo) + geom_col() + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + theme_minimal() A especificar que el eje horizontal del gráfico sea tan sólo el valor 1, mientras que las observaciones están agrupadas por una variable asignada a la estética color, estamos haciendo que todos los datos del gráfico aparezcan uno encima del otro, sin separarse horizontalmente como los ejemplos anteriores.\nUna vez que tenemos este gráfico de barras apilado, podemos agregar una capa coord_polar() para cambiar el sistema de coordenadas del gráfico, con el objetivo de que la escala vertical del gráfico deje de ser una línea vertical y se transforme en un círculo:\npueblos_n |\u0026gt; ggplot() + aes(1, total, fill = pueblo) + geom_col() + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + theme_void() + # coordenadas circulares coord_polar(theta = \u0026#34;y\u0026#34;) # enrollar el gráfico para volverlo circular Si comparas este gráfico de torta con el gráfico anterior, notarás que el gráfico de torta es exactamente el gráfico de barras, pero en enrollado a partir de su esquina inferior derecha.\nPara agregar etiquetas de texto a nuestro gráfico de torta, el proceso es el mismo que para agregar a un gráfico de barras apiladas; es decir, necesitamos agregar una capa geom_text() que especifique que los textos también deben apilarse con position = position_stack():\npueblos_n |\u0026gt; ggplot() + aes(1, total, fill = pueblo) + geom_col() + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + theme_void() + coord_polar(theta = \u0026#34;y\u0026#34;) + # texto geom_text(aes(label = format(total, big.mark = \u0026#34;.\u0026#34;), x = 1.3), color = \u0026#34;white\u0026#34;, position = position_stack(0.5)) Una variación del gráfico de torta es el gráfico de dona, que no es más que un gráfico de torta con un espacio al medio. Para crear el espacio dentro de la torta, y recordando que los gráficos de tortas son gráficos de barras apiladas, necesitamos agregar espacio en el eje horizontal del gráfico de barras:\npueblos_n |\u0026gt; ggplot() + aes(1, total, fill = pueblo) + geom_col() + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + # agregar espacio en el eje scale_x_continuous(expand = expansion(c(2, 0))) Logramos agregar un espacio en el eje horizontal del gráfico modificando la escala correspondiente (scale_x_continuous()), ajustando en ella al argumento expand que controla cuánto espaciado va a haber en cada borde de el eje correspondiente. Por defecto, {ggplot2} agrega un pequeño espaciado en cada eje.\npueblos_n |\u0026gt; ggplot() + aes(1, total, fill = pueblo) + geom_col() + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + # agregar espacio en el eje scale_x_continuous(expand = expansion(c(2, 0))) + # hacer en torta coord_polar(theta = \u0026#34;y\u0026#34;) + theme_void() + # texto geom_text(aes(label = format(total, big.mark = \u0026#34;.\u0026#34;), x = 1), color = \u0026#34;white\u0026#34;, size = 3, position = position_stack(0.5)) Si cambiamos coord_polar() por coord_radial() y ajustamos un poco la escala vertical y otros detalles, podemos hacer que los textos sigan en el ángulo de la torta:\npueblos_n |\u0026gt; ggplot() + aes(1, total, fill = pueblo) + geom_col() + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + # agregar espacio en el eje scale_x_continuous(expand = expansion(c(2, 0))) + scale_y_continuous(expand = expansion(c(0, 0))) + # hacer en torta coord_radial(theta = \u0026#34;y\u0026#34;, rotate.angle = TRUE) + theme_void() + # texto geom_text(aes(label = scales::comma(total, big.mark = \u0026#34;.\u0026#34;), x = 1.7), color = \u0026#34;black\u0026#34;, size = 3, position = position_stack(0.5)) Facetas Se le llama a facetas a la división de un mismo gráfico en múltiples gráficos a partir de una variable determinada. Especificando una variable de agrupación, entonces, podemos generar una visualización que contenga múltiples gráficos del mismo tipo, pero visualizando datos filtrados por la variable seleccionada.\nPrimero calculemos un conjunto de datos agrupados por dos variables:\npueblos_n_sexo \u0026lt;- pueblos |\u0026gt; mutate(pueblo = ifelse(pueblo %in% c(\u0026#34;Mapuche\u0026#34;, \u0026#34;Aymara\u0026#34;, \u0026#34;Diaguita\u0026#34;), pueblo, \u0026#34;Otros\u0026#34;)) |\u0026gt; group_by(pueblo, sexo) |\u0026gt; summarize(total = sum(n)) |\u0026gt; group_by(pueblo) |\u0026gt; mutate(p = total/sum(total)) glimpse(pueblos_n_sexo) Rows: 8 Columns: 4 Groups: pueblo [4] $ pueblo \u0026lt;chr\u0026gt; \u0026quot;Aymara\u0026quot;, \u0026quot;Aymara\u0026quot;, \u0026quot;Diaguita\u0026quot;, \u0026quot;Diaguita\u0026quot;, \u0026quot;Mapuche\u0026quot;, \u0026quot;Mapuche… $ sexo \u0026lt;chr\u0026gt; \u0026quot;Hombres\u0026quot;, \u0026quot;Mujeres\u0026quot;, \u0026quot;Hombres\u0026quot;, \u0026quot;Mujeres\u0026quot;, \u0026quot;Hombres\u0026quot;, \u0026quot;Mujeres… $ total \u0026lt;dbl\u0026gt; 75785, 80969, 43360, 45114, 861241, 883906, 97725, 97692 $ p \u0026lt;dbl\u0026gt; 0.4834645, 0.5165355, 0.4900875, 0.5099125, 0.4935063, 0.506493… Teniendo una variable extra en el conjunto de datos, podemos dividir el gráfico en más de uno especificando una capa facet_wrap() con la variable de división:\npueblos_n_sexo |\u0026gt; ggplot() + aes(1, p, fill = sexo) + geom_col() + # agregar texto geom_text(aes(label = scales::percent(p, accuracy = 1)), color = \u0026#34;white\u0026#34;, position = position_stack(0.5), fontface = \u0026#34;bold\u0026#34;) + coord_polar(theta = \u0026#34;y\u0026#34;) + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + facet_wrap(~pueblo) + theme_void() Al crear una faceta por la variable pueblo, que tiene cuatro niveles, obtenemos cuatro gráficos con el mismo código que necesitamos para generar uno solo. ¡Qué ofertón!\nLa tercera parte de tutorial continuaremos con un último conjunto de datos. Se trata de una base con más de 1 millón de filas que contiene la frecuencia de múltiples tipos de delitos, desagregados por el tipo de delito, ubicación geográfica, y año. Puedes descargar los datos desde este repositorio que recopila las estadísticas oficiales sobre delitos en Chile, o bien puedes ejecutar el siguiente código para descargar los datos y cargarlo en tu entorno de R. Debido a la cantidad de observaciones, el archivo viene en formato parquet, un formato moderno de datos columnares optimizado para eficiencia y velocidad, para el cual necesitas instalar el paquete {arrow}.\n# install.packages(\u0026#34;arrow\u0026#34;) library(arrow) delinc \u0026lt;- arrow::read_parquet(\u0026#34;https://github.com/bastianolea/delincuencia_chile/raw/main/datos_procesados/cead_delincuencia_chile.parquet\u0026#34;) Líneas La gráficos en línea se usan principalmente para mostrar el cambio o evolución de una variable a través de otra, usualmente a través del tiempo. Usaremos geom_line() para visualizar la cantidad total de delitos mensuales desde el año 2018.\ndelinc |\u0026gt; filter(delito == \u0026#34;Robos con violencia o intimidación\u0026#34;) |\u0026gt; group_by(fecha) |\u0026gt; summarize(n = sum(delito_n)) |\u0026gt; ggplot() + aes(fecha, n) + geom_line(alpha = .4) + geom_point(size = 1) + theme_classic() Podemos agregar al mismo tiempo una capa de puntos sobre las líneas para destacar la ubicaciones específica de las observaciones.\nRecordemos que también podemos usar cualquier variable para darle color a cualquier geometría; por ejemplo, crear una variable que indique si la observación aumenta o disminuye con respecto al caso anterior, y pintar los puntos siguiendo estos cambios:\ndelinc |\u0026gt; filter(delito == \u0026#34;Robos con violencia o intimidación\u0026#34;) |\u0026gt; group_by(fecha) |\u0026gt; summarize(n = sum(delito_n)) |\u0026gt; mutate(cambio = ifelse(n \u0026gt; lag(n), \u0026#34;aumento\u0026#34;, \u0026#34;disminución\u0026#34;)) |\u0026gt; filter(!is.na(cambio)) |\u0026gt; ggplot() + aes(fecha, n) + geom_line(alpha = .4) + geom_point(aes(color = cambio), size = 2) + scale_color_manual(values = c(\u0026#34;aumento\u0026#34; = \u0026#34;indianred3\u0026#34;, \u0026#34;disminución\u0026#34; = \u0026#34;darkolivegreen3\u0026#34;)) + guides(color = guide_legend(override.aes = list(size = 5))) + theme_classic() Calculemos una tabla de datos que sume las cantidades de tres delitos en específico a través del tiempo:\ndelinc_filt \u0026lt;- delinc |\u0026gt; filter(delito %in% c(\u0026#34;Robos con violencia o intimidación\u0026#34;, \u0026#34;Robo en lugar habitado\u0026#34;, \u0026#34;Robo por sorpresa\u0026#34;)) |\u0026gt; group_by(delito, fecha) |\u0026gt; summarize(n = sum(delito_n)) Ahora que los datos están agrupados por la variable delito, podemos usarla para dar color a nuestras líneas. Esto tiene como resultado que las líneas se separen para seguir la trayectoria de cada uno de los valores de los niveles de la variable que usamos para el color:\ndelinc_filt |\u0026gt; ggplot() + aes(fecha, n, color = delito) + geom_line(linewidth = 0.8) + theme_classic() + scale_color_brewer(palette = \u0026#34;Accent\u0026#34;) + scale_x_date(expand = expansion(0)) Algunas geometrías en {ggplot2} son capaces de realizar cálculos estadísticos por nosotros a la hora de visualizar. Uno de los más comunes para explorar tendencias en los datos es agregar líneas de regresión lineal sobre los datos. Agregando una capa de la geometría geom_smooth(), se calcularán tres líneas de regresión para cada uno de los grupos que estamos visualizando, debido a que en este caso estamos agrupando nuestra visualización por color.\ndelinc_filt |\u0026gt; ggplot() + aes(fecha, n, color = delito) + geom_line(linewidth = 0.8) + geom_smooth(method = \u0026#34;lm\u0026#34;, se = F, linetype = \u0026#34;dashed\u0026#34;) + theme_classic() + scale_color_brewer(palette = \u0026#34;Accent\u0026#34;) + scale_x_date(expand = expansion(0)) Estas líneas de regresión representan trayectorias ajustadas a la posición de todas las observaciones de cada grupo, entregándonos información visual acerca de si los datos representan en general una tendencia a la alza o a la baja.\nOtra forma de complementar la información en nuestros gráficos es agregar líneas que marquen puntos importantes en una de las escalas. Esto suele ser útil cuando estamos presentando información graficada en torno al tiempo, para marcar por ejemplo alguna fecha clave. En este caso, marcaremos una barra vertical con respecto a la escala del tiempo que indique el inicio del último gobierno, usando geom_vline() (v por vertical). Además, usaremos annotate() para crear una etiqueta que indique a qué corresponde la barra que agregamos:\ndelinc_filt |\u0026gt; ggplot() + aes(fecha, n, color = delito) + geom_line(linewidth = 0.8) + geom_vline(xintercept = as_date(\u0026#34;2022-03-11\u0026#34;), linewidth = 1.3, color = \u0026#34;deeppink3\u0026#34;, alpha = 0.5) + annotate(geom = \u0026#34;label\u0026#34;, label = \u0026#34;Gabriel Boric\u0026#34;, x = as_date(\u0026#34;2022-03-11\u0026#34;) + 40, y = 7000, hjust = 0, fill = \u0026#34;deeppink3\u0026#34;, color = \u0026#34;white\u0026#34;, alpha = 0.6, size = 3.5) + theme_classic() + scale_color_brewer(palette = \u0026#34;Accent\u0026#34;) + scale_x_date(expand = expansion(0)) Barras comparativas Los gráficos de barras nos permiten comparar valores de forma muy clara. Pero a veces también queremos comparar valores dentro de los valores que estábamos comparando. Calcularemos la cantidad de tres delitos distintos a través de 10 comunas, para generar un gráfico que nos permita comparar tanto los delitos entre las comunas como las cantidades de los delitos distintos dentro de cada una de estas comunas.\n# obtener top 10 comunas segun cantidad de delitos comunas_delinc \u0026lt;- delinc |\u0026gt; filter(region == \u0026#34;Metropolitana de Santiago\u0026#34;) |\u0026gt; group_by(comuna) |\u0026gt; summarize(total = sum(delito_n)) |\u0026gt; # sumar delitos por comuna slice_max(total, n = 10) |\u0026gt; # filtrar 10 con mayor cantidad pull(comuna) # extraer comunas # calcular delitos por comunas delinc_comuna \u0026lt;- delinc |\u0026gt; # seleccionar delitos a incluir filter(delito %in% c(\u0026#34;Robos con violencia o intimidación\u0026#34;, \u0026#34;Robo en lugar habitado\u0026#34;, \u0026#34;Robo por sorpresa\u0026#34;)) |\u0026gt; filter(lubridate::year(fecha) == 2023) |\u0026gt; filter(region == \u0026#34;Metropolitana de Santiago\u0026#34;, comuna %in% comunas_delinc) |\u0026gt; group_by(comuna, delito) |\u0026gt; summarize(n = sum(delito_n)) |\u0026gt; ungroup() |\u0026gt; arrange(desc(n)) Creamos un gráfico de barras, que en el eje horizontal tenga los valores y en el vertical tenga las comunas, y usamos las categorías de delitos como el relleno de color:\ndelinc_comuna |\u0026gt; # reordenar las barras por frecuencia mutate(comuna = forcats::fct_reorder(comuna, n)) |\u0026gt; ggplot() + aes(n, comuna, fill = delito) + # especificar color # barras geom_col(width = 0.5) + # texto geom_text(aes(label = ifelse(n \u0026gt; 350, n, \u0026#34;\u0026#34;)), position = position_stack(vjust = 0.5), size = 2.5) + # escalas scale_x_continuous(name = \u0026#34;Delitos\u0026#34;, labels = scales::label_comma(big.mark = \u0026#34;.\u0026#34;), expand = expansion(c(0, 0.1))) + scale_y_discrete(name = \u0026#34;Regiones\u0026#34;, labels = scales::label_wrap(30)) + theme_classic() + # paleta de colores scale_fill_brewer(palette = \u0026#34;Accent\u0026#34;) + # modificar leyenda guides(fill = guide_legend(position = \u0026#34;bottom\u0026#34;, # leyenda abajo nrow = 1)) + # en dos columnas labs(subtitle = \u0026#34;Total de delitos denunciados en el año 2023, por comunas\u0026#34;, caption = \u0026#34;Fuente: Centro de Estudios y Análisis del Delito\u0026#34;) Sin embargo, en este gráfico toma preponderancia la cantidad total de delitos en ciertas comunas, dado que resultan muy mayores que en otras. Tenemos una alternativa para realizar esta visualización, que es poner los segmentos de las barras de cada variable del eje vertical lado al lado en vez de apilarlas. Para ello, podemos definir que los segmentos de color de cada barra se ubiquen en la posición position_dodge() en lugar de position_stack() (la anterior):\ngrafico_delincuencia_1 \u0026lt;- delinc_comuna |\u0026gt; ggplot() + aes(n, comuna, fill = delito) + # relleno geom_col(width = 0.8, position = position_dodge(), # especificar que las categorías aparezcan lado a lado color = \u0026#34;white\u0026#34;, linewidth = 0.5) + geom_text(aes(label = paste(\u0026#34; \u0026#34;, n)), # espaciar texto position = position_dodge(width = 0.8), size = 2.3, hjust = 0) + theme_classic() + scale_x_continuous(name = \u0026#34;Delitos\u0026#34;, labels = scales::label_comma(big.mark = \u0026#34;.\u0026#34;), expand = expansion(c(0, 0.1))) + scale_fill_brewer(palette = \u0026#34;Accent\u0026#34;, name = \u0026#34;Delitos\u0026#34;, labels = scales::label_wrap(20)) + guides(fill = guide_legend(position = \u0026#34;right\u0026#34;, ncol = 1)) + theme(panel.grid.major.x = element_line()) + labs(subtitle = \u0026#34;Total de delitos denunciados en el año 2023, por comunas\u0026#34;, caption = \u0026#34;Fuente: Centro de Estudios y Análisis del Delito\u0026#34;) grafico_delincuencia_1 Con este posicionamiento de las barras obtenemos un gráfico que nos permite comparar cada uno de los subgrupos de cada variable del eje vertical entre sí, dado que ahora los segmentos de colores, en este caso los tipos de delitos, se ubican lateralmente uno con el otro, facilitando la comparación visual.\nTemas Para cambiar el tema de un gráfico, contamos con temas predefinidos en funciones que empiezan con theme_, como theme_minimal() para un tema de fondo blanco con líneas grises, o theme_classic() para uno con bordes negros.\ngrafico \u0026lt;- iris |\u0026gt; ggplot() + aes(x = Sepal.Length, y = Sepal.Width, size = Petal.Length) + geom_point(alpha = 0.5) grafico + theme_minimal() grafico + theme_classic() Si queremos modificar elementos específicos de la visualización, usamos la capa theme(). Dentro de esta función, podemos individualizar cualquier elemento de la visualización. Para saber cómo se llama cada elemento, puedes empezar escribiendo su ubicación general (plot, panel, axis, legend, etc.) y RStudio/Positron debería sugerirte las distintas posibilidades, o bien puedes entrar a esta guía para encontrar los nombres de cada uno de los elementos.\nEn este caso, definiremos la tipografía general del gráfico al incluirla en el llamado a theme_classic(), y dentro de la especificación del tema (theme()) modificaremos la apariencia del subtítulo, de las líneas en el panel del gráfico correspondientes al eje horizontal, eliminaremos el título del eje vertical y las rayitas de los ejes, y especificaremos el tipo de letra del texto del eje vertical.\ngrafico_delincuencia_2 \u0026lt;- grafico_delincuencia_1 + theme_classic(base_family = \u0026#34;Verdana\u0026#34;) + theme(plot.subtitle = element_text(face = \u0026#34;italic\u0026#34;, size = 12), panel.grid.major.x = element_line(), axis.title.y = element_blank(), axis.ticks = element_blank(), axis.text.y = element_text(colour = \u0026#34;black\u0026#34;, face = \u0026#34;bold\u0026#34;, size = 11, lineheight = 0.8)) grafico_delincuencia_2 También podemos cambiar el tema de todos los gráficos que hagamos en la sesión (hasta que reiniciemos R o cambiemos el tema) ejecutando la función theme_set() con el tema (como theme_minimal() o theme() que queramos aplicar dentro.\nPara saber más sobre aplicar temas de colores a tus gráficos, revisa este tutorial Tipografías Existen muchos métodos distintos para usar tipografías personalizadas en nuestros gráficos de {ggplot2}. Debido a que obtener, instalar y activar tipografías suele ser algo complejo, una solución sencilla y compatible es utilizar tipografía web, como las ofrecidas por Google Fonts. puedes navegar a ese sitio y encontrar una tipografía que te interese, y descargarla por medio del paquete {showtext}:\n# install.packages(\u0026#34;showtext\u0026#34;) library(showtext) font_add_google(name = \u0026#34;Montserrat\u0026#34;) Luego, tienes que usar la función showtext_auto() para activar el uso de las tipografías descargadas en tu sesión de R, y posiblemente tengas que ajustar la resolución de las tipografías para que se vean bien en la pantalla.\nshowtext_auto() showtext_opts(dpi = 200) # resolución para que se vean bien las tipografías Habiendo hecho lo anterior, basta con referirse al nombre de la tipografía, ya sea al especificar el tema de nuestro gráfico, o en cualquier geometría que utilice texto como geom_text().\ngrafico_delincuencia_2 + theme_classic(base_family = \u0026#34;Montserrat\u0026#34;) Vale mencionar que el tipo grafías descargadas por este método solamente estarán disponibles durante la sesión de R, por lo que la próxima vez que quieras usarlas deberán ser descargadas de la misma manera. Si tienes algún problema con esas tipografías, basta con reiniciar la sesión de R para dejar de utilizarlas.\nPara más información sobre tipografías personalizadas en tus gráficos, revisa este tutorial Guardar gráficos El último paso para nuestra visualización es, probablemente, guardarla a nuestro equipo para poder insertarla en algún documento u ocuparla. Para ello, la función ggsave() guardará a tu computadora el último gráfico que hayas generado. Solamente debes especificar el nombre del archivo resultante, con su extensión (jpg, png, pdf, etc.).\nggsave(\u0026#34;grafico.jpg\u0026#34;, width = 7, height = 5) La extensión del archivo definirá el formato del archivo resultante. Si no especifica una ruta para archivo, lo más probable es que tu archivo aparezca en tu proyecto de R (si no estás usando proyectos de R (deberías) Lo más probable es que no encuentres la imagen porque aparecerá en la raíz de tu disco).\nSi no específicas un ancho y alto, el gráfico tendrá el ancho y alto de la ventana de previsualización de RStudio. Si deseas ajustar el tamaño de los elementos de tu gráfico, puedes usar el argumento scale, que es un número que mientras más grande hará que los elementos sean más pequeños.\nCon esto concluye este tutorial inicial para aprender a visualizar datos en R con {ggplot2}. Cómo puedes ver, las nociones principales de {ggplot2} son simples, pero es la combinación entre ellas y su uso creativo lo que nos permite crear visualizaciones interesantes! Revisa otras publicaciones sobre visualización de datos para seguir avanzando 🚀\nSi aprendiste con este tutorial, considera hacerme una pequeña donación en el siguiente enlace:\nPara cerrar, dejo algunos enlaces útiles para ayudarte a usar {ggplot2}:\nOtros tutoriales\nA ggplot2 Tutorial for Beautiful Plotting in R Paletas de colores\nTutorial sobre el uso de paletas de colores: https://datavizf24.classes.andrewheiss.com/resource/colors.html Guía sobre herramientas para usar color en R: https://bastianolea.rbind.io/blog/colores/ Guía para el uso de paletas de colores Viridis: https://cran.r-project.org/web/packages/viridis/vignettes/intro-to-viridis.html#the-color-scales Sitio web con paletas de colores ColorBrewer: https://colorbrewer2.org/#type=sequential\u0026scheme=YlGnBu\u0026n=3 Paletas de colores Scico: https://www.data-imaginist.com/posts/2018-05-30-scico-and-the-colour-conundrum/ Temas\nSobre los temas de {ggplot2}: https://ggplot2.tidyverse.org/reference/ggtheme.html Paquete {ggthemes} con temas extra: https://yutannihilation.github.io/allYourFigureAreBelongToUs/ggthemes/ Elementos de los gráficos (para modificar temas):\nhttps://isabella-b.com/blog/ggplot2-theme-elements-reference/ https://henrywang.nl/ggplot2-theme-elements-demonstration/ https://ggplot2.tidyverse.org/reference/theme con mapeo nos referimos al acto de definir una correspondencia entre el comportamiento de una variable y el resultado de algún elemento de nuestro gráfico, el cual pasará a asumir características de la variable que se le mapea.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-03-21T00:00:00Z","excerpt":"`{ggplot2}` es una librería de visualización de datos bastante popular en el mundo de la ciencia de datos. Sus principales características son su atractivo, su conveniencia para la exploración de datos, un gran potencial de personalización, y un extenso ecosistema de extensiones que nos permiten generar visualizaciones prácticamente de cualquier tipo. Sigue este tutorial para aprender desde lo más básico a utilizar `{ggplot2}`","href":"https://bastianoleah.netlify.app/blog/r_introduccion/tutorial_visualizacion_ggplot/","tags":"visualización de datos ; gráficos ; ggplot2","title":"Tutorial: visualización de datos con {ggplot2}"},{"content":"Shiny es un paquete de R que permite crear aplicaciones web interactivas usando sólo código de R. Es muy fácil de aprender, puedes crear cosas interesantes en muy poco tiempo, y tiene la capacidad de crear complejas y atractivas aplicaciones.\nEn este tutorial introductorio aprenderás cómo crear una app Shiny básica, que incluya texto, interacción para el usuario, y un resultado a partir de la interacción.\n¿Por qué usar Shiny? Control del stack completo de la aplicación desde un mismo lenguaje: Sólo necesitas aprender un lenguaje para poder hacer una aplicación completa: desde el procesamiento de los datos a la apariencia visual de tu aplicación, pasando por la interactividad, los gráficos y más. Reutilización del código de R: Como todo está programado con R, puedes usar el códido que usas en otros proyectos o scripts para hacer tus gráficos, cálculos, estadísticos y tablas en tu aplicación Shiny. Reducido tiempo de desarrollo para usuarios que no tienen un trasfondo de informática Flexibilidad a la hora de publicar la aplicación en un servicio o servidor Ecosistema de paquetes de R que se integra perfectamente en el desarrollo de Shiny Elementos de una app Shiny Para entender cómo funciona una aplicación Shiny, debemos saber que una app se crea en un script usualmente llamado app.R, el cual se subdivide en dos partes principales: ui y server.\nUI: interfaz gráfica e interacción Interfaz de la aplicación Disposición de los elementos en la app: botones, texto, títulos Creación de los inputs de la app (botones, sliders, etc.) Establecimiento temas y de estilos CSS Disposición de los outputs de una app: gráficos, tablas, y otros elementos que se renderizan desde R Server: cálculos y salidas Aspectos computacionales de la aplicación Definición de objetos reactivos, que se re-evalúan al cambiar un input u otro reactivo Observadores que realizan cómputos en base a inputs u otros reactivos Renderización de gráficos y tablas, y salida de los mismos como outputs Entendiendo ésto, podemos crear nuestra propia aplicación.\nCreando una aplicación Shiny mínima Crea un nuevo proyecto Primero, como en cualquier otra instancia de desarrollo con R, debes crear un nuevo proyecto, dentro del cual tengas todos los scripts y datos que se vayan a utilizar en tu aplicación.\nEn el menú File, elige New Project y crea un proyecto en una nueva carpeta, con el título de tu aplicación.\nCrear el script En RStudio, crea un nuevo script titulado app.R. Lo primero que vamos a poner en este script vacío, va a ser la carga del paquete {shiny} y del paquete {bslib} (que nos ayuda a construir aplicaciones más atractivas).\nlibrary(shiny) library(bslib) Interfaz básica El segundo paso será crear una interfaz visual para nuestra aplicación que esté vacía.\nEn esta interfaz es donde pondremos los títulos, textos, y botones de nuestra aplicación, y también donde posicionaremos las salidas o outputs de nuestra aplicación, tales como gráficos, tablas, y más.\nPara empezar, la ui de nuestra app se crea con una función que empiece con page_, como page_fluid():\nui \u0026lt;- page_fluid() Esa sería una aplicación vacía, sin nada en ella. Podemos agregar los primeros contenidos usado nuestras primeras funciones para crear textos:\nh1(): un título h2(): un subtítulo p(): un párrafo de texto normal ui \u0026lt;- page_fluid( h1(\u0026#34;Título\u0026#34;), h2(\u0026#34;Introducción\u0026#34;), p(\u0026#34;Bienvenidx a mi primera app\u0026#34;) ) Server provisorio Para poder previsualizar nuestra aplicación, tenemos que tener una ui y un server, así que crearemos un server básico, que no haga nada por ahora.\nserver es una función de R que contiene los argumentos input, output y session. Serán importantes más adelante, pero por ahora, solamente definimos esta función en nuestro script y la dejamos ahí.\nserver \u0026lt;- function(input, output, session) { } Ejecutar aplicación El último paso será unir ambos aspectos de nuestra aplicación, para poder ejecutarla. Esto se hace al final del script:\nshinyApp(ui, server) Al incluir esta línea en nuestro script, RStudio detectará que se trata de una aplicación, y ofrecerá un nuevo botón en la parte superior derecha del panel de scripts: el botón para ejecutar la app (Run App).\nPero antes, revisemos la totalidad de nuestro (breve) script hasta el momento:\nlibrary(shiny) library(bslib) ui \u0026lt;- page_fluid( h1(\u0026#34;Título\u0026#34;), h2(\u0026#34;Introducción\u0026#34;), p(\u0026#34;Bienvenidx a mi primera app\u0026#34;) ) server \u0026lt;- function(input, output, session) { } shinyApp(ui, server) Tenemos un script que carga los paquetes que usamos, crea una interfaz (ui), define un servidor (server), y une ambos al final para poder ejecutarla. Presiona el botón Run App y tu aplicación Shiny debería abrirse en una nueva ventana y mostrarse. ¡Lo logramos!\nCrear una aplicación Shiny básica A continuación aprenderemos a agregar los primeros inputs y outputs a nuestra aplicación.\nInputs Los inputs son todos los elementos visuales e interactivos que podemos poner en una aplicación, y que permiten que un usuario o usuario interactúe con nuestra aplicación, y a su vez, con el código que la compone.\nPueden ser elementos tales como selectores, botones, sliders, y otros. Shiny ofrece una amplia variedad de inputs a nuestra disposición, pero también hay otros paquetes de R que nos entregan más inputs.\nPara agregar un input a tu aplicación, debes definirlo en la sección ui de la app:\nui \u0026lt;- page_fluid( h1(\u0026#34;Título\u0026#34;), h2(\u0026#34;Introducción\u0026#34;), p(\u0026#34;Bienvenidx a mi primera app\u0026#34;), # agregar un input textInput(\u0026#34;nombre\u0026#34;, label = \u0026#34;Escribe tu nombre\u0026#34;) ) Aquí agregamos un textInput(), un input que permite al usuario o usuaria escribir el texto que desee.\nLos inputs tienen siempre como primer argumento su nombre interno. Este nombre interno debe ser único, y es el que se usará más adelante en server para referirnos al contenido de la selección del usuario/a.\nComo segundo argumento, usualmente hay que poner la etiqueta del input, que es el texto que el usuario verá inmediatamente antes del input, que le entrega instrucciones sobre qué debe o puede hacer con él.\nLuego de agregar el input al ui de tu app, puedes volver a ejecutarla para ver cómo va quedando.\nServer Por ahora, el input que creamos no hace nada. Para hacer que haga algo, hay que usarlo en server.\nCrearemos un objeto dentro del server que recibirá el contenido del input. que acabamos de crear. Pero este objeto será un objeto especial, porque es creado con la función reactive(), que crea un objeto reactivo.\nReactividad Los objetos reactivos son la pieza fundamental de Shiny: son objetos de R que tienen una característica especial: cuando cambia uno de los elementos que se usan dentro del objeto, o que se usan para construir el objeto, el objeto reactivo se actualiza.\nEn otras palabras, es como si crearas un objeto de R que se va a actualizar automáticamente si es que uno de los elementos que se usan para calcular el objeto cambian en su valor.\nPor ejemplo:\nnumero_a = 4 numero_b = 8 resultado = numero_a + numero_b En este ejemplo, se usan dos objetos para calcular un tercer objeto, resultado. En una sesión normal de R, si cambiamos el valor de numero_a, tenemos que volver a ejecutar, manualmente, numero_a y resultado = numero_a + numero_b para poder obtener el nuevo valor de resultado.\nPero en Shiny, este tipo de actualizaciones de los valores ocurren automáticamente, siempre y cuando se realicen rentro de la función reactive(). Así, cuando el usuario cambie numero_a o numero_b, el valor de resultado se actualizará automáticamente.\nDe esta forma, los cálculos que hagamos en Shiny se van a actualizar siempre que el usuario o usuario haga cambios en los inputs que afectan al objeto.\nDefinamos nuestro primero objeto reactivo dentro de server:\nserver \u0026lt;- function(input, output, session) { # crear un objeto reactivo texto \u0026lt;- reactive({ # aquí irá el cálculo }) } Ahora, dentro del reactive(), pondremos una operación básica de R que haga uso del input que habíamos creado:\nDentro de un reactive(), podemos hacer cualquier operación o secuencia de operaciones con R que queramos, usando los paquetes de R que necesitemos. Para mantener este tutorial sencillo, usaremos la función paste(), que une dos o mas piezas de texto, separadas por un espacio.\n# crear un objeto reactivo texto \u0026lt;- reactive({ paste(\u0026#34;Hola\u0026#34;, input$nombre) # pegar el contenido del input con otro texto }) De este modo, cuando el input cambie, se recalculará el objeto reactivo texto, y se actualizará su resultado para reflejar el cambio en el input.\nPero aún falta un paso para poder ver el resultado! 😱\nOutputs Para poder ver el resultado, tenemos que crear una salida o output. Los outputs toman el resultado de los cálculos realizados en server, y se los hacen llegar a la parte visual de nuestra aplicación, la ui.\nCreamos un output que grafique o renderice el cálculo en un resultado visible:\noutput$texto \u0026lt;- renderText({ texto() }) Lo que hará este bloque es tomar el objeto reactivo texto() (que al ser reactivo debe llamarse como si fuera una función, con () al final), renderizarlo como texto (con rendertext()), y asignarlo a una salida llamada \u0026quot;texto\u0026quot; (output$texto).\nConfirmemos cómo va quedando nuestra sección server en su totalidad:\nserver \u0026lt;- function(input, output, session) { # crear un objeto reactivo texto \u0026lt;- reactive({ paste(\u0026#34;Hola\u0026#34;, input$nombre) # pegar el contenido del input con otro texto }) # output del objeto reactivo output$texto \u0026lt;- renderText({ texto() }) } Conectar server con ui El último paso es poner en algún lugar de nuestra ui el output que acabamos de crear. Simplemente agregamos textOutput(\u0026quot;texto\u0026quot;) en el lugar de nuestra ui que elijamos:\nui \u0026lt;- page_fluid( h1(\u0026#34;Título\u0026#34;), h2(\u0026#34;Introducción\u0026#34;), p(\u0026#34;Bienvenidx a mi primera app\u0026#34;), textInput(\u0026#34;nombre\u0026#34;, label = \u0026#34;Escribe tu nombre\u0026#34;), hr(), # salida desde server textOutput(\u0026#34;texto\u0026#34;) ) Además, entre el input y el output pusimos una línea separadora con hr(). Si ahora ejecutas la aplicación, podrás ver que el texto de abajo refleja lo que escribas en el input de arriba.\nAhora que vimos lo básico de una app Shiny, podemos complejizar el ejemplo agregando otro input:\nselectInput(\u0026#34;saludo\u0026#34;, label = \u0026#34;Elije un saludo\u0026#34;, choices = c(\u0026#34;Hola\u0026#34;, \u0026#34;Chao\u0026#34;, \u0026#34;Te odio\u0026#34; = \u0026#34;Hasta nunca,\u0026#34;)), Agregamos un selector que permite elegir entre varias opciones. Una de ellas tiene un nombre y un valor (\u0026quot;Te odio\u0026quot; = \u0026quot;Hasta nunca,\u0026quot;), porque el usuario verá el nombre en el selector (Te odio), pero internamente el input entregará el valor (\u0026quot;Hasta nunca,\u0026quot;).\nModificamos el objeto reactivo para que ahora use los dos inputs:\ntexto \u0026lt;- reactive({ # paste(\u0026#34;Hola\u0026#34;, input$nombre) # pegar el contenido del input con otro texto paste(input$saludo, input$nombre) # pegar los dos inputs }) Ejecutamos la app, y vemos que ahora tenemos dos inputs, los cuales se usan en un mismo objeto reactivo, el cual se actualiza cuando cualquiera de los dos inputs cambia.\n¡Listo! 🥳 Tienes una app Shiny básica con inputs, reactividad y outputs. Puedes encontrar todo el código de la aplicación de este tutorial en este enlace.\nEl siguiente paso es compartirla con los demás! Para eso, puedes seguir el tutorial publicar una app Shiny en shinyapps.io, y en unos minutos podrás subir tu aplicación Shiny a internet, gratis.\nSi tienes cualquier consulta, necesitas apoyo con tu aplicación Shiny, o deseas que te ayude a desarrollar un proyecto, no dudes en contactarme.\nRevisa mis clases para ver los cursos anteriores de Shiny que he impartido, los cuales contienen código y ejemplos de aplicaciones.\n","date":"2024-11-08T00:00:00Z","excerpt":"Shiny es un paquete de R que permite crear aplicaciones web interactivas usando sólo código de R. En este tutorial introductorio veremos cómo crear una app Shiny básica y subirla a un servidor gratuito para que puedas compartirla.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/tutorial_shiny_1/","tags":"shiny","title":"Tutorial: introducción al desarrollo de apps con Shiny"},{"content":"¿Desarrollaste una aplicación con Shiny, y quieres compartirla con el mundo? shinyapps.io es, en mi experiencia, el servicio más sencillo de usar para poder hacer públicas tus aplicaciones Shiny.\nEl primer paso es, lógicamente, tener una app Shiny funcionando. Para asegurarte que tu app funcione sin problemas, acá van soluciones a los errores más frecuentes de las apps Shiny:\nAsegúrate de que tus scripts (app.R, o bien, ui.R y server.R) estén todos dentro de una misma carpeta Confirma que los datos que cargue tu app estén en la misma carpeta que tu script, o bien, entro de carpetas que estén al mismo nivel que tus scripts. En otras palabras, si tu app carga datos, las rutas deberían/ser/así.csv o así.rds, y no ~/users/tu/documents/cosas/app/datos.xlsx ni tampoco C:\\Users\\Desktop\\app\\datos\\data.csv. Reinicia tu sesión de R (Session \u0026gt; Restart R) y confirma que tu aplicación funciona correctamente. Así confirmas que tu app carga todos los paquetes necesarios, y que no depende de algo que estaba en tu entorno de R pero que no era reproducible desde su código. Luego, debes crearte una cuenta en shinyapps.io. Las cuentas gratuitas permiten tener hasta 5 aplicaciones, con 25 horas de uso mensual (en total, entre todos tus usuarios). Ésto es más que suficiente para apps de pruebas y con pocos usuarios, considerando que al superar el límite de horas no te interrumpen imnediatamente el acceso a tu aplicación. El plan inicial (13USD) permite 25 aplicaciones y 1000 horas de uso mensual.\nDespués, en tu sesión de R, debes instalar el paquete {rsconnect}, que permite la conexión entre R y el servicio:\ninstall.packages(\u0026#39;rsconnect\u0026#39;) Conectar tu cuenta a RStudio Una vez que tengas tu cuenta y R preparado, debes autorizar tu cuenta de shinyapps.io en RStudio, para que tu cuenta quede guardada en RStudio y así puedas subir cosas. Para ello, ShinyApps te entrega un código secreto asociado a tu cuenta, el cual debes registrar en tu sesión de R.\nAl crear tu cuenta, el sitio te explica ésto, y te entrega un bloque de código similar al que pongo a continuación. Al ejecutar ese código en tu sesión de R, registra tu cuenta de shinyapps.io con tu computadora.\nrsconnect::setAccountInfo(name=‘cuenta’, token=‘\u0026lt;TOKEN\u0026gt;, secret=\u0026#39;\u0026lt;SECRET\u0026gt;\u0026#39;) Si ya tienes tu cuenta, pero necesitas conectarla con RStudio, en el menú superior del sitio puedes entrar a Tokens y crear una nueva llave.\nAhí creas una nueva llave, y puedes realizar el proceso de enlazamiento de tu cuenta con RStudio desde RStudio: en el botón azul publicar en la esquina superior derecha de tu panel de scripts de RStudio, y presiona Manage Accounts, o bien, abre las opciones globales de RStudio (menú Tools \u0026gt; Global Options) y entra al panel Publishing.\nDentro del menú que se abre, elige el botón Connect para agregar una cuenta nueva, y pegas el bloque de código con tu token y secret.\nPublicar una aplicación Una vez realizados los pasos anteriores podrás subir tus aplicaciones. Cuando abras un script de una app Shiny, verás el botón azul de publicar al lado del botón Run App. En este botón eliges Publish Application para empezar el proceso de subir tu aplicación local a shinyapps.io.\nEn la ventana que se abre, debes seleccionar los scripts y archivos que se requieren para que tu aplicación funcione. Puedes des-seleccionar los que no sean cruciales para el funcionamiento de la app. Luego le das un nombre a tu splicación (que será parte del enlace que la gente usará para acceder a tu app, así que elige bien), y presionas Publish.\nSe abrirá un panel nuevo en tu panel de consola, titulado Deploy, donde se indicará el avance de la construcción de tu aplicación. Dependiendo de la cantidad de paquetes que uses, se demorará más o menos en generar el entorno de tu aplicación. Finalmente, el mismo panel te entregará el enlace a tu aplicación, y si seleccionaste la opción en el paso anterior, tu app se abrirá en tu navegador web. ¡Listo! Tu app fue publicada en los servidores de shinyapps.io, y ya está lista para ser compartida.\nSolución de problemas Si tu aplicación remota (la versión que acabas de publicar) no se ejecuta, o arroja errores, puedes entrar a tu perfil en shinyapps.io, elegir la aplicación, y revisar los registros (logs) de tu aplicación para ver lo que dicen los errores. Usualmente puede ocurrir porque faltó cargar un paquete, o una ruta estaba mal especificada.\nEn el panel de logs también puedes ver los mensajes que tu aplicación emite, lo que puede ayudarte a detectar problemas.\n","date":"2024-11-07T00:00:00Z","excerpt":"¿Desarrollaste una aplicación con Shiny, y quieres compartirla con el mundo? shinyapps.io es, en mi experiencia, el servicio más sencillo de usar para poder hacer públicas tus aplicaciones Shiny. Este post contiene instrucciones paso a paso para que publiques y compartas tus aplicaciones.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/tutorial_shinyapps/","tags":"shiny","title":"Tutorial: publicar una app Shiny en shinyapps.io"},{"content":" Se denomina web scraping al conjunto de técnicas que permiten extraer datos e información alojada en páginas web, usualmente en formatos que no son fácilmente convertibles a tablas de datos.\nAl ser R un lenguaje enfocado completamente el análisis de datos, es la plataforma ideal para este tipo de tareas, dado que puedes usar un sólo lenguaje para controlar las herramientas de extracción de datos, programar la lógica para automatizar la extracción, procesar y limpiar los datos, y finalmente analizar y presentar tus resultados.\nA continuación, te presento tres formas distintas de extraer datos desde páginas web con R. Cada una tiene sus ventajas y desventajas, y están acompañadas de un tutorial en el que le explico desde cero a utilizarlas.\n{rvest} {rvest} es uno de los paquetes principales y más usados para extraer datos desde internet. Como forma parte del Tidyverse, su sintaxis es muy intuitiva y coincide con el flujo de trabajo de la mayoría de usuarios/as de R. Además de ser una forma sencilla de acercarse al web scraping, contiene funciones para interpretar el código HTML obtenido, lo cual lo vuelve una herramienta versátil que también sirve a la par de otras herramientas de web scraping.\nSirve para la mayoría de los sitios web, pero presenta dificultades con sitios web dinámicos.\nTutorial de {rvest}\nSitio web oficial\n{RSelenium} Selenium es un software de automatización y testeo de sitios web bastante popular y muy usado. Por esta misma razón, puede ser una de las herramientas de web scraping para la que sea más fácil encontrar recursos, consejos y asistencia online.\nSu fuerte está en la capacidad de controlar distintos navegadores, como Firefox y Chrome, incluso ayudándote con la instalación, u operando desde contendores Docker.\nTutorial de {RSelenium}\nSitio web oficial\n{chromote} Con este paquete se puede controlar una instancia sin interfaz gráfica de Google Chrome. Hoy en día, cuando este navegador ha monopolizado gran parte del internet, puede ser conveniente utilizarlo para la extracción de datos web. También te permite controlar el navegador de forma gráfica, y una de sus fortalezas es que es más difícil de detectar como un web scraper por los sitios web.\nTutorial de {chromote}\nSitio web oficial\nEjemplos de web scraping con R Web scraping de datos electorales desde Servel (Chile), automatizando el control de varios botones y selectores, con {RSelenium} Web scraping de varios sitios del Banco Central usando {rvest}, automatizado de forma recurrente con GitHub Actions Web scraping de perfiles de GitHub con {rvest} para generar tablas de los repositorios Ejemplo de web scraping de noticias con {rvest} Ejemplo de web scraping de un sitio web de Transparencia Activa (Chile) con {RSelenium} Obtención automatizada y masiva de datos de prensa con {rvest} y {chromote} para sitios que bloquean el acceso Extracción de una tabla de Wikipedia con {rvest} Ejemplo de extracción de una tabla del Banco Central con {rvest} ","date":"2025-07-17T00:00:00Z","excerpt":"Se trata del conjunto de técnicas que permiten extraer datos e información alojada en páginas web, usualmente en formatos que no son fácilmente convertibles a tablas de datos. En este post vemos tres formas de extraer datos desde páginas web con R, cada una con ventajas y desventajas, y su propio tutorial para aprender desde cero a usarlas.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/web_scraping/","tags":"web scraping","title":"Web scraping con R"},{"content":" En este tutorial aprenderás a empezar a usar Git con tus proyectos de R, mediante la creación de repositorios. Git te permite llevar un registro de las versiones y un control de cambios en tu código, mientras que GitHub te permite subir tus repositorios a un servicio online, en el cual puedes compartir tu código con otras personas, descargar el código de otros, y colaborar en un mismo proyecto.\nGit y GitHub Lo primero es entender bien el software y servicios que vamos a usar. Git es un software de código abierto ampliamente usado, que instalas en tu computador para llevar un control de las versiones de tu código local. Por otro lado, GitHub es una plataforma online donde las personas pueden subir sus repositorios de Git, permitiendo a otros acceder a su código, y contribuir a los repositorios, entre muchas otras funcionalidades.\nEn este tutorial aprenderemos a realizar las tres tareas más comunes del uso de Git/GitHub en R:\nClonar un repositorio remoto Crear un repositorio local Configurar R para conectarte a GitHub Subir un repositorio a GitHub Clonar un repositorio código de R en GitHub Clonar significa descargar a tu computadora una copia idéntica de un repositorio de código alojado en la web.\nSi alguna vez te encuentras con un proyecto de código abierto interesante, participas de una clase de programación que publica sus contenidos en un repositorio, o encuentras un proyecto de análisis de datos que quieras probar o modificar, lo más probable es que estos proyectos estén alojados como repositorios en GitHub.\nPor ejemplo, en mi perfil de GitHub tengo más de 50 repositorios públicos de análisis de datos con R que puedes clonar a tu computador.\nPara clonar un repositorio de GitHub a tu computador, y así tener todo el código necesario para poder ejecutarlo y explorarlo, sólo necesitas tener RStudio instalado.\nNavega al repositorio El primer paso es navegar al repositorio que te interesa, y encontrar el botón verde que dice Code, en el tendrás que copiar un enlace justo arriba del texto que dice clone using the web URL.\nCrea un proyecto de R a partir del repositorio Con el enlace al repositorio copiado, ve a RStudio y crea un nuevo proyecto. Dentro de las tres opciones que te presenta para crear un nuevo proyecto, elige la tercera, que es para crear un proyecto desde un repositorio de control de versiones:\nLuego elige la opción Git:\nFinalmente, pega el enlace del repositorio en el primer campo de texto, y ajusta el nombre que le darás y la ubicación donde quieres que se guarde.\nAl presionar Crear proyecto, RStudio descargará todo el código y datos del repositorio y los incluirá en el proyecto que creaste. Luego podrás ejecutar el código y navegar en repositorio localmente en tu computador. Recuerda que la mayoría de los repositorios contienen un archivo README.md, en el cual sus autores explican de qué se trata y usualmente incluyen instrucciones, y también un archivo de licencias que especifica qué puedes hacer con el código y que no puedes hacer con él, si es que planeas modificar el código o usarlo para otros fines.\nCrear un repositorio local para tu proyecto de R Si estás trabajando en un proyecto de R, y quieres mantener una suerte de respaldo a medida que vas avanzando en el proyecto, debes usar Git para crear un repositorio local. De esta forma, mantendrás un control de versiones de tu proyecto, y así podrás navegar entre distintas versiones de tu proyecto, por ejemplo, si realizaste un cambio que provocó demasiados problemas y quieres volver a una versión anterior, si borraste algo y quieres recuperarlo, o simplemente si quieres llevar una documentación de tu avance en tu proyecto a través del tiempo.\nInstalar Git El primer paso sería instalar Git en tu computador, aunque varios sistemas operativos ya lo tienen instalado.\nInstalar {usethis} Para muchas de las tareas que involucran usar Git en R, existe un paquete que nos facilita muchísimo el trabajo. {usethis} es un paquete de R que automatiza muchas tareas repetitivas que se hacen al configurar tus proyectos.\nInstala {usethis} en R:\ninstall.packages(\u0026#34;usethis\u0026#34;) Crear repositorio Para crear un repositorio, y para cualquier otra oportunidad en la que trabajes con R, debes estar trabajando en un Proyecto de RStudio. Si no estás trabajando dentro de un Proyecto de RStudio, créalo primero.\nEn tu consola de R, ejecuta usethis::use_git() para crear un repositorio local. La consola te preguntará si deseas commit los archivos actuales. Commit significa hacer una confirmación de que quieres guardar el estado actual de tu código como una versión de tu proyecto. En otras palabras, significa algo así como guardar todo el estado de tu proyecto en el momento actual. Cuando creamos por primera vez un repositorio, hacemos un primer commit de la situación inicial del proyecto.\nHacer commits A medida que hagamos cambios en el repositorio, vamos a ir haciendo nuevos commits que vayan guardando nuestro progreso. Por ejemplo, si cargas un nuevo conjunto de datos, realizas la limpieza de estos datos en un nuevo script, y quedaste conforme con esta limpieza, sería conveniente crear un nuevo commit que guarde el estado de tu proyecto en este nuevo momento en el que avanzaste en tus tareas.\nPara hacer un nuevo commit, puedes volver a ejecutar usethis::use_git(), pero no es la forma más ideal de hacerlo. Lo ideal sería, o bien usar le panel de Git de RStudio, que te permite usar Git de forma interactiva en tu sesión, o bien, usar Git desde la línea de comandos o Terminal de tu computador.\nHacer commit desde el panel de Git de RStudio En la parte superior de RStudio, presiona el icono de Git y elige la opción commit: Se abrirá una nueva ventana donde aparecerán los archivos y datos nuevos en tu proyecto desde la última vez que hiciste commit.\nPara todos los archivos nuevos que desees incluir en este guardado, debes seleccionar la casilla de cada uno para hacer el stage (considerarlos para el commit), y luego tienes que escribir un mensaje que describa cuál es tu nuevo cambio. Este mensaje servirá para que tú puedas identificar en el futuro qué cambios contiene este commit. Luego presiona el botón commit.\nHacer commit desde la terminal En la pestaña de Terminal de RStudio también puedes interactuar con Git, de manera más directa.\nEl comando git add . agrega a todos los nuevos archivos y archivos modificados a la fase de staging, que significa que son archivos considerados para el commit, y luego el comando git commit -m \u0026quot;mensaje\u0026quot; te permite crear el commit. Debes poner entre comillas el mensaje asociado al commit. Si en alguna momento tienes alguna duda, puedes ejecutar git status para conocer el estado de tu repositorio.\nConfigurar GitHub en R Lo primero que tienes que hacer es darle permiso a tu computador para conectarse a tu cuenta de GitHub.\nLógicamente, el primer paso es crearse una cuenta en el servicio de GitHub.\nEn R, ejecuta el comando usethis::create_github_token(), que abrirá una ventana de Github. En esta ventana se podrán crear tokens para tu cuenta.\nUn token es una especie de contraseña única y secreta de un solo uso, que te permite conectar tu cuenta a un computador sin tener que entregar tu clave de tu cuenta de GitHub, volviéndolo mucho más seguro.\nCrea el token que necesitas, y copia del código secreto. Recuerda que este código solamente podrás verlo una vez, así que procura realizar el siguiente paso en seguida.\nEjecuta el comando gitcreds::gitcreds_set() y entrégale el token que copiaste. De esta forma queda enlazada tu cuenta con tu computador.\nConfigurar tu cuenta Solamente queda un último paso de configuración, que es guardar la información básica de tu cuenta en R. Ejecuta el comando usethis::use_git_config(user.name = \u0026quot;usuario\u0026quot;, user.email = \u0026quot;correo\u0026quot;), indicando en sus argumentos tu nombre de usuario y tu correo de tu cuenta GitHub.\nCrear un repositorio remoto en GitHub para tu proyecto de R Si ya tienes un proyecto de RStudio con un repositorio local asociado, y quieres compartir este código en Internet, ya sea para respaldarlo o para que otras personas puedan verlo y utilizarlo, puedes crear un repositorio remoto en GitHub que almacene una copia enlazada de tu código. A medida que vayas actualizando tu código, vas a ir actualizando también el repositorio remoto, por lo que será una especie de espejo de tu código local.\nEn tu proyecto de RStudio, ejecuta el comando usethis::use_github() para que automáticamente se cree un repositorio remoto conectado a tu repositorio local, y se suban los datos y código de tu repositorio local a GitHub. El proceso es automático, y debería abrirse una ventana de tu navegador con tu nuevo repositorio de GitHub.\nPuedes compartir el enlace a este repositorio para que otras personas puedan ver tu código, clonarlo en sus computadoras, y colaborar contigo. Es recomendable crear un archivo readme.md en tu proyecto para que aparezca como una descripción de tu repositorio GitHub, así como ponerle una descripción a tu repositorio de GitHub para que otras personas puedan entender de qué se.\nRecuerda que los repositorios en GitHub son por defecto públicos y visibles para las demás personas. Si necesitas respaldar, almacenar o compartir código y o datos privados, crea un repositorio privado con el comando usethis::use_github(private = TRUE). En un repositorio privado, solamente tus computadoras autorizadas y personas que tú autorices podrán acceder a tu código.\nSi te sirvió este tutorial, por favor considera hacerme una pequeña donación para poder tomarme un cafecito mientras escribo el siguiente tutorial 🥺\nRecursos El libro Happy Git with R detalla todos los pasos necesarios para poder usar Git con R, incluyendo soluciones a problemas comunes.\n","date":"2025-02-07T00:00:00Z","excerpt":"En este tutorial se entregan los pasos para empezar a usar Git con tus proyectos de R. Git te permite llevar un registro de las versiones y un control de cambios en tu código, mientras que GitHub te permite subir tus repositorios a un servicio online, en el cual puedes compartir tu código con otras personas, descargar el código de otros, y colaborar en un mismo proyecto.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/tutorial_github/","tags":"Git ; Consejos","title":"Tutorial: crear un repositorio Git para tu proyecto de R y comparte tu código en GitHub"},{"content":"En esta página voy guardando enlaces a sitios web útiles para otros usuarios de R. Si quieres que incluya tu sitio o iniciativa, puedes escribirme.\nAprender R Sitios con contenido educacional para que aprendas R.\nRecursos para aprender R En mi sitio Aprende R encontrarás todo lo necesario para aprender R de manera autodidacta, incluyendo cuross, tutoriales, libros, y más. Ciencia de Datos para Gente Sociable 📚 libro introductorio de R enfocado en personas de las ciencias sociales Taller de Métodos y Técnicas de Investigación I, materiales de un curso de sociología sobre aplicación práctica de técnicas estadísticas usando R Tutorial de R desde cero, con niveles básico, intermedio, y foco en aprender {dplyr} R para ciencia de datos 📚 traducción al español del libro \u0026ldquo;R for Data Science\u0026rdquo;, recomendado para entrar a R Big Book of R 📚 colección de cientos de libros sobre R, organizada por temas R Resource Database, sitio que compila cientos de recursos para usuarios de R, como tutoriales, información sobre paquetes, y más. R Universe, plataforma para ayudarte a encontrar y publicar contenido sobre R R Packages, lista de paquetes de R populares que facilita el acceso a su documentación RStudio Education, recursos oficiales para aprender R (en inglés) Torpedos o cheat sheets, hojas de referencia rápida para recordar aspectos de R y sus paquetes más populares Large Language Model tools for R, Inteligencia artificial y modelos de lenguaje en R Learning statistics with R: A tutorial for psychology students and other beginners (en inglés) Atelier de código, sitio para el aprendizaje de ciencia de datos con foco en investigación y docencia Visualización de datos en R R Graph Gallery, galería de gráficos y visualizaciones de datos hechas con R R Charts, galería de gráficos y visualizaciones de datos hechas con R Tutorial de visualización de datos con {ggplot2} A ggplot2 Tutorial for Beautiful Plotting in R, tutorial recomendado de {ggplot2} (en inglés) Galería de extensiones de {ggplot2} Data Folks: Galería de profesionales de la visualización de datos, para encontrar inspiración y referentes Desarrollo de apps en R Shiny Assistant, asistente de inteligencia artificial para desarrollar, corregir y revisar aplicaciones Shiny Galería de Shiny Widgets, paquete que ofrece muchas inputs nuevos para Shiny Extensiones de Shiny, colección de paquetes que extienden Shiny Documentación de {bslib}, paquete para construir interfaces en Shiny Galería de apps Shiny en el sector público Otros Reportes Quarto Tutorial para crear documentos Quarto Tutorial para usar Quarto para generar blogs y sitios web Crear, publicar y analizar sitios web personales usando R 📚 libro que detalla todas las instrucciones de crear un sitio web con R y Hugo. Análisis de texto con R Cuentapalabras. Estilometría y análisis de texto con R para filólogos Cursos para aprender R SpatialLab, cursos de R y más (algunos impartidos por mi! 😌) Estación R DataCamp, cursos asíncronos interactivos para aprender R (así aprendí yo) Hazla con Datos 🩺 Escuela de Datos Obtener ayuda sobre R StackOverflow, sitio web dedicado a preguntas y respuestas Comunidad de R en Reddit, donde también se pueden hacer consultas Conectar Conoce a otras personas que usan R o únete a una comunidad de usuarios/as!\nComunidades de R Comunidades de R en Chile 🇨🇱 Grupo de Usuarias y Usuarios de R de Santiago de Chile ⭐️ nuevo grupo para compartir con otras personas que usan R! RLadies Santiago, grupo por la diversidad de género en la comunidad R RLadies Chile RLadies Temuco Comunidades de R en Latinoamérica o hipanohablantes Latin R, Conferencia Latinoamericana de R para investigación y desarrollo Escuela de Datos Grupo de usuarios de R de Madrid 🇪🇸 R en Buenos Aires 🇦🇷 Comunidad R Hispano 🇪🇸 Otras comunidades de R rainbowR 🏳️‍🌈 comunidad de usuarixs de R enfocada en diversidad y disidencias sexo-genéricas y activismo de datos Data Science Learning Community Usuari@s de R Julia Silge, análisis de texto, machine learning Yanina Bellini Saibene 🇦🇷, organizadora, traductora Josiah Parry, geoespacial, Rust Danielle Navarro, data scientist, arte generativo Nicola Rennie, visualización de datos, salud Joachim Schork, estadísticas con R Luis D. Verde Arregoitia 🇲🇽 biólogo, inteligencia artificial Rosana Ferrero 🇪🇸 comparte mucho contenido sobre estadísticas y R Lesly Flores 🇲🇽 comparte contenido de estadísticas en R Kyle Walker, desarrollador del paquete mapGL para mapas interactivos Dominic Royé 🇪🇸 mapas, datos geoespaciales, clima Albert Rapp, tips y tutoriales Deepali Kank, visualización de datos Yan Holtz, visualización de datos Georgios Karamanis, visualización de datos y mapas Mauricio \u0026ldquo;Pacha\u0026rdquo; Vargas 🇨🇱 estadística, econometría Riva Quiroga 🇨🇱 humanidades computacionales, lingüística Bruno Rodrigues, reproducibilidad Kennedy Mwavu Rony Rodríguez-Ramírez 🇳🇮 Alejandro Romero González 🇲🇽 Blogs sobre R R-Bloggers, blog que reúne cientos de posts desde blogs de usuarios y desarrolladores de R R Weekly, curatoría de noticias y posts sobre R Blog del Tidyverse RWorks, blog de curatoría de funcionalidades y paquetes de R Blog de Posit, blog oficial de Posit (antes RStudio) Blog de Posit sobre IA Listas de correo de R RWeekly R for the Rest of Us The R Data Scientist Lista de correo de Escuela de Datos Lista de correos de Estación R Datos Sitios donde puedes encontrar datos que puedes usar para explorar o aprender.\nIniciativas sobre datos en Latinoamérica Argendata 🇦🇷 datos sobre Argentina infoactivismo.org, sitio sobre activismo de datos, tecnología e información en Latinoamérica Vivir sin violencia 🏳️‍🌈 datos abiertos sobre violencia de género y contra el colectivo LGBTIQ+ Abriendo Datos 🇨🇱 fundación chilena que promueve una cultura de datos abiertos Unholster: Chile en 30 años Fuentes de datos Fuentes de datos sobre Chile 🇨🇱 Repositorio de datos sociales abiertos (mantenido y desarrollado por mi) Datos abiertos del Estado de Chile Banco Integrado de Datos (BIDAT) del Ministerio de Desarrollo Social y Familia Banco Central de Chile, repositorio de datos estadísticos sobre economía y trabajo Estadísticas sociales del Instituto Nacional de Estadísticas (INE) Estadísticas y datos abiertos de salud pública en Chile: Departamento de Estadísticas e Información de Salud (DEIS) Datos abiertos de educación en Chile: Centro de estudios Mineduc {guaguas}, paquete de R con base de datos de nombres inscritos en Chile Observa, observatorio del Sistema Nacional de Ciencia, Tecnología, Conocimiento e Innovación Data Observatory, plataforma de datos integrado por el Ministerio de Ciencia, Agencia Nacional de Investigación y Desarrollo, Ministerio de Economía y Universidad Adolfo Ibáñez Otras fuentes de datos Paquete {datos}, traducción al español de conjuntos de datos que vienen por defecto en paquetes de R populares DataSetsVerse, paquete de R que contiene conjuntos de datos de diversos temas dataverse.harvard.edu ourworldindata.org statista.com Otros recursos Imágenes de código Crear capturas de código en imágenes para compartir\nCarbon Code Images Tipografías Tipografías de Google Pares de tipografías Paletas de colores Sitios para encontrar combinaciones de colores para tus gráficos o aplicaciones\nGeoPalettes, paletas de colores específicamente diseñadas para datos geoespaciales Colour de Obumbratta, genera paletas a partir de 1 o más colores, con revisiones de contraste, accesibilidad y más HTML Color Picker Pigment, paletas de colores 2 Color Combinations, pares de colores Palette Visualizer, previsualiza paletas con ejemplos de cómo se verían aplicadas Realtime Colors, simulador de interfaz web con paletas de colores Color Buddy Paletas de colores de Paul Tol, extensa investigación sobre paletas de colores inclusivas ","date":"2024-11-08T00:00:00Z","excerpt":"Colección de sitios web y recursos útiles para usuarios de R. Incluye sitios desde donde obtener datos, blogs de usuarios de R, sitios útiles para visualización de datos, y más.","href":"https://bastianoleah.netlify.app/blog/r_introduccion/recursos_r/","tags":"blog","title":"Recursos, blogs y otros sitios sobre R"},{"content":"** No content below YAML for the blog _index. This file provides front matter for the listing page layout and sidebar content. It is also a branch bundle, and all settings under cascade provide front matter for all pages inside blog/. You may still override any of these by changing them in a page\u0026rsquo;s front matter.**\n","date":null,"excerpt":"\u003cp\u003e** No content below YAML for the blog _index. This file provides front matter for the listing page layout and sidebar content. It is also a branch bundle, and all settings under \u003ccode\u003ecascade\u003c/code\u003e provide front matter for all pages inside blog/. You may still override any of these by changing them in a page\u0026rsquo;s front matter.**\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/","tags":"","title":"Blog: análisis de datos en R"},{"content":" Una de las dificultades más frecuentes en el análisis de datos es poder acceder a datos censales. Los censos suelen ser bases de datos de varios millones de observaciones, lo que suele ser demasiado para la mayoría de los computadores, o bien algo imposible con programas como Excel.\nEn este tutorial veremos cómo cargar los datos del Censo de Población y Vivienda 2024 con R para poder acceder a bases de datos de millones de observaciones sin colapsar nuestros computadores.\nTambién veremos cómo consultar información a nivel comunal desde el censo, y a cruzar las bases de personas y hogares.\nÍndice Descargar datos del Censo Cargar datos del Censo en R Diccionario de variables del Censo Calcular resúmenes de datos censales Obtener población por región Calcular población urbana y rural por región Cruzar datos censales de población con datos por hogares Calcular cantidad de personas de tercera edad según propiedad de su hogar Mapas a partir de datos del Censo Descargar datos del Censo Lo primero que haremos es descargar los datos del Censo, disponibles en su sitio oficial.\nResultados Censo 2024 En esta página se listan todos los archivos de resultados.\nVamos a descargar el archivo Base de microdatos - Viviendas, hogares, personas Censo 2024 (parquet) \u0026ndash; zip, 377 MB.\nEste archivo viene comprimido, y dentro trae los datos en formato Parquet, que es un formato moderno y optimizado para grandes volúmenes de datos.\nEl Censo viene en tres niveles de información:\nViviendas Hogares Personas Partiremos cargando los datos de nivel personas, que es la base más grande de las tres, dado que tiene una fila por cada habitante de Chile.\nCargar datos del Censo en R Para cargar los datos en formato Parquet usaremos el paquete de R {arrow}, que necesitamos instalar con la siguiente línea:\ninstall.packages(\u0026#34;arrow\u0026#34;) Una vez instalado, leemos el Censo como una base de datos con la función open_dataset().\n¿Qué significa cargar los datos como base de datos? Normalmente los datos se cargan en la memoria del computador. Si los datos son muy grandes y no caben a la memoria, el computador colapsa o se niega a cargar los datos. Pero cuando trabajamos mediante bases de datos, los datos no se cargan en la memoria, sino que se representan en una versión optimizada, capaz de realizar operaciones más eficientes y de copiar los datos a la memoria solamente cuando se necesiten. Esto nos permite trabajar con datos que normalmente no cabrían en nuestra memoria. library(arrow) # para cargar datos .parquet # cargar censo como base de datos personas \u0026lt;- open_dataset(\u0026#34;personas_censo2024.parquet\u0026#34;) Si intentamos ver los datos cargados, encontraremos que no tenemos un dataframe normal:\npersonas FileSystemDataset with 1 Parquet file 63 columns id_vivienda: int32 id_hogar: int32 id_persona: int32 region: int32 provincia: int32 comuna: int32 comuna_bajo_umbral: int32 area: int32 tipo_operativo: int32 sexo: int32 edad: int32 edad_quinquenal: int32 parentesco: int32 p23_est_civil: int32 p24_lug_resid5: int32 p24_lug_resid5_esp: int32 p25_lug_nacimiento: int32 p25_lug_nacimiento_rec: int32 p25_lug_nacimiento_esp: int32 p26_llegada_periodo: int32 ... 43 more columns Use `schema()` to see entire schema En lugar de eso, tenemos una base de datos que representa los datos del censo, pero que no los carga en la memoria.\nSin embargo, podemos usar todas las funciones de {dplyr} para trabajar con los datos como normalmente hacemos, solo que al terminar las operaciones podemos obtener una previsualización del resultado, o bien, podemos cargar el resultado a la memoria con la función collect(), tomando precauciones para no cargar casi 19 millones de filas por accidente.\nlibrary(dplyr) personas |\u0026gt; select(comuna, area, sexo, edad) |\u0026gt; # seleccionar columnas head() |\u0026gt; # cargar solamente las primeras filas collect() # traer a memoria # A tibble: 6 × 4 comuna area sexo edad \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; 1 5802 1 2 80 2 5802 1 1 52 3 5802 1 2 45 4 5802 1 2 8 5 4303 2 1 69 6 4303 2 2 65 Para ver la estructura de la base de datos, usamos glimpse() de {dplyr}:\nglimpse(personas) Ver la estructura de los datos FileSystemDataset with 1 Parquet file 18,480,432 rows x 63 columns $ id_vivienda \u0026lt;int32\u0026gt; 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 6, … $ id_hogar \u0026lt;int32\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, … $ id_persona \u0026lt;int32\u0026gt; 1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 1, … $ region \u0026lt;int32\u0026gt; 5, 5, 5, 5, 4, 4, 4, 11, 11, 11, 1, 1, 1, 8, … $ provincia \u0026lt;int32\u0026gt; 58, 58, 58, 58, 43, 43, 43, 112, 112, 112, 11… $ comuna \u0026lt;int32\u0026gt; 5802, 5802, 5802, 5802, 4303, 4303, 4303, 112… $ comuna_bajo_umbral \u0026lt;int32\u0026gt; 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, … $ area \u0026lt;int32\u0026gt; 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, … $ tipo_operativo \u0026lt;int32\u0026gt; 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, … $ sexo \u0026lt;int32\u0026gt; 2, 1, 2, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, … $ edad \u0026lt;int32\u0026gt; 80, 52, 45, 8, 69, 65, 58, -66, -66, -66, 73,… $ edad_quinquenal \u0026lt;int32\u0026gt; 80, 50, 45, 5, 65, 65, 55, 30, 55, 5, 70, 70,… $ parentesco \u0026lt;int32\u0026gt; 1, 11, 5, 12, 9, 7, 1, 1, 4, 5, 1, 2, 5, 1, 5… $ p23_est_civil \u0026lt;int32\u0026gt; 6, 8, 8, NA, 1, 1, 8, 2, 2, NA, 1, 1, 8, 8, 8… $ p24_lug_resid5 \u0026lt;int32\u0026gt; 3, 2, 2, 2, 3, 3, 2, 3, 2, 3, 2, 2, 2, 2, 2, … $ p24_lug_resid5_esp \u0026lt;int32\u0026gt; 13117, 5802, 5802, 5802, 4301, 4301, 4303, 10… $ p25_lug_nacimiento \u0026lt;int32\u0026gt; 2, 2, 2, 1, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, … $ p25_lug_nacimiento_rec \u0026lt;int32\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, … $ p25_lug_nacimiento_esp \u0026lt;int32\u0026gt; 12101, 5101, 13120, 5802, 5109, 4303, 4303, -… $ p26_llegada_periodo \u0026lt;int32\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N… $ p27_nacionalidad \u0026lt;int32\u0026gt; 1, 1, 1, 1, 1, 1, 1, -66, -66, -66, 1, 1, 1, … $ p27_nacionalidad_esp \u0026lt;int32\u0026gt; 152, 152, 152, 152, 152, 152, 152, -66, -66, … $ p27_nacionalidad_rec \u0026lt;int32\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, … $ p28_autoid_pueblo \u0026lt;int32\u0026gt; 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 2, 2, … $ p28_pueblo_pert \u0026lt;int32\u0026gt; NA, NA, NA, NA, NA, NA, NA, 1, NA, 1, NA, NA,… $ p29_afrodescendencia_rec \u0026lt;int32\u0026gt; 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, … $ p29_afrodescendencia \u0026lt;int32\u0026gt; 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, … $ p30_lengua_indigena \u0026lt;int32\u0026gt; 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, … $ p30_lengua_indigena_rec \u0026lt;int32\u0026gt; 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, … $ p31_religion \u0026lt;int32\u0026gt; 12, 12, 12, NA, 1, 1, 12, 2, 1, NA, 1, 1, 1, … $ p31_religion_rec \u0026lt;int32\u0026gt; 2, 2, 2, NA, 1, 1, 2, 1, 1, NA, 1, 1, 1, 1, 1… $ p32a_dificultad_ver \u0026lt;int32\u0026gt; 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, … $ p32b_dificultad_oir \u0026lt;int32\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, … $ p32c_dificultad_mover \u0026lt;int32\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, … $ p32d_dificultad_cogni \u0026lt;int32\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, … $ p32e_dificultad_cuidado \u0026lt;int32\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, … $ p32f_dificultad_comunic \u0026lt;int32\u0026gt; 1, 1, 1, 1, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, … $ discapacidad \u0026lt;int32\u0026gt; 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, … $ p33_edu_asiste \u0026lt;int32\u0026gt; 2, 2, 2, 1, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, … $ asistencia_parv \u0026lt;int32\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N… $ asistencia_basica \u0026lt;int32\u0026gt; NA, NA, NA, 1, NA, NA, NA, NA, NA, -66, NA, N… $ asistencia_media \u0026lt;int32\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N… $ asistencia_superior \u0026lt;int32\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N… $ p37_alfabet \u0026lt;int32\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, … $ escolaridad \u0026lt;int32\u0026gt; 17, 14, 12, 2, 12, 12, 15, 8, 5, 3, 8, 8, 16,… $ cine11 \u0026lt;int32\u0026gt; 9, 6, 6, 3, 6, 6, 6, 5, 3, 3, 5, 5, 6, 5, 5, … $ sit_fuerza_trabajo \u0026lt;int32\u0026gt; 3, 1, 1, NA, 3, 3, 1, 1, 1, NA, 1, 3, 1, 3, 3… $ p40_cise_rec \u0026lt;int32\u0026gt; NA, 1, 2, NA, NA, NA, 1, 2, 1, NA, 1, NA, 2, … $ depend_econ_deficit_hab \u0026lt;int32\u0026gt; 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 2, … $ cod_ciuo \u0026lt;int32\u0026gt; NA, 7, 2, NA, NA, NA, 7, 5, 7, NA, 1, NA, 3, … $ cod_caenes \u0026lt;string\u0026gt; NA, \u0026quot;F\u0026quot;, \u0026quot;P\u0026quot;, NA, NA, NA, \u0026quot;F\u0026quot;, \u0026quot;I\u0026quot;, \u0026quot;F\u0026quot;, NA, … $ p44_lug_trab \u0026lt;int32\u0026gt; NA, 5, 2, NA, NA, NA, 2, 1, 1, NA, 2, NA, 2, … $ p44_lug_trab_esp \u0026lt;int32\u0026gt; NA, 998, 5802, NA, NA, NA, 4303, 11202, 11202… $ p45_medio_transporte \u0026lt;int32\u0026gt; NA, 2, 3, NA, NA, NA, 2, NA, NA, NA, 1, NA, 2… $ p46a_tot_hijs_nac \u0026lt;int32\u0026gt; 3, NA, 1, NA, NA, 3, NA, 2, NA, NA, NA, 3, NA… $ p46b_hijas_nac \u0026lt;int32\u0026gt; 2, NA, 1, NA, NA, 0, NA, 1, NA, NA, NA, 0, NA… $ p46c_hijos_nac \u0026lt;int32\u0026gt; 1, NA, 0, NA, NA, 3, NA, 1, NA, NA, NA, 3, NA… $ p47a_tot_hijs_sobrev \u0026lt;int32\u0026gt; 3, NA, 1, NA, NA, 2, NA, 2, NA, NA, NA, 3, NA… $ p47b_hijas_sobrev \u0026lt;int32\u0026gt; 2, NA, 1, NA, NA, -99, NA, 1, NA, NA, NA, 0, … $ p47c_hijos_sobrev \u0026lt;int32\u0026gt; 1, NA, 0, NA, NA, -99, NA, 1, NA, NA, NA, 3, … $ p48_anio_nac_uh \u0026lt;int32\u0026gt; 1978, NA, 2015, NA, NA, 1984, NA, 2014, NA, N… $ p48_mes_nac_uh \u0026lt;int32\u0026gt; 7, NA, 9, NA, NA, 6, NA, 12, NA, NA, NA, 10, … $ div_genero \u0026lt;int32\u0026gt; 2, 2, 2, NA, -66, -66, -66, -66, -66, NA, 2, … Vemos todos los nombres de las columnas, y el tipo de datos que contienen. Además, vemos que la base de datos tiene más de 18 millones de filas! Esto es algo que en la mayoría de computadores no podríamos cargar en la memoria, pero gracias a que lo cargamos como una base de datos con {arrow}, podemos trabajar con ella sin problemas.\nDiccionario de variables del Censo Como vimos en el output anterior, los datos del Censo vienen con valores codificados en números . Para entender su significado, necesitamos consultar el diccionario de variables del Censo, que también está disponible en la página de resultados del Censo y se llama Diccionario de variables microdatos Censo 2024 \u0026ndash; xlsx, 154 KB, o presiona el siguiente botón:\nDescargar diccionario de variables En esa planilla, la pestaña tabla_personas nos muestra las etiquetas de los valores que vienen en la base de datos. Por ejemplo, vemos que en la variable sexo, el valor 1 es Hombre y 2 es Mujer.\nCalcular resúmenes de datos censales Para hacer un conteo de la cantidad de personas según cualquier variable del Censo, podemos usar count() y luego collect() para que la base de datos haga el cálculo y nos entregue el resultado:\npersonas |\u0026gt; count(sexo) |\u0026gt; collect() # A tibble: 2 × 2 sexo n \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; 1 2 9513399 2 1 8967033 Revisa estos tutoriales sobre resúmenes de datos y estadísticos descriptivos para complementar esta sección del tutorial! Podemos también recodificar el resultado del conteo con case_when(), para convertir los valores codificados en etiquetas legibles:\ntabla_sexo \u0026lt;- personas |\u0026gt; count(sexo) |\u0026gt; mutate(sexo = case_when( sexo == 1 ~ \u0026#34;Hombre\u0026#34;, sexo == 2 ~ \u0026#34;Mujer\u0026#34;)) |\u0026gt; collect() tabla_sexo # A tibble: 2 × 2 sexo n \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 Mujer 9513399 2 Hombre 8967033 Población chilena por sexo, Censo 2024 sexo población Mujer 9.513.399 Hombre 8.967.033 La base de datos de Arrow puede realizar cálculos mucho más eficientes y rápidos, pero solamente puede realizar cálculos generales. Ésto es porque lo que hace {arrow} es traducir las funciones de {dplyr} en el lenguaje de la base de datos, lo que significa que el número de funciones soportadas es limitado, aunque amplio.\n¿Qué pasa si {arrow} no soporta una función? Si {arrow} nos dice que alguna función no es soportada (Expression not supported in Arrow), tenemos que usar estas funciones después de cargar el resultado en la memoria con collect().\nPor ejemplo:\npersonas |\u0026gt; count(sexo) |\u0026gt; mutate(sexo = recode(sexo, \u0026#34;1\u0026#34; = \u0026#34;Hombre\u0026#34;, \u0026#34;2\u0026#34; = \u0026#34;Mujer\u0026#34;)) |\u0026gt; collect() Error in `recode()`: ! Expression not supported in Arrow → Call collect() first to pull data into R. No funciona! Pero si ponemos el collect() antes, haremos que se carguen los resultados a nuestra memoria y podremos seguir normalmente:\npersonas |\u0026gt; count(sexo) |\u0026gt; collect() |\u0026gt; # cargar resultados antes de proseguir mutate(sexo = recode(sexo, \u0026#34;1\u0026#34; = \u0026#34;Hombres\u0026#34;, \u0026#34;2\u0026#34; = \u0026#34;Mujeres\u0026#34;)) # A tibble: 2 × 2 sexo n \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 Mujeres 9513399 2 Hombres 8967033 Ahora sí!\nObtener población por región Para obtener la población por región, nuevamente hacemos un conteo por la variable:\npersonas_region \u0026lt;- personas |\u0026gt; count(region, name = \u0026#34;poblacion\u0026#34;) |\u0026gt; arrange(region) |\u0026gt; collect() personas_region # A tibble: 16 × 2 region poblacion \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; 1 1 369806 2 2 635416 3 3 299180 4 4 832864 5 5 1896053 6 6 987228 7 7 1123008 8 8 1613059 9 9 1010423 10 10 890284 11 11 100745 12 12 166537 13 13 7400741 14 14 398230 15 15 244569 16 16 512289 Otra alternativa sería hacer un resumen de datos con summarize(), donde agrupamos por la variable region y luego contamos las filas con n():\npersonas_region \u0026lt;- personas |\u0026gt; group_by(region) |\u0026gt; summarize(poblacion = n()) |\u0026gt; arrange(region) |\u0026gt; collect() El resultado sería el mismo. Pero en ambos casos, recibimos las regiones en números! Para recodificar las regiones y ponerles sus nombres como corresponde, podemos usar nuevamente el Diccionario de variables para obtener las regiones y sus nombres:\nlibrary(readxl) # para cargar datos en Excel library(janitor) # para limpiar datos # cargar códigos territoriales codigos_territoriales \u0026lt;- read_xlsx(\u0026#34;diccionario_variables_censo2024.xlsx\u0026#34;, sheet = \u0026#34;codigos_territoriales\u0026#34;) |\u0026gt; clean_names() |\u0026gt; rename(division = 2) # limpiar regiones regiones \u0026lt;- codigos_territoriales |\u0026gt; filter(division == \u0026#34;Región\u0026#34;) |\u0026gt; select(region = codigo_territorial, nombre_region = territorio) |\u0026gt; mutate(region = as.integer(region)) Luego de cargar la planilla Excel y limpiarla, obtenemos una tabla con las regiones y sus códigos territoriales:\nregiones # A tibble: 16 × 2 region nombre_region \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; 1 1 Tarapacá 2 2 Antofagasta 3 3 Atacama 4 4 Coquimbo 5 5 Valparaíso 6 6 Libertador General Bernardo O'Higgins 7 7 Maule 8 8 Biobío 9 9 La Araucanía 10 10 Los Lagos 11 11 Aysén del General Carlos Ibáñez del Campo 12 12 Magallanes y de la Antártica Chilena 13 13 Metropolitana de Santiago 14 14 Los Ríos 15 15 Arica y Parinacota 16 16 Ñuble Ahora, para obtener la población por región con sus nombres, usamos left_join() para cruzar los datos del conteo de población con la tabla de regiones:\ntabla_region \u0026lt;- personas_region |\u0026gt; left_join(regiones, join_by(region)) |\u0026gt; select(nombre_region, poblacion) |\u0026gt; collect() tabla_region Población chilena por región, Censo 2024 región población Tarapacá 369.806 Antofagasta 635.416 Atacama 299.180 Coquimbo 832.864 Valparaíso 1.896.053 Libertador General Bernardo O\u0026rsquo;Higgins 987.228 Maule 1.123.008 Biobío 1.613.059 La Araucanía 1.010.423 Los Lagos 890.284 Aysén del General Carlos Ibáñez del Campo 100.745 Magallanes y de la Antártica Chilena 166.537 Metropolitana de Santiago 7.400.741 Los Ríos 398.230 Arica y Parinacota 244.569 Ñuble 512.289 Obtuvimos una tabla de las regiones de Chile con su población censal!\nSi quieres aprender a usar left_join() para cruzar datos, revisa este tutorial! Calcular población urbana y rural por región Ahora vamos por un poco más de detalle. Calculemos la población del país por regiones, pero desagregando las regiones por sus áreas urbanas o rurales, para obtener una población con mayor detalle territorial.\nPara esto, hacemos un conteo por las variables region y area, seguimos los pasos que ya vimos antes para agregar los nombres de regiones, y finalmente recodificamos la variable area:\npersonas_region_area \u0026lt;- personas |\u0026gt; # contar población por región y área count(region, area, name = \u0026#34;poblacion\u0026#34;) |\u0026gt; # agregar nombres de comunas y regiones left_join(regiones, join_by(region)) |\u0026gt; arrange(region, area) |\u0026gt; select(nombre_region, area, poblacion) |\u0026gt; # recodificar área mutate(area = case_when( area == 1 ~ \u0026#34;Urbana\u0026#34;, area == 2 ~ \u0026#34;Rural\u0026#34;)) |\u0026gt; # copiar a memoria collect() personas_region_area Obtenemos los datos con dos filas por cada región, con su población urbana y rural en distintas filas.\nPara hacer estos datos más legibles podemos transformar la estructura de los datos para que los datos del área que están hacia abajo (formato largo) pasen a estar distribuidos en dos columnas: urbana y rural (formato ancho).\nlibrary(tidyr) # para transformar datos tabla_region_area \u0026lt;- personas_region_area |\u0026gt; pivot_wider(names_from = area, # nombres de las columnas values_from = poblacion) # valores de las columnas tabla_region_area Población chilena por región y área, Censo 2024 región Rural Urbana Tarapacá 15.559 354.247 Antofagasta 15.399 620.017 Atacama 31.728 267.452 Coquimbo 165.110 667.754 Valparaíso 199.962 1.696.091 Libertador General Bernardo O\u0026rsquo;Higgins 265.174 722.054 Maule 321.382 801.626 Biobío 215.358 1.397.701 La Araucanía 331.479 678.944 Los Lagos 258.193 632.091 Aysén del General Carlos Ibáñez del Campo 23.180 77.565 Magallanes y de la Antártica Chilena 11.779 154.758 Metropolitana de Santiago 280.498 7.120.243 Los Ríos 129.620 268.610 Arica y Parinacota 23.397 221.172 Ñuble 174.140 338.149 Ahora tenemos las regiones con su población en dos columnas según el área donde las personas habitan.\nSi quieres aprender a usar pivot_wider() y pivot_longer() para transformar datos o pivotar tablas, revisa este tutorial! Podemos además calcular los porcentajes de estas poblaciones por región, y pivotar la tabla:\ntabla_region_area_porcentaje \u0026lt;- personas_region_area |\u0026gt; # calcular porcentajes group_by(nombre_region) |\u0026gt; mutate(porcentaje = poblacion / sum(poblacion), porcentaje = round(porcentaje * 100, 1)) |\u0026gt; rename(población = poblacion) |\u0026gt; ungroup() |\u0026gt; # pivotar pivot_wider(names_from = area, values_from = c(población, porcentaje), names_glue = \u0026#34;{area} ({.value})\u0026#34;, # nombrar columnas values_fill = 0) Población chilena por región y área, Censo 2024 región Rural (población) Urbana (población) Rural (porcentaje) Urbana (porcentaje) Tarapacá 15.559 354.247 4,2% 95,8% Antofagasta 15.399 620.017 2,4% 97,6% Atacama 31.728 267.452 10,6% 89,4% Coquimbo 165.110 667.754 19,8% 80,2% Valparaíso 199.962 1.696.091 10,5% 89,5% Libertador General Bernardo O\u0026rsquo;Higgins 265.174 722.054 26,9% 73,1% Maule 321.382 801.626 28,6% 71,4% Biobío 215.358 1.397.701 13,4% 86,6% La Araucanía 331.479 678.944 32,8% 67,2% Los Lagos 258.193 632.091 29,0% 71,0% Aysén del General Carlos Ibáñez del Campo 23.180 77.565 23,0% 77,0% Magallanes y de la Antártica Chilena 11.779 154.758 7,1% 92,9% Metropolitana de Santiago 280.498 7.120.243 3,8% 96,2% Los Ríos 129.620 268.610 32,5% 67,5% Arica y Parinacota 23.397 221.172 9,6% 90,4% Ñuble 174.140 338.149 34,0% 66,0% Cruzar datos censales de población con datos por hogares En el Censo, la base de personas tiene una fila por cada habitante, mientras que la base de hogares tiene una fila por cada hogar, y la de vivienda tiene una fila por cada vivienda. En una vivenda (la construcción física) pueden haber múltiples hogares (personas que conviven y comparten un mismo presupuesto para alimentación).\nEstas bases tienen variables distintas debido a su distinta unidad de observación : las viviendas tienen edad o sexo, etc., los hogares comparten variables como su fuente de energía para calefaccionar y su tenencia (si la vivienda es propia, etc.), y las viviendas tienen características como su materialidad o número de habitaciones.\nPara cruzar estas bases, necesitamos usar las variables id_vivienda y id_hogar, que son las que permiten identificar a qué hogar y vivienda corresponde cada persona censada.\nSi necesitas aprender a cruzar bases de datos con R, revisa este tutorial sobre left_join(). Calcular cantidad de personas de tercera edad según propiedad de su hogar Obtengamos ahora datos censales que requieren combinar dos niveles de información del Censo: nivel personas y nivel hogares. Queremos saber la cantidad de personas adultas mayores, según el tipo de propiedad de los hogares donde residen.\nEl dato de adultos mayores debe crearse a partir del sexo y la edad, que son variables presentes en la base de microdatos a nivel personas, mientras que el dato de la propiedad o tenencia de los hogares (p12_tenencia_viv) está en al base de nivel hogares.\nEntonces necesitamos cruzar las bases de personas y hogares, para obtener una base de personas con información de su hogar, y así poder contar a las y los adultos mayores según el tipo de propiedad del hogar donde viven.\nCargar datos Primero cargamos ambas bases de datos:\nlibrary(dplyr) library(arrow) # cargar datos de nivel personas personas \u0026lt;- open_dataset(\u0026#34;personas_censo2024.parquet\u0026#34;) # cargar datos de nivel hogares hogares \u0026lt;- open_dataset(\u0026#34;hogares_censo2024.parquet\u0026#34;) Ahora seleccionamos las variables que nos interesan para el análisis:\npersonas_filtrado \u0026lt;- personas |\u0026gt; select(id_vivienda, id_hogar, # identificadores region, comuna, # variables territoriales sexo, edad_quinquenal) # variables sociodemográficas hogares_filtrado \u0026lt;- hogares |\u0026gt; select(id_vivienda, id_hogar, # identificadores # region, comuna, # estas ya vienen en nivel personas p12_tenencia_viv) # variable de interés Cruzar tablas Luego, cruzamos ambas bases a partir de las variables que identifican a cada persona con su hogar y vivienda, que son id_vivienda, y id_hogar:\n# cruzar ambas bases por hogar/vivienda/comuna personas_hogares \u0026lt;- left_join(personas_filtrado, hogares_filtrado, join_by(id_vivienda, id_hogar)) Recordemos que seguimos procesando a nivel de base de datos, por lo que podemos hacer estos cruces entre tablas de millones de filas sin problemas.\nVeamos una previsualización del resultado del cruce:\n# primeras filas de la tabla personas_hogares |\u0026gt; head() |\u0026gt; collect() # A tibble: 6 × 7 id_vivienda id_hogar region comuna sexo edad_quinquenal p12_tenencia_viv \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; 1 1 1 5 5802 2 80 4 2 1 1 5 5802 1 50 4 3 1 1 5 5802 2 45 4 4 1 1 5 5802 2 5 4 5 2 1 4 4303 1 65 9 6 2 1 4 4303 2 65 9 # cantidad de observaciones en la tabla personas_hogares |\u0026gt; tally() |\u0026gt; collect() # A tibble: 1 × 1 n \u0026lt;int\u0026gt; 1 18480432 Recodificar variables Ahora recodificamos las variables para que sean legibles (basándonos en el diccionario), y creamos nuevas variables para identificar a los hogares de adultos mayores y el tipo de propiedad del hogar:\npersonas_hogares_recod \u0026lt;- personas_hogares |\u0026gt; # recodificar variables mutate(sexo = case_when( sexo == 1 ~ \u0026#34;Hombre\u0026#34;, sexo == 2 ~ \u0026#34;Mujer\u0026#34;)) |\u0026gt; # crear variable simplificada mutate(propiedad = case_when( p12_tenencia_viv == 1 ~ \u0026#34;Propia\u0026#34;, p12_tenencia_viv == 2 ~ \u0026#34;Propia\u0026#34;, .default = \u0026#34;No propia\u0026#34;)) |\u0026gt; # crear variable de adulto mayor mutate(adulto_mayor = case_when( sexo == \u0026#34;Hombre\u0026#34; \u0026amp; edad_quinquenal \u0026gt;= 65 ~ \u0026#34;Adulto mayor\u0026#34;, sexo == \u0026#34;Mujer\u0026#34; \u0026amp; edad_quinquenal \u0026gt;= 60 ~ \u0026#34;Adulto mayor\u0026#34;, .default = \u0026#34;No adulto mayor\u0026#34;)) Nótese que para construir la variable adulto_mayor usamos el sexo y la edad_quinquenal para tomar en consideración las diferencias entre sexos en la tercera edad.\nFinalmente agregamos los nombres de regiones y comunas, porque recordemos que vienen como códigos territoriales, igual como hicimos más arriba a partir del diccionario de variables:\ncodigos_territoriales \u0026lt;- read_xlsx(\u0026#34;diccionario_variables_censo2024.xlsx\u0026#34;, sheet = \u0026#34;codigos_territoriales\u0026#34;) |\u0026gt; clean_names() |\u0026gt; rename(division = 2) Hacemos dos dataframes, uno para las regiones y otro para las comunas:\nregiones \u0026lt;- codigos_territoriales |\u0026gt; filter(division == \u0026#34;Región\u0026#34;) |\u0026gt; select(region = codigo_territorial, nombre_region = territorio) |\u0026gt; mutate(region = as.integer(region)) regiones # A tibble: 16 × 2 region nombre_region \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; 1 1 Tarapacá 2 2 Antofagasta 3 3 Atacama 4 4 Coquimbo 5 5 Valparaíso 6 6 Libertador General Bernardo O'Higgins 7 7 Maule 8 8 Biobío 9 9 La Araucanía 10 10 Los Lagos 11 11 Aysén del General Carlos Ibáñez del Campo 12 12 Magallanes y de la Antártica Chilena 13 13 Metropolitana de Santiago 14 14 Los Ríos 15 15 Arica y Parinacota 16 16 Ñuble comunas \u0026lt;- codigos_territoriales |\u0026gt; filter(division == \u0026#34;Comuna\u0026#34;) |\u0026gt; select(comuna = codigo_territorial, nombre_comuna = territorio) |\u0026gt; mutate(comuna = as.integer(comuna)) comunas # A tibble: 346 × 2 comuna nombre_comuna \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; 1 1101 Iquique 2 1107 Alto Hospicio 3 1401 Pozo Almonte 4 1402 Camiña 5 1403 Colchane 6 1404 Huara 7 1405 Pica 8 2101 Antofagasta 9 2102 Mejillones 10 2103 Sierra Gorda # ℹ 336 more rows Y ahora agregamos estas variables con los nombres a la base de datos mediante otro left_join():\npersonas_hogares_comunas \u0026lt;- personas_hogares_recod |\u0026gt; # cruzar tablas left_join(regiones, by = \u0026#34;region\u0026#34;) |\u0026gt; left_join(comunas, by = \u0026#34;comuna\u0026#34;) |\u0026gt; # reordenar variables relocate(nombre_region, .before = region) |\u0026gt; relocate(nombre_comuna, .before = comuna) Así vamos hasta ahora:\npersonas_hogares_comunas |\u0026gt; head() |\u0026gt; collect() # A tibble: 6 × 11 id_vivienda id_hogar nombre_region region nombre_comuna comuna sexo \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; 1 13755 1 Libertador General Ber… 6 Rancagua 6101 Mujer 2 13755 1 Libertador General Ber… 6 Rancagua 6101 Mujer 3 13756 1 Metropolitana de Santi… 13 El Bosque 13105 Homb… 4 13756 1 Metropolitana de Santi… 13 El Bosque 13105 Mujer 5 13756 1 Metropolitana de Santi… 13 El Bosque 13105 Mujer 6 13757 1 Metropolitana de Santi… 13 San Joaquín 13129 Mujer # ℹ 4 more variables: edad_quinquenal \u0026lt;int\u0026gt;, p12_tenencia_viv \u0026lt;int\u0026gt;, # propiedad \u0026lt;chr\u0026gt;, adulto_mayor \u0026lt;chr\u0026gt; Ya tenemos una tabla con datos censales a nivel de personas, pero además con variables asociadas a los hogares donde vive cada persona.\nExplorar datos Hasta ahora, con esta tabla podemos hacer un conteo de nuestra nueva variable de adultos mayores a nivel nacional:\npersonas_hogares_comunas |\u0026gt; count(adulto_mayor) |\u0026gt; collect() # A tibble: 2 × 2 adulto_mayor n \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 No adulto mayor 15316446 2 Adulto mayor 3163986 Podemos obtener el mismo conteo a nivel regional:\npersonas_hogares_comunas |\u0026gt; count(nombre_region, adulto_mayor) |\u0026gt; collect() # A tibble: 32 × 3 nombre_region adulto_mayor n \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 Valparaíso Adulto mayor 379279 2 Valparaíso No adulto mayor 1516774 3 Coquimbo Adulto mayor 145007 4 Coquimbo No adulto mayor 687857 5 Aysén del General Carlos Ibáñez del Campo No adulto mayor 85155 6 Tarapacá Adulto mayor 43959 7 Tarapacá No adulto mayor 325847 8 Biobío Adulto mayor 290340 9 Metropolitana de Santiago No adulto mayor 6198954 10 Biobío No adulto mayor 1322719 # ℹ 22 more rows Calcular datos Ahora introduzcamos las variables de nivel hogar. Calculemos los adultos mayores según la tenencia del hogar donde residen, entendida como la propiedad del hogar versus no propiedad (arriendo, cedida, etc.).\nPara ello hacemos un conteo con count() de las personas adultas mayores y la propiedad de sus viviendas:\nconteo_propiedad \u0026lt;- personas_hogares_comunas |\u0026gt; count(adulto_mayor, propiedad) |\u0026gt; collect() Veamos el resultado calculando el porcentaje y formateando los datos:\ntabla_propiedad \u0026lt;- conteo_propiedad |\u0026gt; arrange(adulto_mayor) |\u0026gt; # calcular porcentaje group_by(adulto_mayor) |\u0026gt; mutate(p = n / sum(n)) |\u0026gt; # dar formato a números mutate(n = label_number()(n), p = label_percent(accuracy = 0.1)(p)) Cantidad y porcentaje de personas adultas mayores según propiedad del hogar adulto mayor propiedad cantidad porcentaje Adulto mayor No propia 682.085 21,6% Adulto mayor Propia 2.481.901 78,4% No adulto mayor No propia 6.244.617 40,8% No adulto mayor Propia 9.071.829 59,2% Transformemos la tabla para hacer más legible y clara la información al distribuir las variables en columnas por medio de pivot_wider():\ntabla_propiedad_ancha \u0026lt;- conteo_propiedad |\u0026gt; arrange(adulto_mayor) |\u0026gt; # calcular porcentaje group_by(adulto_mayor) |\u0026gt; mutate(p = n / sum(n)) |\u0026gt; rename(porcentaje = p, cantidad = n) |\u0026gt; # pivotar a ancho pivot_wider(names_from = propiedad, values_from = c(cantidad, porcentaje), names_glue = \u0026#34;{propiedad} ({.value})\u0026#34;, values_fill = 0) |\u0026gt; # dar formato a números mutate(across(contains(\u0026#34;cantidad\u0026#34;), label_number()), across(contains(\u0026#34;porcentaje\u0026#34;), label_percent())) Cantidad y porcentaje de personas adultas mayores según propiedad del hogar adulto mayor No propia (cantidad) Propia (cantidad) No propia (porcentaje) Propia (porcentaje) Adulto mayor 682.085 2.481.901 22% 78% No adulto mayor 6.244.617 9.071.829 41% 59% Obtenemos un resultado interesante: dentro de la población adulta mayor, un 78% reside en hogares de tipo de tenencia propia, mientras dentro de la población que no es adulta mayor, el porcentaje de personas que residen en hogares de tenencia propia es solo un 59%.\nRevisemos la misma información, pero a nivel regional:\nconteo_propiedad_region \u0026lt;- personas_hogares_comunas |\u0026gt; count(adulto_mayor, nombre_region, propiedad) |\u0026gt; collect() tabla_propiedad_region \u0026lt;- conteo_propiedad_region |\u0026gt; # calcular porcentaje group_by(adulto_mayor, nombre_region) |\u0026gt; mutate(p = n / sum(n)) |\u0026gt; filter(propiedad == \u0026#34;Propia\u0026#34;) |\u0026gt; arrange(-p) |\u0026gt; # dar formato a números mutate(p = label_percent()(p)) |\u0026gt; select(-n) |\u0026gt; # pivotar pivot_wider(names_from = adulto_mayor, values_from = p) Porcentaje de la población que vive en viviendas propias (pagadas o pagándose) según grupos de edad región propiedad Adulto mayor No adulto mayor La Araucanía Propia 83% 67% Los Lagos Propia 83% 64% Maule Propia 83% 69% Ñuble Propia 83% 67% Biobío Propia 82% 66% Aysén del General Carlos Ibáñez del Campo Propia 82% 59% Magallanes y de la Antártica Chilena Propia 82% 59% Coquimbo Propia 81% 64% Los Ríos Propia 81% 62% Libertador General Bernardo O\u0026rsquo;Higgins Propia 80% 64% Atacama Propia 80% 63% Metropolitana de Santiago Propia 76% 55% Valparaíso Propia 76% 58% Arica y Parinacota Propia 74% 50% Antofagasta Propia 73% 47% Tarapacá Propia 72% 47% En todas las regiones del país, el porcentaje de personas adultas mayores que residen en hogares de tenencia propia es mayor que el mismo dato en personas no adultas mayores. En otras palabras, se podría plantear que la población adulta mayor es más propensa a vivir en hogares propios pagados o propios pagándose que la población no adulta mayor. Para asegurar ésto habría que aplicar pruebas estadísticas que están fuera del foco de este tutorial.\nMapas a partir de datos del Censo En otro tutorial mostré cómo visualizar los datos del Censo a nivel de manzana con R, y también hice una pequeña aplicación de muestra para visualizar variables del Censo en mapas como una demostración de la simplicidad y potencia de R con un backend de Arrow.\nTutorial en construcción! Pronto lo seguiré expandiendo. ","date":"2026-04-08T00:00:00Z","excerpt":"Una de las dificultades más frecuentes en el análisis de datos es poder acceder a datos censales, porque su tamaño suele ser muy grande para la mayoría de los computadores, o bien imposible de abrir con programas como Excel. En este tutorial veremos cómo cargar los datos del Censo 2024 de Chile con R, accediendo a bases de datos de millones de observaciones para calcular estadísticas poblacionales sin colapsar nuestros computadores.","href":"https://bastianoleah.netlify.app/blog/censo_2024/","tags":"datos ; chile","title":"Explorar datos del Censo de Población y Vivienda 2024 con R"},{"content":" Muchas gracias a todas las personas que participaron! Fue un gusto conocerles! Todo el material de las clases están disponibles más abajo! Sobre el curso Segundo curso gratuito de introducción al análisis de datos con R, enfocado en las ciencias sociales.\nEn este curso de tres sesiones aprenderemos a usar R para analizar datos sociales, revisando lo básico de la visualización de datos, y explorando aspectos básicos de la creación de mapas con R.\nRecomiendo revisar los contenidos del primer curso gratuito de R que organicé, incluyendo grabaciones y diapositivas, para tener una base de conocimientos básicos! R es un lenguaje diseñado para trabajar con datos. Además de exploración, transformación y análisis de datos, R permite hacer visualizaciones, animaciones, automatización de procesos, reportes, aplicaciones web, y mucho más.\nSu gracia es que es un lenguaje usado por personas de diversas disciplinas, y por lo mismo es un lenguaje orientado a ser usado por personas que no sean expertas en informática o ciencias de la computación.\nAprende desde cero a usar este lenguaje y complementa tu carrera con herramientas de programación que te abrirán muchas posibilidades.\nEl curso va dirigido a profesionales o estudiantes de las ciencias sociales con mínima experiencia programando y/o usando R, o alguna experiencia trabajando con datos.\nMi objetivo con estos cursos es apoyar a personas de las ciencias sociales a prender nuevas habilidades, y hacer crecer la comunidad de usuari@s de R 📈 Cupos Los cupos fueron limitados, y se aplicaron criterios de inclusión para la participación de grupos minoritarios (mujeres, disidencias de sexo y género, personas con discapacidad). De las más de 1.500 postulaciones al curso, se abrieron 150 cupos.\nDocente Bastián Olea Herrera posee un magíster en Sociología de la Universidad Católica de Chile, y se ha dedicado por más de 5 años al análisis de datos con R.\nSu experiencia consiste principalmente en visualización de datos y desarrollo de aplicaciones interactivas con R. También se dedica a escribir tutoriales para ayudar a otr@s a aprender R.\nClases A continuación se entrega todo el material del curso, para que puedas seguirlo a tu propio ritmo.\nDiapositivas En las diapositivas del curso se resumen los temas, se muestran ejemplos de código, y se entregan para descargar los datos necesarios. También cada temática va acompañada de un tutorial para profundizar.\nVer diapositivas interactivas Descargar diapositivas en PDF Clase 1: introducción a R Clase realizada el miércoles 25 de marzo.\nDiapositivas clase 1 Código clase 1 Grabación clase 1 Clase 2: introducción a {dplyr} Clase realizada el lunes 30 de marzo.\nDiapositivas clase 2 Código clase 2 Grabación clase 2 Clase 3: resúmenes de datos, cruzar tablas, y {tidyr} Clase realizada el miércoles 1 de abril.\nDiapositivas clase 3 Código clase 3 Grabación clase 3 Clase 4: visualización de datos con {ggplot2} Clase realizada el martes 7 de abril. Se trató de una clase extra por solicitud del propio curso.\nDiapositivas clase 4 Código clase 4 Grabación clase 4 Código En este enlace puedes acceder al repositorio de GitHub que contiene el código que veremos en el curso, los datos que trabajemos, y también el código de R que genera las diapositivas con Quarto Revealjs.\nCódigo del curso También puedes descargar el proyecto de R del curso para abrirlo en tu computador y tener todos los scripts y datos necesarios:\nDescargar curso ","date":"2026-04-07T00:00:00Z","excerpt":"Segundo curso gratuito de introducción a R enfocado en las ciencias sociales. En este curso de tres sesiones aprenderemos a usar R para **analizar datos sociales**, revisando lo básico de la **visualización de datos**, y explorando aspectos básicos de la **creación de mapas** con R.","href":"https://bastianoleah.netlify.app/blog/curso_gratis_r_intro_2/","tags":"blog ; básico","title":"Curso gratuito: introducción al análisis de datos con R, 2ª versión"},{"content":"Al desarrollar aplicaciones web Shiny, tenemos que considerar que van a ser visitadas desde distintos dispositivos: celulares, tablets, computadores grandes, computadores pequeños\u0026hellip; Por eso es importante diseñarlas pensando en la reactividad; es decir, que la aplicación se adapte a distintos tamaños de ventana o pantalla. Si bien Shiny hace gran parte de eso por sí sólo, puede ser útil usar el ancho de la ventana para adaptar los contenidos de la app: mostrar u ocultar elementos, ajustar los gráficos, cambiar un layout, o elegir qué visualización presentar según si el usuario está en un computador de escritorio o en un dispositivo móvil.\nEn este tutorial veremos cómo capturar el ancho de la ventana como un input reactivo de Shiny, para poder usar esta variable en nuestra app para adaptar sus contenidos.\nCapturar el ancho de la ventana Para obtener el ancho actual de la ventana en cualquier momento de la ejecución de nuestra aplicación, usaremos JavaScript. No es necesario aprender este lenguaje, sino solamente saber cómo integrarlo en la app Shiny.\nHay dos formas de incluir este código JavaScript en tu app:\nCargar código de JavaScript externo Con el siguiente código en la interfaz (UI) de tu aplicación harás que el script se cargue al ejecutar la app:\ntags$head( tags$script(src = \u0026#34;ancho.js\u0026#34;) ), De esta forma, el script queda disponible para ejecutarse en tu app.\nLuego, creas un archivo JavaScript, en este caso llamado ancho.js, dentro de la carpeta www/ de tu aplicación, que va a contener el código JavaScript necesario para medir el ancho tanto al abrir la app como al cambiar la ventana:\n$(document).on(\u0026#39;shiny:connected\u0026#39;, function() { Shiny.setInputValue(\u0026#39;window_width\u0026#39;, window.innerWidth); }); $(window).on(\u0026#39;resize\u0026#39;, function() { Shiny.setInputValue(\u0026#39;window_width\u0026#39;, window.innerWidth); }); El primer bloque envía el ancho apenas la app se conecta, y el segundo lo actualiza cada vez que cambia el tamaño de la ventana.\nGuarda ese código en un archivo ancho.js dentro de la carpeta www de tu aplicación, y asegúrate de que tu app lo incluya en su UI.\nSi prefieres mantener todo en el mismo archivo de tu app, puedes escribir el JavaScript directamente en la UI usando tags$script(HTML(...)):\ntags$head( tags$script(HTML(\u0026#34; $(document).on(\u0026#39;shiny:connected\u0026#39;, function() { Shiny.setInputValue(\u0026#39;window_width\u0026#39;, window.innerWidth); }); $(window).on(\u0026#39;resize\u0026#39;, function() { Shiny.setInputValue(\u0026#39;window_width\u0026#39;, window.innerWidth); }); \u0026#34;)) ) Crear una variable reactiva con el ancho Una vez que el JavaScript está en la UI, Shiny recibirá el ancho de la ventana como input$window_width. De este modo podemos acceder al ancho como una variable.\nSi creamos un observador (observe()), podemos hacer que Shiny imprima el valor del input y así veamos la cifra que nos entrega. Como los observadores se actualizan cada vez que cambia el valor de los inputs que incluye, veremos cómo se actualiza el ancho cada vez que cambiamos el tamaño de la ventana:\nobserve({ message(input$window_width) }) Al ejecutar la app, si jugamos con el ancho de la ventana (o del panel Viewer en RStudio) veremos los cambios:\n645 649 660 714 741 761 786 803 810 817 827 832 836 840 843 848 Notamos que los valores cambian demasiado rápido! 😵‍💫 El input se actualiza con demasiada frecuencia, lo que nos puede causar problemas.\nRalentizar las actualizaciones del input Como el evento resize se dispara muy frecuentemente mientras se cambia el tamaño de la ventana, input$window_width se actualizará decenas de veces por segundo, lo que puede generar cálculos innecesarios y hacer la app más lenta.\nPara evitar esto, usamos la función debounce(), que retarda la reactividad hasta que el valor deje de cambiar por un tiempo determinado (en milisegundos):\nPrimero creamos una variable reactiva a partir del input:\nancho \u0026lt;- reactive(input$window_width) Ahora podemos acceder al ancho con el objeto ancho(). Luego, aplicamos debounce() a este objeto, indicando un tiempo de espera de 100 milisegundos:\nancho \u0026lt;- debounce(ancho, 100) Con este código, ancho() sólo se actualizará cuando el ancho de la ventana lleve al menos 100 milisegundos sin cambiar, reduciendo significativamente la cantidad de actualizaciones.\nPuedes volver a probarlo con un observe():\nobserve({ message(ancho()) }) 725 891 762 904 Ahora los mensajes en la consola aparecerán de forma mucho más espaciada mientras cambias el tamaño de la ventana.\nUsar el ancho en un output Siguiendo los pasos anteriores, puedes usar ancho() como cualquier otro objeto reactivo dentro de la sección server de tu app. Por ejemplo, para mostrar el ancho como un texto en tu app, imprimes el texto con renderText() en el server:\ntexto_ancho \u0026lt;- renderText({ paste(\u0026#34;El ancho de la ventana es:\u0026#34;, ancho()) }) Y luego ubicas el output en alguna parte de la interfaz (UI) de tu app:\ntextOutput(\u0026#34;texto_ancho\u0026#34;) Adaptar los contenidos de la app según el ancho Ahora podemos mostrar u ocultar elementos de la aplicación dependiendo del ancho de la ventana. Para esto podemos combinar ancho() con las funciones show() y hide() del paquete {shinyjs}.\nPor ejemplo, en una app que permite seleccionar ubicaciones desde un mapa (más adecuado para pantallas anchas) y un selector común (más adecuado para pantallas angostas o móviles), podemos alternar entre ambos según el ancho de la ventana:\nobserve({ req(ancho()) if (ancho() \u0026gt; 600) { show(\u0026#34;mapa\u0026#34;) hide(\u0026#34;selector\u0026#34;) } else { hide(\u0026#34;mapa\u0026#34;) show(\u0026#34;selector\u0026#34;) } }) En el código anterior, usamos req(ancho()) para asegurarnos de que el valor ya esté disponible antes de intentar usarlo (en el primer instante de la app, antes de que el JavaScript se ejecute, input$window_width podría ser NULL). elDentro del observador, que se evaluará cada vez que sus elementos internos cambien, sanse detecta queoel de la ventana supera los 600 píxeles, se muestra el map(show() a y se oculta el sel(hide()) ector; y si la ventana es más angosta (como en un celular), se oculta el mapa y se muestra el selector. Los textos a los que se hace referencia en show() y hide() son los id de los elementos que queremos afectar.\nAsí podemos adaptar la experiencia de usuario para optimizarla según el dispositivo que use.\nAquí hay un tutorial más completo sobre mostrar y ocultar elementos en Shiny usando {shinyjs}. Revisa el post para aprender a usar show() y hide() para controlar la visibilidad de los elementos de tu app. Adaptar una visualización de datos según el ancho Si nuestra aplicación muestra gráficos, la mayoría de los paquetes como {ggplot2} adaptarán las visualizaciones al espacio disponible.\nPero usando el ancho de la ventana, podemos tomar decisiones más específicas sobre qué mostrar, y adaptar mejor los gráficos.\nEmpecemos con un gráfico de prueba:\nlibrary(ggplot2) grafico \u0026lt;- ggplot(iris) + aes(Sepal.Width, Sepal.Length, color = Species) + geom_point(alpha = 0.7) + scale_color_discrete(palette = c(\u0026#34;#AC558A\u0026#34;, \u0026#34;#553A74\u0026#34;, \u0026#34;#666BC7\u0026#34;)) + theme_linedraw(paper = \u0026#34;#EAD2FA\u0026#34;, ink = \u0026#34;#553A74\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;) grafico Para incluirlo en la app, debería ir dentro de renderPlot()\ngrafico \u0026lt;- renderPlot({ ggplot(iris) + aes(Sepal.Width, Sepal.Length, color = Species) + geom_point(alpha = 0.7) + scale_color_discrete( palette = c(\u0026#34;#AC558A\u0026#34;, \u0026#34;#553A74\u0026#34;, \u0026#34;#666BC7\u0026#34;)) + theme_linedraw(paper = \u0026#34;#EAD2FA\u0026#34;, ink = \u0026#34;#553A74\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;) }) Este sería el gráfico normal, pero si queremos adaptarlo según el ancho, agregamos un if dentro de renderPlot() para agregarle capas condicionales:\ngrafico \u0026lt;- renderPlot({ # el gráfico normal grafico \u0026lt;- ggplot(iris) + aes(Sepal.Width, Sepal.Length, color = Species) + geom_point(alpha = 0.7) + scale_color_discrete( palette = c(\u0026#34;#AC558A\u0026#34;, \u0026#34;#553A74\u0026#34;, \u0026#34;#666BC7\u0026#34;)) + theme_linedraw(paper = \u0026#34;#EAD2FA\u0026#34;, ink = \u0026#34;#553A74\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;) # condicionalidad if (ancho() \u0026lt; 600) { # si el ancho es menor a 600, mover la leyenda a la parte superior grafico \u0026lt;- grafico + theme(legend.position = \u0026#34;top\u0026#34;) } return(grafico) }) Como el espacio horizontal es más escaso en celulares, adaptamos el gráfico para que la leyenda aparezca arriba, y así haya más espacio para los datos!\nBonus: obtener el ancho aproximado de la ventana desde un output existente Una forma más simple (pero menos prolija) es obtener una aproximación del ancho de la ventana a partir de uno de los output de tu aplicación, dado que cada sesión de Shiny tiene un objeto session$clientData donde viene información sobre los outputs, entre otras. Si en tu app hay un output, por ejemplo output$grafico, que usa el ancho completo de la ventana (o generalmente casi el ancho completo, porque los elementos suelen tener márgenes y espaciados), puedes obtener su ancho en el objeto session$clientData$output_grafico_width, y como es un objeto reactivo, se actualizará en tiempo real.\n","date":"2026-04-06T00:00:00Z","excerpt":"Al desarrollar aplicaciones web Shiny, tenemos que considerar que van a ser visitadas desde distintos dispositivos: celulares, tablets, computadores grandes, computadores pequeños... Por eso es importante diseñarlas pensando en la reactividad. Si bien Shiny crea aplicaciones reacgtivas, puede ser útil usar el ancho de la ventana para adaptar los contenidos de la app: mostrar u ocultar elementos, ajustar los gráficos, cambiar un layout, o elegir qué visualización presentar según si el usuario está en un computador de escritorio o en un dispositivo móvil.","href":"https://bastianoleah.netlify.app/blog/shiny_ancho/","tags":"shiny","title":"Medir el ancho de una aplicación Shiny como una variable reactiva y usarla para adaptar los contenidos de la app"},{"content":"A veces, cuando estoy haciendo una visualización con {ggplot2}, calculo mal los datos o aplico mal una geometría, y obtengo un gráfico tan horrible que le tomo una foto.\nEn esta publicación voy a mantener una galería de errores de visualización de datos tan horribles que merecen ser registrados para la posteridad:\nCreo que son demasiadas observaciones Barras apiladas demasiado apiladas Santiago cubierto de sangre Ni idea de lo que pasó aquí Santiago minimalista Animación previo a ser animada Un clásico probando datos espaciales Mucho texto ","date":"2026-04-01T00:00:00Z","excerpt":"A veces, cuando estoy haciendo una visualización con `{ggplot2}`, calculo mal los datos o aplico mal una geometría, y obtengo un gráfico tan horrible que le tomo una foto. Aquí voy a mantener una galería de errores de visualización de datos tan horribles que merecen ser registrados para la posteridad.","href":"https://bastianoleah.netlify.app/blog/graficos_horribles/","tags":"curiosidades","title":"Gráficos horribles"},{"content":" Publicación en construcción! A medida que encuentre (y recuerde) más extensiones las iré listando aquí. Una de las ventajas de usar {ggplot2} para visualización de datos en R es su flexibilidad y capacidad de personalización. Existen muchas extensiones desarrolladas por la comunidad para agregar nuevas funcionalidades, formas de visualizar datos, mejoras, paletas de colores y más.\nA continuación, compartiré acá enlaces a las extensiones de {ggplot2} que más uso y/o que recomiendo:\nExtensiones generales tidyplots Paquete que simplifica la creación de gráficos atractivos y simples de hacer en R, basándose en {ggplot2}. Facilita mucho la generación de visualizaciones profesionales y estadísticas.\ncamcorder Al activarlo, empieza a grabar todos los pasos de las visualizaciones que hagas, de manera que al terminar la visualización puedes obtener una animación del proceso de su desarrollo. Muy entretenido para poder compartir videos de cómo hiciste un gráfico! Tutorial de uso aquí.\npatchwork Con esta extensión se pueden unir y combinar múltiples gráficos de {ggplot2} tan sólo usando operadores como + y otros; es decir, simplemente sumando dos gráficos obtienes una visualización de gráficos combinados. Esto nos permitirá construir visualizaciones más densas, por medio de la combinación de gráficos en una sola visualización, y la inserción de gráficos dentro de otros. Tutorial de uso aquí.\nggiraph Este paquete agrega interactividad a los gráficos {ggplot2}. Esto significa que tus gráficos podrán mostrar información extra al pasar el cursor encima (tooltips), hacer que se destaquen u oculten elementos al pasar el cursor, hacer clic en elementos del gráfico para generar cambios en aplicaciones, y más. También es posible combinar la interactividad de dos o más gráficos, lo que permite crear visualizaciones más complejas. Tutorial de uso aquí.\nggforce Agrega nuevas geometrías, estadísticas, y facetas a {ggplot2}. Algunas de sus funcionalidades son: ahcer cuadros o círculos que envuelvan tus datos, distintas marcas y flechas para anotaciones, envolver puntos con figuras, crear facetas que amplían tus gráficos, geometrías Voronoi, gráficos aluviales, y más.\nGeometrías ggrepel Este paquete agrega geometrías como geom_text_repel() que permiten que las etiquetas de texto en tus visualizaciones no se sobrepongan, haciendo que se muevan para mantenerlas visibles. Muy útil para gráficos de dispersión con demasiados textos. Tutorial de uso aquí.\ncorrr Agrega la función ggcor() para crear gráficos de correlaciones, que muestran las relaciones entre variables en una de matriz de colores. Tutorial de uso aquí.\nggwordcloud Agrega la geometría geom_text_wordcloud() para crear nubes de palabras, que muestran las palabras más frecuentes en un texto, con tamaños y colores que representan su frecuencia. Tutorial de uso aquí.\ngeomtextpath Agrega geometrías como geom_textpath() que permiten colocar texto que siga una cirva o cualquier tipo de línea.\nggbump Agrega la geometría geom_bump() para crear gráficos de ranking o cambio de posiciones.\nmarimekko Un paquete que permite crear gráficos de mosaico con geom_marimekko(), que permiten controlar el área de los rectángulos para representar dos variables categóricas al mismo tiempo.\nggbeeswarm Agrega la geometría geom_beeswarm() para crear gráficos de enjambre, que muestran distribuciones de datos (como gráficos de densidad o violín) pero por medio de puntos que representan las observaciones.\nggtext Este paquete agrega geometrías como geom_richtext() que permiten darle estilo personalizado a los textos de tus gráficos: agregar colores, negritas, itálicas, personalizar tamaños y espaciados, y más usando HTML.\nggridges Agrega la geometría geom_density_ridges() para crear gráficos de densidad apilados, que tienen la apariencia de cordilleras, y permiten mostrar distribuciones de datos desagregados por categorías.\nggstream Agrega la geometría geom_stream() para crear gráficos de flujo o de corrientes, que muestran cómo cambian las proporciones de distintas categorías a lo largo del tiempo u otra variable. Ejemplo de uso.\nggalluvial Gráficos de flujos o aluviales, que muestran cómo cambian las proporciones de distintas categorías a lo largo del tiempo u otra variable. Ejemplo de uso.\nggfittext Agrega la geometría geom_fit_text() para colocar texto dentro de áreas o formas, ajustando automáticamente el tamaño del texto o haciendo cortes de línea para que el texto se ajuste al espacio disponible.\nEscalas y leyendas ggnewscale Permite agregar múltiples escalas de colores a un mismo gráfico, algo que a veces se requiere en visualizaciones complejas. Por ejemplo, si quieres usar una escala de colores para los puntos de un gráfico de dispersión y otra escala de colores para las líneas de tendencia.\nlegendry Expande las posibilidades de las leyendas y escalas de tus gráficos, agregando rangos encima de los ejes, varias guías simultáneas, corchetes que explican aspectos de los ejes, y más.\nPaletas de colores viridis Colección de paletas de colores perceptualmente uniformes, accesibles para personas con daltonismo, y que también funcionan en blanco y negro.\nggsci Colección de paletas de colores científicas, inspiradas en revistas académicas y cultura pop\nscico Paletas de colores científicas caracterizadas por ser perceptualmente uniformes y accesibles para personas con daltonismo.\nUtilidades ggview Permite poner la función canvas() al final de tus gráficos para delimitar el tamaño de los mismos, y que así el tamaño de la previsualización del gráfico no dependa de tu ventana. Sirve mucho para desarrollar las visualizaciones considerando el tamaño específico con el que vas a guardarlas. Tutorial de uso aquí.\ngghighlight Permite resaltar valores específicos de tus gráficos, como ciertos puntos, líneas o áreas, para llamar la atención a ciertos datos o simplificar visualizaciones complejas.\nAvanzadas ggblend Permite mezclar capas de tus gráficos usando distintos modos de mezcla, como multiplicar, superponer, oscurecer, aclarar, y más. Esto te permitirá crear visualizaciones con efectos visuales interesantes y resaltar ciertas partes de tus gráficos.\ngganimate Ofrece la capacidad de crear visualizaciones de datos animadas con {ggplot2}, usando variables que especifican cómo cambia el gráfico a través del tiempo.\nggstatsplot Extensión que hace posible agregar detalles estadísticos, combinando al visualización de los datos con el modelado y la aplicación de técnicas estadísticas.\nMás extensiones Existen varias listas de extensiones de {ggplot2}:\nAwesome ggplot2 Lista oficial de extensiones Galería interactiva de extensiones Libro ggplot2 extended, de Antti Rask ","date":"2026-03-27T00:00:00Z","excerpt":"Existen muchas extensiones para `{ggplot2}` desarrolladas por la comunidad, que le agregan nuevas funcionalidades, formas de visualizar datos, mejoras, paletas de colores y más. En esta publicación compartiré las extensiones de `{ggplot2}` que más uso y/o que recomiendo.","href":"https://bastianoleah.netlify.app/blog/ggplot_extensiones/","tags":"visualización de datos ; ggplot2 ; consejos","title":"Extensiones recomendadas para mejorar tus gráficos de `{ggplot2}`"},{"content":"Para mi blog personal, bastimapache.cl quise crear publicaciones que muestren los libros que he leído cada año, los cuales registro en mi cuenta de Goodreads.\nPara esto, usé R para cargar el archivo de exportación de datos de Goodreads, que te entrega un archivo .csv con tus libros leídos, puntuación, fecha de lectura, etc.\nSi quieres crear tu propio sitio web o blog con Quarto + R y subirlo a internet gratis, sigue este tutorial que hice! Cargar datos de Goodreads Carguemos los datos que obtenemos de la exportación de Goodreads:\nlibrary(dplyr) # para manipulación de datos library(readr) # para cargar datos library(janitor) # para limpiar nombres de columnas libros \u0026lt;- read_csv(\u0026#34;goodreads_library_export.csv\u0026#34;) |\u0026gt; clean_names() Luego le aplicamos una breve limpieza a los datos, incluyendo el paso clave que es filtrar la base de datos para tener libros de un solo año.\nlibrary(stringr) library(glue) library(lubridate) libros \u0026lt;- libros |\u0026gt; # seleccionar columnas select(date_read, title, author, publisher, my_rating, number_of_pages, book_id) |\u0026gt; # excluir libros que no tiene fecha de lectura filter_out(is.na(date_read)) |\u0026gt; # filtrar libros para un sólo año filter(year(ymd(date_read)) == 2025) |\u0026gt; # limpiar datos de texto mutate(title = str_remove(title, \u0026#34;\\\\(.*\\\\)\u0026#34;), title = str_squish(title), author = str_squish(author)) Después de la limpieza 🧹 los datos se ven más o menos así:\nlibros |\u0026gt; select(book_id, title, author, number_of_pages, my_rating) # A tibble: 17 × 5 book_id title author number_of_pages my_rating \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 35411813 Las fórmulas Carol… 67 4 2 44451854 The Lost and the Damned Guy H… 426 3 3 126280622 amor 8: gordo Vario… 232 4 4 7000922 Soul Hunter Aaron… 416 0 5 59817150 Cuidados trans Hil M… 125 5 6 61875686 La invención de los sexos Lu Ci… 256 5 7 49090040 Saturnine Dan A… 553 4 8 63061873 Ir más allá de la piel: Repensar,… Silvi… 200 4 9 62925523 Feminismo posthumano Rosi … 288 3 10 44825746 Nuestra Obsoleta Mentalidad de Me… Karl … 126 4 11 18714631 Poco hombre: crónicas reunidas Pedro… 284 5 12 34466783 Fulgrim: The Palatine Phoenix Joshu… 219 2 13 55610829 The Infinite and the Divine Rober… 361 5 14 222260782 Contra el sexo como categoría bio… Lu Ci… 327 4 15 35085357 La bebedora de sangre y otros cue… Rachi… 64 5 16 160382327 Ravenor Dan A… NA 3 17 40957778 Invisible Women: Exposing Data Bi… Carol… 432 4 Si bien una cuadrícula de libros con las portadas y otros datos se podría hacer con un gráfico, pero para que la visualización se adapte a cualquier pantalla quise hacerla en HTML, de forma que si entras desde un computador se vean muchos libros a la vez, y en un celular se vean menos columnas.\nCon R tenemos muchas herramientas para transformar datos en código HTML y así presentar tus datos de formas totalmente personalizadas. En este caso usé el paquete {shiny}, que produce HTML para aplicaciones interactivas hechas con R, pero también sirve para generar todo tipo de HTML desde R.\nPero para mostrar los libros, primero necesito tener las portadas de cada uno!\nObtener las portadas de los libros Para obtener las portadas de cada libro usaremos web scraping desde R, y así descargar las imágenes directamente desde Goodreads.\nLa lógica será:\nCada libro en mi base de datos tiene un book_id asociado a cada libro. Con el book_id podemos llegar a la página de cada libro en Goodreads, porque la dirección de los libros es: https://www.goodreads.com/book/show/{book_id} En cada página de libro en Goodreads, la portada está en un elemento con clase .BookCover__image, que podemos usar para detectar la imagen y descargarla. Entonces, por cada libro de la base de datos, entramos a su dirección web, extraemos la dirección de la imagen, y la descargamos: Usamos la función map() de {purrr} para hacer un loop que pase por cada libro, y dentro del loop hacemos el web scraping y la descarga de la imagen:\nlibrary(purrr) library(rvest) # por cada id de libro, descarga la portada map(libros$book_id, \\(id) { # id \u0026lt;- \u0026#34;11111\u0026#34; message(id) # crear url usando el id url \u0026lt;- glue(\u0026#34;https://www.goodreads.com/book/show/{id}\u0026#34;) # web scraping # extraer imagen de portada de la dirección imagen \u0026lt;- read_html(url) |\u0026gt; html_elements(\u0026#34;.BookCover__image\u0026#34;) |\u0026gt; html_element(\u0026#34;img\u0026#34;) |\u0026gt; html_attr(\u0026#34;src\u0026#34;) |\u0026gt; unique() # descargar imagen download.file(imagen, destfile = glue(\u0026#34;portadas/portada_{id}.jpg\u0026#34;)) }) El resultado será una carpeta portadas llena de imágenes de cada libro.\nNaturalmente, el código que usé tiene pasos extras, como crear la carpeta si no existe, revisar si ya se descargó la portada para no volver a descargarla, y agregar un tiempo de espera entre descargas para no saturar al pobre servidor que solidariamente nos está ayudando. Puedes ver el código completo aquí.\nAhora que tenemos las portadas descargadas, agreguemos a la base de datos de libros una columna que represente la portada de cada libro, simplemente creando la ruta a la portada según su book_id:\nlibros \u0026lt;- libros |\u0026gt; # columna con portadas de los libros mutate(portada = glue(\u0026#34;portadas/{book_id}.jpg\u0026#34;), link = glue(\u0026#34;https://www.goodreads.com/book/show/{book_id}\u0026#34;) ) De yapa también le pusimos una columna con el link a Goodreads de cada libro.\nCreando una cuadrícula de libros Para hacer la cuadrícula tenemos que aplicar un mismo código a cada libro, que lo haga pasar de una fila en la base a un libro bonito con imagen y sus datos, por lo que necesitaremos hacer otro loop.\nNecesitamos tener los libros organizados, de forma que podamos generar un elemento HTML por cada libro. Para eso, ordenamos los libros por fecha de lectura con arrange() y luego los separamos con split() en una lista, donde cada elemento de la lista sea un libro. En otras palabras, creamos una lista donde cada elemento sea una tabla con los datos de cada libro en una sola fila.\nlista_libros \u0026lt;- libros |\u0026gt; arrange(date_read, book_id) |\u0026gt; mutate(id = row_number()) |\u0026gt; split(~id) Por ejemplo, veamos el libro número 9:\nlista_libros[[9]] |\u0026gt; select(title, author, number_of_pages, portada) # A tibble: 1 × 4 title author number_of_pages portada \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;glue\u0026gt; 1 Poco hombre: crónicas reunidas Pedro Lemebel 284 portadas/1871463… Hagamos una prueba de la cuadrícula con un sólo libro, porque lo que nos interesa es transformar los datos en contenido HTML.\nGeneraremos un div, que es un elemento HTML que puede contener cualquier cosa dentro, como un contenedor para englobar nuestra cuadrícula.\nDentro de este div general, ejecutaremos un loop con la función map() de {purrr} que va a ir por cada libro de la lista, y por cada libro va a generar un nuevo div, que dentro tendrá la portada del libro correspondiente, el título, el autor, la fecha de lectura y el número de páginas.\nlibrary(shiny) library(purrr) salida \u0026lt;- div( # loop por cada libro map(lista_libros[9], \\(libro) { # libro individual div( # portada del libro div( a(href = libro$link, # enlace a la página del libro img(src = libro$portada, height = 120) # imagen del libro ) ), # datos del libro p(libro$title), p(libro$author), p(\u0026#34;Fecha:\u0026#34;, libro$date_read), p(\u0026#34;Páginas:\u0026#34;, libro$number_of_pages) ) }) # fin del loop ) En otras palabras, escribimos un código que genera HTML en base a un libro hipotético, pero que después nos va a permitir generar código para uno o infinitos libros gracias al loop.\nVeamos qué se generó con el libro de ejemplo:\nsalida Poco hombre: crónicas reunidas\nPedro Lemebel\nFecha: 2025-08-01 Páginas: 284 Está quedando horrible! Pero funciona. Pasamos de una fila a HTML. Siempre es bueno probar la funcionalidad de las cosas antes de dedicarse a que queden bonitas.\nPuntuación de cada libro Aunque falta un detalle: las estrellas que le di a cada libro. Para eso, creamos una la función estrellas() que va a tomar la puntuación que tiene el libro, y luego va a ir del 1 al 5 viendo si la puntuación coincide con el número. Si coincide, estrellita amarilla. Si no coincide, estrellita gris.\n# función para generar estrellas estrellas \u0026lt;- function(rating) { simbolo \u0026lt;- \u0026#34;\\u2605\u0026#34; color_punto \u0026lt;- \u0026#34;#9069c0\u0026#34; color_vacio \u0026lt;- \u0026#34;#8F758F60\u0026#34; # por cada número del 1 al 5, hace un span con una estrella y revisa si es parte del puntaje o no, y le asigna un color; luego retorna los 5 elementos estrellas \u0026lt;- map(1:5, \\(numero) { # color dependiendo de si está dentro del rating color \u0026lt;- if_else(numero \u0026lt;= rating, color_punto, color_vacio) # estrella individual span(simbolo, style = glue(\u0026#34;color: {color}; font-size: 14px; margin: 1px;\u0026#34;) ) }) # retornar las 5 estrellas div(class = \u0026#34;estrellas\u0026#34;, estrellas) } Probemos la función:\nestrellas(3) ★ ★ ★ ★ ★ Muestra 3 estrellas de 5! Super cool y muy clever en su implementación.\nMejorando la apariencia del HTML con CSS Ahora que tenemos funcionando el loop que genera el HTML, podemos enfocarnos en hacer que se vea mejor. Para dat estilo al HTML se usa el código CSS, y el CSS se aplica a los elementos del HTML directamente por el atributo style de cada elemento, o mediante clases.\nClases CSS A cada elemento HTML le puedes asignar una clase, y después definir el estilo de dicha clase para que se le aplique a todos los elementos que la tengan.\nCada elemento que tenga una clase específica adquirirá la configuración que definamos en dicha clase. Así podemos darle un mismo estilo a múltiples elementos.\nPersonalmente primero le pongo la clase a cada elemento HTML, y después voy ajustando las clases para que quede como yo quiera.\nVer código de las clases CSS Éstas son las clases CSS que usé para la cuadrícula de libros:\n/* para cuadrícula de libros */ .cuadricula { display: grid; grid-template-columns: repeat(auto-fill, 120px); grid-gap: 16px; width: 100%; margin: auto; justify-content: space-evenly; white-space: wrap; padding-top: 18px; } /* contenedor de cada libro individual */ .contenedor_libro { align-items: center; margin: auto; margin-top: 0; } .libro_imagen { margin: auto; text-align: center; margin-bottom: 12px; border-radius: 3px; opacity: 97%; img { height: 160px; border-radius: 4px; border: solid 1px #31243B70; } } .libro_titulo { margin: auto; text-align: center; margin-bottom: 4px; font-weight: bold; color: $foreground2; text-decoration: none !important; font-size: 90%; line-height: 1; margin-top: -4px; } .libro_autor { margin: auto; text-align: center; text-decoration: none !important; font-size: 78%; } .estrellas { display: inline-flex; } .estrella { font-size: 14px; margin: 1px; } .libro_rating { margin: auto; text-align: center; } .libro_paginas { margin: auto; text-align: center; opacity: 50%; font-size: 65%; margin-top: 1px; } Cuadrícula de libros con estilo personalizado A continuación mejoramos el código anterior, poniéndole clases a todos los elementos para que se les aplique el CSS y así poder personalizarlos, y también incluiremos la puntuación con estrellas y otros detalles:\nlibrary(shiny) # contenedor principal que contiene todos los libros dentro div(class = \u0026#34;cuadricula\u0026#34;, # generar elementos por cada libro map(lista_libros, \\(libro) { # libro individual div(class = \u0026#34;contenedor_libro\u0026#34;, # portada del libro div(class = \u0026#34;libro_imagen\u0026#34;, a(href = libro$link, # enlace de la imagen img(src = libro$portada, # ruta a la imagen alt = libro$title) # texto alternativo ) ), # datos del libro p(class = \u0026#34;libro_titulo\u0026#34;, libro$title), p(class = \u0026#34;libro_autor\u0026#34;, libro$author), div(class = \u0026#34;libro_rating\u0026#34;, estrellas(libro$my_rating)), # texto con fecha de lectura y número de páginas p(class = \u0026#34;libro_paginas\u0026#34;, glue(\u0026#34;Leído en el {month(libro$date_read)} de {year(libro$date_read)}, {libro$number_of_pages} páginas\u0026#34;) ) ) }) ) Invisible Women: Exposing Data Bias in a World Designed for Men\nCaroline Criado Pérez\n★ ★ ★ ★ ★ Leído en el 2 de 2025, 432 páginas\nThe Lost and the Damned\nGuy Haley\n★ ★ ★ ★ ★ Leído en el 3 de 2025, 426 páginas\nRavenor\nDan Abnett\n★ ★ ★ ★ ★ Leído en el 4 de 2025, NA páginas\nLa bebedora de sangre y otros cuentos\nRachilde\n★ ★ ★ ★ ★ Leído en el 4 de 2025, 64 páginas\nLas fórmulas\nCarolina Rack\n★ ★ ★ ★ ★ Leído en el 4 de 2025, 67 páginas\nSoul Hunter\nAaron Dembski-Bowden\n★ ★ ★ ★ ★ Leído en el 6 de 2025, 416 páginas\nContra el sexo como categoría biológica: Cómo desmontar las premisas sexistas que limitan nuestra vida\nLu Ciccia\n★ ★ ★ ★ ★ Leído en el 7 de 2025, 327 páginas\nThe Infinite and the Divine\nRobert Rath\n★ ★ ★ ★ ★ Leído en el 7 de 2025, 361 páginas\nPoco hombre: crónicas reunidas\nPedro Lemebel\n★ ★ ★ ★ ★ Leído en el 8 de 2025, 284 páginas\nFulgrim: The Palatine Phoenix\nJoshua Reynolds\n★ ★ ★ ★ ★ Leído en el 8 de 2025, 219 páginas\nNuestra Obsoleta Mentalidad de Mercado\nKarl Polanyi\n★ ★ ★ ★ ★ Leído en el 8 de 2025, 126 páginas\nFeminismo posthumano\nRosi Braidotti\n★ ★ ★ ★ ★ Leído en el 9 de 2025, 288 páginas\nIr más allá de la piel: Repensar, rehacer y reivindicar el cuerpo en el capitalismo contemporáneo\nSilvia Federici\n★ ★ ★ ★ ★ Leído en el 10 de 2025, 200 páginas\nSaturnine\nDan Abnett\n★ ★ ★ ★ ★ Leído en el 10 de 2025, 553 páginas\nLa invención de los sexos\nLu Ciccia\n★ ★ ★ ★ ★ Leído en el 11 de 2025, 256 páginas\nCuidados trans\nHil Malatino\n★ ★ ★ ★ ★ Leído en el 11 de 2025, 125 páginas\namor 8: gordo\nVarious\n★ ★ ★ ★ ★ Leído en el 12 de 2025, 232 páginas\nHermoso! 😍 Ahora, como mi blog personal está hecho con Quarto, simplemente puedo copiar este código y cambiar el año del filtro para hacer publicaciones automáticas por cada año.\nEsa es una de las gracias de Quarto: incluir código de R dentro de tus documentos, páginas web o publicaciones de blog, para generar contenido en base a datos! Crear tu propio sitio con Quarto es súper fácil!\n","date":"2026-03-26T00:00:00Z","excerpt":"Para mi [blog personal](https://bastimapache.cl) quise crear publicaciones que muestren los libros que he leído cada año, los cuales registro en mi cuenta de [Goodreads](https://www.goodreads.com/user/show/53224910-basti-n-olea-herrera). Así que usamos R para generar cuadrículas de libros por año, incluyendo la descarga automática de las portadas de los libros. Ésta es una de las gracias de Quarto: incluir código de R dentro de tus documentos, páginas web o publicaciones de blog, para generar contenido basado en datos.","href":"https://bastianoleah.netlify.app/blog/2026-03-26/","tags":"quarto ; shiny ; blog ; web scraping","title":"Galería de libros de _Goodreads_ para tu blog o sitio web con R y Quarto"},{"content":" Inicio de la plataforma del IBG El Índice de Brechas de Género es un nuevo instrumento estadístico desarrollado por la Subsecretaría de Desarrollo Regional y Administrativo (Subdere), diseñado para medir brechas de género a nivel comunal y regional en el país. El IBG se basa en 52 indicadores de nivel comunal y regional, que abarcan las dimensiones de cultura, educación, salud, laboral, participación y social.\nAccede a la plataforma de visualización Para producir este instrumento se realizaron búsquedas exhaustivas de datos sociales de nivel comunal que cuenten con desagregación de género, incluyendo múltiples solicitudes de datos por ley de transparencia a servicios públicos. Así, el Departamento de Estudios y Análisis Territorial de Subdere ha desarrollado una plataforma de visualización de datos única en la cantidad de información con perspectiva de género disponible, además complementada con interpretaciones teóricas y conceptuales de cada indicador.\nEl instrumento además se caracteriza por presentar brechas de género que afectan negativamente tanto a mujeres como a hombres, presentando una visión compleja de las desigualdades e inequidades de género que considera los efectos nocivos de los sesgos, estereotipos y discriminaciones de género sobre toda la población. Esto permite presentar indicadores donde, por ejemplo, las mujeres aparentemente se ven beneficiadas por sobre los hombres, pero que al analizarse esconden sesgos, estereotipos o discriminaciones que las afectan en otras dimensiones de la complejidad social.\nLa lógica de presentación de los resultados va desde una exploración desde lo general a lo particular: las y los usuarios empiezan eligiendo el aspecto más general del estudio, la dimensión, para luego elegir un indicador de la dimensión elegida y recibir resultados generales, con la posibilidad de continuar bajando por la plataforma para indagar en resultados regionales y finalmente comunales. De este modo, la plataforma entrega un análisis de brechas de género en múltiples niveles de detalle.\nDiagrama de desarrollo del proyecto IBG Desarrollo del estudio Todos los datos del estudio fueron procesados en R, y la plataforma interactiva también fue desarrollada en R.\nAl igual que con el Estudio de Brechas Comunales, desarrollar este proyecto en R permitió integrar altamente la obtención y el análisis de los datos con la presentación de resultados, significando un proceso de desarrollo paralelo y de mucha iteración dentro del equipo, cosa que no habría sido posible con equipos externos. En este sentido, también significa una optimización del gasto público, al no necesitar licitar el desarrollo de la plataforma a una empresa externa.\nProcesamiento de datos Luego de catastrar la disponibilidad de datos sociales de nivel comunal y con desagregación por sexo o género, los datos fueron guardados por fuente y procesados con el lenguaje de programación estadística R.\nCada fuente de datos cuenta con un script que limpia los datos y transforma su estructura a una en común, con columnas para los géneros masculino y femenino. Posteriormente, el script procesar.R carga todos los resultados de las fuentes limpias, aplica tests unitarios para validar la calidad de la información, unifica los datos de distintas fuentes en una sola base de datos, y consulta planillas compartidas de Google Sheets para complementar los datos con metadatos clave para la plataforma de visualización, así como interpretaciones teóricas y conceptuales para cada indicador.\nLa base de datos resultante está diseñada para contener toda la metadata necesaria para que la plataforma interactiva muestre las interpretaciones correctas de los datos, así como que las visualizaciones puedan ajustarse a las particularidades de cada indicador.\nDesarrollo de la plataforma El estudio se centra en su presentación por medio de la plataforma interactiva, siendo su informe de resultados un complemento de la plataforma y no el producto central.\nLa plataforma fue desarrollada en R y Shiny, y presenta una serie de visualizaciones interactivas que permiten explorar los resultados del estudio.\nPronto liberaremos el código del procesamiento de datos y de desarrollo de la plataforma! ","date":"2026-03-26T00:00:00Z","excerpt":"El Índice de Brechas de Género es un nuevo instrumento estadístico desarrollado por la Subdere, diseñado para medir brechas de género a nivel comunal y regional en el país. El IBG se basa en 52 indicadores de nivel comunal y regional, que abarcan las dimensiones de cultura, educación, salud, laboral, participación y social. Se realizaron búsquedas exhaustivas de datos sociales de nivel comunal que cuenten con desagregación de género, incluyendo múltiples solicitudes de datos por ley de transparencia a servicios públicos. Así, hemos desarrollado una plataforma de visualización de datos única en la cantidad de información con perspectiva de género disponible, además complementada con interpretaciones teóricas y conceptuales de cada indicador.","href":"https://bastianoleah.netlify.app/blog/indice_brechas_genero/","tags":"apps ; chile ; datos ; blog ; shiny","title":"Plataforma de análisis: Índice de Brechas de Género"},{"content":" Inicio de la plataforma del EBC Ya está disponible la plataforma de visualización de los resultados del Estudio de Brechas Comunales de la Subsecretaría de Desarrollo Regional y Administrativo (Subdere).\nEste estudio mide brechas en infraestructura y servicios a través de 59 indicadores de nivel comunal, tomando en consideración las diferencias territoriales de comunas urbanas, mixtas y rurales.\nTrabajamos con 24 instituciones públicas para obtener datos públicos de calidad y determinar en conjunto los umbrales que definen las situaciones de brecha o ausencia de brecha para cada indicador.\nNos enorgullece poder hacer público un instrumento que aporta en la evaluación de la calidad de vida de las y los chilenos, aportando a una mejor planificación e inversión pública!\nAccede a la plataforma de visualización Desarrollo del estudio Todos los datos del estudio fueron procesados en R, y la plataforma interactiva también fue desarrollada en R.\nDesarrollar este proyecto en R significó un aumento de la velocidad de trabajo importante, pero también un enorme ahorro de fondos públicos al no necesitar licitar el estudio a una empresa externa.\nProcesamiento de datos Luego de la obtención de datos públicos desde fuentes de datos abiertos o en colaboración con servicios públicos, los datos se guardaron en carpetas por fuente. Por cada fuente se escribió un script de limpieza de los datos para dejarlos en una estructura común.\nLuego, por cada fuente limpia, se cargan los datos del paso anterior y se vuelven a procesar para generar los indicadores, umbrales, y otros estadísticos necesarios para el estudio. Estos scripts de procesamiento son todos casi idénticos, dado que se reutilizan una serie de funciones gracias a que los datos llegan limpios y revisados: por ejemplo, bastaba con aplicar funciones como calcular_umbrales(), calcular_brechas() y calcular_indice().\nEntonces, contamos con los datos en tres etapas: el dato crudo, el dato limpio y el dato procesado. Este último conjunto de datos está listo para ser unido en una sola base de datos con los resultados del estudio (script indice/indice.R). En este paso se juntan los indicadores de todas las fuentes en un sólo archivo formato .parquet. Además, durante este paso final, se aplican tests unitarios para confirmar que los datos están 100% correctos y vienen sin ningún tipo de falencia.\nDiagrama del proceso completo de desarrollo del estudio Finalmente, y dado a que corresponde a un momento posterior en el desarrollo de estudio, en el script umbrales/umbrales.R se aplican los valores de comparación definidos por las contrapartes de los servicios públicos responsables de cada indicador, usados para calcular las brechas. La base de datos resultante es la que se utiliza para generar los gráficos, tablas y reportes del estudio, así como alimentar la plataforma de visualización de resultados.\nLos datos del estudio pueden descargarse en el botón ubicado en la parte inferior de la plataforma. Plataforma interactiva La plataforma fue desarrollada en Shiny en no más de 2 semanas, gracias a la facilidad de desarrollo que ofrece dicho paquete, y el hecho de que las visualizaciones y mapas ya habían sido programados en R para otros aspectos del estudio, por lo que fue cosa de tomar el código y pasarlo a la aplicación. La plataforma misma está alojada en un servidor de Subdere, dentro de un contenedor Docker.\nGráficos y tablas Todas las visualizaciones de datos del informe de resultados del Estudio también fueron desarrolladas en R, lo que entregó la conveniencia de poder ir actualizando y corrigiendo los datos sin que ésto signifique volver a hacer los gráficos. Esto significa que el proyecto pudo avanzar más rápido, dado que ante cualquier actualización de datos, los gráficos se regeneraban sin costo alguno de tiempo ni trabajo. Las tablas y cuadros del estudio también fueron hechas en R, con los mismos beneficios mencionados.\nAccede al informe de resultados del estudio Reportes Al producir los resultados del estudio, resultaba crucial poder disponibilizar reportes breves de resultados para cada comuna del país. En proyectos anteriores, reportes comunales de este tipo eran hechos a mano; es decir, 345 reportes individuales. Este trabajo se optimizó generando los reportes con Quarto y R, lo que permitió diseñar un sólo reporte y programar la disposición de los resultados de una comuna tipo dentro del reporte, para luego replicar automáticamente 345 reportes sin tener que hacerlos a mano.\nReporte comunal descargable El documento Quarto parametrizado reportes/reporte_comuna.qmd genera un reporte para la comuna deseada en formato HTML. El script reportes/generar_reporte_comuna.R usa el mismo reporte dentro de un loop para generar 345 reportes individuales sin intervención alguna. Luego, el script reportes/convertir_reportes_pdf.R toma los resultados y los convierte en PDF, y finalmente reportes/combinar_reportes_pdf.R combina los archivos PDF en uno solo, incluyendo agregarle la portada diseñada por el equipo de Comunicaciones. Naturalmente, todo esto se ejecuta desde un sólo script que ejecuta el resto de los scripts: reportes/reportes.R, de modo que si se requiere aplicar una modificación al diseño de los reportes, se aplica en el documento Quarto y se ejecuta reportes.R para obtener en segundos el cambio aplicado a los más de 300 reportes.\nPronto liberaremos el código del procesamiento de datos y de desarrollo de la plataforma! ","date":"2026-03-24T00:00:00Z","excerpt":"Ya está disponible la plataforma de visualización de los resultados del Estudio de Brechas Comunales de la Subsecretaría de Desarrollo Regional y Administrativo (Subdere). Este proyecto fue desarrollado íntegramente en R, al igual que la plataforma interactiva.\nEste estudio mide brechas en infraestructura y servicios a través de 59 indicadores de nivel comunal, tomando en consideración las diferencias territoriales de comunas urbanas, mixtas y rurales.","href":"https://bastianoleah.netlify.app/blog/estudio_brechas_comunales/","tags":"chile ; datos ; apps ; blog ; Quarto ; shiny","title":"Plataforma de visualización de resultados del Estudio de Brechas Comunales"},{"content":" La reunión ya fue, y salió super bien! Revisa lo que hicimos en esta publicación del sitio del grupo de usuarios. Hace tiempo que estamos coordinando un espacio de colaboración entre usuari@s de R en Chile, y pronto verá la luz!\nLa próxima semana tendremos la primera reunión del grupo de usuari@s de R de Santiago, Chile. La idea es crear un espacio de aprendizaje y apoyo entre personas que usan R en el país, para compartir lo que hacemos, aprender de lo que hacen otras personas, y apoyarnos en conjunto.\nEntre otras actividades, habrá espacio para mostrar lo que hayas desarrollado: visualizaciones de datos, investigaciones, aplicaciones, estudios, lo que sea!\nLa idea es saber qué hacemos con R y así apoyarnos y seguir aprendiendo 💜\nMás información e inscripciones ","date":"2026-03-12T00:00:00Z","excerpt":"La próxima semana tendremos la primera reunión del grupo de usuari@s de R de Santiago, Chile. La idea es crear un espacio de aprendizaje y apoyo entre personas que usan R en el país, para compartir lo que hacemos, aprender de lo que hacen otras personas, y apoyarnos en conjunto.","href":"https://bastianoleah.netlify.app/blog/2026-03-12/","tags":"blog ; Chile","title":"Primera reunión: grupo de usuari@s de R, Santiago de Chile"},{"content":" Este tutorial lo hice con los aprendizajes que obtuve del excelente workshop LLMs in R for Data Analysis impartido por Nic Crane en la fabulosa conferencia de RainbowR 2026 Los modelos de lenguaje (LLMs, también llamados coloquialmente inteligencia artificial) nos pueden ayudar a transformar textos de cualquier tipo en datos estructurados. Esto significa que puedes convertir entrevistas, reseñas, opiniones, correos, noticias y más en bases de datos con la información que necesitas debidamente ordenada en filas y columnas.\nEjemplos de uso:\nPuedes pasar de un conjunto de entrevistas a una tabla con las respuestas a preguntas específicas sobre los entrevistados, incluyendo información como edad, género, profesión, opiniones, etc. Puedes convertir reseñas de productos en una tabla con la opinión general, los aspectos positivos y negativos, la puntuación, etc. Puedes tomar conjuntos de noticias o de artículos y obtener tablas que indican los actores principales, ubicaciones clave, fechas, resúmenes, análisis de sentimiento, etc. En ese tutorial usaremos modelos de lenguaje para extraer datos estructurados a partir de noticias reales.\nEl requisito para poder realizar esto es tener acceso a una IA o modelo de lenguaje (LLM), ya sea local o en la nube (ChatGPT, Claude, Gemini, etc.). Puedes seguir este tutorial para configurar el uso de inteligencia artificial con R.\nRequisito: si necesitas configurar tu proveedor de IA con R, sigue este tutorial para empezar a analizar datos con IA! Datos de texto de ejemplo Los datos que usaremos para el tutorial será una base de 10.000 noticias reales obtenidas desde medios digitales de Chile durante el año 2025. Las noticias son obtenidas por web scraping, y el código de este proyecto está disponible en este repositorio.\nDescargar datos (clic derecho y guardar como) También puedes descargar los datos desde R:\n# dirección de los datos url = \u0026#34;https://github.com/bastianolea/prensa_chile/raw/main/prensa_datos_muestra_2025.csv\u0026#34; # descargar el archivo con R download.file(url, destfile = \u0026#34;prensa_datos_muestra_2025.csv\u0026#34;) O bien, cargar los datos directamente desde internet sin necesidad de descargar el archivo:\n# cargar datos directamente desde internet library(readr) noticias \u0026lt;- read_csv2(url) Veamos rápidamente la tabla de datos con la función glimpse():\nlibrary(dplyr) glimpse(noticias) Rows: 10,000 Columns: 4 $ titulo \u0026lt;chr\u0026gt; \u0026quot;Acusa falta de imparcialidad: Defensa de Vivanco pide excluir … $ cuerpo \u0026lt;chr\u0026gt; \u0026quot;La estrategia judicial de la exministra de la Corte Suprema, Á… $ fuente \u0026lt;chr\u0026gt; \u0026quot;CNN Chile\u0026quot;, \u0026quot;The Clinic\u0026quot;, \u0026quot;El Ciudadano\u0026quot;, \u0026quot;La Tercera\u0026quot;, \u0026quot;El Ci… $ fecha \u0026lt;date\u0026gt; 2025-12-31, 2025-12-31, 2025-12-31, 2025-12-31, 2025-12-31, 20… Vemos que tenemos una columna con los títulos, otra con el texto o cuerpo de las noticias, la fuente y la fecha de cada noticia.\nExtraer datos desde un solo texto Empecemos extrayendo una sola noticia como ejemplo, usando slice() para elegir una fila de la tabla, y pull() para extraer la columna cuerpo, que contiene el texto de la noticia.\nlibrary(dplyr) noticia \u0026lt;- noticias |\u0026gt; slice(6) |\u0026gt; pull(cuerpo) Veamos un poco de qué se trata la noticia:\nEl subprefecto Adolfo Domínguez, jefe de la Brigada Contra el Crimen Organizado, señaló que \u0026ldquo;se pudo indagar en diferentes comunas de Santiago y se logró dar con el paradero del sujeto, que mantiene múltiples antecedentes\u0026rdquo;. La Policía de Investigaciones (PDI) recapturó a Matías Jesús Fuentes Roja\u0026hellip;\nPuedes leer la noticia completa en este enlace, pero la idea general es que contamos con el cuerpo entero de la noticia en el objeto noticia.\nPregunta simple al modelo Veamos la forma más simple de extraer datos estructurados desde información no estructurada: preguntarle al modelo de IA y cruzar los dedos 🤞🏼\nCreamos el chat de IA que usaremos para extraer los datos. En mi caso, voy a usar modelos de Anthropic, por lo que uso la función chat_anthropic(), pero si usas otro modelo, puedes usar la función correspondiente de {ellmer} para crear tu chat, como se explica en este tutorial.\nlibrary(ellmer) # crear un chat chat \u0026lt;- chat_anthropic(model = \u0026#34;claude-haiku-4-5\u0026#34;) Si necesitas configurar tu proveedor de IA con R, o configurar modelos locales de IA con R, sigue este tutorial Ahora hagamos una prueba/ejemplo: usemos la noticia que elegimos para entregarla al modelo de lenguaje, junto a una breve instrucción:\n# crear el prompt con la instrucción y el texto prompt \u0026lt;- paste(\u0026#34;En qué lugar se desarrolla la siguiente noticia:\u0026#34;, noticia) # enviar el prompt al modelo chat$chat(prompt) # Lugar donde se desarrolla la noticia La noticia se desarrolla en **Santiago de Chile**, específicamente en la **comuna de Cerrillos**. Según el texto, la recaptura de Matías Jesús Fuentes Rojas fue concretada por la PDI \u0026quot;durante la madrugada de este miércoles, en la comuna de Cerrillos\u0026quot;. Además, se menciona que las indagatorias se realizaron \u0026quot;en diferentes comunas de Santiago\u0026quot;. El modelo de lenguaje trata de ser útil, y por eso entrega mucha más información que la que necesitamos! No podemos poner todo ese texto en una tabla de datos. Habría que limpiarlo muchísimo y dejaría de ser útil 🙄\nPodemos corregir el comportamiento del modelo especificando una instrucción más clara en el system prompt, que es la instrucción general que le damos al modelo al crear el chat:\n# definir instrucción general para el modelo instruccion \u0026lt;- \u0026#34;Eres un experto en noticias de Chile. Entrega la información lo más breve posible, especificando en una o dos palabras y sin comentarios extra. Por ejemplo: {Puente Alto, Santiago}.\u0026#34; # crear chat con el modelo y con system prompt chat \u0026lt;- chat_anthropic(model = \u0026#34;claude-haiku-4-5\u0026#34;, system_prompt = instruccion) # escribir prompt o instrucción particular prompt \u0026lt;- paste(\u0026#34;En qué lugar se desarrolla la siguiente noticia:\u0026#34;, noticia) # enviar el prompt al modelo chat$chat(prompt) Cerrillos, Santiago. Podemos confirmar en la noticia completa que efectivamente los hechos tuvieron lugar en la comuna de Cerrillos.\nAhora sí, el modelo de lenguaje responde tal como esperamos. Pero esta forma de hacerlo aún deja mucho al azar: la especificidad del system prompt que escribas, y la aleatoriedad del modelo que puede responder algo distinto sin previo aviso.\nEspecificación de datos estructurados Ahora veremos una una forma más consistente de extraer datos a partir de textos con IA, para que los modelos de lenguaje respondan específicamente lo que necesitamos.\nPara ello, vamos a especificar las respuestas que queremos recibir del modelo, usando las funciones type_{x} de {ellmer}:\nFunción Tipo de respuesta Descripción type_string() Texto abierto Para respuestas en texto libre, como nombres, conceptos, explicaciones, etc. type_enum() Variable categórica Para respuestas que asuman un valor de un conjunto de valores predefinidos. type_number() Numérica Para respuestas que son cifras (números enteros, decimales, etc.) type_boolean() Variable lógica Para respuestas de verdadero/falso (TRUE / FALSE) Siguiendo con el ejemplo, si queremos extraer la ubicación principal mencionada en la noticia, usamos type_string() para pedirle una respuesta de texto al modelo, junto a una breve descripción de lo que queremos:\ntipo_lugar \u0026lt;- type_string(\u0026#34;Ubicación principal de la noticia\u0026#34;) Ahora, al momento de enviar el prompt al modelo (el objeto noticia que contiene el texto), usamos chat$structured() en vez de chat$chat() para recibir datos estructurados en vez de la respuesta libre del modelo, y le indicamos que queremos que la respuesta sea del tipo tipo_lugar:\n# crear chat con el modelo chat \u0026lt;- chat_anthropic(model = \u0026#34;claude-haiku-4-5\u0026#34;) # empezar chat estructurado especificando el tipo de respuesta resultado \u0026lt;- chat$chat_structured(noticia, type = tipo_lugar) resultado [1] \u0026quot;Santiago, Chile\u0026quot; El modelo de lenguaje responde con la ubicación principal de la noticia, sin ningún comentario extra, y sin necesidad de hacer un procesamiento posterior para limpiar la respuesta.\nExtraer variables categóricas Si queremos extraer desde los datos no estructurados una variable categórica, es decir, que sólo pueda tomar un valor entre varias opciones predefinidas, podemos usar type_enum(), donde especificamos un vector con los valores posibles.\nPor ejemplo, preguntemos qué tipo de noticia se trata, entre categorías como social, economía, política, etc.\n# definir tipo de respuesta categórica tipo_clasificacion \u0026lt;- type_enum(description = \u0026#34;Clasificación de la noticia según su enfoque temático principal\u0026#34;, values = c(\u0026#34;social\u0026#34;, \u0026#34;económico\u0026#34;, \u0026#34;político\u0026#34;, \u0026#34;judicial\u0026#34;, \u0026#34;delincuencia\u0026#34;, \u0026#34;otros\u0026#34;)) # crear chat con el modelo chat \u0026lt;- chat_anthropic(model = \u0026#34;claude-haiku-4-5\u0026#34;) # empezar chat estructurado especificando el tipo de respuesta resultado \u0026lt;- chat$chat_structured(noticia, type = tipo_clasificacion) resultado [1] \u0026quot;delincuencia\u0026quot; De esta forma, el modelo se limita a responder solamente entre las opciones que tú le especifiques, delimitando mucho más la calidad de la respuesta.\nExtracción datos estructurados desde varios textos Hasta ahora hemos extraído datos estructurados a partir de un solo elemento de texto no estructurado.\nRealicemos la extracción de datos estructurados de varias unidades de información al mismo tiempo. Como ejemplo, probemos con una muestra de 5 noticias:\nlibrary(dplyr) muestra \u0026lt;- noticias |\u0026gt; slice(20:25) muestra |\u0026gt; select(titulo) # A tibble: 6 × 1 titulo \u0026lt;chr\u0026gt; 1 Nuevo error en Gendarmería: imputado queda en libertad pese a que debía cumpl… 2 Andes Iron acusa ofensiva política contra Dominga tras conocerse que enfrenta… 3 Fiscalía acusó a Cathy Barriga por cuatro delitos y pide más de 23 años de cá… 4 No fue Patricia Muñoz: Boric nombra a Macarena Cortés como la primera directo… 5 “Ya tomaron las huellas”: dueño de tienda asaltada durante incendio en Mall P… 6 Presidente Boric recibe cartas credenciales de cinco embajadores y cierra pol… Siguiendo la definición de tipos de resultado que aprendimos más arriba, podemos definir de una vez las distintas variables que queremos extraer desde nuestros datos no estructurados: en el caso de las noticias, queremos extraer el lugar, los personajes principales, el tipo de personaje, la clasificación de las noticias, y el sentimiento general de la noticia.\nEstas variables serán generalmente de tipo texto, pero varias de ellas queremos que sean variables categóricas con valores específicos.\ntipo_lugar \u0026lt;- type_string(\u0026#34;Ubicación principal de la noticia\u0026#34;) tipo_actor \u0026lt;- type_string(\u0026#34;Actor, personaje o institución principal de la noticia\u0026#34;) tipo_grupo \u0026lt;- type_enum(description = \u0026#34;Tipo de entidad del actor, personaje o institución principal de la noticia\u0026#34;, values = c(\u0026#34;institución pública\u0026#34;, \u0026#34;político\u0026#34;, \u0026#34;empresa\u0026#34;, \u0026#34;empresario\u0026#34;, \u0026#34;ciudadano\u0026#34;, \u0026#34;otros\u0026#34;)) tipo_clasificacion \u0026lt;- type_enum(description = \u0026#34;Clasificación de la noticia según su enfoque temático principal\u0026#34;, values = c(\u0026#34;social\u0026#34;, \u0026#34;económico\u0026#34;, \u0026#34;político\u0026#34;, \u0026#34;judicial\u0026#34;, \u0026#34;delincuencia\u0026#34;, \u0026#34;otros\u0026#34;)) tipo_sentimiento \u0026lt;- type_enum(values = c(\u0026#34;positivo\u0026#34;, \u0026#34;neutro\u0026#34;, \u0026#34;negativo\u0026#34;), description = \u0026#34;Sentimiento general de la noticia según su contenido\u0026#34;) Ahora podemos usar la función parallel_chat_structured() para enviar varios textos al modelo de lenguaje, y recibir datos estructurados para cada uno de ellos. Esta función recibe como argumentos el chat (que además tiene un system prompt que especifica el actuar del modelo), una lista con los textos que queremos procesar (en este caso, la columna cuerpo de la tabla muestra), y los tipos de respuesta que queremos recibir (al ser varias, van en dentro de type_object()).\ninstruccion \u0026lt;- \u0026#34;Eres un clasificador de noticias experto en el acontecer nacional de Chile\u0026#34; resultado \u0026lt;- parallel_chat_structured( chat = chat_anthropic(model = \u0026#34;claude-haiku-4-5\u0026#34;, system_prompt = instruccion), prompts = as.list(muestra$cuerpo), type = type_object( lugar = tipo_lugar, actor = tipo_actor, actor_grupo = tipo_grupo, sentimiento = tipo_sentimiento, clasificacion = tipo_clasificacion ) ) Al solicitar varias variables al modelo, retorna una tabla o tibble con los datos estructurados de cada noticia, con una fila por cada noticia, en el orden que se entregaron las noticias.\nlugar actor actor_grupo sentimiento clasificacion Santiago Gendarmería de Chile institución pública negativo judicial Chile Andes Iron empresa negativo judicial Maipú, Región Metropolitana Cathy Barriga, exalcaldesa de Maipú político negativo judicial Chile Gabriel Boric político positivo político Mall Plaza Norte, Chile Propietario de tienda Chenza / Tres mujeres involucradas en el robo ciudadano negativo delincuencia Chile (La Moneda, Santiago) Presidente Gabriel Boric político negativo político Como vienen en el mismo orden que las noticias, podemos unir las columnas obtenidas a los datos originales:\ntabla \u0026lt;- muestra |\u0026gt; select(titulo, fecha) |\u0026gt; bind_cols(resultado) titulo fecha lugar actor actor_grupo sentimiento clasificacion Nuevo error en Gendarmería: imputado queda en libertad pese a que debía cumplir prisión preventiva 2025-12-30 Santiago Gendarmería de Chile institución pública negativo judicial Andes Iron acusa ofensiva política contra Dominga tras conocerse que enfrenta indagatoria penal 2025-12-30 Chile Andes Iron empresa negativo judicial Fiscalía acusó a Cathy Barriga por cuatro delitos y pide más de 23 años de cárcel 2025-12-30 Maipú, Región Metropolitana Cathy Barriga, exalcaldesa de Maipú político negativo judicial No fue Patricia Muñoz: Boric nombra a Macarena Cortés como la primera directora de la Defensoría de Víctimas 2025-12-30 Chile Gabriel Boric político positivo político \u0026ldquo;Ya tomaron las huellas\u0026rdquo;: dueño de tienda asaltada durante incendio en Mall Plaza Norte afirma que responsables fueron identificadas 2025-12-30 Mall Plaza Norte, Chile Propietario de tienda Chenza / Tres mujeres involucradas en el robo ciudadano negativo delincuencia Presidente Boric recibe cartas credenciales de cinco embajadores y cierra polémica con EEUU e Israel 2025-12-30 Chile (La Moneda, Santiago) Presidente Gabriel Boric político negativo político Si te interesa profundizar en este tema, Nic Crane ofrece un curso específico sobre el uso de LLMs con R\nReferencias RainbowR Workshop: LLMs in R for Data Analysis por Nic Crane Learn to work with LLMs in R, curso impartido por Nic Crane Documentación del paquete {ellmer} sobre datos estructurados ","date":"2026-03-07T00:00:00Z","excerpt":"Podemos usar la IA para transformar textos de cualquier tipo en datos estructurados. Esto significa que puedes convertir entrevistas, reseñas, opiniones, correos, noticias y más en bases de datos con la información que necesitas debidamente ordenada en filas y columnas. En ese tutorial usaremos modelos de lenguaje para extraer datos estructurados a partir de noticias reales.","href":"https://bastianoleah.netlify.app/blog/datos_estructurados_llm/","tags":"inteligencia artificial ; texto ; análisis de texto ; datos","title":"Extracción de datos estructurados desde texto usando IA"},{"content":"Chatear con modelos de lenguaje (LLM) o IAs —como se les llama coloquialmente— puede tener muchos usos para el análisis de datos:\nUsar IA para generar código de R para tus análisis, visualizaciones o exploraciones de datos Interpretar datos por medio de textos explicativos que describan tus análisis o resultados Convertir texto en datos estructurados, como entrevistas, noticias o contenido web Presentar tus datos a la IA para hacerle consultas y que te ayude a interpretar tus datos Todo esto puedes hacerlo desde tu navegador web, pero cuando analizamos datos puede ser más conveniente usar IA directamente desde R. Así podemos mantener una documentación de nuestro análisis, integrar IA en nuestro procesamiento de datos, usar IA de manera reproducible, y usar directamente los resultados de la IA en nuestros flujos de trabajo.\nVeamos cómo se puede interactuar con LLMs directamente desde R! 🤖\n¿Qué necesitaremos?\nTener acceso a un proveedor de modelos de lenguaje, o usar modelos de lenguaje locales Instalar un paquete para poder interactuar con modelos de lenguaje Configurar la API key de tu proveedor de IA en R Iniciar una conversación con la IA Proveedores de modelos de lenguaje Para usar modelos de lenguaje o IA en R, necesitas tener acceso a un proveedor de IA, o bien, instalar un modelo de IA localmente en tu computador.\nExisten proveedores de IA que te permiten usar sus modelos gratis mediante el navegador, pero nosotrxs queremos dar un uso más avanzado a la IA, y para eso necesitamos una API key o llave de API. Estas llaves nos permiten usar la IA en contextos distintos al típico chat, y suelen tener un costo asociado.\nAlgunos proveedores populares de IA son OpenAI (ChatGPT), Anthropic (Claude), Google (Gemini), GitHub Models, etc.\nNecesitas una cuenta en un proveedor de IA que te pueda entregar una API key para poder usarla en R. Si ya tienes una cuenta y una llave de API, puedes saltarte la siguiente sección y pasar a la subsiguiente.\nModelos de lenguaje locales Si tu computador tiene una tarjeta de video lo suficientemente grande (más de 8GB de memoria de video), si quieres usar IA gratis, o si prefieres no usar modelos en la nube por privacidad, puedes instalar un modelo de lenguaje local en tu computadora.\nEsto significa que descargas el modelo y tu propio computador lo ejecuta, a diferencia de usarlo en la nube por medio de un proveedor.\nUn modelo de lenguaje local solamente funcionará bien en un computador con más de 8GB de memoria de video (GPU), lo que deja fuera a la mayoría de los computadores. En general, todos los Mac con procesadores Apple Silicon (M1, M2, M3, M4…) cumplen este requisito, ya que se caracterizan por compartir la memoria RAM con la memoria de GPU, a diferencia de otros computadores que tienen memoria RAM y memoria de GPU separadas. Un modelo de lenguaje local tiene algunos beneficios:\nes gratis es totalmente privado no necesitas internet ¿ya mencioné que es gratis? Pero también tiene inconvenientes:\ndepende de las capacidades de tu computadora no es tan potente como los modelos en la nube Para instalar y ejecutar un modelo de lenguaje local, necesitamos:\nInstalar Ollama Hacer que R se comunique con Ollama Instalar un modelo de lenguaje local Instalar Ollama Primero tienes que instalar el software Ollama en tu equipo. Este programa permite que tu computador ejecute modelos de lenguaje locales.\nBajar Ollama Una vez instalado, tienes que abrir Ollama en tu computador!\nUsar Ollama desde R Ahora queremos que R se comunique con Ollama para poder usar sus modelos de lenguaje. Instalamos el paquete {ollamar}:\ninstall.packages(\u0026#34;ollamar\u0026#34;) Una vez instalado Ollama y {ollamar}, podemos instalar un modelo de lenguaje local.\nModelos de lenguaje locales Existen muchas alternativas de modelos, y entre ellos se diferencian principalmente por los datos de entrenamiento que se usaron para crearlos, y la cantidad de parámetros que tienen, que representan la cantidad de elementos aprendidos por el modelo, donde en general una mayor cantidad equivale a mejor capacidad para comprender textos, generar respuestas más precisas, y contar con mayor cononocimiento.\nEl modelo Llama 3.2 es pequeño y os moderadamente bueno para comprender textos complejos. Su versión de 3 billones de parámetros, llama3.2:3b, es liviana y potente. Si tu computador no es muy poderoso, existe la versión de Llama 3.1 con 1 billón de parámetros, llama3.1:1b, es más pequeño aún para tareas sencillas. Si tu computador tiene bastante memoria de video (más de 16GB), puedes instalar Llama 3.1, que tiene una versión de 8 billones de parámetros: llama3.1:8b Instalar un modelo de lenguaje local desde R Para instalar un modelo con {ollamar}, usamos el siguiente código en R:\nlibrary(ollamar) pull(\u0026#34;llama3.2:3b\u0026#34;) Ollama descargará e instalará el modelo en tu computador. Recuerda que es necesario tener la aplicación Ollama abierta en tu computadora, dado que ésta aplicación es la que le entrega el modelo de lenguaje a R.\nListo! Ahora tienes un modelo de lenguaje instalado localmente.\nInteractuar con IAs desde R {ellmer} es un paquete de R que facilita la interacción con modelos de lenguaje desde R, y se usa como el motor de muchos otros paquetes que usan IA.\nInstalamos el paquete:\ninstall.packages(\u0026#34;ellmer\u0026#34;) Ahora tenemos que configurar {ellmer} para que use tu modelo de lenguaje, ya sea un modelo local o un modelo en la nube.\nConfigurar el uso de un modelo de lenguaje en R Este paso es solamente si usas modelos de lenguaje desde la nube por medio de proveedores como OpenAI o Anthropic. Si usas un modelo de lenguaje local, puedes saltarte este paso. Como ya dijimos, para poder usar IA de proveedores en la nube necesitas tener una llave que le dice al proveedor que vas a usar tu cuenta fuera de su plataforma. Esto se hace mediante la llave de API.\nLas llaves de API son un código secreto que te entrega tu proveedor de IA, y básicamente es una especie de contraseña que te permite usar tu cuenta fuera de su plataforma web. Esto significa que es una clave privada! Si alguien más la usa, podría resultar en cobros para ti! Por eso, tenemos que guardar la API key de forma segura en un archivo de Entorno.\nEditar tu archivo de Entorno El archivo de Entorno es un script donde puedes guardar secretos que R puede leer, pero que no quedan guardados en tu código ni en tu proyecto, y por lo tanto quedan seguros. Este script contiene variables que se cargan cada vez que abrimos una sesión de R.\nEl archivo de entorno sirve para guardar variables secretas en un archivo que está afuera de tu proyecto de R, y que aplica para todos tus proyectos y sesiones de R: perfecto para guardar las API keys en tu computadora de forma segura y poder usarlas en todos tus proyectos.\nPara editar el archivo de entorno:\nusethis::edit_r_environ() Se abrirá el archivo .Renviron, donde podemos agregar una línea con la API key, por ejemplo:\nOPENAI_API_KEY=345345398475937434534539847593743453453984759374 Si tu proveedor es Claude (Anthropic), el nombre de la variable es ANTHROPIC_API_KEY, etc. Tienen que ir escritas con mayúscula, sin espacios, y sin comillas.\nUna vez guardadas las credenciales, reiniciamos la sesión de R (menú Session y luego Restart R) para que se lean las variables de entorno (siempre se leerán al iniciar R).\nCon esta variable de entorno, el paquete {ellmer} tendrá permiso para usar tu modelo de IA.\nIniciar una conversación con la IA Con tu modelo de lenguaje instalado localmente o con tu API key configurada, ya puedes empezar a interactuar con la IA desde R!\nEn un script, cargamos {ellmer}:\nlibrary(ellmer) Ahora usaremos una función para iniciar un chat. Estas funciones empiezan con chat_, y dependen de tu proveedor:\nSi usas un proveedor de IA en la nube, usa las funciones chat_openai(), chat_anthropic(), chat_gemini(), chat_github() o la que te corresponda. Si usar una IA local con Ollama, usa chat_ollama(). Creemos un chat usando ChatGPT:\n# crear sesión de chat chat \u0026lt;- chat_openai() Creamos un objeto chat que llevará nuestra conversación. Para iniciar la conversación, pasamos el texto de esta manera:\n# preguntar algo a la IA chat$chat(\u0026#34;¿Cuál es el animal más bonito del mundo? Finge que es el mapache y responde brevemente\u0026#34;) ¡El animal más bonito del mundo es el mapache! Sus grandes ojos brillantes, sus suaves patas y su característica \u0026#34;máscara\u0026#34; hacen que sea adorable y cautivador. Vemos que el modelo responde inmediatamente en la consola.\nPodemos continuar la conversación usando el mismo objeto chat otra vez, por lo que la IA podrá responder teniendo en cuenta todo lo que se ha dicho antes:\n# continuar la conversación chat$chat(\u0026#34;Después del mapache, cuál sería el segundo animal más bonito del mundo? Obviamente son los gatos\u0026#34;) Después del mapache, los gatos son el segundo animal más bonito del mundo. Su elegancia, mirada misteriosa y su suave pelaje hacen que sean absolutamente encantadores. Si ejecutamos el objeto chat por sí solo, veremos la conversación entera, un resumen de los tokens usados, y una estimación del costo total:\n\u0026lt;Chat OpenAI/gpt-4.1 turns=14 tokens=1524/284 $0.01\u0026gt; ── user [28] ──────────────────────────────────────────────────────────────────────────────────────── ¿Cuál es el animal más bonito del mundo? Finge que es el mapache y responde brevemente ── assistant [38] ─────────────────────────────────────────────────────────────────────────────────── ¡El animal más bonito del mundo es el mapache! Sus grandes ojos brillantes, sus suaves patas y su característica \u0026#34;máscara\u0026#34; hacen que sea adorable y cautivador. ── user [27] ──────────────────────────────────────────────────────────────────────────────────────── Después del mapache, cuál sería el segundo animal más bonito del mundo? Obviamente son los gatos ── assistant [40] ─────────────────────────────────────────────────────────────────────────────────── ¡Por supuesto! Después del mapache, los gatos son el segundo animal más bonito del mundo. Su elegancia, mirada misteriosa y su suave pelaje hacen que sean absolutamente encantadores. De esta forma podemos capturar las respuestas del chat en objetos de R y usarlos para los fines que deseemos:\nanimal \u0026lt;- chat$chat(\u0026#34;En una sola palabra: ¿cuál es la mascota más popular del mundo y que ronrronea?\u0026#34;) animal Gato. Otra forma de chatear con la IA en R es con un chat interactivo:\nlive_console(chat) De este modo la consola de R se vuelve en un chat donde escribimos y obtenemos respuestas de inmediato.\nUsos avanzados de modelos de lenguaje en R Con esta configuración inicial, ahora puedes pasar a usar la IA con R de maneras más avanzadas, como tener asistentes, generar código, interpretar resultados, analizar datos y más!\nExtraer datos estructurados desde textos libres {gander}, un asistente de código que escribe código de R y reemplaza código o comentarios con lo que le pidas Asistente de IA directo en RStudio que tiene acceso a tus datos, paquetes cargados, y archivos Entregar datos a una IA para generar textos explicativos, resúmenes, o interpretaciones de tus datos Análisis de sentimiento de textos con R Resumir textos desde R Análisis de datos en formato texto con {mall} Recursos Para más paquetes y herramientas de modelos de lenguaje e IA en R, revisa Large Language Model tools for R, de Luis D. Verde Arregoitia.\n","date":"2026-02-26T00:00:00Z","excerpt":"Chatear con modelos de lenguaje (LLM) o IAs puede tener muchos usos para el análisis de datos: generar código de R para tus análisis, visualizaciones o exploraciones de datos; interpretar datos que describan tus análisis o resultados; convertir texto en datos estructurados, como entrevistas, noticias o contenido web; hacerle consultas sobre tus datos y que te ayude a interpretar, y más. Usar IA directamente desde R ayuda a mantener una documentación de nuestro análisis, integrar IA en nuestro procesamiento de datos, usar IA de manera reproducible, y usar directamente los resultados de la IA en nuestros flujos de trabajo.","href":"https://bastianoleah.netlify.app/blog/ellmer/","tags":"inteligencia artificial","title":"Interactúa con modelos de lenguaje (LLM) y chatea con IAs en R"},{"content":"Una tarea común en al trabajar con datos (y también en la computación en general) es necesitar renombrar muchos archivos. R puede ayudarnos a realizar este tipo de tareas repetitivas, dado que, además de ser un lenguaje centrado en el análisis de datos, R también puede usarse para cualquier otro propósito!\nInspeccionar directorios y archivos con R Hagamos como que tenemos una carpeta llena de archivos (pueden ser imágenes, documentos PDF, u otros documentos), en este caso la carpeta se llama reportes y dentro tiene 10 documentos PDF.\nUsamos el paquete {fs} para ver los archivos dentro de la carpeta:\n# install.packages(\u0026#34;fs\u0026#34;) library(fs) # rutas de archivos rutas \u0026lt;- dir_ls(\u0026#34;reportes\u0026#34;) rutas reportes/Reporte 2025 comuna Camarones.pdf reportes/Reporte 2025 comuna Camiña.pdf reportes/Reporte 2025 comuna Canela.pdf reportes/Reporte 2025 comuna Cañete.pdf reportes/Reporte 2025 comuna Carahue.pdf reportes/Reporte 2025 comuna Cartagena.pdf reportes/Reporte 2025 comuna Castro.pdf reportes/Reporte 2025 comuna Catemu.pdf reportes/Reporte 2025 comuna Cauquenes.pdf reportes/Reporte 2025 comuna Cerrillos.pdf Obtenemos un vector con las rutas de los archivos, es decir, el lugar en nuestro computador donde se ubican los archivos1.\nEstas rutas representan los archivos, y con ellas podemos hacer distintas operaciones.\nVeamos cómo podemos usar R para renombrar archivos:\nCambiar nombres Para cambiar el nombre de un archivo, en realidad lo que hacemos es moverlo pero con un nombre distinto. Para eso usamos la función file_move(), que recibe como argumentos la ruta del archivo que queremos mover, y la ruta nueva, que sería el mismo lugar pero con un nombre distinto.\nRenombremos un sólo archivo desde R:\n# mover un sólo archivo file_move(path = \u0026#34;reportes/Reporte 2025 comuna Cartagena.pdf\u0026#34;, new_path = \u0026#34;reportes/reporte_camarones.pdf\u0026#34;) Y revisemos el resultado:\ndir_ls(\u0026#34;reportes\u0026#34;) reportes/Reporte 2025 comuna Camarones.pdf reportes/Reporte 2025 comuna Camiña.pdf reportes/Reporte 2025 comuna Canela.pdf reportes/Reporte 2025 comuna Cañete.pdf reportes/Reporte 2025 comuna Carahue.pdf reportes/Reporte 2025 comuna Castro.pdf reportes/Reporte 2025 comuna Catemu.pdf reportes/Reporte 2025 comuna Cauquenes.pdf reportes/Reporte 2025 comuna Cerrillos.pdf reportes/reporte_camarones.pdf Ahora, si queremos renombrar todos los archivos de la carpeta, podemos crear una tabla que tenga como primera variable los nombres de los archivos, y como segunda variable los nuevos nombres.\nAquí nos servirá mucho el paquete {stringr} para manipulación de texto. Algunas funciones útiles para limpieza de texto son:\nstr_replace() para reemplazar partes del texto por otros textos str_remove() para eliminar partes del texto Creemos una tabla con el vector de rutas usando la función tibble():\nlibrary(dplyr) # crear tabla con las rutas archivos \u0026lt;- tibble(ruta = rutas) archivos # A tibble: 10 × 1 ruta \u0026lt;fs::path\u0026gt; 1 reportes/Reporte 2025 comuna Camarones.pdf 2 reportes/Reporte 2025 comuna Camiña.pdf 3 reportes/Reporte 2025 comuna Canela.pdf 4 reportes/Reporte 2025 comuna Cañete.pdf 5 reportes/Reporte 2025 comuna Carahue.pdf 6 reportes/Reporte 2025 comuna Cartagena.pdf 7 reportes/Reporte 2025 comuna Castro.pdf 8 reportes/Reporte 2025 comuna Catemu.pdf 9 reportes/Reporte 2025 comuna Cauquenes.pdf 10 reportes/Reporte 2025 comuna Cerrillos.pdf Ahora creemos una nueva columna con mutate() que tenga versiones modificadas de los nombres: cambiemos el texto \u0026ldquo;Reporte 2025\u0026rdquo; en el nombre de cada archivo por \u0026ldquo;Documento\u0026rdquo; y así creamos una nueva ruta:\nlibrary(stringr) archivos \u0026lt;- archivos |\u0026gt; mutate(ruta_nueva = str_replace(ruta, pattern = \u0026#34;Reporte 2025\u0026#34;, replacement = \u0026#34;Documento\u0026#34;)) archivos # A tibble: 10 × 2 ruta ruta_nueva \u0026lt;fs::path\u0026gt; \u0026lt;chr\u0026gt; 1 reportes/Reporte 2025 comuna Camarones.pdf reportes/Documento comuna Camaron… 2 reportes/Reporte 2025 comuna Camiña.pdf reportes/Documento comuna Camiña.… 3 reportes/Reporte 2025 comuna Canela.pdf reportes/Documento comuna Canela.… 4 reportes/Reporte 2025 comuna Cañete.pdf reportes/Documento comuna Cañete.… 5 reportes/Reporte 2025 comuna Carahue.pdf reportes/Documento comuna Carahue… 6 reportes/Reporte 2025 comuna Cartagena.pdf reportes/Documento comuna Cartage… 7 reportes/Reporte 2025 comuna Castro.pdf reportes/Documento comuna Castro.… 8 reportes/Reporte 2025 comuna Catemu.pdf reportes/Documento comuna Catemu.… 9 reportes/Reporte 2025 comuna Cauquenes.pdf reportes/Documento comuna Cauquen… 10 reportes/Reporte 2025 comuna Cerrillos.pdf reportes/Documento comuna Cerrill… Entonces tenemos las rutas originales con las rutas nuevas.\nSabemos que con file_move() podemos mover cada archivo a su nuevo nombre. Pero ahora tenemos una tabla con dos columnas, así que podemos mover todos los archivos al mismo tiempo!\nUsaremos las columnas como vectores para aplicar la función a todos los archivos. En el primer argumento (path) insertamos la columna con las rutas originales (archivos$ruta), y en el segundo argumento (new_path) insertamos la columna con las rutas nuevas (archivos$ruta_nueva):\n# renombrar todos los archivos file_move(path = archivos$ruta, new_path = archivos$ruta_nueva) Lo que hicimos fue vectorizar la función, haciendo que se aplique para todos los elementos de los vectores (ambos del mismo largo) que entregamos!\nRevisemos el cambio que hicimos:\ndir_ls(\u0026#34;reportes\u0026#34;) reportes/Documento comuna Camarones.pdf reportes/Documento comuna Camiña.pdf reportes/Documento comuna Canela.pdf reportes/Documento comuna Cañete.pdf reportes/Documento comuna Carahue.pdf reportes/Documento comuna Cartagena.pdf reportes/Documento comuna Castro.pdf reportes/Documento comuna Catemu.pdf reportes/Documento comuna Cauquenes.pdf reportes/Documento comuna Cerrillos.pdf Funcionó!\nEliminar palabras de los nombres Usamos la función str_remove() si queremos eliminar palabras, textos o patrones específicos de los nombres de archivos. Luego usamos str_squish() para eliminar todos los espacios repetidos que pueden qeudar por la eliminación de palabras:\n# obtener rutas rutas \u0026lt;- dir_ls(\u0026#34;reportes\u0026#34;) # crear tabla con las rutas archivos \u0026lt;- tibble(ruta = rutas) archivos \u0026lt;- archivos |\u0026gt; mutate(ruta_nueva = str_remove(ruta, pattern = \u0026#34;comuna\u0026#34;), ruta_nueva = str_squish(ruta_nueva)) archivos # A tibble: 10 × 2 ruta ruta_nueva \u0026lt;fs::path\u0026gt; \u0026lt;chr\u0026gt; 1 reportes/Documento comuna Camarones.pdf reportes/Documento Camarones.pdf 2 reportes/Documento comuna Camiña.pdf reportes/Documento Camiña.pdf 3 reportes/Documento comuna Canela.pdf reportes/Documento Canela.pdf 4 reportes/Documento comuna Cañete.pdf reportes/Documento Cañete.pdf 5 reportes/Documento comuna Carahue.pdf reportes/Documento Carahue.pdf 6 reportes/Documento comuna Cartagena.pdf reportes/Documento Cartagena.pdf 7 reportes/Documento comuna Castro.pdf reportes/Documento Castro.pdf 8 reportes/Documento comuna Catemu.pdf reportes/Documento Catemu.pdf 9 reportes/Documento comuna Cauquenes.pdf reportes/Documento Cauquenes.pdf 10 reportes/Documento comuna Cerrillos.pdf reportes/Documento Cerrillos.pdf Y nuevamente aplicamos los cambios a los archivos:\n# renombrar todos los archivos file_move(path = archivos$ruta, new_path = archivos$ruta_nueva) # revisar cambios dir_ls(\u0026#34;reportes\u0026#34;) reportes/Documento Camarones.pdf reportes/Documento Camiña.pdf reportes/Documento Canela.pdf reportes/Documento Cañete.pdf reportes/Documento Carahue.pdf reportes/Documento Cartagena.pdf reportes/Documento Castro.pdf reportes/Documento Catemu.pdf reportes/Documento Cauquenes.pdf reportes/Documento Cerrillos.pdf Limpiar nombres Si trabajamos con archivos que van a subirse a internet o a algún servicio, se recomienda limpiar los nombres de archivos para evitar problemas de compatibilidad: evitar espacios, tildes, eñes, mayúsculas, etc.\nPodemos usar la función make_clean_names() del paquete {janitor} para limpiar los nombres de los archivos. Pero para hacerlo más ordenado, podemos sacar las rutas para trabajar solamente sobre los nombres de los archivos: luego de dir_ls() para obtener las rutas, usamos path_file() para quedarnos sólo con los nombres, y así no afectamos a las carpetas con la limpieza.\nlibrary(fs) # lista con nombres de archivos sin ruta ruta_archivos \u0026lt;- \u0026#34;reportes\u0026#34; nombres_archivos \u0026lt;- dir_ls(ruta_archivos) |\u0026gt; path_file() nombres_archivos [1] \u0026quot;Documento Camarones.pdf\u0026quot; \u0026quot;Documento Camiña.pdf\u0026quot; [3] \u0026quot;Documento Canela.pdf\u0026quot; \u0026quot;Documento Cañete.pdf\u0026quot; [5] \u0026quot;Documento Carahue.pdf\u0026quot; \u0026quot;Documento Cartagena.pdf\u0026quot; [7] \u0026quot;Documento Castro.pdf\u0026quot; \u0026quot;Documento Catemu.pdf\u0026quot; [9] \u0026quot;Documento Cauquenes.pdf\u0026quot; \u0026quot;Documento Cerrillos.pdf\u0026quot; Luego, creamos la tabla con tibble(), y separamos el nombre del archivo de su extensión (.pdf, .xlsx, etc.) con separate(), para poder limpiar el nombre sin afectar la extensión. Luego usamos make_clean_names() del paquete {janitor} para limpiar los nombres:\nlibrary(tidyr) # para separar columnas a partir de un separador library(dplyr) # para crear nuevas columnas library(janitor) # para limpiar nombres archivos \u0026lt;- tibble(archivo = nombres_archivos) |\u0026gt; # separar nombres de archivos de su extensión separate(archivo, sep = \u0026#34;\\\\.\u0026#34;, into = c(\u0026#34;nombre\u0026#34;, \u0026#34;extension\u0026#34;)) |\u0026gt; # limpiar nombres mutate(nombre_nuevo = janitor::make_clean_names(nombre)) archivos # A tibble: 10 × 3 nombre extension nombre_nuevo \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Documento Camarones pdf documento_camarones 2 Documento Camiña pdf documento_camina 3 Documento Canela pdf documento_canela 4 Documento Cañete pdf documento_canete 5 Documento Carahue pdf documento_carahue 6 Documento Cartagena pdf documento_cartagena 7 Documento Castro pdf documento_castro 8 Documento Catemu pdf documento_catemu 9 Documento Cauquenes pdf documento_cauquenes 10 Documento Cerrillos pdf documento_cerrillos Nótese que las eñes se reemplazaron por enes, y las letras con tilde se cambiaron por sus versiones sin tilde.\nEn la tabla podemos ver que separamos los nombres en distintas columnas. Ahora, usamos estas columnas para reconstruir las rutas originales y las nuevas rutas con los nombres limpios:\narchivos \u0026lt;- archivos |\u0026gt; # reconstruir ruta original mutate(ruta = paste(ruta_archivos, nombre, sep = \u0026#34;/\u0026#34;), ruta = paste(ruta, extension, sep = \u0026#34;.\u0026#34;)) |\u0026gt; # crear ruta nueva mutate(ruta_nueva = paste(ruta_archivos, nombre_nuevo, sep = \u0026#34;/\u0026#34;), ruta_nueva = paste(ruta_nueva, extension, sep = \u0026#34;.\u0026#34;)) |\u0026gt; # ordenar select(ruta, ruta_nueva) archivos # A tibble: 10 × 2 ruta ruta_nueva \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 reportes/Documento Camarones.pdf reportes/documento_camarones.pdf 2 reportes/Documento Camiña.pdf reportes/documento_camina.pdf 3 reportes/Documento Canela.pdf reportes/documento_canela.pdf 4 reportes/Documento Cañete.pdf reportes/documento_canete.pdf 5 reportes/Documento Carahue.pdf reportes/documento_carahue.pdf 6 reportes/Documento Cartagena.pdf reportes/documento_cartagena.pdf 7 reportes/Documento Castro.pdf reportes/documento_castro.pdf 8 reportes/Documento Catemu.pdf reportes/documento_catemu.pdf 9 reportes/Documento Cauquenes.pdf reportes/documento_cauquenes.pdf 10 reportes/Documento Cerrillos.pdf reportes/documento_cerrillos.pdf Renombramos todos los archivos usando las columnas de la tabla:\n# renombrar todos los archivos file_move(archivos$ruta, archivos$ruta_nueva) Claramente, todo lo anterior se puede hacer sin pasos intermedios, pero lo puse en varios pasos por motivos pedagógicos.\nAsí, podemos usar R para renombrar cientos o miles de archivos. Podemos usar este código básico para hacer cosas mucho más complejas, como cargar cada archivo y poner en su nombre algo relacionado a sus datos, crear nombres condicionales, navegar estructuras de directorios y organizar los archivos por carpetas, y más. Pero eso queda para otro tutorial! Si te interesa hacer alguna de estas cosas o algo similar, escríbeme para motivarme a escribir otro post ☺️\nConsidera que las rutas dependen del lugar desde donde se ejecute R: recuerda usar Proyectos! para tener control y orden sobre dónde está trabajando R!\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2026-02-25T00:00:00Z","excerpt":"Una tarea común en al trabajar con datos (y también en la computación en general) es necesitar renombrar muchos archivos. Podemos usar R para automatizar este tipo de tareas repetitivas; un ejemplo de que R no es sólo un lenguaje para análisis de datos.","href":"https://bastianoleah.netlify.app/blog/2026-02-25/","tags":"automatización ; texto ; limpieza de datos","title":"Renombrar archivos desde R"},{"content":" Sitio web con visualizaciones que exploran los datos de egresos médicos del Ministerio de Salud de Chile, distinguiendo entre intentos de suicidio y suicidios consumados.\nLos gráficos buscan describir las diferencias de género en el fenómeno del suicidio, mostrando desigualdades en la cantidad de intentos y en las víctimas fatales, pero también en los métodos utilizados por hombres y mujeres.\nLos datos de egresos médicos incluyen el diagnóstico de las y los pacientes, su género, su ubicación, y si sobrevivieron o no a sus aflicciones. Según el diagnóstico de cada paciente, es posible identificar casos de lesiones autoinfligidas intencionalmente, interpretables como intentos de suicidio. En base al egreso con vida (alta médica) o sin vida (fallecimiento) de los pacientes, se pueden distinguir intentos de suicidio y suicidios consumados.\nLa aplicación fue desarrollada con Quarto y R para el procesamiento de datos y visualizaciones.\nVisita la aplicación Fuentes Ministerio de Salud, Egresos hospitalarios Datos abiertos, Departamento de Estadísticas e Información de Salud ","date":"2026-02-21T00:00:00Z","excerpt":"Conjunto de visualizaciones que exploran los datos de egresos médicos del Ministerio de Salud de Chile, distinguiendo entre intentos de suicidio y suicidios consumados. Los gráficos buscan describir las diferencias de género en el fenómeno del suicidio, mostrando desigualdades en la cantidad de intentos y en las víctimas fatales, pero también en los métodos utilizados por hombres y mujeres.","href":"https://bastianoleah.netlify.app/blog/app_suicidios_genero/","tags":"apps ; Chile ; datos ; gráficos","title":"App: Suicidios en Chile (2017-2024) desde una perspectiva de género"},{"content":" Como vimos en un post anterior, podemos usar funcionalidades de R para generar párrafos de texto que se basen en datos, para integrar cifras y otros valores de un dataframe en un párrafo diseñado para resumir o presentar la información.\nSi bien esto produce textos de máxima precisión y nos entrega todo el control sobre el texto generado, también existen herramientas de inteligencia artificial que pueden facilitar este trabajo.\nAl usar IA siempre debemos recordar que obtendremos resultados probabilísticos; es decir que serán resultados que pueden variar, pueden contener errores, y que no son reproducibles. Existen paquetes que hacen muy fácil usar IA en R!\nIgual que en el post anterior, usaremos un conjunto de datos demográficos: los resultados del Censo de población y vivienda 2024 de Chile, específicamente la población por género en cada región del país.\nDescargar datos Ver código de la limpieza de los datos library(dplyr) library(janitor) library(readxl) library(tidyr) # cargar genero \u0026lt;- read_xlsx(\u0026#34;P5_Genero.xlsx\u0026#34;, sheet = 2) # limpiar genero_limpio \u0026lt;- genero |\u0026gt; row_to_names(3) |\u0026gt; filter(!is.na(Región)) # transformar a largo genero_long \u0026lt;- genero_limpio |\u0026gt; pivot_longer(cols = 4:last_col(), names_to = \u0026#34;genero\u0026#34;, values_to = \u0026#34;poblacion\u0026#34;) |\u0026gt; rename(total = 3) # convertir variables y calcular porcentajes genero_porcentaje \u0026lt;- genero_long |\u0026gt; mutate(poblacion = as.numeric(poblacion), total = as.numeric(total)) |\u0026gt; clean_names() |\u0026gt; mutate(porcentaje = poblacion / total) # filtrar genero \u0026lt;- genero_porcentaje |\u0026gt; filter(region != \u0026#34;País\u0026#34;) Luego de limpiar estos datos, vemos columnas con las regiones del país, la población regional, los distintos géneros consultados en el censo, la cantidad de personas y su porcentaje:\nlibrary(dplyr) datos \u0026lt;- genero |\u0026gt; filter(region == \u0026#34;Valparaíso\u0026#34;) datos # A tibble: 7 × 6 codigo_region region total genero poblacion porcentaje \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 5 Valparaíso 1505034 Masculino 704691 0.468 2 5 Valparaíso 1505034 Femenino 780209 0.518 3 5 Valparaíso 1505034 Transmasculino 3003 0.00200 4 5 Valparaíso 1505034 Transfemenino 1369 0.000910 5 5 Valparaíso 1505034 No binario 2161 0.00144 6 5 Valparaíso 1505034 Otro 624 0.000415 7 5 Valparaíso 1505034 Prefiere no responder/N… 12977 0.00862 Para ver más claramente los datos, aquí una tabla:\nregion total genero poblacion porcentaje Valparaíso 1505034 Masculino 704691 0.4682226 Valparaíso 1505034 Femenino 780209 0.5183996 Valparaíso 1505034 Transmasculino 3003 0.0019953 Valparaíso 1505034 Transfemenino 1369 0.0009096 Valparaíso 1505034 No binario 2161 0.0014358 Valparaíso 1505034 Otro 624 0.0004146 Valparaíso 1505034 Prefiere no responder/No sabe 12977 0.0086224 Ahora que tenemos los datos, pasemos a lo interesante!\nConfigurar un modelo de IA en R con {ellmer} Puedes encontrar instrucciones más detalladas sobre configurar el uso de IA en R en esta publicación. Usaremos un modelo de lenguaje (LLM) en R por medio del paquete {ellmer}.\ninstall.packages(\u0026#34;ellmer\u0026#34;) {ellmer} es un paquete de R que facilita la interacción con modelos de lenguaje desde R, y se usa como el motor de muchos otros paquetes que usan IA. Antes que nada, para poder usar la IA en R tienes que tener una llave API de tu proveedor de IA (OpenAI, Anthropic, GitHub Models, etc.) configurada en tus variables de entorno, como se explica en la documentación de {ellmer}.\nEn mi caso, uso GitHub Copilot, que me da acceso a GitHub Models, que a su vez me da acceso a varias IA como ChatGPT y Claude.\nInstrucciones para configurar tu proveedor de IA en R En resumen, ejecuta usethis::edit_r_environ() para abrir tu archivo .Renviron, donde se pueden guardar secretos que se aplican a todas tus sesiones de R pero quedan ocultos, y agrega una línea con la API key, por ejemplo:\nOPENAI_API_KEY=345345398475937434534539847593743453453984759374 Si tu proveedor es Claude (Anthropic), el nombre de la variable es ANTHROPIC_API_KEY, etc.\nLuego de esto podrás usar las funciones chat_openai(), chat_anthropic() o la que te corresponda, porque estas funciones simplemente buscarán que tengas la API key para poder funcionar. De lo contrario, te aparecerá un mensaje en la consola o instrucciones para configurar las llaves.\nPuedes probar si R puede acceder al modelo de IA iniciando un chat de prueba. Primero creamos una sesión con chat_openai(), chat_anthropic(), o la función que corresponda a tu proveedor de IA, y luego usamos el objeto resultante para mantener una conversación con la IA.\nlibrary(ellmer) # crear sesión de chat chat \u0026lt;- chat_github() # o chat_openai(), chat_anthropic(), etc. # preguntar algo a la IA chat$chat(\u0026#34;¿Cuál es el animal más bonito del mundo? Finge que es el mapache\u0026#34;) El mapache, sin duda, es el animal más bonito del mundo. Con su adorable carita enmascarada, sus ojos grandes y curiosos, y sus pequeñas manos habilidosas, el mapache conquista corazones en todo el planeta.\n🦝 Si nuestro modelo está funcionando, pasemos a su configuración para ponerlo a hacer algo útil!\nInterpretar datos con IA desde R Para generar texto a partir de datos desde R usando modelos de lenguaje necesitamos dos cosas:\nUn prompt que haga que el modelo de lenguaje interprete los datos correctamente Una tabla de datos que el modelo pueda interpretar. Configurar el modelo para interpretar texto El prompt es simplemente una instrucción para el modelo de lenguaje. Procura usar conceptos claros y relacionados al output que esperas (en este caso, palabras como demográfico, estadísticas, etc.):\nprompt \u0026lt;- \u0026#34;Eres un interpretador de datos sobre demográficos. Tu rol es entregar una interpretación clara y precisa de tablas de datos poblacionales. Responde solamente con una interpretación de los datos estadísticos que resuma la información y exponga los puntos clave, escrito en un sólo párrafo conciso.\u0026#34; Luego, al momento de crear el chat en R con la función chat_openai() o equivalente, podemos entregar este prompt como system prompt; es decir, como una instrucción general que coordina el funcionamiento del modelo:\nchat \u0026lt;- chat_github(model = \u0026#34;openai/gpt-5\u0026#34;, system_prompt = prompt) Preparar los datos para la IA Ahora que tenemos el modelo configurado, necesitamos preparar los datos para que el modelo pueda interpretarlos.\nAlgunos modelos y servicios permiten recibir datos directamente, pero en este caso entregaremos los datos como texto.\nPara esto, usamos la función kable() del paquete {knitr} para convertir nuestro dataframe en una tabla de texto (en formato markdown) que el modelo pueda leer:\ntabla \u0026lt;- knitr::kable(datos) tabla |region | total|genero | poblacion| porcentaje| |:----------|-------:|:-----------------------------|---------:|----------:| |Valparaíso | 1505034|Masculino | 704691| 0.4682226| |Valparaíso | 1505034|Femenino | 780209| 0.5183996| |Valparaíso | 1505034|Transmasculino | 3003| 0.0019953| |Valparaíso | 1505034|Transfemenino | 1369| 0.0009096| |Valparaíso | 1505034|No binario | 2161| 0.0014358| |Valparaíso | 1505034|Otro | 624| 0.0004146| |Valparaíso | 1505034|Prefiere no responder/No sabe | 12977| 0.0086224| Otra alternativa popular es convertir los datos a JSON, un formato de datos no estructurados que, en teoría, los modelos de lenguaje saben interpretar mejor:\ntabla \u0026lt;- jsonlite::toJSON(datos) tabla [{\u0026#34;region\u0026#34;:\u0026#34;Valparaíso\u0026#34;,\u0026#34;total\u0026#34;:1505034,\u0026#34;genero\u0026#34;:\u0026#34;Masculino\u0026#34;,\u0026#34;poblacion\u0026#34;:704691,\u0026#34;porcentaje\u0026#34;:0.4682},{\u0026#34;region\u0026#34;:\u0026#34;Valparaíso\u0026#34;,\u0026#34;total\u0026#34;:1505034,\u0026#34;genero\u0026#34;:\u0026#34;Femenino\u0026#34;,\u0026#34;poblacion\u0026#34;:780209,\u0026#34;porcentaje\u0026#34;:0.5184},{\u0026#34;region\u0026#34;:\u0026#34;Valparaíso\u0026#34;,\u0026#34;total\u0026#34;:1505034,\u0026#34;genero\u0026#34;:\u0026#34;Transmasculino\u0026#34;,\u0026#34;poblacion\u0026#34;:3003,\u0026#34;porcentaje\u0026#34;:0.002},{\u0026#34;region\u0026#34;:\u0026#34;Valparaíso\u0026#34;,\u0026#34;total\u0026#34;:1505034,\u0026#34;genero\u0026#34;:\u0026#34;Transfemenino\u0026#34;,\u0026#34;poblacion\u0026#34;:1369,\u0026#34;porcentaje\u0026#34;:0.0009},{\u0026#34;region\u0026#34;:\u0026#34;Valparaíso\u0026#34;,\u0026#34;total\u0026#34;:1505034,\u0026#34;genero\u0026#34;:\u0026#34;No binario\u0026#34;,\u0026#34;poblacion\u0026#34;:2161,\u0026#34;porcentaje\u0026#34;:0.0014},{\u0026#34;region\u0026#34;:\u0026#34;Valparaíso\u0026#34;,\u0026#34;total\u0026#34;:1505034,\u0026#34;genero\u0026#34;:\u0026#34;Otro\u0026#34;,\u0026#34;poblacion\u0026#34;:624,\u0026#34;porcentaje\u0026#34;:0.0004},{\u0026#34;region\u0026#34;:\u0026#34;Valparaíso\u0026#34;,\u0026#34;total\u0026#34;:1505034,\u0026#34;genero\u0026#34;:\u0026#34;Prefiere no responder/No sabe\u0026#34;,\u0026#34;poblacion\u0026#34;:12977,\u0026#34;porcentaje\u0026#34;:0.0086}] Obtener la interpretación datos El último paso es simplemente entregar la tabla de texto al chat, el cual retornará su interpretación, dado que ya le dimos la instrucción en el system prompt al crear el chat:\nresultado \u0026lt;- chat$chat(tabla) resultado En la Región de Valparaíso (total 1.505.034 personas), la población es mayoritariamente femenina con 780.209 personas (≈51,84%), seguida por 704.691 hombres (≈46,82%); las identidades trans y no binarias suman proporciones pequeñas pero presentes: transmasculino 3.003 (≈0,20%), transfemenino 1.369 (≈0,09%), no binario 2.161 (≈0,14%) y \u0026ldquo;otro\u0026rdquo; 624 (≈0,04%); además, 12.977 personas (≈0,86%) prefieren no responder o no saben, configurando un perfil predominantemente femenino con baja no respuesta y diversidad de género minoritaria.\nObtuvimos una interpretación clara de los datos, con cifras exactas, con muy poco esfuerzo 🤖\n⛅️ En algún lugar lejano, 6 litros de agua fueron consumidos por un datacenter 🏭\nLo malo de esto es que no hay garantías de que al pedirle algo similar al modelo obtengamos un resultado equivalente. Sin embargo, existen opciones:\nEntregar el output de la IA como un ejemplo para que el modelo de lenguaje se guíe en su siguiente respuesta, siguiendo la anterior. Usar el modelo de lenguaje para pedirle código de R que genere los textos de descripción 💡😲 Usar IA para obtener código de R Cuando usamos IA para analizar datos, muchas veces pedirle a la IA código que produzca los resultados es mejor que pedirle los resultados directamente!\nEsto se debe a que el código es reproducible, mientras que los resultados de la IA pueden variar cada vez que se los pidas. Además, el código te da control total sobre cómo se generan los resultados, y puedes modificarlo para ajustarlo a tus necesidades.\nPrimero tomamos el párrafo de texto que obtuvimos anteriormente como ejemplo para la IA:\nejemplo \u0026lt;- \u0026#34;En la Región de Valparaíso (total 1.505.034 personas), la población es mayoritariamente femenina con 780.209 personas (≈51,84%), seguida por 704.691 hombres (≈46,82%); las identidades trans y no binarias suman proporciones pequeñas pero presentes: transmasculino 3.003 (≈0,20%), transfemenino 1.369 (≈0,09%), no binario 2.161 (≈0,14%) y “otro” 624 (≈0,04%); además, 12.977 personas (≈0,86%) prefieren no responder o no saben, configurando un perfil predominantemente femenino con baja no respuesta y diversidad de género minoritaria.\u0026#34; Luego escribimos un system prompt distinto, que explicite nuestro nuevo objetivo:\nchat \u0026lt;- chat_github(model = \u0026#34;openai/gpt-5\u0026#34;, system_prompt = \u0026#34;Eres un desarrollador de R experto en ciencias sociales y estadísticas, con experiencia en paquetes del Tidyverse. Entrega respuestas en código de R.\u0026#34;) Luego creamos un prompt que integre nuestra nueva instrucción, el ejemplo de párrafo que esperamos, y los datos nuevos:\nresultado \u0026lt;- chat$chat( paste(\u0026#34;Entrégame código de R que redacte un párrafo similar al siguiente, a partir de la tabla de datos que te adjunto:\u0026#34;, \u0026#34;Ejemplo del texto:\u0026#34;, ejemplo, \u0026#34;Tabla de datos\u0026#34;, tabla) ) El resultado es una función bastante extensa, aunque dudo que sea mejor que mi propia función artesanal, hecha a mano, vegana, y libre de consumo de agua 😌\nVer código para generar párrafos de texto entregado por la IA library(dplyr) library(stringr) library(glue) library(scales) library(tibble) # Función para redactar el párrafo redactar_parrafo \u0026lt;- function(df, region = NULL) { df1 \u0026lt;- df %\u0026gt;% { if (!is.null(region)) filter(., region == !!region) else . } %\u0026gt;% mutate(genero = as.character(genero)) if (nrow(df1) == 0) return(NA_character_) total \u0026lt;- df1$total %\u0026gt;% unique() %\u0026gt;% `[`(1) total_fmt \u0026lt;- number(total, accuracy = 1, big.mark = \u0026#34;.\u0026#34;, decimal.mark = \u0026#34;,\u0026#34;) # Masculino y Femenino mf \u0026lt;- df1 %\u0026gt;% filter(genero %in% c(\u0026#34;Masculino\u0026#34;,\u0026#34;Femenino\u0026#34;)) %\u0026gt;% select(genero, poblacion, porcentaje) if (nrow(mf) == 0) stop(\u0026#34;No se encontraron filas para Masculino/Femenino.\u0026#34;) if (nrow(mf) == 2) { mayor \u0026lt;- mf %\u0026gt;% arrange(desc(porcentaje)) %\u0026gt;% slice(1) menor \u0026lt;- mf %\u0026gt;% arrange(desc(porcentaje)) %\u0026gt;% slice(2) } else { mayor \u0026lt;- mf %\u0026gt;% slice(1) menor \u0026lt;- NULL } adj_map \u0026lt;- c(\u0026#34;Femenino\u0026#34; = \u0026#34;femenina\u0026#34;, \u0026#34;Masculino\u0026#34; = \u0026#34;masculina\u0026#34;) sus_map \u0026lt;- c(\u0026#34;Femenino\u0026#34; = \u0026#34;mujeres\u0026#34;, \u0026#34;Masculino\u0026#34; = \u0026#34;hombres\u0026#34;) mayor_adj \u0026lt;- adj_map[mayor$genero] mayor_pop \u0026lt;- number(mayor$poblacion, accuracy = 1, big.mark = \u0026#34;.\u0026#34;, decimal.mark = \u0026#34;,\u0026#34;) mayor_pct \u0026lt;- percent(mayor$porcentaje, accuracy = 0.01, decimal.mark = \u0026#34;,\u0026#34;) if (!is.null(menor)) { menor_sus \u0026lt;- sus_map[menor$genero] menor_pop \u0026lt;- number(menor$poblacion, accuracy = 1, big.mark = \u0026#34;.\u0026#34;, decimal.mark = \u0026#34;,\u0026#34;) menor_pct \u0026lt;- percent(menor$porcentaje, accuracy = 0.01, decimal.mark = \u0026#34;,\u0026#34;) } # Otras identidades (excluye no respuesta) otros \u0026lt;- df1 %\u0026gt;% filter(!genero %in% c(\u0026#34;Masculino\u0026#34;,\u0026#34;Femenino\u0026#34;,\u0026#34;Prefiere no responder/No sabe\u0026#34;)) %\u0026gt;% mutate( etiqueta = case_when( genero == \u0026#34;Otro\u0026#34; ~ \u0026#34;“otro”\u0026#34;, TRUE ~ str_to_lower(genero) ), pieza = glue(\u0026#34;{etiqueta} {number(poblacion, accuracy=1, big.mark=\u0026#39;.\u0026#39;, decimal.mark=\u0026#39;,\u0026#39;)} (≈{percent(porcentaje, accuracy=0.01, decimal.mark=\u0026#39;,\u0026#39;)})\u0026#34;) ) otros_str \u0026lt;- if (nrow(otros) \u0026gt; 0) { v \u0026lt;- otros$pieza if (length(v) == 1) v else paste0(paste(v[-length(v)], collapse = \u0026#34;, \u0026#34;), \u0026#34; y \u0026#34;, v[length(v)]) } else \u0026#34;\u0026#34; # No respuesta nr \u0026lt;- df1 %\u0026gt;% filter(genero == \u0026#34;Prefiere no responder/No sabe\u0026#34;) nr_pieza \u0026lt;- if (nrow(nr) == 1) { nr_pop \u0026lt;- number(nr$poblacion, accuracy = 1, big.mark = \u0026#34;.\u0026#34;, decimal.mark = \u0026#34;,\u0026#34;) nr_pct \u0026lt;- percent(nr$porcentaje, accuracy = 0.01, decimal.mark = \u0026#34;,\u0026#34;) glue(\u0026#34;{nr_pop} personas (≈{nr_pct})\u0026#34;) } else NA_character_ nr_qual \u0026lt;- if (nrow(nr) == 1) { if (nr$porcentaje \u0026lt; 0.02) \u0026#34;baja\u0026#34; else if (nr$porcentaje \u0026lt; 0.05) \u0026#34;moderada\u0026#34; else \u0026#34;alta\u0026#34; } else \u0026#34;—\u0026#34; region_nom \u0026lt;- unique(df1$region)[1] parte_mf \u0026lt;- if (!is.null(menor)) { glue(\u0026#34;la población es mayoritariamente {mayor_adj} con {mayor_pop} personas (≈{mayor_pct}), seguida por {menor_pop} {menor_sus} (≈{menor_pct});\u0026#34;) } else { glue(\u0026#34;la población reportada es {mayor_adj} con {mayor_pop} personas (≈{mayor_pct});\u0026#34;) } parte_otros \u0026lt;- if (nchar(otros_str) \u0026gt; 0) { glue(\u0026#34; las identidades trans y no binarias suman proporciones pequeñas pero presentes: {otros_str};\u0026#34;) } else \u0026#34;\u0026#34; parte_nr \u0026lt;- if (!is.na(nr_pieza)) { glue(\u0026#34; además, {nr_pieza} prefieren no responder o no saben,\u0026#34;) } else \u0026#34;\u0026#34; cierre \u0026lt;- glue(\u0026#34; configurando un perfil predominantemente {mayor_adj} con {nr_qual} no respuesta y diversidad de género minoritaria.\u0026#34;) texto \u0026lt;- glue(\u0026#34;En la Región de {region_nom} (total {total_fmt} personas), {parte_mf}{parte_otros}{parte_nr}{cierre}\u0026#34;) as.character(texto) } Lo importante es que podemos usar la función generada por la IA para obtener resultados reutilizables y reproducibles:\nredactar_parrafo(genero, region = \u0026#34;Valparaíso\u0026#34;) En la Región de Valparaíso (total 1.505.034 personas), la población es mayoritariamente femenina con 780.209 personas (≈51,84%), seguida por 704.691 hombres (≈46,82%); las identidades trans y no binarias suman proporciones pequeñas pero presentes: transmasculino 3.003 (≈0,20%), transfemenino 1.369 (≈0,09%), no binario 2.161 (≈0,14%) y “otro” 624 (≈0,04%); además, 12.977 personas (≈0,86%) prefieren no responder o no saben, configurando un perfil predominantemente femenina con baja no respuesta y diversidad de género minoritaria. redactar_parrafo(genero, region = \u0026#34;Metropolitana de Santiago\u0026#34;) En la Región de Metropolitana de Santiago (total 5.839.785 personas), la población es mayoritariamente femenina con 3.010.084 personas (≈51,54%), seguida por 2.746.994 hombres (≈47,04%); las identidades trans y no binarias suman proporciones pequeñas pero presentes: transmasculino 13.754 (≈0,24%), transfemenino 5.838 (≈0,10%), no binario 8.230 (≈0,14%) y “otro” 2.318 (≈0,04%); además, 52.567 personas (≈0,90%) prefieren no responder o no saben, configurando un perfil predominantemente femenina con baja no respuesta y diversidad de género minoritaria. La moraleja es que es mejor usar la IA para que produzca código que genere los resultados que esperas, en lugar de pedirle directamente respuestas que no van a ser reproducibles ni reutilizables y pueden contener alucinaciones errores o datos inventados!\n","date":"2026-02-19T00:00:00Z","excerpt":"En un post anterior vimos cómo usar R para [producir textos basados en datos](/blog/redactar_texto/). En este post veremos cómo usar un modelo de lenguaje (LLM) o _IA_ para interpretar datos directamente desde R, pero también usaremos la IA para generar código de R y reutilizarlo. Moraleja: es mejor pedirle código a la IA que pedirle respuestas y arriesgarte a errores o alucinaciones!","href":"https://bastianoleah.netlify.app/blog/redactar_texto_llm/","tags":"texto ; inteligencia artificial","title":"Redactar textos basados en datos usando IA desde R"},{"content":"El otro día me llegó un script (muy probablemente hecho por una IA) de más de 9.000 líneas!\nIgual era entendible, porque era un script que producía cientos de gráficos. Pero revisando el script me doy cuenta de que también se repiten cientos de veces los mismos patrones de código.\nScripts como éstos suelen ser apenas una docena de bloques de código distintos, pero repetidos muchas veces cada uno con mínimas diferencias entre ellos: referencias a columnas distintas, etc.\nEsa extensión y ese nivel de repetición hizo que modificar el script para mejorar los gráficos, que en teoría eran un puñado de visualizaciones repetidas muchas veces, fuera un enorme dolor de cabeza.\nAl tiro pensé: Este script enorme podría haber sido un for loop 😂\n¿Qué tiene de malo la repetición?\nProduce scripts difíciles de revisar y entender, porque son tan eternos que no puedes contenerlos en tu cabeza 🤕 Es código difícil de mantener, porque si quieres hacer un cambio, vas a tener que aplicarlo infinitas veces de forma manual 😫 Producen problemas a largo plazo, porque puede ser que aparezca un error en el código y vas a tener que bucear 🐠 entre miles de líneas para encontrarlo Aquí dejo algunos consejos para escribir código más eficiente, más fácil de entender, y más fácil de mantener.\nSeparar un script en partes Si tienes un script muy largo, lo primero que puedes hacer es separar el script en varias partes, y cada parte ponerla en un archivo distinto.\nSi un script requiere que se ejecute otro, puedes agregar source() para que dentro de un script se ejecute otro script.\nPor ejemplo, en un script pones todo el código de la carga de datos, en otro pones las funciones que usarás, y el tercer script lo empiezas con source(\u0026quot;cargar.R\u0026quot;) y source(\u0026quot;funciones.R\u0026quot;).\nEntonces pasas desde esto:\nscript_enorme.R A esto:\ncargar.R funciones.R pruebas.R gráficos.R Entonces, si necesitas cambiar algo en la carga de datos, vas al script correspondiente y lo arreglas!\nOtra opción es tener todo en scripts separados, y luego tener un script principal que ejecute todos los pasos necesarios con source(), una especie de orquestador de todos los pasos de tu proyecto. En estos casos se recomienda anteceder los scripts con una numeración para registrar el orden de los pasos.\nPara orquestar pipelines en R existe el paquete {targets}, que permite declarar flujos o pipelines con los pasos necesarios para tu proyecto, coordinando la ejecución de todos los pasos. Tiene el beneficio de que optimiza el tiempo de procesamiento al ejecutar solamente los pasos que tienen cambios. Para más información revisa este libro. Crear funciones Si tienes un bloque de código que se repite muchas veces, lo mejor es convertirlo en una función, y luego llamar a esa función cada vez que necesites ejecutar ese bloque de código.\nPor qué usar funciones?\nPermiten ordenar el código, porque esconden la complejidad del código dentro de la función, dejando sólo lo necesario a la vista: pasas de muchas líneas de código a una función con un nombre que describa lo que hace Permiten reutilizar código, porque una vez que creas la función, puedes usarla las veces que quieras sin tener que copiar y pegar el mismo bloque de código Te ayudan a mantener el código, porque si después necesitas hacer un cambio o corrección, la haces en un sólo lugar, y ese cambio se va a aplicar a todas las veces que uses la función Para aprender lo básico sobre crear funciones en R, revisa este tutorial Pongámonos en el caso de que vamos a procesar los resultados del Censo de población y vivienda 2024 de Chile, y cargamos los datos sobre población según género:\nDescargar datos library(readxl) library(dplyr) library(janitor) library(tidyr) # cargar datos genero \u0026lt;- read_xlsx(\u0026#34;P5_Genero.xlsx\u0026#34;, sheet = 2) # limpiar y procesar genero_long \u0026lt;- genero |\u0026gt; row_to_names(3) |\u0026gt; pivot_longer(cols = 4:last_col(), names_to = \u0026#34;genero\u0026#34;, values_to = \u0026#34;poblacion\u0026#34;) |\u0026gt; select(region = 2, genero, poblacion) |\u0026gt; mutate(poblacion = as.numeric(poblacion)) |\u0026gt; mutate(tipo = case_when(genero %in% c(\u0026#34;Masculino\u0026#34;, \u0026#34;Femenino\u0026#34;) ~ \u0026#34;Binario cis\u0026#34;, genero %in% c(\u0026#34;No binario\u0026#34;, \u0026#34;Transfemenino\u0026#34;, \u0026#34;Transmasculino\u0026#34;) ~ \u0026#34;Trans\u0026#34;, TRUE ~ \u0026#34;Otros\u0026#34;)) |\u0026gt; filter(tipo != \u0026#34;Otros\u0026#34;) genero_long # A tibble: 95 × 4 region genero poblacion tipo \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 País Masculino 6836779 Binario cis 2 País Femenino 7455952 Binario cis 3 País Transmasculino 31955 Trans 4 País Transfemenino 13314 Trans 5 País No binario 15395 Trans 6 Arica y Parinacota Masculino 89224 Binario cis 7 Arica y Parinacota Femenino 94037 Binario cis 8 Arica y Parinacota Transmasculino 414 Trans 9 Arica y Parinacota Transfemenino 200 Trans 10 Arica y Parinacota No binario 146 Trans # ℹ 85 more rows Ahora que tenemos los datos por región y género, procedemos a visualizar los datos:\nlibrary(ggplot2) Warning: package 'ggplot2' was built under R version 4.4.3 # visualizar genero_long |\u0026gt; filter(region == \u0026#34;País\u0026#34;) |\u0026gt; ggplot(aes(x = genero, y = poblacion, fill = genero)) + geom_col(width = 0.5, color = \u0026#34;#EAD2FA\u0026#34;) + geom_text(aes(label = scales::comma(poblacion)), position = position_dodge(width = 0.9), vjust = -0.5, size = 3) + scale_fill_brewer(type = \u0026#34;qual\u0026#34;, palette = \u0026#34;Set2\u0026#34;) + scale_y_continuous(labels = scales::comma, expand = expansion(mult = c(0, 0.1))) + scale_x_discrete(labels = label_wrap_gen(15)) + facet_wrap(~tipo, scales = \u0026#34;free\u0026#34;) + guides(fill = guide_none()) + theme_grey(ink = \u0026#34;#553A74\u0026#34;, paper = \u0026#34;#EAD2FA\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;) + labs(title = \u0026#34;Población nacional según género\u0026#34;, subtitle = \u0026#34;Censo 2024\u0026#34;, x = \u0026#34;Géneros\u0026#34;, y = \u0026#34;Población (a distintas escalas)\u0026#34;) Pero imaginemos que ahora queremos hacer el mismo gráfico varias veces. ¿Copiamos el bloque del gráfico y lo pegamos las veces que lo necesitemos? NO! 😡\nEn vez de repetir el código, copiamos el código y creamos funciones para ordenarlo y hacerlo más manejable:\n# función para cargar datos censo_cargar_genero \u0026lt;- function(archivo = \u0026#34;P5_Genero.xlsx\u0026#34;) { read_xlsx(archivo, sheet = 2) } # función para procesar datos censo_procesar_genero \u0026lt;- function(genero) { genero |\u0026gt; row_to_names(3) |\u0026gt; pivot_longer(cols = 4:last_col(), names_to = \u0026#34;genero\u0026#34;, values_to = \u0026#34;poblacion\u0026#34;) |\u0026gt; select(region = 2, genero, poblacion) |\u0026gt; mutate(poblacion = as.numeric(poblacion)) |\u0026gt; mutate(tipo = case_when(genero %in% c(\u0026#34;Masculino\u0026#34;, \u0026#34;Femenino\u0026#34;) ~ \u0026#34;Binario cis\u0026#34;, genero %in% c(\u0026#34;No binario\u0026#34;, \u0026#34;Transfemenino\u0026#34;, \u0026#34;Transmasculino\u0026#34;) ~ \u0026#34;Trans\u0026#34;, TRUE ~ \u0026#34;Otros\u0026#34;)) |\u0026gt; filter(tipo != \u0026#34;Otros\u0026#34;) |\u0026gt; filter(!is.na(region)) } # función para visualizar datos censo_grafico_genero \u0026lt;- function(genero_long, filtro = \u0026#34;País\u0026#34;, titulo = \u0026#34;Población nacional según género\u0026#34;) { genero_long |\u0026gt; filter(region == filtro) |\u0026gt; ggplot(aes(x = genero, y = poblacion, fill = genero)) + geom_col(width = 0.5, color = \u0026#34;#EAD2FA\u0026#34;) + geom_text(aes(label = scales::comma(poblacion)), position = position_dodge(width = 0.9), vjust = -0.5, size = 3) + scale_fill_brewer(type = \u0026#34;qual\u0026#34;, palette = \u0026#34;Set2\u0026#34;) + scale_y_continuous(labels = scales::comma, expand = expansion(mult = c(0, 0.1))) + scale_x_discrete(labels = label_wrap_gen(15)) + facet_wrap(~tipo, scales = \u0026#34;free\u0026#34;) + guides(fill = guide_none()) + theme_grey(ink = \u0026#34;#553A74\u0026#34;, paper = \u0026#34;#EAD2FA\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;) + labs(title = titulo, subtitle = \u0026#34;Censo 2024\u0026#34;, x = \u0026#34;Géneros\u0026#34;, y = \u0026#34;Población (a distintas escalas)\u0026#34;) } Lo que hicimos fue simplemente meter las partes del código dentro de function() para crear funciones.\nAhora cuando necesitemos ejecutar esas partes del código, simplemente llamamos las funciones:\ngenero \u0026lt;- censo_cargar_genero() genero_long \u0026lt;- censo_procesar_genero(genero) censo_grafico_genero(genero_long, filtro = \u0026#34;Valparaíso\u0026#34;, titulo = \u0026#34;Población de Valparaíso según género\u0026#34;) ¡Mucho más breve y ordenado! 😍\nAhora podemos reutilizar la función para generar otro gráfico similar en tan sólo un par de líneas:\ncenso_grafico_genero(genero_long, filtro = \u0026#34;Biobío\u0026#34;, titulo = \u0026#34;Población de Biobío según género\u0026#34;) En el fondo lo que hicimos fue esconder parte del código dentro de las funciones, despejando nuestro script.\nAhora, si queremos hacer algún cambio en el código, cambiamos la función que creamos y re-ejecutamos la función, y así el cambio se aplicará a las siguientes veces que ocupemos la función. Por ejemplo, cambiemos la paleta de colores:\nVer código de la función con cambios # cambiamos la función, agregando el código nuevo dentro de ella censo_grafico_genero \u0026lt;- function(genero_long, filtro = \u0026#34;País\u0026#34;, titulo = \u0026#34;Población nacional según género\u0026#34;) { genero_long |\u0026gt; filter(region == filtro) |\u0026gt; ggplot(aes(x = genero, y = poblacion, fill = genero)) + geom_col(width = 0.5, color = \u0026#34;#EAD2FA\u0026#34;) + geom_text(aes(label = scales::comma(poblacion)), position = position_dodge(width = 0.9), vjust = -0.5, size = 3) + # scale_fill_brewer(type = \u0026#34;qual\u0026#34;, palette = \u0026#34;Set2\u0026#34;) + #### cambio en la función para cambiar paleta de colores scale_fill_manual(values = c(\u0026#34;Femenino\u0026#34; = \u0026#34;#9069C0\u0026#34;, \u0026#34;Masculino\u0026#34; = \u0026#34;#6974C0\u0026#34;, \u0026#34;No binario\u0026#34; = \u0026#34;#C46EBA\u0026#34;, \u0026#34;Transfemenino\u0026#34; = \u0026#34;#9069C0\u0026#34;, \u0026#34;Transmasculino\u0026#34; = \u0026#34;#6974C0\u0026#34;)) + #### scale_y_continuous(labels = scales::comma, expand = expansion(mult = c(0, 0.1))) + scale_x_discrete(labels = label_wrap_gen(15)) + facet_wrap(~tipo, scales = \u0026#34;free\u0026#34;) + guides(fill = guide_none()) + theme_grey(ink = \u0026#34;#553A74\u0026#34;, paper = \u0026#34;#EAD2FA\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;) + #### cambio en la función para usar el filtro en el título labs(title = paste(\u0026#34;Población de\u0026#34;, filtro, \u0026#34;según género\u0026#34;), #### subtitle = \u0026#34;Censo 2024\u0026#34;, x = \u0026#34;Géneros\u0026#34;, y = \u0026#34;Población (a distintas escalas)\u0026#34;) } Ahora que actualizamos la función, al volver a usarla tendrá los cambios nuevos:\ncenso_grafico_genero(genero_long, filtro = \u0026#34;Arica y Parinacota\u0026#34;) Hacer un loop o bucle Otro caso de repetición es cuando tenemos que hacer una misma cosa muchas veces.\nSiguiendo el ejeplo anterior, donde creamos una función, una opción es usar la función muchas veces:\ncenso_grafico_genero(genero_long, filtro = \u0026#34;Arica y Parinacota\u0026#34;) censo_grafico_genero(genero_long, filtro = \u0026#34;Tarapacá\u0026#34;) censo_grafico_genero(genero_long, filtro = \u0026#34;Antofagasta\u0026#34;) censo_grafico_genero(genero_long, filtro = \u0026#34;Coquimbo\u0026#34;) \u0026hellip; y así hasta el infinito. Pero esto no es eficiente!\nLo mejor sería usar un loop o bucle, que es una estructura de código que permite repetir un bloque de código varias veces, cambiando alguna parte del código cada vez.\nPara aprender lo básico sobre crear loops en R, revisa este tutorial En este caso, el código repetido siempre es la misma función (o el bloque de código que genera el gráfico), y lo único que cambia son los argumentos que se le enrega a la función (la región a filtrar), así que podemos hacer un loop que ejecute varias veces el código, y en cada paso cambie el filtro:\n# obtener vector con las regiones regiones \u0026lt;- unique(genero_long$region) Primero obtenemos un vector que contenga el valor que queremos que se use en cada paso del loop, y luego construimos el loop para que, por cada valor del vector (for (region in regiones)), ejecute el bloque de código que queremos repetir:\n# hacer un loop para generar un gráfico por cada región for (region in regiones) { censo_grafico_genero(genero_long, filtro = region) } Con sólo esas pocas líneas, generamos gráficos para todas las regiones del país!\nConclusión En resumidas cuentas, si aplicamos lo aprendido en este tutorial, lo que hicimos quedaría así:\nsource(\u0026#34;funciones.R\u0026#34;) # cargar genero \u0026lt;- censo_cargar_genero() # procesar genero_long \u0026lt;- censo_procesar_genero(genero) # loop for (region in unique(genero$regiones)) { censo_grafico_genero(genero_long, filtro = region) } Hermoso. Elegante. Conciso. Reproducible. Una obra de arte 👌🏼\nQuizás lo mejor que puedes hacer con tu código es hacerlo legible y no repetirte. Como hemos visto, lo mejor es separar el código en partes, crear funciones para ordenar el código y hacerlo más manejable, y usar loops para ejecutar un mismo código varias veces.\n","date":"2026-02-18T00:00:00Z","excerpt":"Cuando trabajas con código, lo mejor es que lo organices para que sea más fácil de entender y más fácil de mantener. Esto, a su vez, reducirá tu carga mental, te volverá más eficiente, y tu _yo_ del futuro te lo agradecerá. En este tutorial te doy algunos consejos para lograrlo usando scripts separados, creando funciones, y haciendo _loops_ para evitar la repetición y mejorar la legibilidad de tu código.","href":"https://bastianoleah.netlify.app/blog/repetirse/","tags":"loops ; optimización ; consejos","title":"Cómo dejar de repetirte y escribir código más eficiente en R"},{"content":"Recientemente Estación R compartió su paquete del día y lo encontré útil.\nComo dice el post, {printtree} crea árboles de las carpetas de tus proyectos, lo que es muy útil para tener una visión general de la estructura de tus proyectos. Esto puede ser útil de incluir en informes, reportes o documentación de proyectos en los que trabajes, para dejar por escrito la estructura del proyecto y así poder comentar sobre ella para lectores.\nPor ejemplo, aquí genero un árbol de la estructura de un proyecto laboral, que sirve para indicar en el informe de entrega la forma en que se guardan los datos originales y los resultados:\nprinttree::print_rtree(format = \u0026#34;unicode\u0026#34;, max_depth = 2) indice_brechas_genero/ ├── indice_brechas_genero.Rproj ├── app/ │ └── ibg/ ├── datos/ │ ├── censo_edad/ │ ├── censo_educacion/ │ ├── censo_poblacion/ │ ├── clasificacion_pndr/ │ ├── conadi_indigena/ │ ├── cultura_agentes/ │ ├── cultura_bibliotecas/ │ ├── cultura_fondos/ │ ├── cut_comunas/ │ ├── fosis_beneficiarios/ │ ├── integra/ │ ├── junji_mineduc/ │ ├── mideso_educacion/ │ ├── mideso_ingresos/ │ ├── mideso_ocupacion/ │ ├── mideso_salud/ │ ├── mineduc_etp/ │ ├── mineduc_paes/ │ ├── mineduc_rendimiento/ │ ├── mineduc_simce/ │ ├── minsal_egresos/ │ ├── minsal_rem/ │ ├── minsal_urgencias/ │ ├── prodesal_agropecuario/ │ ├── rsh_hogares/ │ ├── senadis_mideso/ │ ├── sercotec_beneficiarios/ │ ├── servel_concejales/ │ ├── servel_partidos/ │ ├── sinim_municipios/ │ ├── sp_pensiones/ │ └── transparencia_usarios/ ├── resultados/ │ ├── comunas/ │ ├── regiones/ │ ├── diccionario.parquet │ ├── dimensiones.parquet │ ├── icbg_regional.parquet │ ├── icbg_regional.xlsx │ ├── icbg.parquet │ ├── icbg.xlsx │ ├── indice_brechas_genero_subdere.xlsx │ ├── interpretacion.parquet │ └── portada.xlsx ├── funciones.R ├── pendientes.md ├── procesar_regiones.R ├── procesar.R ├── pruebas.R └── readme.md Opcionalmente se puede guardar como una imagen agregando el argumento snapshot = TRUE.\n","date":"2026-02-18T00:00:00Z","excerpt":"Este paquete crea árboles de las carpetas de tus proyectos, que sirven para tener una visión general de la estructura de tus proyectos. Esto puede ser útil de incluir en informes, reportes o documentación de proyectos en los que trabajes, para dejar por escrito la estructura del proyecto.","href":"https://bastianoleah.netlify.app/blog/2026-02-18/","tags":"consejos","title":"Crea árboles de las carpetas de tus proyectos con `{printtree}` en R"},{"content":" También tengo una versión alternativa de este tutorial donde usamos IA para generar textos desde R. Pero ten en consideración que usar IA y aprender son cosas distintas! Recomiendo aprender primero y luego apoyarte en IA. Luego de explorar o procesar un conjunto de datos, toca presentar los resultados. Si bien esto nos hace pensar en gráficos, tablas o reportes, el texto también es una forma de comunicar resultados que puede ser optimizada mediante la programación.\nEn este tutorial veremos cómo hacer que R redacte párrafos de texto que se pueden adaptar a tus datos, ya sea adoptando la sintaxis y redacción apropiadas, integrando cifras, o escribiendo distintas oraciones de manera condicional a los resultados.\nEmpecemos con un conjunto de datos sociales: los resultados del Censo de población y vivienda 2024 de Chile, específicamente la población por género en cada región del país.\nDescargar datos Ver código de la limpieza de los datos library(dplyr) library(janitor) library(readxl) library(tidyr) # cargar genero \u0026lt;- read_xlsx(\u0026#34;P5_Genero.xlsx\u0026#34;, sheet = 2) # limpiar genero_limpio \u0026lt;- genero |\u0026gt; row_to_names(3) |\u0026gt; filter(!is.na(Región)) # transformar a largo genero_long \u0026lt;- genero_limpio |\u0026gt; pivot_longer(cols = 4:last_col(), names_to = \u0026#34;genero\u0026#34;, values_to = \u0026#34;poblacion\u0026#34;) |\u0026gt; rename(total = 3) # convertir variables y calcular porcentajes genero_porcentaje \u0026lt;- genero_long |\u0026gt; mutate(poblacion = as.numeric(poblacion), total = as.numeric(total)) |\u0026gt; clean_names() |\u0026gt; mutate(porcentaje = poblacion / total) # filtrar genero \u0026lt;- genero_porcentaje |\u0026gt; filter(region != \u0026#34;País\u0026#34;) Luego de limpiar estos datos, vemos columnas con las regiones del país, la población regional, los distintos géneros consultados en el censo, la cantidad de personas y su porcentaje:\nlibrary(dplyr) genero |\u0026gt; filter(region == \u0026#34;Valparaíso\u0026#34;) # A tibble: 7 × 6 codigo_region region total genero poblacion porcentaje \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 5 Valparaíso 1505034 Masculino 704691 0.468 2 5 Valparaíso 1505034 Femenino 780209 0.518 3 5 Valparaíso 1505034 Transmasculino 3003 0.00200 4 5 Valparaíso 1505034 Transfemenino 1369 0.000910 5 5 Valparaíso 1505034 No binario 2161 0.00144 6 5 Valparaíso 1505034 Otro 624 0.000415 7 5 Valparaíso 1505034 Prefiere no responder/N… 12977 0.00862 Usando datos para generar texto Empecemos filtrando un caso de una región específica y un género determinado:\ngenero_f \u0026lt;- genero |\u0026gt; filter(region == \u0026#34;Valparaíso\u0026#34;) |\u0026gt; filter(genero == \u0026#34;No binario\u0026#34;) genero_f # A tibble: 1 × 6 codigo_region region total genero poblacion porcentaje \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 5 Valparaíso 1505034 No binario 2161 0.00144 Sabemos que podemos extraer el texto o los valores de una tabla de datos con el operador $, y si tenemos una tabla de una sola fila ésto se vuelve muy conveniente:\ngenero_f$genero [1] \u0026quot;No binario\u0026quot; genero_f$poblacion [1] 2161 Podemos usar esta técnica para redactar un párrafo simple.\nLa función glue() del paquete {glue} nos permite crear textos que contengan valores de objetos de R, simplemente abriendo paréntesis de llave ({ y }) con el código de R dentro:\nlibrary(glue) glue(\u0026#34;En la región de {genero_f$region} existen {genero_f$poblacion} personas de género {genero_f$genero}, que representan a un {genero_f$porcentaje * 100}% de la población regional.\u0026#34;) En la región de Valparaíso existen 2161 personas de género No binario, que representan a un 0.143584796090985% de la población regional. ✍🏼 Creamos una oración que adquiere sus valores desde la tabla de datos filtrada!\nPor lo tanto, si filtramos la tabla de nuevo, obtendremos un texto que se ajuste a los nuevos datos:\n# volver a filtrar datos genero_f \u0026lt;- genero |\u0026gt; filter(region == \u0026#34;Metropolitana de Santiago\u0026#34;) |\u0026gt; filter(genero == \u0026#34;Transfemenino\u0026#34;) # volver a redactar texto glue(\u0026#34;En la región de {genero_f$region} existen {genero_f$poblacion} personas de género {genero_f$genero}, que representan a un {genero_f$porcentaje * 100}% de la población regional.\u0026#34;) En la región de Metropolitana de Santiago existen 5838 personas de género Transfemenino, que representan a un 0.0999694338062103% de la población regional. Mejoras para la generación de texto Ahora podemos mejorar el texto generado usando algunas funciones que entregan formatos apropiados a las cifras y modifican otros textos.\nEl paquete {scales} cuenta con varias funciones para formatear valores. Primero definimos una configuración por defecto con number_options(), y luego usamos number() y percent() para formatear las cifras de población y porcentaje.\nTambién usamos str_to_lower() de {stringr} para convertir el texto del género a minúscula.\nlibrary(scales) number_options(big.mark = \u0026#34;.\u0026#34;, decimal.mark = \u0026#34;,\u0026#34;) n \u0026lt;- number(genero_f$poblacion) # números con puntos separadores de miles p \u0026lt;- percent(genero_f$porcentaje, 0.01) # porcentaje con 2 decimales library(stringr) g \u0026lt;- str_to_lower(genero_f$genero) # texto a minúscula # redactar texto mejorado glue(\u0026#34;En la región de {genero_f$region} existen {n} personas de género {g}, que representan a un {p} de la población regional.\u0026#34;) En la región de Metropolitana de Santiago existen 5.838 personas de género transfemenino, que representan a un 0,10% de la población regional. Crear una función que redacte texto Ahora que tenemos unas líneas de código que hacen lo que necesitamos, podemos ponerlas dentro de una función para poder utilizarla varias veces sin repetir tanto código.\nUna función es un código que realiza una tarea específica, y que puede ser reutilizado varias veces, recibiendo distintos valores de entrada. Así simplificamos nuestro código, evitamos la repetición de código, y abrimos la posibilidad de automatizar. Para crear la función, simplemente le damos un nombre y usamos function() para definir los argumentos que recibirá la función. En este caso, la función redactar_genero() recibe primero los datos, y luego el argumento region, que es la región para la que queremos filtrar los datos y redactar el texto.\nredactar_genero \u0026lt;- function(genero, region) { # filtrar la región y el género genero_f \u0026lt;- genero |\u0026gt; filter(region == {{region}}) |\u0026gt; filter(genero == \u0026#34;No binario\u0026#34;) # formatear cifras y valores n \u0026lt;- number(genero_f$poblacion) p \u0026lt;- percent(genero_f$porcentaje, 0.01) g \u0026lt;- str_to_lower(genero_f$genero) # elegir artículo según la región art \u0026lt;- recode_values(genero_f$region, \u0026#34;Biobío\u0026#34; ~ \u0026#34;del\u0026#34;, \u0026#34;Metropolitana de Santiago\u0026#34; ~ \u0026#34;\u0026#34;, \u0026#34;Libertador General Bernardo O\u0026#39;Higgins\u0026#34; ~ \u0026#34;del\u0026#34;, \u0026#34;Maule\u0026#34; ~ \u0026#34;del\u0026#34;, default = \u0026#34;de\u0026#34;) # redactar texto glue(\u0026#34;En la **región {art} {genero_f$region}** existen {n} personas de **género {g}**, que representan a un {p} de la población regional.\u0026#34;) } Abre que tenemos la función, simplemente la aplicamos a la región que deseamos:\nredactar_genero(genero, \u0026#34;Valparaíso\u0026#34;) En la **región de Valparaíso** existen 2.161 personas de **género no binario**, que representan a un 0,14% de la población regional. redactar_genero(genero, \u0026#34;Metropolitana de Santiago\u0026#34;) En la **región Metropolitana de Santiago** existen 8.230 personas de **género no binario**, que representan a un 0,14% de la población regional. Redactar texto en serie usando loops El mismo código que usamos arriba, ya sea como líneas de código o como una función, puede ser usado dentro de un loop para generar múltiples veces textos que se adaptan a distintos datos.\nUn loop sirve para repetir muchas veces una misma operación, a partir de un vector con valores distintos, a cada uno de los cuales se le aplicará la operación. En este caso crearemos un loop con {purrr} y la función map(), que primera recibe un vector con los elementos que queremos repetir, y la función que le aplicaremos a cada uno de los elementos:\nlibrary(purrr) regiones \u0026lt;- unique(genero$region) textos \u0026lt;- map(regiones, ~redactar_genero(genero, .x) ) Recibimos como resultado una lista con todos los textos para cada uno de los valores que pusimos en el loop.\nEn la región de Arica y Parinacota existen 146 personas de género no binario, que representan a un 0,08% de la población regional.\nEn la región de Tarapacá existen 203 personas de género no binario, que representan a un 0,07% de la población regional.\nEn la región de Antofagasta existen 314 personas de género no binario, que representan a un 0,07% de la población regional.\nEn la región de Atacama existen 121 personas de género no binario, que representan a un 0,05% de la población regional.\nEn la región de Coquimbo existen 508 personas de género no binario, que representan a un 0,08% de la población regional.\nEn la región de Valparaíso existen 2.161 personas de género no binario, que representan a un 0,14% de la población regional.\nEn la región Metropolitana de Santiago existen 8.230 personas de género no binario, que representan a un 0,14% de la población regional.\nEn la región del Libertador General Bernardo O'Higgins existen 474 personas de género no binario, que representan a un 0,06% de la población regional.\nEn la región del Maule existen 438 personas de género no binario, que representan a un 0,05% de la población regional.\nEn la región de Ñuble existen 181 personas de género no binario, que representan a un 0,04% de la población regional.\nEn la región del Biobío existen 1.016 personas de género no binario, que representan a un 0,08% de la población regional.\nEn la región de La Araucanía existen 533 personas de género no binario, que representan a un 0,07% de la población regional.\nEn la región de Los Ríos existen 388 personas de género no binario, que representan a un 0,12% de la población regional.\nEn la región de Los Lagos existen 458 personas de género no binario, que representan a un 0,07% de la población regional.\nEn la región de Aysén del General Carlos Ibáñez del Campo existen 81 personas de género no binario, que representan a un 0,11% de la población regional.\nEn la región de Magallanes y de la Antártica Chilena existen 143 personas de género no binario, que representan a un 0,11% de la población regional.\nOtras formas de imprimir los resultados Algunas formas de obtener estos resultados son simplemente imprimiendo la lista, o usando cat() para imprimir cada texto en una nueva línea, y así poder copiarlos y pegarlos en otro programa:\ntextos |\u0026gt; unlist() |\u0026gt; cat(sep = \u0026#34;\\n\\n\u0026#34;) En la **región de Arica y Parinacota** existen 146 personas de **género no binario**, que representan a un 0,08% de la población regional. En la **región de Tarapacá** existen 203 personas de **género no binario**, que representan a un 0,07% de la población regional. En la **región de Antofagasta** existen 314 personas de **género no binario**, que representan a un 0,07% de la población regional. En la **región de Atacama** existen 121 personas de **género no binario**, que representan a un 0,05% de la población regional. En la **región de Coquimbo** existen 508 personas de **género no binario**, que representan a un 0,08% de la población regional. En la **región de Valparaíso** existen 2.161 personas de **género no binario**, que representan a un 0,14% de la población regional. En la **región Metropolitana de Santiago** existen 8.230 personas de **género no binario**, que representan a un 0,14% de la población regional. En la **región del Libertador General Bernardo O'Higgins** existen 474 personas de **género no binario**, que representan a un 0,06% de la población regional. En la **región del Maule** existen 438 personas de **género no binario**, que representan a un 0,05% de la población regional. En la **región de Ñuble** existen 181 personas de **género no binario**, que representan a un 0,04% de la población regional. En la **región del Biobío** existen 1.016 personas de **género no binario**, que representan a un 0,08% de la población regional. En la **región de La Araucanía** existen 533 personas de **género no binario**, que representan a un 0,07% de la población regional. En la **región de Los Ríos** existen 388 personas de **género no binario**, que representan a un 0,12% de la población regional. En la **región de Los Lagos** existen 458 personas de **género no binario**, que representan a un 0,07% de la población regional. En la **región de Aysén del General Carlos Ibáñez del Campo** existen 81 personas de **género no binario**, que representan a un 0,11% de la población regional. En la **región de Magallanes y de la Antártica Chilena** existen 143 personas de **género no binario**, que representan a un 0,11% de la población regional. Redactar textos para grupos de observaciones En el ejemplo anterior hicimos que R redacte texto extraído de una sola fila de una tabla; es decir, de una sola observación. Ahora veremos cómo redactar texto que considere los valores de varias filas de datos a la vez.\nPor ejemplo, veamos cómo redactar un texto que nos resuma los resultados del Censo en una región completa, considerando los valores mayores y menores, entre otros datos.\ngenero_r \u0026lt;- genero |\u0026gt; filter(region == \u0026#34;Coquimbo\u0026#34;) |\u0026gt; filter(genero != \u0026#34;Prefiere no responder/No sabe\u0026#34;, genero != \u0026#34;Otro\u0026#34;) |\u0026gt; arrange(desc(poblacion)) genero_r # A tibble: 5 × 6 codigo_region region total genero poblacion porcentaje \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 4 Coquimbo 643271 Femenino 331448 0.515 2 4 Coquimbo 643271 Masculino 304470 0.473 3 4 Coquimbo 643271 Transmasculino 1446 0.00225 4 4 Coquimbo 643271 Transfemenino 594 0.000923 5 4 Coquimbo 643271 No binario 508 0.000790 Luego de filtrar los datos para un grupo de observaciones, nos encontramos con datos para cinco filas.\nPrimero obtenemos el valor del grupo filtrado, en este caso la región, y luego podemos filtrar observaciones clave que queramos destacar en el texto; en este caso, los géneros binarios que son mayoría:\nregion \u0026lt;- genero_r$region[1] genero_masc \u0026lt;- genero_r |\u0026gt; filter(genero == \u0026#34;Masculino\u0026#34;) genero_fem \u0026lt;- genero_r |\u0026gt; filter(genero == \u0026#34;Femenino\u0026#34;) texto_general \u0026lt;- glue(\u0026#34;La región de {region} tiene una distribución entre hombres y mujeres de {percent(genero_masc$porcentaje)} hombres y {percent(genero_fem$porcentaje)} mujeres.\u0026#34;) texto_general La región de Coquimbo tiene una distribución entre hombres y mujeres de 47% hombres y 52% mujeres. Textos condicionales según datos Dado que en el párrafo anterior nos encontramos con porcentajes que varían muy poco, agregaremos un texto donde se compare un valor específico con un valor, generando un texto condicional en base a los datos.\nEn este caso, escribimos un texto que explica si el porcentaje de hombres en una región es mayor al promedio de hombres en el país.\n# calcular promedio de hombres a nivel nacional promedio \u0026lt;- genero |\u0026gt; summarize(total = sum(poblacion), .by = genero) |\u0026gt; mutate(porcentaje = total / sum(total)) |\u0026gt; filter(genero == \u0026#34;Masculino\u0026#34;) |\u0026gt; pull(porcentaje) comparacion \u0026lt;- if_else(genero_masc$porcentaje \u0026gt;= promedio, \u0026#34;mayor\u0026#34;, \u0026#34;menor\u0026#34;) texto_promedio \u0026lt;- glue(\u0026#34;El porcentaje de hombres con respecto de mujeres es {comparacion} al promedio nacional de hombres.\u0026#34;) En el código anterior, si el valor de la observación supera al del promedio calculado, el texto dirá mayor, o de lo contrario dirá menor.\nAhora podemos usar alguna función que nos permita resumir los datos; por ejemplo, la cantidad de valores únicos de una variable relevante; en este caso la cantidad de géneros medidos por el censo:\nn_generos \u0026lt;- n_distinct(genero_r$genero) texto_generos \u0026lt;- glue(\u0026#34;El Censo en la región de {region} cuenta con estadísticas oficiales de {n_generos} géneros distintos, excluyendo no respuestas.\u0026#34;) texto_generos El Censo en la región de Coquimbo cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. Luego podemos filtrar la tabla para obtener estadísticos descriptivos como las mayores y menores observaciones:\ngenero_max \u0026lt;- genero_r |\u0026gt; slice_max(poblacion) genero_min \u0026lt;- genero_r |\u0026gt; slice_min(poblacion) texto_resumen \u0026lt;- glue(\u0026#34;El género con mayor población es {genero_max$genero} con {number(genero_max$poblacion)} personas, mientras que el con menor población es {genero_min$genero}, con {number(genero_min$poblacion)}.\u0026#34;) texto_resumen El género con mayor población es Femenino con 331.448 personas, mientras que el con menor población es No binario, con 508. Ahora redactemos uno texto para un subgrupo de todas las observaciones. En este caso, redactaremos algo específico para los géneros fuera del binario cis:\ncuir \u0026lt;- genero_r |\u0026gt; filter(genero != \u0026#34;Masculino\u0026#34;, genero != \u0026#34;Femenino\u0026#34;) cuir # A tibble: 3 × 6 codigo_region region total genero poblacion porcentaje \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 4 Coquimbo 643271 Transmasculino 1446 0.00225 2 4 Coquimbo 643271 Transfemenino 594 0.000923 3 4 Coquimbo 643271 No binario 508 0.000790 Filtramos en la observación mayoritaria, y luego generamos textos que ordenen las observaciones y las redacten como una secuencia de palabras.\nLa función glue_collapse() del paquete {glue} nos permite unir los elementos de un vector de texto, separándolos por comas, y convenientemente poniendo la letra y griega antes del último elemento! Así, A B C se vuelven A, B Y C.\n# obtener la observación mayoritaria cuir_max \u0026lt;- cuir |\u0026gt; slice_max(poblacion) # redactar valores como secuencia de palabras cuir_gen \u0026lt;- glue_collapse(cuir$genero, sep = \u0026#34;, \u0026#34;, last = \u0026#34; y \u0026#34;) # redactar cifras como secuencia de palabras cuir_pob \u0026lt;- cuir$poblacion |\u0026gt; number() |\u0026gt; # puntos de miles glue_collapse(, sep = \u0026#34;, \u0026#34;, last = \u0026#34; y \u0026#34;) # redactar texto combinando elementos anteriores texto_subgrupo \u0026lt;- glue(\u0026#34;Aparte de masculino y femenino, los demás géneros en la región son {cuir_gen}, con {cuir_pob} personas respectivamente, siendo el género con mayor población {cuir_max$genero} con un {percent(cuir_max$porcentaje, 0.1)} de la región.\u0026#34;) texto_subgrupo Aparte de masculino y femenino, los demás géneros en la región son Transmasculino, Transfemenino y No binario, con 1.446, 594 y 508 personas respectivamente, siendo el género con mayor población Transmasculino con un 0,2% de la región. Finalmente, podemos unir todas estas técnicas dentro de un loop para obtener los párrafos para todas las regiones del país de manera automática:\ntextos \u0026lt;- map(regiones, function(region) { # elegir artículo según la región art \u0026lt;- recode_values(region, \u0026#34;Biobío\u0026#34; ~ \u0026#34;del\u0026#34;, \u0026#34;Metropolitana de Santiago\u0026#34; ~ \u0026#34;\u0026#34;, \u0026#34;Libertador General Bernardo O\u0026#39;Higgins\u0026#34; ~ \u0026#34;del\u0026#34;, \u0026#34;Maule\u0026#34; ~ \u0026#34;del\u0026#34;, default = \u0026#34;de\u0026#34;) # filtrar datos genero_r \u0026lt;- genero |\u0026gt; filter(region == {{region}}) |\u0026gt; filter(genero != \u0026#34;Prefiere no responder/No sabe\u0026#34;, genero != \u0026#34;Otro\u0026#34;) |\u0026gt; arrange(desc(poblacion)) # filtrar observaciones relevantes genero_masc \u0026lt;- genero_r |\u0026gt; filter(genero == \u0026#34;Masculino\u0026#34;) genero_fem \u0026lt;- genero_r |\u0026gt; filter(genero == \u0026#34;Femenino\u0026#34;) texto_general \u0026lt;- glue(\u0026#34;La **región {art} {region}** tiene una distribución entre hombres y mujeres de {percent(genero_masc$porcentaje, 0.01)} hombres y {percent(genero_fem$porcentaje, 0.01)} mujeres.\u0026#34;) # calcular promedio de hombres a nivel nacional promedio \u0026lt;- genero |\u0026gt; summarize(total = sum(poblacion), .by = genero) |\u0026gt; mutate(porcentaje = total / sum(total)) |\u0026gt; filter(genero == \u0026#34;Masculino\u0026#34;) |\u0026gt; pull(porcentaje) # texto condicional según los datos comparacion \u0026lt;- if_else(genero_masc$porcentaje \u0026gt;= promedio, \u0026#34;mayor\u0026#34;, \u0026#34;menor\u0026#34;) texto_promedio \u0026lt;- glue(\u0026#34;En esta región, el porcentaje de hombres con respecto de mujeres es {comparacion} al promedio nacional de hombres.\u0026#34;) # contar observaciones únicas n_generos \u0026lt;- n_distinct(genero_r$genero) texto_generos \u0026lt;- glue(\u0026#34;El censo en la región de {region} cuenta con estadísticas oficiales de {n_generos} géneros distintos, excluyendo no respuestas.\u0026#34;) # redactar textos con estadísticos descriptivos genero_max \u0026lt;- genero_r |\u0026gt; slice_max(poblacion) genero_min \u0026lt;- genero_r |\u0026gt; slice_min(poblacion) texto_resumen \u0026lt;- glue(\u0026#34;El género con mayor población es el **{str_to_lower(genero_max$genero)}** con {number(genero_max$poblacion)} personas, mientras que el con menor población es {str_to_lower(genero_min$genero)}, con {number(genero_min$poblacion)}.\u0026#34;) # redactar textos sobre subgrupos de observaciones cuir \u0026lt;- genero_r |\u0026gt; filter(genero != \u0026#34;Masculino\u0026#34;, genero != \u0026#34;Femenino\u0026#34;) cuir_max \u0026lt;- cuir |\u0026gt; slice_max(poblacion, with_ties = FALSE) cuir_gen \u0026lt;- cuir$genero |\u0026gt; str_to_lower() |\u0026gt; glue_collapse(sep = \u0026#34;, \u0026#34;, last = \u0026#34; y \u0026#34;) cuir_pob \u0026lt;- cuir$poblacion |\u0026gt; number() |\u0026gt; glue_collapse(, sep = \u0026#34;, \u0026#34;, last = \u0026#34; y \u0026#34;) texto_subgrupo \u0026lt;- glue(\u0026#34;Aparte de masculino y femenino, los demás géneros en la región son {cuir_gen} con {cuir_pob} personas respectivamente, siendo **{str_to_lower(cuir_max$genero)}** el de mayor población, con {number(cuir_max$poblacion)} personas en total, lo que representa un {percent(cuir_max$porcentaje, 0.1)} de la población regional.\u0026#34;) # unir todas las oraciones en un párrafo paste(texto_general, texto_promedio, texto_generos, texto_resumen, texto_subgrupo) }) Se trata de una técnica que te permite tener total control sobre la redacción de los textos, y completa certeza acerca de la exactitud de los datos incluidos. Este código se puede usar para aplicaciones interactivas con Shiny, para redactar reportes de manera automatizada con Quarto, y más!\nAhora veremos el resultado de obtener todos los párrafos redactados automáticamente a partir de los datos.\nPasamos de una tabla de datos como ésta:\nregion total genero poblacion porcentaje Valparaíso 1505034 Femenino 780209 0.5183996 Ñuble 404488 No binario 181 0.0004475 Aysén del General Carlos Ibáñez del Campo 76678 Transmasculino 100 0.0013042 Arica y Parinacota 186079 Otro 70 0.0003762 Tarapacá 272334 Masculino 131243 0.4819193 Metropolitana de Santiago 5839785 Femenino 3010084 0.5154443 Ñuble 404488 Otro 81 0.0002003 Arica y Parinacota 186079 Transmasculino 414 0.0022249 A este texto que redacta todos los resultados 😱\nLa región de Arica y Parinacota tiene una distribución entre hombres y mujeres de 47,95% hombres y 50,54% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Arica y Parinacota cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 94.037 personas, mientras que el con menor población es no binario, con 146. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 414, 200 y 146 personas respectivamente, siendo transmasculino el de mayor población, con 414 personas en total, lo que representa un 0,2% de la población regional.\nLa región de Tarapacá tiene una distribución entre hombres y mujeres de 48,19% hombres y 50,15% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Tarapacá cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 136.577 personas, mientras que el con menor población es no binario, con 203. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 854, 370 y 203 personas respectivamente, siendo transmasculino el de mayor población, con 854 personas en total, lo que representa un 0,3% de la población regional.\nLa región de Antofagasta tiene una distribución entre hombres y mujeres de 48,16% hombres y 50,44% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Antofagasta cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 243.287 personas, mientras que el con menor población es no binario, con 314. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 1.315, 514 y 314 personas respectivamente, siendo transmasculino el de mayor población, con 1.315 personas en total, lo que representa un 0,3% de la población regional.\nLa región de Atacama tiene una distribución entre hombres y mujeres de 48,42% hombres y 50,40% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Atacama cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 114.109 personas, mientras que el con menor población es no binario, con 121. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 473, 197 y 121 personas respectivamente, siendo transmasculino el de mayor población, con 473 personas en total, lo que representa un 0,2% de la población regional.\nLa región de Coquimbo tiene una distribución entre hombres y mujeres de 47,33% hombres y 51,53% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Coquimbo cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 331.448 personas, mientras que el con menor población es no binario, con 508. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 1.446, 594 y 508 personas respectivamente, siendo transmasculino el de mayor población, con 1.446 personas en total, lo que representa un 0,2% de la población regional.\nLa región de Valparaíso tiene una distribución entre hombres y mujeres de 46,82% hombres y 51,84% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es menor al promedio nacional de hombres. El censo en la región de Valparaíso cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 780.209 personas, mientras que el con menor población es transfemenino, con 1.369. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, no binario y transfemenino con 3.003, 2.161 y 1.369 personas respectivamente, siendo transmasculino el de mayor población, con 3.003 personas en total, lo que representa un 0,2% de la población regional.\nLa región Metropolitana de Santiago tiene una distribución entre hombres y mujeres de 47,04% hombres y 51,54% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es menor al promedio nacional de hombres. El censo en la región de Metropolitana de Santiago cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 3.010.084 personas, mientras que el con menor población es transfemenino, con 5.838. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, no binario y transfemenino con 13.754, 8.230 y 5.838 personas respectivamente, siendo transmasculino el de mayor población, con 13.754 personas en total, lo que representa un 0,2% de la población regional.\nLa región del Libertador General Bernardo O'Higgins tiene una distribución entre hombres y mujeres de 48,03% hombres y 51,10% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Libertador General Bernardo O'Higgins cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 392.682 personas, mientras que el con menor población es no binario, con 474. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 1.514, 646 y 474 personas respectivamente, siendo transmasculino el de mayor población, con 1.514 personas en total, lo que representa un 0,2% de la población regional.\nLa región del Maule tiene una distribución entre hombres y mujeres de 47,47% hombres y 51,67% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Maule cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 451.238 personas, mientras que el con menor población es no binario, con 438. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 1.953, 759 y 438 personas respectivamente, siendo transmasculino el de mayor población, con 1.953 personas en total, lo que representa un 0,2% de la población regional.\nLa región de Ñuble tiene una distribución entre hombres y mujeres de 47,01% hombres y 52,21% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es menor al promedio nacional de hombres. El censo en la región de Ñuble cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 211.167 personas, mientras que el con menor población es no binario, con 181. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 788, 289 y 181 personas respectivamente, siendo transmasculino el de mayor población, con 788 personas en total, lo que representa un 0,2% de la población regional.\nLa región del Biobío tiene una distribución entre hombres y mujeres de 46,90% hombres y 52,34% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es menor al promedio nacional de hombres. El censo en la región de Biobío cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 662.218 personas, mientras que el con menor población es no binario, con 1.016. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 2.907, 1.141 y 1.016 personas respectivamente, siendo transmasculino el de mayor población, con 2.907 personas en total, lo que representa un 0,2% de la población regional.\nLa región de La Araucanía tiene una distribución entre hombres y mujeres de 46,92% hombres y 52,13% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es menor al promedio nacional de hombres. El censo en la región de La Araucanía cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 408.229 personas, mientras que el con menor población es no binario, con 533. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 1.410, 570 y 533 personas respectivamente, siendo transmasculino el de mayor población, con 1.410 personas en total, lo que representa un 0,2% de la población regional.\nLa región de Los Ríos tiene una distribución entre hombres y mujeres de 47,30% hombres y 51,67% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Los Ríos cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 161.832 personas, mientras que el con menor población es transfemenino, con 198. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, no binario y transfemenino con 554, 388 y 198 personas respectivamente, siendo transmasculino el de mayor población, con 554 personas en total, lo que representa un 0,2% de la población regional.\nLa región de Los Lagos tiene una distribución entre hombres y mujeres de 48,10% hombres y 50,93% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Los Lagos cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 353.827 personas, mientras que el con menor población es no binario, con 458. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, transfemenino y no binario con 1.210, 463 y 458 personas respectivamente, siendo transmasculino el de mayor población, con 1.210 personas en total, lo que representa un 0,2% de la población regional.\nLa región de Aysén del General Carlos Ibáñez del Campo tiene una distribución entre hombres y mujeres de 48,38% hombres y 50,54% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Aysén del General Carlos Ibáñez del Campo cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 38.756 personas, mientras que el con menor población es transfemenino, con 43. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, no binario y transfemenino con 100, 81 y 43 personas respectivamente, siendo transmasculino el de mayor población, con 100 personas en total, lo que representa un 0,1% de la población regional.\nLa región de Magallanes y de la Antártica Chilena tiene una distribución entre hombres y mujeres de 48,58% hombres y 50,13% mujeres. En esta región, el porcentaje de hombres con respecto de mujeres es mayor al promedio nacional de hombres. El censo en la región de Magallanes y de la Antártica Chilena cuenta con estadísticas oficiales de 5 géneros distintos, excluyendo no respuestas. El género con mayor población es el femenino con 66.252 personas, mientras que el con menor población es transfemenino, con 123. Aparte de masculino y femenino, los demás géneros en la región son transmasculino, no binario y transfemenino con 260, 143 y 123 personas respectivamente, siendo transmasculino el de mayor población, con 260 personas en total, lo que representa un 0,2% de la población regional.\nContemplen LA CANTIDAD de texto que redactamos con el código anterior, donde no usamos nada demasiado complejo, solamente pequeñas piezas que juntas fueron armando el texto!\nImagina lo conveniente que es hacer esto si tienes que repetir un mismo análisis o descripción de datos de decenas o cientos de veces, o si necesitas redactar texto acerca de datos que puedan actualizarse o que pueden ser corregidos!\n","date":"2026-02-12T00:00:00Z","excerpt":"Luego de explorar o procesar un conjunto de datos, toca presentar los resultados. Si bien esto nos hace pensar en gráficos, tablas o reportes, el texto también es una forma de comunicar resultados que puede ser optimizada mediante la programación. En este tutorial veremos cómo redactar textos a partir de datos, incluso redactar párrafos complejos que describan la información de múltiples observaciones, usando los resultados oficiales de las preguntas de género del Censo 2024.","href":"https://bastianoleah.netlify.app/blog/redactar_texto/","tags":"texto ; automatización ; loops ; funciones ; género ; Chile","title":"Redactar textos basados en datos automáticamente con R: describiendo resultados del censo de población"},{"content":" Muchas gracias a todas las personas que participaron 🤍 Fue un placer poder enseñarles lo básico de estas herramientas a un curso mayoritariamente conformado por mujeres y personas LBGTIQ. Las grabaciones y las diapositivas están disponibles en este post! ¿Quieres entrar al mundo de los datos, pero no sabes cómo? Existen muchas herramientas para analizar datos, pero hay algo que casi todas tienen en común: la programación. Pero no es tan difícil como suena!\nEn este curso gratuito de R podrás aprender desde cero a usar programación para análisis de datos!\nR es un lenguaje diseñado para trabajar con datos. Además de exploración, transformación y análisis de datos, R permite hacer visualizaciones, animaciones, automatización de procesos, reportes, aplicaciones web, y mucho más.\nSu gracia es que es un lenguaje usado por personas de diversas disciplinas, y por lo mismo es un lenguaje orientado a ser usado por personas que no sean expertas en informática o ciencias de la computación.\nAprende desde cero a usar este lenguaje y complementa tu carrera con herramientas de programación que te abrirán muchas posibilidades.\nEl curso va dirigido a profesionales o estudiantes de humanidades y ciencias sociales que no tengan experiencia previa programando.\nLos cupos son limitados, y se aplicarán criterios de inclusión para la participación de grupos minoritarios (mujeres, disidencias de sexo y género, personas con discapacidad).\nLas clases serán online los días 3, 5, y 9 de febrero a las 7:30PM.\nEl curso se llevó a cabo de manera online y sincrónica los días 3, 5, y 9 de febrero, a un curso con cupos limitados que priorizaron la participación de mujeres y personas LGBTIQ+.\nDiapositivas En las diapositivas del curso se resumen los temas, se muestran ejemplos de código, y se entregan enlaces a los datos necesarios y a los tutoriales para profundizar.\nDiapositivas También puedes descargar las diapositivas en formato PDF aquí.\nCódigo En este enlace puedes acceder al repositorio de GitHub que contiene el código que veremos en el curso, los datos que trabajemos, y también el código de R que genera las diapositivas con Quarto Revealjs.\nCódigo del curso También puedes revisar el código para crear las diapositivas con Quarto.\nScripts de cada clase Scripts vistos en clase, con comentarios explicativos de cada paso y comando. Se sugiere revisar a la par de las diapositivas, que también contienen enlaces a tutoriales más detallados de cada tema.\nScript clase 1 Script clase 2 Script clase 3 Grabaciones Clase 1: introducción a R Grabación clase 1 Clase 2: trabajando con datos en R Grabación clase 2 Clase 3: manipulación de datos en R Grabación clase 3 Puedes descargar todos los materiales del curso (código de clases, diapositivas y enlaces) en un solo archivo comprimido acá:\nDescargar curso ","date":"2026-02-09T00:00:00Z","excerpt":"Existen muchas herramientas para analizar datos, pero hay algo que casi todas tienen en común: la programación. En este curso enseño lo básico de programación en R desde cero, orientado a las ciencias sociales y humanidades. Las diapositivas, el código y las grabaciones están disponibles!","href":"https://bastianoleah.netlify.app/blog/curso_gratis_r_intro_1/","tags":"blog ; básico ; videos","title":"Curso gratuito: introducción al análisis de datos con R"},{"content":"Me pidieron reproducir con R un gráfico de puente que habían hecho en Excel, para poder crear decenas de versiones del mismo gráfico a partir de datos distintos y/o actualizados.\nÉste era el gráfico original:\nEl gráfico de puente original Los gráficos de puente son gráficos donde las barras representan el cambio de un valor original en el tiempo, o bien, la contribución de varias cifras a un valor final, en la forma de barras escalonadas.\nNunca había hecho uno, y tampoco sabía cómo se llamaban como para buscar instrucciones, así que me puse a intentarlo!\nÉstos son los datos originales:\ndatos \u0026lt;- tibble::tribble( ~valor, ~factor, ~region, 0.267569260593819, \u0026#34;produc\u0026#34;, 1, 0.658155474611586, \u0026#34;merc laboral\u0026#34;, 1, 0.124032556711811, \u0026#34;innov\u0026#34;, 1, 0.346384929903161, \u0026#34;empres\u0026#34;, 1, 0.197669772542558, \u0026#34;gob\u0026#34;, 1, 0.1331561948028, \u0026#34;salud\u0026#34;, 1, 1.18645838429506, \u0026#34;segur\u0026#34;, 1, -0.25626249726395, \u0026#34;cap ingr\u0026#34;, 1, 0.0845792632225653, \u0026#34;igualdad\u0026#34;, 1, 0.58032570060242, \u0026#34;ent viv\u0026#34;, 1, 0.107679485231535, \u0026#34;med amb\u0026#34;, 1, 0.0248454494479557, \u0026#34;cap nat\u0026#34;, 1, -0.110064921904146, \u0026#34;cap hum\u0026#34;, 1, 0.782311601326303, \u0026#34;cap fis\u0026#34;, 1, ) Lo primero que hice fue explorar visualmente los datos:\nlibrary(ggplot2) # definir un tema theme_set( theme_classic(base_family = \u0026#34;Rubik\u0026#34;, base_size = 10, ink = \u0026#34;#2d4a6d\u0026#34;) ) # visualizar datos |\u0026gt; ggplot() + aes(factor, valor) + geom_col() Luego fui pensando qué características tenían que tener los datos para poder visualizarlos como en el gráfico original 🤔\nProcesamiento de datos Lo primero que se necesita es ir sumando los valores consecutivamente, dado que el gráfico va visualizando un puente donde cada barra parte desde el alto de la anterior. Así que usamos la función cumsum() para ir sumando los valores de cada fila con los anteriores, de manera acumulativa:\nlibrary(dplyr) datos \u0026lt;- datos |\u0026gt; mutate(suma = cumsum(valor)) datos # A tibble: 14 × 4 valor factor region suma \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 0.268 produc 1 0.268 2 0.658 merc laboral 1 0.926 3 0.124 innov 1 1.05 4 0.346 empres 1 1.40 5 0.198 gob 1 1.59 6 0.133 salud 1 1.73 7 1.19 segur 1 2.91 8 -0.256 cap ingr 1 2.66 9 0.0846 igualdad 1 2.74 10 0.580 ent viv 1 3.32 11 0.108 med amb 1 3.43 12 0.0248 cap nat 1 3.45 13 -0.110 cap hum 1 3.34 14 0.782 cap fis 1 4.13 Con esto obtenemos los valores superiores de cada barra del gráfico.\nDespués necesitamos saber los valores inferiores de las barras, que en este tipo de gráfico aparecen encima de las barras anteriores. Para saber esto, tomamos la suma acumulada que calculamos antes y le restamos el valor de cada una de las barras; de esta forma obtenemos el valor acumulado menos el valor de la barra presente; es decir, su base:\ndatos \u0026lt;- datos |\u0026gt; mutate(base = suma - valor) datos # A tibble: 14 × 5 valor factor region suma base \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 0.268 produc 1 0.268 0 2 0.658 merc laboral 1 0.926 0.268 3 0.124 innov 1 1.05 0.926 4 0.346 empres 1 1.40 1.05 5 0.198 gob 1 1.59 1.40 6 0.133 salud 1 1.73 1.59 7 1.19 segur 1 2.91 1.73 8 -0.256 cap ingr 1 2.66 2.91 9 0.0846 igualdad 1 2.74 2.66 10 0.580 ent viv 1 3.32 2.74 11 0.108 med amb 1 3.43 3.32 12 0.0248 cap nat 1 3.45 3.43 13 -0.110 cap hum 1 3.34 3.45 14 0.782 cap fis 1 4.13 3.34 Luego necesitamos algo simple: indicar si el valor de cada barra es positivo o negativo, así que usamos ifelse():\ndatos \u0026lt;- datos |\u0026gt; mutate(direcc = if_else(valor \u0026gt; 0, \u0026#34;Positivo\u0026#34;, \u0026#34;Negativo\u0026#34;)) datos # A tibble: 14 × 6 valor factor region suma base direcc \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 0.268 produc 1 0.268 0 Positivo 2 0.658 merc laboral 1 0.926 0.268 Positivo 3 0.124 innov 1 1.05 0.926 Positivo 4 0.346 empres 1 1.40 1.05 Positivo 5 0.198 gob 1 1.59 1.40 Positivo 6 0.133 salud 1 1.73 1.59 Positivo 7 1.19 segur 1 2.91 1.73 Positivo 8 -0.256 cap ingr 1 2.66 2.91 Negativo 9 0.0846 igualdad 1 2.74 2.66 Positivo 10 0.580 ent viv 1 3.32 2.74 Positivo 11 0.108 med amb 1 3.43 3.32 Positivo 12 0.0248 cap nat 1 3.45 3.43 Positivo 13 -0.110 cap hum 1 3.34 3.45 Negativo 14 0.782 cap fis 1 4.13 3.34 Positivo Ahora falta el último detalle: el gráfico original tiene una barra al final que muestra el total o la suma acumulada de todas las barras, pero empezando desde abajo.\nComo nuestro dataframe tiene una fila por cada barra, tendríamos que agregar una fila nueva al final con el valor total, y con la base en cero. Para ésto podemos usar la función add_row(), a la que le podemos entregar los valores de una fila nueva, pero no escribiéndolos a mano, sino basándonos en los datos:\ndatos \u0026lt;- datos |\u0026gt; add_row(valor = sum(datos$valor), suma = sum(datos$valor), base = 0, region = 1, factor = \u0026#34;total\u0026#34;, direcc = \u0026#34;Variación\u0026#34;) tail(datos) # A tibble: 6 × 6 valor factor region suma base direcc \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 0.580 ent viv 1 3.32 2.74 Positivo 2 0.108 med amb 1 3.43 3.32 Positivo 3 0.0248 cap nat 1 3.45 3.43 Positivo 4 -0.110 cap hum 1 3.34 3.45 Negativo 5 0.782 cap fis 1 4.13 3.34 Positivo 6 4.13 total 1 4.13 0 Variación Finalmente ordenamos los valores del eje horiontal en el orden que vienen en el dataframe, para que no aparezcan por orden alfabético (el orden por defecto que le da {ggplot2} a las variables discretas que no están ordenadas)\ndatos \u0026lt;- datos |\u0026gt; mutate(factor = forcats::fct_inorder(factor)) Visualización Ahora queda hacer la visualización. Volvemos a probar con las barras:\ndatos |\u0026gt; ggplot() + aes(x = factor, y = valor, fill = direcc) + geom_col() + guides(fill = guide_legend(position = \u0026#34;top\u0026#34;)) Claramente no podemos usar columnas (geom_col()) porque necesitamos indicar el punto de partida de cada columna en el eje vertical.\nSi bien se pueden hacer rectángulos con {ggplot2}, preferí usar líneas con la función geom_segment(), que recibe valores para x e y, pero también para el final de x y el final de y, permitiendo longitudes personalizadas. Al final, ¿qué es una columna, sino una línea gruesa? 🧠\ndatos |\u0026gt; ggplot() + aes(x = factor, y = valor, color = direcc) + geom_segment( aes(xend = factor, y = base, yend = suma) ) + guides(fill = guide_legend(position = \u0026#34;top\u0026#34;)) ¡Estamos cerca! Ahora engrosamos las líneas con el argumento linewidth dentro de geom_segment(), agregamos texto con geom_text() que se posicione según si es positivo o negativo, una escala de colores con scale_color_manual(), y algunos detalles en el tema.\nEl otro cambio principal es agregar líneas horizontales que conecten cada barra con la otra. Lamentablemente, creo que el largo de estas barras es específico a la cantidad de barras y el ancho del gráfico, entre otros factores, así que habrá que experimentar.\ndatos |\u0026gt; ggplot() + aes(x = factor, y = valor, color = direcc) + # barras con alto personalizado geom_segment( aes(xend = factor, y = base, yend = suma), linewidth = 12) + # líneas horizontales que conecten las barras geom_segment( aes(x = as.numeric(factor)-0.4, xend = ifelse(factor == \u0026#34;total\u0026#34;, as.numeric(factor)+0.4, as.numeric(lead(factor))+0.4), y = suma-0.005, yend = suma-0.005), alpha = 0.5) + # textos encima o debajo de barras geom_text(aes(y = suma, label = round(valor, 1), nudge_y = ifelse(valor \u0026gt; 0, 0.1, -0.1)), size = 3, show.legend = F) + # espaciado vertical scale_y_continuous(expand = expansion(c(0, 0.1))) + # escala de colores scale_color_manual(values = c(\u0026#34;Positivo\u0026#34; = \u0026#34;#2d4a6d\u0026#34;, \u0026#34;Negativo\u0026#34; = \u0026#34;#b52141\u0026#34;, \u0026#34;Variación\u0026#34; = \u0026#34;#668243\u0026#34;)) + # leyenda guides(color = guide_legend(title = NULL, position = \u0026#34;top\u0026#34;, override.aes = list(linewidth = 6))) + # temas theme(axis.text.x = element_text(angle = 45, hjust = 1), axis.title = element_blank(), legend.key.width = unit(2, \u0026#34;mm\u0026#34;), panel.grid.major.y = element_line(linetype = \u0026#34;dashed\u0026#34;, linewidth = 0.2, color = \u0026#34;grey80\u0026#34;)) Listo! Quedó idéntico al original, y también más elegante, pero lo más importante: quedó reproducible y listo para empaquetarse en una función que te permita crear cientos de estos mismos gráficos con datos distintos o actualizados.\n","date":"2026-02-05T00:00:00Z","excerpt":"Me pidieron reproducir con R un gráfico de puente que habían hecho en Excel, para poder crear decenas de versiones del mismo gráfico a partir de datos distintos y/o actualizados. En este post veremos cómo hacer gráficos de puente con R, que son gráficos donde las barras representan el cambio de un valor original en el tiempo, o bien, la contribución de varias cifras a un valor final, en la forma de barras escalonadas.","href":"https://bastianoleah.netlify.app/blog/ggplot_puente/","tags":"visualización de datos ; gráficos","title":"Gráficos de puente en `{ggplot2}`"},{"content":"Un gráfico ternario es una visualización de datos que representa tres variables numéricas dentro de un triángulo. En este post usaremos el paquete {ggtern} para crearlos con R y {ggplot2}.\nPost en construcción! A medida que voy aprendiendo iré complementando. Primero instalamos el paquete:\ninstall.packages(\u0026#34;ggtern\u0026#34;) Gráfico ternario básico Veamos lo más básico de este tipo de diagramas. Necesitamos tres variables numéricas:\nlibrary(dplyr) datos \u0026lt;- tibble(x = 33, y = 33, z = 33) Para visualizarlas dentro de un triángulo, usamos la función ggtern() en vez de ggplot(), y definimos las tres variables en aes():\nlibrary(ggplot2) library(ggtern) datos |\u0026gt; ggtern() + aes(x = x, y = y, z = z) + geom_point(size = 4, alpha = 0.7) Aparentemente {ggtern} sobreescribe y cambia varias cosas del funcionamiento interno de `{ggplot2}, por lo que puede que provoque problemas o incompatibilidades. Recuerda que si tienes problemas, siempre se puede Reiniciar R desde el menú Session de RStudio. Como las tres variables tienen el mismo valor, el punto queda en el centro del triángulo.\nSi cambiamos los datos levemente, vemos que el punto se representa dentro del triángulo como si los vértices ejercieran una atracción.\ndatos \u0026lt;- tibble(x = 20, y = 30, z = 60) datos |\u0026gt; ggtern() + aes(x = x, y = y, z = z) + geom_point(size = 4, alpha = 0.7) Esto se debe a que, como se trata de tres variables, no pueden representarse exactamente como sí podrían en un plano cartesiano, sino que los valores son escalados entre 0 y 100. En este sentido, los valores que aparecen en los ejes refieren al porcentaje de la variable con respecto a las demás.\nPor ejemplo, si hacemos que una variable concentre el 100%, el punto aparecerá exactamente en el vértice.\ndatos \u0026lt;- tibble(x = 0, y = 100, z = 0) datos |\u0026gt; ggtern() + aes(x = x, y = y, z = z) + geom_point(size = 8, alpha = 0.7) Pero lo mismo va a pasar si la suma de las tres variables no es 100, dado que, como dijimos, las variables se escalan.\nSi los ejes resultan distractores para la interpretación visual del gráfico, se pueden ocultar:\ndatos \u0026lt;- tibble(x = 33, y = 33, z = 33) datos |\u0026gt; ggtern() + aes(x = x, y = y, z = z) + geom_point(size = 4, alpha = 0.7) + ggtern::theme_hideticks() + ggtern::theme_hidelabels() Visualizar tres variables Teniendo una tabla de datos con tres variables, las mapeamos en aes() y obtenemos sus posiciones dentro del triángulo:\ndatos \u0026lt;- tibble(x = c(10, 30, 60, 20, 40, 80), y = c(30, 60, 20, 40, 30, 10), z = c(60, 20, 20, 40, 30, 10), grupo = c(\u0026#34;B\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;A\u0026#34;, \u0026#34;A\u0026#34;)) datos |\u0026gt; ggtern() + aes(x = x, y = y, z = z, shape = grupo, color = x) + geom_point(size = 4, alpha = 0.8) + theme_linedraw() + ggtern::theme_hideticks() + ggtern::theme_hidelabels() En este ejemplo podemos ver que los valores del grupo A tienen mayor valor en la variable x que los del grupo b, los cuales a su vez tienen mayor valor de z.\nVisualizar una trayectoria Si tenemos datos que cuentan con mediciones consecutivas, podemos graficar una línea que va pasando por todos sus valores:\ndatos \u0026lt;- tibble::tribble( ~año, ~a, ~b, ~c, 2013, 41.68, 44.98, 50.62, 2014, 41.75, 44.18, 50.19, 2015, 42.58, 45.19, 49.70, 2016, 42.91, 46.61, 51.27, 2017, 43.60, 48.17, 51.40, 2018, 43.91, 48.71, 50.68, 2019, 44.35, 48.52, 49.55, 2020, 43.70, 46.53, 47.33, 2021, 44.84, 51.60, 50.65, 2022, 46.62, 53.56, 49.30, 2023, 47.13, 50.73, 49.65, 2024, 47.32, 50.94, 48.93, 2025, 47.53, 50.86, 48.78, ) datos |\u0026gt; ggtern() + aes(x = a, y = b, z = c, color = año, linewidth = año) + geom_line(lineend = \u0026#34;round\u0026#34;) + ggtern::theme_hideticks() + ggtern::theme_zoom_center(.4) + scale_color_continuous(labels = scales::label_number(big.mark = \u0026#34;\u0026#34;, accuracy = 1)) + scale_linewidth_continuous(range = c(0.4, 1.9), labels = scales::label_number(big.mark = \u0026#34;\u0026#34;, accuracy = 1)) + theme_bw() + ggtern::theme_hideticks() + ggtern::theme_hidelabels() + guides(color = guide_legend()) + labs(x = \u0026#34;A\u0026#34;, y = \u0026#34;B\u0026#34;, z = \u0026#34;C\u0026#34;) En este gráfico podemos ver que la unidad de información empieza con alto valor de C, el cual va disminuyendo en la medida que aumenta su valor de B y levemente aumenta su valor de A, terminando en una posición más balanceada.\nAgregar un fondo en degradado Esto lo encontré demasiado bonito! Para explicar mejor la relación de atracción o proporcionalidad entre las tres variables, podemos pensarlo como un espectro de colores, donde cada variable tiene un color. De esta forma se puede expresar mejor la idea de que la posición de los puntos es una proporción entre los valores de las tres variables, una especie de mezcla entre ellas.\nPara crear un fondo creamos la siguiente función, que toma tres colores y crea una matriz de valores que, al mapearse en el triángulo, generarán un degradado.\n# función para crear fondo degradado en gráfico ternario ggtern_degradado \u0026lt;- function(color_x = \u0026#34;#FF6B6B\u0026#34;, color_y = \u0026#34;#4ECDC4\u0026#34;, color_z = \u0026#34;#FFE66D\u0026#34;, resolucion = 150) { # crear grilla de fondo fondo \u0026lt;- expand.grid( x = seq(0, 1, length.out = resolucion), y = seq(0, 1, length.out = resolucion) ) # calcular z y filtrar puntos fuera del triángulo fondo \u0026lt;- fondo |\u0026gt; mutate(z = 1 - x - y) |\u0026gt; filter(x \u0026gt;= 0, y \u0026gt;= 0, z \u0026gt;= 0, x \u0026lt;= 1, y \u0026lt;= 1, z \u0026lt;= 1) # convertir colores a RGB rgb_x \u0026lt;- col2rgb(color_x) / 255 rgb_y \u0026lt;- col2rgb(color_y) / 255 rgb_z \u0026lt;- col2rgb(color_z) / 255 # interpolar colores fondo$color \u0026lt;- rgb( fondo$x * rgb_x[1] + fondo$y * rgb_y[1] + fondo$z * rgb_z[1], fondo$x * rgb_x[2] + fondo$y * rgb_y[2] + fondo$z * rgb_z[2], fondo$x * rgb_x[3] + fondo$y * rgb_y[3] + fondo$z * rgb_z[3] ) return(fondo) } Ejecutamos la función con los tres colores que elijamos, y obtendremos una matriz de valores con sus coordenadas y respectivos colores.\nfondo \u0026lt;- ggtern_degradado(color_x = \u0026#34;#FF6B6B\u0026#34;, color_y = \u0026#34;#4ECDC4\u0026#34;, color_z = \u0026#34;#FFE66D\u0026#34;, resolucion = 200) head(fondo) x y z color 1 0.000000000 0 1.0000000 #FFE66D 2 0.005025126 0 0.9949749 #FFE56D 3 0.010050251 0 0.9899497 #FFE56D 4 0.015075377 0 0.9849246 #FFE46D 5 0.020100503 0 0.9798995 #FFE46D 6 0.025125628 0 0.9748744 #FFE36D Luego usamos esos datos para llenar de puntos el triángulo! Debido a la gran cantidad de puntos y sus cambios leves de colores, se crea un degradado suave:\nggtern() + aes(x = x, y = y, z = z) + # fondo con degradado geom_point(data = fondo, aes(x = x, y = y, z = z), color = fondo$color) + theme_bw() + ggtern::theme_hideticks() + ggtern::theme_hidelabels() Para aclarar los colores del fondo podemos usar la función col_lighter() de {scales}, como detallamos en el post sobre colores en {ggplot2}:\nfondo \u0026lt;- fondo |\u0026gt; mutate(color = scales::col_lighter(color, 10)) ggtern() + geom_point(data = fondo, aes(x = x, y = y, z = z), color = fondo$color) + theme_bw() + ggtern::theme_hideticks() + ggtern::theme_hidelabels() Finalmente podemos agregar los datos en otra capa de geom_point() sobre el fondo:\ndatos \u0026lt;- data.frame( x = c(0.2, 0.4, 0.3, 0.5, 0.2, 0.5, 0.4, 0.3, 0.2, 0.4), y = c(0.3, 0.2, 0.4, 0.2, 0.3, 0.2, 0.4, 0.3, 0.5, 0.2), z = c(0.5, 0.4, 0.3, 0.2, 0.4, 0.3, 0.2, 0.4, 0.2, 0.3) ) datos |\u0026gt; ggtern() + aes(x = x, y = y, z = z) + # fondo con degradado geom_point(data = fondo, aes(x = x, y = y, z = z), color = fondo$color) + # datos geom_point(size = 2.5, alpha = 0.6) + theme_bw() + ggtern::theme_hideticks() + ggtern::theme_hidelabels() Referencias Hamilton NE, Ferry M (2018). \u0026ldquo;ggtern: Ternary Diagrams Using ggplot2.\u0026rdquo; Journal of Statistical Software, Code Snippets, 87(3), 1-17. doi: 10.18637/jss.v087.c03 Claude Sonnet 4.5 (para la función de fondo degradado) ","date":"2026-01-28T00:00:00Z","excerpt":"Un gráfico ternario es una visualización de datos que representa tres variables numéricas dentro de un triángulo. En este post usaremos el paquete `{ggtern}` para crearlos con R y `{ggplot2}`.","href":"https://bastianoleah.netlify.app/blog/ggplot_ternario/","tags":"visualización de datos","title":"Gráficos ternarios o triangulares de tres variables en `{ggplot2}`"},{"content":"Uno de los primeros pasos al trabajar con datos es explorarlos, y si bien existen herramientas estadísticas para resumir datos y obtener medidas que los describan, debemos recordar que visualizar los datos es igual de importante que obtener sus estadísticas descriptivas 🤯\nVeamos un ejemplo clásico de ésto: el cuarteto de Anscombe. Este conjunto de datos, creado por Francis John Anscombe en 1973, está compuesto por cuatro grupos distintos, con las mismas estadísticas descriptivas (media, varianza, correlación y regresión lineal), pero que al visualizarlos revelan distribuciones muy diferentes!\nEl cuarteto de Anscombe viene por defecto en R:\ndatasets::anscombe x1 x2 x3 x4 y1 y2 y3 y4 1 10 10 10 8 8.04 9.14 7.46 6.58 2 8 8 8 8 6.95 8.14 6.77 5.76 3 13 13 13 8 7.58 8.74 12.74 7.71 4 9 9 9 8 8.81 8.77 7.11 8.84 5 11 11 11 8 8.33 9.26 7.81 8.47 6 14 14 14 8 9.96 8.10 8.84 7.04 7 6 6 6 8 7.24 6.13 6.08 5.25 8 4 4 4 19 4.26 3.10 5.39 12.50 9 12 12 12 8 10.84 9.13 8.15 5.56 10 7 7 7 8 4.82 7.26 6.42 7.91 11 5 5 5 8 5.68 4.74 5.73 6.89 Vemos que hay 8 columnas, aparentemente en dos pares de 4 columnas: las que corresponden al eje x de cada grupo, y las del eje y.\nSi encuentras que los datos están desordenados, es porque sí, vienen desordenados 😒\nLimpiar datos Los datos tienen un problema común: las columnas contienen en sus nombres información que deberían ser datos (la variable y el grupo al que pertenece). Esto se ven en que, por ejemplo, la columna x1 te dice dos cosas a la vez: que la variable se llama x, y que pertenece al grupo 1 🙄\nOrdenemos un poco estos datos llevándolos al formato largo, para hacer que cada fila represente una observación individual, y que cada columna presente una sola cosa a la vez:\nlibrary(dplyr) library(tidyr) anscombe_largo \u0026lt;- anscombe |\u0026gt; pivot_longer(cols = everything(), names_to = \u0026#34;grupo\u0026#34;, values_to = \u0026#34;valor\u0026#34;) |\u0026gt; separate(grupo, sep = 1, into = c(\u0026#34;variable\u0026#34;, \u0026#34;grupo\u0026#34;)) anscombe_largo # A tibble: 88 × 3 variable grupo valor \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 x 1 10 2 x 2 10 3 x 3 10 4 x 4 8 5 y 1 8.04 6 y 2 9.14 7 y 3 7.46 8 y 4 6.58 9 x 1 8 10 x 2 8 # ℹ 78 more rows Usamos pivot_longer() para pivotar los datos y hacer que todas las cifras estén en una sola columna, y los nombres de todas las columnas en otra. Luego usamos separate() para separar la columna grupo en dos: una con la variable, y otra con el grupo. Mucho mejor! ☺️\nSi quieres aprender en detalle cómo transformar datos para cambiarlos dal formato largo o anch, revisa este tutorial sobre pivotar datos con {tidyr}. Calcular estadísticos descriptivos Con los datos ordenados, tenemos todas las cifras en una sola columna. Ahora podemos calcular sus estadísticos descriptivos.\nPara aprender a calcular estadísticos descriptivos, revisa este post. Para calcular resúmenes de datos usamos la función summarize(). Ella nos permite calcular, por ejemplo, un promedio de todas las observaciones. Pero como los datos vienen en grupos, agrupamos primero con group_by():\nestadisticos \u0026lt;- anscombe_largo |\u0026gt; group_by(grupo) |\u0026gt; summarize(promedio = mean(valor), mediana = median(valor), varianza = var(valor)) grupo promedio mediana varianza 1 8.250455 8.020 7.792033 2 8.250455 8.440 7.792205 3 8.250000 7.635 7.790533 4 8.250455 8.000 7.790119 Al calcular algunos estadísticos descriptivos (promedio, mediana y varianza) confirmamos que los cuatro grupos del cuarteto tienen cifras casi idénticas!\nSi quieres aprender a usar summarize(), revisa este tutorial sobre resúmenes de datos con {dplyr}. Visualizar los datos Para comparar los grupos del cuarteto de Anscombe, queremos graficar un gráfico de dispersión, y para eso necesitamos tener los valores del eje x e y en columnas distintas. Actualmente están en una sola columna (valor), y se distinguen por el valor de la columna variable, así que podemos usar pivot_wider() para volver a pivotar los datos hacia el formato ancho, y pasar de una columna descrita por otra, a dos columnas con nombres distintos:\n# crear identificación única de cada valor del conjunto anscombe_ids \u0026lt;- anscombe_largo |\u0026gt; group_by(grupo, variable) |\u0026gt; mutate(id = row_number()) |\u0026gt; ungroup() # pivotar los valores hacia el ancho, creando dos columnas según `variable` anscombe_ancho \u0026lt;- anscombe_ids |\u0026gt; pivot_wider(names_from = variable, values_from = valor) |\u0026gt; arrange(grupo, id) |\u0026gt; # ordenar select(-id) # sacar variable id anscombe_ancho # A tibble: 44 × 3 grupo x y \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 1 10 8.04 2 1 8 6.95 3 1 13 7.58 4 1 9 8.81 5 1 11 8.33 6 1 14 9.96 7 1 6 7.24 8 1 4 4.26 9 1 12 10.8 10 1 7 4.82 # ℹ 34 more rows Ahora los datos están en un formato mucho más simple e intuitivo: cada fila es una observación, separadas por la variable grupo, cuyos valores están en las columnas x e y.\nPara visualizar los datos cargamos {ggplot2}:\nlibrary(ggplot2) Antes definimos algunas cositas para que el gráfico se vea bonito:\nVer código de temas y tipografías Vamos a configurar una tipografía para el gráfico, y vamos a cambiar los colores del gráfico.\nlibrary(showtext) # descargar una tipografía desde google fonts font_add_google(name = \u0026#34;Atkinson Hyperlegible\u0026#34;) showtext_auto() showtext_opts(dpi = 200) # aplicar tema de colores theme_set( theme_grey(paper = \u0026#34;#EAD2FA\u0026#34;, ink = \u0026#34;#6E3A98\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;, base_family = \u0026#34;Atkinson Hyperlegible\u0026#34;) ) Para aprender a crear gráficos con {ggplot2}, revisa este tutorial introductorio a la visualización de datos. Al graficar los datos de cada grupo, podemos ver que las distribuciones son muy diferentes entre sí, a pesar de tener estadísticas descriptivas similares.\nanscombe_ancho |\u0026gt; ggplot() + aes(x, y) + # línea de regresión geom_smooth(method = \u0026#34;lm\u0026#34;, se = FALSE, fullrange = T, color = alpha(\u0026#34;#9069C0\u0026#34;, 0.6)) + # puntitos geom_point(size = 2.5, alpha = 0.8) + # separar grupos en gráficos distintos facet_wrap(~grupo, labeller = as_labeller(~glue::glue(\u0026#34;Grupo {.x}\u0026#34;))) + # textos labs(title = \u0026#34;Cuarteto de Anscombe\u0026#34;, subtitle = \u0026#34;Conjuntos de datos con mismas estadísticas pero distintas distribuciones\u0026#34;) + # título en negrita theme(plot.title = element_text(size = 14, face = \u0026#34;bold\u0026#34;)) Si nos hubiéramos quedado con las estadísticas descriptivas, habríamos pasado por alto las diferencias entre los grupos! Moraleja: siempre hacer gráficos exploratorios para comprender con qué estamos trabajando 🤓☝🏼\nBonus: ¿datasaurio? El datasaurio aparece en el paquete {datasauRus}_ y es un conjunto de datos que demuestra el mismo principio, pero de forma más extrema: cada uno de los 13 conjuntos tiene las mismas estadísticas descriptivas, pero visualmente son todas distintas, y uno de ellos tiene forma de dinosaurio! 🦖\ninstall.packages(\u0026#34;datasauRus\u0026#34;) library(dplyr) library(datasauRus) datasaurus_dozen |\u0026gt; group_by(dataset) |\u0026gt; summarize( mean_x = mean(x), mean_y = mean(y), std_dev_x = sd(x), std_dev_y = sd(y), corr_x_y = cor(x, y) ) # A tibble: 13 × 6 dataset mean_x mean_y std_dev_x std_dev_y corr_x_y \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 away 54.3 47.8 16.8 26.9 -0.0641 2 bullseye 54.3 47.8 16.8 26.9 -0.0686 3 circle 54.3 47.8 16.8 26.9 -0.0683 4 dino 54.3 47.8 16.8 26.9 -0.0645 5 dots 54.3 47.8 16.8 26.9 -0.0603 6 h_lines 54.3 47.8 16.8 26.9 -0.0617 7 high_lines 54.3 47.8 16.8 26.9 -0.0685 8 slant_down 54.3 47.8 16.8 26.9 -0.0690 9 slant_up 54.3 47.8 16.8 26.9 -0.0686 10 star 54.3 47.8 16.8 26.9 -0.0630 11 v_lines 54.3 47.8 16.8 26.9 -0.0694 12 wide_lines 54.3 47.8 16.8 26.9 -0.0666 13 x_shape 54.3 47.8 16.8 26.9 -0.0656 datasaurus_dozen |\u0026gt; ggplot() + aes(x = x, y = y, colour = dataset) + geom_point() + # theme_void() + theme(legend.position = \u0026#34;none\u0026#34;) + facet_wrap(~dataset, ncol = 3) + theme(axis.title = element_blank()) ¡Plop!\n","date":"2026-01-27T00:00:00Z","excerpt":"Uno de los primeros pasos al trabajar con datos es explorarlos, y si bien hay formas de resumir los datos y obtener medidas que los describan, no olvidemos que visualizar los datos es igual de importante que obtener sus estadísticas descriptivas. Ejemplifiquemos esto con un caso clásico!","href":"https://bastianoleah.netlify.app/blog/2026-01-27/","tags":"curiosidades ; básico ; estadísticas ; visualización de datos","title":"Por qué siempre visualizar los datos: el cuarteto de Anscombe"},{"content":" Índice Empezar a usar {shinytest2} Grabar el primer test Estructura de carpetas de tests Creando una prueba Ejecutar un test Expectativas del test Debugging Resultados Automatizar tests repetitivos Ejemplo de test automatizado Conclusión Al igual que la validación de datos, validar aplicaciones Shiny te permite crear un conjunto de pruebas para confirmar que tus aplicaciones funcionan bien sin tener que probarlas manualmente. En la práctica, significa programar un bot que apriete todos los botones de tu aplicación y obtenga capturas de pantalla que te confirmen que todo se ve bien, o realizar otros tipos de pruebas más complejas.\nEn esta guía aprenderás a utilizar {shinytest2} para automatizar el testeo de tus aplicaciones Shiny, asegurando su correcto funcionamiento a través de capturas de pantalla y otras validaciones automáticas.\nPrimero que nada, instalamos el paquete:\ninstall.packages(\u0026#34;shinytest2\u0026#34;) Empezar a usar {shinytest2} Lo primero es crear la infraestructura necesaria, ejecutando use_shinytest2() en la carpeta de tu aplicación (donde están los archivos app.R o server.R y ui.R):\nshinytest2::use_shinytest2() Si tu app está en otra carpeta ed tu proyecto, puedes especificar la ruta así:\nshinytest2::use_shinytest2(app_dir = \u0026#34;app/inversion_gores/\u0026#34;) Este comando solamente crea las carpetas de tests que son básicas para validación con {testthat}, pero no crea ningún test en particular.\nGrabar el primer test Una vez creada la carpeta test, grabamos nuestro primer test de manera interactiva, ejecutando:\nshinytest2::record_test() Este comando abre tu aplicación Shiny en un navegador web para que grabes tus interacciones con la app y así generar automáticamente tu primer test.\nAl interactuar con tu app, verás que en el lado derecho se registran las interacciones con la app, para que las mismas acciones puedan ser repetidas en los siguientes tests.\nElige uno de los botones de la parte superior para esperar (expect) que tu app retorne un valor (Expect Shiny value) o genere una captura de pantalla (Expect screenshot). Estas expectativas serán las pruebas con las que se evaluará el funcionamiento de tu app, así que es necesario generar al menos una.\nCuando termines de simular una visita a tu app presiona Save test and exit. Esto generará automáticamente un archivo de pruebas en la carpeta tests/testthat/ de tu aplicación Shiny, que se abrirá y de inmediato ejecutará la prueba en el fondo; es decir, tu app se ejecutará de manera invisible reproduciendo los pasos que tomaste.\nTambién se abrirá el script del test que se generó automáticamente, que empiaza con test-{x}.R y queda guardado en test/testthat/, el cual puedes editar manualmente para agregar más pruebas o modificar las existentes.\nEl script de prueba se verá más o menos así:\nlibrary(shinytest2) test_that(\u0026#34;{shinytest2} recording: ebc\u0026#34;, { app \u0026lt;- AppDriver$new(variant = platform_variant(), name = \u0026#34;ebc\u0026#34;, height = 694, width = 885) app$set_inputs(tabs_resultados = \u0026#34;Resultados por indicador\u0026#34;) app$set_inputs(indicador_ambito = \u0026#34;Ciclovías\u0026#34;) app$set_inputs(indicador_ambito = \u0026#34;Educación\u0026#34;) app$set_inputs(tabs_resultados = \u0026#34;Resultados por comuna\u0026#34;) app$set_inputs(region = \u0026#34;Tarapacá\u0026#34;) app$expect_screenshot() }) De esto se trata la validación de aplicaciones: programar interacciones con tu aplicación para evaluar que siemrpe retorne los valores esperados.\nEstructura de carpetas de tests Entendamos un poco qué hace {shinytest2} dentro de nuestra app.\nEn la carpeta de tu app aparecerá la carpeta tests. Dentro de ella está testthat.R, que contiene la función shinytest2::test_app(), con la que puedes ejecutar todas tus pruebas.\nScripts de tests Dentro de la carpeta testthat/ estarán todos los archivos de tests que hayas grabado o creado manualmente, que siguen la estructura test-{x}.R. Puedes agregar los tests que necesites siguiendo esta estructura de nombres.\nResultados de los tests Además, dentro de tests/testthat/ está _snaps, que contiene las capturas de pantalla tomadas en cada prueba.\nAquí hay un ejemplo de las carpetas de una app mía:\ninversion_gores/ |-- app.R |-- funciones.R |-- styles.css +-- tests/ |-- testthat.R +-- testthat/ |-- setup-shinytest2.R # archivo necesario |-- test-mapas.R # test para probar mapas |-- test-tablas.R # test para probar tablas +-- _snaps/ # carpeta con pantallazos +-- mac-4.4/ +-- test-mapas/ # resultados de tests de mapas |-- test-mapas-1.png |-- test-mapas-2.png |-- ... +-- test-tablas/ # resultados de tests de tablas |-- test-tablas-1.png |-- test-tablas-2.png |-- ... Creando una prueba Para empezar, puedes basarte en el test que se genera al simular y grabar tu interacción con shinytest2::record_test(), o crear un test manualmente.\nAquí hay un ejemplo de un test que navega a una pestaña de una app Shiny, cambia un input, espera a que la app cargue, y toma una captura de pantalla:\nlibrary(shinytest2) test_that(\u0026#34;{shinytest2} recording: inversion_gores_2\u0026#34;, { app \u0026lt;- AppDriver$new(variant = platform_variant(), name = \u0026#34;test-tablas\u0026#34;, height = 985, width = 1254) # cambiar de pestaña app$set_inputs(tabs = \u0026#34;Tabla\u0026#34;) # cambiar un input app$set_inputs(region = \u0026#34;2\u0026#34;) # esperar a que la app haya cargado app$wait_for_idle(200) # tomar pantallazo y guardar archivo con inputs/outputs app$expect_values() }) Ejecutar un test Dentro de tests/testthat/ están todos tus tests, y al abrirlos con RStudio aparecerá el botón Run tests, que si lo aprietas repite el test.\nSe abrirá un panel Tests donde se muestra el avance de los tests, y los avisos o errores que éstos pudieran tener.\nAl final podrás ver el resultado de las pruebas: pruebas con error, con avisos, saltadas y exitosas.\nAl terminar los tests, en la carpeta tests/testthat/_snaps/ estarán los resultados de las pruebas, que pueden ser capturas de pantalla o archivos JSON con los valores de los inputs y outputs de la app en cada prueba.\nEn mi caso, obtuve más de 50 pantallazos de mi aplicación, los cuales puedo ir inspeccionando en búsqueda de gráficos que se desconfiguraron, o casos extremos (edge cases) donde la app no esté comportándose como se esperaba (por ejemplo, con valores muy bajos o muy altos, casos donde no hay observaciones, etc.)\nExpectativas del test Para crear una expectativa del test; es decir, un resultado de la prueba, dependiendo de lo que necesitemos podemos usar app$expect_screenshot() para tomar una captura de pantalla, o app$expect_values() para validar que los valores de los outputs sean los esperados.\nSe recomienda usar app$expect_values(), porque además de tomar una captura de pantalla, te entrega un archivo JSON que contiene el estado de todos los inputs de tu app, con el cual puedes hacer debug del estado de la app en cada test. Así puedes encontrar dónde está el problema!\nDebugging Dentro del test, puedes usar cat() para imprimir texto y que se vea en el panel de tests. Así podrías ayudarte a que el test te diga en qué parte de la app está, o qué operación está haciendo.\nUna línea que me ha sido útil es app$wait_for_idle(), que hace que el test espere hasta que la app esté quieta por un tiempo breve antes de continuar. Así se evita que el test capture la pantalla cuando la app todavía no termina de cargarse! 😂 Si aún así el test no logra avanzar, puedes agregar Sys.sleep(2) para forzar una espera de x segundos antes del siguiente paso (tuve que hacerlo en apps que mostraban outputs demasiado grandes).\nResultados Dentro de la carpeta tests/testthat/_snaps se pueden encontrar los resultados de los pantallazos producidos durante el test con app$expect_screenshot(), o de los pantallazos más los archivos JSON si usaste app$expect_values().\nPodemos inspeccionar visualmente las capturas para corroborar que la aplicación funciona correctamente (asegurarnos que todos los outputs se vean, que la disposición de la app es visualmente correcta), pero también la prueba nos avisará si las capturas de pantalla nuevas difieren de las anteriores, lo que significaría un cambio en los resultados.\nSi el test resultó en un error en alguno de sus pasos, puede inspeccionar el archivo JSON para ver en qué input estaba la prueba, y qué resultados estaba entregando la app. Así puedes ver si hay inconsistencias, o puedes probar la app manualmente a ver qué pasa si reproduces la situación que resultó en error.\nAutomatizar tests repetitivos Si tienes un vector con los valores de uno o varios inputs que quieres testear, puedes correr las pruebas en un loop que repita la prueba para cada elemento.\nDe ser necesario, dentro del test puedes cargar datos que te entreguen el vector de inputs (por ejemplo, los valores únicos de una variable), y usar esos valores para un loop que navegue por todos los valores del input tomando capturas de pantalla 📸\n# por cada variable, navegar y captura de pantalla for(i in variables) { app$set_inputs(indicador_lista = i) app$wait_for_idle(200) app$expect_screenshot() } Para cargar datos dentro del test, recomiendo usar la función here() del paquete {here}, que permite cargar archivos relativos a la carpeta de tu proyecto, para evitar complicaciones con las rutas de los archivos.\nEjemplo de test automatizado En esta app, hay un input con indicadores de un instrumento estadístico, y los indicadores se agrupan por dimensiones. Entonces hay que elegir una dimensión para obtener los indicadores, y elegir un indicador para ver los outputs de la app. Entonces, el test recorre todas las dimensiones de un indicador, y dentro de cada dimensión, pasa por los indicadores de esa dimensión, para luego ir a la siguiente dimensión y así sucesivamente.\nlibrary(shinytest2) library(dplyr) library(arrow) library(here) test_that(\u0026#34;{shinytest2} recording: ebc\u0026#34;, { app \u0026lt;- AppDriver$new(variant = platform_variant(), name = \u0026#34;ebc indicadores\u0026#34;, height = 694, width = 885) # cargar datos indice \u0026lt;- read_parquet(here(\u0026#34;indice_app.parquet\u0026#34;)) # vector con dimensiones ambitos \u0026lt;- unique(indice$ambito) # tabla con dimensiones e indicadores indicadores \u0026lt;- indice |\u0026gt; distinct(ambito, etiqueta) # ir a la pestaña app$set_inputs(tabs_resultados = \u0026#34;Resultados por indicador\u0026#34;) app$wait_for_idle() # por cada dimensión for(i in ambitos) { cat(\u0026#34; Probando ambito\u0026#34;, i) app$set_inputs(indicador_ambito = i) Sys.sleep(2) app$wait_for_idle() # obtener indicadores de la dimensión indicadores_ambito \u0026lt;- indicadores |\u0026gt; filter(ambito == i) |\u0026gt; pull(etiqueta) # por cada indicador del ambito for(j in indicadores_ambito) { cat(\u0026#34; Probando indicador\u0026#34;, j) app$set_inputs(indicador = j) Sys.sleep(2) app$wait_for_idle() app$expect_values() } } }) Conclusión Si desarrollaste una aplicación Shiny medianamente compleja, dedicar unos minutos a crear tests te podrá asegurar que tu app funciona correctamente siempre. Cuando cambie el código o los datos, presionas Run tests y confirmas al 100% que todo está funcionando.\nOtro beneficio no menor es la paz mental 😇 que te entrega haber confirmado que la app funciona bien en todos los escenarios posibles, cosa que solemos omitir luego de dedicar varios días a un mismo proyecto (solamente cruzamos los dedos para que todo funcione bien 🤞🏼 jaja)\nBonus: Si activas la opción de que Shiny guarde caché de tus outputs en un carpeta, ejecutar un test que use todos tus inputs para generar todos los outputs posibles te permitirá generar caché para todos los outputs de tu app de manera automática, lo que aumentará la velocidad de tu app de manera considerable!\n","date":"2026-01-23T00:00:00Z","excerpt":"La validación de aplicaciones te permite crear un conjunto de pruebas para confirmar que tus aplicaciones funcionan bien sin tener que probarlas manualmente. Puedes programar un _bot_ que apriete todos los botones de tu aplicación y obtener capturas de pantalla que te confirmen que todo se ve bien. En esta guía aprenderás a utilizar `{shinytest2}` para automatizar el testeo de tus aplicaciones Shiny, asegurando su correcto funcionamiento a través de capturas de pantalla y otras validaciones automáticas.","href":"https://bastianoleah.netlify.app/blog/shiny_validacion/","tags":"shiny ; automatización","title":"Testeo automatizado de aplicaciones Shiny con {shinytest2}"},{"content":"Darle un toque de color a tus gráficos produce visualizaciones con más personalidad y mayor impacto. Pero cambiar el color de cada elemento puede parecer engorroso 😣\n¡Pero es fácil! Veamos cómo se puede hacer con las nuevas funcionalidades de {ggplot2} versión 4.0.\nPrimero generemos datos al azar para crear una visualización de demostración:\nlibrary(dplyr) # crear datos al azar datos \u0026lt;- tibble(a = 1:10, b = rnorm(10, mean = 7, sd = 2) ) Ahora creemos un gráfico básico:\nlibrary(ggplot2) grafico \u0026lt;- datos |\u0026gt; ggplot() + aes(x = as.factor(a), y = b) + # capa de columnas geom_col(width = 0.5) + # capa de texto geom_text(aes(label = round(b, 0), y = b + 0.6), size = 3, fontface = \u0026#34;bold\u0026#34;) grafico Agreguemos algunas capas extra para mejorar la apariencia de nuestro gráfico:\ngrafico \u0026lt;- grafico + # etiquetas de texto labs(title = \u0026#34;Gráfico de barras\u0026#34;, subtitle = \u0026#34;Números al azar\u0026#34;, x = \u0026#34;Eje horizontal\u0026#34;, y = \u0026#34;Eje vertical\u0026#34;, caption = \u0026#34;Fuente: de los deseos\u0026#34;) + # ajuste del espaciado vertical scale_y_continuous(expand = expansion(c(0, 0.05))) + # detalles del tema theme(plot.title = element_text(face = \u0026#34;bold\u0026#34;), panel.grid.minor.y = element_line(linetype = 2, linewidth = .5), axis.ticks.x = element_blank()) grafico Cambiar colores de los temas de {ggplot2} Ahora, para cambiar los colores del gráfico, podemos usar una función para aplicar un tema, como theme_classic() o theme_minimal(), dentro de la cual podremos definir los colores principales del gráfico:\nEl color de fondo o papel (paper) El color de los elementos o tinta (ink) El color de acento (accent) Probemos con theme_classic():\ngrafico + theme_classic(paper = \u0026#34;#EAD2FA\u0026#34;, ink = \u0026#34;#553A74\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;) Tan solo con definir tres colores, obtenemos un gráfico con una apariencia mucho mejor! 💜\nIntentemos ahora con theme_minimal():\ngrafico + theme_minimal(paper = \u0026#34;#EAD2FA\u0026#34;, ink = \u0026#34;#553A74\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;) Aplicar temas con {thematic} Otra forma de aplicar temas de colores a tus gráficos es con el paquete {thematic}.\nLa idea de {thematic} es que tenemos que activar la función que aplica los temas con thematic_on(), y en el momento que la activamos, definimos los colores principales de nuestro tema. Estos colores son de fondo (bg) y de frente (fg), que como su nombre lo indica, definen el color base del gráfico y el color de los elementos principales.\nlibrary(thematic) thematic_on(fg = \u0026#34;#553A74\u0026#34;, bg = \u0026#34;#EAD2FA\u0026#34;) grafico Solamente con definir dos colores con thematic_on() e imprimir el gráfico obtenemos el gráfico con el tema aplicado. Muy lindo 💜\nLa diferencia entre cambiar los temas con las funciones de {ggplot2} y con {thematic} es que {thematic} toma algunas decisiones extra para hacer más bonitos tus gráficos, como un color de fondo más tenue, entre otras.\nPor otro lado, {thematic} tiene la ventaja de que puede trabajar en conjunto con tus aplicaciones Shiny, de manera que el tema de tu app se aplique a tus gráficos automáticamente.\nEligiendo temas de colores Para encontrar pares de colores interesantes, recomiendo el sitio Pigment, que genera pares de colores a los que puedes ajustar su saturación y su nivel de iluminación.\nVeamos algunos ejemplos de como queda el mismo gráfico con otros pares de colores:\nthematic_on(fg = \u0026#34;#51BBAC\u0026#34;, bg = \u0026#34;#2B5556\u0026#34;) grafico thematic_on(fg = \u0026#34;#7FBCC4\u0026#34;, bg = \u0026#34;#473649\u0026#34;) grafico thematic_on(fg = \u0026#34;#6E6962\u0026#34;, bg = \u0026#34;#C5BEB5\u0026#34;) grafico Para desactivar la aplicación del tema de colores, usa thematic_off() y volverás a la aburrida normalidad.\nthematic_off() grafico ¡Así de simple! Claramente es una forma básica y rápida de personalizar la apariencia de las visualizaciones, pero el resultado suele ser positivo considerando el poquísimo esfuerzo necesario para lograrlo. Eficiencia, yey! 🥰\n","date":"2026-01-22T00:00:00Z","excerpt":"Darle un toque de color a tus gráficos en `{ggplot2}` produce visualizaciones con más personalidad y mayor impacto. En este post veremos cómo aplicar temas de colores a tus gráficos! Para ayudarte con eso también existe el paquete `{thematic}` para cambiar fácilmente los colores de fondo de tus gráficos.","href":"https://bastianoleah.netlify.app/blog/ggplot_temas/","tags":"visualización de datos ; ggplot2 ; gráficos","title":"Temas de colores personalizados para tus gráficos {ggplot2}"},{"content":"En este tutorial veremos cómo convertir celdas de tus tablas a compactas píldoras usando {gt}, una excelente librería de R para crear tablas personalizables. El objetivo es presentar la información de forma más atractiva, permitiendo darle un color personalizado según el dato que se muestre, o bien, destacar determinados datos.\nSi bien existen otras formas de hacer esto, aprender este método te entrega mucha más flexibilidad para trabajar tablas directamente con HTML en el paquete {gt}, desbloqueando su potencial! 🔥\nSi quieres aprender lo básico del paquete {gt} para creación de tablas en R, revisa este tutorial. Tablas básicas con {gt} Primero cargamos los paquetes y creamos datos de prueba:\nlibrary(gt) library(dplyr) library(glue) # crear datos de prueba datos \u0026lt;- tibble(dato = c(\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;), valor = c(1, 4, 8), tipo = c(\u0026#34;bajo\u0026#34;, \u0026#34;medio\u0026#34;, \u0026#34;alto\u0026#34;)) Recordemos que para iniciar una tabla de {gt}, simplemente le ponemos la función gt() a los datos:\ntabla \u0026lt;- datos |\u0026gt; gt() dato valor tipo A 1 bajo B 4 medio C 8 alto Obtenemos una tabla básica.\nCrear columnas con HTML y CSS Ahora crearemos una columna con código HTML, el mismo que se usa para crear páginas web. Esto nos permite crear lo que se nos ocurra, y es la fortaleza principal de este método.\nSimplemente creamos una columna que contenga el código:\ntabla \u0026lt;- datos |\u0026gt; # crear píldora html mutate(pildora = \u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: #9069C0; color: white;\u0026#39;\u0026gt; Hola \u0026lt;/div\u0026gt;\u0026#34;) |\u0026gt; # hacer tabla gt() dato valor tipo pildora A 1 bajo \u0026lt;div style='padding: 2px 12px; border-radius: 12px; background: #9069C0; color: white;'\u0026gt; Hola \u0026lt;/div\u0026gt; B 4 medio \u0026lt;div style='padding: 2px 12px; border-radius: 12px; background: #9069C0; color: white;'\u0026gt; Hola \u0026lt;/div\u0026gt; C 8 alto \u0026lt;div style='padding: 2px 12px; border-radius: 12px; background: #9069C0; color: white;'\u0026gt; Hola \u0026lt;/div\u0026gt; ¡Está horrible! 😣 Esto es porque hay que pedirle a {gt} interpretar la columna como HTML con la función fmt_markdown():\ntabla \u0026lt;- datos |\u0026gt; # crear píldora html mutate(pildora = \u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: #9069C0; color: white;\u0026#39;\u0026gt; Hola \u0026lt;/div\u0026gt;\u0026#34;) |\u0026gt; # hacer tabla gt() |\u0026gt; fmt_markdown(columns = \u0026#34;pildora\u0026#34;) dato valor tipo pildora A 1 bajo Hola B 4 medio Hola C 8 alto Hola ¡Ahora sí! Lo importante del código HTML es la etiqueta \u0026lt;div\u0026gt;, que crea un recuadro, y el atributo style, que define el estilo del mismo recuadro mediante código CSS. En este caso, le dimos un padding (espaciado interno), un borde redondeado (border-radius), un color de fondo (background), y un color de texto (color).\nAprender HTML y CSS es súper útil para mejorar tus aplicaciones Shiny, reportes Quarto, e incluso aspectos de tus gráficos {ggplot2}! Aplicar píldoras a datos de una columna Ahora podemos reemplazar el código anterior para que muestre el valor de la columna tipo. Para ello usamos la función glue(), que permite incrustar valores de R dentro de textos de una forma más cómoda que usar paste():\ntabla \u0026lt;- datos |\u0026gt; # crear píldora html mutate(tipo = glue( \u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: #9069C0; color: white;\u0026#39;\u0026gt; {tipo} \u0026lt;/div\u0026gt;\u0026#34;)) |\u0026gt; # hacer tabla gt() |\u0026gt; # que la columna se interprete como html fmt_markdown(columns = \u0026#34;tipo\u0026#34;) |\u0026gt; cols_align(tipo, align = \u0026#34;center\u0026#34;) dato valor tipo A 1 bajo B 4 medio C 8 alto Ahora la columna tipo muestra sus valores como píldoras. Hermoso!\nCrear una función que cree columnas HTML Hagamos que el código anterior sea más ordenado creando una función que haga las píldoras:\n# crear una función que hace píldoras pildora \u0026lt;- function(valor) { # crear píldora html glue(\u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: #9069C0; color: white;\u0026#39;\u0026gt; {valor} \u0026lt;/div\u0026gt;\u0026#34;) } La función es casi lo mismo que antes, solamente que recibe un valor llamado valor que se ubica dentro del glue(). Ahora veamos cómo se crea la tabla anterior pero usando la función nueva:\n# aplicar función a la tabla tabla \u0026lt;- datos |\u0026gt; mutate(tipo = pildora(tipo)) |\u0026gt; gt() |\u0026gt; fmt_markdown(columns = \u0026#34;tipo\u0026#34;) dato valor tipo A 1 bajo B 4 medio C 8 alto El código queda mucho más compacto y ordenado!\nColores de píldoras según datos Ahora complementemos la función que creamos, para que los colores de las píldoras dependan de los datos. Para ello, dentro de la función pildora() que creamos, podemos poner código que defina el color que tendrá cada píldora dependiendo de su valor:\n# mejorar la función pildora \u0026lt;- function(valor) { # definir colores en base a valores color \u0026lt;- case_match(valor, \u0026#34;alto\u0026#34; ~ \u0026#34;#E56B6F\u0026#34;, \u0026#34;medio\u0026#34; ~ \u0026#34;#b56576\u0026#34;, \u0026#34;bajo\u0026#34; ~ \u0026#34;#6d597a\u0026#34;) # crear píldora html glue(\u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: {color}; color: white;\u0026#39;\u0026gt; {valor} \u0026lt;/div\u0026gt;\u0026#34;) } # aplicar función a la tabla tabla \u0026lt;- datos |\u0026gt; mutate(tipo = pildora(tipo)) |\u0026gt; gt() |\u0026gt; fmt_markdown(columns = \u0026#34;tipo\u0026#34;) dato valor tipo A 1 bajo B 4 medio C 8 alto Hermoso! Vimos que la función pildora() ahora usa valor para dos cosas: para determinar el color de fondo, y para poner el texto dentro de la píldora.\nVariaciones de la función de píldoras Finalmente, podemos crear distintas funciones de píldoras para distintos tipos de datos. Por ejemplo, una para variables categóricas, otra para variables numéricas, y otra para valores con color manual:\nLa función para variables categóricas, que ya vimos, entrega un color específico a valores específicos:\n# función para variables categóricas pildora_categorica \u0026lt;- function(valor) { # definir colores en base a valores color \u0026lt;- case_match(valor, \u0026#34;alto\u0026#34; ~ \u0026#34;#E56B6F\u0026#34;, \u0026#34;medio\u0026#34; ~ \u0026#34;#b56576\u0026#34;, \u0026#34;bajo\u0026#34; ~ \u0026#34;#6d597a\u0026#34;) # crear píldora html glue(\u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: {color}; color: white;\u0026#39;\u0026gt; {valor} \u0026lt;/div\u0026gt;\u0026#34;) } La función para variables numéricas va a asignar color a las celdas dependiendo de si se cumple o no el criterio numérico que le demos:\n# función para variables categóricas pildora_numerica \u0026lt;- function(valor) { # definir colores en base a valores color \u0026lt;- case_when(valor \u0026gt;= 4 ~ \u0026#34;#749c75\u0026#34;, valor \u0026lt; 4 ~ \u0026#34;#b2bd7e\u0026#34;) # crear píldora html glue(\u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: {color}; color: white;\u0026#39;\u0026gt; {valor} \u0026lt;/div\u0026gt;\u0026#34;) } La tercera función simplemente le da el color que especifiquemos a todas las celdas por igual:\n# función para color manual pildora_manual \u0026lt;- function(valor, color) { # crear píldora html glue(\u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: {color}; color: white;\u0026#39;\u0026gt; {valor} \u0026lt;/div\u0026gt;\u0026#34;) } Ahora apliquemos estos tres estilos a las tres columnas con {gt}\n# aplicar función a la tabla tabla \u0026lt;- datos |\u0026gt; mutate(tipo = pildora_categorica(tipo), valor = pildora_numerica(valor), dato = pildora_manual(dato, \u0026#34;#355070\u0026#34;)) |\u0026gt; gt() |\u0026gt; fmt_markdown(columns = c(tipo, valor, dato)) dato valor tipo A 1 bajo B 4 medio C 8 alto Destacar valores específicos Otra variación de las funciones anteriores puede ser para que la píldora solamente destaque ciertos valores a los que queremos llamar la atención:\n# función para color manual pildora_alerta \u0026lt;- function(valor, alerta) { color_alerta \u0026lt;- \u0026#34;#ce4257\u0026#34; ifelse(valor == alerta, # si el valor coincide con la alerta, pildora glue(\u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: {color_alerta}; color: white;\u0026#39;\u0026gt; {valor} \u0026lt;/div\u0026gt;\u0026#34;), # si no, solo cifra con espaciado glue(\u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px;\u0026#39;\u0026gt;{valor}\u0026lt;/div\u0026gt;\u0026#34;) ) } # aplicar función a la tabla tabla \u0026lt;- datos |\u0026gt; mutate(dato = pildora_alerta(dato, alerta = \u0026#34;B\u0026#34;), tipo = pildora_alerta(tipo, alerta = \u0026#34;bajo\u0026#34;)) |\u0026gt; gt() |\u0026gt; fmt_markdown(columns = c(dato, tipo)) dato valor tipo A 1 bajo B 4 medio C 8 alto Esta función nueva tiene la capacidad de que definas en cada caso el valor que quieres destacar, lo que permite reutilizarla en columnas distintas.\nFunción para degradado de colores Finalmente, nos basamos en el código de las píldoras para variables numéricas para crear píldoras con un degradado de colores según el valor numérico.\nEsto podemos hacerlo tomando el valor máximo de la columna y dividiéndolo por el valor de cada celda, para obtener la proporción (un número del 0 al 1), y usar este número para darle transparencia al color elegido. A su vez, podemos hacer que el texto de la píldora cambie de color si el color va a ser muy transparente, para asegurar buen contraste.\npildora_degradado \u0026lt;- function(valor, maximo, color) { # definir transparencia según valor máximo transparencia \u0026lt;- valor/maximo # definir color de texto según transparencia texto \u0026lt;- ifelse(transparencia \u0026gt; 0.5, \u0026#34;white\u0026#34;, \u0026#34;black\u0026#34;) # aplicar transparencia al color color \u0026lt;- scales::alpha(color, transparencia+0.1) glue(\u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: {color}; color: {texto};\u0026#39;\u0026gt; {valor} \u0026lt;/div\u0026gt;\u0026#34;) } tabla \u0026lt;- datos |\u0026gt; mutate(valor = pildora_degradado(valor, max(valor), color = \u0026#34;#E56B6F\u0026#34;)) |\u0026gt; gt() |\u0026gt; fmt_markdown(columns = valor) dato valor tipo A 1 bajo B 4 medio C 8 alto La función alpha() del paquete {scales} nos ayuda a crear colores transparentes a partir de un número entre el 0 (transparente) y el 1 (opaco).\nPara más información sobre manejo de colores desde R, revisa este post! El mismo código puede cambiarse un poco para crear un degradado entre dos colores:\npildora_degradado_2 \u0026lt;- function(valor, color_1, color_2) { # definir transparencia según valor máximo transparencia \u0026lt;- valor/max(valor) # opcional: escalar de 0 a 1 # transparencia \u0026lt;- (transparencia-min(transparencia))/(max(transparencia)-min(transparencia)) # crear colores de texto a partir de colores del degradado color_claro \u0026lt;- scales::col_mix(\u0026#34;white\u0026#34;, color_1, 0.4) color_oscuro \u0026lt;- scales::col_mix(\u0026#34;black\u0026#34;, color_2, 0.4) # definir color de texto según transparencia texto \u0026lt;- ifelse(transparencia \u0026gt; 0.5, color_claro, color_oscuro) # aplicar transparencia al color color \u0026lt;- scales::col_mix(color_1, color_2, transparencia-0.1) glue(\u0026#34;\u0026lt;div style=\u0026#39;padding: 2px 12px; border-radius: 12px; background: {color}; color: {texto};\u0026#39;\u0026gt; {valor} \u0026lt;/div\u0026gt;\u0026#34;) } tabla \u0026lt;- datos |\u0026gt; mutate(valor = pildora_degradado_2(valor, color_1 = \u0026#34;#e0b1cb\u0026#34;, color_2 = \u0026#34;#5e548e\u0026#34;)) |\u0026gt; gt() |\u0026gt; fmt_markdown(columns = valor) dato valor tipo A 1 bajo B 4 medio C 8 alto En esta función usamos dos colores para crear un degradado entre el valor menor y el mayor de la columna, y incluso usamos estos colores para teñir los colores de los textos y así hacerlos más armónicos en vez de usar simplemente blanco y negro.\nY así se puede usar HTML para personalizar celdas de tus tablas hechas con {gt}. Si te interesa este paquete, tengo un tutorial más completo que puedes revisar.\n","date":"2026-01-20T00:00:00Z","excerpt":"En este tutorial veremos cómo convertir celdas de tus tablas a compactas _píldoras_ usando `{gt}`. El objetivo es presentar la información de forma más atractiva, permitiendo darle un color personalizado a cada celda según el dato que se muestre, o bien, destacar determinados datos. Esto te entrega mucha más flexibilidad para trabajar tablas directamente con HTML en el paquete `{gt}`, desbloqueando su potencial!","href":"https://bastianoleah.netlify.app/blog/gt_pildoras/","tags":"tablas","title":"Crea tablas con píldoras de colores usando `{gt}` en R"},{"content":"Docker es una plataforma que permite empaquetar aplicaciones y sus dependencias en contenedores, lo que simplifica el proceso de despliegue (deployment) y gestión de aplicaciones.\nPost en construcción! A medida que voy aprendiendo iré complementando la publicación. Este es un post para usuarios avanzados de Shiny que necesiten desplegar sus apps contextos de empresas o producción. Si eres un usuario casual de Shiny, probablemente sea mejor usar Posit Connect Cloud para publicar tus apps. ¿Qué es Docker? Docker es un programa que te permite crear contenedores dentro de los cuales puedes ejecutar aplicaciones. Estos son entornos exclusivos y específicos para tu app, de manera que ya no la ejecutas en tu computador, sino que la ejecutas dentro del contenedor, que es una especie de computador virtual sólo para tu app. Estos contenedores son independientes entre sí y reproducibles, ayudándote a que tus apps siempre funcionen siempre igual, sin importar dónde las ejecutes.\nLos objetivos principales de usar Docker en aplicaciones Shiny son:\nGarantizar un entorno de ejecución consistente y reproducible: al ejecutar la app en el contenedor, tienes garantizado que se ejecutará igual en otro computador, en un servidor Linux, o en cualquier entorno. Congelar las dependencias de la aplicación, para asegurarte de que se usan las versiones exactas de R y de los paquetes que utilices, además de ejecutarse en un mismo sistema operativo (usualmente Linux) con todas las librerías y configuraciones que éste requiera. Facilitar el despliegue en servidores o plataformas de nube, dado que tu app vive aislada dentro del contenedor, y por ende no requiere cambiar configuraciones ni instalar cosas al servidor. En pocas palabras, Docker sirve para empaquetar tu aplicación Shiny junto con todo lo necesario para que funcione correctamente, y así poder desplegarla en cualquier lugar sin preocuparte por las diferencias en los entornos de ejecución. Esto significa que crearás un contenedor con una versión de Linux y de R específica, junto a todas las configuraciones e instalaciones que necesites.\nImágenes Los contenedores Docker dependende imágenes. Las imágenes contienen un sistema operativo base y las configuraciones e instalaciones necesarias.\nEn el caso de R y Shiny existen las imágenes de Rocker Project, un proyecto que mantiene imágenes con versiones de R y todo lo necesario para usar R.\nExisten imágenes de Shiny y Shiny-Verse, que es Shiny + paquetes del Tidyverse, para acelerar el despliegue si usas esos paquetes (recomendado).\nDockerfiles Un Dockerfile es el archivo que contiene la receta para crear tu contenedor: empezando por la imagen que se usará como base, e incluyendo instalación de libreríass, copiado de archivos, configuración de puertos, y todo lo necesario para que tu aplicación funcione correctamente.\nCreando contenedores para apps Shiny Crear un contenedor automáticamente con {shiny2docker} El paquete {shiny2docker} facilita enormemente esta tarea.\nSimplemente instala el paquete con install.packages(\u0026quot;shiny2docker\u0026quot;) y luego, en el directorio de la app, ejecuta:\nshiny2docker::shiny2docker(path = \u0026#34;.\u0026#34;) La función capturará las dependencias de tu aplicación y generará un dockerfile personalizado.\nEl paquete decidirá la imagen que usará como base, pero puedes cambiarla especificando el argumento FROM = \u0026quot;rocker/shiny-verse\u0026quot;.\nEl dockerfile resultante se vería más o menos así:\nFROM rocker/shiny:4.4.2 RUN apt-get update -y \u0026amp;\u0026amp; apt-get install -y make pandoc libx11-dev libfontconfig1-dev libfreetype6-dev libcairo2-dev libpng-dev zlib1g-dev cmake libssl-dev libgdal-dev gdal-bin libgeos-dev libproj-dev libsqlite3-dev libudunits2-dev libicu-dev \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* RUN mkdir -p /usr/local/lib/R/etc/ /usr/lib/R/etc/ RUN echo \u0026#34;options(renv.config.pak.enabled = FALSE, repos = c(CRAN = \u0026#39;https://cran.rstudio.com/\u0026#39;), download.file.method = \u0026#39;libcurl\u0026#39;, Ncpus = 4)\u0026#34; | tee /usr/local/lib/R/etc/Rprofile.site | tee /usr/lib/R/etc/Rprofile.site RUN R -e \u0026#39;install.packages(\u0026#34;remotes\u0026#34;)\u0026#39; RUN R -e \u0026#39;remotes::install_version(\u0026#34;renv\u0026#34;, version = \u0026#34;1.1.4\u0026#34;)\u0026#39; COPY renv.lock renv.lock RUN --mount=type=cache,id=renv-cache,target=/root/.cache/R/renv R -e \u0026#39;renv::restore()\u0026#39; WORKDIR /srv/shiny-server/ COPY . /srv/shiny-server/ EXPOSE 3838 CMD R -e \u0026#39;shiny::runApp(\u0026#34;/srv/shiny-server\u0026#34;,host=\u0026#34;0.0.0.0\u0026#34;,port=3838)\u0026#39; Vemos que lo primero que hace es basarse en una imagen de Rocker Project con una versión específica de R. Luego instala librerías de Ubuntu con apt-get install. Después ejecuta varios comandos de Linux con RUN, entre ellos los necesarios para activar {renv} y así instalar las versiones específicas de los paquetes usados en tu sesión de R. Finalmente usa COPY para copiar los archivos de tu app a la carpeta /srv/shiny-server/, desde la cual se ejecutará tu app. Crear un contenedor manualmente También podemos crear un contenedor a mano, creando un archivo llamado Dockerfile que contenga, como mínimo, la imagen que usaremos, la instalación de librerías de Ubuntu requeridas y la instalación de paquetes de R.\nFROM rocker/shiny-verse:4.5.2 RUN apt-get update -y \u0026amp;\u0026amp; apt-get install -y RUN apt-get install -y fonts-manrope RUN apt-get install -y libudunits2-dev libgdal-dev libgeos-dev libproj-dev RUN R -e \u0026#39;install.packages(\u0026#34;shinyWidgets\u0026#34;)\u0026#39; RUN R -e \u0026#39;install.packages(\u0026#34;shinycssloaders\u0026#34;)\u0026#39; RUN R -e \u0026#39;install.packages(\u0026#34;shinydisconnect\u0026#34;)\u0026#39; RUN R -e \u0026#39;install.packages(\u0026#34;shinyjs\u0026#34;)\u0026#39; RUN R -e \u0026#39;install.packages(\u0026#34;writexl\u0026#34;)\u0026#39; RUN R -e \u0026#39;install.packages(\u0026#34;gfonts\u0026#34;)\u0026#39; RUN R -e \u0026#39;install.packages(\u0026#34;ragg\u0026#34;)\u0026#39; RUN R -e \u0026#39;install.packages(\u0026#34;gt\u0026#34;)\u0026#39; RUN R -e \u0026#39;install.packages(\u0026#34;sf\u0026#34;)\u0026#39; RUN R -e \u0026#39;install.packages(\u0026#34;cli\u0026#34;)\u0026#39; RUN R -e \u0026#39;install.packages(\u0026#34;ggiraph\u0026#34;)\u0026#39; RUN R -e \u0026#39;remotes::install_github(\u0026#34;hrbrmstr/waffle\u0026#34;)\u0026#39; RUN sudo chown -R shiny:shiny /srv/shiny-server/ Si no sabes qué librerías de Ubuntu/Linux son necesarias, simplemente omite esas líneas e instala los paquetes que necesites. Al ejecutar el contenedor, los mensajes de error te dirán lo que necesitas instalar. Luego, crea un archivo docker-compose.yml para definir el servicio de la app Shiny:\nservices: shiny_server: container_name: app build: context: . dockerfile: Dockerfile ports: - \u0026#34;3838:3838\u0026#34; # cambiar primer puerto si tienes varias apps volumes: - ./app/:/srv/shiny-server/ restart: always Este archivo coordinará el despliegue de tu contenedor, y copiará los contenidos de la carpeta app/ a /srv/shiny-server/ dentro del contenedor, así que asegúrate de que dentro de app/ (o el nombre de la carpeta de tu app) estén los archivos app.R o ui.R y server.R.\nEjecutando apps Shiny desde contenedores Docker Para ejecutar el contenedor con tu app Shiny, deberías tener en un mismo directorio lo siguiente:\nEl archivo Dockerfile con los pasos de instalación de tu contenedor, ya sea escrito por ti o creado por {shiny2docker} El archivo docker-compose.yml para coordinar el despliegue de tu contenedor Una carpeta app/ (o el nombre de tu app) con los scripts y archivos necesarios para tu aplicación Shiny (app.R, etc). Luego necesitas abrir una Terminal y ubicar la terminal en esa carpeta (cd ruta/a/carpeta/), y desde ahí ejecutar el siguiente comando:\ndocker compose up -d Este comando levanta una aplicación Shiny hecha con Docker Compose en segundo plano, asumiendo que la carpeta desde la que ejecutaste el comando contiene el dockerfile y docker-compose.yml.\nLa primera vez que lo ejecutes, debería salir en la terminal el proceso de construcción del contenedor, con la instalación de todo lo que detallaste. Luego de unos minutos (dependiendo de la cantidad de paquetes y librerías que tengas que instalar), el contenedor debería estar listo y ejecutándose.\nPara acceder a la aplicación, accede desde un navegador a localhost:3838, o reemplaza 3838 por el puerto que hayas definido en docker-compose.yml.\nSi tienes varias apps en varios contenedores, cada una debe salir por un puerto distinto! Para confirmar el estado de tus contenedores, sus nombres y puertos, ejecuta:\ndocker ps Configuración avanzada Permisos de escritura para Shiny Si tu aplicación Shiny usa cache y decides que el cache se guarde en el disco (lo que aumenta la velocidad de la app para todos sus usuarios), necesitas que la aplicación tenga permiso para leer y escribir en la carpeta del cache. Esto es porque Shiny por defecto no puede escribir en el disco, por razones de seguridad, pero necesita escribir si se usa cache en disco para ir guardando las combinaciones de inputs nuevas que vayan realizando tus usuarios/as.\nPara que Shiny pueda escribir en la carpeta del cache, en el archivo docker-compose.yml tienes que agregar la siguiente línea dentro del bloque shiny_server:\nservices: shiny_server: container_name: shiny_app ... volumes: - ... post_start: - command: [\u0026#34;sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;chown -R shiny:shiny /srv/shiny-server/app_cache\u0026#34;] # dar permisos después de montar volumen El argumento post_start ejecutará un comando justo después de montar el volumen, lo que le dará permisos a Shiny para escribir en la carpeta del cache. Asegúrate de reemplazar la ruta por el nombre de la carpeta que usas para guardar el cache en disco.\nComandos de Docker más frecuentes Levantar una aplicación Shiny hecha con Docker Compose:\ndocker compose up -d Tienes que estar en la carpeta de la app Bajar la aplicación Shiny hecha con Docker Compose:\ndocker compose down Tienes que estar en la carpeta de la app Ver los contenedores Docker activos:\ndocker ps Reconstruir y levantar la aplicación Shiny hecha con Docker Compose:\ndocker compose up --build --force-recreate -d Tienes que estar en la carpeta de la app Sirve para recibir cambios nuevos y asegurarte de que se aplicarán Entrar a la consola de un contenedor Docker activo:\ndocker ps docker exec -ti xxxx /bin/bash Reemplaza xxxx por el ID del contenedor Revisar los logs de un contenedor Docker:\ndocker ps docker logs xxxx Reemplaza xxxx por el ID o nombre del contenedor Ver las imágenes Docker disponibles:\ndocker image ls Eliminar una imagen Docker:\ndocker image ls docker rmi xxxx Reemplaza xxxx por el ID de la imagen Recursos How To Run Shiny Apps in a Docker Container Containerizing Shiny Apps with {shiny2docker}: A Step-by-Step Guide ","date":"2026-01-20T00:00:00Z","excerpt":"Docker es una plataforma que permite empaquetar aplicaciones y sus dependencias en contenedores, lo que simplifica el proceso de despliegue (_deployment_) y gestión de aplicaciones. Con Docker puedes empaquetar tu app Shiny junto con todo lo necesario para que funcione correctamente, y así poder desplegarla en cualquier lugar sin preocuparte por las diferencias en los entornos de ejecución. Esto significa que crearás un contenedor con una versión de Linux y de R específica, junto a todas las configuraciones e instalaciones que necesites.","href":"https://bastianoleah.netlify.app/blog/shiny_docker/","tags":"shiny ; optimización","title":"Desplegar aplicaciones Shiny a producción en contenedores Docker"},{"content":" Ya se enviaron los correos a las 60 personas que obtuvieron un cupo en el curso! Revisa tu correo y tu carpeta de spam! La convocatoria al curso gratuito de introducción a R fue un éxito! Más de 500 personas postularon para poder aprender.\nLamentablemente no podían participar las 500 personas, así que decidí por hacer una selección aleatoria de participantes, dónde según sus características tendrían mayores o menores probabilidades de obtener un cupo.\nLas personas que no obtuvieron cupo podrán ver el curso en vivo por streaming en YouTube Selección ¿Por qué asignar cupos? Algunas personas dicen que el azar es lo más justo cuando se trata de distribuir recursos limitados. En algunos sentidos es verdad, pero también es cierto que no todas las personas tienen las mismas capacidades ni oportunidades, y también que muchas personas enfrentan barreras culturales, sociales y estructurales para acceder a ciertos recursos y participar de determinados espacios.\nEs por esto que opté por asignar cupos considerando cuotas de género, diversidades y disidencias sexogenéricas, y situación de discapacidad.\nEl objetivo fue poder dar mayores oportunidades a que personas sistemáticamente excluidas pueden participar del curso, sin que esto signifique dejar sin oportunidades a otras personas.\nCriterios de probabilidad Los criterios usados para otorgar las probabilidades de obtener un cupo fueron los siguientes:\nCriterios excluyentes Inscripción antes de fecha de cierre (14 de enero a las 9 PM) Inscripción con correo válido Carrera o área de estudio en ciencias sociales o humanidades Luego de aplicar los criterios excluyentes, quedó un total de 521 postulaciones.\nLas probabilidades de selección aumentaron según los siguientes criterios:\nCriterio de inclusión Probabilidades Género femenino, no binario o queer 2 Persona transgénero 3 Persona LGBTIQ+ 2 Persona con discapacidad 1 Chilenos/as 1 Asimismo, las probabilidades disminuyeron según los siguientes criterios:\nCriterio de exclusión Probabilidades Maneja un lenguaje de programación -1 Posee conocimientos de R -1 En total postularon 12 personas trans, por lo que decidí darles un cupo a todxs.\nSelección de cupos Obviamente utilicé R para hacer la selección 😄\nEl primer paso fue la limpieza de los datos obtenidos por la encuesta programada en R con el paquete {surveydown}.\nCódigo de la limpieza de datos resultados \u0026lt;- data |\u0026gt; # solamente con correo filter(!is.na(correo)) |\u0026gt; # excluir pruebas filter(nombre != \u0026#34;Bastián Olea\u0026#34;) |\u0026gt; # seleccionar columnas select(-starts_with(\u0026#34;time\u0026#34;), time_end, -session_id, -nivel) |\u0026gt; # convertir fechas mutate(time_end = lubridate::as_datetime(time_end)) |\u0026gt; arrange(time_end) |\u0026gt; # limpiar nombres mutate(nombre = str_squish(nombre)) |\u0026gt; distinct(nombre, .keep_all = TRUE) |\u0026gt; # filtrar fecha de cierre filter(time_end \u0026lt;= lubridate::ymd_hms(\u0026#34;2026-01-14 20:59:59\u0026#34;)) |\u0026gt; # limpiar respuestas mutate(across(where(is.character), ~case_match(.x, \u0026#34;sí\u0026#34; ~ \u0026#34;sí\u0026#34;, .default = .x))) De acuerdo a los criterios, cada persona obtuvo un puntaje, a partir del cual las personas postulantes fueron ordenadas en un ranking.\nCódigo de la asignación de puntajes resultados_p \u0026lt;- resultados |\u0026gt; mutate(puntaje = 2) |\u0026gt; # sumar puntos a grupos de interés mutate(puntaje = if_else(trans == \u0026#34;sí\u0026#34;, puntaje + 3, puntaje), puntaje = if_else(lgbt == \u0026#34;sí\u0026#34; | is.na(lgbt), puntaje + 2, puntaje), puntaje = if_else(genero %in% c(\u0026#34;femenino\u0026#34;, \u0026#34;no_binario\u0026#34;, \u0026#34;queer_ag_nero_otro\u0026#34;), puntaje + 2, puntaje), puntaje = if_else(pais == \u0026#34;chile\u0026#34;, puntaje + 1, puntaje), puntaje = if_else(disca != \u0026#34;no\u0026#34;, puntaje + 1, puntaje)) |\u0026gt; # penalizar grupos mutate(puntaje = if_else(nivel_programacion == \u0026#34;sí\u0026#34;, puntaje - 1, puntaje), puntaje = if_else(nivel_r %in% c(\u0026#34;intermedio\u0026#34;, \u0026#34;avanzado\u0026#34;), puntaje - 1, puntaje)) |\u0026gt; # criterios excluyentes filter(!str_detect(areas, \u0026#34;otra_que_no_es_de_ciencias_sociales_o_humanidades\u0026#34;)) |\u0026gt; # crear ranking de postulantes arrange(desc(puntaje)) |\u0026gt; mutate(id = row_number()) Luego, se usó la función sample() para seleccionar 45 cupos aleatorios, pero ponderando las probabilidades de cada persona según su puntaje:\n# selección aleatoria con probabilidad seleccion \u0026lt;- sample(resultados$id, size = 45, prob = resultados$id$puntaje, replace = FALSE) Resultados De un total de 521 postulaciones, 60 personas obtuvieron un cupo.\nDe las 60 personas, 37 son mujeres, y 8 son no binarias.\n12 personas trans obtuvieron cupo.\nFinalmente, 27 personas son de Chile, 16 de México, y 17 de otros países de latinoamérica.\n💜 Gracias a todxs por su interés! En sus correos tendrán la confirmación del cupo, y cerca del día de inicio del curso recibirán el enlace de conexión.\nLas personas que no obtuvieron cupo podrán ver el curso en vivo por streaming en YouTube ","date":"2026-01-19T00:00:00Z","excerpt":"La [convocatoria al curso gratuito de introducción a R](/blog/curso_gratis_r_intro_1/) fue un éxito! **Más de 500 personas** postularon para poder aprender.\nLamentablemente no podían participar las 500 personas, así que decidí por hacer una selección aleatoria de participantes, dónde según sus características tendrían mayores o menores probabilidades de obtener un cupo.","href":"https://bastianoleah.netlify.app/blog/2026-01-19/","tags":"blog","title":"Selección de cupos para curso gratuito de introducción a R"},{"content":"Continuando con la exploración de los datos georeferenciados del Censo 2024 por manzanas, en vez de ponerme a hacer mapas interesantes, hice una aplicación para visualizar mapas de cualquier variable del Censo en cualquier comuna del país, a nivel de manzana.\nAplicación Es una aplicación súper sencilla, que carga las cartografías oficiales del Censo en formato Arrow como una base de datos, a la cual por medio de queries se le filtran región, comuna y la columna de variable elegida, para generar un mapa simple de una comuna dividida en manzanas censales 🍎\nApp mapas Censo 2024 Toda la app fue hecha en R en menos de media hora ⏱️ gracias a la conveniencia de {dplyr} para poder conectarse a los datos cargados como base de datos por Arrow, al tutorial de hacer mapas del Censo 2024 con {ggplot2}, al tutorial para poner temas a las apps Shiny y al tutorial para temas de gráficos ggplot2.\nEn el fondo hice la app para aprender la carga de datos formato Arrow como base de datos, que es extremadamente fácil de hacer con R y {dplyr}, y me hizo pasar de cargar datos y filtrar en 1.1 segundos a tan sólo 0.1 segundos! 🚀 Si quieres que haga un tutorial de eso déjame un comentario o escríbeme 🤭\nSi quieres explorar más aplicaciones de datos sociales, revisa mi portafolio de aplicaciones Resumen del código Quiero destacar lo simple que es hacer aplicaciones como ésta con Shiny.\nEl código de la app son apenas 200 líneas, donde prácticamente lo único que se hace es crear los botoncitos, cargar los datos, filtrarlos y generar el gráfico.\nLa carga de los datos con {arrow} se hace con open_dataset() para cargar como base de datos sin leer todo el archivo ni traer los datos a la memoria.\n# cargar datos datos \u0026lt;- arrow::open_dataset(\u0026#34;Cartografia_censo2024_Pais_Manzanas.parquet\u0026#34;, partitioning = c(\u0026#34;COD_REGION\u0026#34;, \u0026#34;CUT\u0026#34;) ) Luego simplemente con {dplyr} se hacen los filtros, que internamente se convierten a queries SQL que van a cargar y obtener solamente los datos necesarios desde la base de datos:\ndatos_fitrados \u0026lt;- datos() |\u0026gt; # filtrar filas filter(COD_REGION == input$region, CUT == input$comuna) |\u0026gt; # seleccionar columnas select(all_of(input$variable), SHAPE) |\u0026gt; # traer datos a memoria collect() |\u0026gt; # convertir mapa st_as_sf(crs = 4326) Finalmente los datos filtrados se visualizan como un mapa en {ggplot2}:\ndatos_filtrados |\u0026gt; ggplot() + aes(fill = !!sym(input$variable)) + # variable elegida geom_sf(color = \u0026#34;#181818\u0026#34;, linewidth = 0.1) + # paleta de colores scale_fill_fermenter(palette = 13, na.value = \u0026#34;#181818\u0026#34;) + # tema theme(axis.text.x = element_text(angle = 90, vjust = .5), axis.ticks = element_blank(), panel.background = element_blank(), panel.grid = element_line(color = \u0026#34;#333333\u0026#34;), axis.text = element_text(color = \u0026#34;#444444\u0026#34;), legend.text = element_text(color = \u0026#34;#666666\u0026#34;)) + guides(fill = guide_legend(title = NULL, position = \u0026#34;top\u0026#34;)) Si quieres aprender a hacer estos mismos mapas, revisa este [tutorial de mapas del Censo 2024]/blog/mapas_censo_2024), o revisa el tutorial completo de mapas en R La gracia de Shiny es que puedes tomar cualquier código de R que harías normalmente, y ponerlo dentro de una app interactiva, para lo que solamente necesitas reemplazar los filtros, selecciones, y otros lugares donde escribirías cosas a mano por un input$algo que sería la selección hecha por la/él usuaria/o de la aplicación web.\n","date":"2025-12-19T00:00:00Z","excerpt":"Continuando con la exploración de los datos georeferenciados del Censo 2024 por manzanas, en vez de ponerme a hacer mapas interesantes, hice una [aplicación](https://bastianoleah.shinyapps.io/censo_2024_mapas/) para visualizar mapas de cualquier variable del Censo en cualquier comuna del país, a nivel de manzana.","href":"https://bastianoleah.netlify.app/blog/app_censo_mapas/","tags":"apps ; shiny ; mapas ; Chile","title":"App: visualizador de mapas comunales del Censo 2024 por manzanas"},{"content":" Ahora que recientemente lanzaron los datos 2024 del Censo de Población y Vivienda de Chile, me di unos minutos para visualizarlos. En este tutorial veremos dos formas de visualizar datos censales a nivel de manzana: con mapas estáticos en {ggplot2}, y con mapas interactivos en {mapgl}.\nDatos censales Descarga los datos cartográficos del Censo 2024 desde la página del INE, entrando a Cartografía Censal y luego descargando el archivo Cartografía País Censo 2024 (geoparquet).\nDescargar datos nivel manzana Cargar cartografías censales Los datos censales vienen en formato geoparquet, que es un formato moderno y eficiente para datos espaciales.\nPodemos cargar datos en geoparquet con la función read_parquet() del paquete {arrow}.\nSi no tenemos {arrow} instalado, lo instalamos con install.packages(\u0026quot;arrow\u0026quot;).\nlibrary(arrow) manzanas \u0026lt;- read_parquet(\u0026#34;Cartografia_censo2024_Pais/Cartografia_censo2024_Pais_Manzanas.parquet\u0026#34;) Con la función glimpse() de {dplyr} podemos echar un vistazo a las columnas que vienen en la tabla, pero no las mostraremos aquí porque son más de 200 😂\nlibrary(dplyr) glimpse(manzanas) Ahora que tenemos los datos, vamos a ver cómo visualizarlos en mapas!\nSi necesitas aprender a visualizar mapas con R, revisa este tutorial de {sf}! Mapas estáticos Para generar mapas a partir de cartografía censal, lo que haremos será: filtrar la base de datos para obtener la unidad territorial que nos interesa, luego convertir la cartografía a formato {sf}, y finalmente visualizar con {ggplot2}.\nPreparar datos Primero filtremos una comuna del país:\n# filtrar datos manzanas_comuna \u0026lt;- manzanas |\u0026gt; filter(COMUNA == \u0026#34;LA FLORIDA\u0026#34;) Luego, convertimos a formato {sf}, para poder usar en R los polígonos que vienen en la cartografía, especificando un sistema de referencia de coordenadas (CRS) adecuado:\nlibrary(sf) # convertir a sf manzanas_comuna_sf \u0026lt;- manzanas_comuna |\u0026gt; st_as_sf(crs = 4326) Ahora podemos ver la tabla de datos, que vienen con sus características espaciales incluídas:\nmanzanas_comuna_sf Simple feature collection with 3584 features and 217 fields Geometry type: MULTIPOLYGON Dimension: XY Bounding box: xmin: -70.61478 ymin: -33.57023 xmax: -70.51814 ymax: -33.50389 Geodetic CRS: WGS 84 # A tibble: 3,584 × 218 OBJECTID CUT COD_REGION REGION COD_PROVINCIA PROVINCIA COMUNA AREA_C * \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 148637 13110 13 METROPOLITAN… 131 SANTIAGO LA FL… URBANO 2 148638 13110 13 METROPOLITAN… 131 SANTIAGO LA FL… URBANO 3 148639 13110 13 METROPOLITAN… 131 SANTIAGO LA FL… URBANO 4 148640 13110 13 METROPOLITAN… 131 SANTIAGO LA FL… URBANO 5 148641 13110 13 METROPOLITAN… 131 SANTIAGO LA FL… URBANO 6 148642 13110 13 METROPOLITAN… 131 SANTIAGO LA FL… URBANO 7 148643 13110 13 METROPOLITAN… 131 SANTIAGO LA FL… URBANO 8 148644 13110 13 METROPOLITAN… 131 SANTIAGO LA FL… URBANO 9 148645 13110 13 METROPOLITAN… 131 SANTIAGO LA FL… URBANO 10 148646 13110 13 METROPOLITAN… 131 SANTIAGO LA FL… URBANO # ℹ 3,574 more rows # ℹ 210 more variables: MANZENT \u0026lt;dbl\u0026gt;, DISTRITO \u0026lt;chr\u0026gt;, COD_DISTRITO \u0026lt;int\u0026gt;, # COD_LOCALIDAD \u0026lt;int\u0026gt;, COD_ZONA \u0026lt;int\u0026gt;, LOCALIDAD \u0026lt;chr\u0026gt;, COD_ENTIDAD \u0026lt;int\u0026gt;, # COD_MANZANA \u0026lt;int\u0026gt;, ENTIDAD \u0026lt;chr\u0026gt;, TIPO_MZ \u0026lt;chr\u0026gt;, COD_CATEGORIA \u0026lt;int\u0026gt;, # CATEGORIA \u0026lt;chr\u0026gt;, MZ_BASE_CENSO \u0026lt;int\u0026gt;, ID_ENTIDAD \u0026lt;dbl\u0026gt;, ID_LOCALIDAD \u0026lt;dbl\u0026gt;, # ID_DISTRITO \u0026lt;dbl\u0026gt;, ID_ZONA \u0026lt;dbl\u0026gt;, n_per \u0026lt;dbl\u0026gt;, n_hombres \u0026lt;dbl\u0026gt;, # n_mujeres \u0026lt;dbl\u0026gt;, n_edad_0_5 \u0026lt;dbl\u0026gt;, n_edad_6_13 \u0026lt;dbl\u0026gt;, n_edad_14_17 \u0026lt;dbl\u0026gt;, … Este dataframe espacial posee una columna especial (SHAPE) que contiene los polígonos territoriales.\nCada fila del dataframe corresponde a una unidad territorial, y en la columna SHAPE se indica la forma o el polígono que ocupa esa unidad en el territorio.\nVisualización Vamos a generar mapas estáticos con {ggplot2}, un paquete muy poderoso y personalizable para visualización de datos.\nSi necesitas aprender a usar {ggplot2}, revisa este tutorial completo de introducción a {ggplot2}! Para visualizar estos datos en {ggplot2}, usamos la función de geometría geom_sf(), que está diseñada para trabajar con datos espaciales en formato {sf}. Esta función detecta la columna que contiene los datos espaciales y otras características espaciales, y permite visualizarlas como mapas.\nlibrary(ggplot2) manzanas_comuna_sf |\u0026gt; ggplot() + geom_sf() + theme_minimal(base_size = 10) Con ésto anterior obtenemos el mapa base, generado a partir de los polígonos de la columna SHAPE.\nAhora podemos especificar una variable censal para visualizar, definiéndola en aes(). Luego podemos personalizar el gráfico agregando paletas de colores, ajustes visuales, y textos:\nmanzanas_comuna_sf |\u0026gt; ggplot() + aes(fill = n_edad_60_mas) + geom_sf(color = \u0026#34;white\u0026#34;, linewidth = 0.02) + scale_fill_fermenter(palette = 13) + theme_minimal(base_size = 10) + theme(axis.text.x = element_text(angle = 90, vjust = .5)) + guides(fill = guide_legend(title = \u0026#34;Población\u0026#34;, position = \u0026#34;top\u0026#34;)) + labs(title = \u0026#34;Población de 60 años o más por manzana\u0026#34;, subtitle = \u0026#34;Comuna de La Florida\u0026#34;, caption = \u0026#34;Fuente: Censo 2024, INE\u0026#34;) Reutilizar mapa Podemos repetir todo el proceso, cambiando sólo un par de líneas, para obtener el mapa de otra variable en una comuna distinta:\nmanzanas |\u0026gt; # filtrar filter(COMUNA == \u0026#34;PUENTE ALTO\u0026#34;) |\u0026gt; # comuna # convertir st_as_sf(crs = 4326) |\u0026gt; # graficar ggplot() + aes(fill = n_discapacidad) + # variable geom_sf(color = \u0026#34;white\u0026#34;, linewidth = 0.01) + scale_fill_fermenter(palette = 3) + theme_minimal(base_size = 10) + theme(axis.text.x = element_text(angle = 90, vjust = .5)) + guides(fill = guide_legend(title = \u0026#34;Población\u0026#34;, position = \u0026#34;top\u0026#34;)) + labs(title = \u0026#34;Población con discapacidad por manzana\u0026#34;, subtitle = \u0026#34;Comuna de Puente Alto\u0026#34;, caption = \u0026#34;Fuente: Censo 2024, INE\u0026#34;) Lo bueno de disponer del código es que podemos automatizar la creación de mapas para distintas comunas o regiones, simplemente cambiando los filtros y las variables que queremos graficar, o podemos cambiar el nivel territorial cargando uno de los otros archivos, o bien, realizando una operación de resumen sobre los datos.\nMapa por comunas Solamente como prueba, filtremos una región del país, luego agrupemos por comunas, y sumemos la población de una de las variables en cada comuna con summarize() para pasar desde un mapa por manzanas a un mapa por comunas:\nsf_use_s2(FALSE) comunas \u0026lt;- manzanas |\u0026gt; # filtrar filter(REGION == \u0026#34;METROPOLITANA DE SANTIAGO\u0026#34;) |\u0026gt; # convertir st_as_sf(crs = 4326) |\u0026gt; # resumir por comuna group_by(COMUNA) |\u0026gt; summarize(n_ocupado = sum(n_ocupado), # sumar datos por grupo SHAPE = st_union(SHAPE)) # unir polígonos por grupo Obtenemos como resultado una tabla de datos por comuna:\ncomunas Simple feature collection with 52 features and 2 fields Geometry type: MULTIPOLYGON Dimension: XY Bounding box: xmin: -71.51187 ymin: -34.05721 xmax: -70.22923 ymax: -32.96357 Geodetic CRS: WGS 84 # A tibble: 52 × 3 COMUNA n_ocupado SHAPE \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;MULTIPOLYGON [°]\u0026gt; 1 ALHUÉ 2542 (((-71.24133 -34.05452, -71.2413 -34.05457, -71.24… 2 BUIN 47409 (((-70.85738 -33.81385, -70.85736 -33.81381, -70.8… 3 CALERA DE TANGO 5893 (((-70.83452 -33.66046, -70.83477 -33.66032, -70.8… 4 CERRILLOS 36188 (((-70.73703 -33.51005, -70.73702 -33.51005, -70.7… 5 CERRO NAVIA 52273 (((-70.76262 -33.42757, -70.76262 -33.42757, -70.7… 6 COLINA 62631 (((-70.71874 -33.32434, -70.71884 -33.32437, -70.7… 7 CONCHALÍ 54640 (((-70.68942 -33.38985, -70.68916 -33.3904, -70.68… 8 CURACAVÍ 10081 (((-71.15734 -33.41664, -71.15734 -33.41663, -71.1… 9 EL BOSQUE 64497 (((-70.68728 -33.5783, -70.68727 -33.57831, -70.68… 10 EL MONTE 13596 (((-71.05384 -33.69958, -71.05354 -33.69968, -71.0… # ℹ 42 more rows Ahora podemos visualizar un mapa de la población ocupada por comuna en la Región Metropolitana:\ncomunas |\u0026gt; ggplot() + aes(fill = n_ocupado) + # variable geom_sf(color = \u0026#34;white\u0026#34;, linewidth = 0.01) + colorspace::scale_fill_continuous_sequential(palette = \u0026#34;Sunset\u0026#34;, na.value = \u0026#34;white\u0026#34;, labels = scales::label_number(big.mark = \u0026#34;.\u0026#34;), breaks = c(0, 50000, 100000, 150000, 200000, 250000)) + theme_minimal(base_size = 10) + theme(axis.text.x = element_text(angle = 90, vjust = .5), legend.key.width = unit(3, \u0026#34;mm\u0026#34;)) + guides(fill = guide_colorbar(title = \u0026#34;Ocupados/as\u0026#34;, position = \u0026#34;right\u0026#34;)) + coord_sf(xlim = c(-70.83, -70.47), ylim = c(-33.3, -33.68)) + labs(title = \u0026#34;Población ocupada\u0026#34;, subtitle = \u0026#34;Región Metropolitana\u0026#34;, caption = \u0026#34;Fuente: Censo 2024, INE\u0026#34;) Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, : 'big.mark' and 'decimal.mark' are both '.', which could be confusing Para más información sobre visualización de mapas de Chile comunales y regionales, revisa este tutorial sobre mapas básicos de Chile, o este tutorial para usar cartografías oficiales. Mapas interactivos Para visualizar mapas interactivos en R existen varias opciones, como el paquete {leaflet}, pero yo quería usar el paquete {mapgl}, desarrollado por Kyle Walker que permite crear mapas interactivos usando MapLibre, una biblioteca de código abierto para mapas web.\nEste paquete se caracteriza por el acceso que ofrece a usuarixs de R a mapas online interactivos de alta calidad y alto rendimiento.\nPrimero instalamos {mapgl}:\ninstall.packages(\u0026#34;mapgl\u0026#34;) Luego lo cargamos:\nlibrary(mapgl) Y probamos su funcionamiento con un mapa vacío (prueba haciendo zoom o navegando el mapa con el cursor)\nmaplibre(center = c(-71.5, -33.0), zoom = 3) Ahora usemos {mapgl} con los datos censales. Podemos repetir el proceso de filtrado de la comuna o región que nos interese:\n# filtrar y seleccionar manzanas_comuna \u0026lt;- manzanas |\u0026gt; filter(COMUNA == \u0026#34;VALPARAÍSO\u0026#34;) |\u0026gt; select(REGION, n_edad_60_mas, SHAPE) Luego hacemos la misma conversión a {sf} para trabajar mejor con datos espaciales:\n# convertir a sf manzanas_comuna_sf \u0026lt;- manzanas_comuna |\u0026gt; st_as_sf(crs = 4326) Finalmente podemos visualizar los datos en un mapa interactivo con la función maplibre_view(), especificando la variable que queremos graficar en el mapa:\nmanzanas_comuna_sf |\u0026gt; maplibre_view(column = \u0026#34;n_edad_60_mas\u0026#34;) ¡Así de simple! 💜\nLo interesante es que {mapgl} también permite hacer cosas mucho más increíbles usando Mapbox, una plataforma de mapas de primer nivel que tiene muchas más opciones de personalización y estilos de mapas, incluyendo figuras tridimensionales, edificios, efectos visuales y más.\n","date":"2025-12-17T00:00:00Z","excerpt":"Recientemente lanzaron los datos 2024 del Censo de Población y Vivienda de Chile, incluyendo más de 200 variables a nivel de manzana. En este tutorial veremos dos formas de visualizar datos censales a nivel de manzana: con mapas estáticos en `{ggplot2}`, y con mapas interactivos con `{mapgl}`.","href":"https://bastianoleah.netlify.app/blog/mapas_censo_2024/","tags":"mapas ; Chile ; datos","title":"Visualiza datos del Censo 2024 en mapas a nivel de manzana con R"},{"content":"Para publicitar mi nueva página para aprender R gratis y de manera autodidacta, quise grabar un video mostrando los contenidos.\nPero al grabar el video, no me gustaba cómo quedaba cuando yo iba haciendo scroll manualmente, porque a veces me quedaban cortados los contenidos o se veía muy errático.\nAsí que hice lo que cualquier persona racional y ocupada haría, y automatizé el scroll con R, para que la página se moviera solita sin que yo intervenga. Así la grabación sale perfecta 💖\nPara automarizar el scroll usé el paquete {RSelenium}, que permite controlar un navegador web desde R.\nCargamos el paquete:\nlibrary(RSelenium) Luego creamos un navegador:\n# crear driver para controlar navegador driver \u0026lt;- rsDriver(browser = \u0026#34;firefox\u0026#34;, port = 4560L, verbose = F, chromever = NULL, phantomver = NULL) remote \u0026lt;- driver$client Se debería abrir una ventana de Firefox controlada por R!\nAhora navegamos al sitio:\nremote$navigate(url = \u0026#34;https://bastianolea.github.io/aprende_r/\u0026#34;) Si necesitas aprender lo básico sobre web scraping, revisa este tutorial introductorio, o bien, revisa el tutorial avanzado de RSelenium. Para hacer scroll, necesitamos ejecutar el siguiente comando de JavaScript en la página:\nremote$executeScript(\u0026#34;window.scrollBy(0, 300);\u0026#34;) Así la página baja 300 pixeles. Pero el scroll es instantáneo y se ve súper mal! ☹️\nSi le agregamos behavior: 'smooth', hacemos scroll suave:\nremote$executeScript(\u0026#34;window.scrollBy({ top: 300, left: 0, behavior: \u0026#39;smooth\u0026#39;});\u0026#34;) ¡Mucho mejor! Así podemos bajar poco a poco por el sitio.\nAhora quiero que el navegador haga scroll hasta llegar a un título específico de la página, así no tengo que ir haciendo scroll por cantidades específicas de pixeles. Para esto, tengo que encontrar el selector del elemento al que quiero llegar.\nEn este caso usaré el ID (#) de cada subtítulo. Por ejemplo, para llegar al título \u0026ldquo;Obtener R\u0026rdquo;, el selector es #obtener-r:\nremote$executeScript(\u0026#34;document.querySelector(\u0026#39;#obtener-r\u0026#39;).scrollIntoView({behavior: \u0026#39;smooth\u0026#39;});\u0026#34;) Ahora el comando me lleva a un elemento específico, con suavidad 😌\n¡Pero hay otro problema! 😣\nComo la página tiene un menú arriba, al scrollear a un elemento, el elemento queda tapado por el menú. Así que necesito hacer scroll ligeramente más arriba que el elemento al que quiero llegar, de manera que se alcance a leer el título!\nEntonces, en el siguiente comando primero se encuentra al elemento, se obtiene su posición vertical en la página, a la cual se le resta una cantidad de pixeles (en este caso 90), y luego se hace scroll a esa posición:\nremote$executeScript(\u0026#34; const element = document.querySelector(\u0026#39;#obtener-r\u0026#39;); const y = element.getBoundingClientRect().top + window.scrollY - 90; window.scrollTo({ top: y, behavior: \u0026#39;smooth\u0026#39; });\u0026#34;) Increíble, sinceramente.\nAhora estamos listos para hacer el boceto del desplazamiento por la página.\nPero antes, vamos a simplificar todo, metiendo esos comandos en funciones, porque no tiene sentido copiar y pegar 20 veces el mismo código!\n# scroll hasta el elemento + offset scrollear_elemento \u0026lt;- function(elemento = \u0026#34;#introduccion\u0026#34;, scroll = 90) { remote$executeScript(paste(\u0026#34; const element = document.querySelector(\u0026#39;\u0026#34;, elemento, \u0026#34;\u0026#39;); const y = element.getBoundingClientRect().top + window.scrollY - \u0026#34;, scroll, \u0026#34;; window.scrollTo({ top: y, behavior: \u0026#39;smooth\u0026#39; });\u0026#34;) ) } # scroll por pixeles exactos scrollear_manual \u0026lt;- function(scroll = 90) { remote$executeScript(paste(\u0026#34; window.scrollBy({ top:\u0026#34;, scroll, \u0026#34;, left: 0, behavior: \u0026#39;smooth\u0026#39; });\u0026#34;)) } # función de espera entre scrolls espera \u0026lt;- function(largo = \u0026#34;larga\u0026#34;) { if (largo == \u0026#34;larga\u0026#34;) { Sys.sleep(1.1) } else if (largo == \u0026#34;corta\u0026#34;) { Sys.sleep(0.7) } } Creamos tres funciones: scrollear_elemento() para hacer scroll a un elemento específico, scrollear_manual() para bajar una cantidad exacta de pixeles, y espera() para pausar el código entre scrolls para dar tiempo para leer, y que puede ser una espera larga o corta.\nAsí, la escritura de los comandos es más rápida, y el código es más sencillo de leer. Solamente hay que ir poniendo las funciones una tras la otra.\nEl guión se ve más o menos así:\nscrollear_elemento(\u0026#34;#introduccion\u0026#34;) espera() scrollear_elemento(\u0026#34;#obtener-r\u0026#34;) espera() scrollear_manual(400) espera() scrollear_elemento(\u0026#34;#basico\u0026#34;) espera() scrollear_manual(400) espera(\u0026#34;corta\u0026#34;) scrollear_manual(260) espera(\u0026#34;corta\u0026#34;) scrollear_manual(260) espera(\u0026#34;corta\u0026#34;) scrollear_manual(260) espera(\u0026#34;corta\u0026#34;) scrollear_elemento(\u0026#34;#trabajando-con-datos-en-r\u0026#34;) espera() Entonces la página va suavemente al primer subtítulo, espera, luego va a la siguiente, espera, baja un poquito para mostrar más contenido, espera, luego al siguiente\u0026hellip; y así hasta recorrer toda la página.\nAl llegar al final agregamos lo siguiente para subir hasta arriba1\n# volver arriba scrollear_elemento(\u0026#34;body\u0026#34;) Entonces solamente queda poner a grabar la pantalla y ejecutar todas esas líneas al mismo tiempo, y listo, el sitio se presenta por sí solo de manera perfecta y yo puedo reclinarme a tomar café ☕️\nno vamos a subir hasta abajo 🤪\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-12-16T00:00:00Z","excerpt":"Para publicitar mi nueva página para [aprender R](https://bastianolea.github.io/aprende_r/) quise grabar un video, así que lo hice controlando el movimiento de la página desde R usando `{RSelenium}`, herramienta de web scraping que te permite controlar un navegador web. ¿Quedó bonito?","href":"https://bastianoleah.netlify.app/blog/2025-12-16/","tags":"web scraping","title":"Automatizar el scroll de una página web desde R con `{RSelenium}`"},{"content":"** No content below YAML for the series _index. This file is a leaf bundle, and provides settings for the listing page layout and sidebar content.**\n","date":null,"excerpt":"\u003cp\u003e** No content below YAML for the series _index. This file is a leaf bundle, and provides settings for the listing page layout and sidebar content.**\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/r_introduccion/","tags":"","title":"Introducción a R"},{"content":" En este blog ya hemos visto varias herramientas de IA que te pueden ayudar a usar R, por ejemplo el paquete {gander}, que al presionar una combinación de teclas cuando tienes código seleccionado invoca una ventana donde puedes pedir a una IA que haga cosas con ese código y/o datos, gracias a un conocimiento contextual de tu entorno de R.\nAhora te presento otro paquete que va más alla: el paquete {btw} (abreviación de by the way o por cierto) trae un chat de IA interactivo directamente a RStudio o Positron, que además cuenta con la capacidad de utilizar herramientas para interactuar con tus datos y tu código, y posee conocimiento contextual no sólo de tu entorno de R y tus datos, sino también de las funciones de los paquetes que usas y su documentación!\nTodas estas características potencian a los modelos de lenguaje al entregarles contexto valioso para que puedan ayudarte de forma más certera. Esto significa que:\nHabrá menos alucinación, ya que en vez de adivinar, el modelo recurrirá a la documentación de cada paquete y función para saber exactamente cómo se usan, El modelo conocerá exactamente tus datos, sus columnas y tipos, así que sus respuestas incluirán nombres de variable y funciones correctas, Al saber qué paquetes tienes instalados y cargados, el modelo podrá sugerirte funciones específicas de esos paquetes para realizar tareas concretas, en vez de cosas que no usas. En resumen, se trata de un asistente de IA que te entregará respuestas más certeras sin que tengas que estar explicándole todo.\nInstala {btw} con:\ninstall.packages(\u0026#34;btw\u0026#34;) Chat básico con una IA en R Antes de mostrarte {btw}, veremos como comparación un chat normal con una IA desde R, usando {ellmer}. La hipótesis es que no va a poder responder bien 🤓\n{ellmer} es un paquete de R que facilita la interacción con modelos de lenguaje desde R, y se usa como el motor de muchos otros paquetes que usan IA. Antes que nada, para poder usar la IA en R tienes que tener una llave API de tu proveedor de IA (OpenAI, Anthropic, Copilot, etc.) configurada en tus variables de entorno, como se explica en la documentación de {ellmer}.\nPuedes encontrar instrucciones más detalladas sobre configurar el uso de IA en R en esta publicación. Configurar tu proveedor de IA en R En resumen, ejecuta usethis::edit_r_environ() para abrir tu archivo .Renviron, donde se pueden guardar secretos que se aplican a todas tus sesiones de R pero quedan ocultos, y agrega una línea con la API key, por ejemplo:\nOPENAI_API_KEY=345345398475937434534539847593743453453984759374 Si tu proveedor es Claude (Anthropic), el nombre de la variable es ANTHROPIC_API_KEY, etc.\nPrimero creamos el chat con el proveedor de IA que usemos:\nchat \u0026lt;- ellmer::chat_github() # yo uso Copilot # chat \u0026lt;- ellmer::chat_openai() Luego podemos interactuar con el modelo por medio del objeto chat$chat(). Vamos a hacerle una pregunta sencilla:\n¿Qué objetos tengo en mi entorno de R?\nchat$chat(\u0026#34;¿Qué objetos tengo en mi entorno de R?\u0026#34;) El modelo responde:\nPara **ver los objetos que tienes en tu entorno de R**, puedes usar la función: ```r ls() ``` Esto te muestra una lista de los nombres de los objetos `(.GlobalEnv)` que has creado (como variables, data frames, funciones, etc.). Al igual que ChatGPT o cualquier otro chat de IA que usarías en la web, este modelo no tiene acceso a tu sesión de R ni conocimiento sobre tu contexto, por lo que sugiere soluciones genéricas! 🙄\nChat interactivo con conocimiento contextual A diferencia de un modelo sin contexto, al que tienes que darle mucha información en cada prompt para que pueda ayudarte, la función btw_app() de {btw} invoca una aplicación interactiva con un chat cuyo modelo tiene acceso a tu entorno de R, tus datos, los paquetes que tienes instalados y cargados, y otras herramientas que le van a permitir responder mejor tus consultas.\nchat \u0026lt;- ellmer::chat_github() # crear chat library(btw) btw_app(client = chat) # lanzar char interactivo Al ejecutar btw_app() se abre una aplicación interactiva con la que puedes chatear:\nComo vemos en la imagen, ante la misma pregunta este modelo responde de forma mucho más precisa, entregando información exacta sobre los objetos que realmente tengo en mi entorno de R!\nEsto funciona porque {btw} registra herramientas que el modelo puede usar para obtener contexto e información, sin que tú tengas que hacer nada. Por ejemplo, en el ejemplo anterior vemos que el modelo llama por sí solo una herramienta que le entrega tus datos en formato JSON, y por eso responde bien:\nUsa este chat interactivo para hacerle preguntas sobre tu entorno de R, tus datos, paquetes o funciones, y más, sin salir de RStudio!\nOtra forma de invocar {btw} es desde el menú de Addins de RStudio, con el beneficio extra de que el asistente corre en otro proceso y así no bloquea tu consola.\nPara acceder más rápido a este chat IA, recomiendo configurar un atajo de teclado: en RStudio, ve a Tools \u0026gt; Modify Keyboard Shortcuts, busca btw y asigna un atajo como Shift + Cmd + B)\nAtajo de teclado en macOS (usa control o alt en vez de comando si usas Linux o Windows Así el atajo de teclado me queda cerca del de {gander}, otra herramienta útil para aplicar IA directamente a tu código de R 😊\nChat con conocimiento contextual por la consola También es posible reforzar un chat con una IA para que tenga conocimiento contextual, sin necesidad de usar la aplicación interactiva, sino directamente desde la consola de R. Para eso usamos btw_client() sobre un chat nuevo o anterior, para que {btw} le entregue contexto y herramientas al modelo:\nchat \u0026lt;- btw_client(client = ellmer::chat_github()) # reforzar chat chat$chat(\u0026#34;¿qué objetos tengo en mi entorno de R?\u0026#34;) A diferencia de el primer ejemplo, ahora el modelo de lenguaje sí puede acceder a mi entorno de R y responder correctamente.\nEjemplo de uso En una sesión de R cargué un archivo con datos, luego lancé una sesión de chat con btw::btw_app(), y le pregunté:\nCon la base de datos que tengo cargada, ¿cómo puedo hacer un gráfico que compare el valor del capital humano en cada región?\nGracias al conocimiento contextual y el acceso a herramientas, responde correctamente, usando nombres y valores correctos:\nMe entregó este código, que pude copiar con un clic:\nhuman_capital \u0026lt;- datos |\u0026gt; filter(factor == \u0026#34;Capital Humano\u0026#34;) library(ggplot2) ggplot(human_capital, aes(x = factor(region), y = stock_2023)) + geom_bar(stat = \u0026#34;identity\u0026#34;, fill = \u0026#34;steelblue\u0026#34;) + labs(title = \u0026#34;Capital Humano por Región\u0026#34;, x = \u0026#34;Región\u0026#34;, y = \u0026#34;Stock 2023 (Capital Humano)\u0026#34;) + theme_minimal() No es el gráfico más bonito del mundo (a mi me quedan mejores, todavía no me quitan el trabajo 😌), pero demuestra que sabe hacer la pega! Otros modelos hubieran inventado columnas, no hubieran podido hacer el filtro, o no sabrían poner los títulos de los ejes.\nOtros Para evitar tener que crear un chat y/o especificar proveedor y modelo cada vez, puedes configurarlos en tu perfil de R: en usethis::edit_r_profile(), incluye la siguiente línea especificando tu proveedor y modelo:\noptions(btw.client = ellmer::chat_github()) |\u0026gt; suppressMessages() Conoce más herramientas de IA en R en este video de RLadies Paris:\n","date":"2025-12-10T00:00:00Z","excerpt":"El paquete `{btw}` te ofrece un chat de IA interactivo directamente a RStudio o Positron, que además cuenta con la capacidad de utilizar herramientas para interactuar con tus datos y tu código, y posee conocimiento contextual no sólo de tu entorno de R y tus datos, sino también de las funciones de los paquetes que usas y su documentación. Se trata de un asistente de IA que te entregará respuestas más certeras sin que tengas que estar explicándole todo.","href":"https://bastianoleah.netlify.app/blog/btw/","tags":"inteligencia artificial ; consejos","title":"Interactúa desde R con una IA que conoce tus datos, archivos y paquetes"},{"content":"Las hojas de Excel pueden ser cómodas para organizar información, pero no mucho para procesarla o analizarla. Por lo mismo, una de las operaciones iniciales de limpieza de datos suele ser unir datos que vienen repartidos en varias hojas de Excel.\nEn este tutorial vamos a ver cómo se hace paso a paso. Usaremos el paquete {readxl} para leer los datos, {dplyr} para manipular y combinar las hojas, y {purrr} para realizar operaciones sobre todas las hojas de forma automática.\nDatos Como ejemplo, usaremos un archivo de Excel con datos falsos, que puedes descargar a continuación:\nDescargar datos falsos Ver código para generar los datos falsos library(purrr) library(dplyr) nombres_hojas \u0026lt;- paste(\u0026#34;hoja\u0026#34;, 1:20) # crear una lista con tablas de datos sintéticos datos_falsos \u0026lt;- map( # iterar por cada elemento set_names(nombres_hojas), # poner nombre a cada elemento ~{ n = sample(5:30, 1) # cantidad de filas al azar # datos al azar datos \u0026lt;- tibble(variable_a = sample(letters, n, replace = T), variable_b = rnorm(n), variable_c = rnorm(n), ) # que una de las tablas sea distinta if (.x == \u0026#34;hoja 13\u0026#34;) { datos \u0026lt;- datos |\u0026gt; mutate(variable_c = sample(letters, n, replace = T)) } return(datos) } ) # guardar archivo writexl::write_xlsx(datos_falsos, \u0026#34;datos_falsos.xlsx\u0026#34;) Se trata de un archivo con 20 hojas, y tres columnas con datos al azar. La planilla de Excel se ve más o menos así:\nCargar datos desde una hoja de Excel Para cargar datos desde una hoja específica de un archivo Excel, usamos la función read_excel() del paquete readxl, definiendo la hoja en el argumento sheet (ya sea según su posición o su nombre).\nlibrary(readxl) datos \u0026lt;- read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;, sheet = 2) head(datos) # A tibble: 6 × 3 variable_a variable_b variable_c \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 j 0.0841 -0.339 2 z -0.0351 -0.278 3 k -1.24 0.353 4 p 1.32 -0.767 5 f 1.06 -0.569 6 x 1.27 0.750 Obtenemos sólo los datos de la hoja especificada. Esta es la base que nos permitirá cargar desde múltiples hojas.\nUnir datos desde varias hojas de Excel manualmente La forma básica de unir los datos de varias hojas sería repetir la lectura de datos anterior, y luego unir los objetos resultantes con bind_rows() de dplyr.\nLa función bind_rows() une varias tablas con las mismas columnas, apilándolas una debajo de la otra, como una torta 🍰 # cargar hojas individualmente datos_1 \u0026lt;- read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;, sheet = 1) datos_2 \u0026lt;- read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;, sheet = 2) datos_3 \u0026lt;- read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;, sheet = 3) library(dplyr) # unir todas las hojas bind_rows(datos_1, datos_2, datos_3) # A tibble: 52 × 3 variable_a variable_b variable_c \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 b 0.0980 0.605 2 f -1.56 0.815 3 i 0.245 1.05 4 m 0.178 0.779 5 x -0.587 0.443 6 l -1.27 0.591 7 y -0.317 -0.276 8 j 0.0841 -0.339 9 z -0.0351 -0.278 10 k -1.24 0.353 # ℹ 42 more rows Pero pronto nos damos cuenta de que esto no es sostenible: si tenemos 20 hojas, o 50, o 100, no podemos estar copiando y pegando el mismo código una y otra vez! Ni menos crear 100 objetos distintos para cada hoja!\nCuando repitas código 3 veces, significa que lo correcto sería hacer una función o un loop Necesitamos automatizar este código para aplicarlo a todas las hojas que queramos.\nCorregir diferencias entre hojas al unirlas Pero ¿qué pasa si las hojas tienen datos inesperados? Intentemos unir otras hojas del mismo archivo:\n#Se cambian los datos porque no calzan entre las dos columnas el tipo de dato, datos_11 \u0026lt;- read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;, sheet = 11) datos_12 \u0026lt;- read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;, sheet = 12) datos_13 \u0026lt;- read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;, sheet = 13) bind_rows(datos_11, datos_12, datos_13) Error in `bind_rows()`: ! Can't combine `..1$variable_c` \u0026lt;double\u0026gt; and `..3$variable_c` \u0026lt;character\u0026gt;. ¡Obtenemos un error! No siempre podemos asumir que todo va a salir bien (casi nunca todo sale bien). Al unir varias hojas, si alguna viene con datos incorrectos, la unión con bind_rows() falla.\nEn este caso, según el error vemos que la columna variable_c es distinta en una de las hojas:\nwaldo::compare(datos_12$variable_c, datos_13$variable_c) `old` is a double vector (0.104654760635383, -0.87864148501293, -0.67300275472249, 1.290359654706, 2.25558071386618, ...) `new` is a character vector ('h', 'd', 'w', 'm', 'i', ...) Si comparamos las columnas con {waldo}, confirmamos usando que la columna c viene con datos tipo carácter en una de las hojas, mientras que en las otras hojas es numérica, por lo que R se niega a hacer la unión.\nRecordemos que las columnas sólo pueden ser de un tipo en R, por lo que no puedes mezclar números con texto en una columna! La solución parche sería corregir los datos en esa hoja específica, y reintentar la unión:\nlibrary(dplyr) datos_13b \u0026lt;- datos_13 |\u0026gt; mutate(variable_c = as.numeric(variable_c)) Warning: There was 1 warning in `mutate()`. ℹ In argument: `variable_c = as.numeric(variable_c)`. Caused by warning: ! NAs introduced by coercion bind_rows(datos_11, datos_12, datos_13b) # A tibble: 51 × 3 variable_a variable_b variable_c \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 v 0.227 -0.374 2 q 0.299 -1.09 3 k -1.03 0.117 4 m -0.828 1.24 5 k -0.475 -0.631 6 d 0.448 0.918 7 i 0.845 -0.682 8 l 0.232 0.885 9 r 0.150 -0.652 10 w -0.474 -0.667 # ℹ 41 more rows ¡Funciona!\nYa vimos cómo cargar y unir varias hojas de Excel, así que ahora veremos cómo automatizar este proceso para que funcione con cualquier cantidad de hojas.\nUnir datos desde varias hojas de Excel automáticamente Para realizar operaciones que se repiten a lo largo de una serie de elementos (sean hojas de Excel, archivos, columnas, filas, etc), usamos loops (bucles) para automatizar el proceso.\nRepaso de loops con purrr::map() Antes de seguir avanzando, haremos un repaso de loops con el paquete {purrr}.\nUn loop o bucle en R es una estructura de control que permite repetir un bloque de código varias veces, iterando sobre una secuencia de elementos En un loop, tenemos una secuencia de algo, a la cual vamos a repetirle una operación. Se realizan tantas operaciones o pasos como elementos haya en la secuencia.\nCon las funciones para loops del paquete {purrr}, cada paso va agregando los resultados como un elemento de una lista, la cual podemos combinar al final si queremos.\nVeamos un ejemplo básico: tenemos números del 1 al 4, y por cada número, queremos multiplicar por 10, y obtener el resultado.\n# install.packages(\u0026#34;purrr\u0026#34;) library(purrr) # creamos una secuencia de elementos hojas \u0026lt;- c(1, 2, 3, 4) # por cada elemento de la secuencia, repetimos una operación map(hojas, ~{.x * 10} ) [[1]] [1] 10 [[2]] [1] 20 [[3]] [1] 30 [[4]] [1] 40 En el ejemplo anterior, iteramos sobre una secuencia de números del 1 al 4. Por cada número, que en cada paso se representa por .x, multiplicamos el número por 10 (.x * 10), y el resultado de cada paso se guarda como un elemento de una lista.\nUna lista en R es un objeto que puede contener varios elementos, los cuales pueden ser de distintos tipos y tamaños Loop para cargar hojas Siguiendo el mismo principio del ejemplo anterior, iteramos por las hojas del 1 al 3, y dentro del loop, definimos que se cargue el archivo Excel en la hoja correspondiente a cada número de la secuencia.\nEntonces, en el paso 1 se carga la hoja 1, en el paso 2 se carga la hoja 2, y así sucesivamente hasta la hoja 10.\nAl final le ponemos list_rbind() (parecido a bind_rows()) para que todos los elementos de la lista se unan en un sólo dataframe., asumiendo que todas las hojas tienen datos compatibles.\n# secuencia de hojas a cargar hojas \u0026lt;- c(1:3) # loop datos \u0026lt;- map(hojas, ~{ # por cada hoja read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;, sheet = .x) # cargar el archivo en la hoja correspondiente } ) datos [[1]] # A tibble: 7 × 3 variable_a variable_b variable_c \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 b 0.0980 0.605 2 f -1.56 0.815 3 i 0.245 1.05 4 m 0.178 0.779 5 x -0.587 0.443 6 l -1.27 0.591 7 y -0.317 -0.276 [[2]] # A tibble: 16 × 3 variable_a variable_b variable_c \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 j 0.0841 -0.339 2 z -0.0351 -0.278 3 k -1.24 0.353 4 p 1.32 -0.767 5 f 1.06 -0.569 6 x 1.27 0.750 7 z -0.0293 0.775 8 k -1.51 -0.415 9 c -1.57 -1.88 10 e -1.39 0.618 11 c 0.924 -1.40 12 q -1.82 1.26 13 w 1.71 1.62 14 u -1.08 -1.10 15 y 0.358 -0.141 16 b 1.47 0.936 [[3]] # A tibble: 29 × 3 variable_a variable_b variable_c \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 w 0.235 -0.204 2 m -0.107 0.0811 3 r -0.0999 -1.58 4 p 0.731 -0.0249 5 w 0.146 -0.730 6 y -0.160 -1.63 7 c 0.915 0.451 8 h -0.144 -1.00 9 x 1.14 0.636 10 w 0.106 -0.135 # ℹ 19 more rows ¡Cargamos 3 hojas! El resultado es una lista con tres elementos. Ahora unimos el resultado con list_rbind() para que quede una sola tabla con el contenido de cada hoja:\ndatos |\u0026gt; list_rbind() # unir todo al final # A tibble: 52 × 3 variable_a variable_b variable_c \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 b 0.0980 0.605 2 f -1.56 0.815 3 i 0.245 1.05 4 m 0.178 0.779 5 x -0.587 0.443 6 l -1.27 0.591 7 y -0.317 -0.276 8 j 0.0841 -0.339 9 z -0.0351 -0.278 10 k -1.24 0.353 # ℹ 42 more rows Pero ¿qué pasa si ampliamos la cantidad de hojas, en específico al pasar por la hoja 13 que tenía datos incorrectos?\n# secuencia de hojas a cargar hojas \u0026lt;- c(1:20) # loop map(hojas, ~{ read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;, sheet = .x) } ) |\u0026gt; list_rbind() # unir todo al final Error in `list_rbind()`: ! Can't combine `..1$variable_c` \u0026lt;double\u0026gt; and `..13$variable_c` \u0026lt;character\u0026gt;. Error! Como vimos antes, el problema con esta hoja que tenía una columna distinta va a evitar que los resultados se unan al final del loop.\nEntonces, dentro del loop podemos aplicar la misma corrección que probamos antes:\nhojas \u0026lt;- c(1:20) map(hojas, ~{ read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;,sheet = .x) |\u0026gt; mutate(variable_c = as.numeric(variable_c)) # corregir columna } ) |\u0026gt; list_rbind() Warning: There was 1 warning in `mutate()`. ℹ In argument: `variable_c = as.numeric(variable_c)`. Caused by warning: ! NAs introduced by coercion # A tibble: 350 × 3 variable_a variable_b variable_c \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 b 0.0980 0.605 2 f -1.56 0.815 3 i 0.245 1.05 4 m 0.178 0.779 5 x -0.587 0.443 6 l -1.27 0.591 7 y -0.317 -0.276 8 j 0.0841 -0.339 9 z -0.0351 -0.278 10 k -1.24 0.353 # ℹ 340 more rows Con este código cargamos los datos de todas las hojas, aplicando la corrección necesaria para que los datos se puedan unir correctamente, y obtuvimos como resultado una sola tabla con todos los datos hacia abajo!\nOtra opción más específica (menos extrapolable) sería aplicar la corrección sólo a la hoja que sabemos que tiene el problema, usando una condición if dentro del loop:\nmap(hojas, ~{ datos_hoja \u0026lt;- read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;,sheet = .x) if (.x == 13) { # si es la hoja 13 datos_hoja \u0026lt;- datos_hoja |\u0026gt; mutate(variable_c = as.numeric(variable_c)) # corregir columna } return(datos_hoja) } ) |\u0026gt; list_rbind() Esta forma de hacerlo es menos reutilizable, pero si te permite una mayor flexibilidad al momento de aplicar correcciones más complejas.\nAgregar el nombre de la hoja como una variable nueva Si queremos agregar una columna que indique desde qué hoja vienen los datos, primero usamos la función excel_sheets() para obtener los nombres de las hojas:\n# obtener nombres de las hojas nombres_hojas \u0026lt;- readxl::excel_sheets(\u0026#34;datos_falsos.xlsx\u0026#34;) Como se trata de un vector, podemos extraer sus elementos usando su posición, para saber cómo se llama cada hoja:\n# consultar el nombre de una hoja nombres_hojas[10] [1] \u0026quot;hoja 10\u0026quot; Ahora que sabemos los nombres de las hojas, podemos iterar el loop usando los nombres directamente (en vez de números), y aprovechar de usar el nombre en cada paso para agregar una columna que se llame hoja:\ndatos \u0026lt;- map( nombres_hojas, # iterar por el nombre de cada hoja ~{ message(.x) # decir la hoja al leerla read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;, sheet = .x) |\u0026gt; mutate(variable_c = as.numeric(variable_c)) |\u0026gt; mutate(hoja = .x) # agregar nombre de hoja como columna } ) |\u0026gt; list_rbind() hoja 1 hoja 2 hoja 3 hoja 4 hoja 5 hoja 6 hoja 7 hoja 8 hoja 9 hoja 10 hoja 11 hoja 12 hoja 13 Warning: There was 1 warning in `mutate()`. ℹ In argument: `variable_c = as.numeric(variable_c)`. Caused by warning: ! NAs introduced by coercion hoja 14 hoja 15 hoja 16 hoja 17 hoja 18 hoja 19 hoja 20 # obtener 10 filas al azar slice_sample(datos, n = 10) # A tibble: 10 × 4 variable_a variable_b variable_c hoja \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 w -0.931 NA hoja 13 2 c -1.27 -0.451 hoja 12 3 w -0.983 -1.32 hoja 6 4 q -0.655 -2.06 hoja 16 5 a 0.0417 -0.0949 hoja 10 6 l 0.636 0.122 hoja 20 7 j 1.21 -0.671 hoja 11 8 u -0.818 -0.133 hoja 4 9 e -0.626 1.74 hoja 9 10 n -0.911 -0.572 hoja 10 Otra opción más rudimentaria, pero a veces necesaria, es iterar por la posición de cada hoja, y luego usar esa posición para extraer el nombre de la hoja desde el vector nombres_hojas. Usamos seq_along() para obtener un vector de números sucesivos por cada elemento del objeto, e iteramos por el loop siguiendo esos números.\nLa función seq_along() genera una secuencia de números desde 1 hasta el largo del objeto que le pasemos como argumento. Es equivalente a 1:length(x) Dentro del loop referimos el número (.x) para obtener el mismo elemento del vector de nombres (nombres_hojas[.x]), y así agregar una columna nueva con el nombre de la hoja:\ndatos \u0026lt;- map( seq_along(nombres_hojas), # iterar por la posición de cada hoja ~{ message(nombres_hojas[.x]) # decir la hoja al leerla read_excel(\u0026#34;datos_falsos.xlsx\u0026#34;,sheet = .x) |\u0026gt; # cargar hoja mutate(variable_c = as.numeric(variable_c)) |\u0026gt; # corrección mutate(hoja = nombres_hojas[.x]) # agregar nombre de hoja como columna } ) |\u0026gt; list_rbind() head(datos) Guardar archivo resultante Ahora que tenemos los datos de todas las hojas unidos en una sola planilla, podemos guardarlos en un nuevo archivo Excel usando la función write_xlsx() del paquete writexl:\nwritexl::write_xlsx(datos, \u0026#34;datos_unidos.xlsx\u0026#34;) ¡Y listo! Hemos aprendido a cargar y unir datos desde múltiples hojas de Excel, enfrentando problemas comunes como datos incompatibles, y automatizando el proceso para cualquier cantidad de hojas.\n","date":"2025-12-09T00:00:00Z","excerpt":"Las hojas de Excel pueden ser cómodas para organizar información, pero no mucho para procesarla o analizarla. Por lo mismo, una de las operaciones iniciales de limpieza de datos suele ser unir datos que vienen repartidos en varias hojas de Excel. Veamos cómo se hace paso a paso. Usaremos el paquete `{readxl}` para leer los datos, `{dplyr}` para manipular y combinar las hojas, y `{purrr}` para realizar operaciones sobre todas las hojas de forma automática.","href":"https://bastianoleah.netlify.app/blog/excel_unir_hojas/","tags":"Excel ; limpieza de datos ; datos","title":"Cargar y unir datos de múltiples hojas de Excel con R"},{"content":"Viendo el streaming de la conferencia LatinR 2025 conocí el paquete de R {blockr}, que nos ofrece una interfaz gráfica interactiva para usar R sin escribir código.\nComo su nombre lo indica, funciona por medio de bloques que se conectan entre sí, representando de forma visual el flujo de procesamiento de datos.\nEsta herramienta puede servirle a gente que esté aprendiendo R y quizás les sirva empezar por lo práctico antes de entrar al código. También puede ser útil para personas que les cueste programar en R, o que tengan alguna dificultad para escribir código, o bien, para quienes quieran esbozar un proceso antes de escribirlo.\nEmpezar Para empezar a usarlo, primero hay que instalar el paquete. Se requiere tener el paquete {pak} instalado, y luego usarlo para instalar {blockr}:\n# install.packages(\u0026#34;pak\u0026#34;) pak::pak(\u0026#34;BristolMyersSquibb/blockr\u0026#34;) Una vez instalado, lo cargamos y ejecutamos la interfaz gráfica:\nlibrary(blockr) run_app() La interfaz empieza como un lienzo vacío, al cual hay que agregar bloques.\nLes voy a mostrar un ejemplo sencillo, pero recomiendo seguir las instrucciones oficiales, que son bien completas.\nAl agregar un bloque, se nos abre un panel donde escribimos lo que necesitamos:\nCargar datos Para partir, agregamos un bloque de carga de datos. Podemos cargar datos desde un archivo local, importarlos a {blockr} para que el paquete los guarde, o bien usar datos de ejemplo.\nAl elegir el bloque, éste aparece en el lienzo, y en el panel derecho aparecen las opciones, que son equivalentes a los argumentos de las funciones en R:\nPero además tiene otros beneficios: por ejemplo, el bloque de carga de datos detecta automáticamente el tipo de datos que vamos a cargar, simplificando un poco el proceso de tener que encontrar un paquete y una función específicas.\nUna vez que tenemos un bloque, le agregamos (append) una conexión a un nuevo bloque:\nManipular datos Vamos a hacer una selección de columnas para acotar el conjunto de datos:\nEl bloque de la acción nueva aparece conectado a los datos, y en el panel derecho elegimos las columans y vamos previsualizando los datos en tiempo real:\nLuego podemos agregar un filtro, para elegir las filas de la tabla. La interfaz va sugiriendo variables y columnas en todo momento:\nCrear y transformar columnas Ahora vamos a transformar algunas columnas:\nAl transformar columnas, nos encontramos con una interfaz parecida a la de mutate(), donde vamos definiendo nuevas columnas a partir de las existentes. Pero en este punto ya es necesario ir aplicando un poquito de código. En este caso, redondeamos los valores de una columna.\nAsí va quedando nuestro flujo hasta ahora:\nAhora vamos a guardar el tablero que hemos ido trabajando. Para guardar se aprieta la barra negra que está al costado derecho de la aplicación y aparece una barra lateral con opciones para guardar y cargar (browse) tableros:\nVisualizar datos Para ir finalizando, crearemos un gráfico con {blockr} basado en {ggplot2}. Para ello, agregamos un bloque de gráfico:\nNos encontramos con un panel donde podemos elegir el tipo de gráfico que queremos, y seleccionamos las columnas que vamos a aplicar a cada aspecto del gráfico. En este sentido se parece un poco a {esquisse}, paquete para hacer gráficos de forma interactiva.\nObtenemos un gráfico básico, que acá podemos ver junto al flujo de procesamiento de datos que lo genera. Es interesante poder ir agregando bloques para obtener caminos separados en el flujo, que luego puedes reordenar o reconectar entre sí.\nExportar código Finalmente, podemos exportar el código R que genera todo el flujo de procesamiento de datos y gráficos que hemos creado. Esto es súper útil, porque nos permite aprender a programar en R viendo el código que genera cada bloque, y luego obtener el código para continuar o mejorar el análisis fuera de {blockr}.\nEntramos al panel lateral y elegimos show code:\nSe abre una ventana con el código R completo que genera todo el flujo de procesamiento de datos y gráficos que hemos creado:\nMe parece una alternativa bien interesante para usar R! Tiene harto potencial para aprender, explorar, y enseñar de una manera más liviana e interactiva, con miras a luego profundizar usando el lenguaje.\nVideo Mira el video de la presentación en LatinR para saber más:\nOtros recursos {esquisse}, paquete de R para hacer gráficos de forma interactiva. DisplayR, servicio online para programar dashboards analíticos con R sin necesidad de usar código, que además tiene varios ejemplos de productos. ","date":"2025-12-05T00:00:00Z","excerpt":"El paquete de R `{blockr}` nos ofrece una interfaz gráfica interactiva para poder usar R sin escribir código. Como su nombre lo indica, funciona por medio de bloques que se conectan entre sí, representando de forma visual el flujo de procesamiento de datos. En esta guía te muestro un ejemplo de su uso!","href":"https://bastianoleah.netlify.app/blog/blockr/","tags":"curiosidades ; básico","title":"Procesa datos con R sin programar y de forma interactiva"},{"content":"Hoy fue el lanzamiento del Sistema de Indicadores y Estándares Territoriales (SIET), y durante su exposición, Rodolfo Arriagada presentó este gráfico exploratorio que hicimos en conjunto.\nEl gráfico ubica las 346 comunas del país en un plano según su porcentaje de acceso al agua potable y el porcentaje de red vial pavimentada. Los colores y los cuadrantes destacan el comportamiento de las comunas según su clasificación: urbana, mixta o rural.\nEl resultado son tres \u0026ldquo;focos\u0026rdquo; que agrupan a las comunas de acuerdo a sus realidades en términos de infraestructura y servicios básicos, evidenciando la utilidad de las nuevas clasificaciones para poder pensar en detalle los desafíos territoriales en Chile.\nLes dejo un video del proceso de desarrollo de la visualización:\n🗺️📊 El SIET es una nueva herramienta que reúne datos sociales, económicos, ambientales y culturales, y sus estándares territoriales permitirán medir brechas y orientar políticas e inversiones públicas. Felicitaciones al Consejo Nacional de Desarrollo Territorial, al Instituto Nacional de Estadísticas de Chile y otros organismos involucrados en este gran proyecto para el seguimiento y evaluación de políticas territoriales en el país!\nMira la presentación aquí:\n","date":"2025-12-04T00:00:00Z","excerpt":"Hoy fue el lanzamiento del [Sistema de Indicadores y Estándares Territoriales](https://www.siet-chile.cl) (SIET), y durante su exposición, [Rodolfo Arriagada](https://www.linkedin.com/in/rarriagadac/) presentó este gráfico exploratorio que hicimos en conjunto. El gráfico ubica las 346 comunas del país en un plano según su porcentaje de acceso al agua potable y el porcentaje de red vial pavimentada.","href":"https://bastianoleah.netlify.app/blog/2025-12-04/","tags":"blog ; gráficos ; videos","title":"Gráfico exploratorio para el Sistema de Indicadores y Estándares Territoriales"},{"content":"Recientemente he estado retocando algunos aspectos de este blog. Quería contarles los principales cambios: tablas de contenido en todas las publicaciones, nuevo tema de colores para bloques de código, y resultados de búsqueda con resúmenes de posts, y más.\nTablas de contenido en posts Uno de los cambios principales, y que quería hacer hace mucho tiempo, es mostrar un menú con la tabla de contenidos de cada post (títulos y subtítulos) al lado del texto, para facilitar la navegación. Este tipo de menú aparecen por defecto en los documentos y blogs Quarto, pero no en los de Hugo. Así que tuve que hacer algunos ajustes en el tema que uso.\nMás detalles sobre cómo lo hice Tuve que cambiar la plantilla (layout) de los posts a single-sidebar en el front matter del blog (content/blog/_index.md), y luego personalizar el sidebar para que tenga etiquetas y la tabla de contenidos (que se agrega con {{ $page.TableOfContents }} en Hugo).\nEn concreto, el código que agregué al sidebar revisa que el post tenga títulos, de lo contrario no tiene sentido mostrar un índice, y si los tiene, mostrarlo con un estilo y atributos determinados. El código es:\n\u0026lt;!-- índice de la página --\u0026gt; {{ $headers := findRE \u0026#34;\u0026lt;h[2].*\u0026gt;\u0026#34; $page.Content }} {{- $has_headers := ge (len $headers) 1 -}} {{- if $has_headers -}} \u0026lt;div id=\u0026#34;PageTableOfContents\u0026#34;, style=\u0026#34;margin-top: 24px; position: sticky; top: 108px;\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;blog-info ph4 pb4 pb0-l\u0026#34;\u0026gt; \u0026lt;h3 style=\u0026#34;margin-bottom: 12px;\u0026#34;\u0026gt;Índice:\u0026lt;/h3\u0026gt; \u0026lt;div class=\u0026#34;pl2 pr0 mh0\u0026#34; style = \u0026#34;font-size: 90%; margin-top: -8px; margin-left: -22px; margin-bottom: 32px;\u0026#34;\u0026gt; {{ $page.TableOfContents }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; {{ end }} Para hacer la tabla de contenido flote mientras las personas hacen scroll en el sitio simplemente tuve que agregarle position: sticky; en el estilo CSS de la tabla.\nTambién agregué las etiquetas a esta barra lateral, para que las personas que lean puedan ir saltando a otras temáticas. Las etiquetas las tengo guardadas como un partial, así que las puedo agregar en cualquier parte del sitio con tan sólo poner {{ partial \u0026quot;shared/tags-wide.html\u0026quot; . }} o `{{ partial \u0026ldquo;shared/tags-long.html\u0026rdquo; . }}, dependiendo de si las quiero hacia el lado o hacia abajo.\nAntes de implementar ésto estaba usando un shortcode que agregaba un índice o tabla de contenidos al principio de las publicaciones, así que ahora hice que estos índices estuvieran cerrados por defecto, ya que se hicieron un poco redundantes. El shortcode para el índice es casi igual que el código del índice en la sidebar:\n\u0026lt;div style = \u0026#34;margin-left: -16px;\u0026#34;\u0026gt; \u0026lt;details {{.Get 2 | default \u0026#34;closed\u0026#34;}} id=\u0026#34;PageTableOfContents\u0026#34;\u0026gt; \u0026lt;summary\u0026gt; \u0026lt;h2 class=\u0026#34;mv0 f5 fw7 ttu tracked dib\u0026#34; style = \u0026#34;margin-left: 6px; font-size: 120%;\u0026#34;\u0026gt;Índice\u0026lt;/h2\u0026gt; \u0026lt;/summary\u0026gt; \u0026lt;div class=\u0026#34;pl2 pr0 mh0\u0026#34; style = \u0026#34;font-size: 90%; margin-top: -8px; margin-left: 16px; margin-bottom: 32px;\u0026#34;\u0026gt; {{ .Page.TableOfContents }} \u0026lt;/div\u0026gt; \u0026lt;/details\u0026gt; \u0026lt;/div\u0026gt; Detalles! ¡¿Qué fue eso?! 😟 Un nuevo shortcode llamado detalles! Así puedo agregar secciones de contenido que están ocultas por defecto, y que se pueden expandir o contraer al hacer clic en un botón. Así no es necesario marearlos con cosas siempre, y puedo esconder las cosas menos interesantes!\nDetalles sobre los detalles El shortcode es demasiado sencillo:\n\u0026lt;details\u0026gt; \u0026lt;summary\u0026gt; {{ (.Get 0 | default \u0026#34;**Ver código**\u0026#34;) | markdownify }} \u0026lt;/summary\u0026gt; \u0026lt;p\u0026gt; {{ .Inner | markdownify }} \u0026lt;/p\u0026gt; \u0026lt;/details\u0026gt; Solamente ese código guardado en layouts/shortcodes/detalles.html, y se usa poniendo el shortcode de apertura y después uno de cierre con un / antes de la palabra detalles.\nMejoras al buscador También mejoré el buscador de mi blog, que es una aplicación Shiny, para que sea un poquito más rápido, pero principalmente para que los resultados de búsqueda contengan un resumen de cada publicación. Más información sobre el buscador en este post.\nMás detalles sobre cómo lo hice Para esto, tuve que cambiar un poco la configuración de Hugo para que genere una versión del blog en JSON, agregándole que también incluya los textos de resumen o excerpt.\nSiguiendo las instrucciones que di antes, en el archivo layouts/index.json hay que agregarle \u0026quot;excerpt\u0026quot; (default $page.Summary $page.Params.excerpt) para que registre ese atributo de cada post en el archivo JSON, que se regenera con cada compilación (build) del sitio.\nLuego simplemente actualizar la app para que agregue ese texto, previamente interpretado como markdown con la función shiny::markdown().\nBuscador Nuevo sitio: Aprende R Como ya se estaban acumulando muchos tutoriales sobre R en este blog, quise hacer un sitio nuevo donde pudiera organizar todo, para que cualquier persona entre a este sitio y encuentre todo lo necesario para aprender R, ya sean contenidos hechos por mí o por otras personas. Más información sobre el sitio Aprende R en este post.\nAprende R Nueva etiqueta \u0026ldquo;básico\u0026rdquo; Siguiendo la idea de organizar el contenido para principiantes, agregué una nueva etiqueta a las publicaciones sobre contenido básico de R.\nMás morado! Otro cambio que tenía pendiente desde el inicio de este blog era usar un tema para el código (syntax highlighting) que combinara mejor con los colores del blog. Por fin me dediqué a hacerlo, y fue más fácil de lo que yo pensaba. Ahora el tema del código en este blog y el tema morado que uso en RStudio son iguales 💜\n\u0026#34;miren lo\u0026#34; |\u0026gt; hermoso() # que quedó list(TRUE, \u0026#34;maravilloso\u0026#34;, 100, 🔥) \u0026#34;el\u0026#34;; tema() -\u0026gt; morado !! Detalles sobre cómo lo hice El tema del código del blog Hugo se puede cambiar en config.toml, pero las opciones de colores son limitadas.\nPara poder crear tu propio tema de syntax highlighting primero tienes que cambiar la opción para que cada elemento de tu código use una clase CSS específica:\n[markup] defaultMarkdownHandler = \u0026#34;goldmark\u0026#34; [markup.highlight] style = \u0026#34;rose-pine-moon\u0026#34; noClasses = false Al especificar noClasses = false, el texto de los bloques de código pasa a tener clases CSS distintas para cada elemento del código (comentarios, cadenas de texto, palabras reservadas, etc.).\nLuego, tienes que crear un archivo CSS donde definas los colores que quieres para cada clase, a partir de uno de los temas existentes, ejecutando el siguiente código en la Terminal:\nhugo gen chromastyles --style=rose-pine-moon \u0026gt; syntax.css Con esto se crea el archivo syntax.css, que puedes editar para cambiar los colores. Para saber la clase de cada elemento del código, usas el inspector del navegador:\nFinalmente tienes que poner el archivo CSS en la carpeta static/css/syntax.css, y cargarlo en todas las páginas del blog agregando \u0026lt;link rel=\u0026quot;stylesheet\u0026quot; href=\u0026quot;/css/syntax.css\u0026quot;\u0026gt; al archivo layouts/partials/head.html (si no lo tienes, lo copias desde la carpeta del tema).\nMe hace muy feliz que todo se vea tan bonito, y si quieren su RStudio también puede verse así de cute 💕\nTema morado para RStudio Eso por ahora! Recuerda que siempre puedes escribirme por LinkedIn, Twitter a @bastimapache, o por correo.\n","date":"2025-12-03T00:00:00Z","excerpt":"He estado retocando algunos aspectos de este blog. Quería contarles los principales cambios: tablas de contenido en todas las publicaciones, nuevos _shortcodes_, nuevo tema de colores para bloques de código, resultados de búsqueda con resúmenes de posts, y más.","href":"https://bastianoleah.netlify.app/blog/2025-12-02/","tags":"blog","title":"Actualización del blog: menús, mejoras, y más morado"},{"content":" Tema morado oscuro Tema para RStudio enfocado en una paleta de colores morada y rosada, basado en el tema base16 Default Dark de {rsthemes}, el cual a su vez está basado en base16.\nTema claro Variación del tema Tomorrow de RStudio, con una paleta morada y rosada para complementar con el tema morado oscuro.\nDescarga Tema oscuro: clic derecho aquí y elige Descargar archivo enlazado Tema claro: clic derecho aquí y elige Descargar archivo enlazado O bien, entra al repositorio y descarga el archivo .rstheme.\nInstalación Para agregar el tema a RStudio, descarga el archivo .rstheme desde el repositorio, luego abre las opciones globales de RStudio Global Options (⌘;), entra al menú Appareance, presiona Add\u0026hellip; en la parte de abajo, y elige el archivo .rstheme que quieras agregar. Luego puedes aplicar el tema desde ese mismo panel.\nBonus Si quieres configurar RStudio para que el tema cambie entre claro y oscuro de forma automática dependiendo de la hora del día, sigue las instrucciones en este post!\nActualizaciones 2025/12/03: Ahora más morado! 💜 y más rosado! 🩷 Celebrando la actualización del tema para el código de este blog. ","date":"2025-12-03T00:00:00Z","excerpt":"Agrégale moradito a tu análisis de datos! Tema para RStudio enfocado en una paleta de colores morada y rosada, basado en el tema _base16 Default Dark_ de [`{rsthemes}`](https://github.com/gadenbuie/rsthemes?tab=readme-ov-file), el cual a su vez se basa en [base16](https://github.com/chriskempson/base16).","href":"https://bastianoleah.netlify.app/blog/tema_morado/","tags":"curiosidades","title":"Temas morados oscuro y claro para RStudio"},{"content":"Calcular estadísticos descriptivos en R es tan simple como usar la función summary() sobre cualquier tabla de datos o dataframe.\nÍndice Estadísticos descriptivos iniciales Estadísticos descriptivos con {dplyr} Calcular promedio con summarize() Calcular promedio de varias variables con summarize(across()) Estadísticos descriptivos con summarize() Estadísticos descriptivos por grupo Estadísticos descriptivos para todas las variables Estadísticos descriptivos para todas las variables, por grupos Estadísticos descriptivos con {skimr} Estadísticos descriptivos iniciales Usaremos como ejemplo el conjunto de datos iris, que viene incorporado en R, para aplicarle summary() y así obtener sus estadísticos descriptivos.\nsummary(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 La función summary() calcula estadísticos descriptivos básicos para cada variable numérica del conjunto de datos, incluyendo:\nMínimo y máximo Mediana: el valor central, si los datos se ordenaran de menor a mayor Promedio Primer y tercer cuartil: si se ordenan los datos de menor a mayor, el primer cuartil indica el valor que corta la distrución de manera que el 25% de los datos son menores o iguales a él, y el tercer cuartil lo mismo pero cortando el 75% de los datos son menores o iguales a él. Estadísticos descriptivos con {dplyr} Si queremos tener más control sobre los cálculos de estadísticos descriptivos, podemos usar {dplyr}.\nVamos a partir viendo cómo se hace un sólo estadístico descriptivo promedio para una sola variable, y vamos a ir avanzando desde ahí hasta poder aplicar múltiples estadísticos descriptivos a todas las variables.\nCalcular promedio con summarize() Si bien {dplyr} no tiene una función que lo haga automáticamente como summary(), podemos usar la función summarize() para calcular resúmenes estadísticos. A summarize() le damos una función que usaremos para resumir los datos a una sola fila.\nPor ejemplo, calculemos el promedio de una variable:\nlibrary(dplyr) iris |\u0026gt; summarize( promedio = mean(Sepal.Length) ) promedio 1 5.843333 Calcular promedio de varias variables con summarize(across()) Para hacerlo más útil, en vez de calcular para una sola variable, podemos pedirle que calcule el promedio para todas las variables numéricas. Esto lo logramos usando across(), que permite aplicar las operaciones a varias columnas a la vez, indicando que queremos aplicar la operación donde (where()) las variables sean numéricas (is.numeric).\nEntonces, este código calculará el promedio para todas las columnas numéricas del dataframe:\niris |\u0026gt; summarize( # resumir los datos across( # donde las columnas where(is.numeric), # sean numéricas ~mean(.x), # calculando el promedio .names = \u0026#34;{col}_promedio\u0026#34;) # cambiar el nombre de columnas ) Sepal.Length_promedio Sepal.Width_promedio Petal.Length_promedio 1 5.843333 3.057333 3.758 Petal.Width_promedio 1 1.199333 Estadísticos descriptivos con summarize() Siguiendo los mismos principios, ahora podemos calcular varios estadísticos descriptivos a la vez agregándolos a summarize().\nPrimero calculemos los descriptivos para una sola variable:\niris |\u0026gt; # definir variable rename(var = Petal.Width) |\u0026gt; # omitir datos perdidos filter(!is.na(var)) |\u0026gt; # calcular summarize( \u0026#34;minimo\u0026#34; = min(var), \u0026#34;primer\u0026#34; = quantile(var, probs = 0.25), \u0026#34;promedio\u0026#34; = mean(var), \u0026#34;mediana\u0026#34; = median(var), \u0026#34;tercer\u0026#34; = quantile(var, probs = 0.75), \u0026#34;maximo\u0026#34; = max(var), \u0026#34;desviacion\u0026#34; = sd(var) ) minimo primer promedio mediana tercer maximo desviacion 1 0.1 0.3 1.199333 1.3 1.8 2.5 0.7622377 Estadísticos descriptivos por grupo La gracia de {dplyr} es que al código anterior podemos agregarle una agrupación (group_by()) para calcular los mismos estadísticos descriptivos por grupos:\niris |\u0026gt; # definir variable rename(var = Petal.Width) |\u0026gt; # omitir datos perdidos filter(!is.na(var)) |\u0026gt; # grupo group_by(Species) |\u0026gt; # calcular summarize( \u0026#34;minimo\u0026#34; = min(var), \u0026#34;primer\u0026#34; = quantile(var, probs = 0.25), \u0026#34;promedio\u0026#34; = mean(var), \u0026#34;mediana\u0026#34; = median(var), \u0026#34;tercer\u0026#34; = quantile(var, probs = 0.75), \u0026#34;maximo\u0026#34; = max(var), \u0026#34;desviacion\u0026#34; = sd(var) ) # A tibble: 3 × 8 Species minimo primer promedio mediana tercer maximo desviacion \u0026lt;fct\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 setosa 0.1 0.2 0.246 0.2 0.3 0.6 0.105 2 versicolor 1 1.2 1.33 1.3 1.5 1.8 0.198 3 virginica 1.4 1.8 2.03 2 2.3 2.5 0.275 Estadísticos descriptivos para todas las variables Podemos automatizar el cálculo de todos los estadísticos descriptivos para todas las variables numéricas usando across() dentro de summarize(). El resultado lo podemos transformar a formato largo con {tidyr} para facilitar su lectura:\nlibrary(tidyr) iris |\u0026gt; summarise( across(where(is.numeric), list(\u0026#34;minimo\u0026#34; = ~min(.x), \u0026#34;primer\u0026#34; = ~quantile(.x, probs = 0.25), \u0026#34;promedio\u0026#34; = ~mean(.x), \u0026#34;mediana\u0026#34; = ~median(.x), \u0026#34;tercer\u0026#34; = ~quantile(.x, probs = 0.75), \u0026#34;maximo\u0026#34; = ~max(.x), \u0026#34;desviacion\u0026#34; = ~sd(.x)) ) ) |\u0026gt; pivot_longer(everything(), names_sep = \u0026#34;_\u0026#34;, names_to = c(\u0026#34;variable\u0026#34;, \u0026#34;.value\u0026#34;)) # A tibble: 4 × 8 variable minimo primer promedio mediana tercer maximo desviacion \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Sepal.Length 4.3 5.1 5.84 5.8 6.4 7.9 0.828 2 Sepal.Width 2 2.8 3.06 3 3.3 4.4 0.436 3 Petal.Length 1 1.6 3.76 4.35 5.1 6.9 1.77 4 Petal.Width 0.1 0.3 1.20 1.3 1.8 2.5 0.762 Estadísticos descriptivos para todas las variables, por grupos Finalmente, el mismo proceso anterior de calcular todos los descriptivos para todas las variables, pero ahora por grupos, simplemente agregándole group_by() al código y cambiando las columnas a pivotar para que no incluya la variable de grupo:\niris |\u0026gt; rename(grupo = Species) |\u0026gt; group_by(grupo) |\u0026gt; summarise( across(where(is.numeric), list(\u0026#34;minimo\u0026#34; = ~min(.x), \u0026#34;primer\u0026#34; = ~quantile(.x, probs = 0.25), \u0026#34;promedio\u0026#34; = ~mean(.x), \u0026#34;mediana\u0026#34; = ~median(.x), \u0026#34;tercer\u0026#34; = ~quantile(.x, probs = 0.75), \u0026#34;maximo\u0026#34; = ~max(.x), \u0026#34;desviacion\u0026#34; = ~sd(.x)) ) ) |\u0026gt; pivot_longer(c(everything(), -grupo), # todas menos la variable de agrupación names_sep = \u0026#34;_\u0026#34;, names_to = c(\u0026#34;variable\u0026#34;, \u0026#34;.value\u0026#34;)) # A tibble: 12 × 9 grupo variable minimo primer promedio mediana tercer maximo desviacion \u0026lt;fct\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 setosa Sepal.Len… 4.3 4.8 5.01 5 5.2 5.8 0.352 2 setosa Sepal.Wid… 2.3 3.2 3.43 3.4 3.68 4.4 0.379 3 setosa Petal.Len… 1 1.4 1.46 1.5 1.58 1.9 0.174 4 setosa Petal.Wid… 0.1 0.2 0.246 0.2 0.3 0.6 0.105 5 versicolor Sepal.Len… 4.9 5.6 5.94 5.9 6.3 7 0.516 6 versicolor Sepal.Wid… 2 2.52 2.77 2.8 3 3.4 0.314 7 versicolor Petal.Len… 3 4 4.26 4.35 4.6 5.1 0.470 8 versicolor Petal.Wid… 1 1.2 1.33 1.3 1.5 1.8 0.198 9 virginica Sepal.Len… 4.9 6.22 6.59 6.5 6.9 7.9 0.636 10 virginica Sepal.Wid… 2.2 2.8 2.97 3 3.18 3.8 0.322 11 virginica Petal.Len… 4.5 5.1 5.55 5.55 5.88 6.9 0.552 12 virginica Petal.Wid… 1.4 1.8 2.03 2 2.3 2.5 0.275 Si bien R base trae herramientas útiles como summary(), metiéndole un poco de {dplyr} podemos tener formas más flexibles y personalizables para calcular estadísticos descriptivos.\nEstadísticos descriptivos con {skimr} Otra alternativa a summary() es la función skim() del paquete {skimr}, que nos entrega resúmenes estadísticos permite obtener un resumen estadístico más completo y visualmente atractivo.\nInstalamos el paquete:\ninstall.packages(\u0026#34;skimr\u0026#34;) Luego lo probamos con nuestros datos:\nlibrary(skimr) skim(iris) La función entregará tablas con resúmenes de cantidad de observaciones, tipo de las columnas, y estadísticas descriptivas específicas para variables categóricas y numéricas.\n── Data Summary ──────────────────────── Values Name iris Number of rows 150 Number of columns 5 _______________________ Column type frequency: factor 1 numeric 4 ________________________ Group variables None ── Variable type: factor ─────────────────────────────────────────────────────────────────────────── skim_variable n_missing complete_rate ordered n_unique top_counts 1 Species 0 1 FALSE 3 set: 50, ver: 50, vir: 50 ── Variable type: numeric ────────────────────────────────────────────────────────────────────────── skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist 1 Sepal.Length 0 1 5.84 0.828 4.3 5.1 5.8 6.4 7.9 ▆▇▇▅▂ 2 Sepal.Width 0 1 3.06 0.436 2 2.8 3 3.3 4.4 ▁▆▇▂▁ 3 Petal.Length 0 1 3.76 1.77 1 1.6 4.35 5.1 6.9 ▇▁▆▇▂ 4 Petal.Width 0 1 1.20 0.762 0.1 0.3 1.3 1.8 2.5 ▇▁▇▅▃ Gracias a Darakhshan Nehal por enseñarme esta función! ","date":"2025-12-02T00:00:00Z","excerpt":"Calcular estadísticos descriptivos, como promedio, mediana y cuartiles, es un paso inicial de la exploración de datos. En este post aprendemos varias formas de calcular descriptivos personalizables a todas las variables de tu _dataframe_ al mismo tiempo.","href":"https://bastianoleah.netlify.app/blog/estadisticos_descriptivos/","tags":"estadísticas ; básico ; dplyr","title":"Calcula estadísticos descriptivos básicos en R"},{"content":"Las tablas de datos o dataframes son la estructura de información principal que usamos en R.\nEn general cargamos los datos desde archivos o bases de datos, pero a veces necesitamos crear dataframes sencillos a mano, ya sea para introducir datos manualmente, corregir datos, o crear pequeñas tablas auxiliares o de consulta (lookup tables).\nAquí te muestro dos formas de hacerlo: usando la función base data.frame() y la función tribble() del paquete tibble.\nCrear tablas de datos con data.frame() La función data.frame() es la forma base de R para crear dataframes (tablas de datos). Puedes usarla para combinar vectores en columnas.\nanimal \u0026lt;- c(\u0026#34;mapache\u0026#34;, \u0026#34;gato\u0026#34;, \u0026#34;gallina\u0026#34;) patas \u0026lt;- c(4, 4, 2) color \u0026lt;- c(\u0026#34;gris\u0026#34;, \u0026#34;negro\u0026#34;, \u0026#34;blanco\u0026#34;) data.frame(animal, patas, color) Así vas creando cada columna como un vector, y luego combinas los vectores para crear una tabla.\nEl resultado es un objeto tipo data.frame, que es una tabla más tosca y primitiva que un tibble, pero siempre puedes convertir cualquier dataframe a un tibble con la función tibble().\nCrear tablas de datos con tribble() La función tribble() del paquete tibble (parte del tidyverse) permite crear dataframes escribiéndolos como si fueran una planilla: por columnas y filas. Es lo más parecido a abrir Excel y escribir los datos, pero saltándote la parte de abrir Excel 🤢\ntibble::tribble( ~animal, ~patas, ~color, \u0026#34;mapache\u0026#34;, 4, \u0026#34;gris\u0026#34;, \u0026#34;gato\u0026#34;, 4, \u0026#34;negro\u0026#34;, \u0026#34;gallina\u0026#34;, 2, \u0026#34;blanco\u0026#34;) Así vas escribiendo los datos igual como si fuese una planilla, lo que puede ser más intuitivo, y el resultado sale como un tibble, que es una versión mejorada de los dataframes base de R.\nTe dejo otro post para donde puedes ver cómo convertir un dataframe a código para poder compartirlo o prescindir de archivos, cómo copiar datos desde R y pegarlos en una planilla, y cómo copiar datos desde planilla Excel y pegarla como código que genere el dataframe.\nEste post nace de la idea compartida por Les Flores en Twitter!\nVer tuit original ","date":"2025-12-02T00:00:00Z","excerpt":"Las tablas de datos o _dataframes_ son la estructura de información principal que usamos en R. En este post veremos cómo crear _dataframes_ sencillos a mano de dos formas: usando la función base `data.frame()` y la función `tribble()`.","href":"https://bastianoleah.netlify.app/blog/crear_dataframes/","tags":"consejos ; datos ; básico","title":"Crear tablas de datos manualmente en R"},{"content":" Estoy haciendo hace varios días un sitio llamado Aprende R. La idea es agrupar en un sólo lugar todos los posts y tutoriales orientados a principiantes que tengo, y así poder dirigir a la gente que quiere aprender a un sitio donde tendrán todo lo necesario (o al menos eso espero).\nEl objetivo es poder entregar una guía clara para el aprendizaje autodidacta de R para el análisis de datos, y lo pienso también como algo que me puede motivar más a seguir escribiendo tutoriales y mejorando los que ya tengo. Por ejemplo, me di cuenta que me faltaba explicar mejor el uso de conectores o pipes en R, así que hice un post más completo para ponerlo en Aprender R.\nTambién me sirve para juntar ahí mismo enlaces útiles para el aprendizaje, y así destacarlos entre el mar de enlaces sobre R que tengo en la página de recursos de R. Por ejemplo, destacar libros clave, paquetes principales, y más.\nAdemás, esto me permite mezclar mis posts y tutoriales con los de otras personas, porque la idea no es promocionar mi contenido, sino ayudar a aprender!\nEspero que les sirva, y si falta o sobra algo, déjamelo en los comentarios o puedes contactarme.\n","date":"2025-12-01T00:00:00Z","excerpt":"Estoy haciendo hace varios días un sitio llamado Aprende R. La idea es agrupar en un sólo lugar todos los posts y tutoriales orientados a principiantes que tengo. Quiero que sea una guía clara para el aprendizaje autodidacta de R para el análisis de datos, y lo pienso también como algo que me puede motivar más a seguir escribiendo tutoriales y mejorando los que ya tengo.","href":"https://bastianoleah.netlify.app/blog/aprender_r/","tags":"consejos ; básico","title":"Nuevo sitio con todo lo necesario para aprender R"},{"content":"Me gustaría que existiera un grupo donde gente que use o aprenda R, ya sea en espacios profesionales, académicos o como hobby, puedan conversar, mostrar lo que hacen, compartir aprendizajes y darnos retroalimentación.\nAsí que estoy viendo si existe interés para organizar un grupo de usuarios/as de R en Santiago de Chile.\nSi te interesaría compartir cada cierto tiempo con otros usuari@s de R, de forma relajada, amistosa e inclusiva, o bien ayudar a la coordinación del grupo, por favor completa esta breve encuesta para ver si resulta:\nResponde la encuesta aquí! ","date":"2025-11-27T00:00:00Z","excerpt":"\u003cp\u003eMe gustaría que existiera un grupo donde gente que use o aprenda R, ya sea en espacios profesionales, académicos o como hobby, puedan conversar, mostrar lo que hacen, compartir aprendizajes y darnos retroalimentación.\u003c/p\u003e\n\u003cp\u003eAsí que estoy viendo si existe interés para organizar un \u003cstrong\u003egrupo de usuarios/as de R\u003c/strong\u003e en Santiago de Chile.\u003c/p\u003e\n\u003cp\u003eSi te interesaría \u003cstrong\u003ecompartir cada cierto tiempo\u003c/strong\u003e con otros usuari@s de R, de forma relajada, amistosa e inclusiva, o bien ayudar a la \u003cstrong\u003ecoordinación\u003c/strong\u003e del grupo, por favor completa esta breve encuesta para ver si resulta:\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/grupo_usuarixs_r/","tags":"blog ; Chile","title":"Grupo de usuarios/as de R"},{"content":"Luego de hacer el post de hacer que RStudio haga cosas al abrirse, se me ocurrió la idea de hacer que muestre imágenes divertidas al azar.\nAsí que hice un pequeño script que muestra fotos al azar de gatos desde la API Cats As A Service, o bien imágenes de bendiciones de Piolín que busqué en internet 💕\nLas fotos aparecen al azar al abrir RStudio en el panel Viewer:\nPara lograrlo, hay que editar el archivo .RProfile, que es un script de R que se ejecuta automáticamente al abrirlo.\nPara editar el .RProfile, ejecuta:\nusethis::edit_r_profile() Se abrirá un script, y dentro debes pegar lo siguiente:\nsetHook(\u0026#34;rstudio.sessionInit\u0026#34;, function(newSession) { if (newSession) { # opciones saludito = \u0026#34;gatos\u0026#34; # gatos o piolín tamaño = \u0026#34;70%\u0026#34; fondo = \u0026#34;#181818\u0026#34; # escoger imagen if (saludito == \u0026#34;gatos\u0026#34;) { imagen \u0026lt;- \u0026#34;https://cataas.com/cat\u0026#34; } else if (saludito == \u0026#34;piolín\u0026#34;) { imagen \u0026lt;- paste0(\u0026#34;https://raw.githubusercontent.com/bastianolea/piolines/master/img/piolin_\u0026#34;, 1:20, \u0026#34;.jpg\u0026#34;) |\u0026gt; sample(1) } # crear página en html library(shiny) imagen \u0026lt;- tags$body( style = paste(\u0026#34;background-color:\u0026#34;, fondo, \u0026#34;; display: flex; justify-content: center;\u0026#34;), img(src = imagen, style = paste(\u0026#34;max-height:\u0026#34;, tamaño, \u0026#34;; margin: auto;\u0026#34;))) temporal \u0026lt;- tempfile(fileext = \u0026#34;.html\u0026#34;) # crear archivo temporal writeLines(text = as.character(imagen), temporal) # guardar la página al archivo temporal rstudioapi::viewer(temporal) # ver archivo temporal en Rstudio rm(temporal, imagen, fondo, tamaño, saludito) # limpieza } }, action = \u0026#34;append\u0026#34;) Ahora si cierras y vuelves a abrir RStudio aparecerá una foto al azar!\nEn la línea donde se define saludito puedes elegir entre \u0026quot;gatos\u0026quot; o \u0026quot;piolín\u0026quot; para elegir qué tipo de imagen quieres que aparezca al abrir RStudio! ✨\nOriginalmente, cuando estaba probando la idea, hice que solo se mostrara una imagen en el panel Viewer, pero salía con fondo blanco. Entonces hice que la imagen tuviera un fondo oscuro usando {magick} (nunca lo había usado), que permite editar imágenes desde R. Lo que hice fue crear una imagen negra del mismo tamaño que el panel Viewer de RStudio (lo que se puede obtener con la función dev.size(\u0026quot;px\u0026quot;), y sobre esa imagen ponía una de las imágenes al azar, reescaladas y al centro, y mostraba eso. Pero después se me ocurrió que era mucho mejor mostrar un HTML con fondo negro y la foto centrada.\nEl código de lo que intenté con ImageMagick queda aquí para la posteridad:\n# install.packages(\u0026#34;magick\u0026#34;) library(magick) fondo = \u0026#34;#181818\u0026#34; # color del fondo porcentaje = 0.4 # tamaño de la imagen # obtener un gatito gatito \u0026lt;- \u0026#34;https://cataas.com/cat\u0026#34; # obtener tamaño del panel tamaño \u0026lt;- dev.size(\u0026#34;px\u0026#34;)/2 # achicar imagen imagen \u0026lt;- image_read(gatito) |\u0026gt; image_resize(tamaño*porcentaje) # crear fondo fondo \u0026lt;- image_blank(width = tamaño[1], height = tamaño[2], color = fondo) # unir imagen y fondo salida \u0026lt;- image_composite(fondo, imagen, gravity = \u0026#34;center\u0026#34;) print(salida, info = FALSE) Igual es interesante saber que se puede mostrar cualquier contenido en HTML en el panel Viewer con el siguiente código:\n# código html contenido \u0026lt;- tags$body( style = \u0026#34;background-color: #181818; display: flex; justify-content: center;\u0026#34;, p(\u0026#34;Hola\u0026#34;, style = \u0026#34;color: #DDD5DE; font-size: 48px; font-family: \u0026#39;Arial\u0026#39;;\u0026#34;) ) # crear archivo temporal temporal \u0026lt;- tempfile(fileext = \u0026#34;.html\u0026#34;) # escribir la página al archivo temporal writeLines(text = as.character(contenido), temporal) # ver en Rstudio rstudioapi::viewer(temporal) ","date":"2025-11-27T00:00:00Z","excerpt":"Para hacer más ameno tu trabajo en R, qué tal si haces que te aparezcan fotos de gatos como bienvenida? Sigue estas instrucciones para darle un toque divertido a tu RStudio.","href":"https://bastianoleah.netlify.app/blog/2025-11-27/","tags":"curiosidades","title":"Recibe gatos o bendiciones automáticamente al abrir RStudio"},{"content":"Con el paquete {surveydown} podemos crear encuestas desde R gratis, cuyas respuestas se almacenan en una base de datos también gratuita.\nLa gracia de crear tu propia encuesta es que puedes personalizarla según tus necesidades, pero también es que no necesitas depender de un servicio para generar la encuesta, ni tampoco tener que pagar para poder crearla o publicarla. Por el contrario, podemos utilizar tecnologías gratuitas y de código abierto para crear nuestras encuestas, almacenar sus resultados y analizarlos.\nEjemplo de una encuesta El paquete {surveydown} combina el uso de Quarto y Shiny para crear encuestas fáciles de diseñar, y con altas capacidades de personalización. Aquí te dejo una encuesta de prueba para que veas cómo son!\nEl diseño de la encuesta, con todas sus preguntas, títulos, textos, páginas y botones, se hace por medio de un documento Quarto, donde literalmente vas poniendo todo el contenido que quieres que tenga tu encuesta, especificando los saltos de página de la encuesta, y listo.\nEl funcionamiento interno de la encuesta es resuelto por {surveydown}, y no tenemos que realizar nada de configuración ni programación por nuestro lado. Todo funciona de forma inmediata (la encuesta, la interacción del usuario y la base de datos) por medio de una aplicación Shiny. Lo único que tenemos que proveer es la conexión a la base de datos donde se van a ir guardando las respuestas, y de la cual también vamos a poder obtener los resultados cuando queramos.\nAquí les dejo una encuesta de prueba, hecha siguiendo este tutorial, para ver quién gana: ¿gatos 🐈 o perros 🐕? Responde aquí Índice Instalar el paquete Crear una encuesta Diseñar la encuesta Crear preguntas Navegación Probar la encuesta Configurar la base de datos Probar la encuesta Subir tu encuesta y compartirla Obtener resultados de la encuesta Conclusión Instalar el paquete Instalamos la versión más reciente:\n# install.packages(\u0026#34;pak\u0026#34;) pak::pak(\u0026#34;surveydown-dev/surveydown\u0026#34;) Crear una encuesta Para crear nuestra encuesta, creamos un nuevo proyecto de RStudio para la encuesta, y usamos la función surveydown::sd_create_survey() para crear una plantilla de encuesta lista para editar. Sólo una encuesta por proyecto!\nEn la función sd_create_survey() puedes elegir plantillas, como sd_create_survey(template = \u0026quot;question_types\u0026quot;) para obtener una encuesta con varios tipos de preguntas distintos para partir.\nCuando creamos nuestra primera encuesta, obtenemos un proyecto de R con dos archivos principales:\nsurvey.qmd: la encuesta, en un documento Quarto, donde diseñas todo el contenido de la encuesta, desde subtítulos, textos de introducción, títulos para las preguntas, las preguntas en sí misma, y otros textos que quieras agregar entremedio de la encuesta. app.R: la aplicación Shiny donde va a aparecer tu encuesta, y que posibilita que las respuestas vayan guardándose en una base de datos. El rol de la aplicación Shiny es hacer la gestión de que la encuesta funcione y sea interactiva, y que las respuestas que ingresamos quedan registradas en la base de datos. Pero también aquí se puede configurar lógica condicional para la encuesta, y además podemos ir mostrando resultados en tiempo real, visualizaciones de nuestros resultados que se actualizan automáticamente, y más. Diseñar la encuesta Estando en el proyecto de R de nuestra nueva encuesta, abrimos el documento Quarto survey.qmd, y en él encontraremos una encuesta que viene por defecto.\nMirando este documento vemos que las páginas de la encuesta van delimitadas por cercos :::, y que dentro de estos cercos va todo el contenido que queramos poner en cada página, como títulos, textos y preguntas.\n::: {.sd-page id=bienvenida} # Encuesta Texto de _introducción_ ::: Crear preguntas Dentro de los cercos que delimitan cada página, cada pregunta de la encuesta va en el documento como un bloque de código R, que en su interior contiene la función sd_question().\n```{r} sd_question( type = \u0026#39;mc\u0026#39;, id = \u0026#39;pinguinos\u0026#39;, label = \u0026#34;¿Qué tipo de pingüino te gusta más?\u0026#34;, option = c( \u0026#39;Adélie\u0026#39; = \u0026#39;adelie\u0026#39;, \u0026#39;Chinstrap\u0026#39; = \u0026#39;chinstrap\u0026#39;, \u0026#39;Gentoo\u0026#39; = \u0026#39;gentoo\u0026#39; ) ) ``` Con esta función se crean las preguntas de nuestra encuesta, y en sus argumentos tenemos todas las opciones para personalizarlas.\nEn el sitio de {surveydown} puedes conocer todos los tipos de preguntas, para que veas cómo se ven y cómo se crean.\nTambién existe una encuesta online de prueba que te muestra todos los tipos de preguntas en una encuesta real.\nAquí te dejo algunos ejemplos de preguntas básicas que puedes incluir en tu encuesta:\nPreguntas de selección múltiple sd_question( type = \u0026#39;mc_buttons\u0026#39;, id = \u0026#39;animal\u0026#39;, label = \u0026#34;¿Cuál es tu animal favorito?\u0026#34;, option = c( \u0026#34;Perro\u0026#34;, \u0026#34;Gato\u0026#34;, \u0026#34;Mapache\u0026#34; ) ) Pregunta de ingreso de números sd_question( type = \u0026#39;numeric\u0026#39;, id = \u0026#39;edad\u0026#39;, label = \u0026#34;¿Cuál es tu edad?\u0026#34; ) Pregunta de ingreso de texto sd_question( type = \u0026#34;text\u0026#34;, id = \u0026#34;temas\u0026#34;, label = \u0026#34;Escribe tus comentarios aquí\u0026#34;, placeholder = \u0026#34;(opcional)\u0026#34; ) Pregunta de selección múltiple con selección de más de una respuesta sd_question( type = \u0026#39;mc_multiple_buttons\u0026#39;, id = \u0026#39;genero\u0026#39;, label = \u0026#34;¿Con qué género te identificas?\u0026#34;, option = c( \u0026#34;Femenino\u0026#34;, \u0026#34;No binario/Otros\u0026#34;, \u0026#34;Masculino\u0026#34;, \u0026#34;Prefiero no responder\u0026#34; ) ) Preguntas condicionales o filtros Para ahcer preguntas condicionales o filtros, revisa aquí la página correspondiente de la documentación oficial.\nNavegación Al finalizar cada página de tu encuesta, tienes que agregar el botón de siguiente, para que los usuarios/as puedan avanzar a la siguiente página:\nsd_nav(label_next = \u0026#34;Siguiente\u0026#34;) En este botón también puedes configurar si llevar a les usuaries a otra página distinta.\nEn la página final de tu encuesta puedes poner un texto de agradecimiento y más información, y especificar el botón para terminar la encuesta:\nsd_close(label_close = \u0026#34;Terminar\u0026#34;) Probar la encuesta Si quieres probar cómo va quedando tu encuesta, puedes ejecutar la aplicación Shiny desde el archivo app.R, presionando el botón Run.\nEn este punto del desarrollo de tu encuesta, todavía no configuramos la base de datos que guardará las respuestas, así que, temporalmente, las respuestas van a quedar registradas en un archivo preview_data.csv.\nEn este repositorio te dejo el código de una encuesta básica, de tres preguntas, con gráficos que visualizan las respuestas de la encuesta al terminar de responderla. Puedes usarla como guía al momento de diseñar tu propia encuesta.\nSin embargo, para poder capturar respuestas como corresponde, tienes que configurar la base de datos primero. Veremos eso a continuación, pero si de todas maneras quieres probar tu encuesta antes de configurar la base de datos, puedes probar la aplicación Shiny ejecutándola, y las respuestas se van a guardar en un archivo local.\nConfigurar la base de datos Para que las preguntas que respondan tus usuarios/as queden registradas, es necesario especificar un método de recolección de los datos que guarde las respuestas en una base de datos centralizada.\nPero una de las limitaciones de las aplicaciones Shiny, particularmente las publicadas en shinyapps.io, es que no pueden almacenar datos persistentes, sino que los datos creados durante el uso de las apps son eliminados al terminar la sesión.\nPor lo tanto, hay que crear una base de datos donde las respuestas de cada persona sean registradas en cada interacción con la encuesta, y que luego podamos consultar para obtener los resultados.\nEsto no es tan difícil de hacer como suena. En la documentación de {surveydown} hay instrucciones detalladas para crear una base de datos para tu encuesta, y acá tengo un tutorial completo para crear bases de datos, pero acá de te dejo un resumen:\nPaso 1: crea una cuenta en Supabase Lo primero es averiguar dónde podemos crear una base de datos remota y gratuita. En la documentación de {surveydown} recomiendan usar Supabase. Supabase es un proveedor abierto y gratuito de bases de datos Postgres. Crea una cuenta ahí, y sigue las instrucciones para tener tu primer proyecto, el cual cuenta con una base de datos. En tu base de datos podrás tener tablas donde se almacenarán las respuestas de tu encuesta. En una misma base de datos pueden haber múltiples tablas, una tabla para cada encuesta distinta que crees.\nPaso dos: crear la base de datos Luego tienes que crear un proyecto. En este proyecto habrá una base de datos donde se almacenarán los datos de respuesta de tu encuesta, en una tabla específica.\nAquí lo importante es que tienes que definir una contraseña segura, que será la contraseña que uses para que tu encuesta pueda escribir sus resultados en la base, y también para que tú puedas obtener los resultados desde la base.\nPaso tres: obtener parámetros de la base de datos Luego de crear la base, necesitas obtener los parámetros de conexión para poder hacer la conexión entre tu encuesta y la base.\nAl entrar a tu proyecto, arriba presiona el botón Connect. Se abrirá un panel donde se nos entregarán los parámetros de acceso a la base de datos.\nImportante: en Method que elegir Transaction Pooler.\nTen a la vista estos parámetros para ingresarlos en el siguiente paso!\nPaso cuatro: guardar los parámetros de la base de datos Ahora que tienes los parámetros de conexión, ejecuta el siguiente comando:\nsd_db_config() Este comando irá pidiéndote uno por uno los parámetros de conexión que tienes en Supabase, incluyendo la contraseña que ingresaste al momento de crear el proyecto.\nCuando te pregunte por el nombre de la tabla, puedes poner el que tú quieras. La tabla se va a crear solita en la base de datos. Si luego creas una nueva encuesta, y pones un nombre de tabla distinto, las respuestas de esta nueva encuesta aparecerá en una tabla separada. Basta con especificar el nombre de una tabla nueva para que la tabla nueva se cree, no es necesario crearla antes en otro lugar.\nEstas credenciales se guardarán en un archivo oculto .env dentro de tu proyecto, y que será leído por la encuesta para poder conectarse a la base de datos.\nPaso cinco: configurar la base de datos en tu proyecto de R Abre app.R, y revisa que la función sd_db_connect() que está al principio del script. Por defecto dice db \u0026lt;- sd_db_connect(ignore = TRUE), para funcionar en modo de prueba sin una base de datos, pero ahora queremos que diga solamente db \u0026lt;- sd_db_connect(). De este modo, la aplicación va a buscar el archivo .env que creamos en el paso anterior y se va a conectar a la base de datos.\n¡Y listo! Ahora cada respuesta que se marque en tu encuesta quedará registrada automáticamente en la base de datos.\nProbar la encuesta Para ejecutar tu encuesta, tienes que abrir el script de la aplicación Shiny, app.R, y ejecutar la app con el botón Run. Tu encuesta se abrirá una nueva ventana y podrás ponerla a prueba, o responderla.\nTodas las respuestas que ingreses quedarán registradas en la base de datos de Supabase, incluso cuando la ejecutes localmente. En el sitio web de Supabase, dentro de tu proyecto de la encuesta, puedes acceder a Table editor para revisar la tabla que contiene las respuestas, y revisar las respuestas en una planilla interactiva.\nSubir tu encuesta y compartirla El último paso es publicar tu encuesta y compartirla con otras personas para que la respondan. La forma más sencilla de hacer esto es publicar la aplicación en shinyapps.io. En este post te doy todas las instrucciones para que puedas publicar tu aplicación gratuitamente a shinyapps.io. Para publicar la encuesta como una aplicación en shinyapps.io, abre el script app.R y presiona el botón azul de Publicar (en la esquina superior derecha del panel de script), y publícala a como si de cualquier otra aplicación se tratase. Una vez publicada, tendrás un enlace que puedes compartir con otras personas.\nTodas las respuestas quedarán registradas en tu base de datos, incluso desde la aplicación publicada, porque la app contiene la autenticación que hiciste a la base de datos, así que tiene permiso para escribir en la tabla remota.\nObtener resultados de la encuesta Para obtener los resultados de tu encuesta, simplemente en un nuevo script te conectas a la base con sd_db_connect() y usas la función sd_get_data() para obtener los datos:\nlibrary(surveydown) db \u0026lt;- sd_db_connect() data \u0026lt;- sd_get_data(db) Vale decir que la obtención de los datos remotos tienes que hacerla desde el mismo proyecto de R donde creaste y configuraste las credenciales de tu base de datos.\nUna vez establecida la conexión con la base de datos, si vuelves a ejecutar la función sd_get_data() obtendrás los datos actualizados.\nConclusión Siguiendo esta instrucciones, en unos minutos podrás tener una encuesta sencilla, personalizable, y totalmente gratis, que puedes publicar online y enviarla para empezar a recolectar datos. En el proceso, también habrás aprendido a crear y conectarte a una base de datos SQL, una herramienta crucial y que abre muchas posibilidades en el mundo del análisis de datos y el desarrollo de aplicaciones Shiny.\nPersonalmente, creé una encuesta de evaluación anónima para las alumnas y alumnos de mis cursos de R, y otra encuesta de bienvenida a nuevos alumnxs de mis cursos, donde rellenaron sus datos de caracterización, respondieron sobre sus conocimientos previos de R y sus expectativas del curso, y al final de la encuesta los resultados se visualizaban en tiempo real, actualizándose cada cinco segundos, y así todas y todos podíamos ver gráficos que describían a los participantes del curso a medida que respondían! 😍\nGráficos al final de la encuesta, actualizados en tiempo real\nEn este repositorio te dejo el código de una encuesta que también muestra gráficos en tiempo real con las respuestas al finalizar, y en este enlace puedes ver la misma encuesta en funcionamiento y responderla.\nSi este tutorial te sirvió, por favor considera hacerme una pequeña donación para poder tomarme un cafecito mientras escribo el siguiente tutorial 🥺\n","date":"2025-11-26T00:00:00Z","excerpt":"Con el paquete [`{surveydown}`](https://surveydown.org) podemos crear encuestas desde R gratis, cuyas respuestas se almacenan en una base de datos también gratuita. Así, no necesitas depender de un servicio para generar la encuesta, ni tampoco tener que pagar para poder crearla o publicarla. En este tutorial te explico cómo hacerlo paso a paso.","href":"https://bastianoleah.netlify.app/blog/surveydown_encuestas/","tags":"quarto ; shiny ; ciencias sociales ; datos","title":"Crea encuestas online gratis en R con {surveydown}"},{"content":"Se puede configurar R para que haga cosas automáticamente al iniciarse. Esto se logra creando un archivo .Rprofile, que es un script de R que se ejecuta automáticamente cuando se inicia la sesión.\nPara crear un archivo de perfil o .Rprofile, usamos la función edit_r_profile() del paquete {usethis}:\nusethis::edit_r_profile() Con esto se abrirá el .Rprofile. Este es un archivo invisible que se guarda en tu carpeta de usuario (home directory), que se va a ejecutar en todas tus sesiones de R. Pero también es posible tener otro dentro de cada proyecto de R, que sólo aplica a ese proyecto.\nAl iniciar R, R ejecutará el perfil global (en tu carpeta de usuario) y luego el perfil del proyecto (si existe). Para crear un .Rprofile que sólo aplique a un proyecto específico, hay que ejecutar usethis::edit_r_profile(scope = \u0026quot;project\u0026quot;).\nDentro del .Rprofile, cualquier código que agreguemos se ejecutará automáticamente al abrir R o el proyecto.\nSi agregamos el siguiente código, R nos dará un mensaje de bienvenida al iniciarse:\nmessage(\u0026#34;Bienvenido/a a RStudio! 💜\u0026#34;) Puedes confirmarlo reiniciando la sesión de R (menú Session, opción Restart R).\nPodemos usar esto para definir configuraciones, o ejecutar cualquier tarea que necesitemos que se realice apenas abramos R.\nPor ejemplo, podemos configurar la cantidad de decimales de los números antes de pasar a notación científica agregando:\noptions(scipen = 999) # probar: 1/900000 [1] 0.000001111111 O cambiar el lenguaje de R:\nSys.setenv(LANG = \u0026#34;es\u0026#34;) O podemos configurar globalmente el lenguaje de las fechas de R:\n# poner las fechas de R en español Sys.setlocale(\u0026#34;LC_TIME\u0026#34;, \u0026#34;es_ES.UTF-8\u0026#34;) # pedirle una fecha para confirmar que usa meses en español lubridate::today() |\u0026gt; format(\u0026#34;%d de %B\u0026#34;) [1] \u0026#34;25 de noviembre\u0026#34; Podemos hacer que R nos de un saludo personalizado al azar:\nsaludos \u0026lt;- c(\u0026#34;Holi 💕\u0026#34;, \u0026#34;Te extrañaba 🥺\u0026#34;, \u0026#34;Holi polli 🐥\u0026#34;, \u0026#34;Hoy es un buen día para la ciencia\u0026#34;, \u0026#34;Que tengas un bonito día, nerd 🤓\u0026#34;) message(sample(saludos, 1)) Holi 💕 O podemos hacer algo más útil; que R nos entregue información al iniciarse; por ejemplo, que nos diga la fecha de actualización del último archivo modificado en una carpeta:\n# obtener información del contenido de una carpeta directorio \u0026lt;- fs::dir_info(\u0026#34;datos/\u0026#34;) # obtener la fecha de la última modificación ultimo \u0026lt;- max(directorio$modification_time) # formatear fecha fecha \u0026lt;- format(ultimo, \u0026#34;%d de %B a las %H:%M\u0026#34;) message(\u0026#34;Bienvenidx! ☺️ Última actualización: \u0026#34;, fecha) Bienvenidx! ☺️ Última actualización: 25 de noviembre a las 15:10 Pero siempre tenemos que tener cuidado de no agregar al perfil código que haga que nuestros proyectos dejen de ser reproducibles! Si el proyecto depende de código que está en el perfil, alguien más que abra el proyecto no podrá correrlo correctamente.\nEn mi caso, tengo un proyecto específico donde quiero que al abrirlo se abra un script y se navegue a una carpeta del proyecto. Como el software que puede abrir scripts e interactuar con los paneles de Rstudio es RStudio (y no R) este caso, necesitamos agregar código extra para que R confirme que estamos en RStudio.\nEjecuto usethis::edit_r_profile(scope = \u0026quot;project\u0026quot;) para crear un perfil solamente par ami proyecto, y le agrego:\n# abrir script al iniciar RStudio setHook(\u0026#34;rstudio.sessionInit\u0026#34;, function(newSession) { if (newSession) # navegar a carpeta rstudioapi::filesPaneNavigate(here::here(\u0026#34;content/blog/\u0026#34;)) # abrir script principal rstudioapi::documentOpen(\u0026#34;_instrucciones.R\u0026#34;) }, action = \u0026#34;append\u0026#34;) Para probar que funciona, hay que cerrar y abrir RStudio, no sólo reiniciar R.\nDel mismo modo, en otro post del blog explico que podemos configurar RStudio para que use un tema durante el día y otro durante la noche. En este caso, usamos el paquete {rsthemes} para definir los temas claro y oscuro, y las horas del día a las que se activan:\n# especificar temas para modo claro y oscuro if (interactive() \u0026amp;\u0026amp; requireNamespace(\u0026#34;rsthemes\u0026#34;, quietly = TRUE)) { # definir temas favoritos rsthemes::set_theme_light(\u0026#34;Basti Purple Light\u0026#34;) # tema claro rsthemes::set_theme_dark(\u0026#34;Basti Purple Dark\u0026#34;) # tema oscuro # cambiar al tema dependiendo de la hora al iniciar una sesión en RStudio setHook(\u0026#34;rstudio.sessionInit\u0026#34;, function(isNewSession) { rsthemes::use_theme_auto(dark_start = \u0026#34;21:00\u0026#34;, # hora del tema oscuro dark_end = \u0026#34;6:00\u0026#34;) #hora del tema claro }, action = \u0026#34;append\u0026#34;) } También hay que poner código extra, porque es RStudio (y no R) quien define los temas de RStudio.\n","date":"2025-11-25T00:00:00Z","excerpt":"Se puede configurar R para que haga cosas automáticamente al iniciarse. Esto se logra creando un archivo `.Rprofile`, que es un script de R que se ejecuta automáticamente cuando se inicia la sesión.","href":"https://bastianoleah.netlify.app/blog/2025-11-25/","tags":"curiosidades ; automatización","title":"Configura R para que haga cosas automáticamente al abrirlo"},{"content":" ⚠️ Este tutorial se encuentra en construcción! ⚠️ R cuenta con un muy amplio ecosistema de paquetes para datos geoespaciales. Uno de los paquetes más importantes es {sf}, que permite manipular datos espaciales a partir del estándar simple features (características simples), ampliamente utilizado en sistemas de información geográfica (SIG/GIS).\nEn esta guía iré guardando los comandos que uso frecuentemente para manipular, transformar y visualizar datos geoespaciales en R. En la medida que voy aprendiendo más sobre hacer mapitas, iré actualizando y complementando.\nLo inicial es instalar {sf}:\ninstall.packages(\u0026#34;sf\u0026#34;) Y cargarlo junto a {dplyr} para empezar a trabajar con datos geoespaciales.\nlibrary(sf) library(dplyr) Nota: para simplificar, en este tutorial voy a ocultar el código de los gráficos, pero siempre estará disponible bajo flechitas que despliegan el código, como la siguiente:\nVer código para los gráficos library(ggplot2) # tema de colores thematic::thematic_on(fg = \u0026#34;#553A74\u0026#34;, bg = \u0026#34;#EAD2FA\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;) theme_set( # fondo transparente para mapas theme(plot.background = element_rect(fill = \u0026#34;transparent\u0026#34;, color = \u0026#34;transparent\u0026#34;)) + # borrar líneas feas theme(axis.ticks = element_blank()) ) Índice Carga de mapas Cargar mapas de cualquier país Cargar shapes Visualización básica de mapas Operaciones sobre geometrías Calcular centroide Extraer longitud y latitud Calcular buffer Calcular caja de un polígono Poner un punto en un mapa Crear un cuadrado Crear un polígono a partir de coordenadas Recortar un mapa Mover puntos de un mapa Recursos para aprender más Apuntes Libros Carga de mapas Una de las fuentes principales de datos geoespaciales son los shapefiles. Pero también existen paquetes de R que contienen información geoespacial, para mayor conveniencia. Exploraremos ambas opciones a continuación.\nCargar mapas de cualquier país Si no tenemos o no queremos descargar shapes, podemos cargar mapas de cualquier país directo en R gracias a {rnaturalearth}. Con este paquete obtenemos directamente mapas de cualquier país del mundo, incluyendo sus estados o regiones internos, sin necesidad de descargas.\nInstala {rnaturalearth} si no lo tienes:\ninstall.packages(\u0026#34;rnaturalearth\u0026#34;) Puedes insertar el nombre de tu país para seguir con este tutorial usando ejemplos de tu territorio.\nlibrary(rnaturalearth) pais \u0026lt;- ne_states(\u0026#34;Argentina\u0026#34;) # pais \u0026lt;- ne_states(\u0026#34;Mexico\u0026#34;) # pais \u0026lt;- ne_states(\u0026#34;Colombia\u0026#34;) mapa \u0026lt;- pais |\u0026gt; # seleccionar columnas select(pais = admin, region = name_es, geometry) mapa Simple feature collection with 24 features and 2 fields Geometry type: MULTIPOLYGON Dimension: XY Bounding box: xmin: -73.57274 ymin: -55.05202 xmax: -53.66155 ymax: -21.78694 Geodetic CRS: WGS 84 First 10 features: pais region geometry 1 Argentina Entre Ríos MULTIPOLYGON (((-58.20011 -... 12 Argentina Salta MULTIPOLYGON (((-68.49647 -... 13 Argentina Jujuy MULTIPOLYGON (((-67.25133 -... 741 Argentina Formosa MULTIPOLYGON (((-62.34136 -... 746 Argentina Misiones MULTIPOLYGON (((-54.66497 -... 749 Argentina Chaco MULTIPOLYGON (((-58.35127 -... 750 Argentina Corrientes MULTIPOLYGON (((-58.6042 -2... 1318 Argentina Catamarca MULTIPOLYGON (((-68.33776 -... 1320 Argentina La Rioja MULTIPOLYGON (((-69.65405 -... 1321 Argentina San Juan MULTIPOLYGON (((-69.95766 -... Cargar shapes Un shapefile es un formato de archivo común para datos geoespaciales, que en realidad consiste en una carpeta con archivos relacionados (.shp, .shx, .dbf, entre otros) que juntos representan la geometría y atributos de los objetos geográficos.\nTeniendo la carpeta, basta con cargarla con la función read_sf().\nEjemplo de descarga y carga de un shapefile de Chile Para aprender, podemos descargar un shape y usarlo para practicar. Si no tienes uno a mano, en el siguiente botón podemos bajar un shapefile de Chile por regiones, que proviene de la Mapoteca de la Biblioteca del Congreso Nacional.\nDivision regional: polígonos de las regiones de Chile\nTambién puedes descargarlo directamente desde R con download.file() y luego unzip(), como se indica en este post.\nUna vez descargado, descomprimimos el archivo y obtenemos una carpeta. Esta carpeta es nuestro shapefile, así que la guardamos dentro de nuestro proyecto de RStudio, idealmente dentro de una carpeta donde guardemos nuestros mapas.\n# descargar download.file(\u0026#34;https://www.bcn.cl/obtienearchivo?id=repositorio/10221/10398/2/Regiones.zip\u0026#34;, \u0026#34;Regiones.zip\u0026#34;) # descomprimir unzip(\u0026#34;Regiones.zip\u0026#34;, exdir = \u0026#34;shapes/Regiones\u0026#34;) En el siguiente ejemplo, guardamos el shapefile en una carpeta shapes, y lo cargamos con read_sf().\nmapa \u0026lt;- read_sf(\u0026#34;shapes/Regiones\u0026#34;) |\u0026gt; janitor::clean_names() mapa Simple feature collection with 17 features and 7 fields Geometry type: GEOMETRY Dimension: XY Bounding box: xmin: -12183900 ymin: -7554306 xmax: -7393644 ymax: -1978920 Projected CRS: WGS 84 / Pseudo-Mercator # A tibble: 17 × 8 objectid cir_sena codregion area_km st_area_sh st_length region * \u0026lt;dbl\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1084 1 15 16867. 18868687744. 750530. Región de Arica … 2 1085 2 1 42285. 48306372203. 1213713. Región de Tarapa… 3 1086 3 2 126071. 150845155633 2516112. Región de Antofa… 4 1087 15 12 133053. 358131609833 90498304. Región de Magall… 5 1088 14 11 106703. 224274263072 41444811. Región de Aysén … 6 1089 4 3 75661. 96439063562. 2401741. Región de Atacama 7 1090 5 4 40576. 54980818749. 2065933. Región de Coquim… 8 1091 6 5 16323. 23014748571. 1679609. Región de Valpar… 9 1092 7 13 15392. 22252038246. 1064253. Región Metropoli… 10 1093 13 10 48408. 87718341940. 7874158. Región de Los La… 11 1094 12 14 18245. 31086613540. 1844423. Región de Los Rí… 12 1095 11 9 31838. 52215073344. 1501025. Región de La Ara… 13 1096 10 8 24022. 38176117509. 2097147. Región del Bío-B… 14 1097 10 16 13104. 20376298459. 1074094. Región de Ñuble 15 1098 9 7 30322. 45969426092. 1388328. Región del Maule 16 1099 8 6 16349. 24090278437. 984853. Región del Liber… 17 1100 0 0 3937. 9306245194. 388722. Zona sin demarcar # ℹ 1 more variable: geometry \u0026lt;GEOMETRY [m]\u0026gt; Puedes seguir este tutorial independientemente del mapa que hayas cargado (el shape de Chile, con rnaturalearth, u otro shape que tú elijas); lo importante es que cargues un objeto mapa que tenga columna geometry y region. Como vemos, una vez que cargamos los datos geoespaciales obtenemos una tabla con características especiales. Arriba de la tabla vemos una descripción de las características del mapa, el tipo de geometrías, la caja o bounding box que enmarca los polígonos, y el sistema de referencia de coordenadas del mapa.\nEn {sf} los datos geoespaciales además contienen una columna geometry, que contiene las geometrías (puntos, líneas o polígonos) que definen la forma y ubicación de los elementos geográficos. Vamos a trabajar con esta columna si queremos modificar la geometría de los elementos geoespaciales o calcular algo a partir de ellos. Cada fila de la tabla representa un objeto geográfico (una región, comuna, país, etc.) y las demás columnas son variables relacionadas al objeto geográfico (población, superficie, nombre, etc.)\nVisualización básica de mapas Para visualizar un mapa con {sf} usamos la geometría geom_sf() dentro de un gráfico de {ggplot2}. Esta función reconoce automáticamente la columna geometry del objeto espacial, y dibuja las formas geográficas correspondientes.\nmapa |\u0026gt; ggplot() + geom_sf(fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#EBD2FA\u0026#34;, linewidth = 0.1) Si necesitas aprender lo básico de {ggplot2} para visualización de datos, revisa este completo tutorial. Operaciones sobre geometrías Las operaciones sobre geometrías permiten manipular y analizar las formas y ubicaciones de los objetos geográficos. A continuación veremos algunas operaciones comunes que se pueden realizar con el paquete {sf} en R.\nCalcular centroide El centroide es el punto central de un polígono. Calcularlo sirve, por ejemplo, para ubicar una etiqueta de texto sobre un polígono, poner un punto sobre un territorio que crece con respecto a una variable, etc.\nmapa_centroide \u0026lt;- mapa |\u0026gt; select(region, geometry) |\u0026gt; mutate(centroide = st_centroid(geometry)) region centroide 1 Entre Ríos POINT (-59.20493 -32.03189) 12 Salta POINT (-64.80661 -24.28824) 13 Jujuy POINT (-65.77985 -23.31783) 741 Formosa POINT (-59.94839 -24.89604) 746 Misiones POINT (-54.65212 -26.86956) 749 Chaco POINT (-60.77109 -26.39039) 750 Corrientes POINT (-57.79471 -28.76018) 1318 Catamarca POINT (-66.99006 -27.3092) 1320 La Rioja POINT (-67.21371 -29.66235) 1321 San Juan POINT (-68.88199 -30.82274) 1323 Mendoza POINT (-68.60826 -34.58944) 1328 Neuquén POINT (-70.11374 -38.62865) 1333 Chubut POINT (-68.51499 -43.78523) 1335 Río Negro POINT (-67.21765 -40.40738) 1337 Santa Cruz POINT (-69.8981 -48.76187) 1339 Tierra del Fuego POINT (-67.4401 -54.32625) 1726 Buenos Aires POINT (-60.54573 -36.66279) 1746 Buenos Aires POINT (-58.45093 -34.64272) 3852 Santa Fe POINT (-60.93513 -30.66904) 3853 Tucumán POINT (-65.36557 -26.9491) 3854 Santiago del Estero POINT (-63.2909 -27.71835) 3855 San Luis POINT (-66.03363 -33.74238) 3856 La Pampa POINT (-65.44562 -37.12691) 3857 Córdoba POINT (-63.76859 -32.07319) Ver código del gráfico mapa_centroide |\u0026gt; filter(region == mapa$region[3]) |\u0026gt; ggplot() + geom_sf(fill = \u0026#34;#9069C0\u0026#34;, linewidth = NA) + geom_sf( aes(geometry = centroide), size = 3, alpha = .6) + geom_sf_text( aes(geometry = centroide), label = \u0026#34;centroide\u0026#34;, size = 3, vjust = 2) Extraer longitud y latitud Para obtener las coordenadas (longitud y latitud) de un elemento espacial, necesitamos primero que sea un punto, porque un polígono es una figura compleja que no tiene solamente una latitud y una longitud. Si tenemos un polígono, primero calculamos el centroide, y luego extraemos las coordenadas con st_coordinates(). Como esta función retorna la longitud y latitud de cada punto al mismo tiempo, tenemos que pedirle que entregue la una o la otra usando corchetes.\nmapa_centroide_coordenadas \u0026lt;- mapa_centroide |\u0026gt; mutate(lon = st_coordinates(centroide)[,1], lat = st_coordinates(centroide)[,2]) region lon lat 1 Entre Ríos -59.20493 -32.03189 12 Salta -64.80661 -24.28824 13 Jujuy -65.77985 -23.31783 741 Formosa -59.94839 -24.89604 746 Misiones -54.65212 -26.86956 749 Chaco -60.77109 -26.39039 750 Corrientes -57.79471 -28.76018 1318 Catamarca -66.99006 -27.30920 1320 La Rioja -67.21371 -29.66235 1321 San Juan -68.88199 -30.82274 1323 Mendoza -68.60826 -34.58944 1328 Neuquén -70.11374 -38.62865 1333 Chubut -68.51499 -43.78523 1335 Río Negro -67.21765 -40.40738 1337 Santa Cruz -69.89810 -48.76187 1339 Tierra del Fuego -67.44010 -54.32625 1726 Buenos Aires -60.54573 -36.66279 1746 Buenos Aires -58.45093 -34.64272 3852 Santa Fe -60.93513 -30.66904 3853 Tucumán -65.36557 -26.94910 3854 Santiago del Estero -63.29090 -27.71835 3855 San Luis -66.03363 -33.74238 3856 La Pampa -65.44562 -37.12691 3857 Córdoba -63.76859 -32.07319 Ver código del gráfico mapa_centroide_coordenadas |\u0026gt; distinct(region, .keep_all = TRUE) |\u0026gt; mutate(region = forcats::fct_reorder(region, lat)) |\u0026gt; # ordenar regiones ggplot() + aes(lat, region) + geom_col(width = 0.7, fill = \u0026#34;#9069C0\u0026#34;) + geom_text(aes(label = region), size = 3, hjust = -0.05, color = \u0026#34;#EAD2FA\u0026#34;) + scale_y_discrete(labels = NULL) + labs(y = NULL, x = \u0026#34;Latitud\u0026#34;) Calcular buffer Un buffer es una zona alrededor de un objeto geográfico, definida por una distancia específica. Calcular un buffer es útil para analizar áreas de influencia, proximidad a ciertos puntos o regiones, o para hacer modificiones sobre mapas con fines de visualización.\nCon la función st_buffer() definimos el espacio en torno a un polígono, especificando en el argumento dist la distancia del buffer en las unidades del sistema de coordenadas del mapa (por ejemplo, metros si el mapa está en UTM), y con max_cells podemos controlar la calidad del polígono resultante.\nmapa_buffer \u0026lt;- mapa |\u0026gt; # filtrar una región/polígono filter(region == mapa$region[3]) |\u0026gt; # crear buffer mutate(buffer = st_buffer(geometry, dist = 20000, max_cells = 10000)) Ver código del gráfico mapa_buffer |\u0026gt; ggplot() + # capa de la región geom_sf(aes(geometry = geometry), fill = \u0026#34;#9069C0\u0026#34;, linewidth = NA) + # capa del buffer geom_sf(aes(geometry = buffer), fill = \u0026#34;#9069C0\u0026#34;, alpha = 0.5, linewidth = NA) Calcular caja de un polígono Con la función st_bbox() obtenemos las coordenadas de la caja que envuelve a un polígono o conjunto de polígonos. Esta caja está definida por las coordenadas mínimas y máximas en los ejes x (longitud) e y (latitud).\ncaja \u0026lt;- st_bbox(mapa) caja xmin ymin xmax ymax -73.57274 -55.05202 -53.66155 -21.78694 Convertir caja a polígono Teniendo una caja con sus coordenadas, podemos convertirla a un polígono para aplicarlo sobre (o debajo de) un mapa.\nrectangulo \u0026lt;- caja |\u0026gt; st_as_sfc(crs = st_crs(mapa)) Ver código del gráfico ggplot() + # mapa de fondo geom_sf(data = mapa, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#EBD2FB\u0026#34;, linewidth = 0.1) + # capa con la caja encima geom_sf(data = rectangulo, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#9069C0\u0026#34;, linewidth = 0.8, alpha = 0.4) + # tema theme(axis.text = element_blank(), panel.background = element_blank()) Poner un punto en un mapa Si queremos poner puntos en posiciones específicas de un mapa, sólo necesitamos crear una tabla con las coordenadas de los puntos y convertir la tabla a sf con st_as_sf(). Al hacer esto, es necesario especificar el sistema de referencia de coordenadas (CRS) del mapa, para que los puntos se ubiquen correctamente. Hacemos esto extrayendo el CRS del mapa con st_crs() y aplicándolo a nuestra nueva tabla sf.\n# definir coordenadas coordenadas \u0026lt;- tibble(longitud = -70, latitud = -38) # convertir tabla a sf punto \u0026lt;- coordenadas |\u0026gt; st_as_sf(coords = c(\u0026#34;longitud\u0026#34;, \u0026#34;latitud\u0026#34;), crs = st_crs(mapa)) Ver código del gráfico ggplot() + # mapa de fondo geom_sf(data = mapa, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#EBD2FB\u0026#34;, linewidth = 0.1) + # capa con el punto geom_sf(data = punto, size = 3, alpha = .5) Poner varios puntos en un mapa Si necesitamos agregar más de un punto, simplemente hacemos una tabla con todos los que necesitemos. Obviamente también podemos especificar otras columnas que podemos usar para etiquetar los puntos, especificar sus colores, y más.\ncoordenadas \u0026lt;- tribble(~nombre, ~lon, ~lat, ~n, \u0026#34;A\u0026#34;, -69, -38, 4, \u0026#34;B\u0026#34;, -65, -37, 6, \u0026#34;C\u0026#34;, -61, -36, 9) puntos \u0026lt;- coordenadas |\u0026gt; # convertir tabla a sf st_as_sf(coords = c(\u0026#34;lon\u0026#34;, \u0026#34;lat\u0026#34;), crs = st_crs(mapa)) Ver código del gráfico ggplot() + # mapa de fondo geom_sf(data = mapa, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#EBD2FB\u0026#34;, linewidth = 0.1) + # capa con puntos geom_sf(data = puntos, aes(size = n), alpha = .5) + # capa con textos geom_sf_label(data = puntos, aes(label = nombre), size = 3, vjust = -0.8, fontface = \u0026#34;bold\u0026#34;) + # escala de tamaño scale_size(range = c(3, 6), breaks = scales::breaks_pretty(n = 3)) + guides(size = guide_legend(override.aes = list(color = \u0026#34;#9069C0\u0026#34;, alpha = 1))) Crear un cuadrado Para poner un cuadrado encima de una ubicación del mapa, primero creamos un punto, luego lo expandimos con un buffer, calculamos la caja que envuelve a dicho punto, y finalmente convertimos la caja en un polígono sf.\ncoordenadas \u0026lt;- tibble(longitud = -58, latitud = -38) punto \u0026lt;- coordenadas |\u0026gt; # convertir tabla a sf st_as_sf(coords = c(\u0026#34;longitud\u0026#34;, \u0026#34;latitud\u0026#34;), crs = st_crs(mapa)) cuadrado \u0026lt;- punto |\u0026gt; st_buffer(dist = 100000) |\u0026gt; # agrandar punto st_bbox() |\u0026gt; # crear caja al rededor st_as_sfc(crs = st_crs(mapa)) # convertir a sf Ver código del gráfico ggplot() + # mapa de fondo geom_sf(data = mapa, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#EBD2FB\u0026#34;, linewidth = 0.1, alpha = .7) + # capa con puntos geom_sf(data = cuadrado, fill = NA, linewidth = 0.8, linejoin = \u0026#34;mitre\u0026#34;, #linejoin = \u0026#34;round\u0026#34;, color = \u0026#34;#402E5A\u0026#34;) + coord_sf(xlim = c(-72, -56), ylim = c(-40, -30)) Paso a paso, el proceso de crear el punto, agrandarlo, y formarlo en un cuadrado se vería así:\nVer código del proceso y del gráfico punto_grande \u0026lt;- punto |\u0026gt; st_buffer(dist = 100000, max_cells = 10000) cuadrado \u0026lt;- punto_grande |\u0026gt; st_bbox(crs = st_crs(mapa)) |\u0026gt; st_as_sfc() |\u0026gt; st_as_sf(crs = st_crs(mapa)) ggplot() + # mapa de fondo geom_sf(data = mapa, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#EBD2FB\u0026#34;, linewidth = 0.1) + # capas geom_sf(data = punto, color = \u0026#34;#402E5A\u0026#34;, size = 3) + geom_sf(data = punto_grande, fill = NA, color = \u0026#34;#402E5A\u0026#34;, lwd = 0.7) + geom_sf(data = cuadrado, fill = NA, color = \u0026#34;#402E5A\u0026#34;, lwd = 0.7) + # acercar mapa coord_sf(xlim = c(-61, -55), ylim = c(-40, -35.7)) Crear un rectángulo a partir de puntos Si tenemos varios puntos y necesitamos crear un rectángulo que los contenga a todos, podemos repetir lo anterior pero partiendo de varios puntos a la vez.\n# tabla con coordenadas coordenadas \u0026lt;- tribble(~nombre, ~lon, ~lat, ~n, \u0026#34;A\u0026#34;, -69, -38, 4, \u0026#34;B\u0026#34;, -65, -37, 6, \u0026#34;C\u0026#34;, -61, -36, 9) # convertir coordenadas a puntos puntos \u0026lt;- coordenadas |\u0026gt; # convertir tabla a sf st_as_sf(coords = c(\u0026#34;lon\u0026#34;, \u0026#34;lat\u0026#34;), crs = st_crs(mapa)) # convertir puntos a rectándulo rectangulo \u0026lt;- puntos |\u0026gt; st_buffer(dist = 80000) |\u0026gt; # ampliar st_bbox() |\u0026gt; st_as_sfc(crs = st_crs(mapa)) Ver código del gráfico ggplot() + # mapa de fondo geom_sf(data = mapa, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#EBD2FB\u0026#34;, linewidth = 0.2, alpha = .7) + # capa con puntos geom_sf(data = puntos, alpha = .5) + geom_sf(data = rectangulo, fill = NA, color = \u0026#34;#402E5A\u0026#34;, lwd = 0.7) Crear un polígono a partir de coordenadas Teniendo una tabla con un conjunto de coordenadas, podemos unirlas para formar un polígono. Es decir, unir los puntos para conformar una figura geométrica cerrada. Para ello, usamos st_combine() para combinar las coordenadas en un solo elemento, y luego st_cast() para convertir esos puntos combinados en un polígono.\ncoordenadas \u0026lt;- tribble(~nombre, ~lon, ~lat, \u0026#34;A\u0026#34;, -68, -36, \u0026#34;B\u0026#34;, -64, -31, \u0026#34;C\u0026#34;, -60, -36) # convertir tabla a sf puntos \u0026lt;- coordenadas |\u0026gt; st_as_sf(coords = c(\u0026#34;lon\u0026#34;, \u0026#34;lat\u0026#34;), crs = st_crs(mapa)) poligono \u0026lt;- puntos |\u0026gt; # combinar coordenadas en un solo elemento summarise(geometry = st_combine(geometry)) |\u0026gt; # convertir puntos a polígono st_cast(\u0026#34;POLYGON\u0026#34;) Ver código del gráfico ggplot() + # mapa de fondo geom_sf(data = mapa, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#E0C7F0\u0026#34;, linewidth = 0.2, alpha = 0.7) + # capa con puntos geom_sf(data = puntos, alpha = .6) + # capa con polígono geom_sf(data = poligono, fill = \u0026#34;#402E5A\u0026#34;, color = \u0026#34;#402E5A\u0026#34;, lwd = 0.7, alpha = 0.7) Crear varios polígonos a partir de coordenadas Podemos realizar el mismo proceso anterior, pero agrupando los datos con group_by() para crear varios polígonos a la vez, de acuerdo a la variable de agrupación (en este caso, tipo). Entonces, cuando usemos st_combine(), se combinarán las coordenadas de cada grupo por separado, y luego st_cast() convertirá cada grupo de puntos combinados en un polígono distinto.\ncoordenadas \u0026lt;- tribble(~tipo, ~lon, ~lat, \u0026#34;Triángulo\u0026#34;, -68, -30, \u0026#34;Triángulo\u0026#34;, -64, -25, \u0026#34;Triángulo\u0026#34;, -60, -30, \u0026#34;Cuadrado\u0026#34;, -68, -38, \u0026#34;Cuadrado\u0026#34;, -60, -38, \u0026#34;Cuadrado\u0026#34;, -60, -32, \u0026#34;Cuadrado\u0026#34;, -68, -32) # convertir tabla a sf puntos \u0026lt;- coordenadas |\u0026gt; st_as_sf(coords = c(\u0026#34;lon\u0026#34;, \u0026#34;lat\u0026#34;), crs = st_crs(mapa)) poligono \u0026lt;- puntos |\u0026gt; # combinar coordenadas en elementos únicos agrupados por una variable group_by(tipo) |\u0026gt; summarise(geometry = st_combine(geometry)) |\u0026gt; # convertir puntos a polígono st_cast(\u0026#34;POLYGON\u0026#34;) Ver código del gráfico ggplot() + # mapa de fondo geom_sf(data = mapa, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#E0C7F0\u0026#34;, linewidth = 0.2, alpha = 0.7) + # capa con polígonos de color geom_sf(data = poligono, aes(fill = tipo, color = tipo), lwd = 0.6, alpha = .7) + # capa con puntos geom_sf(data = puntos, alpha = .4) + # escala de colores labs(fill = \u0026#34;Tipo\u0026#34;, color = \u0026#34;Tipo\u0026#34;) + theme(legend.key.spacing.y = unit(1, \u0026#34;mm\u0026#34;)) Recortar un mapa Para enfocar un mapa en una zona específica y eliminar el resto de los polígonos, usamos la función st_crop(), definiendo los límites del recorte con las coordenadas mínimas y máximas en los ejes x (longitud) e y (latitud).\nPrimero obtengamos un mapa de Chile desde {rnaturalearth}:\nlibrary(rnaturalearth) chile \u0026lt;- ne_countries(country = \u0026#34;Chile\u0026#34;, scale = 10) |\u0026gt; select(pais = admin, region = name_es, geometry) Ahora lo recortamos con st_crop(). Recomiendo hacerlo terminando el recorte con una visualización del mapa, para ver inmediatamente el resultado y así poder ir ajustando el recorte:\nchile_recortado \u0026lt;- chile |\u0026gt; st_crop(xmin = -78, ymax = -17, xmax = -65, ymin = -56) En este caso, hacemos un recorte para obtener el territorio continental de Chile (excluyendo las islas Juan Fernández, Isla de Pascua y Antártica)\nVer código del gráfico mapa_completo \u0026lt;- chile |\u0026gt; ggplot() + geom_sf(fill = \u0026#34;#9069C0\u0026#34;, linewidth = 0) mapa_recortado \u0026lt;- chile_recortado |\u0026gt; ggplot() + geom_sf(fill = \u0026#34;#9069C0\u0026#34;, linewidth = 0) # poner los dos mapas lado a lado library(patchwork) mapa_completo + mapa_recortado También podemos hacer recortes que pasen por encima de los polígonos, eliminando la geografía que quede fuera del recorte. En este caso hacemos un recorte más cercano a la zona costera de Temuco y Valdivia:\nMover puntos de un mapa Cuando ya contamos con tabla con datos geoespaciales en puntos, a veces es necesario mover la posición de los puntos en un mapa, por ejemplo, para evitar que se sobrepongan etiquetas, para ajustar una coordenada incorrecta, o para mejorar la visualización.\nLos puntos pueden moverse al modificar las coordenadas de la geometría. En el caso de los puntos se las coordenadas son un vector de dos elementos (longitud y latitud), por lo que podemos sumar o restarle a la columna geometry para modificar la posición de cada punto.\nCreemos una tabla con latitudes y longitudes, y convirtámosla a sf con st_as_sf(), como vimos antes:\npuntos \u0026lt;- tribble(~nombre, ~lon, ~lat, \u0026#34;Hualqui\u0026#34;, -8113368, -4434476, \u0026#34;Coronel\u0026#34;, -8136065, -4440419, \u0026#34;Concepción\u0026#34;, -8132714, -4414370, \u0026#34;Lota\u0026#34;, -8139383, -4451456, \u0026#34;Penco\u0026#34;, -8119934, -4402016, \u0026#34;Talcahuano\u0026#34;, -8139277, -4399701) |\u0026gt; # convertir a tabla sf st_as_sf(coords = c(\u0026#34;lon\u0026#34;, \u0026#34;lat\u0026#34;), crs = 3395) Así se ven los puntitos por sí solos:\nAhora recortemos el mapa de Chile para enfocarnos en la zona donde están los puntos. Creamos un buffer alrededor de los puntos para agrandarlos con st_buffer(), y luego calculamos la caja que los contiene con st_bbox() para usarla como recorte.\ncaja_puntos \u0026lt;- puntos|\u0026gt; st_buffer(50000) |\u0026gt; # agrandar puntos st_bbox() # calcular caja que contiene a los puntos Ahora que tenemos la caja, la usamos dentro de st_crop() para recortar el mapa, pero ojo, que ambas capas tienen que tener el mismo sistema de referencias de coordenadas (CRS), así que primero transformamos el mapa de Chile con st_transform() para que use el mismo sistema que la capa de puntos.\nchile_recorte \u0026lt;- chile |\u0026gt; # ajustar crs para que coincidan st_transform(crs = st_crs(puntos)) |\u0026gt; # recortar con la caja (bounding box) st_crop(caja_puntos) Ver código del gráfico ggplot() + geom_sf(data = chile_recorte, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#E0C7F0\u0026#34;, alpha = 0.5) + geom_sf(data = puntos, color = \u0026#34;#402E5A\u0026#34;, size = 3, alpha = 0.7) + geom_sf_text(data = puntos, aes(label = paste(\u0026#34; \u0026#34;, nombre)), color = \u0026#34;#402E5A\u0026#34;, size = 3, hjust = 0, angle = -35) Imaginemos que queremos mover o modificar puntos específicos del mapa. Igual como haríamos con una tabla de datos, usamos mutate() para modificar la columna geometry, que contiene las coordenadas de cada punto, y le aplicamos una función para modificar sus valores condicionalmente, como case_when(). Así podemos modificar los puntos dependiendo de si coinciden con criterios basados en datos, en vez de hacerlo a mano.\nPara este ejemplo, modificaremos puntos a partir de su valor en la variable nombre. Simplemente sumamos a las coordenadas de los puntos que queremos mover, recordando que como es un vector de dos elementos, sumamos un vector de dos elementos. Para moverlos horizontalmente (hacia el este), sumamos a la primera posición del vector (longitud, c(7000, 0)) y dejamos la segunda posición (latitud) en cero.\nEl problema es que, al hacer esto, convertimos las coordenadas en meros números, por lo que pierden atributos geométricos, lo que causa problemas. Para resolverlo, le sacamos también los atributos geométricos a los demás puntos que no se mueven (con st_set_crs(NA)), y después luego le reasignamos el sistema de coordenadas (CRS) original a toda la tabla con st_set_crs().\npuntos_movidos \u0026lt;- puntos |\u0026gt; # modificar puntos condicionalmente mutate(geometry = case_when(nombre == \u0026#34;Hualqui\u0026#34; ~ geometry + c(7000, 0), nombre == \u0026#34;Penco\u0026#34; ~ geometry + c(7000, 0), .default = st_sfc(geometry) |\u0026gt; st_set_crs(NA)) ) |\u0026gt; st_set_crs(st_crs(puntos)) Otra alternativa sería\u0026hellip; Otra alternativa al problema de perder las coordenadas al modificar los puntos es hacer lo contrario: volver a agregar las coordenadas a cada observación que se modifique agregándoles st_set_crs(st_crs(mapa)), y dejando las demás observaciones sin cambios.\npuntos |\u0026gt; mutate(geometry = case_match(nombre, \u0026#34;Hualqui\u0026#34; ~ st_sfc(geometry + c(7000, 0)) |\u0026gt; st_set_crs(st_crs(puntos)), \u0026#34;Tomé\u0026#34; ~ st_sfc(geometry + c(7000, 0)) |\u0026gt; st_set_crs(st_crs(puntos)), .default = geometry) ) Personalmente encuentro que la primera forma es más legible.\nLo que sumemos o restemos a las coordenadas depende del sistema de coordenadas de cada mapa. En algunos va a haber que sumar números muy pequeños, pero en otros sistemas de coordenadas se usan números muy grandes.\nEn el gráfico vemos en color más tenue las posiciones originales de los puntos que fueron modificados.\nVer código del gráfico ggplot() + geom_sf(data = chile_recorte, fill = \u0026#34;#9069C0\u0026#34;, color = \u0026#34;#E0C7F0\u0026#34;, alpha = 0.5) + geom_sf(data = puntos, color = \u0026#34;#402E5A\u0026#34;, size = 3, alpha = 0.2) + geom_sf(data = puntos_movidos, color = \u0026#34;#402E5A\u0026#34;, size = 3, alpha = 0.7) + geom_sf_text(data = puntos_movidos, aes(label = paste(\u0026#34; \u0026#34;, nombre)), color = \u0026#34;#402E5A\u0026#34;, size = 3, hjust = 0, angle = -35) ⚠️ Este tutorial se encuentra en construcción ⚠️ Recursos para aprender más Apuntes Libros Drawing beautiful maps programmatically with R, sf and ggplot2 Geocomputation with R Spatial Data Science With Applications in R Using Spatial Data with R ","date":"2025-11-18T00:00:00Z","excerpt":"R cuenta con un muy amplio ecosistema para trabajar con datos geoespaciales. Uno de los paquetes más importantes es `{sf}`, que permite manipular datos espaciales a partir del estándar _simple features_ (características simples). En esta guía iré recopilando los comandos básicos para manipular y visualizar datos geoespaciales en R. En la medida que voy aprendiendo más sobre hacer mapas, iré actualizando y complementando.","href":"https://bastianoleah.netlify.app/blog/mapas_sf/","tags":"mapas ; visualización de datos","title":"Mapas y visualización de datos geoespaciales en R con {sf}"},{"content":"Hoy domingo 17 de noviembre celebramos una nueva fecha electoral en Chile, esta vez eligiendo presidente.\nApliqué el código que he usado en elecciones pasadas para obtener los datos del Servicio Electoral (Servel) en tiempo real, para así ir generando gráficos, tablas y mapas con los resultados preliminares.\nEl repositorio contiene todo el código para acceder en tiempo real a los datos preliminares publicados en la web del Servel.\nEl sistema que programé usa RSelenium para hacer web scraping de las tablas, a las que se debe acceder presionando botones en el sitio para elegir elección, región, y comuna, por lo que Selenium resulta ideal para ir probando junto al navegador títere las formas de controlar la navegación por medio de código, y eventualmente automatizar el acceso a todas las tablas mediante un loop. Luego se aplica un script de limpieza de datos, y finalmente, según las comunas del país que se definan, el sistema genera gráficos, tablas, mapas y textos en base a los resultados de cada comuna, los cuales se guardan y se ordenan en una carpeta llamada salidas, la que me permite obtener todos los resultados juntos (las imágenes y el texto con cifras y otros datos) y listos para subir a redes sociales.\nMapa comparativo por candidato presidencial en la Región Metropolitana, por comuna, para Jeanette Jara (26,85%), José Antonio Kast (23,93%) y Franco Parisi (19,69%):\nMapa de ventajas de candidatos presidenciales en la Región Metropolitana, comparando ventaja porcentual por comuna entre las dos primeras mayorías:\nGráficos y tablas para comunicar resultados comunales:\nGráfico de resultados presidenciales por comuna Tabla de resultados presidenciales por comuna ","date":"2025-11-17T00:00:00Z","excerpt":"En una nueva fecha electoral en Chile, apliqué el código que he usado en elecciones pasadas para obtener los datos del Servicio Electoral en tiempo real para así ir generando gráficos, tablas y mapas con los resultados preliminares.","href":"https://bastianoleah.netlify.app/blog/elecciones_presidenciales_2025/","tags":"procesamiento de datos ; web scraping ; visualización de datos ; gráficos ; tablas ; mapas ; datos ; Chile","title":"Visualización y scraping de resultados de las elecciones presidenciales 2025"},{"content":"Las aplicaciones Shiny funcionan con un servidor detrás, que es el proceso de R que realiza los cálculos necesarios para mostrar tus contenidos. Por lo mismo, estas aplicaciones no pueden estar conectadas por siempre, porque el proceso no puede estar esperando que la o el usuario hagan algo por siempre, así que y luego de un tiempo de inactividad se desconectan.\nPor ejemplo, si pasa mucho tiempo, o si el usuario de la app presiona un enlace y se va de la aplicación, y después aprieta atrás en el navegador, la app podría haberse desconectado al detectar que el usuario se fue. En estos casos la aplicación se pone gris, o bien, aparece un mensaje en inglés sobre la desconexión.\nPara mejorar la experiencia de uso podemos configurar mensaje de desconexión más amigable. Con el paquete {shinydisconnect} de Dean Attali podemos personalizar el mensaje de desconexión de la app para que les usaries entiendan mejor que la app requiere recargarse.\nInstala el paquete con install.packages(\u0026quot;shinydisconnect\u0026quot;) y agrégalo al código de tu app:\nlibrary(shinydisconnect) Usaremos una aplicación sencilla de prueba, que tiene el siguiente código para hacerla desconectarse:\nobserveEvent(input$desconectar, { session$close() }) Para personalizar el mensaje de desconexión, en la interfaz de usuario (ui), agregamos la función disconnectMessage(), donde puedes personalizar el texto y los colores:\n# mensaje en caso de desconexión disconnectMessage( refresh = \u0026#34;Volver a cargar\u0026#34;, background = \u0026#34;#EAD1FA\u0026#34;, colour = \u0026#34;#553A74\u0026#34;, refreshColour = \u0026#34;#9069C0\u0026#34;, overlayColour = \u0026#34;#553A74\u0026#34;, size = 14, text = \u0026#34;La aplicación se desconectó. Vuelve a cargar la página.\u0026#34; ), Ahora cuando la aplicación se cae o desconecta, aparece un mensaje más amigable:\nMucho mejor!\nVer código completo de la aplicación library(shiny) library(bslib) library(ggplot2) library(shinydisconnect) thematic::thematic_shiny() ui \u0026lt;- page_fill( # tema theme = bs_theme( fg = \u0026#34;#553A74\u0026#34;, bg = \u0026#34;#EAD1FA\u0026#34;, primary = \u0026#34;#6E3A98\u0026#34; ), # mensaje en caso de desconexión disconnectMessage( refresh = \u0026#34;Volver a cargar\u0026#34;, background = \u0026#34;#EAD1FA\u0026#34;, colour = \u0026#34;#553A74\u0026#34;, refreshColour = \u0026#34;#9069C0\u0026#34;, overlayColour = \u0026#34;#553A74\u0026#34;, size = 14, text = \u0026#34;La aplicación se desconectó. Vuelve a cargar la página.\u0026#34; ), # interfaz div( h1(\u0026#34;Una aplicación muy aburrida\u0026#34;), plotOutput(\u0026#34;grafico\u0026#34;, height = 240, width = 320), actionLink(\u0026#34;desconectar\u0026#34;, \u0026#34;Desconectar la app\u0026#34;) ) ) server \u0026lt;- function(input, output, session) { # desconectar la app observeEvent(input$desconectar, { session$close() }) output$grafico \u0026lt;- renderPlot({ iris |\u0026gt; ggplot() + aes(Sepal.Length, Sepal.Width) + geom_point() }) } shinyApp(ui, server) ","date":"2025-11-16T00:00:00Z","excerpt":"Las aplicaciones Shiny funcionan con un servidor detrás, que es el proceso de R que realiza los cálculos necesarios para mostrar tus contenidos. Por lo mismo, estas aplicaciones no pueden estar conectadas por siempre. Lo bueno es que podemos personalizar el mensaje de desconexión de la app para que les usaries entiendan mejor que la app requiere recargarse.","href":"https://bastianoleah.netlify.app/blog/shiny_desconexion/","tags":"shiny","title":"Mensajes de desconexión personalizados en Shiny"},{"content":"Acabo de publicar una actualización a mi app de visualización de estadísticas delictuales. Consiste en la actualización a los datos más recientes a la fecha: junio de 2025, publicados en la plataforma de estadísticas delictuales del Centro de Estudios y Análisis del Delito.\nSe trata de una aplicación tipo dashboard que presenta visualizaciones de los datos oficiales de casos policiales, entendidos como:\ndenuncias formales que la ciudadanía realiza en alguna unidad policial posterior a la ocurrencia del delito, más los delitos de los que la policía toma conocimiento al efectuar una detención en flagrancia, es decir, mientras ocurre el ilícito.\nUna de las características principales de la aplicación es facilitar la exploración de datos delictuales a nivel comunal, y además por mes y año, lo cual es complicado y poco amigable de hacer en la plataforma oficial de CEAD. También es posible descargar los datos desde el repositorio.\nGráfico de delitos de mayor connotación social Gráfico de total de delitos mensuales en el país Gráfico de delitos en una comuna específica Los datos de la aplicación se obtienen mediante web scraping, usando código de R que emula las requests internas que la plataforma oficial de CEAD realiza para obtener sus datos. Esto significa que se emulan los miles de requests necesarios para acceder a todas las comunas del país, por todos los años, en todos los meses, solicitando todos los delitos disponibles, dentro de un loop que toma un par de horas en terminar1. El proceso de extracción de datos se explica en este tutorial.\nCaptura de mi sesión de R obteniendo los datos actualizados La extracción de datos automatizada recibe las tablas en formato HTML, y las guarda tal cual para luego ser limpiadas en otro script, también automáticamente. En este punto el único inconveniente fue pasar de una tabla con múltiples encabezados, y grupos y subgrupos que solamente se distinguen de las filas de datos por su color o el tamaño de las letras 😣\nOdiamos los datos sucios En el repositorio hay más información sobre el proceso y un breve apartado metodológico sobre los delitos considerados (porque este año actualizaron las categorías de delitos incluidos).\ndebido a la espera ética entre requests que toma en consideración el tiempo de respuesta del servidor para no sobrecargarlo.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-11-15T00:00:00Z","excerpt":"Acabo de publicar una actualización a mi [app de visualización de estadísticas delictuales](https://bastianoleah.shinyapps.io/delincuencia_chile). Es una aplicación tipo dashboard que presenta visualizaciones de los datos oficiales de casos policiales. La app se actualizó a los datos más recientes a la fecha: junio de 2025, publicados en la plataforma de estadísticas delictuales del Centro de Estudios y Análisis del Delito.","href":"https://bastianoleah.netlify.app/blog/2025-11-14/","tags":"blog ; datos ; Chile ; apps","title":"Actualización: plataforma de visualización de estadísticas delictuales"},{"content":"Éste es un proyecto personal de visualización de datos que realiza un cruce entre distintas fuentes de datos para ver la correlación entre la cobertura mediática de casos de delincuencia en Chile en comparación con las estadísticas mensuales de delitos en el país.\nLa hipótesis es que, si bien la delincuencia ha aumentado, la cobertura de prensa de temas sobre delincuencia ha aumentado en una medida mucho más grande que el fenómeno que pretende reflejar.\nNotas metodológicas Sobre la obtención de datos, ver repositorio de Análisis de prensa chilena.\nLas noticias se detectan en su temática por coincidencia de conceptos parciales, donde cada noticia debe superar un umbral de palabras relacionadas al concepto para clasificarse en una temática. Se clasificaron dos temáticas: delincuencia y homicidios.\nPor ejemplo, una noticia va a clasificar como en el tema homicidios si contiene conceptos como \u0026ldquo;asesinato\u0026rdquo;, \u0026ldquo;homicidio\u0026rdquo;, y si contiene palabras como \u0026ldquo;falleció\u0026rdquo; o \u0026ldquo;murió\u0026rdquo; pero solamente si son antecedidas o precedidas por el uso de armas. Además, una noticia debe contener al menos 3 instancias de estos conceptos para mantener su clasificación.\nUna noticia puede clasificar en ambos temas a la vez si cumple con los umbrales de ambos temas.\nEn el caso de las noticias, se calculan los procentajes para eliminar el efecto de días con mayor o menor cantidad de noticias. Por la misma razón, también se calcula la media móvil de las cantidades de noticias para suavizar las series.\nFuentes Los datos de homicidios son obtenidos desde el Centro para la Prevención de Homicidios y Delitos Violentos.\nLos datos de noticias son obtenidos de mi proyecto Análisis de prensa chilena, un proyecto que contiene más de 700 mil noticias únicas de medios de prensa digitales de Chile.\nLos datos de delincuencia son obtenidos desde el Centro de Estudio y Análisis del Delito (CEAD), por medio de mi proyecto Visualizador de estadísticas oficiales de delincuencia en Chile.\nLos delitos de mayor connotación social son: Homicidios, Violencia intrafamiliar, Robos con violencia o intimidación, Robo violento de vehículo motorizado, Robo en lugar habitado, Robos en lugar no habitado, Robo por sorpresa, Robo frustrado, Hurtos, Robo de vehículo motorizado, Otros robos con fuerza en las cosas, Lesiones graves o gravísimas, Lesiones menos graves, Lesiones leves, Homicidios, Femicidios, Violaciones.\nCódigo bajo licencia de código abierto GPL3.\nBastián Olea Herrera. Sociólogx y analista de datos. https://bastianolea.rbind.io\n","date":"2025-11-15T00:00:00Z","excerpt":"Éste es un proyecto personal de visualización de datos que realiza un cruce entre distintas fuentes de datos para ver la correlación entre la cobertura mediática de casos de delincuencia en Chile en comparación con las estadísticas mensuales de delitos en el país.","href":"https://bastianoleah.netlify.app/blog/delincuencia_prensa/","tags":"Chile ; gráficos ; datos ; ciencias sociales","title":"Comparación entre estadísticas oficiales de delincuencia y la cobertura de la delincuencia en medios de comunicación"},{"content":"Las aplicaciones Shiny funcionan con reactividad: una cadena de causalidad que va desde los inputs de tu app, pasando por las expresiones reactivas (objetos de R que se actualizan automáticamente cuando cambian sus dependencias), hasta los outputs que se muestran en la interfaz de usuario.\nCuando programamos una app Shiny vamos haciendo éste tipo de conexiones:\nestos dos inputs van a hacer que en este reactive() se filtren los datos, y el resultado del reactive() va a usarse para generar un output en forma de gráfico.\nCon {reactlog} puedes explorar visualmente esta cadena o red de dependencias, para entender cómo tu aplicación se va generando, analizar las dependencias entre elementos, y buscar posibles optimizaciones.\nPara activar {reactlog}, antes de ejecutar tu app, ejecuta lo siguiente:\nreactlog::reactlog_enable() Luego ejecuta tu aplicación normalmente, o simplemente déjala cargarse, y tienes dos opciones:\nPresiona Ctrl + F3 (o Cmd + F3 en Mac) mientras la aplicación se ejecuta para abrir la visualización de reactlog en una nueva ventana. Después de cerrar la aplicación, ejecuta reactlog::reactlog_show() para abrir la visualización. Aparecerá un diagrama de nodos y conexiones que representan los distintos elementos de tu aplicación y sus relaciones entre sí.\nEn esta visualización vemos al lado izquierdo los inputs, que usualmente son el principio de la cadena de reactividad. Estos inputs se conectan a las expresiones reactivas que usan sus valores, y éstas a su vez se conectan con otras expresiones reactivas, o con outputs, que salen al lado derecho.\nRecordemos que, en Shiny, la reactividad va desde el final hacia el principio: todo parte porque existen outputs que requieren los resultados de elementos reactivos (datos, cálculos), las cuales a su vez dependen de otros inputs, etc., y así se van evaluando todas las operaciones hasta que la app queda cargada (idle).\nPuedes hacer clic en los nodos para ver detalles adicionales, como el tiempo que tomó cada operación, las dependencias, y más.\nPuedes hacer doble clic en los nodos para que el gráfico se reordene y muestre las dependencias directas.\nEn la parte superior, puedes avanzar o retroceder para ir viendo el estado de tu aplicación en distintos momentos de su ejecución; por ejemplo, empezar desde el inicio e ir viendo cómo los outputs van llamando a sus dependencias hasta que la app se carga completa.\nEn este video vemos el funcionamiento interno de una aplicación compleja, y arriba vemos que todo pasó en menos de 2 segundos.\nRecursos Posit: Reactivity - An overview Mastering Shiny: Basic reactivity ","date":"2025-11-14T00:00:00Z","excerpt":"Las aplicaciones Shiny funcionan con _reactividad_: una cadena de causalidad que va desde los _inputs_ de tu app, pasando por las expresiones reactivas (objetos de R que se actualizan automáticamente cuando cambian sus dependencias), hasta los _outputs_ que se muestran en la interfaz de usuario. Con `{reactlog}` puedes explorar visualmente esta cadena o red de dependencias, para entender cómo tu aplicación se va generando, analizar las dependencias entre elementos, y buscar posibles optimizaciones.","href":"https://bastianoleah.netlify.app/blog/shiny_reactlog/","tags":"shiny ; optimización","title":"Analiza el funcionamiento de tus aplicaciones Shiny con {reactlog}"},{"content":"Comparto un poco de mi trabajo como analista de datos en la Subsecretaría de Desarrollo Regional y Administrativo (Subdere).\nEn esta oportunidad me pidieron hacer mapas que visualizaran las propuestas de Áreas Metropolitanas en distintas regiones de Chile, junto a algunas estadísticas relacionadas.\nLos mapas combinan shapes y datos de múltiples fuentes:\nPolígonos comunales: División Político Administrativa 2023, IDE Chile Límites comunales: División Político Administrativa 2022, Subdere Polígonos de áreas pobladas: Biblioteca del Congreso Nacional Red vial: Biblioteca del Congreso Nacional Polígonos de masas lacustres: Biblioteca del Congreso Nacional Este proyecto lo organicé en cuatro scripts:\nEn funciones.R se definen las funciones para cargar, procesar, filtrar y generar los mapas.\nEl script procesar.R recibe el identificador de una región, y usando las funciones definidas en funciones.R carga los datos y filtra los mapas para esa región, haciendo ajustes específicos al territorio si es que corresponde. El resultado son varios objetos que contienen las capas geográficas y datos para la región.\nLuego el script graficar.R recibe esos objetos y genera la visualización, incluyendo el gráfico de torta, el minimapa, y los textos que van encima de las visualizaciones. En este paso las funciones realizan varias adaptaciones particulares a cada región para que se vean bien, como recortes, posicionamiento de los elementos, dimensiones del mapa, etc.\nTodo lo anterior (idealmente) se ejecuta desde generar.R, que es el loop/orquestador que produce todo pasando por todas las regiones, de modo que si se modifica un dato, un shape o algún detalle de la visualización, se ejecuta el orquestador y se obtienen todos los mapas actualizados.\n","date":"2025-11-14T00:00:00Z","excerpt":"Comparto un poco de mi trabajo como analista de datos. En esta oportunidad me pidieron hacer mapas que visualizaran las propuestas de Áreas Metropolitanas en distintas regiones de Chile, junto a algunas estadísticas relacionadas.","href":"https://bastianoleah.netlify.app/blog/mapas_areas_metropolitanas/","tags":"blog ; mapas ; Chile ; gráficos","title":"Portafolio: mapas de Áreas Metropolitanas"},{"content":" Llevo varios años trabajando con datos, tanto en el sector privado, como independiente y en el sector público, y hay algo que es una constante: siempre te van a pedir hacer cambios!\nYa sea porque los datos cambian/se actualizan, porque hay que modificar los cálculos, porque hay que generar nuevos productos, o porque hay que cambiar los resultados (textos, gráficos, tablas).\nPor eso, para estos trabajos no basta con \u0026ldquo;hacer\u0026rdquo; el resultado de principio a fin (un reporte, etc.), porque cualquier cambio (en datos/cálculo/salidas) implicaría \u0026ldquo;re-hacer\u0026rdquo; el trabajo. Un cambio en los datos significa volver a hacer los gráficos, ponerlos en el informe, actualizar los textos, etc.\nCon la programación aplicada al trabajo con datos, tú no \u0026ldquo;haces\u0026rdquo; los resultados del trabajo, sino que desarrollas un sistema que \u0026ldquo;haga\u0026rdquo; los resultados por ti.\nEn otras palabras, tu trabajo no es hacer el resultado, sino desarrollar las instrucciones (código) que produzcan el resultado que buscas, lo que te da la libertad de cambiar estas instrucciones para modificar el resultado.\nTeniendo las instrucciones, un cambio en los datos solo significa volver a correr el proceso y obtener los resultados actualizados, o un cambio en los gráficos es solo cambiar las instrucciones que los crean, y volver a generar el reporte que los contiene.\nLa programación es una forma de optimizar tu trabajo al volverlo reutilizable: tu trabajo no se \u0026ldquo;gasta\u0026rdquo; al producir un resultado, no fue tiempo perdido, sino que queda disponible como herramientas para volver a aplicarlas las veces que sean necesarias, incluso por otras personas. Obviamente esto tiene beneficios secundarios como la reproducibilidad, la trazabilidad, la transparencia, el respaldo, la auditabilidad y más.\nLa programación es, entonces, una forma de trabajo que permite adaptarse a la posibilidad permanente de cambios y mejoras, y también una estrategia para acumular herramientas para el futuro.\n","date":"2025-11-13T00:00:00Z","excerpt":"Llevo varios años trabajando con datos, tanto en el sector privado, como independiente y en el sector público, y hay algo que es una constante: siempre te van a pedir hacer cambios! Por eso, para estos trabajos no basta con “hacer” el resultado de principio a fin (un reporte, etc.), porque cualquier cambio (en datos/cálculo/salidas) implicaría “re-hacer” el trabajo...","href":"https://bastianoleah.netlify.app/blog/2025-11-13/","tags":"blog","title":"¿Por qué programar para trabajar con datos?"},{"content":" Índice Generando el índice de búsqueda Obteniendo los datos del sitio Implementando la búsqueda Desarrollando la aplicación Obtener datos desde la app Búsqueda desde la app Salida de los resultados de búsqueda Apariencia de la app Conclusión Este blog ya lleva más de 100 publicaciones! 🎉 y si bien uso las etiquetas para mantener todo organizado y ayudar a descubrir nuevos posts, a veces hasta a mi me cuesta encontrar publicaciones entre tanta cosa 😅\nPor esa razón hace tiempo que quería implementar un buscador en este sitio, cosa que resultó ser más complicada de lo que esperaba. Pero lo logré, y estoy irracionalmente feliz por eso 💜\nAlgo hermoso de programar es la sensación de logro y orgullo que provoca poder crear cosas bonitas y que funcionan! ✨\nPero les cuento la historia. Pasa que lamentablemente1 decidí hacer este blog con Hugo en vez de con Quarto, que recientemente se ha vuelto una excelente herramienta para crear blogs y sitios web. Quarto viene con buscador implementado, pero en Hugo había que implementarlo a mano. Y si bien encontré un tutorial muy completo para implementar búsquedas con LunrJS en Hugo, era realmente larguísimo y complicado 😣 Así que me rendí.\nPero resulta que, cuando estaba intentando seguir ese tutorial, noté que en los primeros pasos se configuraba Hugo para generar un indice del blog en formato JSON. Un índice del sitio es un documento de texto que contiene todo el contenido de tu sitio web. El resto del tutorial era sobre usar ese índice para implementar la búsqueda. Entonces quedé pensando… 🤔\nDespués de semanas de darle vuelta a la frustración de no haberlo logrado, decidí hacerlo a mi manera, y me puse a hacer una app Shiny con R que usara el índice para buscar contenido y entregar resultados. ¿Qué tan difícil podía ser? 🫢\nResulta que nada de difícil. En menos de una hora ya tenía un producto funcional!\nGenerando el índice de búsqueda El primer paso para el buscador fue hacer que mi blog generara un índice de su contenido para poder buscarlo. Esto que usualmente es complejo, porque implica instalar programas que se corren regularmente para re-generar el índice, con Hugo se hace facilito porque viene integrado en su forma de funcionar.\nVer código e instrucciones para crear el índice Simplemente había que agregar al config.toml (archivo de configuración del sitio) que, además de HTML y XML, genere un output JSON del sitio. Luego, en una plantilla, decirle qué queremos que contenga el sitio.\nEn config.toml, agregar al final estas líneas:\n[outputs] home = [\u0026#34;HTML\u0026#34;, \u0026#34;RSS\u0026#34;, \u0026#34;JSON\u0026#34;] page = [\u0026#34;HTML\u0026#34;] Luego, crear un archivo index.json en la carpeta layouts para decirle que queremos que el JSON contenga título, enlace, fecha, etiquetas, y el texto completo de cada post:\n{{- $.Scratch.Add \u0026#34;pagesIndex\u0026#34; slice -}} {{- range $index, $page := .Site.Pages -}} {{- if in (slice \u0026#34;post\u0026#34; \u0026#34;blog\u0026#34; \u0026#34;tutoriales\u0026#34;) $page.Type -}} {{- if gt (len $page.Content) 0 -}} {{- $pageData := (dict \u0026#34;title\u0026#34; $page.Title \u0026#34;href\u0026#34; $page.Permalink \u0026#34;date\u0026#34; $page.Params.Date \u0026#34;tags\u0026#34; (delimit (default (slice) $page.Params.tags) \u0026#34; ; \u0026#34;) \u0026#34;content\u0026#34; $page.Plain) -}} {{- $.Scratch.Add \u0026#34;pagesIndex\u0026#34; $pageData -}} {{- end -}} {{- end -}} {{- end -}} {{- $.Scratch.Get \u0026#34;pagesIndex\u0026#34; | jsonify -}} Finalmente, reconstruimos el sitio ejecutando blogdown::build_site(). En la carpeta public va a aparecer el archivo index.json.\nLa gracia es que el índice se construye y actualiza solito, sin depender de otros programas ni instalar nada. Además, este índice queda expuesto a la internet, por lo que se puede acceder a él por la url https://bastianolea.rbind.io/index.json\nEntonces, sin necesidad de web scraping ni APIs, podía hacer una app que leyera directamente los datos del sitio 😱\nObteniendo los datos del sitio Empecé con las primeras pruebas.\nAntes de hacer lo del índice JSON, primero empecé leyendo el índice XML, con el paquete {xml2} leía la dirección con read_xml(), y usando xml_find_all() iba apuntando a los elementos del índice para crear un dataframe. Pero pronto me di cuenta de que ese índice no tenía el texto completo de los posts, y tampoco leía el código de los posts.\nAsí que implementé el índice en JSON, y con el paquete {jsonlite} y la función fromJSON() pude obtener directamente un dataframe desde el índice, sin pasos intermedios como con la versión XML.\nAsí que hice una función procesar_json() que fue más o menos así:\nobtener \u0026lt;- sitio |\u0026gt; jsonlite::fromJSON() datos \u0026lt;- obtener |\u0026gt; tibble() |\u0026gt; mutate(texto = limpiar_html(texto)) |\u0026gt; mutate(fecha = extraer_fechas(fecha)) |\u0026gt; mutate(link = corregir_enlace(link)) Con esto ya tenía un dataframe con las más de 100 publicaciones.\nImplementando la búsqueda Existen muchos servicios y paquetes especializados en búsqueda… pero yo no soy informáticx ni quería complicarme. ¿Qué tan malo podía ser usar {stringr} para detectar texto y hacerlo pasar por motor de búsqueda? 🤔\nResulta que nada de malo 😌 Los resultados de str_detect() no son para nada lentos, sobre todo cuando estamos hablando de apenas cientos de observaciones, cada una con apenas unas miles de palabras.\nAsí que implementar la búsqueda fue tan sencillo como:\nbusqueda \u0026lt;- \u0026#34;waldo\u0026#34; resultado \u0026lt;- datos |\u0026gt; filter(str_detect(texto, busqueda)) Y listo. Se obtiene el dataframe filtrado, limpio, bonito. Literalmente desde la obtención de los datos a los resultados de búsqueda en menos de 50 líneas de código. No hay mucho más que agregar, aguante R 😂\nDesarrollando la aplicación En resumen, la app es básicamente:\nUn input de texto libre para las búsquedas con textInput() Un reactive() que cargue los datos del índice index.json alojado en mi sitio Otro rective() que filtre estos datos en base al texto de búsqueda Un output de texto que diga la cantidad de resultados Un output de HTML para los resultados construidos en base a los datos Amononar el UI de la app para que combine con mi sitio, usando bs_theme() para encargarse del tema del sitio y una hoja de estilos CSS Obtener datos desde la app En la obtención de datos del sitio solamente puse la función procesar_json(). Esto implica, naturalmente, la carga (o descarga) del índice, que como tiene tanto texto puede pesar un par de megas.\nEste sería el único *cuello de botella de la app, así que le puse un bindCache() para que se guarden los resultados en la app, acelerando la carga del índice y disminuyendo el impacto en el servidor del sitio, y le puse que el cache durara una hora (usando como llave del cache la fecha/hora del día redondeada a la hora con floor_date())\n# obtener datos del sitio sitio \u0026lt;- reactive({ message(\u0026#34;obteniendo sitio...\u0026#34;) procesar_json(\u0026#34;https://bastianolea.rbind.io/index.json\u0026#34;) }) |\u0026gt; # guardar cache por hora bindCache(floor_date(now(), unit = \u0026#34;hours\u0026#34;)) Búsqueda desde la app La búsqueda es literal un filter(str_detect(texto, input$busqueda)), que retorna un objeto reactivo con el dataframe filtrado por las coincidencias.\nSe pone un debounce() para que lo que el usuario escriba no se busque a cada rato, sino que se esperen que el input se quede quieto 300 milisegundos antes de empezar la búsqueda.\nPara el texto de los resultados usé cli::pluralize() para escribir texto singular o plural automáticamente:\npluralize(\u0026#34;Se encontr{?ó/aron} {n} publicaci{?ón/ones}:\u0026#34;) Salida de los resultados de búsqueda Finalmente, teniendo un dataframe con los resultados de búsqueda, no puedo solamente mostrar una tabla con títulos y enlaces. Así que viene la parte más compleja: generar el código HTML en base a los datos para mostrar los resultados de búsqueda con una interfaz personalizada.\nSe trata de un output de HTML, que naturalmente requiere (req()) que el usuario haya buscado algo y existan resultados:\noutput$resultados \u0026lt;- renderUI({ req(termino() != \u0026#34;\u0026#34;) req(n_resultados() \u0026gt; 0) ... }) Luego, mi truco (o mala práctica?) favorito: separar un dataframe por filas para meterlas a un loop de purrr::map():\n# separar resultados elementos \u0026lt;- busqueda() |\u0026gt; mutate(id = row_number()) |\u0026gt; split(~id) Con este código conviertes un dataframe en una lista donde cada elemento de la lista es un dataframe de una fila.\nEntonces, por cada resultado de búsqueda (un elemento de la lista que contiene una fila del dataframe), lo metemos a una función donde definimos qué hacemos con las variables asociadas a cada resultado.\nEn el siguiente loop, cada elemento de los resultados se llama elemento, y como es un dataframe de una fila, con $ extraemos sus variables como titulo, link, etc., y las vamos ubicando libremente en un div():\nmap(elementos, \\(elemento) { div(class = \u0026#34;resultado\u0026#34;, # título con link a(href = elemento$link, target = \u0026#34;_blank\u0026#34;, h3(elemento$titulo)), # fecha div(class = \u0026#34;fecha\u0026#34;, elemento$fecha), # etiquetas con links div(class = \u0026#34;contenedor_etiquetas\u0026#34;, etiquetas(elemento$tags)), # separador hr() ) }) Me gusta esta forma de hacerlo, porque es como llegar al código HTML con un dataframe de una fila y varias columnas, básicamente una lista con varios elementos, y voy decidiendo qué hago con cada elemento en la interfaz que va a crearse. Es como mezclar el UI y el server de Shiny en un mismo proceso.\nLa única parte complicada de esto es el código para las etiquetas o tags, que como pueden ser más de una, creé la función etiquetas() para ponerlas como rectangulitos una al lado de la otra:\nVer código # texto de etiquetas separado por punto y comas etiquetas \u0026lt;- function(tag) { elementos \u0026lt;- tag |\u0026gt; # separar porque es un puro texto delimitado por \u0026#34;;\u0026#34; str_split(\u0026#34;;\u0026#34;) |\u0026gt; unlist() |\u0026gt; # eliminar espacios entre términos str_trim() # por cada elemento, crear un \u0026lt;div\u0026gt; con el texto y un enlace map(elementos, ~div(class = \u0026#34;etiquetas\u0026#34;, # enlace a( div(.x, class = \u0026#34;texto_etiquetas\u0026#34;), href = paste0(\u0026#34;https://bastianolea.rbind.io/tags/\u0026#34;, str_replace_all(.x, \u0026#34; \u0026#34;, \u0026#34;-\u0026#34;)), target = \u0026#34;_blank\u0026#34;) )) } Apariencia de la app Ninguna aplicación sería nada si no cuidamos su apartado estético. En la interfaz de la aplicación primero definimos un tema de colores y tipografías con {bslib}:\n# tema theme = bs_theme( fg = \u0026#34;#553A74\u0026#34;, bg = \u0026#34;#EAD1FA\u0026#34;, primary = \u0026#34;#6E3A98\u0026#34;, font_scale = 1.1, base_font = font_google(\u0026#34;Atkinson Hyperlegible\u0026#34;), heading_font = font_google(\u0026#34;EB Garamond\u0026#34;), ) Luego podemos afinar los detalles con una hoja de estilos CSS. CSS es el lenguaje usado para definir la apariencia de toda página web. Esto es un archivo styles.css que cargamos a la interfaz de la app con includeCSS(\u0026quot;styles.css\u0026quot;).\nMientras hacemos la interfaz de la app, ya sea en UI o dentro de una función como el map() que vimos antes, cuando definimos un div(), en el argumento class creamos clases CSS que luego usaremos para hacer que cada elemento tenga la apariencia deseada en styles.css.\nEntonces, en la hoja de estilo le damos apariencia a las clases que fuimos creando en la app, por ejemplo:\n/* estilo de título de resultados */ h3 { color: #9069C0; font-size: 24px; font-weight: 700; } /* estilo de textos de fechas */ .fecha { font-size: 12px; margin-bottom: 4px; margin-top: -4px; } Finalmente afinamos un detallito de la app: con el paquete {shinydisconnect} de Dean Attali podemos personalizar el mensaje de desconexión de la app para que les usaries entiendan mejor que la app requiere recargarse.\n# mensaje en caso de desconexión disconnectMessage( background = \u0026#34;#EAD1FA\u0026#34;, colour = \u0026#34;#553A74\u0026#34;, refreshColour = \u0026#34;#9069C0\u0026#34;, overlayColour = \u0026#34;#553A74\u0026#34;, size = 14, text = \u0026#34;La aplicación se desconectó. Vuelve a cargar la página.\u0026#34;, refresh = \u0026#34;Volver a cargar\u0026#34; ), Esto pasa porque, como las apps Shiny tienen un servidor detrás, no pueden estar conectadas por siempre, y luego de un tiempo de inactividad se desconectan. Por ejemplo, si el usuario de la app se va a un resultado de búsqueda y después aprieta atrás en el navegador, la app podría haberse desconectado al detectar que el usuario se fue.\nConclusión Acabo de contar, y en total (app.R + funciones.R) la app tiene exactamente 300 líneas de código, que puedes trajinear en su repositorio. Me alegra mucho haber resuelto tan rápido este problema que tenía con el sitio, y el resultado se ve lindo y es funcional 🥰\nSi llegaste hasta aquí leyendo, muchas gracias 💜 Anímate a aprender a programar para que también hagas cosas útiles y bonitas!\nMuchxs usuarixs de R están migrando sus blogs Hugo Apéro a Quarto, principalmente porque Quarto tiene muchas más funcionalidades integradas y permite hacer blogs con códgio de R con muchísimas cosas que Hugo no tiene, como pestañas, notas al margen, estilos y más.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-11-11T00:00:00Z","excerpt":"En este post innecesariamente largo les comento mi irracional alegría de haber programado en R+Shiny [un buscador para este mismo sitio](https://bastianoleah.shinyapps.io/buscador/) (funcionalidad que ya se estaba volviendo muy necesaria). En menos de media hora ya tenía un producto mínimo funcional gracias a lo simple que es programar con R. Esta funcionalidad nueva va a ayudar a poder encontrar posts sobre cualquier tema, sobre todo para mí, que uso este blog a diario para copiar y pegar mi propio código 😂","href":"https://bastianoleah.netlify.app/blog/buscador/","tags":"blog ; Shiny","title":"Desarrollando un buscador para mi blog con Shiny"},{"content":" {waldo} es un paquete de R para encontrar diferencias entre objetos y conjuntos de datos.\nCreemos dos vectores de datos de ejemplo:\nvector_a \u0026lt;- c(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;, \u0026#34;e\u0026#34;) vector_b \u0026lt;- c(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;) Si los comparamos con la función all.equal(), de R base, obtenemos un resultado poco informativo:\n# comparar con base all.equal(vector_a, vector_b) [1] \u0026quot;Lengths (5, 4) differ (string compare on first 4)\u0026quot; [2] \u0026quot;1 string mismatch\u0026quot; Básicamente nos dice \u0026ldquo;son distintos\u0026rdquo; 🤨\nPero si usamos waldo::compare(), obtenemos una comparación ordenada y clara de las diferencias:\n# comparar con waldo waldo::compare(vector_a, vector_b) Se nos informa con color de las diferencias en los datos.\nProbemos con otro ejemplo simple de vectores similares:\nvector_c \u0026lt;- c(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;) vector_d \u0026lt;- c(\u0026#34;d\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;a\u0026#34;) waldo::compare(vector_c, vector_d) La comparación destaca las diferencias de posición entre ambos vectores.\nDesordenemos y ensuciemos un dataframe con ayuda del paquete {messy}, de Nicola Rennie, para compararlo con su versión original:\niris_b \u0026lt;- iris |\u0026gt; messy::messy() compare(iris, iris_b) Las diferencias son tantas que indica que las variables son de tipo distinto, y muestra sus primeras observaciones.\nHagamos otra verión sucia de iris para comparar ambos dataframe sucios:\niris_c \u0026lt;- iris |\u0026gt; messy::messy() compare(head(iris_b), head(iris_c)) En este caso te muestra las filas de ambas tablas intercaladas, para que se vean las diferencias, y luego muestra las columnas con los primeros valores distintos.\nHagamos otro par de versiones sucias, pero ahora que sólo difieran en su cantidad de datos perdidos o missing:\niris_c \u0026lt;- iris |\u0026gt; messy::make_missing() iris_d \u0026lt;- iris |\u0026gt; messy::make_missing() compare(iris_c, iris_d) En este caso, va destacando las diferencias entre ambos dataframes, indicando con color sus cambios y la cantidad extra de cambios que hay.\nPara terminar, veamos un ejemplo más práctico: acá hay dos tablas de datos con muchas columnas, todas con nombres levemente distintos. Es una situación que pasa mucho cuando nos encontramos con datos cuyas columnas son solamente ids de variables que luego hay que ir a buscar a un diccionario de datos, y que por lo tanto tienen nombres muy parecidos 😣\nvar_ead32 var_efe23 var_eea31 var_edr52 var_ead30 var_eae31 0.1150604 0.7647429 0.5429455 0.5328345 0.2072198 0.3364374 var_ead32 var_efe23 var_eae31 var_ede52 var_ead30 var_eae30 0.8271902 0.0638212 0.5359525 0.9399669 0.8877144 0.36798 Así a la rápida es casi imposible saber si las columnas son las mismas, o si no lo son, cuáles sobran y cuáles faltan! 😭\ncompare(tabla_a, tabla_b) Así podemos ver claramente que hay tres columnas distintas entre ambas tablas, y cuáles son, en vez de partirnos la cabeza y los ojos comparando nombres de columnas 🤓\n","date":"2025-11-10T00:00:00Z","excerpt":"`{waldo}` es un paquete de R para encontrar diferencias entre objetos y conjuntos de datos. Es muy útil para solucionarnos problemas comunes que generalmente intentamos resolver a mano (o a ojo, jaja). Aprende a usarlo con algunos ejemplos prácticos.","href":"https://bastianoleah.netlify.app/blog/waldo/","tags":"limpieza de datos ; consejos ; datos perdidos","title":"Encuentra diferencias entre objetos de R con {waldo}"},{"content":" {ggiraph} es un paquete de R que permite agregar interactividad a gráficos {ggplot2}. Esto significa que tus gráficos podrán mostrar información extra al pasar el cursor encima (tooltips), hacer que se destaquen u oculten elementos al pasar el cursor, hacer clic en elementos del gráfico para generar cambios en aplicaciones, y más.\nUn beneficio de {ggiraph} es que, a diferencia de alternativas como {plotly} o {highcharter}, no necesitas aprender a usar un paquete de visualización nuevo, porque {ggiraph} usa los gráficos generados por {ggplot2} y mantiene al 100% su apariencia. Esto significa que tu gráfico interactivo se verá idéntico a tu gráfico de {ggplot2} (a diferencia de con ggplotly() 😒). Esto es gracias a que {ggiraph} toma el gráfico que hiciste y lo convierte a SVG, un formato de imagen vectorial que es compatible con HTML y CSS, lo que permite la interactividad.\nIntroducción Lo primero es instalar el paquete:\ninstall.packages(\u0026#34;ggiraph\u0026#34;) En términos generales, el procedimiento para crear gráficos interactivos con {ggiraph} es:\nHacer un gráfico con {ggplot2} Cambiar las funciones geom_x() por geom_x_interactive() Usar la función girafe() en vez de plot() para generar el gráfico interactivo. Veámoslo con un ejemplo:\nAdaptar gráficos Para entender el proceso, crearemos un gráfico básico y luego lo adaptaremos para incluir interactividad.\nVamos a crear un gráfico de dispersión con {ggplot2}, agregándole un tema de colores personalizado con {thematic}.\nlibrary(ggplot2) library(thematic) thematic_on(bg = \u0026#34;#EAD1FA\u0026#34;, fg = \u0026#34;#6E3998\u0026#34;, accent = \u0026#34;#9069C0\u0026#34;) iris |\u0026gt; ggplot(aes(Sepal.Length, Sepal.Width, color = Species)) + geom_point(size = 2) + scale_color_brewer(palette = \u0026#34;Dark2\u0026#34;) Ahora convertimos el gráfico en interactivo usando {ggiraph}. Para ello, identificaremos las funciones de geometrías del gráfico (en este caso geom_point() y la cambiaremos a geom_point_interactive(). Esta capa del gráfico seguirá funcionando igual que antes, pero ahora podremos agregarle nuevos argumentos:\nEn el argumento data_id, dentro de aes() indicaremos una variable que identifique los elementos del gráfico. Esto puede ser un identificador único si queremos que la interactividad se aplique a cada elemento de manera independiente, o una variable que agrupe las observaciones para aplicar interactividad que opere a varios elementos a la vez. El argumento tooltip dentro de aes() será donde definamos el texto que aparecerá al pasar el cursor sobre un elemento del gráfico. En este texto podemos usar paste() o glue() para agregar información de otras variables que correspondan al elemento destacado, y también podemos usar HTML para dar estilo al texto. library(ggiraph) Warning: package 'ggiraph' was built under R version 4.4.3 library(dplyr) library(glue) grafico \u0026lt;- iris |\u0026gt; mutate(id = row_number()) |\u0026gt; # identificador de observaciones ggplot(aes(Sepal.Length, Sepal.Width, color = Species)) + # geom point(size = 3) + geom_point_interactive( size = 3, aes(data_id = id, tooltip = glue(\u0026#34;\u0026lt;b\u0026gt;Largo:\u0026lt;/b\u0026gt; {Sepal.Length} \u0026lt;b\u0026gt;Ancho:\u0026lt;/b\u0026gt; {Sepal.Width} \u0026lt;b\u0026gt;Especie:\u0026lt;/b\u0026gt; {Species}\u0026#34;)) ) + scale_color_brewer(palette = \u0026#34;Dark2\u0026#34;) Una vez hechas las modificaciones, en vez de usar plot(grafico), usaremos la función girafe() para generar el gráfico interactivo:\ngirafe(ggobj = grafico) Pasa el cursor sobre el gráfico para ver la interactividad!\nTooltips Una de las funcionalidades principales en la interactividad de gráficos es el tooltip, que es el cuadro de texto que aparece al pasar el cursor sobre un elemento del gráfico. Sirven para poder mostrar datos extra sobre los elementos del gráfico sin saturar la visualización, tales como cifras exactas, textos que describen la observación, etc.\nPara modificar el estilo del tooltip, usamos la función opts_tooltip() dentro del argumento options de girafe(). Ahí podemos escribir código CSS para configurar el estilo, como color de fondo y texto, tipografía, tamaños y espaciados, y más.\nestilo_tooltip \u0026lt;- \u0026#34;background-color: #EAD1FA; color: black; font-family: sans-serif; font-size: 9pt; padding: 6px; padding-left: 8px; padding-right: 8px; border-radius: 4px;\u0026#34; girafe(ggobj = grafico, options = list( # estilo de tooltip opts_tooltip(css = estilo_tooltip), # ocultar botones opts_toolbar(saveaspng = FALSE)) ) Dentro de opts_tooltip(), el argumento use_fill hace que el color del tooltip coincida con el color del elemento del gráfico, definido por la paleta de colores y la variable asignada al color.\ngirafe(ggobj = grafico, options = list( opts_toolbar(saveaspng = FALSE), # estilo de tooltip opts_tooltip(use_fill = TRUE, css = estilo_tooltip)) ) Hover El hover es el efecto visual que ocurre cuando pasamos el cursor sobre un elemento del gráfico. Podemos usarlo para destacar un punto, agrandarlo, cambiar su color, o hacer que el resto de los puntos pasen a segundo plano.\nCon opts_hover() definimos el estilo CSS para los puntos en estado de hover, mientras que con opts_hover_inv() definimos el estilo CSS para los puntos que no están en estado de hover; es decir, el resto de los puntos.\nPodemos cambiar el color del punto bajo el cursor:\ngirafe(ggobj = grafico, options = list( opts_toolbar(saveaspng = FALSE), opts_tooltip(use_fill = TRUE, css = estilo_tooltip), # opciones de hover opts_hover(css = \u0026#34;stroke: #6E3998; fill: #6E3998;\u0026#34;) )) Agrandar el punto y hacer que el resto de los puntos se transparenten:\ngirafe(ggobj = grafico, options = list( # estilo de tooltip opts_tooltip(use_fill = TRUE, css = estilo_tooltip), # ocultar botones opts_toolbar(saveaspng = FALSE), # opciones de hover opts_hover(css = \u0026#34;stroke-width: 4px;\u0026#34;), opts_hover_inv(css = \u0026#34;opacity: 0.3;\u0026#34;) )) Hacer que el resto de los elementos cambien de color al posar el cursor sobre un punto:\ngirafe(ggobj = grafico, options = list( # estilo de tooltip opts_tooltip(use_fill = TRUE, css = estilo_tooltip), # ocultar botones opts_toolbar(saveaspng = FALSE), # opciones de hover opts_hover(css = \u0026#34;stroke: none;\u0026#34;), opts_hover_inv(css = \u0026#34;stroke: grey; fill: grey; opacity: 0.3;\u0026#34;) )) Combinar gráficos interactivos Otra funcionalidad clave de la interactividad es la posibilidad de que las interacciones en un gráfico afecten a otros gráficos. Por ejemplo, posar el cursor sobre un elemento del gráfico revela información extra sobre el mismo elemento en un segundo gráfico, o posar el cursor sobre una observación indica su ubicación en una segunda visualización.\nPara combinar gráficos usaremos {patchwork}, paquete que nos permite unir dos o más gráficos en R. Si necesitas una guía más completa sobre combinación de gráficos, revisa este tutorial.\nEn el siguiente ejemplo, crearemos un gráfico de dispersión como el que hemos visto, pero con un data_id que agrupe los puntos por una variable categórica, y un segundo gráfico de barras donde dicha variable categórica se use para calcular promedios de los elementos del primer gráfico. Entonces, al pasar el cursor sobre puntos de un valor de la variable categórica, se destacará su valor promedio en el gráfico de barras, y viceversa.\nlibrary(patchwork) # gráfico de dispersión puntos \u0026lt;- iris |\u0026gt; ggplot() + aes(Sepal.Length, Sepal.Width, color = Species) + geom_point_interactive( size = 3, aes(data_id = Species, tooltip = glue(\u0026#34;\u0026lt;b\u0026gt;Largo:\u0026lt;/b\u0026gt; {Sepal.Length} \u0026lt;b\u0026gt;Ancho:\u0026lt;/b\u0026gt; {Sepal.Width} \u0026lt;b\u0026gt;Especie:\u0026lt;/b\u0026gt; {Species}\u0026#34;))) + scale_color_brewer(palette = \u0026#34;Dark2\u0026#34;) + guides(color = guide_legend(position = \u0026#34;top\u0026#34;)) # gráfico de barras barras \u0026lt;- iris |\u0026gt; group_by(Species) |\u0026gt; summarize(Sepal.Length = mean(Sepal.Length)) |\u0026gt; ggplot() + aes(Species, Sepal.Length, fill = Species) + geom_col_interactive( width = 0.4, aes(data_id = Species, tooltip = glue(\u0026#34;\u0026lt;b\u0026gt;Largo:\u0026lt;/b\u0026gt; {Sepal.Length} \u0026lt;b\u0026gt;Especie:\u0026lt;/b\u0026gt; {Species}\u0026#34;))) + scale_fill_brewer(palette = \u0026#34;Dark2\u0026#34;) + guides(fill = \u0026#34;none\u0026#34;) # combinar ambos gráficos combinado \u0026lt;- barras + puntos + plot_layout(widths = c(1, 3)) # interactividad girafe(ggobj = combinado, options = list( opts_tooltip(use_fill = TRUE, css = estilo_tooltip), opts_toolbar(saveaspng = FALSE), opts_hover(css = \u0026#34;stroke: none;\u0026#34;), opts_hover_inv(css = \u0026#34;opacity: 0.3;\u0026#34;) )) Listo! Con pocas líneas de código convertimos un par de visualizaciones sencillas en algo bastante más interesante e informativo.\nPara conocer ejemplos más interesantes sobre las posibilidades de {ggplot2} con {ggiraph}, recomiendo revisar este conjunto de diapositivas presentadas por Tanya Shapiro y Cédric Scherer en la conferencia UseR 2025.\nRecursos Libro de {ggiraph}, por David Gohel Diapositivas: Plot Twist. Adding Interactivity to the Elegance of {ggplot2} with {ggiraph}, por Tanya Shapiro y Cédric Scherer ","date":"2025-11-07T00:00:00Z","excerpt":"`{ggiraph}` es un paquete de R que permite agregar interactividad a gráficos `{ggplot2}`. Esto significa que tus gráficos podrán mostrar información extra al pasar el cursor encima (_tooltips_), hacer que se destaquen u oculten elementos al pasar el cursor, hacer clic en elementos del gráfico para generar cambios en aplicaciones, y más. También es posible combinar la interactividad de dos o más gráficos, lo que permite crear visualizaciones más complejas y reveladoras.","href":"https://bastianoleah.netlify.app/blog/ggiraph/","tags":"visualización de datos ; ggplot2","title":"Convierte gráficos `{ggplot2}` en visualizaciones interactivas con `{ggiraph}`"},{"content":" Índice Qué es una base de datos Cuándo usar una base de datos Crear una base de datos en Supabase Conectarse a la base de datos Credenciales de acceso Variables de entorno Conexión a la base de datos Crear una tabla en la base de datos Leer una tabla desde la base de datos Procesar desde la base de datos con {dplyr} Avanzado Conexión por IPv4 Recursos Qué es una base de datos Cuando se habla de datos, mucha gente (me incluyo) usa coloquialmente el término base de datos para referirse a datos que están en Excel o csv 😣 Pero la realidad es que una base de datos es algo distinto: un sistema de almacenamiento y procesamiento de datos que puede contener múltiples tablas, que está hosteado en un computador, servidor o en la nube, y que entrega datos de acuerdo a las solicitudes que se le hagan. En este sentido una base de datos es distinto a leer un archivo, porque la base de datos siempre tiene cargados los datos, y está esperando que se los pidan para entregarlos de manera optimizada.\nUna de las diferencias principales al usar bases de datos es que no necesitas cargar los datos en tu computador, porque se encuentran en la base de datos remota. En vez de cargarlos, los puedes solicitar para que la base los procese y te los entregue. Solamente cuando los necesitas en memoria, los cargas localmente.\nPero además, una base de datos puede hacer más que simplemente almacenar los datos. Las solicitudes que hacemos a la base, usualmente hechas por medio del lenguaje SQL, son procesadas de forma rápida y eficiente, entregándote solamente lo necesario. De este modo puedes acceder a conjuntos de muchos millones de observaciones sin que tu computador explote 💥\nCuándo usar una base de datos Cuando la cantidad de observaciones es muy grande, y te encuentras con límites de memoria Cuando necesitas acceder a los datos desde varios equipos o aplicaciones Cuando tus datos ocupan mucho almacenamiento local y preferirías que estuvieran en la nube Cuando tienes muchos datos, y solo los cargas para obtener subconjuntos o resúmenes de los mismos Cuando quieres optimizar la velocidad de acceso a los datos, sobre todo la velocidad de cargar archivos Cuando tienes un conjunto de datos complejos que requieren de múltiples tablas relacionadas entre sí Cuando no puedes mantener todos los datos en memoria A continuación veremos cómo crear una base de datos gratuita, cómo conectarnos a ella desde R, y cómo subir y trabajar con los datos en la base de datos remota.\nCrear una base de datos en Supabase Como una base de datos requiere estar instalada en un computador o servidor, necesitamos un proveedor que nos permita alojar la base de datos1. Una opción es Supabase, una plataforma para bases de datos que se basa en código abierto, y que ofrece bases de datos Postgres gratuitas para proyectos pequeños.\nPara empezar, crea una cuenta en Supabase.\nUna vez en tu cuenta, en la sección Projects, crea un nuevo proyecto:\nUn proyecto es una instancia en el servidor de Supabase con su propia base de datos Postgres2, donde podrás escribir tus tablas de datos. Ponle un nombre y define una contraseña segura. Con esta contraseña podrás acceder a tus datos.\nLuego de crear el proyecto se abrirá el panel principal, donde te indica que (por ahora) tienes cero tablas 👎🏻\nConectarse a la base de datos Con el proyecto creado, ahora podemos conectarnos a la base de datos. Pero necesitamos crear una conexión con la misma, y para ello requerimos ciertas credenciales y datos para que tu computador conecte con el servidor donde está tu base de datos.\nPresiona Connect en la parte superior. para ver los parámetros de conexión.\nSe abrirá un panel donde se nos entregarán los parámetros de acceso a la base de datos. Presiona View parameters para desplegar la información:\nNecesitamos los parámetros host, port, database y user. Ahora iremos a R para introducirlos y conectarnos.\nImportante: si quieres usar la base de datos desde aplicaciones Shiny conectadas a Shinyapps.io, tienes que elegir en Method la opción Transaction Pooler para que tu base de datos funcione por IPv4. Credenciales de acceso Para interactuar con bases de datos, necesitamos el paquete {DBI}, que gestiona las conexiones con bases de datos, y en nuestro caso el paquete {RPostgres}, que es el motor para trabajar con bases de datos Postgres.\ninstall.packages(\u0026#34;DBI\u0026#34;) install.packages(\u0026#34;RPostgres\u0026#34;) Para conectarnos a una base de datos desde R usamos la función dbConnect(). Dentro de esta función explicitamos el driver de la base de datos (en este caso Postgres()), y los parámetros de conexión que obtuvimos en Supabase: dbname, host, port, user y password.\ndb_con \u0026lt;- DBI::dbConnect( RPostgres::Postgres(), dbname = \u0026#34;postgres\u0026#34;, host = \u0026#34;host\u0026#34;, port = 5432, user = \u0026#34;postgres\u0026#34;, password = \u0026#34;contraseña\u0026#34; ) ⚠️ ¡Pero espera! No es seguro escribir credenciales privadas en un script. Así que vamos a seguir las buenas prácticas y vamos a guardar las credenciales de forma segura.\nUna opción es no escribir la contraseña y en su lugar usar rstudioapi::askForPassword() para ingresar la contraseña manualmente pero de forma segura. Sin embargo, no queremos estar escribiendo la contraseña cada vez que necesitemos los datos!\nVariables de entorno Vamos a crear un script donde podamos guardar variables de entorno, que son variables que R puede leer pero que no quedan en el código, y que además siempre se cargan cuando abrimos el proyecto. Es decir, quedan disponibles para poder usarlas, pero estarán ocultas. Así podemos compartir y respaldar nuestro código sin exponer información sensible, y podemos conectarnos a la base de datos sin tener que volver a introducir las credenciales.\nCreamos un script de entorno llamado .Renviron en la raíz de nuestro proyecto ejecutando una función que lo hace por nosotres:\nusethis::edit_r_environ(scope = \u0026#34;project\u0026#34;) En el script que se abre guardamos las credenciales de la siguiente manera:\ndb_host=db.blablablabla.supabase.co db_port=5432 db_user=postgres db_pass=clavebasededatosprueba Una vez guardadas las credenciales, reiniciamos la sesión de R para que se lean las variables de entorno (siempre se leerán al iniciar R), o podemos ejecutar readRenviron(\u0026quot;.Renviron\u0026quot;) para cargarlas.\n⚠️ Importante: Si usas git, no olvides agregar el archivo .Renviron a tu .gitignore para evitar subir a internet credenciales privadas! Conexión a la base de datos Procedemos a hacer la conexión a la base de datos de forma segura, usando Sys.getenv() para obtener las variables de entorno que guardamos en .Renviron:\nlibrary(DBI) library(RPostgres) db_con \u0026lt;- DBI::dbConnect( RPostgres::Postgres(), dbname = \u0026#34;postgres\u0026#34;, host = Sys.getenv(\u0026#34;db_host\u0026#34;), port = Sys.getenv(\u0026#34;db_port\u0026#34;), user = Sys.getenv(\u0026#34;db_user\u0026#34;), password = Sys.getenv(\u0026#34;db_pass\u0026#34;) ) Al conectarnos creamos un objeto que representa nuestra conexión a la base de datos.\ndb_con \u0026lt;PqConnection\u0026gt; postgres@db.ybqnpazmjvictxbevqqk.supabase.co:5432 Usaremos este objeto para interactuar con la base de datos.\nCrear una tabla en la base de datos Con la función dbWriteTable() creamos en la base de datos una tabla nueva a partir de un dataframe; por ejemplo, usando el conjunto de datos iris que viene por defecto en R.\nlibrary(DBI) # crear tabla dbWriteTable(conn = db_con, # conexión a la base name = \u0026#34;prueba\u0026#34;, # nombre de la tabla a crear iris # datos a escribir ) Confirmamos que la tabla se subió a la base de datos consultándole a la base los nombres de las tablas existentes:\ndbListTables(db_con) [1] \u0026quot;prueba\u0026quot; \u0026quot;pg_stat_statements\u0026quot; [3] \u0026quot;pg_stat_statements_info\u0026quot; También podemos confirmar en Supabase que la tabla existe:\nAhora nuestros datos están almacenados remotamente en la base de datos! Acaso ésta será la mística nube de la que tanto hablan? ☁️\nLeer una tabla desde la base de datos Podemos leer las tablas existentes en la base de datos con dbReadTable() tan solo indicando el nombre de la tabla:\ndatos_db \u0026lt;- dbReadTable(db_con, # conexión a la base \u0026#34;prueba\u0026#34; # nombre de la tabla a leer ) head(datos_db) Sepal.Length Sepal.Width Petal.Length Petal.Width Species 1 5.1 3.5 1.4 0.2 setosa 2 4.9 3.0 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.0 3.6 1.4 0.2 setosa 6 5.4 3.9 1.7 0.4 setosa De esta manera solicitamos a la base que nos entregue los datos de la tabla, y los cargamos en la sesión de R como un dataframe. Desde este punto podemos usar los datos normalmente, pero a continuación veremos una forma mejor de hacerlo 🫢\nProcesar desde la base de datos con {dplyr} Una de las ventajas de una base de datos es solicitar y obtener datos procesados desde la base de datos 🏭 Normalmente esto se hace con el lenguaje SQL, pero en R hay formas más sencillas: usando directamente {dplyr} para interactuar con la base de datos.\nPara ello instalamos {dbplyr}, paquete que traducirá el código de {dplyr} en SQL.\ninstall.packages(\u0026#34;dbplyr\u0026#34;) Esta funcionalidad se carga automáticamente al cargar {dplyr}.\nPara obtener datos desde la base de datos, usamos tbl() con el objeto de conexión y el nombre de la tabla, y recibiremos los datos en el formato tibble de {dplyr}:\nlibrary(dplyr) datos_db \u0026lt;- tbl(db_con, \u0026#34;prueba\u0026#34;) datos_db # Source: table\u0026lt;\u0026quot;prueba\u0026quot;\u0026gt; [?? x 5] # Database: postgres [postgres@db.ybqnpazmjvictxbevqqk.supabase.co:5432/postgres] Sepal.Length Sepal.Width Petal.Length Petal.Width Species \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 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 # ℹ more rows Esta tabla generada desde la base de datos no es un dataframe normal 😱 sino que es una conexión abierta con la base de datos:\nclass(datos_db) [1] \u0026quot;tbl_PqConnection\u0026quot; \u0026quot;tbl_dbi\u0026quot; \u0026quot;tbl_sql\u0026quot; \u0026quot;tbl_lazy\u0026quot; [5] \u0026quot;tbl\u0026quot; Esto nos permite seguir pidiéndole instrucciones que se procesarán en la base, no en nuestra sesión local de R. Esto es lo que nos permite realizar cálculos con datos de grandes volúmenes o manejar bases de datos gigantes que no caben en la memoria de tu computador 😲\nUsemos la tabla obtenida desde la base de datos para hacer un filtro y calcular un promedio:\ncalculo_db \u0026lt;- datos_db |\u0026gt; filter(Species == \u0026#34;setosa\u0026#34;) |\u0026gt; summarise(mean_sepal_length = mean(Sepal.Length)) Estas operaciones se realizan remotamente en la base, y con una evaluación perezosa 🦥 (lazy evaluation); es decir, solamente se calcula el resultado cuando es estrictamente necesario. Entonces, recién al solicitar el resultado la base de datos va a realizar el cálculo requerido:\ncalculo_db Warning: Missing values are always removed in SQL aggregation functions. Use `na.rm = TRUE` to silence this warning This warning is displayed once every 8 hours. # Source: SQL [?? x 1] # Database: postgres [postgres@db.ybqnpazmjvictxbevqqk.supabase.co:5432/postgres] mean_sepal_length \u0026lt;dbl\u0026gt; 1 5.01 Podemos confirmar que el cálculo se hizo en la base de datos usando show_query() para ver el código SQL que se usó para hacer el cálculo:\ncalculo_db |\u0026gt; show_query() \u0026lt;SQL\u0026gt; SELECT AVG(\u0026quot;Sepal.Length\u0026quot;) AS \u0026quot;mean_sepal_length\u0026quot; FROM ( SELECT \u0026quot;prueba\u0026quot;.* FROM \u0026quot;prueba\u0026quot; WHERE (\u0026quot;Species\u0026quot; = 'setosa') ) AS \u0026quot;q01\u0026quot; 🪄✨ Magia! Es como si hubiéramos aprendido SQL: combinamos el poder de SQL con la conveniencia de R.\nSi necesitamos traer los datos desde la base a tu computador, los cargamos en la memoria con la función collect():\ndatos_df \u0026lt;- datos_db |\u0026gt; filter(Species == \u0026#34;setosa\u0026#34;) |\u0026gt; collect() Ahora sí que sí el objeto datos_df se encuentra cargado en tu sesión local de R como un dataframe normal.\nComo los cálculos que hagamos sobre los datos se traducen desde R a SQL y luego se nos entregan, un uso importante de collect() es para anteponerlo a operaciones que no tienen traducción a SQL.\nPor ejemplo, me consta que las funciones de {forcats} no se traducen a SQL:\ndatos_db |\u0026gt; group_by(Species) |\u0026gt; summarise(Sepal.Width = mean(Sepal.Width)) |\u0026gt; mutate(Species = forcats::fct_reorder(Species, Sepal.Width)) Error in `forcats::fct_reorder()`: ! No known SQL translation En este caso, anteponemos collect() para pasar de SQL a R en medio del proceso y listo.\ndatos_db |\u0026gt; group_by(Species) |\u0026gt; summarise(Sepal.Width = mean(Sepal.Width)) |\u0026gt; collect() |\u0026gt; # cargar a la memoria mutate(Species = forcats::fct_reorder(Species, Sepal.Width)) |\u0026gt; arrange(Species) # A tibble: 3 × 2 Species Sepal.Width \u0026lt;fct\u0026gt; \u0026lt;dbl\u0026gt; 1 versicolor 2.77 2 virginica 2.97 3 setosa 3.43 De esta forma, aprovechamos la base de datos SQL lo más posible, y cuando necesitemos operaciones más complejas nos pasamos a R! 😉\nYa sea porque necesitas la capacidad de almacenamiento, porque requieres procesamiento optimizado de grandes volúmenes de datos, o porque vas a usar tus datos en una aplicación o plataforma, aprender a manejar bases de datos es una habilidad crucial para escalar tus capacidades y pasar al siguiente nivel ✨\nAvanzado Conexión por IPv4 Las bases de Supabase usan IPv6 por defecto, pero algunos servicios requieren de conexiones IPv4 por limitaciones técnicas o de infraestructura. Uno de estos servicios es ShinyApps.io, el servicio que uso yo para alojar mis aplicaciones Shiny.\nPara usar IPv4 en conexiones a bases de datos Supabase, en el panel de Conexión (el mismo que usaste para obtener las credenciales), en la opción Method puedes elegir Session pooler para obtener credenciales IPv4.\nRecursos A Crash Course on PostgreSQL for R Users, por Mauricio Vargas S. También se puede correr la base de datos en tu mismo computador, pero eso lo dejamos para otro post 🤓\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nPostgres es un sistema de gestión de bases de datos relacionales , uno de los más usados, pero existen muchos otros más, como SQLite, MariaDB, y más.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-11-06T00:00:00Z","excerpt":"Coloquialmente se usa el término _base de datos_ para referirse a datos que están en Excel. Pero en realidad una base de datos es algo distinto: un sistema de almacenamiento y procesamiento de datos que puede contener múltiples tablas, alojado en un computador, servidor o en la nube, que puede entregar datos de forma rápida y eficiente de acuerdo a las solicitudes que se le hagan. En este post veremos cómo crear una base de datos **gratuita**, cómo conectarnos a ella desde R, a leer y escribir tablas, y procesar datos desde la base de datos remota.","href":"https://bastianoleah.netlify.app/blog/db_supabase/","tags":"datos ; optimización ; dplyr","title":"Crear y conectarse a una base de datos en R"},{"content":" En una aplicación Shiny, cada interacción que la/el usuario hace suele conllevar un cálculo de R que se ejecuta para producir un output, como una tabla o un gráfico. Dependiendo de la cantidad de datos, de la complejidad del cálculo o del output, estas interacciones pueden tomar más tiempo del esperado.\nUna forma de optimizar la velocidad de una aplicación Shiny es implementando un cache. Un cache significa que los resultados de los cálculos realizados por la app se guardan, de modo que si el cálculo se repite, el resultado se obtiene desde el cache, sin necesidad de volver a ejecutar el cálculo. En la mayoría de los casos esto significa una gran mejora de velocidad! 🚀\nConsideraciones previas Antes que nada, hay que ver si es posible (o si vale la pena) implementar un cache para cada situación.\n¿Cuándo agregar un cache?\nSi los cálculos son repetitivos; es decir, que uno o varios usuarios pueden volver a solicitar el mismo cálculo varias veces. Si los cálculos son determinísticos; es decir, que para los mismos inputs siempre se produzca el mismo output. No tiene sentido si el cálculo implica azar o variables fuera del control de la app, como la fecha u hora. Si la cantidad de inputs que definen el cálculo es baja, porque si son demasiados inputs, no tiene sentido agregar un cache para interacciones que escasamente se van a repetir. Si los inputs son limitados, ya que si son muy específicos (como un campo de texto o el subir un archivo) tampoco tiene sentido cachear un input que no se va a repetir. Implementando el cache Con la función bindCache() podemos hacer que un output (gráficos, tablas, textos dinámicos) o un objeto reactivo (creado con reactive() y que se actualiza según su dependencia de inputs u otros objetos reactivos) guarden sus resultados en un cache.\ncalculo_lento \u0026lt;- reactive({ # cálculo que demora mucho }) |\u0026gt; bindCache() Al especificar que queremos cachear un objeto/output, también debemos que explicitar sus dependencias: los inputs u objetos cuyo valor hace que el cálculo cambie.\ncalculo_lento \u0026lt;- reactive({ # cálculo que demora mucho }) |\u0026gt; bindCache(input$selector_1, input$selector_2) En este ejemplo, el objeto reactivo calculo_lento depende de los valores de input$selector_1 e input$selector_2, definidos por el/la usuario/a de la app.\nCuando un usuario/a elija estos inputs y por consiguiente obtenga un resultado de calculo_lento(), el cache se guardará para la combinación de valores de estos dos inputs.\nEntonces: si el usuario/a selecciona una combinación que ya ha sido calculada antes, se omite el cálculo y el resultado se carga desde el cache, y si es una combinación nueva, el resultado se calcula y se guarda en el cache para futuras solicitudes.\nComparación de desempeño En este ejemplo, vemos una aplicación Shiny con varios outputs, los cuales tienen agregado bindCache().\nEn la primera ejecución, la app demora aproximadamente 3 segundos en cargar los outputs por primera vez:\nPero en una segunda ejecución, la carga es casi instantánea, dado que los resultados de los cálculos estaban preguardados:\nUna vez que las combinaciones se cargaron, las siguientes veces que se soliciten la carga es mucho más rápida! 🔥\nEn este ejemplo, el bindCache() se puso en cada output, en la misma función render_x() que general el objeto output$grafico_x, con una dependencia al input$region que hace cambiar los gráficos. Entonces, cuando la/el usuaria/o cambia el input, Shiny se salta todos los cálculos y carga el output correspondiente. Pero puedes experimentar con poner el bindCache() en distintos lugares que puedan ser cuello de botella de tu app.\nConfigurar el almacenamiento del cache Por defecto, el cache se guarda en memoria, lo que significa que al cerrar la aplicación, el cache se pierde. Si queremos que el cache persista entre sesiones, podemos configurar el almacenamiento del cache en disco, agregando la siguiente línea en el archivo app.R:\nshinyOptions(cache = cachem::cache_disk(\u0026#34;app_cache/cache\u0026#34;)) De este modo, el cache quedará guardado en la carpeta app_cache/cache de la app, y podremos ver en tiempo real que los archivos del cache se van generando a medida que los usuarios interactúan con la app.\nEsto entrega el beneficio de poder ampliar el cache a múltiples usuarios, porque si varios usuarios solicitan el mismo cálculo, el segundo usuario en adelante obtendrá el resultado desde el cache en disco. También te permite evitar que el primer usuario tenga que esperar la carga de los elementos, al poder subir una app que ya venga con su cache generado.\nOtro beneficio es que te entrega el control sobre el cache, en el sentido que simplemente puedes borrar los archivos para obligar que Shiny re-calcule los outputs. Esto sirve para ayudarte cuando cambies el código pero no veas cambios y después te des cuenta que era porque los gráficos se estaban cargando desde el cache 😅\nBonus: generando el cache de antemano Si quieres evitar que el primer usuario de tu app tenga que esperar a que se generen los cálculos y el cache, puedes generar el cache de antemano. Para esto simplemente configuras que el cache se guarde en el disco, y manualmente exploras las combinaciones de inputs de tu app\u0026hellip; lo cual sería bastante aburrido de hacer.\nPuedes combinar la generación de cache con el testeo de aplicaciones Shiny con {shinytest2} para automatizar el uso de tu aplicación, con el doble objetivo de probar que cada interacción de tu app funcione correctamente, y en tanto la app es puesta a prueba, generar un cache de todos los cálculos sin tener que esperar que el usuario realice las interacciones manualmente.\nRecursos Guía oficial de Posit para optimizar aplicaciones Shiny ","date":"2025-11-04T00:00:00Z","excerpt":"Mejora la velocidad de carga de tus aplicaciones Shiny aplicando una sola función a tus outputs. En este post veremos la función `bindCache()` que permite que los resultados de los cálculos de tu app se guarden, evitando que deban volver a calcularse y por consiguiente acelerando significativamente tus aplicaciones.","href":"https://bastianoleah.netlify.app/blog/shiny_optimizar/","tags":"shiny ; optimización","title":"Optimiza la velocidad de tus aplicaciones Shiny con bindCache()"},{"content":"Si eres usuario/a de macOS y de R, aquí va un truquito para facilitar tu trabajo.\nEn el Finder puedes crear carpetas inteligentes, que muestran automáticamente archivos que cumplen ciertos criterios, intependiente de donde estén en tu computador. Básicamente son carpetas que guardan resultados de búsqueda que se mantienen actualizados.\nCon esto podemos crear una carpeta que muestre todos tus proyectos de R, para poder acceder a ellos más rápido.\nEn el Finder, vamos a Archivo y creamos una nueva carpeta inteligente.\nEn la barra de búsqueda, escribimos Rproj, que es la extensión de los proyectos de R, y especificamos El nombre contiene\u0026hellip; para sólo buscar nombres de archivo que coincidan (y no, por ejemplo, archivos de texto que contengan esa palabra).\nLuego, en la barra de arriba, seleccionamos \u0026ldquo;Este Mac\u0026rdquo; si queremos buscar en todo el computador, o elegimos otra carpeta si queremos restringir la búsqueda.\nEn este punto ya debería aparecer el resultado dentro de la carpeta. Para guardar esta carpeta inteligente, hacemos clic en el botón Guardar en la esquina superior derecha, y elegimos dónde guardarla.\nFinalmente podemos agregar la carpeta al Dock para tener un acceso rápido a todos los proyectos de R, y si hacemos clic derecho podemos especificar que se ordenen por fecha de modificación, que aparezcan como abanico, y que el ícono se vea como carpeta.\n","date":"2025-10-29T00:00:00Z","excerpt":"Si eres usuario/a de macOS y de R, aquí va un truquito para facilitar tu trabajo: crear un ícono en tu Dock que muestre todos tus proyectos de R, para poder acceder a ellos más rápido.","href":"https://bastianoleah.netlify.app/blog/2025-10-29/","tags":"consejos","title":"Crea una carpeta inteligente con todos tus proyectos de R en Mac"},{"content":"git es una herramienta para el control de versiones de código, respaldo de código, y colaboración. En otro post hice un tutorial para aprender a usar git y GitHub con R y RStudio. En este post voy a mantener una lista1 de los comandos que más uso en git, así como los comandos que necesito para resolver los problemas más frecuentes.\nOtro recurso mucho más completo es ohshitgit.com, que como su nombre indica, está enfocado en resolver problemas comunes con git.\nEste post está en construcción, y a medida que me encuentro con problemas o voy aprendiendo iré agregando y desarrollando. Ten mucho cuidado al ejecutar estos u otros comandos de git, ya que pueden afectar tus archivos. Recuerda siempre tener respaldos! Comandos git básicos Crear repositorio git en proyecto de RStudio git init usethis::use_git() Para más información sobre la integración de R con git, revisa este post. También existe el libro Happy Git with R, que detalla todos los pasos necesarios para poder usar git con R, incluyendo soluciones a problemas comunes.\nVer estado del repositorio Para ver qué archivos han sido modificados, cuáles están en el área de preparación, y cuáles no:\ngit status Agregar archivos al área de preparación (staging area) Cuando creaste o modificaste archivoc, y quieres registrarlos para ser agregados a la nueva versión del proyecto:\ngit add archivo.R Para agregar todos los archivos cambiados desde el último commit:\ngit add . Guardar los cambios preparados en una versión (commit) Un commit es la operación en la que tomas los archivos del área de preparación y los guardas como una nueva versión del proyecto. Siempre hay que agregar un mensaje que describa los cambios de esta versión:\ngit commit -m \u0026#34;script de procesamiento de datos actualizado\u0026#34; Cambiar un mensaje de commit Por si te equivocaste en el mensaje de commit, te permite cambiar el mensaje, siempre que lo ejecutes antes de haber hecho push\ngit commit --amend -m \u0026#34;nuevo mensaje de commit\u0026#34; Ver todos los commits que has hecho Te entrega una lista con las versiones de tu repositorio, con sus mensajes respectivos y el índice de cada uno de ellos (un código único que identifica cada cambio)\ngit log También está el comando git reflog, que muestra un historial de todos los movimientos en el repositorio, incluyendo commits, cambios de ramas, y otras operaciones.\ngit reflog Subir los cambios guardados al repositorio remoto Asumiendo que tu proyecto de RStudio tiene un repositorio git, que has hecho commit de tus cambios, y que ya conectaste el repositorio local con un repositorio remoto en GitHub o GitLab:\ngit push Si no has creado una versión remota de tu repositorio aún, puedes usar el comando use_github() para crear una rápidamente.\nusethis::use_github() Solucionar problemas Deshacer git add archivo.R Por si la embarraste y agregaste un archivo incorrecto a la zona de preparación:\ngit rm archivo.R Deshacer git add . git reset Deshacer commit Si guardaste una versión del código pero ésta tenía archivos equivocados, puedes deshacer el commit pero sin perder los cambios que hiciste. Con esto, tu repositorio volverá a la versión antes del commit, pero el código y archivos nuevos no se perderán, y estarán disponibles para volver a agregarlos con git add y rehacer la versión con git commit.\ngit reset --soft HEAD~1 Eliminar archivos agregados con git add después del commit Para cuando agregaste archivos al commit pero luego te das cuenta que no debías haberlo hecho. Por ejemplo, si agregaste un archivo muy grande por error:\ngit rm --cached \u0026#34;archivo\u0026#34; git commit -m \u0026#34;mensaje\u0026#34; git push -u origin branch Recuperar cambios Volver al último commit Borra el último commit, perdiendo tus cambios locales.\ngit reset --hard HEAD Volver a una versión anterior de tu código Busca el índice de la versión a la cual quieres volver, y usa git reset para regresar a esa versión:\ngit reflog git reset HEAD@{index} Forzar cambios Forzar push Por si no te deja subir los cambios, pero estás segurx de que quieres sobreescribir el repositorio remoto con tu versión local:\ngit push origin main --force Forzar pull Cuando la versión del código que te importa es la remota (en GitHub o equivalente) y deseas descartar los cambios locales, por ejemplo, si hiciste cambios locales pero no puedes subirlos porque olvidaste ahcer pull antes):\ngit fetch --all git branch backup-main git reset --hard origin/main Ramas Aprende a hacer ramas online aquí\nHago esto porque tengo una nota en el computador con todos los comandos y siempre vuelvo a buscarla para copiarlos, así que mejor dejo todo eso acá 😋\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-10-27T00:00:00Z","excerpt":"Colección de comandos de `git` para realizar acciones comunes y resolver problemas frecuentes. `git` es una herramienta para el control de versiones de código, respaldo de código, y colaboración.","href":"https://bastianoleah.netlify.app/blog/git_comandos/","tags":"git ; programación","title":"Comandos comunes de Git"},{"content":"El viernes 22 de agosto de 2025 tuve el honor de participar en las clases magistrales del Diplomado de Ciencia de Datos para Ciencias Sociales y Humanidades de la Facultad de Estudios Superiores Acatlán, Universidad Nacional Autónoma de México (UNAM).\nMe pidieron hacer un taller acerca de cómo crear una marca personal como científico de datos, pero dándole unas vueltas a esa idea, terminé hablando sobre tres principios: crear, diferenciarse y compartir.\nEn la clase hablo de mi trayectoria, mi salto desde la sociología al análisis de datos, y voy dando consejos (mezclados con ejemplos de código) para tomar una postura participativa y solidaria con respecto a la programación, el trabajo con datos, y la creación de comunidades en torno al código abierto.\nGrabación Diapositivas Para la ocasión hice unas diapositivas con Quarto que también puedes revisar aquí.. El código de las diapositivas está en este repositorio.\nResúmen Las ideas principales del taller fueron:\nMantenerse creando desde la experticia, la experiencia, y la relevancia social ✊🏼 Destacar respecto del resto diferenciándose con contenido inclusivo, atractivo, novedoso y con personalidad 💎 Intentar compartir y participar lo más posible para mantener viva la comunidad datera, ayudar a otrxs a aprender, y dar a cambio lo que hemos recibido gracias al código abierto y los datos abiertos 🎁 Recibí muy lindos comentarios de las y los participantes, que agradezco de corazón 💜\nLas y los invito a crear su propio lugar en la internet, destinado a visibilizar su trabajo y compartir sus aprendizajes!\nEnlaces Portafolio de aplicaciones Shiny de análisis de datos Tutorial Git y GitHub desde R Tutorial para crear blog con Quarto Bonus Fotos mías vendiéndola durante la clase:\n","date":"2025-10-27T00:00:00Z","excerpt":"El viernes 22 de agosto participé de las clases magistrales del Diplomado de Ciencia de Datos para Ciencias Sociales y Humanidades de la UNAM. En la clase hablo de mi trayectoria, mi salto desde la sociología al análisis de datos, y voy dando consejos (mezclados con ejemplos de código) para tomar una postura participativa y solidaria con respecto a la programación, el trabajo con datos, y la creación de comunidades en torno al código abierto.","href":"https://bastianoleah.netlify.app/blog/taller_datos_unam/","tags":"videos ; consejos ; quarto ; github ; blog","title":"Taller: Compartir y colaborar desde el cruce entre las ciencias de datos y las ciencias sociales"},{"content":" Como Chile es un país tan largo, a veces cuesta publicar mapas que se vean bien por el espacio que requiere. En otro post intentamos resolver esto girando Chile para que quede horizontal o acostado. Ahora veremos otra opción: partir Chile en tres secciones, norte centro y sur, que podemos disponer lado a lado para ocupar mejor el espacio.\nÍndice Descargar mapas Cargar mapa Visualizar mapa Cortar mapa de Chile continental Calcular recortes Cortar mapas Unir mapas Descargar mapas En el siguiente botón descargaremos un shapefile de Chile por regiones, que proviene de la Mapoteca de la Biblioteca del Congreso Nacional. También puedes descargarlo en el script con download.file().\nDivision regional: polígonos de las regiones de Chile Una vez descargado, descomprimimos el archivo (podemos usar unzip()) y obtendremos una carpeta. Como siempre, es importante que estemos trabajando en un proyecto de RStudio, y que guardemos los datos y shapes dentro de la carpeta del proyecto.\nPuedes hacer esto mediante código:\n# descargar download.file(\u0026#34;https://www.bcn.cl/obtienearchivo?id=repositorio/10221/10398/2/Regiones.zip\u0026#34;, \u0026#34;Regiones.zip\u0026#34;) # descomprimir unzip(\u0026#34;Regiones.zip\u0026#34;, exdir = \u0026#34;Regiones\u0026#34;) Cargar mapa Cargamos el shape con la función read_sf() del paquete {sf}. Si es tu primera vez haciendo mapas, revisa esta guía.\nlibrary(sf) mapa \u0026lt;- read_sf(\u0026#34;Regiones\u0026#34;) Visualizar mapa Primero veamos el mapa tal como viene desde el shapefile:\nlibrary(ggplot2) mapa |\u0026gt; ggplot() + geom_sf() + theme_minimal() Aquí se nota al tiro el problema: el país es tan delgado y alto 🫦 que si contamos con poco espacio vertical, no se notan los detalles y dejamos márgenes sin uso.\nSi la visualización se demora mucho en generar en tu computador, prueba simplificando los detalles del mapa.\nCortar mapa de Chile continental Un primer ajuste para la visualización es cortar los márgenes del mapa para enfocarnos en Chile continental. Perdón, Rapa Nui 😔\nmapa |\u0026gt; # reproyectar st_transform(crs = 5360) |\u0026gt; # visualizar ggplot() + geom_sf() + # recortar Chile continental coord_sf(expand = FALSE, xlim = c(-79, -62), ylim = c(-56.2, -17.3)) + theme_minimal(base_size = 8) Uno de los pasos para el recorte fue reproyectar el mapa, dado que vienen con un sistema de coordenadas distinto al que típicamente se usa. Al reproyectar, las coordenadas pasan a estar en grados decimales.\nCalcular recortes Ahora toca sacar la calculadora (también se puede hace a mano, pero acá nos gusta el código reproducible y sobrecomplejizar todo) 🤓\nLa idea es: medir cuánto mide cada sección del país, y calcular los cortes que hay que hacerle al mapa para dividirlo en tres.\nPrimero tomamos el límite superior o norte del país, y el límite sur, y calculamos el largo en término de grados de latitud. Dividimos ese largo en tres partes para saber cuánto va a medir cada una de las tres secciones.\nlimite_sur = -56.2 limite_norte = -17.3 largo = abs(limite_sur) - abs(limite_norte) parte = largo / 3 Obtenemos que cada sección medirá 12.9666667 grados de latitud.\nSi empezamos desde el norte del país (n, que equivale a -17.3), la primera sección llegará hasta n - parte (-30.3). Luego la segunda sección empieza donde termina la anterior, y llega hasta n - parte*2 (-43), y la tercera sección llegará hasta n - parte*2 (-56.2), que coincide con el límite sur del país.\nsecciones norte sur 1 n n-p 2 n-p n-p*2 3 n-p*2 n-p*3 Podemos calcular esto a mano, o hacerlo al ojo, pero vamos a creamor una tablita mejor.\nlibrary(dplyr) cortes \u0026lt;- tibble(secciones = c(1, 2, 3)) cortes \u0026lt;- cortes |\u0026gt; mutate(norte = limite_norte, sur = limite_norte) cortes # A tibble: 3 × 3 secciones norte sur \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 1 -17.3 -17.3 2 2 -17.3 -17.3 3 3 -17.3 -17.3 cortes \u0026lt;- cortes |\u0026gt; mutate(norte = norte - (parte * (secciones - 1)), sur = sur - (parte * secciones)) cortes # A tibble: 3 × 3 secciones norte sur \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 1 -17.3 -30.3 2 2 -30.3 -43.2 3 3 -43.2 -56.2 secciones norte sur 1 -17.30000 -30.26667 2 -30.26667 -43.23333 3 -43.23333 -56.20000 Cortar mapas Creamos un mapa base, y definimos un objeto que contiene los cortes horizontales, que en este caso serán iguales para los tres mapas.\nmapa_base \u0026lt;- mapa |\u0026gt; # reproyectar st_transform(crs = 5360) |\u0026gt; # visualizar ggplot() + geom_sf() + theme_minimal(base_size = 8) limites_continental \u0026lt;- c(-78, -64) Ahora, usando el mapa_base, aplicamos el corte de coordenadas con coors_sf() a partir de la tabla cortes, seleccionando la columna de la tabla que corresponde (norte o sur), y la posición del elemento para cada sección (1, 2 o 3).\nmapa_norte \u0026lt;- mapa_base + coord_sf(expand = FALSE, xlim = limites_continental, ylim = c(cortes$norte[1], cortes$sur[1])) mapa_centro \u0026lt;- mapa_base + coord_sf(expand = FALSE, xlim = limites_continental, ylim = c(cortes$norte[2], cortes$sur[2])) mapa_sur \u0026lt;- mapa_base + coord_sf(expand = FALSE, xlim = limites_continental, ylim = c(cortes$norte[3], cortes$sur[3])) Unir mapas El paso final es unir los tres gráficos: como detallamos en un post anterior, con el paquete {patchwork} podemos unir y combinar gráficos de {ggplot2}, y la sintaxis es muy simple: si conectas los gráficos con el signo + se unirán lado a lado, si los divides con / aparecerán uno sobre otro. Con el signo \u0026amp; puedes agregar capas a todos los gráficos de una.\nlibrary(patchwork) mapa_norte + mapa_centro + mapa_sur + plot_annotation(title = \u0026#34;Mapa de Chile\u0026#34;, subtitle = \u0026#34;dividido en tres secciones\u0026#34;) \u0026amp; theme(axis.text.x = element_blank()) Listo! Ahora el mapa cabe mucho mejor en una hoja, página o imagen.\nTambién podemos ajustar los cortes horizontales del mapa, para que cada sección tenga un espaciado similar. Acá dejo el código con los números exactos para copiar y pegar, o bien, revisa el script completo en este Gist.\nmapa_norte \u0026lt;- mapa_base + coord_sf(expand = FALSE, xlim = c(-74, -65), ylim = c(-17.3, -30.2)) mapa_centro \u0026lt;- mapa_base + coord_sf(expand = FALSE, xlim = c(-76, -68), ylim = c(-30.2, -43.2)) mapa_sur \u0026lt;- mapa_base + coord_sf(expand = FALSE, xlim = c(-78, -65), ylim = c(-43.2, -56.2)) mapa_norte + mapa_centro + mapa_sur + plot_annotation(title = \u0026#34;Mapa de Chile\u0026#34;, subtitle = \u0026#34;dividido en tres secciones\u0026#34;) \u0026amp; theme(axis.text.x = element_blank()) ","date":"2025-10-23T00:00:00Z","excerpt":"Como Chile es un país tan largo, a veces cuesta publicar mapas que se vean bien por el espacio que requiere. En otro post [intentamos resolver esto girando Chile para que quede horizontal o _acostado_](/blog/mapa_chile_horizontal/). Ahora veremos otra opción: partir Chile en tres secciones, norte centro y sur, que podemos disponer lado a lado para ocupar mejor el espacio.","href":"https://bastianoleah.netlify.app/blog/mapa_chile_triple/","tags":"mapas ; Chile","title":"Visualizar un mapa de Chile separado en tres secciones"},{"content":"Cuando estamos limpiando datos frecuentemente nos preguntamos cuántos datos perdidos tiene una columna. La respuesta se obtiene pidiendo un resumen (summarize()) que cuente la suma de datos perdidos (sum(is.na())) en una columna específica.\nlibrary(dplyr) # contar datos perdidos en una columna iris |\u0026gt; summarize( missings = sum(is.na(Sepal.Length)) ) missings 1 0 ¿Pero qué pasa si tienes muchas columnas y quieres revisarlo en todas? No vas a andar escribiendo las columnas una por una\u0026hellip; 😵‍💫\nUsando las función across() de {dplyr}, podemos aplicar la misma operación a todas las columnas de un dataframe, y así obtener el conteo de datos perdidos de todas las columnas.\nPrimero creemos datos perdidos al azar con el paquete {messy} para ensuciar datos:\nlibrary(messy) iris_m \u0026lt;- messy::make_missing(iris) Ahora con summarize() calculamos un resumen de datos (es decir, reducir las filas de la tabla a un sólo resultado), especificando con across() que el resumen será a través de todas las columnas (everything()), y finalmente pedimos que el resumen sea la suma (sum()) de los datos de cada columna (.x) que son missing is.na().\nlibrary(dplyr) iris_m |\u0026gt; # resumir los datos summarize( # a través de todas las columnas across( everything(), # contar la cantidad de missings ~sum(is.na(.x)) ) ) Sepal.Length Sepal.Width Petal.Length Petal.Width Species 1 15 17 10 12 21 El mismo código sirve si quieres contar todos los ceros, o todos los valores infinitos, etc.\n# contar los ceros iris_m |\u0026gt; summarize( across(everything(), ~sum(.x == 0, na.omit = TRUE) )) Sepal.Length Sepal.Width Petal.Length Petal.Width Species 1 NA NA NA NA NA # contar infinitos iris_m |\u0026gt; summarize( across(everything(), ~sum(is.infinite(.x)) )) Sepal.Length Sepal.Width Petal.Length Petal.Width Species 1 0 0 0 0 0 # etc Si quieres saber los datos perdidos sólo en las columnas numéricas, en vez de everything() puedes usar where(is.numeric), o si necesitas saberlo en un conjunto de columnas específicas, usa starts_with() o pon sus nombres dentro de c().\nTambién hay otras formas de revisar datos perdidos, como por ejemplo con el paquete {visdat} que resume tus datos visualmente.\n","date":"2025-10-21T00:00:00Z","excerpt":"Cuando estamos limpiando datos frecuentemente nos preguntamos cuántos datos perdidos tiene una columna. La respuesta se obtiene pidiendo un resumen (`summarize()`) que cuente la suma de datos perdidos (`sum(is.na())`) en una columna específica. ¿Pero qué pasa si tienes muchas columnas? No vas a andar escribiendo las columnas una por una... 😵‍💫 Usando las función `across()` de `{dplyr}`, podemos aplicar la misma operación a todas las columnas de un dataframe, y así obtener el conteo de datos perdidos de todas las columnas.","href":"https://bastianoleah.netlify.app/blog/2025-10-21/","tags":"datos perdidos ; limpieza de datos","title":"Contar datos perdidos en una o varias columnas"},{"content":"Cuando desarrollamos reportes, gráficos o aplicaciones en R, necesitamos hacer que nuestros textos se adapten a los datos que estamos analizando.\nSi tenemos que poner texto sobre una cantidad de elementos, tenemos que escribir en singular o plural dependiendo de la cantidad (1 gato, 2 gatos).\nPor ejemplo, si tenemos un objeto cantidad que contiene el número de gatos, y queremos escribir un texto que diga \u0026ldquo;gato\u0026rdquo; o \u0026ldquo;gatos\u0026rdquo; dependiendo de la cantidad, podemos usar la función ifelse():\ncantidad \u0026lt;- 12 singular \u0026lt;- \u0026#34;gato\u0026#34; plural \u0026lt;- \u0026#34;gatos\u0026#34; texto \u0026lt;- ifelse(cantidad == 1, yes = singular, no = plural) texto [1] \u0026quot;gatos\u0026quot; Para evitar repetir el código cada vez que necesitemos escribir en singular o plural, creamos una función llamada plural(), que recibe como argumentos la cantidad n, y luego el texto en singular y en plural, y dependiendo de la cantidad se retornará el texto apropiado:\nplural \u0026lt;- function(n, singular, plural) { ifelse(n == 1, singular, plural) } plural(1, \u0026#34;gato\u0026#34;, \u0026#34;gatos\u0026#34;) [1] \u0026quot;gato\u0026quot; plural(3, \u0026#34;gato\u0026#34;, \u0026#34;gatos\u0026#34;) [1] \u0026quot;gatos\u0026quot; Podemos mejorar esta función agregando una validación para chequear que la cantidad sea realmente un número, y podemos mejorar el ifelse() haciendo que el argumento plural sea opcional, y si no se entrega, la función agregue una \u0026ldquo;s\u0026rdquo; al final del texto en singular para formar el plural.\nplural \u0026lt;- function(n, singular, plural = NULL) { # validación if (!is.numeric(n)) stop(\u0026#34;se requiere un valor numérico\u0026#34;) # si la cantidad es 1, singular ifelse(n == 1, singular, # si la cantidad es mayor a 1 ifelse( # pero no se definió el plural, agregar \u0026#34;s\u0026#34; is.null(plural), paste0(singular, \u0026#34;s\u0026#34;), # de lo contrario, usar el plural plural) ) } plural(3, \u0026#34;gato\u0026#34;) [1] \u0026quot;gatos\u0026quot; plural(1, \u0026#34;gato\u0026#34;) [1] \u0026quot;gato\u0026quot; El uso de esta función sería más o menos algo así:\nn \u0026lt;- 23 paste(\u0026#34;Existe un total de\u0026#34;, n, plural(n, \u0026#34;gato\u0026#34;), \u0026#34;en la vivienda.\u0026#34;) [1] \u0026quot;Existe un total de 23 gatos en la vivienda.\u0026quot; Para adecuarnos a la posibilidad de que la cantidad sea cero, creamos una nueva función que use la función anterior plural() si la cifra es igual o mayor a 1, o si no, retorna un texto distinto.\nn \u0026lt;- 4 cantidad \u0026lt;- function(n, singular) { # si la cantidad es missing o cero ifelse(is.na(n) | n == 0, # anteponer \u0026#34;ningún\u0026#34; a la cifra paste(\u0026#34;ningún\u0026#34;, singular), # si la cantidad es 1 o más, usar la función plural # anteponiento la cifra paste(n, plural(n, singular)) ) } Probemos distintos casos:\nn \u0026lt;- 6 cantidad(n, \u0026#34;gato\u0026#34;) [1] \u0026quot;6 gatos\u0026quot; n \u0026lt;- 1 cantidad(n, \u0026#34;gato\u0026#34;) [1] \u0026quot;1 gato\u0026quot; n \u0026lt;- NA cantidad(n, \u0026#34;gato\u0026#34;) [1] \u0026quot;ningún gato\u0026quot; Todo lo anterior fue una excusa para ✨aprender✨, porque existe una mejor opción: usar la función pluralize() del paquete {cli}, que soluciona el problema (a cambio de tener que cargar un paquete más).\nlibrary(cli) cantidad \u0026lt;- 3 pluralize(\u0026#34;Total de {cantidad} gato{?s}.\u0026#34;) Total de 3 gatos. cantidad \u0026lt;- 1 pluralize(\u0026#34;Total de {cantidad} gato{?s}.\u0026#34;) Total de 1 gato. ","date":"2025-10-21T00:00:00Z","excerpt":"Cuando desarrollamos reportes, gráficos o aplicaciones en R, necesitamos hacer que nuestros textos se adapten a los datos que estamos analizando. En este post veremos cómo hacer que el texto se adapte según las cantidades para redactar palabras en singular o plural.","href":"https://bastianoleah.netlify.app/blog/2025-10-21b/","tags":"texto","title":"Redactar palabras en plural en R"},{"content":" Índice Instalación Configurar un proveedor de IA/modelo de lenguaje Opción A: instalar un modelo de lenguaje local desde R Opción B: configurar un proveedor de IA en la nube Guardar configuración del modelo Configurar atajo de teclado Usar {gander} Ejemplo de uso {gander} es un paquete de R para integrar asistentes de inteligencia artificial (IA) directamente en RStudio. Solo con presionar una combinación de teclas, podrás pedirle a un asistente con tus propias palabras que escriba código, corrija tu código, o incluso que genere gráficos.\nSe trata de una herramienta capaz de acelerar tu trabajo escribiéndote código, ayudándote a salir de bloqueos mentales, y solucionando problemas en los que usualmente perdemos tiempo tipeando.\nPara qué sirve: selecciona código en RStudio, presiona una combinación de teclas, y se abrirá una ventana donde puedes pedir a una IA que corrija tu código, que escriba código por tí, y más. Tendrás un asistente de IA a tu alcance para ayudarte a mejorar tu código y escribir más rápido! Pasos a seguir: tienes que configurar su acceso a un modelo de lenguaje (como OpenAI, Anthropic, Copilot, o un modelo local como Llama), luego configurar el atajo de teclado para invocarlo, y listo! Cómo funciona: en vez de tener que copiar tu código e ir pidiéndole a una IA que te ayude, {gander} se conecta directamente a la IA para entregarle lo que le pides, y te lo entrega directo en tu código. Pero además le entrega al modelo información contextual sobre tu proyecto que hará que el código recibido se adecúe a tu entorno de R. Esto significa que el modelo sabrá los objetos que tienes cargados, sus columnas, y más, por lo que las respuestas serán mucho más útiles. La idea principal es que puedes seleccionar tu código en R, o invocarlo en una línea nueva, y presionar una combinación de teclas para abrir una ventana en la que puedes escribir tu solicitud. Presionas enter y el modelo te entrega el código directamente en RStudio, ya sea reemplazando el código que tenías seleccionado, o agregando código en el punto que le pediste.\nEste paquete requiere configurar RStudio con tu proveedor de Inteligencia Artificial. Puedes encontrar instrucciones más detalladas sobre configurar el uso de IA en R en esta publicación. Instalación Instala el paquete con:\ninstall.packages(\u0026#34;gander\u0026#34;) Una vez instalado, aparecerá en el botón Addins, porque se trata de un paquete que entrega funcionalidades extras a RStudio.\nPero antes de usarlo hay que configurar dos cosas: la IA que usará, y las teclas para invocarlo ✨\nConfigurar un proveedor de IA/modelo de lenguaje Para conectar la IA a {gander}, tenemos dos opciones: usar un modelo de IA local, o usar un proveedor de IA en la nube.\nPuedes encontrar instrucciones más detalladas sobre configurar el uso de IA en R en esta publicación. Opción A: instalar un modelo de lenguaje local desde R Si tu computador tiene una tarjeta de video lo suficientemente grande (más de 16GB de memoria de video), si quieres usar IA gratis, o si prefieres no usar modelos en la nube por privacidad, puedes instalar un modelo de lenguaje localmente en tu equipo.\nEl modelo Llama 3.2 tiene 3 billones de parámetros, por lo que puede correr localmente en varios equipos. Para instalar un modelo localmente en tu equipo desde R, primero tienes que instalar Ollama en tu equipo, abrir la aplicación, y luego instalar el paquete de R {ollamar} para descargar un modelo de lenguaje local desde R.1\nSi no tienes un modelo instalado, usa este comando para descargar e instalar un modelo desde R:\nlibrary(ollamar) pull(\u0026#34;llama3.2:3b\u0026#34;) Es necesario tener la aplicación Ollama abierta en tu computadora, dado que ésta aplicación es la que le entrega el modelo de lenguaje a R.\nOpción B: configurar un proveedor de IA en la nube Para conectarte a un proveedor en la nube de IA, como ChatGPT y otros, necesitas darle a R la API key o clave de acceso, como se explica en este tutorial de {ellmer}. Como esta clave es privada (nadie más la debería ver!), la mejor forma de guardarla es en las variables de entorno de R.\nVariables de entorno Las variables de entorno son variables definidas en un archivo .Renviron que está fuera de tus proyectos de R, y que permanece oculto y no se sube a GitHub ni similares. Puedes editar este archivo ejecutando usethis::edit_r_environ(). Se abrirá un script en el que puedes agregar variables disponibles para todos tus proyectos de R.\nLas variables de entorno se definen sin comillas, siguiendo este formato VARIABLE=valor. Por ejemplo, para guardar una clave de API de ChatGPT (OpenAI) en tus variables de entorno, agrega la siguiente línea en tu archivo .Renviron:\nOPENAI_API_KEY=345345398475937434534539847593743453453984759374 Si tu proveedor es Claude (Anthropic), el nombre de la variable es ANTHROPIC_API_KEY, etc.\nPara una explicación más detallada de este proceso, revisa este post Guardar configuración del modelo Ahora que tienes tu modelo de lenguaje listo, tienes que indicarle a {gander} qué modelo usar. Esto lo haces configurando la opción .gander_chat en R. Pero la gracia es configurar ésto una sola vez y que quede siempre configurado, como veremos a continuación.\nSimilar a las variables de entorno, el perfil de R es un script global de R llamado .Rprofile, que no se ubica en un proyecto de R específico, sino que se guarda en tu carpeta de usuario y por ende aplica a todos tus proyectos de R. El código del perfil de R se ejecuta automáticamente cada vez que abras un proyecto o crees una sesión de R nueva. Es el lugar perfecto para guardar algo que necesites que siempre se ejecute en todos tus proyectos, como definir configuraciones y similares.\nPara editar tu perfil de R, ejecuta usethis::edit_r_profile(). Se abrirá un script en el que puedes agregar código que se ejecutará cada vez que inicies una sesión de R.\nAgrega una línea para decirle a {gander} que use el modelo que elijas:\noptions(.gander_chat = ellmer::chat_ollama(model = \u0026#34;llama3.2:3b\u0026#34;)) O si usas ChatGPT:\noptions(.gander_chat = ellmer::chat_openai()) Personalmente uso Copilot, así que pongo ellmer::chat_copilot().\nPara entender en más detalle la integración de IA con R, revisa este post sobre {ellmer} Configurar atajo de teclado Un paso clave para poder integrar rápidamente IA en tu flujo de trabajo es configurar un atajo de teclado para invocar a {gander}.\nPara esto, entra al menú Tools de RStudio, presiona Modify Keyboard Shortcuts, y en la ventana que se abre busca gander. Se sugiere configurar algo como shift+comando+G en Mac1 o control+alt+G en Windows.\nUsar {gander} Ahora que tienes todo configurado, ya puedes usar {gander}!\nSelecciona código, o pon el cursor de texto en cualquier parte de tu script, y presiona el atajo que configuraste 😱\nSe abrirá una ventana para escribir lo que necesites pedirle al asistente. Usa tus propias paralabras para especificar la tarea que quieres lograr.\nEscribe algo como:\ntransforma estos datos a formato wide crea un gráfico de barras con estos datos recodifica la variable género limpia los datos de la columna nombre exporta los datos a formato Excel Presiona enter, y el modelo te entregará los resultados de inmediato! 🎉 Además, si usas proveedores de IA, lograste que se consumieran 200ml de agua! 🤑\nEjemplo de uso En este ejemplo, con tan sólo seleccionar el código e invocar {gander} con shift+comando+G, se abre una ventana a la que le pido que transforme y recodifique los datos, y luego que haga un gráfico.\nMagia? No! Estadísticas! Yey! 🥳\nPara más paquetes y herramientas de modelos de lenguaje e IA en R, revisa Large Language Model tools for R, de Luis D. Verde Arregoitia. Si tu computador no es muy poderoso, te recomiendo instalar llama3.2:1b (más liviano) o llama3.2:3b (un poco mejor), pero si tienes suficiente memoria y tarjeta de video, puedes instalar llama3.1:8b.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-10-20T00:00:00Z","excerpt":"`{gander}` es un paquete de R para integrar asistentes de inteligencia artificial (IA) directamente en RStudio. Solo con presionar una combinación de teclas, podrás pedirle a un asistente con tus propias palabras que escriba código, corrija tu código, o incluso que genere gráficos.","href":"https://bastianoleah.netlify.app/blog/gander/","tags":"inteligencia artificial ; consejos","title":"Acelera tus análisis con `{gander}`, un asistente de IA integrado en RStudio"},{"content":"Por fin tuve un respiro en el trabajo, así que aproveché de darle una manito de gato a este sitio. Cambié algunas cosas estéticas menores y algunas funcionalidades que tenía pendientes de hace tiempo.\nAgregué al inicio del sitio un panel que contiene las etiquetas o tags más usados en el blog, para indicar los temas que trato en los posts y para que la gente interesada pueda llegar a los posts.\nPude hacerlo gracias a estas instrucciones.\nTambién agregué otro panel con las etiquetas de los posts en la parte lateral de la página del blog, para usar ese espacio vacío y destacar los temas principales.\nComo se ve en la foto anterior, también he ido agregando más botones con enlaces relacionados a cada post, incluyendo snippets de código para quienes solo quieren copiar y pegar en R (válido).\nPor fin le agregué un contador de visitas de código abierto y enfocado en privacidad, GoatCounter 🐐 para monitorear el tráfico del sitio. Pude hacerlo gracias a estas instrucciones para agregar contador de visita a sitios Hugo. Por opción personal no uso Google Analytics ni ningún otro servicio que afecte la privacidad de las personas que visitan el sitio ni que usen esos datos con fines comerciales. Quizás después haga un dashboard para ver las visitas o algo así.\nFinalmente, tengo algunos bocetos de posts grandes para el futuro próximo, principalmente uno sobre procesamiento de datos geoespaciales en R, que voy complementando a medida que aprendo yo misme 😌\nEste sitio nació porque yo aprendí R de forma autodidacta, gracias a lo que compartían otras personas. Así que quise ayudar también a que más gente aprendiera a usar R para análisis de datos.\nCon el tiempo, me di cuenta que el sitio también me sirve a mi como referencia: muy seguido entro a mi blog buscando mi propio código para copiarlo! Espero que les sirva 💜\nComo siempre, el código entero de este sitio está en su repositorio.\n","date":"2025-10-16T00:00:00Z","excerpt":"Por fin tuve un respiro del trabajo, así que aproveché de darle una manito de gato a este sitio. Cambié algunas cosas estéticas menores y algunas funcionalidades que tenía pendientes de hace tiempo.","href":"https://bastianoleah.netlify.app/blog/2025-10-16/","tags":"blog","title":"Actualización del blog: etiquetas"},{"content":" En un post anterior hablé sobre cómo hacer validación básica de datos en R. A grandes razgos, vimos la utilidad de crear funciones que contengan pruebas simples para validar la calidad de tus datos, tales como revisar cantidad de filas, cantidad de datos perdidos, y otros.\nDado que R es un lenguaje enfocado en el análisis de datos, existen varios paquetes que nos pueden ayudar con la validación de datos!\nEn este post veremos {testthat}, un paquete que facilita implementar pruebas unitarias a tu código para validar su funcionamiento, y {pointblank}, un paquete diseñado para la validación de datos con poderosas capacidades de reportabilidad. En unos minutos aprenderás a usar este paquete para garantizar que tus datos cumplen con tus expectativas de calidad.\n¿Para qué sirve la validación de datos? Para que, en cualquier punto de tus procesos de análisis de datos, puedas verificar si los datos cumplen con los criterios de calidad que tú definas, y así enterarte de si vienen como esperas o si es que traen sorpresas. En la validación de datos se crean pruebas para, por ejemplo, confirmar que una columna no tenga datos perdidos, que los valores de una columna estén dentro de un rango esperado, etcétera.\nAl crear una serie de pruebas, podemos automatizar el proceso de validación de datos. De esta forma, si modificamos nuestro código, o si cambian los datos, no necesitamos revisar manualmente que todo esté en orden, sino que tenemos una especie de protocolo para certificar que los datos son los esperados. Cada vez que hagamos cambios en el código, podemos ejecutar las pruebas para confirmar que todo sigue funcionando como se espera.\nÍndice Datos de ejemplo Validación con {testthat} Estructura del código Crear pruebas unitarias Ejemplos de pruebas unitarias para validación de datos Conclusión de {testthat} para validación de datos Validación de datos con {pointblank} Reportes de validación de datos Ejemplo de validación de datos sucios Crear un plan básico de validación de datos Conclusiones Recursos para aprender {pointblank} Datos de ejemplo Creemos una pequeña tabla para aprender a validar datos:\nlibrary(dplyr) datos \u0026lt;- tribble(~animal, ~patas, ~lindura, ~color, \u0026#34;mapache\u0026#34;, \u0026#34;4\u0026#34;, 100, \u0026#34;gris\u0026#34;, \u0026#34;gato\u0026#34;, \u0026#34;80\u0026#34;, 90, \u0026#34;negro\u0026#34;, \u0026#34;pollo\u0026#34;, \u0026#34;2\u0026#34;, NA, \u0026#34;plumas\u0026#34;, \u0026#34;rata\u0026#34;, \u0026#34;cuatro\u0026#34;, 90, \u0026#34;#CCCCCC\u0026#34;) De inmediato podemos ver en esta tabla creada con tribble() que hay varios problemas: la columna patas viene como caracteres, hay datos perdidos en lindura, y hay un color hexadecimal en color. Pero nos damos cuenta de ésto porque la tabla contiene pocos datos. Cuando trabajemos con miles o millones de observaciones, se vuelve más difícil detectar este tipo de problemas. Ahí es cuando la validación de datos nos puede ayudar!\nValidación con {testthat} A pesar de que {testthat} se usa en general para el desarrollo de paquetes, y se enfoca a validar que cálculos y métodos estadísticos funcionen como es esperado, igual se puede usar para validar en proyectos de análisis de datos.\nEstructura del código Asumiendo que nuestro proyecto posee varios scripts donde se procesan los datos, la idea general será crear scripts con pruebas, y periodicamente ejecutar estos scripts de pruebas para confirmar que todo esté en orden.\nPrimero necesitamos crear una carpeta para los tests, y scripts con pruebas para cada script o paso en nuestro flujo de trabajo que queramos validar. Podemos hacerlo a mano, o bien crear una carpeta para las pruebas con fs::dir_create(), y dentro creamos los scripts que necesitemos con fs::file_create(), siguiendo la convención de anteponer test- al nombre de cada script de pruebas.\nSe recomienda crear un script de pruebas por cada script de nuestro proyecto: si tenemos un script llamado datos.R, creamos un script de pruebas llamado test-datos.R dentro de la carpeta tests/. Dentro de este script empezamos a diseñar las pruebas unitarias. Las pruebas unitarias son pruebas que validan que una unidad específica de código (una función, un cálculo, una transformación de datos) funcione como se espera.\nCrear pruebas unitarias Usamos la función test_that() para definir cada prueba, indicando primero el nombre de la prueba. Dentro, usamos funciones como expect_true(), expect_equal(), expect_type(), para declarar que esperamos que luego de cierta operación ocurra algo. Por ejemplo: espero (expect) que mi tabla tenga una columna determinada, o que una columna sea de cierto tipo. Estas son las condiciones que deben cumplirse para que la prueba pase.\nVeamos un ejemplo de una prueba:\nlibrary(testthat) Attaching package: 'testthat' The following object is masked from 'package:dplyr': matches test_that(\u0026#34;números iguales\u0026#34;, expect_equal(4, 4) ) Test passed 🥳 Esta prueba evalúa si dos números son iguales (expect_equal()), y en este ejemplo se cumple: {testthat} nos entrega un emoji de celebración 🎉 Veamos la siguiente prueba:\ntest_that(\u0026#34;números desiguales\u0026#34;, expect_equal(4, 5) ) ── Failure: números desiguales ───────────────────────────────────────────────── 4 not equal to 5. 1/1 mismatches [1] 4 - 5 == -1 Error: ! Test failed Como la prueba no se cumple, porque 4 es distinto a 5, y la prueba nos dará un error explicando en dónde está el problema.\nEjemplos de pruebas unitarias para validación de datos Apliquemos pruebas similares a los datos de ejemplo, dentro de un script que se llame tests/test-datos.R, donde cargamos los datos (es importante que el script sea reproducible, ya que no lee los datos desde tu entorno sino que los carga en su propio entorno) y luego hacemos las pruebas:\n# código para que el script de pruebas cargue los datos # datos \u0026lt;- readr::read_rds(\u0026#34;datos.rds\u0026#34;) # esperamos que exista un objeto llamado \u0026#34;datos\u0026#34; test_that(\u0026#34;se cargaron los datos\u0026#34;, expect_true(exists(\u0026#34;datos\u0026#34;)) ) Test passed 😸 # esperamos que el número de columnas sea 4 test_that(\u0026#34;suficientes columnas\u0026#34;, expect_equal(ncol(datos), 4) ) Test passed 😀 # esperamos que la columna `animal` sea tipo caracter test_that(\u0026#34;columnas tipo texto\u0026#34;, expect_type(datos$animal, \u0026#34;character\u0026#34;) ) Test passed 😸 # esperamos que la columna `patas` sea tipo numérico test_that(\u0026#34;columnas tipo texto\u0026#34;, expect_type(datos$patas, \u0026#34;numeric\u0026#34;) ) ── Failure: columnas tipo texto ──────────────────────────────────────────────── datos$patas has type 'character', not 'numeric'. Error: ! Test failed # esperamos que los colores estén dentro de un conjunto determinado test_that(\u0026#34;colores factibles\u0026#34;, expect_in(datos$color, c(\u0026#34;negro\u0026#34;, \u0026#34;gris\u0026#34;, \u0026#34;blanco\u0026#34;, \u0026#34;amarillo\u0026#34;, \u0026#34;café\u0026#34;)) ) ── Failure: colores factibles ────────────────────────────────────────────────── datos$color (`actual`) isn't fully contained within c(\u0026quot;negro\u0026quot;, \u0026quot;gris\u0026quot;, \u0026quot;blanco\u0026quot;, \u0026quot;amarillo\u0026quot;, \u0026quot;café\u0026quot;) (`expected`). * Missing from `expected`: \u0026quot;plumas\u0026quot;, \u0026quot;#CCCCCC\u0026quot; * Present in `expected`: \u0026quot;negro\u0026quot;, \u0026quot;gris\u0026quot;, \u0026quot;blanco\u0026quot;, \u0026quot;amarillo\u0026quot;, \u0026quot;café\u0026quot; Error: ! Test failed Una vez que guardamos este script, podemos ejecutar sus pruebas manualmente, o bien podemos usar test_file(\u0026quot;tests/test-script.R\u0026quot;) para ejecutar todas las pruebas de un script, o test_dir(\u0026quot;tests.R\u0026quot;) para ejecutar todas las pruebas de la carpeta de pruebas, validando tu proyecto entero de una sola vez.\nA partir de las pruebas que definimos podemos confirmar que hay problemas en las columnas patas y color, ya que no cumplen con las expectativas que definimos en las pruebas.\nPodemos ejecutar las funciones que realizan la validación desde donde más nos resulte conveniente: desde algún script principal de nuestro proyecto, desde un script tests.R específico para ejecutar las pruebas, al final de cada script del proyecto, al final de un script donde ejecutemos todo el procesamiento de nuestro proyecto, o manualmente.\nUso completo 💡 Lo que yo haría sería algo así como agregar un test_file() al final del script de limpieza de datos, que confirme que la limpieza salió bien, en otro script donde procese datos tendría otro test_file() con pruebas relacionadas a este paso, etcétera.\nOtra opción es guardar los scripts de pruebas en tests y ejecutar todos los scripts de pruebas con test_dir(\u0026quot;tests/\u0026quot;), en cuyo caso {testthat} arroja un resumen de los resultados.\nTambién, al guardar un script con pruebas, RStudio se da cuenta y aparece el botón Run Tests en la parte superior derecha del script.\nUso simple Si queremos hacerlo todo más simple, simplemente usemos las funciones testthat::expect_x() entremedio del código, de modo que si la prueba falla, arroja error, pero si no falla, no pasa nada. En este sentido, funciona igual que stopifnot(), pero para mi resulta mucho más claro (esa función me confunde mucho 😣). Con expect_x() declaramos: esperamos que lo siguiente retorne TRUE, y si las cosas se dan así, no pasa nada y la vida sigue.\ntestthat::expect_equal(n_distinct(iris$Species), 3) testthat::expect_equal(n_distinct(iris$Species), 4) Error: n_distinct(iris$Species) not equal to 4. 1/1 mismatches [1] 3 - 4 == -1 Conclusión de {testthat} para validación de datos Con {testthat} podemos crear un flujo de trabajo que incluya la validación de datos a nuestros proyectos, con funciones e interfaz amigable que te dan un golpe de dopamina cuando aparece el mensajito verde con el emoji de felicitación. Si bien es ampliamente usado en la comunidad de R, su uso principal no es la validación de datos, por lo que ahora veremos un segundo paquete especialmente diseñado para ello.\nValidación de datos con {pointblank} A diferencia de {testthat}, el paquete {pointblank} está diseñado para evaluar la calidad de conjuntos de datos. Sirve para definir pruebas que validen que los datos cumplen con ciertos estándares, integrando las pruebas en cadenas de comandos unidos por conectores o pipes (|\u0026gt; o %\u0026gt;%), y se destaca por su capacidad de crear agentes que generan reportes automáticos de validación de datos.\nLas funciones de validación de {pointblank} sirven para integrarlas en pipelines. Si se pasa la prueba, el proceso continúa, pero si la prueba falla, el proceso se detiene y te avisa. Probemos la validación de algunos aspectos de la tabla de ejemplo:\nlibrary(pointblank) datos |\u0026gt; col_is_numeric(lindura) |\u0026gt; col_is_character(c(animal, color)) |\u0026gt; col_vals_in_set(animal, set = c(\u0026#34;perro\u0026#34;, \u0026#34;gato\u0026#34;, \u0026#34;sapo\u0026#34;, \u0026#34;pollo\u0026#34;, \u0026#34;mapache\u0026#34;, \u0026#34;pez\u0026#34;, \u0026#34;rata\u0026#34;)) # A tibble: 4 × 4 animal patas lindura color \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 mapache 4 100 gris 2 gato 80 90 negro 3 pollo 2 NA plumas 4 rata cuatro 90 #CCCCCC Obtenemos de vuelta la tabla de datos, porque implícitamente las tres pruebas se pasaron correctamente. Es decir, si todo está correcto, seguimos con nuestros procesos.\nProbemos qué pasa si incluimos pruebas más estrictas que nuestra humilde tabla no podrá pasar:\ndatos |\u0026gt; col_is_numeric(lindura) |\u0026gt; col_is_character(c(animal, color)) |\u0026gt; col_vals_in_set(animal, set = c(\u0026#34;perro\u0026#34;, \u0026#34;gato\u0026#34;, \u0026#34;sapo\u0026#34;, \u0026#34;pollo\u0026#34;, \u0026#34;mapache\u0026#34;, \u0026#34;pez\u0026#34;, \u0026#34;rata\u0026#34;)) |\u0026gt; col_vals_in_set(patas, set = 2:100) |\u0026gt; col_vals_not_null(lindura) Error: Exceedance of failed test units where values in `patas` should have been in the set of `2`, `3`, `4` (and 96 more). The `col_vals_in_set()` validation failed beyond the absolute threshold level (1). * failure level (1) \u0026gt;= failure threshold (1) Recibimos un aviso que indica un exceso de test fallidos, y una explicación de lo que falló. El paquete ofrece la posibilidad de ajustar o soltar el nivel de dificultad de las pruebas, por ejemplo, para permitir un cierto porcentaje de problemas, pero avisar si este nivel se supera.\nVeamos otro ejemplo de pruebas aplicadas a un pipeline: aquí intentamos corregir uno de los problemas con los datos, detectado con una de las pruebas anteriores, y aplicamos nuevamente la prueba para confirmar que quedó bien:\ndatos |\u0026gt; # corregir mutate(lindura = tidyr::replace_na(lindura, mean(lindura, na.rm = TRUE))) |\u0026gt; # probar col_vals_not_null(lindura) # A tibble: 4 × 4 animal patas lindura color \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 mapache 4 100 gris 2 gato 80 90 negro 3 pollo 2 93.3 plumas 4 rata cuatro 90 #CCCCCC En otras palabras, corregimos los datos e inmediatamente probamos que la corrección funciona, sin necesidad de revisar manualmente.\nAlternativamente existen variedades de las funciones de validación que empiezan con test_, y que retornan TRUE o FALSE dependiendo de si se cumple o no la prueba. Sirven para usarlas dentro de condicionales if, o dentro de funciones if() o ifelse(). Por ejemplo, puede usarse en un if para aplicar una corrección si la prueba no se cumple.\nReportes de validación de datos Aparte de las funciones de validación, el verdadero potencial de {pointblank} está en la creación de agentes que generan reportes automáticos de validación de datos. Por agentes se refieren a un objeto que contiene las pruebas que queremos aplicar a los datos, y que al interrogarlo genera un reporte con los resultados de las pruebas.\nCreamos un agente entregándole nuestros datos y opcionalmente el nivel de acción, que le indica cuándo actuar sobre los problemas en nuestros datos. Luego, le indicamos al agente las pruebas que queremos realizar. Finalmente, interrogamos al agente para que nos entregue su reporte.\nlibrary(pointblank) # crear agente agente \u0026lt;- create_agent(datos, actions = action_levels(warn_at = 1, stop_at = 2)) # definir pruebas agente \u0026lt;- agente |\u0026gt; col_is_numeric(c(lindura, patas)) |\u0026gt; col_is_character(c(animal, color)) |\u0026gt; col_vals_in_set(animal, set = c(\u0026#34;perro\u0026#34;, \u0026#34;gato\u0026#34;, \u0026#34;sapo\u0026#34;, \u0026#34;pollo\u0026#34;, \u0026#34;mapache\u0026#34;, \u0026#34;pez\u0026#34;, \u0026#34;rata\u0026#34;)) |\u0026gt; col_vals_between(patas, left = 2, right = 4) |\u0026gt; col_vals_not_null(lindura) # interrogar interrogate(agente) Recibimos un reporte interactivo que indica la calidad de nuestros datos en base a las pruebas definidas:\nEn el reporte vemos los pasos de validación (las pruebas) hacia abajo en la columna steps, y los colores indican si la prueba se pasó (verde), si hubo advertencias (amarillo), o si la prueba falló (rojo). En la columna units se indica las unidades o pruebas individuales aplicadas en cada paso: probar el formato de una columna es una prueba, pero buscar datos perdidos corresponde a una prueba por cada observación de la tabla.\nEjemplo de validación de datos sucios Probemos otro ejemplo con datos ensuciados gracias al paquete {messy}, que te permite agregar datos perdidos, errores gramaticales, símbolos raros y otras asquerosidades a cualquier conjunto de datos. Ensuciaremos el famoso dataset iris:\niris_sucio \u0026lt;- datasets::iris |\u0026gt; tibble() |\u0026gt; janitor::clean_names() |\u0026gt; messy::messy(messiness = 0.2) iris_sucio # A tibble: 150 × 5 sepal_length sepal_width petal_length petal_width species \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 \u0026quot;5.1\u0026quot; \u0026quot;3.5\u0026quot; 1.4 \u0026quot;0.2\u0026quot; \u0026quot;se#tosa \u0026quot; 2 \u0026quot;4.9\u0026quot; \u0026quot;3\u0026quot; \u0026lt;NA\u0026gt; \u0026quot;0.2 \u0026quot; \u0026quot;se_tos*a\u0026quot; 3 \u0026quot;4.7\u0026quot; \u0026quot;3.2\u0026quot; 1.3 \u0026quot;0.2 \u0026quot; \u0026quot;se%tosa\u0026quot; 4 \u0026lt;NA\u0026gt; \u0026quot;3.1\u0026quot; 1.5 \u0026quot;0.2 \u0026quot; \u0026lt;NA\u0026gt; 5 \u0026quot;5\u0026quot; \u0026quot;3.6\u0026quot; \u0026lt;NA\u0026gt; \u0026quot;0.2\u0026quot; \u0026quot;se+tosa\u0026quot; 6 \u0026quot;5.4\u0026quot; \u0026quot;3.9 \u0026quot; 1.7 \u0026quot;0.4\u0026quot; \u0026quot;set!osa\u0026quot; 7 \u0026quot;4.6 \u0026quot; \u0026quot;3.4\u0026quot; 1.4 \u0026quot;0.3\u0026quot; \u0026quot;set$osa\u0026quot; 8 \u0026quot;5\u0026quot; \u0026lt;NA\u0026gt; 1.5 \u0026lt;NA\u0026gt; \u0026lt;NA\u0026gt; 9 \u0026lt;NA\u0026gt; \u0026quot;2.9 \u0026quot; 1.4 \u0026quot;0.2 \u0026quot; \u0026quot;SETO@SA\u0026quot; 10 \u0026quot;4.9\u0026quot; \u0026quot;3.1\u0026quot; 1.5 \u0026lt;NA\u0026gt; \u0026quot;set)osa \u0026quot; # ℹ 140 more rows Luego creamos un agente para validar estos datos:\nagente_iris \u0026lt;- create_agent(iris_sucio, actions = action_levels(warn_at = 0.02, stop_at = 0.5)) agente_iris \u0026lt;- agente_iris |\u0026gt; col_is_numeric(columns = everything()) |\u0026gt; col_is_character(species) |\u0026gt; col_vals_in_set(species, c(\u0026#34;virginica\u0026#34;, \u0026#34;setosa\u0026#34;, \u0026#34;versicolor\u0026#34;)) |\u0026gt; col_vals_between(sepal_length, 1, 6) |\u0026gt; col_vals_not_null(columns = everything()) interrogate(agente_iris) Confirmamos que {messy} destruyó a nuestro querido iris 😔🕊️\nCrear un plan básico de validación de datos Si no sabes cómo empezar a validar tus datos, {pointblank} te ayuda a crear un plan básico de validación de datos con la función draft_validation(), la cual genera un script con pruebas básicas para que lo edites y adaptes a tus necesidades:\ndraft_validation( tbl = ~datasets::iris, filename = \u0026#34;test-iris\u0026#34; ) Este código nos crea un script que contiene 10 pruebas para el dataset en base a sus propios datos y características, y así tenemos material para definir los estándares para el conjunto de datos, y volver a validarlo en el futuro luego de que se apliquen cambios, actualizaciones o correcciones.\nConclusiones Aplicar principios de validación de datos a tus proyectos de análisis de datos te va a ayudar a tener mayor confianza en tus datos, dándote certeza de que no hay sorpresas inesperadas entre las miles o millones de observaciones con las que trabajas. También puede ahorrarte dolores de cabeza, ya que si los datos cambian y estos cambios se desajustan de tus estándares, te enterarás de inmediato en vez de darte cuenta cuando se eche a perder algún gráfico o tabla más adelante 😅\nRecursos para aprender {pointblank} Introducción a {pointblank} Guía oficial de {pointblank} Workshop de {pointblank} por Richard Iannone (requiere clonar el proyecto y generar los reportes Markdown) ","date":"2025-10-15T00:00:00Z","excerpt":"La validación de datos sirve para verificar durante el proceso de análisis si los datos cumplen con requerimientos de calidad y con tus expectativas, con el objetivo de evitar problemas futuros relacionados a datos inesperados, incompletos, o erróneos. En este post veremos dos paquetes para validar el funcionamiento de tu código y para validar tus datos.","href":"https://bastianoleah.netlify.app/blog/validacion_avanzada/","tags":"procesamiento de datos ; consejos ; automatización ; limpieza de datos","title":"Validación de datos con {testthat} y {pointblank}"},{"content":"En post anteriores mostré cómo hacer mapas comunales y regionales de Chile con R, y a hacer mapas de los territorios urbanos del país.\nEn este tutorial aprenderemos a crear mapas de Chile en R usando datos geográficos o shapes oficiales de Chile, obtenidos desde la Subsecretaría de Desarrollo Regional y Administrativo (Subdere) y la Biblioteca del Congreso Nacional de Chile (BCN).\nEl objetivo será aprender a visualizar mapas desde shapefiles obtenidos de internet, y a procesar datos geográficos más complejos con R, para generar mapas de Chile con polígonos y límites geográficamente correctos.\nObtener datos geográficos El primer paso para visualizar mapas es obtener los datos geográficos. Con esto nos referimos a los polígonos (figuras geométricas que usualmente representan territorios), líneas (que pueden representar límites o redes viales) y puntos (que pueden representar ubicaciones como municipios o capitales).\nLos datos geográficos suelen venir como shapes, que usualmente son carpetas que contienen archivos, principalmente shapefiles (.shp).\nLos siguientes botones ofrecen descargas directas de los datos que usaremos desde sus fuentes originales, la mayoría de ellos obtenidos desde la IDE Subdere.\nLa división político-administrativa contiene los polígonos a nivel comunal, provincial y regional que consituyen el territorio de Chile, y provienen de la Subdere (2023):\nPolígonos de división político-administrativa Los límites comunales de la división político-administrativa contiene las líneas que limitan las unidades administrativas (comunas) del país, y provienen de la Subdere (2022):\nLíneas de límites comunales Las ubicaciones de los municipios marcan con puntos la localización de estas instituciones, y provienen de la Subdere (2022):\nPuntos de ubicación de municipios del país La red vial es el conjunto de líneas que representan las carreteras y caminos de todo el país, y provienen de la Bibioteca del Congreso Nacional de Chile (BCN):\nLíneas de red vial del país Alternativamente, puedes descargar los archivos desde R ejecutando este código:\ndir.create(\u0026#34;shapes\u0026#34;) # crear carpeta # descargar download.file(\u0026#34;https://ide.subdere.gov.cl/wp-content/uploads/74_limites_dpa_2022.zip\u0026#34;, \u0026#34;shapes/74_limites_dpa_2022.zip\u0026#34;) download.file(\u0026#34;https://ide.subdere.gov.cl/descargas/SHP/Limite_DPA_03082023.rar\u0026#34;, \u0026#34;shapes/Limite_DPA_03082023.rar\u0026#34;) download.file(\u0026#34;https://ide.subdere.gov.cl/descargas/SHP/Municipios_15112022.zip\u0026#34;, \u0026#34;shapes/Municipios_15112022.zip\u0026#34;) download.file(\u0026#34;https://www.bcn.cl/obtienearchivo?id=repositorio/10221/10403/2/Red_Vial.zip\u0026#34;, \u0026#34;shapes/Red_Vial.zip\u0026#34;) Una vez que descargamos los archivos, debemos descomprimirlos para obtener las carpetas que contienen los shapes.\nCargar datos geográficos Para trabajar con datos geográficos usamos el paquete {sf}, abreviación de simple features, que es un estándar para manejar datos geoespaciales. Si no tienes instalado el paquete, instálalo con install.packages(\u0026quot;sf\u0026quot;).\nPara leer un archivo geoespacial usamos la función read_sf() apuntada a la carpeta que contiene los shapefiles:\nlibrary(sf) library(janitor) # polígonos de comunas comunas \u0026lt;- read_sf(\u0026#34;shapes/DPA_2023/COMUNAS\u0026#34;) |\u0026gt; clean_names() # límites comunales limites \u0026lt;- read_sf(\u0026#34;shapes/74_limites_dpa_2022\u0026#34;) |\u0026gt; clean_names() # ubicación de municipalidades municipios \u0026lt;- read_sf(\u0026#34;shapes/Municipios_15112022\u0026#34;) |\u0026gt; clean_names() # red vial calles \u0026lt;- read_sf(\u0026#34;shapes/Red_Vial\u0026#34;) |\u0026gt; clean_names() Algunos de estos mapas pueden ser muy detallados y/o complejos, por lo que pueden tardarse en cargar1.\nSi verificamos la clase de uno de los objetos geográficos cargados, vemos que una de sus clases es sf, pero al mismo tiempo tbl y data.frame:\nclass(comunas) [1] \u0026quot;sf\u0026quot; \u0026quot;tbl_df\u0026quot; \u0026quot;tbl\u0026quot; \u0026quot;data.frame\u0026quot; Si lo imprimimos en la consola, confirmamos que los objetos cargados desde los shapefiles son tablas de datos que arriba dicen Simple feature collection; es decir, son tablas de datos que además tienen información geográfica.\ncomunas Simple feature collection with 345 features and 7 fields Geometry type: MULTIPOLYGON Dimension: XY Bounding box: xmin: -109.4549 ymin: -56.53777 xmax: -66.41559 ymax: -17.4984 Geodetic CRS: GCS_SIRGAS-Chile # A tibble: 345 × 8 cut_reg cut_prov cut_com region provincia comuna superficie \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 11 113 11302 Aysén del General Carlo… Capitán … O'Hig… 7785. 2 05 055 05503 Valparaíso Quillota Hijue… 268. 3 08 083 08308 Biobío Biobío Quila… 1124. 4 09 092 09209 La Araucanía Malleco Renai… 265. 5 09 092 09202 La Araucanía Malleco Colli… 1306. 6 15 152 15201 Arica y Parinacota Parinaco… Putre 5890. 7 07 071 07110 Maule Talca San R… 263. 8 08 083 08311 Biobío Biobío Santa… 1251. 9 12 121 12102 Magallanes y de la Antá… Magallan… Lagun… 3568. 10 11 113 11303 Aysén del General Carlo… Capitán … Tortel 19991. # ℹ 335 more rows # ℹ 1 more variable: geometry \u0026lt;MULTIPOLYGON [°]\u0026gt; Estas tablas de datos cuentan con una columna geometry, que contiene la información geográfica de los polígonos, puntos y/o líneas de cada observación. A su vez, encima de la tabla de datos vemos información especial, como el tipo de geometría, las dimensiones de la caja (bounding box), y el sistema de coordenadas de referencia (CRS), que indica el tipo de proyección usada.\nVisualizar mapas Una vez cargados los shapes, simplemente es cosa de aplicarlos por capas a un gráfico de {ggplot2}. Las capas de datos geográficos se agregan con geom_sf(), y en este caso, que tenemos varios shapes, en cada capa habría que definir el objeto correspondiente en el argumento data; es decir, cada capa se basa en datos distintos, pero que coinciden en términos de coordenadas.\nEntonces, empezamos un gráfico con ggplot(), y luego vamos agregando capas de geom_sf(), recordando que el orden en que agreguemos las capas importa: las primeras (más arriba) serán los objetos en el fondo, y las siguientes capas (en sucesivas líneas) se visualizarán encima de las anteriores.\nlibrary(dplyr) library(ggplot2) ggplot() + # capa de fondo: polígonos de comunas geom_sf(data = comunas, aes(fill = region), linewidth = 0) + # límites regionales encima de las comunas geom_sf(data = limites, color = \u0026#34;peachpuff4\u0026#34;, linewidth = 0.2, alpha = 0.8) + # calles geom_sf(data = calles |\u0026gt; filter(clase_ruta \u0026lt;= 3), color = \u0026#34;peachpuff4\u0026#34;, linewidth = 0.1, alpha = 0.3) + # puntos de municipios geom_sf(data = municipios, color = \u0026#34;peachpuff4\u0026#34;, size = 0.7, alpha = 0.6) + # borde oscuro geom_sf(data = municipios, color = \u0026#34;cornsilk1\u0026#34;, size = 0.1, alpha = 0.9) + # centro claro # recortar Chile continental coord_sf(expand = FALSE, xlim = c(-76, -66), ylim = c(-56.2, -17)) + # paleta de colores colorspace::scale_fill_discrete_qualitative( palette = \u0026#34;Warm\u0026#34;, c1 = 40, # intensidad del color l1 = 85) + # brillo del color theme_minimal() + theme(legend.position = \u0026#34;none\u0026#34;) A las capas de objetos geográficos le especificamos el sistema de coordenadas con coord_sf() para recortar el mapa al territorio continental del país, y además le pusimos una escala de colores cálidos desde el paquete {colorspace}, que además tiene la cualidad de poder adaptar la intensidad y brillo de sus colores.\nListo! La gracia de {sf} es poder entregar herramientas para cargar cualquier tipo de datos geográficos y habilitar que {ggplot2} los pueda graficar sin problemas. A menos que ocurran problemas. Lo cual veremos en otro post 😬\nAl tratarse de mapas oficiales, estos datos geográficos contienen mucho nivel de detalle; usualmente mucho más del necesario para hacer mapas a nivel nacional, donde el nivel de detalle es visualmente imperceptible. Dado que una mayor calidad significa mapas más pesados y por ende procesos más lentos, podemos simplificar las geometrías con la función ms_simplify() del paquete {rmapshaper}, como detallamos en este post.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-10-14T00:00:00Z","excerpt":"En este tutorial aprenderemos a crear mapas de Chile en R usando datos geográficos o _shapes_ oficiales, obtenidos desde la Subsecretaría de Desarrollo Regional y Administrativo (Subdere) y la Biblioteca del Congreso Nacional de Chile. El objetivo será aprender a visualizar mapas desde _shapefiles_ obtenidos de internet, y a procesar datos geográficos más complejos con R, para generar mapas de Chile con polígonos y límites geográficamente correctos.","href":"https://bastianoleah.netlify.app/blog/tutorial_mapa_chile_subdere/","tags":"mapas ; ggplot2 ; gráficos ; ciencias sociales ; Chile","title":"Crea un mapa de Chile desde datos geoespaciales oficiales en R"},{"content":"Gracias al paquete {shinyjs} podemos hacer que una app Shiny muestre u oculte elementos según los datos que correspondan o los inputs que haya seleccionado el usuario.\nPor ejemplo:\nSi el/la usuario/a selecciona una opción en particular, mostrar un contenedor con información adicional. Si no hay datos para graficar, ocultar el gráfico y mostrar un aviso. Se trata de una habilidad clave para crear aplicaciones que se adapten a datos complejos, cambiantes, o modificables por los usuarios.\nLo primero que tenemos que hacer en nuestra app es activar el uso de {shinyjs} agregando la función useShinyjs() en nuestra UI:\nlibrary(shiny) library(bslib) library(shinyjs) ui \u0026lt;- page_fluid( # activar javascript useShinyjs() ) Tutorial 1: mostrar/ocultar elementos según selección del usuario Para el primer ejemplo, haremos una app simple que muestre y oculte un elemento de acuerdo a lo que elija el/la usuario/a.\nEn esta app ponemos un título y un selector de opciones (selectInput()), una salida de texto (textOutput()) para mostrar la selección hecha, y un contenedor div() que contiene un aviso que queremos mostrar u ocultar según la selección del usuario. En este contenedor, el argumento id es lo que nos permitirá identificar el elemento para poder mostrarlo u ocultarlo.\nlibrary(shiny) library(bslib) library(shinyjs) ui \u0026lt;- page_fluid( # activar javascript useShinyjs(), h1(\u0026#34;Mostrar/ocultar\u0026#34;), # selector de opciones selectInput(\u0026#34;filtro\u0026#34;, label = \u0026#34;Filtrar\u0026#34;, choices = c(\u0026#34;gato\u0026#34;, \u0026#34;perro\u0026#34;, \u0026#34;gallina\u0026#34;) ), textOutput(\u0026#34;animales\u0026#34;), # contenedor con un aviso para mostrar div(id = \u0026#34;aviso\u0026#34;, style = css(margin_top = \u0026#34;8px\u0026#34;), span( style = css(border = \u0026#34;solid 1px #A085B7\u0026#34;, border_radius = \u0026#34;6px\u0026#34;, padding = \u0026#34;6px\u0026#34;, font_weight = \u0026#34;bold\u0026#34;), \u0026#34;⚠️ Muy muy feo!\u0026#34; ) ) ) En la sección server de nuestra app vamos a programar la lógica que hará que se muestre u oculte el aviso:\nserver \u0026lt;- function(input, output, session) { # texto con la selección del usuario/a output$animales \u0026lt;- renderText({ paste(\u0026#34;Animal seleccionado:\u0026#34;, input$filtro) }) # mostrar contenedor sólo si se selecciona \u0026#34;perro\u0026#34; observe({ if(input$filtro == \u0026#34;perro\u0026#34;) { show(\u0026#34;aviso\u0026#34;) } else { hide(\u0026#34;aviso\u0026#34;) } }) } Usamos observe() para que ocurra una acción en nuestra app si es que cambia algún objeto reactivo. En este caso, haremos que si se cambia input$filtro, se detecte si la selección es \u0026quot;perro\u0026quot;, y si es así, mostrar (show()) el elemento llamado \u0026quot;aviso\u0026quot;. Si la selección es cualquier otra, se oculta (hide()) el elemento.\nVeamos la app en acción!\nPara este ejemplo, agregamos un toque de color para que se vea más bonita la aplicación, definido al principio de la UI:\ntheme = bs_theme(bg = \u0026#34;#EAD1FA\u0026#34;, fg = \u0026#34;#553A74\u0026#34;, primary = \u0026#34;#8557AB\u0026#34;) Para más información sobre aplicar temas a apps Shiny, revisa este post.\nPuedes ver el código completo de esta aplicación en este Gist de Github.\nTutorial 2: mostrat/ocultar elementos según los datos Crearemos una aplicación básica que muestre un gráfico, con un input encima que filtrará los datos del gráfico. Primero definimos la interfaz:\nlibrary(shiny) library(bslib) library(shinyjs) library(dplyr) library(ggplot2) ui \u0026lt;- page_fluid( # activar javascript useShinyjs(), h1(\u0026#34;Mostrar/ocultar\u0026#34;), sliderInput(\u0026#34;filtro\u0026#34;, \u0026#34;Filtrar\u0026#34;, min = 5, max = 10, value = 1), # contenedor con el elemento a mostrar u ocultar div(id = \u0026#34;grafico\u0026#34;, plotOutput(\u0026#34;grafico_barras\u0026#34;, width = 320, height = 200) ), # otro contenedor con un aviso para mostrar div(id = \u0026#34;aviso\u0026#34;, p(em(\u0026#34;No hay datos para mostrar!\u0026#34;)) ) ) En la sección server de nuestra app vamos a crear un objeto reactivo con reactive() que filtre los datos a partir del selector input$filtro:\nserver \u0026lt;- function(input, output, session) { datos \u0026lt;- reactive({ iris |\u0026gt; filter(Sepal.Length \u0026gt;= input$filtro) }) } Con este código, creamos el objeto reactivo datos() que se actualizará cada vez que la/el usuario cambie el input input$filtro, y por consiguiente actualizará los elementos que usen este objeto.\nLuego, definimos un gráfico sencillo en base a datos(), y que por consiguiente se actualizará automáticamente cuando cambien los datos reactivos:\n# generar un gráfico de barras output$grafico_barras \u0026lt;- renderPlot({ datos() |\u0026gt; ggplot() + aes(Sepal.Length, Sepal.Width) + geom_point() }) Finalmente, programamos la lógica que hará que se muestren u oculten los elementos. La idea principal es que los elementos aparezcan y se oculten a partir de los datos, por lo que creamos un observador con observe() que ejecute código cada vez que los datos cambien. En este caso, el observador evaluará con un if si el número de filas de los datos (nrow(datos())) es cero, y si llega a ser cero, ocultar el gráfico (hide(\u0026quot;grafico\u0026quot;)) y mostrar un texto de aviso (show(\u0026quot;aviso\u0026quot;)).\n# mostrar y ocultar elementos en base a los datos observe({ if (nrow(datos()) == 0) { hide(\u0026#34;grafico\u0026#34;) show(\u0026#34;aviso\u0026#34;) } else { show(\u0026#34;grafico\u0026#34;) hide(\u0026#34;aviso\u0026#34;) } }) Vale mencionar que debemos especificar también la condición donde el gráfico debe volver a mostrarse, y el aviso debe volver a ocultarse, para que cuando el o la usuaria cambie el filtro y vuelvan a haber datos, el gráfico vuelva a aparecer.\nVeamos la aplicación!\nPuedes ver el código completo de esta aplicación en este Gist de Github.\nNuevamente, le agregamos el tema a la aplicación Shiny para que se vea bonita, y además agregamos la función thematic_shiny() del paquete {thematic} para que los gráficos usen los colores del tema automáticamente.\n","date":"2025-10-10T00:00:00Z","excerpt":"En este post vemos dos tutoriales para aprender a mostrar y ocultar elementos de una app Shiny a partir de datos o inputs del usuario, usando el paquete `{shinyjs}`. Se trata de una habilidad clave para crear aplicaciones que se adapten a datos complejos, cambiantes, o modificables por los usuarios.","href":"https://bastianoleah.netlify.app/blog/shiny_ocultar/","tags":"shiny","title":"Mostrar y ocultar elementos de una app Shiny a partir de datos o inputs"},{"content":"Elevar tus aplicaciones Shiny al siguiente nivel es muy fácil! Una app con un diseño atractivo y profesional puede marcar la diferencia entre que alguien la use o no, o bien, que alguien la recuerde o no!\nEn este tutorial veremos cómo personalizar los temas de colores en tus aplicaciones Shiny utilizando el paquete {bslib}, y además combinaremos esto con la capacidad para que nuestros gráficos {ggplot2} se ajusten automáticamente al tema gracias al paquete {thematic}.\nCrear una aplicación Shiny básica Comencemos con una aplicación Shiny muy básica, que no tiene ningún tema de colores personalizado. El código es el siguiente:\nlibrary(shiny) library(bslib) ui \u0026lt;- page_fluid( h1(\u0026#34;App Shiny ✨\u0026#34;), # selector de opciones selectInput(\u0026#34;animal\u0026#34;, label = \u0026#34;Animalito\u0026#34;, choices = c(\u0026#34;Gatito\u0026#34;, \u0026#34;Gallineta\u0026#34;, \u0026#34;Ratita\u0026#34;)), # deslizador de tamaño sliderInput(\u0026#34;tamaño\u0026#34;, label = \u0026#34;Tamaño\u0026#34;, min = 16, max = 128, value = 48, ticks = FALSE), htmlOutput(\u0026#34;animales\u0026#34;) ) server \u0026lt;- function(input, output, session) { } shinyApp(ui, server) En esta aplicación extra básica tenemos un texto, un selector de alternativas, y un deslizador numérico para seleccionar valores.\nCambiar los colores de una app Shiny Para cambiar el tema de colores, usaremos la función bs_theme() del paquete {bslib}. Esta función nos permite definir los colores principales de la aplicación, y luego aplicarlos a la interfaz de usuario con el argumento theme de la función que usemos para construir la UI o interfaz de nuestra app (en este caso, page_fluid()).\nPara definir los colores del tema, agregamos el argumento theme a page_fluid(), y definimos el tema de la siguiente manera:\ntheme = bs_theme(bg = \u0026#34;#EAD1FA\u0026#34;, fg = \u0026#34;#553A74\u0026#34;, primary = \u0026#34;#8557AB\u0026#34;) Aquí se configuran los tres colores principales: el fondo (bg), el color de los textos (fg), y el color principal (primary) que se usa en los botones, barras de navegación, y otros elementos interactivos.\nEn este caso, pondremos los colores de este mismo sitio 💜\nAgregar funcionalidad a la app Shiny Antes de seguir, agreguemos alguna funcionalidad a nuestra app! En la función server agregaremos el siguiente código para hacer que la app muestre el emoji del animal seleccionado junto a un texto, y el deslizador nos permitirá cambiar el tamaño del animal elegido:\nserver \u0026lt;- function(input, output, session) { # generar animalito con texto y emoji output$animales \u0026lt;- renderUI({ # animal y texto según selección if (input$animal == \u0026#34;Gatito\u0026#34;) { animal \u0026lt;- list(\u0026#34;texto\u0026#34; = \u0026#34;Miu\u0026#34;, \u0026#34;emoji\u0026#34; = \u0026#34;🐈\u0026#34;) } else if (input$animal == \u0026#34;Gallineta\u0026#34;) { animal \u0026lt;- list(\u0026#34;texto\u0026#34; = \u0026#34;Cocorocó\u0026#34;, \u0026#34;emoji\u0026#34; = \u0026#34;🐓\u0026#34;) } else if (input$animal == \u0026#34;Ratita\u0026#34;) { animal \u0026lt;- list(\u0026#34;texto\u0026#34; = \u0026#34;Mimimi\u0026#34;, \u0026#34;emoji\u0026#34; = \u0026#34;🐁\u0026#34;) } # generar interfaz para mostrar el animalito div( # título del animal h3(em(animal$texto)), # emoji del animal div(animal$emoji, # tamaño del animalito style = css(font_size = paste0(input$tamaño, \u0026#34;px\u0026#34;)) ) ) }) } Ahora sí, veamos cómo cambia la app al aplicar este tema:\nQueda súper bonito! ☺️ Encuentra el código completo de esta app en este Gist de Github.\nAplicar el tema de tu app Shiny a tus gráficos Ahora que definimos colores base para el tema de la aplicación, podemos usar {thematic} para aplicar los mismos colores a los gráficos, tal como vimos en el tutorial de temas para {ggplot2}..\nPrimero hagamos una app básica que muestre un gráfico:\nlibrary(shiny) library(bslib) library(dplyr) library(ggplot2) ui \u0026lt;- page_fluid( br(), h1(\u0026#34;App Shiny ✨\u0026#34;), # selector de números sliderInput(\u0026#34;filtro\u0026#34;, label = \u0026#34;Filtrar\u0026#34;, min = 5, max = 10, value = 1), # gráfico plotOutput(\u0026#34;grafico_barras\u0026#34;, width = 320, height = 200) ) server \u0026lt;- function(input, output, session) { datos \u0026lt;- reactive({ iris |\u0026gt; filter(Sepal.Length \u0026gt;= input$filtro) }) # generar un gráfico de barras output$grafico_barras \u0026lt;- renderPlot({ datos() |\u0026gt; ggplot() + aes(Sepal.Length, Sepal.Width) + geom_point() }) } shinyApp(ui, server) Esta aplicación tiene un selector de números que filtran los datos, que a su vez alimentan el gráfico. Al cambiar la selección de números, el gráfico se actualiza gracias a la reactividad del objeto datos().\nAplicar el tema es tan sencillo como agregar el tema en el argumento theme de page_fluid():\ntheme = bs_theme(bg = \u0026#34;#EAD1FA\u0026#34;, fg = \u0026#34;#553A74\u0026#34;, primary = \u0026#34;#8557AB\u0026#34;) Luego, para que el gráfico adopte la paleta de colores del tema, solamente tenemos que cargar el paquete {thematic} con library(thematic), y luego activar el uso de temas con thematic_shiny() antes de la interfaz/UI, y listo! No es necesario cambiar nada en el código del gráfico ni del resto de la aplicación.1\nLa parte superior de la aplicación quedaría así:\nlibrary(shiny) library(bslib) library(dplyr) library(ggplot2) library(thematic) thematic_shiny() # activar el tema ui \u0026lt;- page_fluid( # definir los colores del tema theme = bs_theme(bg = \u0026#34;#EAD1FA\u0026#34;, fg = \u0026#34;#553A74\u0026#34;, primary = \u0026#34;#8557AB\u0026#34;) # el resto de la app... Listo! Así de fácil. Ahora no hay excusas para hacer que tus aplicaciones se vean bonitas y dejen una buena impresión en sus usuarios.\na menos que el gráfico ya tenga aplicado un tema de {ggplot2} como theme_minimal(), en cuyo caso habría que eliminarlo para que el tema de {thematic} funcione correctamente.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-10-09T00:00:00Z","excerpt":"En este post muestro lo básico para personalizar la apariencia de tus aplicaciones Shiny con temas de colores personalizados usando el paquete `{bslib}`, y además cómo hacer que los gráficos `{ggplot2}` se ajusten automáticamente al tema usando {thematic}. Recuerda que una app con un diseño atractivo puede marcar la diferencia entre que alguien la use o no, o bien, que alguien la recuerde o no!","href":"https://bastianoleah.netlify.app/blog/shiny_temas/","tags":"shiny","title":"Temas de colores personalizados en apps Shiny"},{"content":"Esta plataforma visualiza datos de ingresos a nivel regional, comunal, y por género.\nSu objetivo es visibilizar diferencias de los ingresos recibidos por las personas, permitiendo compararlos por regiones y comunas, así como también producir comparaciones de brechas de género entre hombres y mujeres.\nLa fuente de los datos está en el Banco Integrado de Datos de Mideso.\nDatos Obtén los datos procesados en formato csv en este enlace.\nVisualizador Accede al visualizador web en este enlace.\nAnimaciones Las siguientes son grabaciones del proceso de visualización de datos, grabadas con el paquete {camcorder}.\n","date":"2025-09-24T00:00:00Z","excerpt":"Visualizador sencillo de datos de ingresos de Chile a nivel regional, comunal, y por género. Su objetivo es visibilizar diferencias de los ingresos recibidos por las y los trabajadores, permitiendo compararlos por regiones y comunas, así como también producir comparaciones de brechas de género entre hombres y mujeres.","href":"https://bastianoleah.netlify.app/blog/app_mideso_ingresos_genero/","tags":"apps ; datos ; Chile ; género ; ciencias sociales ; animaciones","title":"App: comparación de ingresos regionales y comunales"},{"content":" Infografía que recopila visualizaciones de datos acerca de los crímenes de lesa humanidad cometidos por el régimen dictatorial en Chile, entre entre 1973 y 1989.\nEntrar a la infografía El objetivo es poder compartir información de forma pedagógica y clara, para que las nuevas generaciones puedan comprender la magnitud de las violaciones a los derechos humanos cometidas en ese período, y así contribuir a la memoria histórica del país.\nAquí dejo un video cortito que hice mostrando la página web y explicando un poco las motivaciones:\n@bastimapache Septiembre es el mes de la memoria! Revisa la infografía en el link de mi perfil, y ayúdame a mejorarla 🙏🏼 #chile #septiembre ♬ Ventolera - Quilapayún Se buscan contribuciones, como por ejemplo relatos, estadísticas y datos, contribución de textos y similares. Contáctame!\nActualizaciones 10/09/2025: fichas de principales violadores de derechos humanos: Krasnoff, Contreras, Corvalán, Romo y Moren. 10/09/2025: apartados con caso de los 119, caso de Michel Nash, y caso degollados 09/09/2025: apartado sobre censura 08/09/2025: apartado sobre segregación, posibilidad de hacer zoom a fotos 07/09/2025: apartado sobre infancias, línea de tiempo 03/09/2025: apartados con relatos sobre casos insignes de víctimas: caso Sebastián Acevedo y caso quemados 01/09/2025: secciones dispersas de testimonios de sobrevivientes 31/08/2025: nueva sección de mujeres detenidas desaparecidas embarazadas Notas técnicas La página fue construida completamente usando Quarto desde RStudio, publicando el sitio gratuitamente a GitHub Pages.\n","date":"2025-09-11T00:00:00Z","excerpt":"Infografía que recopila visualizaciones de datos acerca de los crímenes de lesa humanidad cometidos por el régimen dictatorial en Chile, entre entre 1973 y 1989. El objetivo es poder compartir información de forma pedagógica y clara, para que las nuevas generaciones puedan comprender la magnitud de las violaciones a los derechos humanos cometidas en ese período, y así contribuir a la memoria histórica del país.","href":"https://bastianoleah.netlify.app/blog/infografia_violaciones_ddhh_chile/","tags":"apps ; Chile","title":"Infografía: Violaciones a los Derechos Humanos cometidas por la dictadura cívico-militar chilena"},{"content":"El otro día tuve que usar R para generar comandos muy largos (para automatizar el convertir cientos de reportes html a pdf usando un navegador web), y el código resultante fue eterno. Al pegarlo en la consola se produjo un aluvión de texto:\nEntre el océano de texto y código aparecen algunas figuras diagonales con cierta regularidad. Es un fenómeno que me había pasado antes, y que siempre llamó mi atención.\nCuando programo loops me gusta hacer que R imprima mensajes de estado en cada paso (con message() o cat()) para saber cómo avanza el proceso, y en algunos casos esto genera un caos de texto, particularmente cuando los procesos son demasiado rápidos.\nMe interesaba replicar estas situaciones. Lo más básico sería hacer un loop que imprima todos los números del 1 al n. Una operación de este tipo es instantánea, ya que no tiene mayor complejidad, así que para que se note mejor, le ponemos una breve espera1 de una milésima de segundo entre cada paso:\n# función para pausar entre cada paso wait \u0026lt;- Sys.sleep # loop desde el 1 al 10000 for (i in 1:10000) { cat(i) # imprimir valor a la consola wait(0.001) # tomarse una pausa # repetir } El loop simplemente imprime a la consola el valor que le toca en cada paso, empezando por el 1 y terminando en el 10.000. Pero el movimiento de cientos de caracteres ASCII en la pantalla hace que emerjan patrones visuales interesantes.\nMe puse a explorar formas de crear distintos patrones de código que producen estas secuencias de ruido visual.\nPrimero definimos algunas constantes para la cantidad de pasos de los loops y la espera entre cada paso:\nn \u0026lt;- 60000 t \u0026lt;- 0.0002 Si en cada paso imprimimos una cantidad al azar de barras (|), e imprimimos estas barras en los números impares y espacios vacíos en los pares, obtenemos una especie de fondo blanco con muchos espacios distribuidos aleatoriamente.\nfor (i in 1:n) { cantidad \u0026lt;- sample(1:16, 1) # números al azar entre 1 y 16 barras \u0026lt;- paste(rep(\u0026#34;|\u0026#34;, cantidad), collapse = \u0026#34;\u0026#34;) # cantidad de barras if (i %% 2 == 0) cat(\u0026#34; \u0026#34;) else cat(barras) # imprimir vacío si el número es par, barras si es impar wait(t) # espera antes del siguiente paso } Si en cada paso del loop imprimimos una cantidad al azar de espacios en blanco, imprimimos estos espacios solamente en los pasos divisibles por 10, y el resto de los pasos imprimimos una barra, obtenemos un patrón de ruido con figuras de vacío contínuas hacia abajo, que parecen líneas de zebra.\nfor (i in 1:n) { espacios \u0026lt;- rep(\u0026#34; \u0026#34;, sample(1:6, 1)) # espacios al azar entre 1 y 6 vacio \u0026lt;- paste(espacios, collapse = \u0026#34;\u0026#34;) # unir los espacios resultantes if (i %% 10 == 0) cat(vacio) else cat(\u0026#34;|\u0026#34;) # imprimir vacío en todos los pasos excepto en múltiplos de 10 wait(3e-04) # espera } En el siguiente, por cada número de los pasos del loop, se imprime una cantidad de barras (si el número es impar) o vacío (si el número es par) dependiendo del último dígito del número. Por ejemplo, en el paso 543 se imprimen 3 barras, porque 3 es el último número de 543, y es impar.\nSe genera una especie de cuadrantes con cierto degradado, o diagonales crecientes con espaciado y figuras geométricas, dependiendo del tamaño del texto y/o de la ventana de la consola.\nfor (i in 1:n) { vacio \u0026lt;- paste(rep(\u0026#34; \u0026#34;, i %% 10), collapse = \u0026#34;\u0026#34;) barras \u0026lt;- paste(rep(\u0026#34;|\u0026#34;, i %% 10), collapse = \u0026#34;\u0026#34;) if (i %% 2 == 0) cat(vacio) else cat(barras) wait(2e-03) } Encuentro maravilloso que los patrones cambien según el tamaño de la consola (la cantidad de columnas de texto, en realidad), y espero poder explorar más en ese sentido.\nEn este otro intento generé un número al azar en cada paso, y se imprimen barras y vacíos en esa cantidad, dependiendo de si el paso es es par o impar. El resultado es un ruido con barras anchas, que genera manchas que se conectan entre si.\nfor (i in 1:n) { cantidad \u0026lt;- sample(1:18, 1) barras \u0026lt;- paste(rep(\u0026#34;|\u0026#34;, cantidad), collapse = \u0026#34;\u0026#34;) vacio \u0026lt;- paste(rep(\u0026#34; \u0026#34;, cantidad), collapse = \u0026#34;\u0026#34;) if (i %% 2 == 0) cat(vacio) else cat(barras) wait(2e-03) } Para el siguiente intento me basé en el código anterior, pero en un loop sobre los números de una distribución poisson con un lambda de 1. El resultado es mucho más ruidoso, con figuras geométricas esporádicas interesantes.\nfor (i in rpois(n, 1)) { barras \u0026lt;- paste(rep(\u0026#34;|\u0026#34;, i), collapse = \u0026#34;\u0026#34;) vacio \u0026lt;- paste(rep(\u0026#34; \u0026#34;, i), collapse = \u0026#34;\u0026#34;) if (i %% 2 == 0) cat(vacio) else cat(barras) wait(t) } Si se crea una secuencia de pares de vacío y barras del mismo largo (||| ||| |||), se generan barras verticales o diagonales dependiendo de la cantidad de columnas de la consola.\nfor (i in 1:n) { cantidad \u0026lt;- 8 barras \u0026lt;- paste(rep(\u0026#34;|\u0026#34;, cantidad), collapse = \u0026#34;\u0026#34;) vacio \u0026lt;- paste(rep(\u0026#34; \u0026#34;, cantidad), collapse = \u0026#34;\u0026#34;) if (i %% 2 == 0) cat(vacio) else cat(barras) wait(2e-3) } En esta última prueba generé dos loops dentro de cada paso del loop principal, donde los loops internos van del 1 al 4 creando una secuencia que genera esa cantidad de elementos, una vez del 1 al 4, y luego del 4 al 1:\n| | | | | | | | | El resultado son secuencias que en ciertos anchos de consola se ven onduladas, luego se producen patrones diagonales más espaciados entre ellos.\nfor (i in 1:n) { x = 4 for (y in 1:x) { cat(rep(\u0026#34; \u0026#34;, y) |\u0026gt; paste(collapse = \u0026#34;\u0026#34;)) cat(\u0026#34;|\u0026#34;) } for (y in x:1) { cat(rep(\u0026#34; \u0026#34;, y) |\u0026gt; paste(collapse = \u0026#34;\u0026#34;)) cat(\u0026#34;|\u0026#34;) } wait(t*20) } Me encantó esta primera aproximación al arte generativo, y ojalá poder seguir descrubriendo nuevas formas de generar patrones interesantes! Sería ideal desarrollar una aplicación interactiva donde los patrones fluyan continuamente y vayan siendo alterados directamente desde el código, en tiempo real.\nLa función Sys.sleep(x) hace que R pause por X segundos. Acá renombré la función a wait(), simplemente porque me carga el nombre Sys.sleep con mayúscula y puntos.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-08-27T00:00:00Z","excerpt":"Una aproximación al arte generativo ASCII por medio de loops de código que generan secuencias de textos aleatorias, produciendo patrones de ruido interesantes.","href":"https://bastianoleah.netlify.app/blog/2025-08-27/","tags":"curiosidades ; arte generativo ; loops","title":"Generando ruido visual con R"},{"content":"¿Quieres darle un toque personalizado a tus gráficos? O tal vez alinearlos mejor al mensaje que quieres entregar, o a los lineamientos estéticos de tu organización. En esta breve guía te explico cómo cambiar las tipografías, tipos de letra o fuentes de tus gráficos hechos en R con {ggplot2}.\nPara demostrar, primero creemos datos ficticios siguiento distribuciones normales:\nlibrary(ggplot2) library(dplyr) # crear datos al azar datos \u0026lt;- tibble(a = rnorm(20, mean = 10, sd = 1), b = rnorm(20, mean = 10, sd = 1), c = round(rnorm(20, mean = 10, sd = 1), 1)) Ahora creemos un gráfico de dispersión como muestra, con un poco de texto extra:\ngrafico \u0026lt;- datos |\u0026gt; ggplot() + aes(x = a, y = b) + geom_point(size = 2, alpha = 0.4) + labs(title = \u0026#34;Gráfico de dispersión\u0026#34;, subtitle = \u0026#34;Números al azar\u0026#34;, x = \u0026#34;Eje horizontal\u0026#34;, y = \u0026#34;Eje vertical\u0026#34;, caption = \u0026#34;Fuente: de los deseos\u0026#34;) + theme_light(base_size = 12) + theme(plot.title = element_text(face = \u0026#34;bold\u0026#34;)) Por defecto, {ggplot2} usa las tipografías Helvetica en Mac y Arial en Windows:\ngrafico A pesar de no ser malas tipografías, se vuelven aburridas rápidamente.\nSi necesitas aprender a usar {ggplot2} para gráficos en R, primero revisa este tutorial introductorio! Tipografías instaladas Podemos cambiar la tipografía de los gráficos {ggplot2} fácilmente si éstas se encuentran instaladas en tu computador. Por ejemplo, La tipografía Menlo que viene por defecto en Mac.\nPara establecer la tipografía de los gráficos, modificamos desde el tema (theme()) los elementos de texto (text = element_text()), especificando en el argumento family el nombre de la tipografía:\n# especificar tipografía desde el tema grafico + theme(text = element_text(family = \u0026#34;Menlo\u0026#34;)) Si agregamos capas de texto al gráfico (con geom_text(), geom_label() u otros), tenemos que especificar la tipografía que usaremos en cada capa. En el siguiente ejemplo, agregamos con geom_text_repel() una capa de texto que se repele de los puntos y otras etiquetas de texto automáticamente.\n# especificar tipografía para geom_text() y también para el tema grafico + ggrepel::geom_text_repel(aes(label = c), family = \u0026#34;Menlo\u0026#34;) + theme(text = element_text(family = \u0026#34;Menlo\u0026#34;)) Tipografías web desde Google Fonts Si no tienes la tipografía que buscas instalada en tu computadora, puedes usar tipografías web sin necesidad de instalar ni descargar nada. En la página Google Fonts existe un catálogo de cientos de tipografías de todo tipo, gratuitas y de código abierto.\nEntra a Google Fonts y busca una tipografía. Recomiendo seleccionar tipografías en Español (para que tengan soporte para tildes y eñes), y usar las opciones del lado izquierdo para encontrar tipografías por varias cualidades, como que evoquen calma o innovación, o según el estilo de fuente como serif (con terminaciones al final de los trazos) o sans serif (sin terminaciones al final de los trazos).\nCuando la encuentres, copia el nombre de la tipografía para agregarla a tu entorno de R con la función font_add_google() del paquete {showtext}. Este paquete te permite agregar tipografías a R para usarlas en tus gráficos.\nPara usar la tipografía debes agregarla con font_add_google(), luego activar {showtext} ejecutando showtext_auto(), y finalmente especificar la resolución de tu pantalla con showtext_opts(dpi = 300).\nlibrary(showtext) # descargar una tipografía desde google fonts font_add_google(name = \u0026#34;Pacifico\u0026#34;) # activar tipografías showtext_auto() showtext_opts(dpi = 300) # usar la tipografía desde Google Fonts grafico + ggrepel::geom_text_repel(aes(label = c), family = \u0026#34;Pacifico\u0026#34;) + theme(text = element_text(family = \u0026#34;Pacifico\u0026#34;)) Ahora puedes usar cientos de tipografía con tus gráficos en R! 🥳 Solamente busca la que necesites en Google Fonts y podrás usarla en {ggplot2}.\nTipografías descargadas Si descargaste tipografías desde otros sitios (en formatos como ttf, otf, woff y otros) también puedes agregarlas a R con {showtext}. Para agregar las tipografías, usa font_add(), especificando los archivos que corresponden a los estilos de letras como regular, bold e italic.\nPor ejemplo, vamos a Google Fonts y descargamos la tipografía Arvo.\nal descargarla obtendremos archivos como los siguientes:\nArvo-Regular.ttf Arvo-Bold.ttf Arvo-Italic.ttf Agregamos las tipografías a R con font_add(), asumiendo que las guardamos en una carpeta llamada tipografías:\n# agregar una tipografía desde un archivo font_add(\u0026#34;Arvo\u0026#34;, regular = \u0026#34;tipografías/Arvo-Regular.ttf\u0026#34;, bold = \u0026#34;tipografías/Arvo-Bold.ttf\u0026#34;, italic = \u0026#34;tipografías/Arvo-Italic.ttf\u0026#34;) Luego seguimos los mismos pasos de antes para usarlas en nuestros gráficos {ggplot2}:\n# activar tipografías showtext_auto() showtext_opts(dpi = 300) # probar tipografía descargada como archivo desde Google Fonts grafico + ggrepel::geom_text_repel(aes(label = c), family = \u0026#34;Arvo\u0026#34;) + theme(text = element_text(family = \u0026#34;Arvo\u0026#34;)) Este método puede optimizar la ejecución de nuestros scripts, dado que la función font_add_google() descarga las tipografías cada vez que ejecutamos la función, mientras que si descargamos los archivos directamente no necesitamos descargar múltiples veces los archivos.\nTambién se puede usar la función setup_font(\u0026quot;arvo\u0026quot;, \u0026quot;tipografías\u0026quot;) del paquete {gfonts} para descargar automáticamente las tipografías de Google Fonts en tu proyecto de R, y así usarlas sin descargas ni necesidad de conexión a internet. Para averiguar los nombres de las tipografías puedes ejecutar gfonts::get_all_fonts().\n","date":"2025-08-26T00:00:00Z","excerpt":"¿Quieres darle un toque personalizado a tus gráficos? O tal vez alinearlos mejor al mensaje que quieres entregar, o a los lineamientos estéticos de tu organización. En esta breve guía te explico cómo cambiar las tipografías, tipos de letra o fuentes de tus gráficos hechos en R con `{ggplot2}`, incluyendo la posibilidad de usar cientos de tipografías web gratuitas directamente desde Google Fonts.","href":"https://bastianoleah.netlify.app/blog/ggplot_tipografias/","tags":"visualización de datos ; ggplot2","title":"Tipografías personalizadas en gráficos {ggplot2}"},{"content":"Si necesitas saber a qué hora es un evento que ocurre en otro país, puedes usar {lubridate} para convertir las zonas horarias:\nPrimero definimos la fecha del evento, la zona horaria del evento y tu zona horaria:\nfecha_evento \u0026lt;- \u0026#34;2025-08-22 18:00:00\u0026#34; zona_horaria_evento \u0026lt;- \u0026#34;Mexico/General\u0026#34; zona_horaria_local \u0026lt;- \u0026#34;Chile/Continental\u0026#34; Si no sabes cuál es tu zona horaria, en esta tabla hay una lista de identificadores de zona horaria (TZ Identifier).\nLuego usamos {lubridate} para convertir la fecha/hora en al zona horaria de tu interés:\nlibrary(lubridate) ymd_hms(fecha_evento, tz = zona_horaria_evento) |\u0026gt; with_tz(zona_horaria_local) [1] \u0026quot;2025-08-22 20:00:00 -04\u0026quot; Ahora sé que el taller que voy a dar para estudiantes de México va a ser a las 20 horas de Chile y no a las 16 como yo pensaba 🙄😂\nOtro ejemplo:\nlibrary(lubridate) ymd_hms(\u0026#34;2025-08-27 16:00:00\u0026#34;) |\u0026gt; with_tz(\u0026#34;Europe/Vienna\u0026#34;) [1] \u0026quot;2025-08-27 18:00:00 CEST\u0026quot; Este tip lo aprendí en el sitio de RainborR, una comunidad de usuarixs de R que también son parte de la comunidad LGBT+ 🏳️‍🌈\n","date":"2025-08-22T00:00:00Z","excerpt":"Si necesitas saber a qué hora es un evento que ocurre en otro país, puedes usar `{lubridate}` para convertir las zonas horarias.","href":"https://bastianoleah.netlify.app/blog/2025-08-22/","tags":"consejos ; curiosidades ; fechas","title":"Convertir zonas horarias con R"},{"content":"El left join es una de las operaciones básicas del trabajo con datos, en el sentido de que realiza una operación sencilla que a la vez es muy útil. Sirve tanto para limpiar datos como para procesarlos y obtener nuevas relaciones entre ellos.\nUn left join realiza una unión o combinación entre dos tablas de datos a partir de una variable en común o clave (key). En otras palabras, un left join toma dos tablas que tienen datos distintos, pero que comparten una variable o columna en común, y usa esta variable en común para unir las observaciones de ambas tablas.\nEsquema de una unión entre tablas con left join En el ejemplo anterior, tenemos dos tablas, ambas con tres variables distintas, pero en ambas tablas tenemos una variable que contiene información equivalente. En la vida real, esto podrían ser nombres de personas, nombres de países, identificadores únicos de personas o instituciones, fechas, etcétera. Esta variable compartida entre las tablas es la llave que usaremos para la unión.\nAl realiazr la unión con left join obtenemos una nueva tabla que combina las observaciones de ambas, uniendo las filas de la primera tabla (x) con las filas de al segunda tabla (y) que se correspondan según la llave en común.\nEsquema del resultado del left join Ejemplo de una unión de tablas en R Para unir dos tablas en R, usamos la función left_join() de {dplyr}.\nlibrary(dplyr) Primero creemos una tabla de base con datos de ejemplo.\nfrutas_x \u0026lt;- tibble(fruta = c(\u0026#34;pera\u0026#34;, \u0026#34;manzana\u0026#34;, \u0026#34;uva\u0026#34;), color = c(\u0026#34;verde\u0026#34;, \u0026#34;roja\u0026#34;, \u0026#34;morada\u0026#34;)) fruta color pera verde manzana roja uva morada En esa tabla tenemos tres observaciones que corresponden a animales, descritos en la primera columna, y una segunda columna con características de los mismos.\nAhora creemos una segunda tabla, que además de tener la misma columna que describe las frutas, agrega una nueva variable sobre las frutitas 🍐🍎🍇\nfrutas_y \u0026lt;- tibble(fruta = c(\u0026#34;pera\u0026#34;, \u0026#34;manzana\u0026#34;, \u0026#34;uva\u0026#34;), sabor = c(\u0026#34;deliciosa\u0026#34;, \u0026#34;buena\u0026#34;, \u0026#34;ricolina\u0026#34;)) fruta sabor pera deliciosa manzana buena uva ricolina Dado que ambas tablas comparten la variable fruta, si hacemos un left join ambas tablas se cruzarán en base a esta variable, resultando en una tabla nueva:\nfrutas_2 \u0026lt;- left_join(frutas_x, frutas_y) Joining with `by = join_by(fruta)` fruta color sabor pera verde deliciosa manzana roja buena uva morada ricolina El resultado de la unión es una nueva tabla que tiene las tres variables únicas obtenidas del cruce de las dos tablas distintas. En este caso, el left join nos permite agregar información sobre una misma unidad de observación proveniente de distintas fuentes.\nOtro ejemplo Veamos un segundo ejemplo con más filas y más columnas, ésta vez sobre animalitos 🐈🐀🐕\nanimales_x \u0026lt;- tibble(animal = c(\u0026#34;gato\u0026#34;, \u0026#34;ratón\u0026#34;, \u0026#34;perro\u0026#34;, \u0026#34;pez\u0026#34;), color = c(\u0026#34;gris\u0026#34;, \u0026#34;negro\u0026#34;, \u0026#34;blanco\u0026#34;, \u0026#34;azul\u0026#34;), tamaño = c(34, 16, 50, 3)) animal color tamaño gato gris 34 ratón negro 16 perro blanco 50 pez azul 3 La primera tabla contiene los nombres de animales (identificador único), sus colores y sus medidas. La segunda tabla también contiene nombres, pero agrega su cantidad de patas1 y sus edades.\nanimales_y \u0026lt;- tibble(animal = c(\u0026#34;perro\u0026#34;, \u0026#34;gato\u0026#34;, \u0026#34;pez\u0026#34;), patas = c(4, 3, 0), edad = c(8, 3, 1)) animal patas edad perro 4 8 gato 3 3 pez 0 1 Las dos tablas tienen distinta cantidad de observaciones, y además las observaciones clave (columna animal) que se usarán como llave de unión están desordenadas.\nCuando los data frames tienen distinta cantidad de filas, el orden en que hacemos la unión es importante: en un left join la primera tabla (o izquierda) es la tabla principal, a la cual se le agregan las columnas de una segunda tabla, en base a las coincidencias de la columna clave. Por lo tanto, si unimos o cruzamos las tablas poniendo primero la tabla más grande y después la más pequeña, obtendremos casos perdidos por las filas de la primera tabla que no obtuvieron coincidencias en la segunda.\nanimales_3 \u0026lt;- left_join(animales_x, animales_y) Joining with `by = join_by(animal)` animal color tamaño patas edad gato gris 34 3 3 ratón negro 16 NA NA perro blanco 50 4 8 pez azul 3 0 1 Como podemos ver, obtenemos celdas con NA en la observación ratón, debido a que en la segunda tabla (animales_y) no habían datos sobre este animalito. Una pena 😔\nTambién sería una opción hacer el cruce al revés, poniendo primero la tabla animales_y (3 filas) y después animales_x (4 filas), con el objetivo de que las observaciones de la tabla animales_x complementen las observaciones de animales_y donde hayan coincidencias, y descartando los casos que no tienen en común:\nanimales_3 \u0026lt;- left_join(animales_y, animales_x) Joining with `by = join_by(animal)` animal patas edad color tamaño perro 4 8 blanco 50 gato 3 3 gris 34 pez 0 1 azul 3 Ahora no tenemos datos perdidos, porque si bien animales_x tenía más observaciones que animales_y, animales_y contenía todas las observaciones de animales_x identificadas por la variable animal, lo que podemos confirmar comparando las diferencias entre ambas columnas con la función waldo::compare():\nlibrary(waldo) compare(sort(animales_x$animal), sort(animales_y$animal)) `old`: \u0026quot;gato\u0026quot; \u0026quot;perro\u0026quot; \u0026quot;pez\u0026quot; \u0026quot;ratón\u0026quot; `new`: \u0026quot;gato\u0026quot; \u0026quot;perro\u0026quot; \u0026quot;pez\u0026quot; Caso licencias falsas Un caso icónico de uso de left join fue el estudio realizado por la Contraloría General de la República de Chile, donde se cruzaron bases de datos que tenían en común la posibilidad de identificar funcionarios públicos:\nSe trata de un estudio a partir del cruce de información de las salidas del país registradas por la Policía de Investigaciones (PDI), la base de funcionarios públicos y las licencias médicas que se otorgaron entre el 2023 y 2024. (Fuente: Bío Bío)\nLa hipótesis sería que una persona que sale del país durante su licencia médica sería probablemente una persona que consiguió una licencia falsa para tomarse vacaciones 🏖️☀️\nSimulemos lo que podría haber hecho la Contraloría para detectar estos casos. Asumimos que contaron con (al menos) tres bases de datos: una de funcionarios públicos, una de licencias médicas, y otra de viajes al extranjero, las tres teniendo en común el RUN (código de identificación único de ciudadanos chilenos) para poder cruzar las personas.\noptions(scipen = 9999) funcionarios \u0026lt;- tibble(run = c(10000001, 10000002, 10000003), nombre = c(\u0026#34;maría\u0026#34;, \u0026#34;pepito\u0026#34;, \u0026#34;juanito\u0026#34;), ministerio = c(\u0026#34;defensa\u0026#34;, \u0026#34;economía\u0026#34;, \u0026#34;hacienda\u0026#34;)) run nombre ministerio 10000001 maría defensa 10000002 pepito economía 10000003 juanito hacienda Tenemos identificados tres funcionarios públicos de distintos servicios.\nLuego tenemos una base de datos con licencias médicas de trabajadores que pueden o no ser funcionarios públicos. Con la base de los RUN de funcionarios públicos podemos identificar si las licencias corresponden a funcionarios públicos y no a trabajadores del sector privado.\nlicencias \u0026lt;- tibble(run = c(10000001, 10000002, 10000003, 10000004), licencia = c(FALSE, FALSE, TRUE, TRUE), licencia_inicio = c(NA, \u0026#34;2024-04-10\u0026#34;, \u0026#34;2024-02-04\u0026#34;, \u0026#34;2010-09-18\u0026#34;), licencia_fin = c(NA, \u0026#34;2024-04-13\u0026#34;, \u0026#34;2024-02-18\u0026#34;, \u0026#34;2030-12-31\u0026#34;)) run licencia licencia_inicio licencia_fin 10000001 FALSE NA NA 10000002 FALSE 2024-04-10 2024-04-13 10000003 TRUE 2024-02-04 2024-02-18 10000004 TRUE 2010-09-18 2030-12-31 Lo importante de esta tabla es que tenemos una fecha de inicio y una fecha de fin de la licencia médica.\nFinalmente tenemos la información de salidas del país de distintas personas, que pueden o no ser funcionarios públicos, y que pueden o no haber estado con licencia médica 🤔\nviajes \u0026lt;- tibble(run = c(10000005, 10000001, 10000002, 10000003, 10000008), viaje_destino = c(\u0026#34;Bolivia\u0026#34;, \u0026#34;Perú\u0026#34;, \u0026#34;Argentina\u0026#34;, \u0026#34;Italia\u0026#34;, \u0026#34;España\u0026#34;), viaje_fecha = c(\u0026#34;2021-01-06\u0026#34;, \u0026#34;2021-02-15\u0026#34;, \u0026#34;2023-06-23\u0026#34;, \u0026#34;2024-02-09\u0026#34;, \u0026#34;2025-08-17\u0026#34;) ) run viaje_destino viaje_fecha 10000005 Bolivia 2021-01-06 10000001 Perú 2021-02-15 10000002 Argentina 2023-06-23 10000003 Italia 2024-02-09 10000008 España 2025-08-17 Ahora que tenemos datos simulados, usaremos {dplyr} para trabajar con los datos y encontrar respuestas.\nSi necesitas aprender lo básico del manejo de datos con R, revisa este tutorial básico de {dplyr}! Lo primero sería filtrar las licencias médicas para que solamente correspondan con funcionarios públicos (también podría hacerse con left_join() pero creo que es más pertinente un filtro)\nlicencias_f \u0026lt;- licencias |\u0026gt; # sólo funcionarios públicos filter(run %in% funcionarios$run) Luego, cruzamos los datos de licencias de funcionarios públicos con el registro de salidas del país:\n# cruzar con viajes cruce \u0026lt;- licencias_f |\u0026gt; left_join(viajes, join_by(run)) Finalmente revisamos si los viajes fueron durante el tiempo de licencia médica, y si los viajes fueron fuera del país. Podemos hacer esto con un filtro, o preferentemente creando una columna nueva (irregular) que indique si se cumple o no la evaluación.\n# revisar si viajaron fuera del país durante licencia resultado \u0026lt;- cruce |\u0026gt; # crear una nueva variable mutate(irregular = licencia == TRUE \u0026amp; # personas on licencia viaje_destino != \u0026#34;Chile\u0026#34; \u0026amp; # viajes fuera de Chile between(viaje_fecha, licencia_inicio, licencia_fin) # viaje durante el tiempo de licencia ) |\u0026gt; select(-viaje_destino) run licencia licencia_inicio licencia_fin viaje_fecha irregular 10000001 FALSE NA NA 2021-02-15 FALSE 10000002 FALSE 2024-04-10 2024-04-13 2023-06-23 FALSE 10000003 TRUE 2024-02-04 2024-02-18 2024-02-09 TRUE ⚠️ Detectamos un caso de funcionario público que viajó fuera del país durante su licencia médica! La última columna marca TRUE si se trata de un caso irregular.\nConclusión Podemos usar left_join() para completar los datos sobre nuestra unidad de análisis si la información viene diseminada en distintas tablas o archivos. Pero también podemos utilizarla para complementar datos desde distintas fuentes, aumentando la información que tenemos sobre las observaciones que estemos estudiando, o bien, para realizar cruces de información que nos entreguen coincidencias relevantes.\nel gatito lamentablemente sufrió un accidente y perdió una patita trasera 😿\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-08-16T00:00:00Z","excerpt":"Un _left join_ realiza una unión o combinación entre dos tablas de datos a partir de una variable en común o _clave_ (_key_). En otras palabras, un _left join_ toma dos tablas que tienen datos distintos, pero que comparten una variable o columna en común, y usa esta variable en común para unir las observaciones de ambas tablas. En este tutorial explico a hacer _left joins_ con frutas, animales, y uso irregular de licencias médicas.","href":"https://bastianoleah.netlify.app/blog/left_join/","tags":"procesamiento de datos ; limpieza de datos ; básico","title":"Unir o cruzar datos con left_join()"},{"content":"Al terminar el procesamiento o limpieza de un conjunto de datos, usualmente necesitamos entregar la base en un formato más amigable para otros colegas o usuarios/as, lo que significa: Excel. Exportar datos de R a Excel es fácil: con la función writexl::write_xlsx() podemos guardar cualquier dataframe en Excel. Pero esta función hace sólo eso, y el archivo resultante es básico y hasta feo.\nSin embargo, existe el paquete {openxlsx}, que además de leer y escribir archivos Excel, nos entrega funciones para generar archivos Excel desde R que contengan todo tipo de formato, estilo de tablas, y manipulación celda por celda del archivo resultante, con lo que podemos generar planillas de Excel atractivas desde R.\nLa gracia es que podremos usar estas herramientas de forma reproducible (poder re-hacer gratis la planilla con datos actualizados o corregidos), y también podremos combinar la programación de R para crear estilos condicionales, crear cientos de archivos Excel, o planillas de cientos de hojas, y mucho más.\nCarguemos un conjunto de datos de indicadores de calidad de vida urbana de Chile, del Sistema de Indicadores y Estándares de Desarrollo Urbano.\nlibrary(arrow) datos \u0026lt;- read_parquet(\u0026#34;https://github.com/bastianolea/siedu_indicadores_urbanos/raw/main/datos/siedu_indicadores_desarrollo_urbano.parquet\u0026#34;) datos # A tibble: 6,701 × 12 codigo_comuna codigo_region codigo_provincia nombre_region nombre_provincia \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 1107 01 011 Tarapacá Iquique 2 1107 01 011 Tarapacá Iquique 3 1107 01 011 Tarapacá Iquique 4 1107 01 011 Tarapacá Iquique 5 1107 01 011 Tarapacá Iquique 6 1107 01 011 Tarapacá Iquique 7 1107 01 011 Tarapacá Iquique 8 1107 01 011 Tarapacá Iquique 9 1107 01 011 Tarapacá Iquique 10 1107 01 011 Tarapacá Iquique # ℹ 6,691 more rows # ℹ 7 more variables: nombre_comuna \u0026lt;chr\u0026gt;, id \u0026lt;chr\u0026gt;, año \u0026lt;dbl\u0026gt;, variable \u0026lt;chr\u0026gt;, # valor \u0026lt;dbl\u0026gt;, medida \u0026lt;chr\u0026gt;, estandar \u0026lt;chr\u0026gt; Filtremos los datos para dejar tres indicadores, y sólo los valores más recientes para cada unidad territorial.\nlibrary(dplyr) datos \u0026lt;- datos |\u0026gt; # filtrar variables filter(variable %in% c(\u0026#34;Distancia a plazas públicas\u0026#34;, \u0026#34;Número de víctimas mortales en siniestros de tránsito por cada 100.000 habitantes\u0026#34;, \u0026#34;Porcentaje de cobertura de la red de ciclovía sobre la red vial\u0026#34;)) |\u0026gt; # dejar sólo valores más recientes por comuna group_by(nombre_region, nombre_comuna, variable) |\u0026gt; filter(año == max(año)) |\u0026gt; ungroup() datos |\u0026gt; select(-starts_with(\u0026#34;codigo\u0026#34;)) # A tibble: 340 × 9 nombre_region nombre_provincia nombre_comuna id año variable valor \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Tarapacá Iquique Alto Hospicio BPU_… 2018 Distanc… 275. 2 Tarapacá Iquique Alto Hospicio DE_28 2021 Número … 9.70 3 Tarapacá Iquique Alto Hospicio EA_93 2021 Porcent… 1.46 4 La Araucanía Malleco Angol BPU_… 2018 Distanc… 342. 5 La Araucanía Malleco Angol DE_28 2021 Número … 12.4 6 La Araucanía Malleco Angol EA_93 2021 Porcent… 4.59 7 Antofagasta Antofagasta Antofagasta BPU_… 2018 Distanc… 426. 8 Antofagasta Antofagasta Antofagasta DE_28 2021 Número … 7.84 9 Antofagasta Antofagasta Antofagasta EA_93 2021 Porcent… 2.74 10 Arica y Parinacota Arica Arica BPU_… 2018 Distanc… 324. # ℹ 330 more rows # ℹ 2 more variables: medida \u0026lt;chr\u0026gt;, estandar \u0026lt;chr\u0026gt; Ahora pivotemos los datos para crear una típica tabla de Excel donde los indicadores estén en columnas hacia el lado, mientras que hacia abajo están las unidades u observaciones; en este caso, comunas.\nlibrary(tidyr) datos \u0026lt;- datos |\u0026gt; select(nombre_region, nombre_comuna, codigo_comuna, variable, valor) |\u0026gt; pivot_wider(names_from = variable, values_from = valor) |\u0026gt; arrange(codigo_comuna) datos |\u0026gt; select(-starts_with(\u0026#34;codigo\u0026#34;)) # A tibble: 117 × 5 nombre_region nombre_comuna Distancia a plazas púb…¹ Número de víctimas m…² \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Tarapacá Iquique 282. 3.52 2 Tarapacá Alto Hospicio 275. 9.70 3 Antofagasta Antofagasta 426. 7.84 4 Antofagasta Calama 274. 8.28 5 Atacama Copiapó 245. 8.66 6 Atacama Tierra Amarilla 289. 41.7 7 Atacama Vallenar 241. 12.2 8 Coquimbo La Serena 303. 6.29 9 Coquimbo Coquimbo 291. 10.3 10 Coquimbo Ovalle 275. 18.0 # ℹ 107 more rows # ℹ abbreviated names: ¹​`Distancia a plazas públicas`, # ²​`Número de víctimas mortales en siniestros de tránsito por cada 100.000 habitantes` # ℹ 1 more variable: # `Porcentaje de cobertura de la red de ciclovía sobre la red vial` \u0026lt;dbl\u0026gt; Si guardamos este archivo con {writexl}, obtenemos una planilla básica de Excel:\nlibrary(writexl) write_xlsx(datos, \u0026#34;indicadores.xlsx\u0026#34;) Este resultado puede ser suficiente para algo rápido, pero definitivamente no es algo presentable ni atractivo.\nCrear planillas Excel personalizadas con {openxlsx} El primer paso para crear nuestro Excel es crear la planilla con la función createWorkbook(). Esta función se asigna a un objeto que representará a nuestra planilla.\nlibrary(openxlsx) tabla \u0026lt;- createWorkbook() Iremos aplicando distintas funciones de {openxlsx} sobre este objeto tabla para ir modificando la planilla Excel resultante.\nCon nuestra planilla creada, tenemos que crear una hoja de Excel para que podamos ponerle datos a la planilla.\naddWorksheet(tabla, \u0026#34;Hoja\u0026#34;) Podemos usar esta función una o varias veces para crear una o varias hojas en nuestra planilla.\nGuardar la planilla Excel En cualquier momento del proceso puedes guardar la planilla como archivo Excel para previsualizar como está quedando tu tabla, aunque en este paso del tutorial nuestra planilla está vacía.\n# guardar saveWorkbook(tabla, \u0026#34;indicadores.xlsx\u0026#34;, overwrite = TRUE) Escribir datos en la hoja de Excel Ahora tenemos que rellenar la planilla con datos con la función writeDataTable(), a la que hay que especificarle primero la planilla que vamos a editar (tabla), y la hoja (\u0026quot;Hoja\u0026quot;) en que queremos escribir los datos. En el argumento x entregamos el dataframe que queremos escribir en la hoja de la planilla.\n# tabla con formato personalizado writeDataTable(tabla, \u0026#34;Hoja\u0026#34;, x = datos, # la tabla que queremos escribir en el Excel tableStyle = \u0026#34;TableStyleLight9\u0026#34;, # estilo de la tabla startRow = 1, startCol = 1, colNames = TRUE, bandedCols = TRUE, bandedRows = FALSE, withFilter = FALSE, # keepNA = TRUE, # na.string = \u0026#34;sin datos\u0026#34; ) Esta función tiene varios argumentos que permiten personalizar la planilla. El más relevante es tableStyle, con el que le damos uno de los temas de Excel a la tabla. Podemos encontrar los temas de Excel en el botón Dar formato como tabla del panel Inicio de Excel.\nEn este panel, si pones el cursor sobre los íconos puedes ver que cada estilo tiene un número (9 es una tabla azul con fila de encabezado, 13 es la misma pero morada, 5 es una tabla celeste con encabezado sin relleno, etc.), y una intensidad (claro, medio y oscuro)\nAsí va quedando nuestra planilla!\nModificar ancho de columnas Si tenemos columnas que requieren más ancho, podemos definirlo con setColWidths(). En esta y otras funciones de {openxlsx}, en el argumento cols le decimos qué columnas queremos afectar, y luego le decimos el valor que queremos darle a cada columna correspondiente.\n# ancho de columnas setColWidths(tabla, \u0026#34;Hoja\u0026#34;, cols = c(1, 2, 3, 4, 5, 6), widths = c(22, 22, 13, 30, 30, 30) ) En este ejemplo, afectamos las 6 columnas: a las 1 y 2 le damos 22 de ancho, luego a la 3 le damos 13, y a las tres finales (columnas con los indicadores) les damos un mayor ancho de 30.\nSi el texto aún no cabe en las celdas, podemos definir un estilo a las celdas para que el texto se corte y aumente el alto de las celdas con mucho texto. Para definir un estilo usamos addStyle() y le damos un estilo que se hace con createStyle(). Luego, se define las filas (rows) y columnas (cols) que recibirán este estilo.\n# flujo de texto addStyle(tabla, \u0026#34;Hoja\u0026#34;, style = createStyle(wrapText = TRUE), rows = 1:nrow(datos)+1, cols = c(1, 4, 5, 6), stack = TRUE, gridExpand = T) En este caso le puse que las filas a las que se aplica el estilo sean todas las filas del dataframe (nrow(datos)) más 1, porque recordemos que en Excel la fila de los nombres de columna cuenta como una fila. Por eso el estilo se aplica desde la fila 1 a la nrow(datos)+1.\nDefinir estilo de texto Ahora quiero que las primeras dos columnas vayan en negrita. Hacemos lo mismo que en el paso anterior, pero ahora creamos un estilo distinto con createStyle():\n# celdas en negrita addStyle(tabla, \u0026#34;Hoja\u0026#34;, style = createStyle(textDecoration = \u0026#34;BOLD\u0026#34;), rows = 1:nrow(datos)+1, cols = c(1, 2), stack = TRUE, gridExpand = T) Es importante definir los argumentos stack = TRUE, gridExpand = T para que los estilos se sumen en vez de reemplazarse.\nNotamos que, cuando el texto de las celdas fluye para usar múltiples líneas, los valores quedan alineados verticalmente hacia abajo, y se ven feos, así que los alinearemos verticalmente en el centro:\n# centrado vertical addStyle(tabla, \u0026#34;Hoja\u0026#34;, style = createStyle(valign = \u0026#34;center\u0026#34;), rows = 1:nrow(datos)+1, cols = 1:length(datos), stack = TRUE, gridExpand = T) Nótese que aplicamos este estilo a todas las filas (1:nrow(datos)+1) y a todas las columnas 1:length(datos).\nFormatear variables numéricas Los números con decimales pueden ser ajustados definiendo el estilo createStyle(numFmt = \u0026quot;0.00\u0026quot;) o similar, donde el texto representa la cantidad de decimales que queremos mostrar:\n# decimales addStyle(tabla, \u0026#34;Hoja\u0026#34;, style = createStyle(numFmt = \u0026#34;0.00\u0026#34;), rows = 1:nrow(datos)+1, cols = c(6), stack = TRUE, gridExpand = TRUE) addStyle(tabla, \u0026#34;Hoja\u0026#34;, style = createStyle(numFmt = \u0026#34;0.0\u0026#34;), rows = 1:nrow(datos)+1, cols = c(4, 5), stack = TRUE, gridExpand = TRUE) Estilo condicional de celdas de acuerdo a los valores de los datos Como hemos visto hasta ahora, todos los estilos se aplican definiendo la posición de las filas y columnas que queremos modificar. Por ejemplo, si queremos modificar la cuarta fila de la segunda columna, sería rows = 4, cols = 2.\nPodemos aprovechar esta lógica para aplicar estilos que dependan del valor de los datos.\nPor ejemplo, si tenemos un vector de datos:\ndatos[[4]] [1] 282.38 274.86 425.99 273.83 245.02 288.62 241.10 303.18 291.29 [10] 275.40 698.60 331.96 337.03 1014.74 487.85 422.81 195.91 258.19 [19] 335.53 349.64 692.08 463.75 383.77 579.18 418.08 326.72 533.01 [28] 629.13 1066.37 394.09 193.46 341.26 309.32 271.24 237.21 626.54 [37] 196.91 261.74 243.70 277.05 250.93 334.88 260.42 298.01 397.34 [46] 630.78 278.38 250.96 269.13 361.94 842.69 174.37 268.58 241.55 [55] 249.80 242.14 405.63 342.34 269.90 211.65 459.62 203.42 223.62 [64] 302.26 355.57 241.34 207.37 198.72 260.57 271.98 264.33 325.99 [73] 379.62 206.06 218.95 196.25 442.02 330.57 475.15 213.77 197.15 [82] 238.16 170.11 349.66 260.62 219.73 345.05 185.01 206.69 344.91 [91] 260.17 216.00 252.60 429.94 224.27 444.36 187.97 692.96 708.93 [100] 263.33 302.88 519.54 220.60 303.81 489.65 327.53 335.11 196.11 [109] 385.91 468.69 255.61 313.70 385.68 323.99 232.25 313.80 288.51 Podemos evaluar una comparación para ver qué valores cumplen o no; en este caso, qué valores son superiores a 400:\ndatos[[4]] \u0026gt; 400 [1] FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE [13] FALSE TRUE TRUE TRUE FALSE FALSE FALSE FALSE TRUE TRUE FALSE TRUE [25] TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE FALSE FALSE FALSE TRUE [37] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE [49] FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE [61] TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE [73] FALSE FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE FALSE [85] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE TRUE [97] FALSE TRUE TRUE FALSE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE [109] FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE Ahora que sabemos qué valores cumplen, con la función which() podemos obtener su posición:\nwhich(datos[[4]] \u0026gt; 400) [1] 3 11 14 15 16 21 22 24 25 27 28 29 36 46 51 57 61 77 79 [20] 94 96 98 99 102 105 110 Es decir, obtenemos un vector que dice en qué filas se cumple la condición. Teniendo esto, podemos crear formatos condicionales según los datos: creamos un estilo con createStyle() que defina un relleno, borde y color de borde, que se aplique solamente a las celdas donde se cumple la condición, pero sumándole 1 para saltarnos la primera fila (nombres de columnas en Excel).\n# color condicional addStyle(tabla, \u0026#34;Hoja\u0026#34;, # crear estilo de color de relleno style = createStyle(fgFill = \u0026#34;#E6B8B7\u0026#34;, border = c(\u0026#34;top\u0026#34;, \u0026#34;bottom\u0026#34;), borderColour = c(\u0026#34;#DA9694\u0026#34;, \u0026#34;#DA9694\u0026#34;)), # filas a las que se va a aplicar el estilo rows = which(datos[[4]] \u0026gt; 400)+1, # celdas donde x es mayor a 400, + 1 para saltarse la primera fila cols = 4, stack = TRUE, gridExpand = T) # otro estilo para otra columna addStyle(tabla, \u0026#34;Hoja\u0026#34;, style = createStyle(fgFill = \u0026#34;#E6B8B7\u0026#34;, border = c(\u0026#34;top\u0026#34;, \u0026#34;bottom\u0026#34;), borderColour = c(\u0026#34;#DA9694\u0026#34;, \u0026#34;#DA9694\u0026#34;)), rows = which(datos[[5]] \u0026gt; 10)+1, cols = 5, stack = TRUE, gridExpand = T) # un tercer estilo con colores distintos addStyle(tabla, \u0026#34;Hoja\u0026#34;, style = createStyle(fgFill = \u0026#34;#D7E4BC\u0026#34;, border = c(\u0026#34;top\u0026#34;, \u0026#34;bottom\u0026#34;), borderColour = c(\u0026#34;#C4D79B\u0026#34;, \u0026#34;#C4D79B\u0026#34;)), rows = which(datos[[6]] \u0026gt; 5)+1, cols = 6, stack = TRUE, gridExpand = T) En este ejemplo aplicamos tres estilos condicionales a tres columnas distintas, cada uno con un criterio personalizado para destacar celdas con valores que cumplen la condición apropiada.\nCuando estemos satisfechxs con nuestra planilla, la guardamos como Excel.\n# guardar saveWorkbook(tabla, \u0026#34;indicadores.xlsx\u0026#34;, overwrite = TRUE) Otros Aquí voy a ir dejando otras funcionalidades útiles de {openxlsx}:\nDefinir el tamaño que tendrá la ventana al abrir la planilla\n# tamaño de la ventana setWindowSize(tabla, yWindow = 12, xWindow = 12, windowWidth = \u0026#34;20000\u0026#34;, windowHeight = \u0026#34;15000\u0026#34;) Cambiar la altura de las celdas:\n# altura para celdas con texto setRowHeights(tabla, \u0026#34;Hoja\u0026#34;, rows = c(6, 7, 12, 13), heights = c(64, 64, 64)) Otros tutoriales: Creating Professional Excel Reports with R: A Comprehensive Guide to openxlsx Package Making pretty Excel files in R ","date":"2025-08-08T00:00:00Z","excerpt":"El paquete `{openxlsx}` nos entrega funciones para generar archivos Excel desde R que contengan todo tipo de formato, estilo de tablas, y manipulación celda por celda del archivo resultante, con lo que podemos generar planillas de Excel atractivas desde R. La gracia es que podremos usar estas herramientas de forma reproducible (poder re-hacer gratis la planilla con datos actualizados o corregidos), y también podremos combinar la programación de R para crear estilos condicionales, crear cientos de archivos Excel, o planillas de cientos de hojas, y mucho más.","href":"https://bastianoleah.netlify.app/blog/excel_openxlsx/","tags":"limpieza de datos ; procesamiento de datos ; automatización ; tablas ; Excel","title":"Crea planillas de Excel con formato personalizado desde R con {openxlsx}"},{"content":"En una clase reciente me preguntaron cómo saber de una dónde hay datos perdidos o missing en un conjunto de datos. La respuesta que di fue usar summarize() para contar la cantidad de datos perdidos en todas las columnas de un dataframe:\nlibrary(dplyr) # manipulación de datos library(messy) # ensuciar datos # agregar datos perdidos al azar iris_m \u0026lt;- iris |\u0026gt; messy::make_missing(cols = names(iris)) iris_m |\u0026gt; # resumir los datos summarize( # en todas las columnas across(everything(), # contar la cantidad de missing ~sum(is.na(.x)) ) ) Sepal.Length Sepal.Width Petal.Length Petal.Width Species 1 12 21 16 21 11 Pero hay formas más convenientes de hacerlo!\nEl paquete {visdat} tiene funciones para visualizar tus conjuntos de datos completos, para poder entenderlos de manera visual antes de proseguir con la limpieza o análisis. El paquete entrega varias funciones vis_x() para visualzar la tabla de datos entera, destacando distintos aspectos de la misma.\ninstall.packages(\u0026#34;visdat\u0026#34;) library(visdat) En este post de ROpenSci hay una reseña más completa del paquete, pero te dejo ejemplos útiles a continuación:\nVisualizar datos perdidos Para visualizar si es que hay datos perdidos en nuestro dataframe, y además saber dónde están esos datos perdidos, usamos la función vis_miss():\nvis_miss(iris_m) {visdat} nos muestra visualmente toda la tabla de datos como un rectángulo, destacando los datos perdidos. ¡Súper útil!\nCon el argumento cluster podemos agrupar los datos perdidos para tener una mejor noción de la proporción en cada columna.\nvis_miss(iris_m, cluster = TRUE) Visualizar el tipo de las columnas Con vis_dat() vemos con colores distintos las columnas que corresponden a tipos distintos (numérico, caracter, factor, etc.)\nvis_dat(iris) Visualizar los valores de las variables numéricas Para explorar los datos de tipo numérico, podemos usar vis_value() para visualizar con una escala de colores los valores de cada columna, dándonos una noción sobre las cifras dentro de nuestra tabla:\niris |\u0026gt; select(where(is.numeric)) |\u0026gt; vis_value() Visualizar valores numéricos que cumplan una condición Para indagar en los datos numéricos, podemos entregar una condición dentro de una función lambda para aplicarla a todas las columnas y visualizar los resultados:\nvis_expect(iris, ~.x \u0026gt;= 5) Warning in Ops.factor(.x, 5): '\u0026gt;=' not meaningful for factors ","date":"2025-08-08T00:00:00Z","excerpt":"El paquete `{visdat}` tiene funciones para visualizar tus conjuntos de datos completos, para poder entenderlos de manera visual antes de proseguir con la limpieza o análisis. El paquete entrega varias funciones `vis_x()` para visualzar la tabla de datos entera, destacando distintos aspectos de la misma. En este post muestro ejemplos de uso de este paquete para encontrar datos perdidos, explorar datos, y más.","href":"https://bastianoleah.netlify.app/blog/visdat/","tags":"visualización de datos ; datos perdidos ; limpieza de datos ; consejos","title":"Echa un vistazo preliminar a tus datos con {visdat}"},{"content":" Índice Validación básica Funciones de validación Validación avanzada Si estás procesando muchos datos y/o datos que vienen de distintas fuentes con R, validarlos puede ayudarte a encontrar problemas antes de que sea tarde! 😱\n¿Qué es la validación de datos? Son las distintas pruebas que crearemos para confirmar que nuestros datos cumplen ciertos criterios. El objetivo es entregarnos la certeza de que nuestros datos son como esperamos luego de procesarlos. Para lograrlo, ponemos a prueba nuestros datos en distintos puntos de nuestros procesos de análisis de datos.\nPor ejemplo: luego de cargar un conjunto de datos a R y realizar una limpieza básica, ¿realmente los datos quedaron como debían quedar?\n¿Tenemos datos perdidos en variables donde no esperamos que los hayan? ¿La tabla tiene la cantidad de filas esperadas? En una variable numérica, ¿existen observaciones que se salen del rango esperable? Como años en el futuro, edades imposibles, etc. Las columnas que contienen números, ¿son realmente numéricas, o hay texto en algunas celdas escurridizas? ¿Las variables categóricas o de texto están bien escritas, o vienen cosas repetidas en minúsculas y otras en mayúsculas? ¿Una variable categórica contiene exactamente las categorías posibles, o tiene más o menos que las esperadas? Éste tipo de revisiones las hacemos generalmente de forma manual mientras exploramos los datos. Pero la idea de la validación de datos es que formalicemos estas pruebas para poder aplicarlas en distintos momentos, y a distintos conjuntos de datos.\nValidación básica En el fondo, una validación no es más que evaluar una expresión condicional para ver si se cumple o no se cumple. Creemos un conjunto de datos de prueba:\nlibrary(dplyr) animales \u0026lt;- tribble(~animal, ~patas, ~lindura, ~color, \u0026#34;mapache\u0026#34;, 4, 100, \u0026#34;gris\u0026#34;, \u0026#34;gato\u0026#34;, 80, 90, \u0026#34;negro\u0026#34;, \u0026#34;gallina\u0026#34;, 2, NA, \u0026#34;plumas\u0026#34;) Para validar los datos, puedes usar expresiones ifs para crear pruebas que revisan si tus resultados cumplen con criterios mínimos, como contener ciertas columnas, que no hayan datos perdidos, o lo que necesites.\nCreemos una expresión condicional para revisar si es que nuestra tabla de datos tiene al menos una observación:\n# validar si el dataframe tiene filas if (nrow(animales) \u0026gt; 0) { message(\u0026#34;tabla con filas!\u0026#34;) } else { warning(\u0026#34;tabla sin filas\u0026#34;) } tabla con filas! Con este código confirmamos que los datos cumplen con este criterio mínimo. Si modificamos los datos y probamos de nuevo, la validación nos avisará que hay un problema con los datos:\n# modificar el dataframe y ver si sigue cumpliendo animales2 \u0026lt;- animales |\u0026gt; filter(patas == 0) if (nrow(animales2) \u0026gt; 0) { message(\u0026#34;tabla con filas!\u0026#34;) } else { warning(\u0026#34;tabla sin filas :(\u0026#34;) } Warning: tabla sin filas :( Siguiendo la misma idea, podemos crear otras validaciones para confirmar que los datos vienen como esperamos:\n# revisar que no hayan datos perdidos en columna `lindura` if (sum(is.na(animales$lindura)) == 0) { message(\u0026#34;sin datos perdidos en variable `lindura`\u0026#34;) } else { stop(\u0026#34;datos perdidos en variable `lindura`, ¿acaso demasiada lindura?\u0026#34;) } Error: datos perdidos en variable `lindura`, ¿acaso demasiada lindura? # revisar que variable `patas` no tenga observaciones fuera del rango posible if (all(between(animales$patas, 2, 8))) { message(\u0026#34;cantidad de patas aceptable\u0026#34;) } else { stop(\u0026#34;demasiadas patas!\u0026#34;) } Error: demasiadas patas! # revisar que los valores de `colores` sean válidos if (all(animales$color %in% c(\u0026#34;verde\u0026#34;, \u0026#34;negro\u0026#34;, \u0026#34;café\u0026#34;, \u0026#34;gris\u0026#34;, \u0026#34;blanco\u0026#34;))) { message(\u0026#34;colores aceptables\u0026#34;) } else { stop(\u0026#34;variable colores tiene datos fuera de lo esperado\u0026#34;) } Error: variable colores tiene datos fuera de lo esperado Al ejecutar esta validaciones obtenemos mensajes que nos indican el estado de los datos, o su calidad.\nDentro de las condicionales puedes agregar warning() o message() para recibir avisos en tu consola dependiendo de lo que se obtiene en cada prueba. También puedes usar stop() para que detener la ejecución si la validación sale negativa, en el caso de que sea muy importante de resolver el problema con los datos de manera inmediata en vez de continuar con el flujo de procesamiento.\nOtra alternativa para crear validaciones es usar la función stopifnot(), a la que le entregamos una condición que esperamos que se cumpla, y si no se cumple, la ejecución se detiene:\nstopifnot(\u0026#34;valores perdidos\u0026#34; = is.na(animales$lindura)) Error: valores perdidos La diferencia es que con stopifnot() tenemos menos control sobre qué hacer cuando se cumple o no se cumple la condición, y solamente te avisa si es que no se cumple.\nFunciones de validación El siguiente paso es reunir todas estas pruebas en una sola función, para que puedas reutilizarla en distintas versiones de la tabla, y en distintos conjuntos de datos.\nCreamos una función con function(), donde el argumento data va a representar el conjunto de datos que pasemos a la función.\nrevisar \u0026lt;- function(data) { # prueba de filas if (nrow(data) \u0026gt; 0) { message(\u0026#34;filas ok\u0026#34;) } else { warning(\u0026#34;tabla sin filas\u0026#34;) } # prueba de perdidos if (any(is.na(data$lindura))) { warning(\u0026#34;datos perdidos\u0026#34;) } else { message(\u0026#34;sin datos perdidos\u0026#34;) } # prueba de rangos if (all(between(data$patas, 2, 8))) { message(\u0026#34;rango aceptable\u0026#34;) } else { warning(\u0026#34;datos fuera de rango\u0026#34;) } # prueba de valores categóricos if (all(data$color %in% c(\u0026#34;verde\u0026#34;, \u0026#34;negro\u0026#34;, \u0026#34;café\u0026#34;, \u0026#34;rosado\u0026#34;, \u0026#34;gris\u0026#34;, \u0026#34;blanco\u0026#34;))) { message(\u0026#34;valores esperados\u0026#34;) } else { warning(\u0026#34;valores inesperados\u0026#34;) } } Ahora podemos aplicar la función a los datos para validarlos en cualquier momento:\nrevisar(animales) filas ok Warning in revisar(animales): datos perdidos Warning in revisar(animales): datos fuera de rango Warning in revisar(animales): valores inesperados Podemos corregir los datos y volver a aplicar la validación para confirmar que ahora están correctos:\n# corregir datos luego de las pruebas animales_3 \u0026lt;- animales |\u0026gt; filter(patas \u0026lt;= 8) |\u0026gt; mutate(color = recode(color, \u0026#34;plumas\u0026#34; = \u0026#34;blanco\u0026#34;)) |\u0026gt; mutate(lindura = if_else(is.na(lindura), 0, lindura)) # validar revisar(animales_3) filas ok sin datos perdidos rango aceptable valores esperados Finalmente, podemos volver a aplicar la validación a un conjunto de datos actualizado o una versión distinta de una tabla con las mismas características:\nanimales_4 \u0026lt;- tribble(~animal, ~patas, ~lindura, ~color, \u0026#34;perro\u0026#34;, 4, 50, \u0026#34;café\u0026#34;, \u0026#34;ratita\u0026#34;, 3, 99, \u0026#34;café\u0026#34;, \u0026#34;chancho\u0026#34;, 4, 70, \u0026#34;rosado\u0026#34;, \u0026#34;araña\u0026#34;, 8, -100, \u0026#34;negro\u0026#34;) animales_4 |\u0026gt; revisar() filas ok sin datos perdidos rango aceptable valores esperados animales_4 |\u0026gt; filter(lindura \u0026gt; 50) |\u0026gt; revisar() filas ok sin datos perdidos rango aceptable valores esperados Estas pequeñas buenas prácticas te van a ayudar a reducir la incertidumbre en rutinas largas de procesamiento de datos!\nValidación avanzada El paquete de R {pointblank} se especializa en validación de datos, así que si requieres algo más avanzado para garantizar la calidad de tus datos y la estabilidad de tus procesos, revisa este post con un tutorial!\n","date":"2025-08-07T00:00:00Z","excerpt":"Si estás procesando muchos datos y/o datos que vienen de distintas fuentes con R, validarlos puede ayudarte a encontrar problemas antes de que sea tarde! ¿Qué es la validación de datos? Son las distintas pruebas que crearemos para confirmar que nuestros datos cumplen ciertos criterios. El objetivo es entregarnos la certeza de que nuestros datos son como esperamos luego de procesarlos. Para lograrlo, ponemos a prueba nuestros datos en distintos puntos de nuestros procesos de análisis de datos.","href":"https://bastianoleah.netlify.app/blog/validacion_basica/","tags":"procesamiento de datos ; consejos ; automatización ; control de flujo ; funciones ; básico","title":"Validación básica de datos con R"},{"content":" Last.fm es una plataforma donde las personas van registrando la música que escuchan diariamente, y luego pueden obtener estadísticas sobre sus gustos musicales y recomendaciones basadas en los gustos de usuarios similares.\nTenía ganas de reproducir en R uno de los gráficos que aparecen en el reporte mensual:\nAsí que me apliqué con {ggplot2}, usando un paquete que agrega una visualización similar, y usando otro paquete para descargar los datos de mi perfil de Last.fm.\nObtención de datos Para obtener los datos de mis canciones escuchadas usé el paquete {lastfmR}:\ndevtools::install_github(\u0026#34;ppatrzyk/lastfmR\u0026#34;) library(lastfmR) La obtención de los datos de Last.fm se hace con las siguientes funciones:\nscrobbles \u0026lt;- get_scrobbles(user = \u0026#34;bastimapache\u0026#34;) artist_info \u0026lt;- get_library_info(user = \u0026#34;bastimapache\u0026#34;) scrobbles # A tibble: 17,000 × 4 date artist track album \u0026lt;POSIXt\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 2025-07-27 18:38:27 Bullet for My Valentine Tears Don't Fall The Poison 2 2025-07-25 15:35:20 Premiata Forneria Marconi Geranio Per un amico 3 2025-07-25 15:13:14 Premiata Forneria Marconi Il Banchetto Per un amico 4 2025-07-25 15:06:31 Premiata Forneria Marconi Per Un Amico Per un amico 5 2025-07-25 15:01:41 Premiata Forneria Marconi Generale Per un amico 6 2025-07-25 14:48:01 Premiata Forneria Marconi Appena un po' Per un amico 7 2025-07-25 13:46:32 The Body \u0026amp; OAA Docile Gift Enemy of Love 8 2025-07-25 13:43:07 The Body \u0026amp; OAA Ignorant Messiah Enemy of Love 9 2025-07-25 13:40:40 The Body \u0026amp; OAA Miserable Freedom Enemy of Love 10 2025-07-25 13:37:31 The Body \u0026amp; OAA Barren of Joy Enemy of Love # ℹ 16,990 more rows artist_info # A tibble: 3,882 × 5 artist user_scrobbles global_listeners global_scrobbles artist_tags \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; 1 Opeth 5006 1066799 110560572 Progressiv… 2 Magma 4905 158887 3353010 Zeuhl; Pro… 3 Between the Bur… 4519 411709 27305709 Progressiv… 4 Erik Satie 3439 1690933 35397288 Classical;… 5 ASIAN KUNG-FU G… 3318 601332 33012597 J-rock; ja… 6 Gojira 3289 898440 71798696 death meta… 7 Boris 3178 538299 21493173 drone; Sto… 8 Rosetta 3148 140465 6019868 Post-Metal… 9 The Beatles 3019 5830439 885054050 classic ro… 10 Mastodon 3006 1139246 72786659 Progressiv… # ℹ 3,872 more rows Los datos obtenidos son el historial de canciones escuchadas, y los artistas escuchados que además incluye las etiquetas de cada artista, que corresponden a los géneros o subgéneros musicales que las y los usuarios de Last.fm agregan.\nProcesamiento de datos Teniendo estos dos conjuntos de datos, los unimos según la variable del nombre de artista, para que cada canción escuchada tenga las etiquetas del artista:\nlibrary(dplyr) library(lubridate) library(tidyr) scrobbles_tags \u0026lt;- scrobbles |\u0026gt; tibble() |\u0026gt; left_join(artist_info |\u0026gt; select(artist, artist_tags)) Luego, como cada artista puede tener más de una etiqueta, separamos las etiquetas en distintas columnas:\nscrobbles_tags_2 \u0026lt;- scrobbles_tags |\u0026gt; separate(artist_tags, sep = \u0026#34;; \u0026#34;, into = paste(\u0026#34;artist_tags\u0026#34;, 1:10, sep = \u0026#34;_\u0026#34;)) glimpse(scrobbles_tags_2) Rows: 17,000 Columns: 14 $ date \u0026lt;POSIXt\u0026gt; 2025-07-27 18:38:27, 2025-07-25 15:35:20, 2025-07-25… $ artist \u0026lt;chr\u0026gt; \u0026quot;Bullet for My Valentine\u0026quot;, \u0026quot;Premiata Forneria Marconi\u0026quot;,… $ track \u0026lt;chr\u0026gt; \u0026quot;Tears Don't Fall\u0026quot;, \u0026quot;Geranio\u0026quot;, \u0026quot;Il Banchetto\u0026quot;, \u0026quot;Per Un … $ album \u0026lt;chr\u0026gt; \u0026quot;The Poison\u0026quot;, \u0026quot;Per un amico\u0026quot;, \u0026quot;Per un amico\u0026quot;, \u0026quot;Per un a… $ artist_tags_1 \u0026lt;chr\u0026gt; \u0026quot;metalcore\u0026quot;, \u0026quot;Progressive rock\u0026quot;, \u0026quot;Progressive rock\u0026quot;, \u0026quot;P… $ artist_tags_2 \u0026lt;chr\u0026gt; \u0026quot;metal\u0026quot;, \u0026quot;italian progressive rock\u0026quot;, \u0026quot;italian progressi… $ artist_tags_3 \u0026lt;chr\u0026gt; \u0026quot;hardcore\u0026quot;, \u0026quot;italian\u0026quot;, \u0026quot;italian\u0026quot;, \u0026quot;italian\u0026quot;, \u0026quot;italian\u0026quot;,… $ artist_tags_4 \u0026lt;chr\u0026gt; \u0026quot;emocore\u0026quot;, \u0026quot;Progressive\u0026quot;, \u0026quot;Progressive\u0026quot;, \u0026quot;Progressive\u0026quot;,… $ artist_tags_5 \u0026lt;chr\u0026gt; \u0026quot;seen live\u0026quot;, \u0026quot;italian prog\u0026quot;, \u0026quot;italian prog\u0026quot;, \u0026quot;italian p… $ artist_tags_6 \u0026lt;chr\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,… $ artist_tags_7 \u0026lt;chr\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,… $ artist_tags_8 \u0026lt;chr\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,… $ artist_tags_9 \u0026lt;chr\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,… $ artist_tags_10 \u0026lt;chr\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,… Ahora que tenemos hasta 10 columnas de etiquetas, transformamos los datos a formato largo, para obtener una sola variable que contenga hacia abajo todas las etiquetas de cada artista.\nscrobbles_tags_3 \u0026lt;- scrobbles_tags_2 |\u0026gt; pivot_longer(cols = starts_with(\u0026#34;artist_tags\u0026#34;), names_to = \u0026#34;tags_n\u0026#34;, values_to = \u0026#34;tags\u0026#34;) |\u0026gt; filter(!is.na(tags)) |\u0026gt; select(-tags_n) |\u0026gt; mutate(tags = tolower(tags)) scrobbles_tags_3 |\u0026gt; slice(65:75) |\u0026gt; select(artist, tags) # A tibble: 11 × 2 artist tags \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 The Body \u0026amp; OAA drone 2 The Body \u0026amp; OAA drone metal 3 The Body \u0026amp; OAA noise 4 The Body \u0026amp; OAA experimental 5 The Body \u0026amp; OAA drone 6 The Body \u0026amp; OAA drone metal 7 Alberich noise 8 Alberich power electronics 9 Alberich industrial 10 Alberich death industrial 11 Alberich electronic Excluimos algunas etiquetas de géneros musicales que son demasiado amplios:\nscrobbles_tags_4 \u0026lt;- scrobbles_tags_3 |\u0026gt; filter(tags != \u0026#34;seen live\u0026#34;, !tags %in% c(\u0026#34;metal\u0026#34;, \u0026#34;electronic\u0026#34;, \u0026#34;hardcore\u0026#34;, \u0026#34;experimental\u0026#34;)) |\u0026gt; add_count(tags, name = \u0026#34;tag_n\u0026#34;) Luego especificamos cuántas etiquetas máximas queremos tener, y extraemos el Top 15 de géneros musicales más escuchados por mi:\nn_tags \u0026lt;- 15 top_tags \u0026lt;- scrobbles_tags_4 |\u0026gt; distinct(tags, tag_n) |\u0026gt; slice_max(tag_n, n = n_tags) |\u0026gt; pull(tags) top_tags [1] \u0026quot;black metal\u0026quot; \u0026quot;death metal\u0026quot; \u0026quot;noise\u0026quot; [4] \u0026quot;jazz\u0026quot; \u0026quot;post-black metal\u0026quot; \u0026quot;grindcore\u0026quot; [7] \u0026quot;ambient\u0026quot; \u0026quot;progressive metal\u0026quot; \u0026quot;sludge\u0026quot; [10] \u0026quot;brutal death metal\u0026quot; \u0026quot;avant-garde\u0026quot; \u0026quot;doom metal\u0026quot; [13] \u0026quot;drone\u0026quot; \u0026quot;post-rock\u0026quot; \u0026quot;deathcore\u0026quot; Filtramos todas las canciones escuchadas para que solamente queden las que corresponden a uno de estos 15 géneros más escuchados, y dejamos solamente la primera etiqueta de cada canción:\nscrobbles_tags_5 \u0026lt;- scrobbles_tags_4 |\u0026gt; filter(tags %in% top_tags) |\u0026gt; group_by(date) |\u0026gt; slice_max(tag_n, n = 1) scrobbles_tags_5 # A tibble: 14,127 × 6 # Groups: date [14,110] date artist track album tags tag_n \u0026lt;POSIXt\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 2023-11-06 13:23:28 Dreamcrusher INCINERATOR INCINERATOR noise 2633 2 2023-11-06 13:29:44 Dreamcrusher Cathedral Of Moths INCINERATOR noise 2633 3 2023-11-06 13:29:44 Dreamcrusher Pseudogender INCINERATOR noise 2633 4 2023-11-06 13:36:34 Dreamcrusher Pseudogender INCINERATOR noise 2633 5 2023-11-06 13:36:34 Dreamcrusher Vulpeculae Freeze INCINERATOR noise 2633 6 2023-11-06 13:44:43 Dreamcrusher Vulpeculae Freeze INCINERATOR noise 2633 7 2023-11-06 13:44:43 Dreamcrusher Bane INCINERATOR noise 2633 8 2023-11-06 13:57:07 Merzbow Promotion Man Merzbeat noise 2633 9 2023-11-06 13:57:07 Dreamcrusher Bane INCINERATOR noise 2633 10 2023-11-06 14:06:04 Pharmakon No Natural Order Contact noise 2633 # ℹ 14,117 more rows Finalmente hacemos el conteo de géneros musicales por semana:\nscrobbles_tags_6 \u0026lt;- scrobbles_tags_5 |\u0026gt; mutate(date = floor_date(date, \u0026#34;week\u0026#34;)) |\u0026gt; count(date, tags) scrobbles_tags_6 # A tibble: 858 × 3 # Groups: date [90] date tags n \u0026lt;dttm\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 2023-11-05 00:00:00 ambient 23 2 2023-11-05 00:00:00 avant-garde 1 3 2023-11-05 00:00:00 black metal 60 4 2023-11-05 00:00:00 death metal 16 5 2023-11-05 00:00:00 grindcore 1 6 2023-11-05 00:00:00 jazz 21 7 2023-11-05 00:00:00 noise 35 8 2023-11-05 00:00:00 post-rock 4 9 2023-11-05 00:00:00 progressive metal 12 10 2023-11-05 00:00:00 sludge 17 # ℹ 848 more rows Visualización Para reproducir el gráfico usé el paquete {ggstream} que facilita la creación de gráficos de áreas apiladas una sobre otra.\ninstall.packages(\u0026#34;ggstream\u0026#34;) library(ggplot2) library(ggview) library(scales) library(stringr) library(ggstream) Primero creamos paletas de colores para el gráfico:\npaleta \u0026lt;- c(\u0026#34;#C490FF\u0026#34;, \u0026#34;#62D4B6\u0026#34;, \u0026#34;#223689\u0026#34;, \u0026#34;#5911AC\u0026#34;, \u0026#34;#894BD2\u0026#34;, \u0026#34;#165159\u0026#34;, \u0026#34;#3262B8\u0026#34;) show_col(paleta) color_base = \u0026#34;#5911AC\u0026#34; color_fondo = col_mix(color_base, \u0026#34;black\u0026#34;, amount = 0.8) color_texto = col_mix(color_base, \u0026#34;white\u0026#34;, amount = 0.7) color_detalle = col_mix(color_base, \u0026#34;white\u0026#34;, amount = 0.1) |\u0026gt; col_saturate(-15) show_col(c(color_base, color_fondo, color_texto, color_detalle)) Luego definimos algunos elementos del tema del gráfico, como el fondo oscuro y las letras claras, y la eliminación de elementos del panel del gráfico que no necesitamos:\ntheme_set( theme(panel.background = element_rect(color = color_fondo, fill = color_fondo), plot.background = element_rect(color = color_fondo, fill = color_fondo), legend.background = element_rect(color = color_fondo, fill = color_fondo), panel.grid = element_blank(), axis.ticks = element_blank(), axis.text = element_blank(), axis.title = element_blank(), text = element_text(color = color_texto), legend.key.size = unit(3, \u0026#34;mm\u0026#34;), legend.key.spacing.y = unit(2, \u0026#34;mm\u0026#34;), legend.margin = margin(l = 2, t = 10), plot.margin = margin(4, 4, 4, 4, \u0026#34;mm\u0026#34;)) ) scrobbles_tags_5 |\u0026gt; filter(date \u0026gt; \u0026#34;2024-08-1\u0026#34;) |\u0026gt; # límite inferior de fecha ggplot() + aes(date, n, color = tags, fill = tags) + # geometría principal geom_stream(bw = .7, sorting = \u0026#34;inside_out\u0026#34;, color = color_fondo, size = 0.1, show.legend = F) + # puntos invisibles para hacer una leyenda con puntos geom_point(size = 0, alpha = 0) + # texto del gráfico, geom_stream_label( aes(label = case_when(tags %in% top_tags[1:7] ~ str_wrap(tags, 12), # sólo texto del top 7 de tags .default = \u0026#34;\u0026#34;)), bw = .7, sorting = \u0026#34;inside_out\u0026#34;, color = \u0026#34;white\u0026#34;, size = 2, fontface = \u0026#34;bold\u0026#34;, lineheight = 0.8, hjust = 0.5, vjust = 0.5, n_grid = 200) + # escala de colores en base a la paleta scale_fill_manual(values = colorRampPalette(paleta)(n_tags), aesthetics = c(\u0026#34;color\u0026#34;, \u0026#34;fill\u0026#34;)) + # escala horizontal de fechas annotate(geom = \u0026#34;text\u0026#34;, x = as.POSIXct(c(\u0026#34;2024-09-15\u0026#34;, \u0026#34;2024-12-15\u0026#34;, \u0026#34;2025-03-15\u0026#34;, \u0026#34;2025-06-15\u0026#34;)), label = format(as_date(fechas), \u0026#34;%m/%y\u0026#34;), y = -160, color = color_detalle, size = 3) + coord_cartesian(expand = F, clip = \u0026#34;off\u0026#34;) + # leyenda: sacar leyenda de relleno, y usar la de los puntos invisibles para que salga de puntos guides(fill = guide_none(), color = guide_legend(title = NULL, override.aes = list(size = 3, alpha = 1))) + # textos labs(title = \u0026#34;Géneros musicales más escuchados\u0026#34;, caption = \u0026#34;Last.fm/user/bastimapache\u0026#34;) + theme(plot.title = element_text(face = \u0026#34;bold\u0026#34;, margin = margin(b = -10), hjust = 0), plot.caption = element_text(color = color_detalle, hjust = 1, size = 8, vjust = 0), plot.caption.position = \u0026#34;plot\u0026#34;) No se parece mucho el gráfico que quería copiar, quizá porque el original iba en un rango de tiempo mucho más breve, pero me gustó igual.\nAlgunos comentarios sobre esta visualización:\ncrear paletas de colores haciendo mezclas de colores es más conveniente que andarlos eligiendo con un programa es complicado clasificar la música por géneros, sobre todo cuando la música que te gusta es rara y está muy metida en combinaciones de subgéneros. Elegir la etiqueta más popular de cada artista quizá no es la mejor idea, pero elegir varias etiquetas por artista igual podría inflar la representación de ciertos géneros con respecto a otros siempre va a ser difícil ponerle colores a gráficos con más de ocho categorías, así que en este caso los colores son casi de adorno el paquete {ggstream} es súper conveniente para generar este tipo de visualizaciones, y tiene un montón de opciones para modificarlas y tener alternativas, pero realiza un montón de transformaciones de los datos por dentro, lo que dificulta su compatibilidad con algunas otras cosas de {ggplot2}; por ejemplo, cambia la escala de fechas para transformar los números, y eso te dificulta poder modificar la forma en que se ve la escala de fechas, lo que resolví simplemente agregando las fechas con annotate(). junto con lo anterior, se devuelve casi obligatorio tener que usar la función geom_stream_label() para agregar textos, lo que te quita harta libertad quizás relacionado a lo anterior: traté de grabar la evolución del gráfico con {camcorder}, pero por alguna razón no pescaba al pasar a {ggstream} el paquete {lastfmR} de obtención de datos solamente extrajo las últimas 17.000 canciones escuchadas, pero en mi caso eso fue un poco más de un año de datos, así que fue suficiente. De todos modos hay ene paquetes y tutoriales para sacar datos de Last.fm, y ambién hay otras alternativas para bajar todos los datos de tu perfil, pero en este caso no creí que fuera necesario, sobre todo porque tengo un lapsus de un par de años donde dejé de usar Last.fm! Después de esta, hice una variación de la misma visualización, pero en vez de mostrar géneros musicales, muestra artistas. Como ya tenía todo el código para hacer la visualización y procesar los datos, modificarlo para este nuevo fin fue muy fácil. Pueden ver todo el código que necesité en el script lastfm_artists.R en el repositorio..\nAl ver las marcadas tendencias que habían en mi rotación de artistas más escuchados, me entró la curiosidad y generé un ranking de artistas por semana:\nEste fue bastante más complejo que los anteriores, porque tuve que programar toda la lógica para llegar a hacer un ranking que no tuviera empates1, y luego de eso, hacer el seguimiento de los artistas que iban repitiéndose semana a semana, para ver cómo iban cambiando en su posición de ranking entre las distintas semanas2. Todo este código se puede ver en el script lastfm_weekly.R en el repositorio..\nEn términos visuales, este gráfico en realidad es una tabla hecha con geom_tile(), con líneas verticales con annotate(\u0026quot;segment\u0026quot;) que hacen una separación entre semanas, líneas que van conectando a los artistas consecutivos hechas con geom_step(), y efecto de sombra a medida que el ranking es menor que se hicieron con un geom_tile() donde la transparencia era ligada al ranking. También tiene distinto tamaño y grosor de letra para los artistas en el número 1, y colores que responden a recodificación de los géneros musicales en categorías más generales en vez de ponerle un color a cada subgénero del mundo. Mención especial al circulito con un 1 hecho con annotate().\nSi bien a este gráfico tampoco lo grabé con {camcorder} mientras lo hacía, luego de terminarlo hice una especie de simulación paso por paso de cómo se genera la visualización de principio a fin:\nAquí la dificultad era que el ranking se define por el porcentaje semanal de canciones escuchadas que pertenecen a cada artista, pero ocurría que, si los números eran muy bajos, los porcentajes se repetían, porque un mismo artista era escuchado la misma cantidad de veces que otros. En estos casos, el desempate se hizo agregándole a los artistas un porcentaje que tenía que ver con el porcentaje de canciones escuchadas en total \u0026mdash;de todos los tiempos\u0026mdash; que del artista, y de esta manera dar privilegio al artista más escuchado en general en el ranking semanal. También se agrega un pequeñísimo número aleatorio para desempatar otros casos.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nEl problema en estos casos era que se podía ir trazando una línea que muestre el cambio de posición semanal de un artista, pero la dificultad era hacer que la línea se cortara si el artista deja de ser escuchado una semana, pero que volviera a aparecer si vuelve a ser escuchado en una semana futura. La solución fue generar una variable que agrupa con un número único las escuchas consecutivas semanales de un artista, y si el artista no entra en el ranking en una o más semanas se genera un grupo distinto. Luego se usan estos grupos para que las líneas se unan solamente cuando las escuchas son consecutivas, en vez de qué se haga una línea que pase por encima de semanas donde el artista no entró al ranking.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-07-30T00:00:00Z","excerpt":"Tenía ganas de reproducir en R uno de los gráficos que aparecen en el reporte mensual de Last.fm, una plataforma donde las personas van registrando la música que escuchan diariamente, así que aquí va el proceso y el resultado final. También incluye otras visualizaciones alternativas, y una animación del proceso de visualización de una de ellas!","href":"https://bastianoleah.netlify.app/blog/2025-07-28/","tags":"ggplot2 ; visualización de datos ; blog ; gráficos ; animaciones","title":"Gráfico de mis artistas y géneros musicales más escuchados según Last.fm"},{"content":"Veamos un mini ejemplo de automatización de tareas con R: los resultados del Censo 2024 vienen en 20 archivos, en 20 enlaces distintos!\n¿El problema? para obtenerlos, tendríamos que entrar al sitio con el navegador, ir al enlace de descargas, bajar cada uno de los 20 archivos manualmente, y guardarlos en una carpeta para poder usarlos.\nEn un script de R, con {rvest} extraemos todos los enlaces del sitio, y con {purrr} descargamos todos los archivos de una 🚀\nBeneficios? Muchos!\nNo tendrás que apretar 20 veces un botón como un perdedor/a 😎 Obtendrás una fuente reproducible de los datos originales 🤓 Creaste trazabilidad de los datos (nadie puede alegar que son manipulados) 🕵🏻‍♀️ Puedes actualizarlos o volver a descargarlos en otro equipo con un clic Creaste un proceso que puedes aplicar a otros sitios! Fastidiaste a la gente del INE descargando 20 archivos en menos de 1 segundo 😣 Lo primero obtener el código fuente de la página y extraer los en enlaces que están en cada botón con web scraping. Puedes aprender cómo extraer elementos de una página web con este tutorial de web scraping o viendo este taller donde lo explico en un video.\nenlace \u0026lt;- \u0026#34;https://censo2024.ine.gob.cl/estadisticas/\u0026#34; # extraer enlaces de descarga enlaces \u0026lt;- rvest::read_html(enlace) |\u0026gt; # descargar sitio rvest::html_elements(\u0026#34;.fusion-button\u0026#34;) |\u0026gt; # botones rvest::html_attr(\u0026#34;href\u0026#34;) |\u0026gt; # enlaces stringr::str_subset(\u0026#34;xls\u0026#34;) # sólo excel enlaces [1] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/06/P1-Discapacidad.xlsx\u0026quot; [2] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/06/P2-Pueblos-indigenas.xlsx\u0026quot; [3] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/06/P3_Lenguas-indigenas.xlsx\u0026quot; [4] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/06/P4-Afrodescendencia.xlsx\u0026quot; [5] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/06/P5_Genero.xlsx\u0026quot; [6] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/06/P6_Religion-o-credo.xlsx\u0026quot; [7] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/06/P7_Educacion.xlsx\u0026quot; [8] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/06/P8-Escolaridad-poblacion-inmigrante-internacional.xlsx\u0026quot; [9] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/05/V2_Caracteristicas-vivienda-y-viv-irrecuperables.xlsx\u0026quot; [10] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/05/V3_N-de-dormitorios-hacinamiento-y-viv-con-mas-de-un-hogar.xlsx\u0026quot; [11] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/05/V4_Servicios_basicos_de_la_vivienda.xlsx\u0026quot; [12] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/05/H1_Servicios-basicos-hogar-y-tenencia-vivienda.xlsx\u0026quot; [13] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/05/V5_Tipologias-de-viviendas-censadas.xlsx\u0026quot; [14] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/04/D5_Migracion-interna.xlsx\u0026quot; [15] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/04/D4_Inmigracion-Internacional.xlsx\u0026quot; [16] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/04/D6_Fecundidad.xlsx\u0026quot; [17] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/03/D1_Poblacion-censada-por-sexo-y-edad-en-grupos-quinquenales.xlsx\u0026quot; [18] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/03/D2_Indice-de-envejecimiento-por-sexo.xlsx\u0026quot; [19] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/03/D3_Poblacion-censada-por-tipo-de-operativo.xlsx\u0026quot; [20] \u0026quot;https://censo2024.ine.gob.cl/wp-content/uploads/2025/03/V1_Viviendas-y-hogares-censados.xlsx\u0026quot; Obtuvimos todos los enlaces que están en la página! Ahora usaremos un poco de magia (regex) con {stringr} para que solamente queden los enlaces que apuntan a archivos Excel:\n# extraer nombres de archivos archivos \u0026lt;- stringr::str_extract(enlaces, \u0026#34;(?\u0026lt;=\\\\d{2}/\\\\d{2}/).*\u0026#34;) archivos [1] \u0026quot;P1-Discapacidad.xlsx\u0026quot; [2] \u0026quot;P2-Pueblos-indigenas.xlsx\u0026quot; [3] \u0026quot;P3_Lenguas-indigenas.xlsx\u0026quot; [4] \u0026quot;P4-Afrodescendencia.xlsx\u0026quot; [5] \u0026quot;P5_Genero.xlsx\u0026quot; [6] \u0026quot;P6_Religion-o-credo.xlsx\u0026quot; [7] \u0026quot;P7_Educacion.xlsx\u0026quot; [8] \u0026quot;P8-Escolaridad-poblacion-inmigrante-internacional.xlsx\u0026quot; [9] \u0026quot;V2_Caracteristicas-vivienda-y-viv-irrecuperables.xlsx\u0026quot; [10] \u0026quot;V3_N-de-dormitorios-hacinamiento-y-viv-con-mas-de-un-hogar.xlsx\u0026quot; [11] \u0026quot;V4_Servicios_basicos_de_la_vivienda.xlsx\u0026quot; [12] \u0026quot;H1_Servicios-basicos-hogar-y-tenencia-vivienda.xlsx\u0026quot; [13] \u0026quot;V5_Tipologias-de-viviendas-censadas.xlsx\u0026quot; [14] \u0026quot;D5_Migracion-interna.xlsx\u0026quot; [15] \u0026quot;D4_Inmigracion-Internacional.xlsx\u0026quot; [16] \u0026quot;D6_Fecundidad.xlsx\u0026quot; [17] \u0026quot;D1_Poblacion-censada-por-sexo-y-edad-en-grupos-quinquenales.xlsx\u0026quot; [18] \u0026quot;D2_Indice-de-envejecimiento-por-sexo.xlsx\u0026quot; [19] \u0026quot;D3_Poblacion-censada-por-tipo-de-operativo.xlsx\u0026quot; [20] \u0026quot;V1_Viviendas-y-hogares-censados.xlsx\u0026quot; Finalmente, creamos un loop que vaya por cada uno de los enlaces, descargue cada uno de los archivos, y le ponga el nombre al archivo correspondiente:\n# crear carpeta dir.create(\u0026#34;datos/originales\u0026#34;, showWarnings = F) # por cada posición de los enlaces (x va del 1 al n) seq_along(enlaces) |\u0026gt; purrr::walk( ~{message(\u0026#34;descargando \u0026#34;, enlaces[.x]) # enlace correspondiente a x # descargar archivo en el enlace download.file(enlaces[.x], # nombre del archivo a guardar destfile = paste0(\u0026#34;datos/originales/\u0026#34;, archivos[.x]) # nombre de archivo de x ) Sys.sleep(1) # espera post-descarga }) Con este breve script obtuviste un proceso que te permite descargar automáticamente todos los datos, lo cual es una forma certera de crear una trazabilidad y una fuente real de los datos, además de darte un proceso que puedes volver a utilizar en otros sitios para optimizar tu trabajo!\nPuedes encontrar el código de este ejemplo (y los datos) en este repositorio sobre extracción y limpieza de los datos del Censo 2024.\n","date":"2025-07-27T00:00:00Z","excerpt":"Éste es un mini ejemplo de automatización de tareas con R: los resultados del Censo 2024 vienen en 20 archivos, en 20 enlaces distintos! En un script de R, con `{rvest}` extraemos todos los enlaces del sitio, y con `{purrr}` descargamos todos los archivos de una.","href":"https://bastianoleah.netlify.app/blog/2025-07-27/","tags":"datos ; Chile ; automatización ; loops ; purrr ; web scraping","title":"Descargar todos los archivos de la página web del Censo 2024 con {rvest}"},{"content":"Taller online que impartí para el Congreso Estudiantil de Sociología Interdisciplinaria. En este taller introduje a estudiantes de sociología al lenguaje de programación R, explicando los beneficios del análisis de datos desarrollado en flujos de trabajo basados en la programación, y las posibilidades que se abren para producir estudios y obtener resultados usando código. Puse énfasis en el uso de tecnologías y datos abiertos, y en el principal beneficio de la programación (en mi opinión): el poder actualizar resultados, aplicaciones y visualizaciones automáticamente.\nHerramientas y Estrategias contra la Corrupción: Talleres para la Medición y Análisis de la Corrupción en Chile desde el Análisis de Datos y Herramientas Abiertas Sesión 2: Extracción de Datos desde Medios: Web scraping y Criterios de Selección/ Análisis y Visualización de Datos\nWeb scraping en investigación social: definición, casos de uso. Selección de medios y criterios éticos. Exploración de estructuras HTML simples (solo como contexto). Uso de la app de Bastián para extraer datos de prensa. Visualización básica en RStudio con ggplot2 Aplicaciones vistas en la sesión Visualizador de casos de corrupción en Chile Visualizador de análisis de prensa digital en Chile Código En este repositorio está todo el código usado en el taller.\nEjemplo de web scraping de un medio digital chileno: scraping.qmd\nEjemplo de modelamiento de tópicos en análisis de texto: modelamiento_stm.R\nAnálisis de datos de un corpus de noticias de corrupción (6.000 noticias): explorar.qmd, los datos están disponibles en: datos/prensa_corrupcion.parquet\nDatos Base de datos de casos de corrupción: https://github.com/bastianolea/corrupcion_chile/ (descargar en formato Excel) Datos de prensa chilena: https://github.com/bastianolea/prensa_chile Datos obtenidos en ejemplo de web scraping: datos/noticias.csv Base de datos con noticias de corrupción (6.000 noticias), desde 2023 a julio de 2025: datos/prensa_corrupcion.parquet Base de datos con muestra de noticias chilenas (10.000, al azar) de toda temática del año 2024: datos/prensa_datos_muestra.csv Gráficos Densidad Gráfico de densidad de conceptos más frecuentes en noticias sobre corrupción, desde 2023 a julio de 2025 Nube de palabras Conceptos más frecuentes en noticias sobre corrupción, por fuente periodística Correlaciones Correlaciones de conceptos más fuertes para seis conceptos relacionados a corrupción ","date":"2025-07-23T00:00:00Z","excerpt":"\u003cp\u003eTaller online que impartí para el \u003cem\u003eCongreso Estudiantil de Sociología Interdisciplinaria.\u003c/em\u003e En este taller introduje a estudiantes de sociología al lenguaje de programación R, explicando los beneficios del análisis de datos desarrollado en flujos de trabajo basados en la programación, y las posibilidades que se abren para producir estudios y obtener resultados usando código. Puse énfasis en el uso de tecnologías y datos abiertos, y en el principal beneficio de la programación (en mi opinión): el poder actualizar resultados, aplicaciones y visualizaciones automáticamente.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/taller_corrupcion_cesi/","tags":"web scraping ; Chile ; visualización de datos ; análisis de texto ; videos","title":"Video: Taller Medición y Análisis de la Corrupción en Chile desde el Análisis de Datos y Herramientas Abiertas"},{"content":"En este blog ya hemos cubierto un par de herramientas de web scraping, cada una con sus características y beneficios: {rvest}, un paquete fácil y rápido de usar para extraer datos desde sitios webs comunes, y {RSelenium}, una interfaz en R para utilizar un software de automatización de navegadores bastante popular, aunque un poco menos amigable.\nAhora veremos cómo extraer datos desde páginas web usando un paquete de R que, a grandes rasgos, hace web scraping controlando Google Chrome.\nSi necesitas aprender lo básico del web scraping, primero revisa este tutorial de {rvest} El paquete {chromote} permite utilizar desde R Chrome DevTools para controlar navegadores Chromium, como Google Chrome, entre otros. Esto significa que podremos usar Chrome para conectarnos a los sitios web e interpretarlos usando su propio motor para cargar sitios web dinámicos y complejos, como con {RSelenium}.\nInstalamos el paquete, si es que no lo tenemos aún:\ninstall.packages(\u0026#34;chromote\u0026#34;) Cargamos {chromote} y especificamos que queremos utilizar el nuevo modo sin interfaz gráfica (headless) de Chrome, ya recientemente hubo una actualización de este modo que cambió la forma en que se usa.\nlibrary(chromote) options(chromote.headless = \u0026#34;new\u0026#34;) Ahora abrimos una nueva sesión en el navegador para poder empezar a controlarlo:\nchrome \u0026lt;- ChromoteSession$new() Lo que hicimos aquí fue crear un objeto chrome te representa la instancia del navegador que abrimos, y desde el cual podremos ir ejecutando las instrucciones.\nComo {chromote} principalmente funciona sin un interfaz gráfica, igual que {rvest}, podremos seguir desde nuestra consola de R sin tener que cambiar de aplicación.\nAhora podemos aplicar una de las fortalezas de estas estrategia para web scraping, que es especificar un agente de usuario personalizado. Lo que hace esto es decirle a los sitios web que te estás conectando desde un navegador común y corriente, en vez de que detecten que estas haciendo scraping de manera programática, es decir, que no sospechen que eres un robot 🤖\nnavegador \u0026lt;- \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36\u0026#34; chrome$Network$setUserAgentOverride(userAgent = navegador) De este modo estamos simulando que usamos un navegador real, lo que puede ayudarte a resolver problemas con conexiones a determinados sitios.\nNavegar a páginas web Ahora podemos controlar el navegador para dirigirlo a una página web con $Page$navigate(). Luego podemos ejecutar $Page$loadEventFired() para que la sesión se bloquee hasta que la página web esté completamente cargada. Esto es súper conveniente cuando los sitios son complejos, para evitar extraer el código del sitio antes de qué esté completamente cargado.\nenlace \u0026lt;- \u0026#34;https://bastianolea.rbind.io\u0026#34; chrome$Page$navigate(enlace) # navegar a la página chrome$Page$loadEventFired() # esperar a que cargue Extraer el código fuente Una vez que estamos en el sitio que deseamos, ejecutamos lo siguiente para extraer el código fuente de la página:\nfuente \u0026lt;- chrome$Runtime$evaluate(\u0026#34;document.querySelector(\u0026#39;html\u0026#39;).outerHTML\u0026#34;)$result$value # obtener datos Ahora contamos con todo el código HTML que genera el contenido de la página web, podemos recurrir a {rvest} para interpretar el código y así extraer los contenidos que necesitemos:\nlibrary(rvest) # leer código fuente del sitio web sitio \u0026lt;- read_html(fuente) # extraer un párrafo de texto sitio |\u0026gt; html_elements(\u0026#34;.page-main\u0026#34;) |\u0026gt; html_elements(\u0026#34;p\u0026#34;) |\u0026gt; html_text2() [1] \u0026quot;Este sitio contiene todo tipo de recursos sobre programación con el lenguaje R para análisis de datos, con un foco en las ciencias sociales. En el blog comparto consejos, novedades y tutoriales de R para que otras personas aprendan a programar en este lenguaje.\u0026quot; Otros Si queremos vigilar visualmente el control que hacemos sobre Chrome con {chromote}:\nchrome$view() También podemos tomar capturas de pantalla de lo que está viendo nuestro navegador remoto:\nchrome$screenshot(\u0026#34;pantallazo.png\u0026#34;) Finalmente, para terminar la sesión y apagar el proceso del navegador remoto, ejecutamos lo siguiente:\nchrome$close() ","date":"2025-07-17T00:00:00Z","excerpt":"El paquete `{chromote}` permite utilizar desde R _Chrome DevTools_ para controlar navegadores Chromium, como Google Chrome, entre otros. Esto significa que podremos usar Chrome para conectarnos a los sitios web e interpretarlos usando su propio motor para cargar sitios web dinámicos y complejos.","href":"https://bastianoleah.netlify.app/blog/tutorial_scraping_chromote/","tags":"web scraping","title":"Web scraping usando Google Chrome desde R con {chromote}"},{"content":"Selenium es un programa para automatizar y controlar navegadores web, lo que lo vuelve en una buena herramienta para realizar web scraping. El paquete de R {RSelenium} nos permitirá controlar un navegador por medio de código de R, lo cual abre infinitas posibilidades al momento de automatizar la obtención de datos e información desde sitios web dinámicos y/o complejos.\nEn este tutorial aprenderemos a usar {RSelenium} para programar scripts de R que automaticen el control de un navegador para interactuar con sitios web y así scrapear datos más difíciles de obtener.\nSi necesitas aprender lo básico del web scraping, primero revisa este tutorial de {rvest} En otro post aprendimos a hacer web scraping con {rvest}, un paquete muy sencillo de usar para obtener información desde sitios web. Entonces ¿por qué aprender también {RSelenium}? La diferencia es que Selenium es capaz de controlar un navegador web real en tu computador, como Chrome o Firefox, lo cual puede marcar la diferencia para extraer datos de sitios que buscan dificultar o impedir el acceso a herramientas automatizadas de web scraping.\nIniciar un cliente La primera que necesitamos para hacer web scraping es un navegador web. La función rsDriver() se encarga de la descarga e instalación de un navegador para poder usarlo desde R. Elegimos Firefox, y opcionalmente definimos un puerto1.\nlibrary(RSelenium) driver \u0026lt;- rsDriver(browser = \u0026#34;firefox\u0026#34;, port = 4560L, verbose = F, chromever = NULL, phantomver = NULL) Éste código pondrá a descargar el navegador y lo abrirá:\nAparece una ventana de navegador con la barra superior en rojo, indicando que está siendo controlado por otro programa, en este caso por R.\nAhora tenemos que pasarle el control a un objeto de R que representa al navegador remoto:\nremote \u0026lt;- driver$client Navegar a un sitio web Desde el navegador remoto podemos ir ejecutando las acciones que necesitamos. Lo primero es navegar al sitio web que nos interesa\nremote$navigate(\u0026#34;https://www.portaltransparencia.cl/PortalPdT/directorio-de-organismos-regulados/?org=MU291\u0026#34;) Si nos vamos a ver el navegador, vemos que la página efectivamente se ha cargado.\nDesde esta página web de Transparencia podemos extraer datos sobre la gestión de organismos públicos, pero para llegar a ellos tenemos que hacer clic en varios enlaces. Usaremos {RSelenium} para navegar el sitio y obtener los datos, generando un script que podremos reutilizar para actualizar los datos, obtener datos de manera masiva de este sitio, u obtener los datos del mismo portal pero de otro organismo público, dado que funcionan igual.\nIdentificar elementos En este punto podemos usar el navegador para ir explorando el sitio. Obviamente podríamos ir haciendo clic en el mismo navegador, pero no es la idea. La idea es poder automatizar el proceso por medio de un script que controle las acciones que se hacen en el navegador. Lo que sí podemos hacer es usar las herramientas del navegador para ayudarnos.\nSi hacemos clic derecho sobre cualquier elemento del sitio y elegimos Inspeccionar, se abrirá el panel de herramientas de desarrollador web.\nCon este panel podemos inspeccionar el código fuente del sitio web, desde donde podremos extraer texto, enlaces, imágenes, tablas, y más.\nEn el panel de herramientas de desarrollador apretamos el botón de selección (destacado con un círculo en la imagen anterior, o comando + shift + C) para que cuando pasemos el cursor sobre los elementos del sitio web, se destaque el código fuente correspondiente.\nCuándo identificamos el elemento que necesitamos, vamos al código correspondiente, hacemos clic derecho, y copiamos el xpath o el selector css. Ambas son formas de identificar de manera única elementos en un sitio web, pero esta vez copiaremos el xpath.\nInteractuar con elementos Ahora que tenemos identificado el enlace que queremos apretar por medio de su xpath, le decimos al navegador que le haga clic:\nremote$findElement(\u0026#34;xpath\u0026#34;, \u0026#39;//*[@id=\u0026#34;A6428:formInfo:j_idt39:0:datalist:3:j_idt43:4:j_idt47\u0026#34;]\u0026#39;)$ clickElement() Funcionó! El navegador navegó al enlace que le pedimos.\nAhora repetimos el proceso para navegar al enlace Municipal:\nremote$findElement(\u0026#34;xpath\u0026#34;, \u0026#39;//*[@id=\u0026#34;A6428:formInfo:j_idt76:0:j_idt78\u0026#34;]\u0026#39;)$ clickElement() Ahora el sitio muestra una selección de todos los años que tienen datos. Si volvemos a repetir el proceso, nos damos cuenta de que el xpath del año 2025 termina con un 0, y el del año 2024 termina en 1, el de 2023 en 2, y así sucesivamente.\nEsto significa que podríamos automatizar el acceso a todos los años simplemente creando un loop que vaya desde 0 a 10 (año 2015). Pero por ahora accedamos al segundo año:\nremote$findElement(\u0026#34;xpath\u0026#34;, \u0026#39;//*[@id=\u0026#34;A6428:formInfo:j_idt94:0:j_idt95\u0026#34;]\u0026#39;)$ clickElement() Ahora llegamos a una página con los meses, donde se repita el mismo patrón: todos los meses comparten un xpath que termina con el número del cero a la cantidad de meses presentes.\nSi navegamos alguno de los meses, finalmente llegamos a los datos que estábamos buscando:\nremote$findElement(\u0026#34;xpath\u0026#34;, \u0026#39;//*[@id=\u0026#34;A6428:formInfo:j_idt110:0:j_idt111\u0026#34;]\u0026#39;)$ clickElement() Obtener el código fuente del sitio Ahora que llegamos a una tabla de datos, podemos pedirle al navegador que nos entregue todo el código de fuente del sitio, para continuar el scraping con el paquete {rvest}:\nlibrary(rvest) fuente \u0026lt;- remote$getPageSource()[[1]] sitio \u0026lt;- read_html(fuente) Si necesitas aprender a usar {rvest}, revisa este tutorial primero Podemos usar la función html_elements() para extraer elementos del sitio por su selector CSS, y convertirlos en texto. Por ejemplo, extraer el texto que está en el título de la tabla:\nsitio |\u0026gt; html_elements(\u0026#34;.section-title\u0026#34;) |\u0026gt; html_text2() [1] \u0026quot;04. Personal y remuneraciones\u0026quot; Extraer tablas Si es que el sitio web tiene datos en forma de tabla, podemos extraerlo fácilmente con la función html_table()\ntabla \u0026lt;- sitio |\u0026gt; html_table() tabla[[1]] # A tibble: 100 × 19 Estamento `Nombre completo` `Cargo o función` `Grado EUS o jornada` \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 Auxiliar ACEVEDO BERTRIN, ANDR… CUADRILLA OPERAC… 14 2 Administrativo AEDO ACEVEDO, ESTEFAN… ADMINISTRATIVA H… 11 3 Administrativo AGUILAR MARTINEZ, ELI… ADMINISTRATIVO D… 12 4 Administrativo AHUMADA COFRE, GERALD… ADMINISTRATIVA V… 16 5 Profesional ALARCON HERRERA, CARL… PROFESIONAL DE A… 6 6 Administrativo ALARCON NEIRA, CLAUDI… INSPECTOR VIGILA… 14 7 Auxiliar ALCAINO MARTINEZ, EDU… AUXILIAR ASEO Y … 14 8 Administrativo ALDERETE ALMENDRAS, N… ADMINISTRATIVA V… 16 9 Administrativo ALTAMIRANO ROJAS, ANN… SECRETARIA ADMIN… 11 10 Administrativo ALVAREZ FARFAN, CYNTH… INSPECTORA FERIA… 16 # ℹ 90 more rows # ℹ 15 more variables: `Calificación profesional o formación` \u0026lt;chr\u0026gt;, # Región \u0026lt;chr\u0026gt;, `Asignaciones especiales` \u0026lt;chr\u0026gt;, # `Remuneración bruta mensualizada` \u0026lt;chr\u0026gt;, # `Remuneración líquida mensualizada` \u0026lt;chr\u0026gt;, # `Remuneraciones adicionales` \u0026lt;chr\u0026gt;, `Remuneración Bonos incentivos` \u0026lt;chr\u0026gt;, # `Derecho a horas extraordinarias` \u0026lt;chr\u0026gt;, … Ahora que tenemos los datos, sólo resta guardarlos en nuestro proyecto de R y seguir procesándolos en otro script.\ntabla[[1]] |\u0026gt; readr::write_rds(\u0026#34;datos.rds\u0026#34;) Descargar archivos Finalmente, tenemos la opción de descargar los datos de este sitio como un archivo en el botón Descargar CSV que aparece arriba. Pero a diferencia de los enlaces, generalmente los botones de los sitios web no conllevan un enlace, sino que esperan que los aprietes para ejecutar un acción internamente que te entrega el archivo. En estos casos, sería imposible obtener el enlace del botón y descargar el archivo enlazado con download.file(\u0026quot;enlace\u0026quot;), Sino que simplemente hay que perder el botón y esperar que el sitio te entregue el archivo.\nUtilizamos las herramientas del navegador para identificar el xpath o selector CSS del botón para presionarlo.\n# descargar archivo remote$findElement(\u0026#39;css selector\u0026#39;, \u0026#39;.fa-file-csv\u0026#39;)$ clickElement() Como estamos controlando un navegador web real, el archivo descargado aparecerá en la carpeta de descargas de tu computadora, no en tu proyecto de R. Pero podemos mover el archivo desde la carpeta de descargas a tu proyecto con el siguiente código:\nlibrary(fs) # crear una carpeta en tu proyecto dir_create(\u0026#34;datos\u0026#34;) # mover archivo descargado al proyecto file_move(path = \u0026#34;~/Downloads/TransparenciaActiva.csv\u0026#34;, new_path = \u0026#34;datos/TransparenciaActiva.csv\u0026#34;) Opcionalmente, al crear el navegador con rsDriver(), puedes configurar el navegador para especificar la ubicación de las descargas:\nlibrary(here) perfil \u0026lt;- makeFirefoxProfile( list(browser.download.dir = here()) ) rsDriver(browser = \u0026#34;firefox\u0026#34;, chromever = NULL, phantomver = NULL, extraCapabilities = perfil) Terminar la navegación Cuándo termines de usar el navegador, tienes que cerrar la sesión. Esto se hace principalmente para liberar el puerto que asignaste al navegador al crearlo.\ndriver$server$stop() Pero te preguntarás, ¿por qué hicimos todo esto si desde el principio podíamos apretar el botón de descargas y obtener los datos? Lo primero es porque de esta manera tenemos un script con el que podemos seguir las instrucciones paso a paso para volver a descargar exactamente el mismo archivo en el futuro. Pero también, porque podemos usar este Skip para modificarlo y obtener un archivo de otra fecha, o incluso modificar la dirección de raíz del scraping y obtener el archivo equivalente pero de otro organismo público.\nEl potencial del web scraping no solamente obtener datos, sino obtenerlos de una manera tal que podamos automatizar la obtención masiva de los mismos.\nOtros Tomar captura de pantalla También es posible capturar lo que está mostrando el navegador por medio de una captura de pantalla:\nremote$screenshot(file = \u0026#39;pantallazo_1.jpg\u0026#39;) Este tipo de acciones no es posible con herramientas de web scraping como {rvest}, que funcionan navegando directamente el código fuente del sitio, sin cargarlo como normalmente haría un navegador web.\nDesplazarse por el sitio Otro tipo de acciones que a veces son necesarias de hacer para obtener datos en un sitio es scrollear por el mismo para que se carguen los elementos. Esto puede hacerse en Selenium ejecutando un script:\n# scrolear remote$executeScript(paste(\u0026#34;window.scrollBy(0, \u0026#34;, 500, \u0026#34;);\u0026#34;)) # verificar visualmente remote$screenshot(file = \u0026#39;pantallazo_2.jpg\u0026#39;) Cambiar dimensiones de la ventana remote$setWindowSize(width = 640, height = 480) Obtener dimensiones de la ventana ventana_alto \u0026lt;- remote$executeScript(\u0026#34;return window.innerHeight\u0026#34;)[[1]] ventana_ancho \u0026lt;- remote$executeScript(\u0026#34;return window.innerWidth\u0026#34;)[[1]] Grabar tu interacción con el navegador Usa Selenium IDE para grabar interacciones con un sitio. Puedes instalar esta extensión de Firefox para grabar tu interacción con un sitio web, y que queden registrado todos los pasos que hiciste en el sitio, de manera que puedas reproducirlos después en un script.\nEl puerto es como la conexión entre tu computador y el navegador, por lo que no pueden haber dos navegadores en un mismo puerto. Si necesitas abrir más de un navegador, o te aparece ocupado el puerto, intenta con otro.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-07-15T00:00:00Z","excerpt":"Selenium es una herramienta que permite realizar web scraping avanzado por medio del control programático de un navegador web, lo cual abre infinitas posibilidades al momento de automatizar la obtención de datos e información desde sitios web dinámicos y/o complejos. En este tutorial aprenderemos a usar {RSelenium} para programar scripts de R que automaticen el control de un navegador para interactuar con sitios web y así scrapear datos mas difíciles de obtener.","href":"https://bastianoleah.netlify.app/blog/tutorial_scraping_selenium/","tags":"web scraping ; datos","title":"Tutorial: web scraping controlando un navegador web con {RSelenium} en R"},{"content":"Uno de los principales beneficios del análisis de datos en base a programación es que el código es reutilizable. Esto significa que cualquier cosa que hayas hecho puedes reutilizarla, y así ahorrar trabajo. El siguiente paso es reutilizar el código de tal forma que sirva para aplicarlo a varios casos a la vez, incluso cientos o miles de veces.\nLa reutilización de código es súper conveniente para la visualización de datos: una vez que diseñaste un gráfico, con muy pocas modificaciones puedes adaptarlo para que funcione con una fuente de datos distintas, una fuente actualizada, o para que visualice distintas variables.\nEn este post vamos a ver cómo automatizar la creación de gráficos para que solamente tengas que diseñar una visualización que te genere múltiples resultados.\nLo que necesitamos para automatizar la generación de gráficos es:\nPreparar los datos para la visualización Diseñar una visualización que pueda adaptarse a distintos datos o variables Crear un loop o iteración donde el código que genera el gráfico se ejecute múltiples veces en base a lo que necesites replicar. Datos Para este tutorial usaremos un conjunto de datos relativamente grande sobre educación, sacado de mi repositorio de datos sociales abiertos. Se trata de una bases de datos de resultados de puntajes de la Prueba de Acceso a la Educación Superior (PAES).\nDado que los datos ya vienen limpios y procesados desde su fuente original en el repositorio de GitHub, podemos cargarlos directamente desde su repositorio usando el enlace:\nlibrary(arrow) # cargar datos desde GitHub datos \u0026lt;- read_parquet(\u0026#34;https://github.com/bastianolea/puntajes_prueba_paes/raw/main/datos/puntajes_paes_2024.parquet\u0026#34;) Antes que nada, miremos los datos:\nlibrary(dplyr) glimpse(datos) Rows: 304,420 Columns: 12 $ nombre_comuna \u0026lt;chr\u0026gt; \u0026quot;Ñuñoa\u0026quot;, \u0026quot;Santiago\u0026quot;, \u0026quot;San Antonio\u0026quot;, \u0026quot;Concepción\u0026quot;, \u0026quot;Pe… $ codigo_comuna \u0026lt;dbl\u0026gt; 13120, 13101, 5601, 8101, 9113, 13130, 7101, 13118, 1… $ nombre_region \u0026lt;chr\u0026gt; \u0026quot;Metropolitana de Santiago\u0026quot;, \u0026quot;Metropolitana de Santia… $ codigo_region \u0026lt;chr\u0026gt; \u0026quot;13\u0026quot;, \u0026quot;13\u0026quot;, \u0026quot;05\u0026quot;, \u0026quot;08\u0026quot;, \u0026quot;09\u0026quot;, \u0026quot;13\u0026quot;, \u0026quot;07\u0026quot;, \u0026quot;13\u0026quot;, \u0026quot;13\u0026quot;,… $ año \u0026lt;dbl\u0026gt; 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024,… $ cod_sexo \u0026lt;chr\u0026gt; \u0026quot;Femenino\u0026quot;, \u0026quot;Femenino\u0026quot;, \u0026quot;Masculino\u0026quot;, \u0026quot;Femenino\u0026quot;, \u0026quot;Fem… $ promedio_notas \u0026lt;dbl\u0026gt; 6.50, NA, NA, 5.10, 6.35, 5.45, 6.50, 6.10, NA, NA, 5… $ paes_complectora \u0026lt;dbl\u0026gt; 711, NA, 484, 597, 491, 364, NA, 584, 510, NA, NA, NA… $ paes_matematica1 \u0026lt;dbl\u0026gt; NA, NA, 513, 498, 436, 424, NA, 498, 478, NA, NA, NA,… $ paes_matematica2 \u0026lt;dbl\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N… $ paes_histciesoc \u0026lt;dbl\u0026gt; 739, NA, 520, 647, 373, 405, NA, 654, 540, NA, NA, NA… $ paes_ciencias \u0026lt;dbl\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 4… En este conjunto de datos, de 304 mil observaciones, cada fila representa a una persona que dio la Prueba de Acceso a la Educación Superior (PAES), y en las columnas se detallan características de las personas y los puntajes obtenidos. Las columnas que empiezan con paes_ contienen los puntajes.\nExploremos visualmente dos de las variables con un gráfico de dispersión:\nlibrary(ggplot2) datos |\u0026gt; # filter(nombre_region == \u0026#34;Metropolitana de Santiago\u0026#34;) |\u0026gt; ggplot() + aes(paes_matematica1, paes_complectora) + geom_jitter(height = 10, width = 10, size = 0.2, alpha = 0.1) + theme_classic() + coord_fixed() + labs(x = \u0026#34;Matemáticas\u0026#34;, y = \u0026#34;Comprensión lectora\u0026#34;) Obtenemos una tenebrosa nube que correlaciona los puntajes de la prueba de comprensión lectora y la de matemátitas. En este gráfico cada punto es un estudiante, y la ubicación del punto corresponde al puntaje que obtuvo en las pruebas de Matemática y Comprensión Lectora. Como los datos vienen con más variables de caracterización de las observaciones, podemos filtrar los datos por comuna:\nlibrary(dplyr) datos |\u0026gt; filter(nombre_comuna == \u0026#34;La Florida\u0026#34;) # A tibble: 6,306 × 12 nombre_comuna codigo_comuna nombre_region codigo_region año cod_sexo \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 La Florida 13110 Metropolitana de Sa… 13 2024 Masculi… 2 La Florida 13110 Metropolitana de Sa… 13 2024 Masculi… 3 La Florida 13110 Metropolitana de Sa… 13 2024 Femenino 4 La Florida 13110 Metropolitana de Sa… 13 2024 Masculi… 5 La Florida 13110 Metropolitana de Sa… 13 2024 Femenino 6 La Florida 13110 Metropolitana de Sa… 13 2024 Femenino 7 La Florida 13110 Metropolitana de Sa… 13 2024 Femenino 8 La Florida 13110 Metropolitana de Sa… 13 2024 Femenino 9 La Florida 13110 Metropolitana de Sa… 13 2024 Femenino 10 La Florida 13110 Metropolitana de Sa… 13 2024 Femenino # ℹ 6,296 more rows # ℹ 6 more variables: promedio_notas \u0026lt;dbl\u0026gt;, paes_complectora \u0026lt;dbl\u0026gt;, # paes_matematica1 \u0026lt;dbl\u0026gt;, paes_matematica2 \u0026lt;dbl\u0026gt;, paes_histciesoc \u0026lt;dbl\u0026gt;, # paes_ciencias \u0026lt;dbl\u0026gt; Esto significa qeu podemos generar gráficos para subgrupos de la población; en este caso, gráficos por unidades administrativas o comunas.\nGráfico Si necesitas aprender a visualizar datos desde cero, revisa este tutorial de {ggplot2} Podemos intentar volver a generar el mismo gráfico con exactamente el mismo código, pero antes filtrando una comuna, para obtener sólo los resultados de estudiantes que viven en la comuna de La Florida:\ndatos |\u0026gt; # filtrar datos filter(nombre_comuna == \u0026#34;La Florida\u0026#34;) |\u0026gt; # gráfico ggplot() + aes(paes_matematica1, paes_complectora) + geom_jitter(shape = 3, height = 5, width = 5, size = 0.4, alpha = 0.5) + theme_classic() + coord_fixed() + labs(subtitle = \u0026#34;La Florida\u0026#34;, x = \u0026#34;Matemáticas\u0026#34;, y = \u0026#34;Comprensión lectora\u0026#34;) Obviamente podemos filtrar por cualquier otra comuna (o cualquier otra variable) y obtendremos el gráfico de los estudiantes correspondientes.\nMejoremos un poco la visualización, ahora además con la reutilización en mente:\n# el nombre de la comuna que seleccionamos para visualizar comuna \u0026lt;- \u0026#34;Lo Espejo\u0026#34; # filtrado de los gráficos datos \u0026lt;- datos |\u0026gt; # crear variable que indica si la observacion pertenece o no a la comuna seleccionada mutate(seleccion = if_else(nombre_comuna == comuna, true = \u0026#34;Destacada\u0026#34;, false = \u0026#34;Otras\u0026#34;)) |\u0026gt; # ordenar las observaciones según la selección de comuna arrange(desc(seleccion)) |\u0026gt; # redondear punteajes mutate(across(c(paes_matematica1, paes_complectora), ~signif(.x, 2))) # gráfico básico grafico_base \u0026lt;- datos |\u0026gt; ggplot() + aes(paes_matematica1, paes_complectora, color = seleccion, alpha = seleccion) + geom_point(size = 0.4) # modificar detalles del gráfico grafico_bonito \u0026lt;- grafico_base + # escala de colores para destacar la comuna seleccionada scale_color_manual(values = c(\u0026#34;Otras\u0026#34; = \u0026#34;grey85\u0026#34;, \u0026#34;Destacada\u0026#34; = \u0026#34;#6E3A98\u0026#34;)) + # escala de transparencia para mejorar visualización scale_alpha_manual(values = c(\u0026#34;Otras\u0026#34; = 0.1, \u0026#34;Destacada\u0026#34; = 0.3)) + # temas theme_classic() + theme(legend.position = \u0026#34;none\u0026#34;) + # ocultar leyenda coord_fixed(expand = FALSE) + # gráfico cuadrado labs(subtitle = comuna, # nombre de la comuna elegida title = \u0026#34;Puntajes de prueba PAES 2024\u0026#34;, x = \u0026#34;Matemáticas\u0026#34;, y = \u0026#34;Comprensión lectora\u0026#34;) grafico_bonito Efectivamente obtenemos un gráfico más bonito ☺️ donde especificamos de antemano un objeto comuna con el nombre de la unidad administrativa que queremos visualizar. Luego, este objeto se usa para filtrar los datos y para poner un título al gráfico.\nTambién usamos la función signif() para redondear los puntajes a dos dígitos significativos; es decir, un número como 456 se vuelve 450, para simplificar el ordenamiento de los puntos en el plano del gráfico.\nFunción Si nunca has creado una función en R, revisa esta guía El siguiente paso es opcional, pero lo recomiendo porque simplifica bastante la lectura del código y su mantenimiento futuro. Vamos a crear una función que genere el mismo gráfico que antes, para facilitar su reutilización.\nPara crear una función, simplemente incluimos el código dentro de function() { } y definimos los argumentos de la función en function() y dentro de la misma. Pero como somos bacanes, en vez de function() vamos a usar la abreviatura \\() 😎\ngrafico \u0026lt;- \\(datos, comuna) { datos |\u0026gt; mutate(seleccion = if_else(nombre_comuna == comuna, true = \u0026#34;Destacada\u0026#34;, false = \u0026#34;Otras\u0026#34;)) |\u0026gt; arrange(desc(seleccion)) |\u0026gt; mutate(across(c(paes_matematica1, paes_complectora), ~signif(.x, 2))) |\u0026gt; ggplot() + aes(paes_matematica1, paes_complectora, color = seleccion, alpha = seleccion) + geom_point(size = 0.4) + scale_color_manual(values = c(\u0026#34;Otras\u0026#34; = \u0026#34;grey85\u0026#34;, \u0026#34;Destacada\u0026#34; = \u0026#34;#6E3A98\u0026#34;)) + scale_alpha_manual(values = c(\u0026#34;Otras\u0026#34; = 0.1, \u0026#34;Destacada\u0026#34; = 0.3)) + theme_classic() + theme(legend.position = \u0026#34;none\u0026#34;) + coord_fixed(expand = FALSE) + labs(subtitle = comuna, title = \u0026#34;Puntajes de prueba PAES 2024\u0026#34;, x = \u0026#34;Matemáticas\u0026#34;, y = \u0026#34;Comprensión lectora\u0026#34;) } Hacer una función en R es como empaquetar tu código en su propio programita para que sea más fácil de usar sin tener que copiar y pegar todo el código cada vez que lo necesites. También tiene el beneficio de que, si necesitas corregir o mejorar el código, lo modificas una sola vez y el cambio va a aparecer en todos los demás lugares que la uses.\nAhora que creamos la función, solo tenemos que ejecutar grafico() para generar el gráfico!\ngrafico(datos, \u0026#34;La Reina\u0026#34;) grafico(datos, \u0026#34;Cerrillos\u0026#34;) Automatización Una vez que hayamos desarrollado el código que genera la visualización, y que hayamos probado y confirmado que el código va a servir en distintas situaciones (en nuestro caso, filtrando distintas comunas), podemos introducir el código a un loop para generar todas las variaciones que queramos del mismo gráfico.\nEn el caso de nuestro conjunto de datos, esto sería hacer un gráfico por cada comuna en la base de datos. Si hiciéramos esto a mano, tendríamos que copiar y pegar más de 300 veces el mismo código 😰 Así que, para no tener que hacer las cosas a mano como los perdedores, vamos a automatizar el proceso de generación de gráficos.\nDefinamos primero la lista de elementos por los que queremos crear los gráficos. Esto pueden ser años, países, nombres, o en nuestro caso, divisiones administrativas como pueden ser comunas.\ncomunas \u0026lt;- unique(datos$nombre_comuna) El vector anterior nos da todas las categorías de la variable nombre_comuna del dataset datos, eliminando duplicados con unique(). Estos serán los valores por los que iremos iterando: por cada uno de estos valores vamos a querer que se produzca un gráfico distinto.\nEn otras palabras, queremos aplicar nuestra función grafico() (o el código para generar el gráfico) una vez por cada comuna en el dataset. Para esto utilizaremos un bucle o loop en R nos permite repetir un conjunto de instrucciones varias veces.\nSi nunca has hecho un loop en R, [revisa esta guía]( https://bastianolea.rbind.io/blog/r_introduccion/r_intermedio/#bucles Si bien en R podemos crear loops o bucles con for, usaremos map() del paquete {purrr} para lograr lo mismo de una forma más elegante y eficiente.\nDentro de map(), primero entregamos los elementos por los que queremos iterar (en nuestro caso, los nombres de las comunas), y luego se aplica una función1, dentro de la cual ponemos lo que queremos que pase en cada paso de la iteración. O sea que, por cada elemento de comunas, se va a aplicar el código del gráfico.\nDentro de la función, simplemente ponemos el código que genera el gráfico, y luego le pedimos que guarde el resultado en nuestro computador. Es muy importante que los nombres de archivo sean distintos! De lo contrario los resultados se van a ir sobrescribiendo unos a otros 🙄 Así que usamos el nombre de la comuna dentro de paste() para crear nombres de archivo únicos.\nlibrary(purrr) map(comunas[1:12], # elementos por los que se va a iterar \\(comuna) { # cada elemento va a pasar a la función como un objeto llamado `comuna` # crear gráficos grafico \u0026lt;- grafico(datos, comuna) # guardar gráficos ggsave(plot = grafico, filename = paste0(\u0026#34;graficos/Gráfico Paes \u0026#34;, comuna, \u0026#34;.jpeg\u0026#34;), # nombre del archivo width = 6, height = 6) }) [[1]] [1] \u0026quot;graficos/Gráfico Paes Ñuñoa.jpeg\u0026quot; [[2]] [1] \u0026quot;graficos/Gráfico Paes Santiago.jpeg\u0026quot; [[3]] [1] \u0026quot;graficos/Gráfico Paes San Antonio.jpeg\u0026quot; [[4]] [1] \u0026quot;graficos/Gráfico Paes Concepción.jpeg\u0026quot; [[5]] [1] \u0026quot;graficos/Gráfico Paes Perquenco.jpeg\u0026quot; [[6]] [1] \u0026quot;graficos/Gráfico Paes San Miguel.jpeg\u0026quot; [[7]] [1] \u0026quot;graficos/Gráfico Paes Talca.jpeg\u0026quot; [[8]] [1] \u0026quot;graficos/Gráfico Paes Macul.jpeg\u0026quot; [[9]] [1] \u0026quot;graficos/Gráfico Paes Quilicura.jpeg\u0026quot; [[10]] [1] \u0026quot;graficos/Gráfico Paes San Joaquín.jpeg\u0026quot; [[11]] [1] \u0026quot;graficos/Gráfico Paes Valparaíso.jpeg\u0026quot; [[12]] [1] \u0026quot;graficos/Gráfico Paes Limache.jpeg\u0026quot; Con tan sólo ejecutar el código anterior, obtendremos más de 300 gráficos! Pero como son tantos, le puse comunas[1:10] para que sólo se hagan las primeras 10 😅\nProcesando\u0026hellip;\nGrabación en tiempo real de los gráficos siendo generados dentro de un loop de purrr::map() Optimización Naturalmente, generar cientos de gráficos puede demorar unos minutos, sobre todo si los datos son muy grandes (y en nuestro caso, cada gráfico tiene más de 300 mil puntos\u0026hellip;).\nLo bueno es que podemos aprovechar todo el potencial de nuestras computadoras modificando el código para que el proceso sea multiprocesador; es decir, que se usen todos los procesadores de nuestro computador al mismo tiempo. Tan sólo especificamos cuántos procesadores queremos usar con plan(multisession, workers = 6), y luego reemplazamos map() por future_map() de {furrr}:\nlibrary(furrr) plan(multisession, workers = 8) future_map(comunas[1:20], \\(comuna) { grafico \u0026lt;- grafico(datos, comuna) ggsave(plot = grafico, filename = paste0(\u0026#34;graficos/Gráfico Paes \u0026#34;, comuna, \u0026#34;.jpeg\u0026#34;), # nombre del archivo width = 6, height = 6) }) Visualización de uso de CPU al procesar gráficos multicore con R, demostrando que se usan todos los procesadores al mismo tiempo Procesando\u0026hellip;\nGrabación en tiempo real de los gráficos siendo generados, usando 8 procesadores Listo! 🎉 Imagínate todo el tiempo que ahorramos: podemos obtener cientos de gráficos con tan sólo presionar el botón Run (o la combinación control + enter), y si tenemos que actualizar, modificar o mejorar los gráficos, simplemente cambiamos el código en la función y volvemos a ejecutar el loop. Excelente! Esto hace que aprender a programar valga la pena, cierto?\u0026hellip; cierto? 🥺\nEn vez de una función, usaremos una función anónima, lo que significa que crearemos la función dentro de map() solamente porque es más rápido.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-07-14T00:00:00Z","excerpt":"La reutilización de código es súper conveniente para la visualización de datos: una vez que diseñaste un gráfico, con muy pocas modificaciones puedes adaptarlo para que funcione con una fuente de datos distintas, una fuente actualizada, o para que visualice distintas variables. En este post vamos a ver cómo automatizar la creación de gráficos para que solamente tengas que diseñar una visualización que te genere múltiples resultados.","href":"https://bastianoleah.netlify.app/blog/ggplot_purrr/","tags":"visualización de datos ; automatización ; purrr ; loops ; ggplot2 ; optimización","title":"Generar múltiples gráficos automáticamente con R"},{"content":"Existe un gráfico estadístico muy famoso por haber aparecido en la portada del disco Unknown Pleasures de Joy Division:\nSe trata de un gráfico de densidad, donde las densidades están apiladas verticalmente y se sobreponen, dando una apariencia montañosa, cordillerana y casi tridimensional.\nEn este post reproduciremos este gráfico en R, pero usando datos socioeconómicos de la Encuesta de caracterización socioeconómica nacional (Casen) 2022.\nEste post es sobre visualización de datos con {ggplot2}. Si quieres aprender, puedes revisar este tutorial sobre visualización de datos desde cero! Los paquetes que usaremos son los siguientes:\nlibrary(arrow) # carga de datos en formato parquet library(dplyr) # manipulación de datos library(ggplot2) # visualización de datos library(scales) # escalas para gráficos library(ggridges) # geometría de densidad apilada para {ggplot2} library(forcats) # orden ordenamiento de datos tipo factor library(tidyr) # transformación y ordenamiento de datos Vamos a obtener los datos de la Casen de forma más rápida desde un repositorio de datos de ingresos de la encuesta Casen, en el cual dispongo los datos en un formato más rápido de cargar.\nComo dato al margen, los datos de ese repositorio se usan en una aplicación interactiva desarrollada en R. donde puedes comparar la densidades de las distribuciones de ingreso de cualquier comuna de Chile.\nAplicación web comparador de ingresos Casen, desarrollada en R. Pruébala aquí. Datos Primero cargamos los datos de población comunal, que solamente usaremos para luego seleccionar las comunas con mayor población del país. Como en el siguiente código los datos se cargan directamente desde el repositorio, no necesita descargar ningún archivo para poder ejecutar este código en tu computadora.\n# cargar datos de población poblacion \u0026lt;- arrow::read_parquet(\u0026#34;https://github.com/bastianolea/casen_comparador_ingresos/raw/main/datos/censo_proyecciones_año.parquet\u0026#34;) # obtener población comunal poblacion_comunas \u0026lt;- poblacion |\u0026gt; filter(año == 2024) |\u0026gt; arrange(desc(población)) Luego cargamos los datos de la encuesta Casen desde el repositorio:\n# cargar encuesta casen casen2022_2 \u0026lt;- arrow::read_parquet(\u0026#34;https://github.com/bastianolea/casen_comparador_ingresos/raw/main/datos/casen_ingresos.parquet\u0026#34;) Procesamiento Para poder usar estos datos de la forma correcta necesitamos procesarlos en base al diseño de encuestas de muestreo complejo, en el que se basa la Casen. Esto debido a que la encuesta requiere de la aplicación de factores de expansión para poder obtener resultados que busquen representar a la población real. Para más información, revisa este post donde lo explico.\n# procesar encuesta de diseño complejo library(srvyr) casen_svy \u0026lt;- casen2022_2 |\u0026gt; # filtrar 80 comunas con más población filter(comuna %in% poblacion_comunas$comuna[1:80]) |\u0026gt; # crear diseño de encuestas complejas as_survey(weights = expc, strata = estrato, ids = id_persona, nest = TRUE) En el código anterior se cargó el conjunto de datos, se filtraron las 80 comunas con más población, para no tener un gráfico eterno, y se aplicó el diseño de encuestas complejas con as_survey().\nUna vez que tenemos el objeto survey que nos permite trabajar con los datos usando la metodología apropiada de factor de expansión, calculamos la cantidad de personas a la que representa cada observación en la encuesta. Usaremos la variable de ingresos propios de la ocupación principal, yoprcor.\n# calcular cantidades usando factor de expansión casen_ingresos \u0026lt;- casen_svy |\u0026gt; group_by(comuna, yoprcor) |\u0026gt; summarize(n = survey_total(), p = survey_mean()) Si realizamos los datos, nos encontramos con las observaciones de la encuesta, que incluyen la información de la comuna y la variable de ingresos que elegimos, pero además, tenemos la columna n que nos indica a cuántas personas representa cada observación de la encuesta, gracias a la aplicación del factor de expansión.\n# A tibble: 9,155 × 6 # Groups: comuna [80] comuna yoprcor n n_se p p_se \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Alto Hospicio 14789 36 36 0.000260 0.000233 2 Alto Hospicio 20000 32 32 0.000231 0.000213 3 Alto Hospicio 30000 211 109. 0.00152 0.000691 4 Alto Hospicio 40000 36 36 0.000260 0.000233 5 Alto Hospicio 45000 36 36 0.000260 0.000233 6 Alto Hospicio 50000 527 262. 0.00380 0.00124 7 Alto Hospicio 60000 405 333. 0.00292 0.00207 8 Alto Hospicio 70000 48 48 0.000346 0.000334 9 Alto Hospicio 75000 77 52.1 0.000556 0.000332 10 Alto Hospicio 80000 264 136. 0.00191 0.000779 # ℹ 9,145 more rows Ahora limpiamos un poco los datos, calculamos las medianas de ingresos comunales, y limitamos los ingresos máximos en una cifra que considero un ingreso suficientemente alto como para mostrar la inequidad de ingresos sin que los ingresos bajos se vuelvan invisibles.\n# limpiar datos y limitar ingresos casen_ingresos_2 \u0026lt;- casen_ingresos |\u0026gt; rename(variable = yoprcor) |\u0026gt; filter(!is.na(variable)) |\u0026gt; # calcular mediana de ingresos group_by(comuna) |\u0026gt; mutate(mediana = median(variable, na.rm = T)) |\u0026gt; # limitar ingresos máximos, ordenar comunas filter(variable \u0026lt; 2500000) |\u0026gt; ungroup() |\u0026gt; # ordenar las comunas de mayor a menor por su mediana de ingresos mutate(comuna = as.factor(comuna), comuna = fct_reorder(comuna, mediana)) Finalmente, preparamos los datos para la visualización. El tipo de visualización que vamos a usar, el gráfico de densidad, realiza un cálculo de densidades; es decir, distribuye todas las observaciones a través del eje horizontal del gráfico, y eleva a una curva en relación a la cantidad de personas que percibe los ingresos correspondientes a cada punto del eje.\nEntonces, necesitamos que cada observación de nuestra tabla corresponda a una persona que percibe un ingreso específico.\nComo vimos más atrás, tenemos una columna de comunas, una columna de la cifra de los ingresos (yoprcor), y la columna n que indica cuántas personas perciben cada ingreso. Por lo tanto, tenemos que alargar nuestra base de datos para que cada ingreso percibido aparezca repetido en tantas filas como personas se indican en n. En otras palabras, tenemos que hacer el ejercicio inverso a contar las personas que perciben un ingreso; es decir, des-contar la tabla de datos: justamente lo que hace la función uncount():\n# expandir observaciones casen_ingresos_3 \u0026lt;- casen_ingresos_2 |\u0026gt; mutate(n = as.integer(n)) |\u0026gt; uncount(weights = n) Obtenemos una tabla de datos con 6 millones de filas, donde cada fila representa a una persona.\nGráfico de densidad apilada El gráfico que queremos hacer tiene en el eje horizontal los ingresos, y en el eje vertical las comunas, y cada una de estas filas del eje vertical mostrará la densidad de ingresos de cada comuna.\nEn su versión más básica sería algo así:\ncasen_ingresos_3 |\u0026gt; ggplot() + aes(x = variable, y = comuna) + geom_density_ridges() A este gráfico horripilante le aplicaremos un poco de magia de {ggplot2} para darle la apariencia que merece.\nEl punto clave es la función geom_density_ridges(), que produce las densidades apiladas o crestas. El argumento scale define la elevación de cada densidad por encima de la que tiene detrás, y bandwidth controla que tanto se suavizan los datos al calcular la curva de densidad, aumentando disminuyendo el detalle de las figuras. Al extremo derecho del gráfico agregué la mediana de ingresos de cada comuna con geom_text().\nnumber_options(decimal.mark = \u0026#34;,\u0026#34;, big.mark = \u0026#34;.\u0026#34;) # opciones de números grandes casen_ingresos_3 |\u0026gt; ggplot() + aes(x = variable, y = comuna) + # densidades geom_density_ridges(rel_min_height = 0, scale = 4, bandwidth = 30000, fill = \u0026#34;black\u0026#34;, color = \u0026#34;white\u0026#34;) + # textos de medianas a la derecha geom_text(data = ~distinct(.x, variable, comuna, mediana), aes(label = label_currency()(mediana |\u0026gt; signif(digits = 2)), x = 2500000), nudge_x = 120000, color = \u0026#34;white\u0026#34;, size = 2.5, check_overlap = T, hjust = 0, vjust = 0.3) + # expandir escala horizontal scale_x_continuous(expand = expansion(c(0, 0.09)), breaks = c(0, .5, 1, 1.5, 2, 2.5)*1000000, labels = label_currency()) + # no cortar geometrías fuera del plano de coordenadas coord_cartesian(clip = \u0026#34;off\u0026#34;) + # tema theme_void(base_family = \u0026#34;Helvetica\u0026#34;) + # texto eje horizontal theme(axis.text.x = element_text(size = 6, color = \u0026#34;white\u0026#34;, margin = margin(t = 4))) + # texto eje vertical theme(axis.text.y = element_text(size = 7, color = \u0026#34;white\u0026#34;, hjust = 1, vjust = 0.3, margin = margin(r = 7))) + # fondo theme(panel.background = element_rect(fill = \u0026#34;black\u0026#34;, linewidth = 0), plot.background = element_rect(fill = \u0026#34;black\u0026#34;, linewidth = 0), plot.margin = unit(c(5, 4, 5, 4), \u0026#34;mm\u0026#34;)) + theme(plot.title = element_text(size = 10, hjust = 0.4, colour = \u0026#34;white\u0026#34;, margin = margin(b = 3)), plot.subtitle = element_text(size = 8, hjust = 0.4, colour = \u0026#34;white\u0026#34;, margin = margin(b = 6))) + # textos labs(title = \u0026#34;Distribución de ingresos por comuna: Chile\u0026#34; |\u0026gt; toupper(), subtitle = \u0026#34;Ingreso de la ocupación principal, CASEN 2022\u0026#34;) Toca la imagen o este enlace para ver en tamaño completo Evidentemente, este gráfico es más estético que funcional. Sin embargo, se trata de un ejercicio de visualización basada en datos reales. El gráfico nos permite ver rápidamente la concentración de ingresos en la mayoría de las comunas en torno al sueldo mínimo, cada una con distintas desviaciones respecto a esta columna montañosa. Las densidades de más arriba corresponden a comunas de mayores ingresos, y por lo tanto sus distribuciones muestran una mayor cantidad de personas que perciben ingresos mayores a 500.000, incluso formándose una pequeña montañita sobre los 2 millones. A medida que bajamos la vista por el gráfico vemos como las demás comunas van acercando sus densidades hacia la mediana nacional.\n","date":"2025-07-12T00:00:00Z","excerpt":"Existe un gráfico estadístico muy famoso por haber aparecido en la portada del disco _Unknown Pleasures_ de Joy Division. Se trata de un gráfico de densidad, donde las densidades están apiladas verticalmente y se sobreponen, dando una apariencia montañosa, cordillerana y casi tridimensional. En este post reproduciremos este gráfico en R, usando datos socioeconómicos de la Encuesta de caracterización socioeconómica nacional (Casen) 2022.","href":"https://bastianoleah.netlify.app/blog/2025-07-11/","tags":"ggplot2 ; gráficos","title":"Gráfico de densidad tipo Joy Division en {ggplot2}"},{"content":" En este post veremos a agregar textos que se distancian entre sí automáticamente a tus gráficos. Esto sirve, por ejemplo, para mejorar visualizaciones de datos a las que queremos agregarle texto que identifique las observaciones, aún cuando las observaciones son demasiadas como para etiquetarlas a todas.\nUsaremos el paquete de R {ggrepel} para etiquetar puntos en un gráfico con textos que se repelen entre sí de forma automática.\nEste post requiere conocimientos de {ggplot2}. Si quieres aprender a hacer gráficos en R, revisa este tutorial sobre visualización de datos primero. Datos A modo de ejemplo, usaremos datos del plebiscito de entrada de 2020 en Chile, obtenidos desde el repositorio de datos sobre los plebiscitos constitucionales. Éste repositorio, parte de mi mini sitio de datos sociales chilenos, ofrece versiones limpias y listas para usar de los resultados electorales de estos procesos.\nlibrary(arrow) plebiscito \u0026lt;- read_parquet(\u0026#34;https://github.com/bastianolea/plebiscitos_chile/raw/main/datos/plebiscito_2020_comunas.parquet\u0026#34;) Como los datos están alojados en el repositorio, pueden cargarse directamente en tu sesión de R tan sólo cargándolos desde el enlace anterior, sin necesidad de descargarse en tu computador.\nLos datos vienen con cada fila representando una opción de voto por comuna del país, con su respectiva cantidad de votos, y se ven así:\nlibrary(dplyr) head(plebiscito) # A tibble: 6 × 6 cut_region region cut_comuna comuna opciones votos \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 1 Tarapacá 1101 Iquique Apruebo 61103 2 1 Tarapacá 1101 Iquique Rechazo 18879 3 1 Tarapacá 1101 Iquique Votos En Blanco 114 4 1 Tarapacá 1101 Iquique Votos Nulos 275 5 1 Tarapacá 1107 Alto Hospicio Apruebo 21589 6 1 Tarapacá 1107 Alto Hospicio Rechazo 4534 Procesamiento Vamos a hacer un gráfico de dispersión con {ggplot2}, para el cual necesitamos dos columnas con las dos opciones de voto: apruebo y rechazo. Como en el conjunto de datos que cargamos las opciones vienen en una columna (opciones), y el conteo viene en otra (votos), transformaremos la estructura de los datos a un formato ancho usando pivot_wider():\nlibrary(tidyr) plebiscito_ancho \u0026lt;- plebiscito |\u0026gt; pivot_wider(names_from = opciones, values_from = votos) |\u0026gt; janitor::clean_names() Lo que hizo pivot_wider() fue a usar la variable opciones para crear nuevas columnas, y la variable votos para rellenar las nuevas columnas con valores. De este modo, nuestros datos pasaron del formato largo al formato ancho, con una columna que contiene los votos del apruebo y otra del rechazo:\nhead(plebiscito_ancho) # A tibble: 6 × 8 cut_region region cut_comuna comuna apruebo rechazo votos_en_blanco \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 1 Tarapacá 1101 Iquique 61103 18879 114 2 1 Tarapacá 1107 Alto Hospicio 21589 4534 37 3 1 Tarapacá 1401 Pozo Almonte 3730 1076 11 4 1 Tarapacá 1402 Camiña 293 207 2 5 1 Tarapacá 1403 Colchane 131 374 3 6 1 Tarapacá 1404 Huara 1136 379 6 # ℹ 1 more variable: votos_nulos \u0026lt;dbl\u0026gt; Ahora usaremos {dplyr} para calcular qué opción ganó en cada comuna del país, y el porcentaje de votos que obtuvo:\nlibrary(dplyr) plebiscito_comunas \u0026lt;- plebiscito_ancho |\u0026gt; group_by(cut_comuna) |\u0026gt; # calcular resultado final mutate(resultado = case_when(apruebo \u0026gt; rechazo ~ \u0026#34;Apruebo\u0026#34;, rechazo \u0026gt;= apruebo ~ \u0026#34;Rechazo\u0026#34;), .before = apruebo) |\u0026gt; # calcular porcentaje de votos mutate(porcentaje = apruebo/(apruebo+rechazo)) |\u0026gt; ungroup() head(plebiscito_comunas) # A tibble: 6 × 10 cut_region region cut_comuna comuna resultado apruebo rechazo votos_en_blanco \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 1 Tarapa… 1101 Iquiq… Apruebo 61103 18879 114 2 1 Tarapa… 1107 Alto … Apruebo 21589 4534 37 3 1 Tarapa… 1401 Pozo … Apruebo 3730 1076 11 4 1 Tarapa… 1402 Camiña Apruebo 293 207 2 5 1 Tarapa… 1403 Colch… Rechazo 131 374 3 6 1 Tarapa… 1404 Huara Apruebo 1136 379 6 # ℹ 2 more variables: votos_nulos \u0026lt;dbl\u0026gt;, porcentaje \u0026lt;dbl\u0026gt; Gráfico Para realizar un gráfico de densidad, y especificamos las dos variables que pondremos en los ejes x e y, y una variable que representará el color de cada observación, las cuales se visualizarán como puntos usando geom_point():\nlibrary(ggplot2) plebiscito_comunas |\u0026gt; ggplot() + aes(rechazo, apruebo, color = resultado) + geom_point() Ahora mejoraremos un poco el gráfico anterior: definiremos el formato de los números de las escalas con scales::number_options(), ajustaremos el tamaño transparencia de los puntos de geom_point(), luego aplicaremos escalas numéricas más bonitas con scale_{x}_continuous(), y finalmente ajustaremos un poco el tema y textos del gráfico:\nlibrary(scales) # formato de números number_options(decimal.mark = \u0026#34;,\u0026#34;, big.mark = \u0026#34;.\u0026#34;) grafico_resultado \u0026lt;- plebiscito_comunas |\u0026gt; ggplot() + aes(rechazo, apruebo, color = resultado) + geom_point(size = 3, alpha = 0.5) + scale_x_continuous(labels = label_number()) + scale_y_continuous(labels = label_number()) + theme_minimal() + theme(legend.text = element_text(margin = margin(l = 2))) + labs(x = \u0026#34;Rechazo\u0026#34;, y = \u0026#34;Apruebo\u0026#34;, color = \u0026#34;Resultado\u0026#34;) grafico_resultado Ahora, al gráfico anterior, le agregaremos inocentemente etiquetas de texto a cada punto, a ver qué pasa:\ngrafico_resultado + geom_text(aes(label = comuna), size = 2, color = \u0026#34;grey30\u0026#34;, hjust = 0) Absolutamente nefasto. Son tantos puntos que los textos se transforman en una masa gris debido a la concentración.\nUn intento desesperado de corregir esto podría ser el argumento check_overlap, eliminará los textos que aparezcan encima de otros:\ngrafico_resultado + geom_text(aes(label = comuna), size = 2, color = \u0026#34;grey30\u0026#34;, hjust = 0, check_overlap = TRUE) Sin embargo, esta solución es demasiado básica, y aún hay textos que aparece encima de puntos, o fuera del margen del gráfico.\nEn estos casos resulta ideal geom_text_repel() como reemplazo de geom_text():\nlibrary(ggrepel) grafico_resultado + geom_text_repel(aes(label = comuna), size = 2.5, color = \u0026#34;grey30\u0026#34;, point.padding = 2 # margen de cada punto ) La función geom_text_repel() calcula la posesión de las etiquetas de texto con respecto a las demás, y decide el posicionamiento óptimo para que la mayor cantidad de textos aparezcan, sin que salgan encima de otros textos, ni encima de los puntos.\nEsta función tiene varios argumentos para ajustar el algoritmo que decide la ubicación de los textos. Uno de ellos es max.overlaps, cuyo defecto es 10, y determina la cantidad máxima de etiquetas que pueden estar unas encima de otras antes de qué sean descartadas por estar demasiado concentrados en un mismo lugar. Si aumentamos este argumento, la función tomará en cuenta etiquetas de texto en ubicaciones más complejas, e intentará graficarlas moviéndolas más lejos de los puntos, o moviendo otras etiquetas de texto a posiciones más lejanas del punto que lo origina. Para mejorar el resultado, podemos aumentar el valor del argumento max.time, el cual le da más tiempo al algoritmo para buscar resolver el posicionamiento de los textos.\ngrafico_resultado + geom_text_repel(aes(label = comuna), size = 2.5, color = \u0026#34;grey30\u0026#34;, segment.size = 0.1, # ancho de líneas (si es que salen) max.overlaps = 30, max.time = 1) En el gráfico anterior, aparecen nuevas etiquetas que no salieron antes, algunas de ellas conectadas por líneas con el punto correspondiente.\nTambién podemos determinar ciertos parámetros para que aparezcan o no aparezcan ciertos textos en el gráfico. En el siguiente ejemplo, agregamos una condicional para que solamente aparezcan textos que superen ciertos valores de cada eje, excluyendo los demás.\ngrafico_resultado + geom_text_repel(aes(label = ifelse(rechazo \u0026gt; 30000 | apruebo \u0026gt; 100000, comuna, \u0026#34; \u0026#34;)), size = 2.5, color = \u0026#34;grey30\u0026#34;, segment.size = 0, max.overlaps = 30) Es importante dejar las observaciones que queremos excluir de el etiquetado de texto con un texto vacío o un espacio (\u0026quot; \u0026quot;), y no solamente filtrar las observaciones del dataset, porque así los puntos del gráfico seguirán teniendo un texto vacío encima de ellos, lo que hará que el resto de las etiquetas de texto también se alejen de los puntos sin etiqueta.\nHaremos un acercamiento al gráfico, definiendo los límites verticales y horizontales con coord_cartesian(), para que hayan puntos más dispersos:\ngrafico_resultado + coord_cartesian(xlim = c(0, 10000), ylim = c(0, 50000), clip = \u0026#34;off\u0026#34;) + geom_text_repel( aes(label = ifelse(rechazo \u0026gt; 3000 \u0026amp; rechazo \u0026lt; 10000 | apruebo \u0026gt; 20000 \u0026amp; apruebo \u0026lt; 50000, comuna, \u0026#34; \u0026#34;)), size = 2.5, color = \u0026#34;grey30\u0026#34;, segment.size = 0, max.overlaps = 30, max.time = 5) En este caso aplicamos condicionales más estrictas para que aparezcan o desaparezcan los puntos, con el objetivo de que aparezca la mayor cantidad de textos de un sector específico del gráfico.\nTambién podemos utilizar criterios estadísticos para incluir o excluir el etiquetado de textos. Por ejemplo, en el siguiente gráfico solamente mostramos el nombre de los puntos cuyo valor es superior al percentil 73 de cada variable.\ngrafico_resultado + geom_text_repel(aes(label = case_when(rechazo \u0026gt; quantile(rechazo, 0.73) ~ comuna, apruebo \u0026gt; quantile(apruebo, 0.73) ~ comuna)), size = 2.5, color = \u0026#34;grey30\u0026#34;, segment.size = 0, max.overlaps = 30, max.time = 5) + scale_x_continuous(labels = label_number(), limits = c(0, 10000)) + scale_y_continuous(labels = label_number(), limits = c(0, 30000)) + labs(x = \u0026#34;Rechazo (detalle)\u0026#34;, y = \u0026#34;Apruebo (detalle)\u0026#34;) Gráfico interactivo Finalmente, una mejor solución para estos casos sería permitir a las y los usuarios explorar la información de todas las observaciones del gráfico mediante la interactividad. El paquete {ggiraph} para visualizaciones interactivas nos puede ayudar a transformar cualquier gráfico de {ggplot2} en un gráfico interactivo, donde los usuarios pueden tocar o poner el Mouse encima de un elemento para obtener más información. Sólo es necesario agregarle _interactive a las funciones geom_ del gráfico, y especificar el texto que aparecerá cuándo se ponga el cursor encima de cada elemento en tooltip dentro de aes():\nlibrary(ggiraph) library(glue) grafico_interactivo \u0026lt;- plebiscito_comunas |\u0026gt; ggplot() + aes(rechazo, apruebo, color = resultado) + geom_point_interactive(size = 3, alpha = 0.5, aes(tooltip = glue(\u0026#34;\u0026lt;b\u0026gt;{comuna}\u0026lt;/b\u0026gt;: gana {resultado} con un {percent(porcentaje, 0.1)}\u0026#34;)) ) + geom_text_repel(aes(label = comuna), size = 2.5, color = \u0026#34;grey30\u0026#34;, segment.size = 0.1, point.padding = 2) + scale_x_continuous(labels = label_number()) + scale_y_continuous(labels = label_number()) + theme_minimal() + theme(legend.text = element_text(margin = margin(l = 2))) + labs(x = \u0026#34;Rechazo\u0026#34;, y = \u0026#34;Apruebo\u0026#34;, color = \u0026#34;Resultado\u0026#34;) Para que el gráfico se vea y funcione interactivamente, debemos mostrarlo mediante la función girafe():\ngirafe(ggobj = grafico_interactivo, # dimensiones del gráfico width_svg = 7, height_svg = 5, # opciones para los tooltips options = list( # ocultar barra de opciones opts_toolbar(hidden = \u0026#34;selection\u0026#34;, saveaspng = FALSE), # color al poner el cursor encima opts_hover(css = \u0026#34;fill: white; stroke: black;\u0026#34;), # personalizar la apariencia del tooltip opts_tooltip(opacity = 0.7, use_fill = TRUE, css = \u0026#34;font-family: sans-serif; font-size: 70%; color: white; padding: 4px; border-radius: 5px;\u0026#34;)) ) Vemos que en el gráfico anterior, podemos obtener información extra de cada observación tan sólo poniendo el cursor encima, lo cual amplía bastante las posibilidades de comunicación, por ejemplo, permitiendo saber información de los puntos de los sectores más densos, o agregando la información de otras variables del conjunto de datos, como el porcentaje.\nPróximamente: ejemplos para aplicar {ggrepel} en mapas y otras visualizaciones geoespaciales.\n","date":"2025-07-11T00:00:00Z","excerpt":"En este post veremos cómo mejorar visualizaciones de datos a las que queremos agregarle texto que identifique las observaciones, aún cuando las observaciones son demasiadas como para etiquetarlas a todas. Usaremos el [paquete de R `{ggrepel}`](https://ggrepel.slowkow.com) para etiquetar puntos en un gráfico con textos que se repelen entre sí de forma automática.","href":"https://bastianoleah.netlify.app/blog/ggrepel/","tags":"visualización de datos ; ggplot2 ; gráficos ; texto","title":"Etiquetas de texto que se repelen entre sí con {ggrepel}"},{"content":"El análisis de correlación es una técnica estadística que nos permite identificar si existen relaciones lineales entre distintas variables.\nLo que hace una correlación es indicarnos si dos variables tienen una relación entre sí, en el sentido de que el aumento o disminución de una de las variables ocurra en concordancia con la otra variable, ya sea una correlación positiva (si una variable aumenta, la otra también) o correlación negativa (si una variable aumenta, la otra disminuye).\nUn ejemplo de correlación positiva sería: mientras más solcito, más calor. Una correlación negativa sería: a mayor frío, menos ganas de levantarse 😴\nEn R podemos realizar análisis de correlación en conjuntos de datos enteros, y de este modo podemos encontrar todas las correlaciones que existen entre las variables de los datos. Esto se lograría tomando todas las variables y cruzándolas todas con todas, para luego identificar cuáles se correlacionan, en qué dirección y con qué intensidad.\nCargar datos Para hacer más interesante el tutorial, vamos a cargar dos conjuntos de datos sociales, obtenidos de mi repositorio de datos sociales públicos.\nEn esta oportunidad cargaremos un conjunto de datos del Sistema de Información Municipal (Sinim), que es una base de datos sobre los municipios chilenos mantenida anualmente por la Subsecretaría de Desarrollo Regional y Administrativo (Subdere), y el conjunto de datos del Sistema de Indicadores y Estándares de Desarrollo Urbano, conjunto desarrollado por el Instituto Nacional de Estadísticas de Chile que agrupa estadísticas Sobre medio ambiente, planificación de ciudades, desarrollo sostenible, acceso a servicios básicos, y movilidad.\nGracias al repositorio de datos sociales, tenemos enlaces directos a conjuntos de datos sociales listos para usar, y cargarlos a tu sesión de R es tan fácil como cargar el dato directamente desde internet:\nlibrary(arrow) # cargar datos sinim sinim \u0026lt;- arrow::read_parquet(\u0026#34;https://github.com/bastianolea/sinim_datos_comunales/raw/main/datos/sinim_2019-2023.parquet\u0026#34;) # cargar datos de siedu siedu \u0026lt;- arrow::read_parquet(\u0026#34;https://github.com/bastianolea/siedu_indicadores_urbanos/raw/main/datos/siedu_indicadores_desarrollo_urbano.parquet\u0026#34;) Limpieza de datos Antes que nada, vamos a cargar {dplyr} para el manejo y la limpieza de los datos.\nlibrary(dplyr) Echémosle un vistazo a los datos con glimpse():\nglimpse(sinim) Rows: 842,510 Columns: 11 $ municipio \u0026lt;chr\u0026gt; \u0026quot;IQUIQUE\u0026quot;, \u0026quot;ALTO HOSPICIO\u0026quot;, \u0026quot;POZO ALMONTE\u0026quot;, \u0026quot;CAMIÑA\u0026quot;, \u0026quot;C… $ cut_comuna \u0026lt;chr\u0026gt; \u0026quot;01101\u0026quot;, \u0026quot;01107\u0026quot;, \u0026quot;01401\u0026quot;, \u0026quot;01402\u0026quot;, \u0026quot;01403\u0026quot;, \u0026quot;01404\u0026quot;, \u0026quot;0… $ año_id \u0026lt;int\u0026gt; 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, … $ año \u0026lt;int\u0026gt; 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 20… $ variable_id \u0026lt;chr\u0026gt; \u0026quot;879\u0026quot;, \u0026quot;879\u0026quot;, \u0026quot;879\u0026quot;, \u0026quot;879\u0026quot;, \u0026quot;879\u0026quot;, \u0026quot;879\u0026quot;, \u0026quot;879\u0026quot;, \u0026quot;879\u0026quot;, … $ variable \u0026lt;chr\u0026gt; \u0026quot;Ingresos Propios Permanentes (IPP)\u0026quot;, \u0026quot;Ingresos Propios … $ variable_desc \u0026lt;chr\u0026gt; \u0026quot;Indicador Bep Ingresos (variable sistema antiguo)\u0026quot;, \u0026quot;In… $ area \u0026lt;chr\u0026gt; \u0026quot;01. ADMINISTRACION Y FINANZAS MUNICIPALES\u0026quot;, \u0026quot;01. ADMI… $ subarea \u0026lt;chr\u0026gt; \u0026quot;A. INGRESOS MUNICIPALES (M$)\u0026quot;, \u0026quot;A. INGRESOS MUNICIPALES… $ unidad \u0026lt;chr\u0026gt; \u0026quot;M$\u0026quot;, \u0026quot;M$\u0026quot;, \u0026quot;M$\u0026quot;, \u0026quot;M$\u0026quot;, \u0026quot;M$\u0026quot;, \u0026quot;M$\u0026quot;, \u0026quot;M$\u0026quot;, \u0026quot;M$\u0026quot;, \u0026quot;M$\u0026quot;, \u0026quot;M… $ valor \u0026lt;dbl\u0026gt; 34745945, 4107522, 3776040, 108439, 160661, 1067476, 333… glimpse(siedu) Rows: 6,701 Columns: 12 $ codigo_comuna \u0026lt;dbl\u0026gt; 1107, 1107, 1107, 1107, 1107, 1107, 1107, 1107, 1107,… $ codigo_region \u0026lt;chr\u0026gt; \u0026quot;01\u0026quot;, \u0026quot;01\u0026quot;, \u0026quot;01\u0026quot;, \u0026quot;01\u0026quot;, \u0026quot;01\u0026quot;, \u0026quot;01\u0026quot;, \u0026quot;01\u0026quot;, \u0026quot;01\u0026quot;, \u0026quot;01\u0026quot;,… $ codigo_provincia \u0026lt;chr\u0026gt; \u0026quot;011\u0026quot;, \u0026quot;011\u0026quot;, \u0026quot;011\u0026quot;, \u0026quot;011\u0026quot;, \u0026quot;011\u0026quot;, \u0026quot;011\u0026quot;, \u0026quot;011\u0026quot;, \u0026quot;011… $ nombre_region \u0026lt;chr\u0026gt; \u0026quot;Tarapacá\u0026quot;, \u0026quot;Tarapacá\u0026quot;, \u0026quot;Tarapacá\u0026quot;, \u0026quot;Tarapacá\u0026quot;, \u0026quot;Tara… $ nombre_provincia \u0026lt;chr\u0026gt; \u0026quot;Iquique\u0026quot;, \u0026quot;Iquique\u0026quot;, \u0026quot;Iquique\u0026quot;, \u0026quot;Iquique\u0026quot;, \u0026quot;Iquique\u0026quot;… $ nombre_comuna \u0026lt;chr\u0026gt; \u0026quot;Alto Hospicio\u0026quot;, \u0026quot;Alto Hospicio\u0026quot;, \u0026quot;Alto Hospicio\u0026quot;, \u0026quot;A… $ id \u0026lt;chr\u0026gt; \u0026quot;BPU_8\u0026quot;, \u0026quot;BPU_17\u0026quot;, \u0026quot;EA_34\u0026quot;, \u0026quot;EA_22a\u0026quot;, \u0026quot;EA_22\u0026quot;, \u0026quot;EA_33… $ año \u0026lt;dbl\u0026gt; 2018, 2019, 2019, 2021, 2021, 2022, 2018, 2018, 2018,… $ variable \u0026lt;chr\u0026gt; \u0026quot;Cantidad de jornadas diarias completas de trabajo de… $ valor \u0026lt;dbl\u0026gt; 32.750000, 0.580000, 1.220618, 168.223082, 484.296058… $ medida \u0026lt;chr\u0026gt; \u0026quot;Jornadas diarias / 10.000 habs\u0026quot;, \u0026quot;Relación (Número d… $ estandar \u0026lt;chr\u0026gt; \u0026quot;Sin estándar\u0026quot;, \u0026quot;Sin estándar\u0026quot;, \u0026quot;Hasta 1 kilogramo / … Notamos que ambos conjuntos de datos vienen en el formato largo, dónde tenemos una columna con los nombres de las variables o indicadores, y otra columna con los valores correspondientes. Así tenemos una tabla con menor cantidad de columnas, donde cada fila es una observación que corresponde a una comuna del país, en un año específico, para una de las variables del conjunto de datos, con su valor correspondiente.\nHaremos tres cosas con los datos:\nPrimero haremos una selección de variables interesantes de cada conjunto de datos. Luego, como ambos conjuntos de datos poseen mediciones de distintos años en cada una de sus indicadores o estadísticos, realizaremos una agrupación por comuna y variable para dejar las mediciones más recientes en cada indicador y en cada comuna. Finalmente, dejaremos sólo las columnas que nos interesan # filtrar variables sinim_2 \u0026lt;- sinim |\u0026gt; filter(variable %in% c(\u0026#34;Ingresos Propios Permanentes per Cápita (IPPP)\u0026#34;, \u0026#34;Disponibilidad Presupuestaria Municipal por Habitante (M$)\u0026#34;, \u0026#34;Inversión Municipal\u0026#34;, \u0026#34;Participación del Fondo Común Municipal en el Ingreso Total\u0026#34;, \u0026#34;Participación de Ingresos por Transferencias en el Ingreso Total\u0026#34;, \u0026#34;Porcentaje de Ejecución Presupuestaria Devengada Municipal\u0026#34;, \u0026#34;Porcentaje de Puntajes PSU Igual o Superior a 450 Puntos en Establecimientos Municipales de Educación\u0026#34;, \u0026#34;Metros Cuadrados (M2) de Areas Verdes con Mantenimiento por Habitante\u0026#34;, \u0026#34;Porcentaje de mujeres funcionarias municipales\u0026#34;, \u0026#34;Densidad de Población por Km2\u0026#34;, \u0026#34;Población Comunal, Estimada por el INE\u0026#34;, \u0026#34;Porcentaje de Hogares de 0-40% de Ingresos respecto del Total Regional (RSH)\u0026#34;)) # dejar sólo la medición más reciente de cada variable en cada comuna sinim_3 \u0026lt;- sinim_2 |\u0026gt; group_by(cut_comuna, variable) |\u0026gt; slice_max(año) |\u0026gt; ungroup() |\u0026gt; mutate(cut_comuna = as.numeric(cut_comuna)) # seleccionar columnas sinim_4 \u0026lt;- sinim_3 |\u0026gt; select(cut_comuna, variable, valor) # filtrar variables siedu_2 \u0026lt;- siedu |\u0026gt; filter(variable %in% c( \u0026#34;Consumo de energía eléctrica per cápita residencial\u0026#34;, \u0026#34;Tiempo de viaje en hora punta mañana\u0026#34;, \u0026#34;Superficie de áreas verdes públicas por habitante\u0026#34;, \u0026#34;Número de microbasurales por cada 10.000 habitantes\u0026#34;, \u0026#34;Población estimada de migrantes internacionales por comuna\u0026#34;, \u0026#34;Porcentaje de la población en situación de pobreza multidimensional\u0026#34;, \u0026#34;Porcentaje de viviendas en situación de hacinamiento\u0026#34;, \u0026#34;Tasa de víctimas de delitos violentos por casos policiales cada 10.000 habitantes\u0026#34;, \u0026#34;Tasa de conexiones residenciales fijas de internet por cada 100 habitantes\u0026#34;)) # dejar sólo la medición más reciente de cada variable en cada comuna siedu_3 \u0026lt;- siedu_2 |\u0026gt; group_by(codigo_comuna, variable) |\u0026gt; slice_max(año) |\u0026gt; ungroup() # seleccionar columnas siedu_4 \u0026lt;- siedu_3 |\u0026gt; select(cut_comuna = codigo_comuna, variable, valor) Veamos cómo van quedando los datos:\nsinim_4 # A tibble: 3,806 × 3 cut_comuna variable valor \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 1101 Densidad de Población por Km2 1.02e2 2 1101 Disponibilidad Presupuestaria Municipal por Habitante (M$) 4.27e2 3 1101 Ingresos Propios Permanentes per Cápita (IPPP) 1.92e2 4 1101 Inversión Municipal 2.57e6 5 1101 Participación de Ingresos por Transferencias en el Ingreso… 4.38e1 6 1101 Participación del Fondo Común Municipal en el Ingreso Total 6.4 e0 7 1101 Población Comunal, Estimada por el INE 2.31e5 8 1101 Porcentaje de Ejecución Presupuestaria Devengada Municipal 7.58e1 9 1101 Porcentaje de Hogares de 0-40% de Ingresos respecto del To… 4.44e1 10 1101 Porcentaje de Puntajes PSU Igual o Superior a 450 Puntos e… 7.61e1 # ℹ 3,796 more rows siedu_4 # A tibble: 891 × 3 cut_comuna variable valor \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 1101 Consumo de energía eléctrica per cápita residencial 7.09e2 2 1101 Número de microbasurales por cada 10.000 habitantes 1.21e1 3 1101 Población estimada de migrantes internacionales por comuna 4.46e4 4 1101 Porcentaje de la población en situación de pobreza multidi… 1.97e1 5 1101 Porcentaje de viviendas en situación de hacinamiento 9.92e0 6 1101 Superficie de áreas verdes públicas por habitante 2.13e0 7 1101 Tasa de conexiones residenciales fijas de internet por cad… 2.14e1 8 1101 Tasa de víctimas de delitos violentos por casos policiales… 1.46e2 9 1101 Tiempo de viaje en hora punta mañana 3 e1 10 1107 Consumo de energía eléctrica per cápita residencial 4.84e2 # ℹ 881 more rows Unir datos Vamos a combinar estos dos conjuntos de datos para tener una mezcla de variables de temas socioeconómicos que sería interesante correlacionar. Como hicimos que ambos conjuntos de datos estén ordenados bajo la misma lógica, para unirlos sólo necesitamos agregar las filas de un conjunto al otro.\ndatos \u0026lt;- bind_rows(sinim_4, siedu_4) Ahora que los datos están unidos, contamos con 20 variables para correlacionar.\ndatos |\u0026gt; distinct(variable) |\u0026gt; print(n=Inf) # A tibble: 20 × 1 variable \u0026lt;chr\u0026gt; 1 Densidad de Población por Km2 2 Disponibilidad Presupuestaria Municipal por Habitante (M$) 3 Ingresos Propios Permanentes per Cápita (IPPP) 4 Inversión Municipal 5 Participación de Ingresos por Transferencias en el Ingreso Total 6 Participación del Fondo Común Municipal en el Ingreso Total 7 Población Comunal, Estimada por el INE 8 Porcentaje de Ejecución Presupuestaria Devengada Municipal 9 Porcentaje de Hogares de 0-40% de Ingresos respecto del Total Regional (RSH) 10 Porcentaje de Puntajes PSU Igual o Superior a 450 Puntos en Establecimientos… 11 Porcentaje de mujeres funcionarias municipales 12 Consumo de energía eléctrica per cápita residencial 13 Número de microbasurales por cada 10.000 habitantes 14 Población estimada de migrantes internacionales por comuna 15 Porcentaje de la población en situación de pobreza multidimensional 16 Porcentaje de viviendas en situación de hacinamiento 17 Superficie de áreas verdes públicas por habitante 18 Tasa de conexiones residenciales fijas de internet por cada 100 habitantes 19 Tasa de víctimas de delitos violentos por casos policiales cada 10.000 habit… 20 Tiempo de viaje en hora punta mañana Pivotar datos a ancho El último paso antes del análisis de correlación es pivotar la estructura de los datos al formato ancho., porque las funciones que realizan correlaciones en R esperan que los datos vengan de esta forma.\nSi bien en el formato largo tenemos una columna con el nombre de las variables y otra columna con el valor de cada variable, siendo cada fila una observación, en el formato ancho cada columna corresponde a una variable, mientras que cada fila corresponde a una observación.\nlibrary(tidyr) datos_ancho \u0026lt;- datos |\u0026gt; # pivotar a ancho pivot_wider(id_cols = cut_comuna, # columna que identifica las observaciones values_from = valor, # columna con los valores names_from = variable # columna con los nombres de columna ) Se consultamos los nombres de las columnas, confirmamos que ahora cada variable se encuentra una columna individual:\nnames(datos_ancho) [1] \u0026quot;cut_comuna\u0026quot; [2] \u0026quot;Densidad de Población por Km2\u0026quot; [3] \u0026quot;Disponibilidad Presupuestaria Municipal por Habitante (M$)\u0026quot; [4] \u0026quot;Ingresos Propios Permanentes per Cápita (IPPP)\u0026quot; [5] \u0026quot;Inversión Municipal\u0026quot; [6] \u0026quot;Participación de Ingresos por Transferencias en el Ingreso Total\u0026quot; [7] \u0026quot;Participación del Fondo Común Municipal en el Ingreso Total\u0026quot; [8] \u0026quot;Población Comunal, Estimada por el INE\u0026quot; [9] \u0026quot;Porcentaje de Ejecución Presupuestaria Devengada Municipal\u0026quot; [10] \u0026quot;Porcentaje de Hogares de 0-40% de Ingresos respecto del Total Regional (RSH)\u0026quot; [11] \u0026quot;Porcentaje de Puntajes PSU Igual o Superior a 450 Puntos en Establecimientos Municipales de Educación\u0026quot; [12] \u0026quot;Porcentaje de mujeres funcionarias municipales\u0026quot; [13] \u0026quot;Consumo de energía eléctrica per cápita residencial\u0026quot; [14] \u0026quot;Número de microbasurales por cada 10.000 habitantes\u0026quot; [15] \u0026quot;Población estimada de migrantes internacionales por comuna\u0026quot; [16] \u0026quot;Porcentaje de la población en situación de pobreza multidimensional\u0026quot; [17] \u0026quot;Porcentaje de viviendas en situación de hacinamiento\u0026quot; [18] \u0026quot;Superficie de áreas verdes públicas por habitante\u0026quot; [19] \u0026quot;Tasa de conexiones residenciales fijas de internet por cada 100 habitantes\u0026quot; [20] \u0026quot;Tasa de víctimas de delitos violentos por casos policiales cada 10.000 habitantes\u0026quot; [21] \u0026quot;Tiempo de viaje en hora punta mañana\u0026quot; Correlación El paquete {corrr}, parte del framework tidymodels, nos facilita realizar una correlación cuyo resultado viene en una tabla ordenada con tan sólo una función: correlate()\nlibrary(corrr) correlación \u0026lt;- datos_ancho |\u0026gt; select(-cut_comuna) |\u0026gt; correlate() correlación # A tibble: 20 × 21 term Densidad de Població…¹ Disponibilidad Presu…² Ingresos Propios Per…³ `Inversión Municipal` Participación de Ing…⁴ Participación del Fo…⁵ Población Comunal, E…⁶ Porcentaje de Ejecuc…⁷ Porcentaje de Hogare…⁸ Porcentaje de Puntaj…⁹ Porcentaje de mujere…˟ Consumo de energía e…˟ Número de microbasur…˟ Población estimada d…˟ Porcentaje de la pob…˟ Porcentaje de vivien…˟ Superficie de áreas …˟ Tasa de conexiones r…˟ Tasa de víctimas de …˟ Tiempo de viaje en h…˟ \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Densidad de Población por Km2 NA -0.0969 -0.0445 0.222 0.0415 -0.255 0.480 0.192 -0.311 0.0238 -0.0105 0.0691 -0.119 0.516 -0.0295 0.379 -0.112 0.248 0.657 0.213 2 Disponibilidad Presupuestaria Municip… -0.0969 NA 0.782 -0.0794 0.110 0.0764 -0.190 -0.217 -0.327 -0.0743 -0.0905 0.681 -0.0329 -0.0473 -0.247 -0.371 0.404 0.148 -0.0675 -0.232 3 Ingresos Propios Permanentes per Cápi… -0.0445 0.782 NA 0.0366 0.0233 -0.224 -0.0802 -0.117 -0.359 -0.106 -0.0711 0.762 -0.0139 0.0200 -0.308 -0.391 0.512 0.214 -0.0761 -0.286 4 Inversión Municipal 0.222 -0.0794 0.0366 NA 0.00615 -0.290 0.555 0.0647 -0.317 0.153 -0.0946 0.222 -0.113 0.142 -0.409 -0.297 0.162 0.375 0.0877 -0.0957 5 Participación de Ingresos por Transfe… 0.0415 0.110 0.0233 0.00615 NA -0.331 0.0326 -0.0573 -0.0969 0.0340 0.00478 -0.134 -0.0293 -0.150 0.00375 0.0973 -0.188 0.0971 0.238 0.0176 6 Participación del Fondo Común Municip… -0.255 0.0764 -0.224 -0.290 -0.331 NA -0.400 -0.263 0.511 -0.1000 -0.0315 -0.543 -0.0714 -0.220 0.406 0.273 -0.336 -0.370 -0.222 0.238 7 Población Comunal, Estimada por el INE 0.480 -0.190 -0.0802 0.555 0.0326 -0.400 NA 0.213 -0.416 0.110 -0.0592 -0.0770 -0.0972 0.404 -0.298 -0.0503 -0.0699 0.421 0.313 -0.0764 8 Porcentaje de Ejecución Presupuestari… 0.192 -0.217 -0.117 0.0647 -0.0573 -0.263 0.213 NA -0.0976 0.0992 0.00160 0.000366 0.0701 -0.0124 -0.0596 0.0518 0.00593 0.0264 0.149 0.182 9 Porcentaje de Hogares de 0-40% de Ing… -0.311 -0.327 -0.359 -0.317 -0.0969 0.511 -0.416 -0.0976 NA -0.143 -0.0263 -0.487 0.121 -0.279 0.733 0.462 -0.260 -0.664 -0.195 0.257 10 Porcentaje de Puntajes PSU Igual o Su… 0.0238 -0.0743 -0.106 0.153 0.0340 -0.1000 0.110 0.0992 -0.143 NA -0.0230 0.277 -0.0532 0.230 -0.572 -0.316 0.0845 0.412 0.0600 -0.235 11 Porcentaje de mujeres funcionarias mu… -0.0105 -0.0905 -0.0711 -0.0946 0.00478 -0.0315 -0.0592 0.00160 -0.0263 -0.0230 NA -0.0355 0.0486 -0.130 0.206 0.0925 -0.142 -0.0206 -0.0459 0.210 12 Consumo de energía eléctrica per cápi… 0.0691 0.681 0.762 0.222 -0.134 -0.543 -0.0770 0.000366 -0.487 0.277 -0.0355 NA 0.0685 0.0691 -0.346 -0.482 0.450 0.342 -0.0319 -0.155 13 Número de microbasurales por cada 10.… -0.119 -0.0329 -0.0139 -0.113 -0.0293 -0.0714 -0.0972 0.0701 0.121 -0.0532 0.0486 0.0685 NA -0.0890 0.0742 0.0720 -0.120 -0.251 -0.0503 -0.191 14 Población estimada de migrantes inter… 0.516 -0.0473 0.0200 0.142 -0.150 -0.220 0.404 -0.0124 -0.279 0.230 -0.130 0.0691 -0.0890 NA -0.230 0.246 -0.145 0.0871 0.523 -0.119 15 Porcentaje de la población en situaci… -0.0295 -0.247 -0.308 -0.409 0.00375 0.406 -0.298 -0.0596 0.733 -0.572 0.206 -0.346 0.0742 -0.230 NA 0.628 -0.169 -0.681 0.0239 0.301 16 Porcentaje de viviendas en situación … 0.379 -0.371 -0.391 -0.297 0.0973 0.273 -0.0503 0.0518 0.462 -0.316 0.0925 -0.482 0.0720 0.246 0.628 NA -0.235 -0.505 0.454 0.266 17 Superficie de áreas verdes públicas p… -0.112 0.404 0.512 0.162 -0.188 -0.336 -0.0699 0.00593 -0.260 0.0845 -0.142 0.450 -0.120 -0.145 -0.169 -0.235 NA 0.135 -0.0458 -0.241 18 Tasa de conexiones residenciales fija… 0.248 0.148 0.214 0.375 0.0971 -0.370 0.421 0.0264 -0.664 0.412 -0.0206 0.342 -0.251 0.0871 -0.681 -0.505 0.135 NA 0.172 -0.338 19 Tasa de víctimas de delitos violentos… 0.657 -0.0675 -0.0761 0.0877 0.238 -0.222 0.313 0.149 -0.195 0.0600 -0.0459 -0.0319 -0.0503 0.523 0.0239 0.454 -0.0458 0.172 NA -0.0366 20 Tiempo de viaje en hora punta mañana 0.213 -0.232 -0.286 -0.0957 0.0176 0.238 -0.0764 0.182 0.257 -0.235 0.210 -0.155 -0.191 -0.119 0.301 0.266 -0.241 -0.338 -0.0366 NA # ℹ abbreviated names: ¹​`Densidad de Población por Km2`, ²​`Disponibilidad Presupuestaria Municipal por Habitante (M$)`, ³​`Ingresos Propios Permanentes per Cápita (IPPP)`, ⁴​`Participación de Ingresos por Transferencias en el Ingreso Total`, ⁵​`Participación del Fondo Común Municipal en el Ingreso Total`, ⁶​`Población Comunal, Estimada por el INE`, ⁷​`Porcentaje de Ejecución Presupuestaria Devengada Municipal`, ⁸​`Porcentaje de Hogares de 0-40% de Ingresos respecto del Total Regional (RSH)`, # ⁹​`Porcentaje de Puntajes PSU Igual o Superior a 450 Puntos en Establecimientos Municipales de Educación`, ˟​`Porcentaje de mujeres funcionarias municipales`, ˟​`Consumo de energía eléctrica per cápita residencial`, ˟​`Número de microbasurales por cada 10.000 habitantes`, ˟​`Población estimada de migrantes internacionales por comuna`, ˟​`Porcentaje de la población en situación de pobreza multidimensional`, ˟​`Porcentaje de viviendas en situación de hacinamiento`, # ˟​`Superficie de áreas verdes públicas por habitante`, ˟​`Tasa de conexiones residenciales fijas de internet por cada 100 habitantes`, ˟​`Tasa de víctimas de delitos violentos por casos policiales cada 10.000 habitantes`, ˟​`Tiempo de viaje en hora punta mañana` En la tabla anterior (muy rudimentaria aún) podemos ver el cruce entre todas las variables. La tabla se lee partiendo por una fila, que representa una de las variables, y cada vez que esta fila se intercepta con una columna, el valor representa el cruce de la variable de la fila con la variable de la columna.\nComo estamos cruzando todas con todas las variables, obviamente cada variable también se cruza consigo misma, lo cual resulta en un NA.\nComo el resultado es muy grande, y la cantidad de columnas muy alta, {corrr} ofrece la función stretch() para convertir fácilmente el resultado a un formato largo:\ncorrelación |\u0026gt; stretch() # A tibble: 400 × 3 x y r \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Densidad de Población por Km2 Densidad de Población por Km2 NA 2 Densidad de Población por Km2 Disponibilidad Presupuestaria Municipal por… -0.0969 3 Densidad de Población por Km2 Ingresos Propios Permanentes per Cápita (IP… -0.0445 4 Densidad de Población por Km2 Inversión Municipal 0.222 5 Densidad de Población por Km2 Participación de Ingresos por Transferencia… 0.0415 6 Densidad de Población por Km2 Participación del Fondo Común Municipal en … -0.255 7 Densidad de Población por Km2 Población Comunal, Estimada por el INE 0.480 8 Densidad de Población por Km2 Porcentaje de Ejecución Presupuestaria Deve… 0.192 9 Densidad de Población por Km2 Porcentaje de Hogares de 0-40% de Ingresos … -0.311 10 Densidad de Población por Km2 Porcentaje de Puntajes PSU Igual o Superior… 0.0238 # ℹ 390 more rows Esto nos puede servir para encontrar las correlaciones con una de las variables en particular; por ejemplo, encontrar la correlación de las variables con el tiempo de viaje en hora punta por las mañanas:\ncorrelación |\u0026gt; stretch() |\u0026gt; filter(x == \u0026#34;Tiempo de viaje en hora punta mañana\u0026#34;) |\u0026gt; select(y, r) |\u0026gt; head() # A tibble: 6 × 2 y r \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Densidad de Población por Km2 0.213 2 Disponibilidad Presupuestaria Municipal por Habitante (M$) -0.232 3 Ingresos Propios Permanentes per Cápita (IPPP) -0.286 4 Inversión Municipal -0.0957 5 Participación de Ingresos por Transferencias en el Ingreso Total 0.0176 6 Participación del Fondo Común Municipal en el Ingreso Total 0.238 Interpretación de correlaciones La columna r nos indica el valor de la correlación de la variable filtrada con todo el resto de las variables.\nComo las correlaciones pueden ser positivas o negativas, el valor de la correlación (r) puede ser positivo o negativo. Los valores de correlación van del 0 al 1 (o del 0 al -1), donde una correlación igual a 0 significa que no existe correlación, y una correlación igual a 1 significa que la correlación es total. Usualmente, una correlación mayor a 0,3 se considera moderada, y mayor a 0,5 se considera fuerte, pero las interpretaciones De estos valores son múltiples.\nOrdenemos las variables por su intensidad de correlación con el tiempo de viaje:\ncorrelación |\u0026gt; stretch() |\u0026gt; filter(x == \u0026#34;Tiempo de viaje en hora punta mañana\u0026#34;) |\u0026gt; select(y, r) |\u0026gt; arrange(desc(abs(r))) |\u0026gt; head() # A tibble: 6 × 2 y r \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Tasa de conexiones residenciales fijas de internet por cada 100 habitantes -0.338 2 Porcentaje de la población en situación de pobreza multidimensional 0.301 3 Ingresos Propios Permanentes per Cápita (IPPP) -0.286 4 Porcentaje de viviendas en situación de hacinamiento 0.266 5 Porcentaje de Hogares de 0-40% de Ingresos respecto del Total Regional (RSH) 0.257 6 Superficie de áreas verdes públicas por habitante -0.241 En las primeras 3 filas podemos ver correlaciones negativas fuertes: las comunas del país donde los tiempos de viaje de viaje en hora punta por la mañana son mayores, también son comunas donde los puntajes en la prueba de selección universitaria (PSU) son menores, y menores son los recursos municipales por habitante. Dicho de otro modo, a menores recursos municipales por habitantes, mayor tiempo de viaje en hora punta por la mañana.\nEn las filas 4 y 5 vemos unas correlaciones positivas moderadas: los tiempos de viaje en horario punta por las mañanas también son más altos en las comunas donde existen más hogares con ingresos bajos (según el Registro Social de Hogares), y donde los municipios dependen más del financiamiento del Fondo Común Municipal.\nBúsqueda de correlaciones Cómo tenemos todos los valores de correlación en una misma columna gracias a stretch(), podemos filtrar los valores para encontrar solamente con relaciones fuertes. Podemos lograr esto filtrando valores mayores a 0,5 o menores a -0,5, o filtrando valores mayores a el valor absoluto de 0,5 (abs(0.5)). Luego ordenamos los valores de mayor a menor, usando el valor absoluto de r (el valor en positivo).\ncorrelación |\u0026gt; stretch(remove.dups = T) |\u0026gt; filter(r \u0026gt; 0.5 | r \u0026lt; -0.5) |\u0026gt; arrange(desc(abs(r))) |\u0026gt; head() # A tibble: 6 × 3 x y r \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Disponibilidad Presupuestaria Municipal por Habitante (M$) Ingresos Propios Permanentes per Cápita (IPPP) 0.782 2 Ingresos Propios Permanentes per Cápita (IPPP) Consumo de energía eléctrica per cápita residencial 0.762 3 Porcentaje de Hogares de 0-40% de Ingresos respecto del Total Regional (RSH) Porcentaje de la población en situación de pobreza multidimensional 0.733 4 Porcentaje de la población en situación de pobreza multidimensional Tasa de conexiones residenciales fijas de internet por cada 100 habitantes -0.681 5 Disponibilidad Presupuestaria Municipal por Habitante (M$) Consumo de energía eléctrica per cápita residencial 0.681 6 Porcentaje de Hogares de 0-40% de Ingresos respecto del Total Regional (RSH) Tasa de conexiones residenciales fijas de internet por cada 100 habitantes -0.664 Evidentemente, las correlaciones más fuertes son entre variables similares: los hogares de menores ingresos correlacionan con la población en situación de pobreza, la disponibilidad de presupuesto municipal por habitante se correlaciona con los ingresos municipales percápita, y otras obviedades. Habría que afinar la selección de variables para remover aquellas que representan a un mismo fenómeno social subyacente.\nVisualización Otra gracia de {corrr} es que facilita visualizar las correlaciones por medio de un gráfico con la función rplot(), que produce un gráfico {ggplot2} con nuestra matriz de correlación.\nEn teoría, visualizar una correlación sería así de fácil:\ndatos |\u0026gt; correlate() |\u0026gt; rplot() Como nuestra matriz de correlación tiene muchas variables, tendremos que agregar algunos ajustes para que se vea bien.\nlibrary(ggplot2) correlación |\u0026gt; rearrange() |\u0026gt; # ordenar por intensidad rplot(print_cor = T, # agregar valores encima legend = F) + # sin leyenda # cortar los nombres de variable scale_y_discrete(labels = scales::label_wrap(70)) + scale_x_discrete(labels = scales::label_wrap(70)) + # variables inferiores inclinadas theme(axis.text.x = element_text(angle = 40, hjust = 1), axis.text = element_text(lineheight = 0.9, color = \u0026#34;black\u0026#34;)) Con esta visualización, podemos ver el color y el tamaño de los círculos para encontrar rápidamente los cruces entre variables que están correlacionados.\nA la rápida, podemos ver que arriba a la izquierda la variable de consumo de energía eléctrica se correlaciona con los recursos municipales, y abajo la izquierda podemos ver qué el porcentaje de hogares en los tramos menores de ingresos se correlaciona con menos conexiones a Internet y menos consumo eléctrico. También al centro del gráfico podemos ver una alta correlación entre la tasa de víctimas de delitos violentos y la densidad poblacional.\nAlternativas El paquete {ggcorrplot} es muy usado para obener gráficos de correlaciones, y tiene opciones que permiten personalizar la forma de visualizarlas:\nlibrary(ggcorrplot) corr \u0026lt;- round(cor(mtcars), 1) ggcorrplot(corr) También permite presentar con círculos:\nggcorrplot(corr, method = \u0026#34;circle\u0026#34;) Y tiene la opción de entregar una matriz de correlación de los valores p para excluir las correlaciones con coeficientes que no sean estadísticamente significativos:\nggcorrplot(corr, hc.order = TRUE, type = \u0026#34;lower\u0026#34;, p.mat = cor_pmat(mtcars)) Otra función de R que permite realizar correlaciones y visualizarlas de inmediato es ggcor() del paquete {GGally}, que entrega varios tipos de gráficos estadísticos para análisis exploratorios de datos.\ndatos_ancho |\u0026gt; select(-cut_comuna) |\u0026gt; GGally::ggcorr(hjust = 1, nbreaks = 5, layout.exp = 10, label = TRUE) El resultado es menos atractivo, pero si es bastante más legible.\nManualmente Finalmente, y como no podía faltar, también podemos crear un gráfico de la matriz de correlación desde cero con {ggplot2}. Esto no es tan complejo gracias a que correlate() y stretch() entregan los resultados bien ordenaditos.\ndatos_ancho |\u0026gt; correlate() |\u0026gt; rearrange() |\u0026gt; stretch() |\u0026gt; # gráfico ggplot() + aes(x, y, fill = r) + geom_tile(color = \u0026#34;white\u0026#34;, linewidth = 0.6) + # geometría de cuadros o mosaicos geom_text(aes(label = round(r, 1)), color = \u0026#34;black\u0026#34;, size = 3) + # texto # escala de color de los cuadros scale_fill_gradient2(high = \u0026#34;indianred2\u0026#34;, mid = \u0026#34;white\u0026#34;, low = \u0026#34;skyblue1\u0026#34;, na.value = \u0026#34;white\u0026#34;) + # formato de las escalas de los ejes scale_y_discrete(labels = scales::label_wrap(50), expand = c(0, 0)) + scale_x_discrete(labels = scales::label_wrap(60), expand = c(0, 0)) + # ajustes de tema theme_minimal() + theme(axis.text = element_text(lineheight = 0.9, color = \u0026#34;black\u0026#34;), axis.text.x = element_text(angle = 40, hjust = 1), axis.title = element_blank(), legend.key.width = unit(2, \u0026#34;mm\u0026#34;)) También podemos aprovechar el vuelo para hacer una bonita obra de arte con nuestras correlaciones:\ndatos_ancho |\u0026gt; correlate() |\u0026gt; rearrange() |\u0026gt; stretch() |\u0026gt; ggplot() + aes(x, y, fill = r) + geom_tile(color = \u0026#34;white\u0026#34;, linewidth = 0.6) + scale_fill_gradient2(high = \u0026#34;#865BAB\u0026#34;, mid = \u0026#34;white\u0026#34;, low = \u0026#34;#AB5B90\u0026#34;, na.value = \u0026#34;white\u0026#34;) + guides(fill = guide_none()) + theme_void() + coord_fixed() Como siempre, {ggplot2} es una herramienta extremadamente versátil para visualizar cualquier tipo de información. Puedes aprender a crear visualizaciones de datos desde cero en R siguiendo este tutorial de {ggplot2}.\nSi te gustó este contenido, puedes ayudarme donándome un cafecito si presionas el siguiente botón. Te lo agradecería mucho y me anima a seguir compartiendo!\n","date":"2025-07-09T00:00:00Z","excerpt":"El análisis de correlación es una técnica estadística de análisis exploratorio que nos permite identificar si existen relaciones lineales entre distintas variables. En este tutorial aprenderemos a realizar correlaciones entre múltiples variables, interpretarlas, y visualizarlas de varias maneras distintas.","href":"https://bastianoleah.netlify.app/blog/correlaciones/","tags":"estadística ; ggplot2 ; gráficos ; visualización de datos","title":"Análisis y visualización de correlaciones en R"},{"content":" {ggview} es un paquete de R que te ayuda a crear gráficos en {ggplot2} manteniendo un tamaño fijo.\nEn RStudio, los gráficos que aparecen en el panel de gráficos (Plots) se adaptan al tamaño de dicho panel. Por ejemplo, si tu panel es chico, el gráfico no tendría espacio para verse bien:\nPero si amplías el tamaño del panel lo suficiente, el gráfico se verá mejor:\nEsto es conveniente para ir explorando visualizaciones, pero puede confundirte cuando quieras guardar el gráfico. Una vez que tu gráfico está listo, procedes a guardarlo\u0026hellip;\nggsave(\u0026#34;grafico.jpg\u0026#34;) Pero cuando abres el gráfico, sorpresa, se ve distinto! 🙄\nEsto es porque las dimensiones del panel Plots no son las mismas que las dimensiones con que se guardan por defecto los gráficos. En la función ggsave() puedes especificar ancho, alto y resolución de la imagen que se va a guardar. Entonces, lo que podrías hacer es intentar configurar ggsave() para que el gráfico se guarde como tú esperas\u0026hellip; pero a veces esto se vuelve en un juego de ir adivinando, intentando números varias veces hasta que le achuntas al gráfico que esperabas.\nUna mejor alternativa es usar {ggview} para previsualizar tus gráficos con un tamaño fijo:\nlibrary(ggview) grafico + canvas(7, 5) De esta forma, no importa el tamaño de tu ventana de RStudio: tu gráfico se previsualizará con la resolución y proporción que tu especifiques.\nRecomiendo empezar a usar ggview::canvas() cuando tu gráfico ya está casi completo y te pones a afinar detalles estéticos finales.\nCuando ya sea la hora de guardar el gráfico como una imagen, si usas la función ggview::save_ggplot() (no confundir con la común, ggsave()), el gráfico se guardará con las dimensiones que habías puesto en canvas() y se verá exactamente como lo estabas previsualizando en RStudio!\ngrafico_2 \u0026lt;- grafico + canvas(7, 5) save_ggplot(grafico_2, \u0026#34;grafico_2.jpg\u0026#34;) # mantiene las dimensiones Instala {ggview} ejecutando lo siguiente en tu consola:\ninstall.packages(\u0026#34;ggview\u0026#34;) ","date":"2025-07-08T00:00:00Z","excerpt":"¿Te ha pasado que estás haciendo un gráfico con `{ggplot2}` pero al momento de guardarlo te das cuenta que sale en otro tamaño y en otra proporción? Entonces este consejo es para ti: `{ggview}` es un paquete de R que te ayuda a previsualizar gráficos en `{ggplot2}` manteniendo un tamaño fijo.","href":"https://bastianoleah.netlify.app/blog/ggview/","tags":"visualización de datos ; gráficos ; ggplot2","title":"Controla las dimensiones de tus gráficos con {ggview}"},{"content":"Consejo: si tus documentos Quarto salen pesados por tener muchos gráficos, intenta cambiar en el YAML el siguiente argumento para que los gráficos incluidos vayan en formato .jpg en vez de .png (por defecto).\nfig-format: \u0026#34;jpeg\u0026#34; Las imágenes .jpg son más livianas, porque usan compresión, aunque pueden tener un poco menos de calidad y no tienen fondo transparente. Por otro lado, las imágenes .png son lossless (no tienen compresión ni pérdida de calidad) y transparencia, pero a cambio son más pesadas.\nAcá se nota la diferencia de tamaños entre un mismo reporte con gráficos en .png y .jpg: una reducción de un 39,9% en el peso del archivo con sólo agregar una línea.\n","date":"2025-07-08T00:00:00Z","excerpt":"Si tus documentos Quarto salen pesados por tener muchos gráficos, intenta cambiar en el `YAML` el siguiente argumento para que los gráficos incluidos vayan en formato `.jpg` en vez de `.png`.","href":"https://bastianoleah.netlify.app/blog/2025-07-08/","tags":"quarto ; consejos","title":"Reduce el tamaño de tus reportes Quarto con este truco"},{"content":"Una de las formas más intuitivas de visualizar datos de texto son las nubes de palabras. En las nubes de palabras seleccionamos un subconjunto de las palabras del texto que queremos analizar y las distribuimos en un gráfico, donde las palabras que aparecen más frecuentemente aparecen más grandes, y usualmente al centro. Sirven para ver rápidamente los conceptos clave de un documento o un corpus de documentos.\nEn este post veremos dos formas de crear nubes de palabras con R: con {wordcloud2} y con {ggplot2}. Para empezar, necesitamos una base de datos que tenga información de texto; por ejemplo, una base donde cada fila contenga una respuesta abierta de una encuesta, una reseña de un producto, un párrafo de un texto, un capítulo de un libro, o un libro completo.\nCon el siguiente código descargaremos una base de datos con 10.000 documentos de texto, en este caso se trata de noticias de la prensa chilena. Los datos son obtenidos de este repositorio de obtención automatizada de textos de noticias de prensa escrita chilena.\nlibrary(dplyr) Warning: package 'dplyr' was built under R version 4.4.3 # dirección web donde se encuentran los datos url_datos \u0026lt;- \u0026#34;https://raw.githubusercontent.com/bastianolea/prensa_chile/refs/heads/main/prensa_datos_muestra.csv\u0026#34; # lectura de los datos ubicados en internet noticias \u0026lt;- readr::read_csv2(url_datos) noticias |\u0026gt; select(titulo, cuerpo, fecha) # A tibble: 10,000 × 3 titulo cuerpo fecha \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;date\u0026gt; 1 \u0026quot;Hombre de 66 años fue asesinado en plena vía pública en M… \u0026quot;El h… 2024-07-21 2 \u0026quot;Revive el cuarto capítulo de Indecisos: Candidatos a alca… \u0026quot;Este… 2024-09-03 3 \u0026quot;Menos personas circulando y miedo de los residentes: Así … \u0026quot;Poca… 2024-04-08 4 \u0026quot;CEO de Walmart Chile sostiene que si la empresa no da “se… \u0026quot;Hace… 2024-12-22 5 \u0026quot;Pdte. Boric entregó mensaje por la muerte de Piñera y rec… \u0026quot;El p… 2024-02-06 6 \u0026quot;Encuentran dos cadáveres en un canal de regadío en Pirque\u0026quot; \u0026quot;Pers… 2024-07-13 7 \u0026quot;Magisterio denuncia \\\u0026quot;abandono de la educación pública\\\u0026quot; … \u0026quot;Carl… 2024-02-21 8 \u0026quot;“El día que la democracia triunfó sobre la dictadura”: Mi… \u0026quot;Los … 2024-10-05 9 \u0026quot;Accidente de tránsito deja un fallecido y tres heridos en… \u0026quot;En h… 2024-12-22 10 \u0026quot;Dos personas resultan baleadas tras intentar evitar asalt… \u0026quot;Aton… 2024-11-02 # ℹ 9,990 more rows En este conjunto de datos, los textos completos de los documentos vienen en la columna cuerpo:\nnoticias |\u0026gt; select(cuerpo) # A tibble: 10,000 × 1 cuerpo \u0026lt;chr\u0026gt; 1 \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue abordada p… 2 \u0026quot;Este martes 3 de septiembre fue el cuarto capítulo de la nueva temporada de… 3 \u0026quot;Pocas personas circulan en la toma \\\u0026quot;Nuevo Amanecer\\\u0026quot; de Cerrillos, luego d… 4 \u0026quot;Hace unos días, Walmart Chile anunció un plan de inversión en Chile de US$1… 5 \u0026quot;El presidente Gabriel Boric lamentó el fallecimiento del expresidente Sebas… 6 \u0026quot;Personal del Grupo de Operaciones Policiales Especiales (GOPE) de Carabiner… 7 \u0026quot;Carlos Rodríguez, presidente regional de los Profesores, aseguró que el Min… 8 \u0026quot;Los ministros de Educación, Nicolás Cataldo, y del Interior, Carolina Tohá,… 9 \u0026quot;En horas de la mañana de este domingo, ocurrió un lamentable accidente en l… 10 \u0026quot;Aton / Imagen Referencial Dos personas resultaron heridas a bala en un inte… # ℹ 9,990 more rows Usualmente los datos de texto van a venir de esta forma, con todo el texto de cada documento dentro de una celda. Para poder analizar los datos en este formato tenemos que tokenizar los textos: transformar la estructura de los datos para pasar desde filas que contienen documentos, párrafos u oraciones, a filas que contienen palabras individuales, una palabra por observación.\nUsaremos la función unnest_tokens de {tidytext} para tokenizar y limpiar1 el texto:\nlibrary(tidytext) palabras \u0026lt;- noticias |\u0026gt; # tokenizar columna que contiene los documentos unnest_tokens(input = cuerpo, drop = FALSE, output = palabra, token = \u0026#34;words\u0026#34;, # limpieza mínima de texto to_lower = TRUE, strip_punct = TRUE, strip_numeric = TRUE) |\u0026gt; # eliminar stopwords o palabras vacías filter(!palabra %in% stopwords::stopwords(\u0026#34;spanish\u0026#34;)) palabras |\u0026gt; select(palabra, cuerpo) # A tibble: 1,817,901 × 2 palabra cuerpo \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 hecho \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue … 2 ocurrió \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue … 3 horas \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue … 4 madrugada \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue … 5 víctima \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue … 6 abordada \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue … 7 grupo \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue … 8 sujetos \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue … 9 atacó \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue … 10 arma \u0026quot;El hecho ocurrió en horas de la madrugada, cuando la víctima fue … # ℹ 1,817,891 more rows Luego de tokenización, vemos que la columna que contiene el texto completo de los documentos está acompañada de una nueva variable que contiene cada palabra por separado. De esta forma puedes continuar analizando el documento con palabras individuales, pero teniendo como referencia al texto completo donde aparece cada palabra si es que lo necesitas.\nA partir de la variable de palabras individuales, vamos a hacer un conteo de las palabras más frecuentes:\npalabras_conteo \u0026lt;- palabras |\u0026gt; count(palabra, sort = TRUE) palabras_conteo # A tibble: 74,911 × 2 palabra n \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 chile 7208 2 años 5457 3 según 5071 4 parte 4952 5 personas 4921 6 si 4912 7 región 4830 8 ser 4656 9 país 4628 10 gobierno 4599 # ℹ 74,901 more rows Rápidamente podemos ver dos problemas: tenemos demasiadas palabras individuales (más de 80 mil), y tenemos palabras muy cortas, como la palabra si, así que haremos una pequeña limpieza:\npalabras_conteo \u0026lt;- palabras_conteo |\u0026gt; # largo mínimo de palabras filter(nchar(palabra) \u0026gt;= 3) |\u0026gt; # sólo las palabras más frecuentes slice_max(n, n = 2000) También podemos realizar una búsqueda dentro de las palabras y contar cuántas veces aparece cada una:\npalabras_conteo |\u0026gt; filter(palabra %in% c(\u0026#34;guerra\u0026#34;, \u0026#34;paz\u0026#34;)) # A tibble: 2 × 2 palabra n \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; 1 guerra 398 2 paz 267 Nube de palabras con {wordcloud2} {wordcloud2} es un paquete muy sencillo de usar que permite visualizar datos de nubes de palabras con muy poca configuración.\nSi no tienes el paquete instalado, puedes instalarlo con el siguiente código:\ndevtools::install_github(\u0026#34;lchiffon/wordcloud2\u0026#34;) Para crear una nube de palabras con {wordcloud2} solo necesitas que la primera columna del dataframe contenga las palabras, y la segunda contenga el conteo de frecuencia de las palabras:\nlibrary(wordcloud2) palabras_conteo |\u0026gt; # sólo palabras que aparezcan n veces filter(n \u0026gt; 1000) |\u0026gt; wordcloud2(backgroundColor = \u0026#34;#EAD1FA\u0026#34;, color = \u0026#34;#543A74\u0026#34;) Usamos algunas de las opciones de personalización de wordcloud2() para obtener el resultado que necesitamos. Personalmente no me gusta mucho que las palabras aparezcan tan en ángulo, y prefiero que las palabras sean menos grandes para que se vea una mayor cantidad.\npalabras_conteo |\u0026gt; filter(n \u0026gt; 1000) |\u0026gt; wordcloud2(backgroundColor = \u0026#34;#EAD1FA\u0026#34;, color = \u0026#34;#543A74\u0026#34;, # personalización rotateRatio = 0.1, # rotación máxima gridSize = 8, # espaciado entre cada palabra size = 0.5, # tamaño del texto en general minSize = 11) # tamaño mínimo de las letras Nube de palabras con {ggplot2} y {ggwordcloud} Otra forma de hacer nubes de palabras es agregando una geometría personalizada a {ggplot2}: geom_text_wordcloud() del paquete {ggwordcloud}\nInstalar el paquete:\ndevtools::install_github(\u0026#34;lepennec/ggwordcloud\u0026#34;) Configuramos un tema general para los gráficos, para que se vean bonitos aquí 🥰\nlibrary(ggplot2) library(ggwordcloud) # definir el tema para todos los gráficos theme_set( theme_void() + theme(plot.background = element_rect(fill = \u0026#34;#EAD1FA\u0026#34;, linewidth = 0)) ) Para crear la nube de palabras con {ggplot2}, solamente tenemos que especificar en la estética aes() la variable que contiene las palabras (palabra) y la variable que controla el tamaño de las mismas (n). Luego simplemente indicar que los datos van a visualizarse por medio de la geometría geom_text_wordcloud(), que se encargará de distribuir los textos en el área del gráfico.\npalabras_conteo |\u0026gt; filter(n \u0026gt; 2000) |\u0026gt; ggplot() + aes(label = palabra, size = n) + geom_text_wordcloud(shape = \u0026#34;circle\u0026#34;, color = \u0026#34;#543A74\u0026#34;) + # definir rango de tamaños de las palabras scale_size_continuous(range = c(3, 20)) El beneficio de usar esta estrategia es que te entrega toda la flexibilidad de visualizar datos con {ggplot2}. Por ejemplo, podemos agregar un mapeo de la transparencia y el color para crear una nube donde las palabras menos frecuentes sean más transparentes y donde ciertas palabras claves tengan un color distinto:\npalabras_conteo |\u0026gt; filter(n \u0026gt; 2000) |\u0026gt; mutate(clave = ifelse(palabra %in% c(\u0026#34;gobierno\u0026#34;, \u0026#34;presidente\u0026#34;, \u0026#34;boric\u0026#34;, \u0026#34;ministerio\u0026#34;), \u0026#34;clave\u0026#34;, \u0026#34;otras\u0026#34;)) |\u0026gt; ggplot() + aes(label = palabra, size = n, alpha = n, color = clave) + geom_text_wordcloud(shape = \u0026#34;circle\u0026#34;, grid_size = 6) + # definir rango de tamaño de palabras scale_size_continuous(range = c(3, 15)) + # especificar colores de las palabras clave scale_color_manual(values = c(\u0026#34;clave\u0026#34; = \u0026#34;#D93E98\u0026#34;, \u0026#34;otras\u0026#34; = \u0026#34;#543A74\u0026#34;)) + # definir el rango de la transparencia de palabras scale_alpha_continuous(range = c(0.4, 1)) También podemos crear una variable que destaques ciertas palabras específicas dentro de la nube:\npalabras_conteo |\u0026gt; filter(n \u0026gt; 1500) |\u0026gt; # crear variable que destaque palabras específicas mutate(clave = ifelse(palabra %in% c(\u0026#34;gobierno\u0026#34;, \u0026#34;chile\u0026#34;, \u0026#34;presidente\u0026#34;, \u0026#34;boric\u0026#34;, \u0026#34;ministerio\u0026#34;, \u0026#34;ministra\u0026#34;, \u0026#34;interior\u0026#34;, \u0026#34;partido\u0026#34;, \u0026#34;público\u0026#34;), \u0026#34;clave\u0026#34;, \u0026#34;otras\u0026#34;)) |\u0026gt; ggplot() + aes(label = palabra, size = n, # mapear transparencia a la variable con palabras clave alpha = clave) + geom_text_wordcloud(shape = \u0026#34;circle\u0026#34;, color = \u0026#34;#543A74\u0026#34;, rm_outside = TRUE) + # especificar rango de tamaños scale_size_continuous(range = c(3, 15)) + # especificar nivel de transparencia de las palabras clave y de las demás scale_alpha_manual(values = c(\u0026#34;clave\u0026#34; = 1, \u0026#34;otras\u0026#34; = 0.3)) Warning in wordcloud_boxes(data_points = points_valid_first, boxes = boxes, : Some words could not fit on page. They have been removed. Con esta aproximación podemos crear visualizaciones más personalizadas y complejas a partir de datos de texto, sobre todo si tenemos variables adicionales que nos digan más información sobre las palabras que queremos graficar, cómo puede ser alguna otro criterio de prevalencia de palabras en los documentos, alguna clasificación de las palabras en tópicos o temas, una agrupación de los documentos de donde provienen las palabras, etc.\nLa limpieza del texto es un paso clave en este tipo de análisis, que para simplificar esta guía, omitiremos. Es muy importante procesar los textos para poder eliminar palabras vacías, palabras mal escritas, remover símbolos, remover palabras irrelevantes, e incluso lematizar palabras para agrupar diferentes conjugaciones de una misma palabra en una sola.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-07-05T00:00:00Z","excerpt":"\u003cp\u003eUna de las formas más intuitivas de visualizar datos de texto son las nubes de palabras. En las nubes de palabras seleccionamos un subconjunto de las palabras del texto que queremos analizar y las distribuimos en un gráfico, donde las palabras que aparecen más frecuentemente aparecen más grandes, y usualmente al centro. Sirven para ver rápidamente los conceptos clave de un documento o un corpus de documentos.\u003c/p\u003e\n\u003cp\u003eEn este post veremos dos formas de crear nubes de palabras con R: con \u003ccode\u003e{wordcloud2}\u003c/code\u003e y con \u003ccode\u003e{ggplot2}\u003c/code\u003e. Para empezar, necesitamos una base de datos que tenga información de texto; por ejemplo, una base donde cada fila contenga una respuesta abierta de una encuesta, una reseña de un producto, un párrafo de un texto, un capítulo de un libro, o un libro completo.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/nubes_de_palabras/","tags":"visualización de datos ; gráficos ; ggplot2 ; análisis de texto","title":"Visualizando texto como nubes de palabras en R"},{"content":"Complementando el post sobre ordenar regiones de Chile de norte a sur, en esta publicación veremos cómo ordenar las comunas del país de norte a sur. Esto puede servirnos para mostrar datos a nivel comunal de una gran cantidad de regiones de una manera más intuitiva, en los casos donde ordenarlas por orden alfabético no entrega mucha información, o cuando queramos mostrar en nuestras visualizaciones o tablas que el factor geográfico incide en el dato principal.\nPartamos con una tabla de datos de varias comunas del país. Es necesario que estas comunas cuenten con su código único territorial, el códgo numérico que identifica de manera única a cada región del país. Si tu base de datos no tiene los códigos únicos territoriales, tendrás que realizar una unión entre los nombres de las comunas y los códigos, que puedes encontrar en csv y rds en este respositorio..\nlibrary(dplyr) comunas \u0026lt;- tibble::tribble( ~nombre_comuna, ~codigo_comuna, \u0026#34;Rancagua\u0026#34;, 6101, \u0026#34;San Fernando\u0026#34;, 6301, \u0026#34;Coyhaique\u0026#34;, 11101, \u0026#34;La Pintana\u0026#34;, 13112, \u0026#34;Rengo\u0026#34;, 6115, \u0026#34;Puerto Montt\u0026#34;, 10101, \u0026#34;San Pedro de la Paz\u0026#34;, 8108, \u0026#34;Alto Hospicio\u0026#34;, 1107, \u0026#34;Chillán\u0026#34;, 16101, \u0026#34;Angol\u0026#34;, 9201, \u0026#34;Talcahuano\u0026#34;, 8110, \u0026#34;Calama\u0026#34;, 2201, \u0026#34;Concepción\u0026#34;, 8101, \u0026#34;Hualpén\u0026#34;, 8112, \u0026#34;La Serena\u0026#34;, 4101, \u0026#34;Calera\u0026#34;, 5502, \u0026#34;Valdivia\u0026#34;, 14101, \u0026#34;Cerro Navia\u0026#34;, 13103, \u0026#34;Lampa\u0026#34;, 13302, \u0026#34;Quinta Normal\u0026#34;, 13126, \u0026#34;Huechuraba\u0026#34;, 13107, \u0026#34;Linares\u0026#34;, 7401, \u0026#34;Tomé\u0026#34;, 8111, \u0026#34;Viña del Mar\u0026#34;, 5109, \u0026#34;Recoleta\u0026#34;, 13127, \u0026#34;Talagante\u0026#34;, 13601, \u0026#34;Valparaíso\u0026#34;, 5101, \u0026#34;Coronel\u0026#34;, 8102, \u0026#34;Buin\u0026#34;, 13402, \u0026#34;Macul\u0026#34;, 13118, \u0026#34;Punta Arenas\u0026#34;, 12101, \u0026#34;Talca\u0026#34;, 7101, \u0026#34;Los Ángeles\u0026#34;, 8301, \u0026#34;Puente Alto\u0026#34;, 13201, \u0026#34;Copiapó\u0026#34;, 3101, \u0026#34;Curicó\u0026#34;, 7301, \u0026#34;Coquimbo\u0026#34;, 4102, \u0026#34;Melipilla\u0026#34;, 13501, \u0026#34;Vitacura\u0026#34;, 13132, \u0026#34;Antofagasta\u0026#34;, 2101, \u0026#34;Chiguayante\u0026#34;, 8103, \u0026#34;Renca\u0026#34;, 13128, \u0026#34;Quilicura\u0026#34;, 13125, \u0026#34;Pedro Aguirre Cerda\u0026#34;, 13121, \u0026#34;Ovalle\u0026#34;, 4301, \u0026#34;San Bernardo\u0026#34;, 13401, \u0026#34;Los Andes\u0026#34;, 5301, \u0026#34;San Joaquín\u0026#34;, 13129, \u0026#34;Colina\u0026#34;, 13301, \u0026#34;Quillota\u0026#34;, 5501, \u0026#34;Peñalolén\u0026#34;, 13122, \u0026#34;Estación Central\u0026#34;, 13106, \u0026#34;San Ramón\u0026#34;, 13131, \u0026#34;Lo Prado\u0026#34;, 13117, \u0026#34;Arica\u0026#34;, 15101, \u0026#34;San Antonio\u0026#34;, 5601, \u0026#34;Cerrillos\u0026#34;, 13102, \u0026#34;Santiago\u0026#34;, 13101, \u0026#34;Lo Barnechea\u0026#34;, 13115, \u0026#34;Temuco\u0026#34;, 9101, \u0026#34;La Florida\u0026#34;, 13110, \u0026#34;San Felipe\u0026#34;, 5701, \u0026#34;Ñuñoa\u0026#34;, 13120, \u0026#34;La Cisterna\u0026#34;, 13109, \u0026#34;Machalí\u0026#34;, 6108, \u0026#34;Independencia\u0026#34;, 13108, \u0026#34;La Granja\u0026#34;, 13111, \u0026#34;Villa Alemana\u0026#34;, 5804, \u0026#34;San Miguel\u0026#34;, 13130, \u0026#34;Las Condes\u0026#34;, 13114, \u0026#34;Lo Espejo\u0026#34;, 13116, \u0026#34;Quilpué\u0026#34;, 5801, \u0026#34;Conchalí\u0026#34;, 13104, \u0026#34;Peñaflor\u0026#34;, 13605, \u0026#34;El Bosque\u0026#34;, 13105, \u0026#34;Maipú\u0026#34;, 13119, \u0026#34;La Reina\u0026#34;, 13113, \u0026#34;Pudahuel\u0026#34;, 13124, \u0026#34;Providencia\u0026#34;, 13123, \u0026#34;Padre Hurtado\u0026#34;, 13604, \u0026#34;Iquique\u0026#34;, 1101, \u0026#34;Osorno\u0026#34;, 10301 ) Puedes copiar y pegar este código para obtener en tu sesión de R la tabla con comunas. Si ejecutamos esta tabla de datos, veremos que las regiones no están ordenadas geográficamente:\ncomunas # A tibble: 82 × 2 nombre_comuna codigo_comuna \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Rancagua 6101 2 San Fernando 6301 3 Coyhaique 11101 4 La Pintana 13112 5 Rengo 6115 6 Puerto Montt 10101 7 San Pedro de la Paz 8108 8 Alto Hospicio 1107 9 Chillán 16101 10 Angol 9201 # ℹ 72 more rows Si hacemos un gráfico con estas comunas, veremos que el orden del eje con las comunas será alfabético, dado que las comunas son una variable de tipo carácter o texto:\nlibrary(ggplot2) comunas |\u0026gt; ggplot() + aes(codigo_comuna, nombre_comuna, fill = codigo_comuna) + geom_col() + theme_minimal() + scale_x_continuous(expand = c(0, 0)) Tampoco nos serviría ordenarla por los códigos únicos territoriales, dado que éstos empiezan con el código de las regiones, y como vimos en el otro post, los códigos regionales no ordenan geográficamente las regiones, y menos a las comunas.\nUna forma de ordenar las comunas de Chile geográficamente de norte a sur es valiéndonos de la geografía vertical del país. Podemos obtener un mapa de Chile con el paquete {chilemapas}, y luego extraer la latitud de las comunas, para usarla como variable de ordenamiento de las comunas.\nLa función chilemapas::mapa_comunas de {chilemapas} nos entrega un dataframe con los datos geográficos del país a nivel comunal.\nlibrary(dplyr) library(chilemapas) Loading required package: sf Linking to GEOS 3.13.0, GDAL 3.8.5, PROJ 9.5.1; sf_use_s2() is TRUE La documentacion del paquete y ejemplos de uso se encuentran en https://pacha.dev/chilemapas/. Visita https://buymeacoffee.com/pacha/ si deseas donar para contribuir al desarrollo de este software. # obtener mapa mapa_chile \u0026lt;- chilemapas::mapa_comunas |\u0026gt; mutate(codigo_comuna = as.numeric(codigo_comuna)) |\u0026gt; select(codigo_comuna, geometry) mapa_chile # A tibble: 345 × 2 codigo_comuna geometry \u0026lt;dbl\u0026gt; \u0026lt;MULTIPOLYGON [°]\u0026gt; 1 1401 (((-68.86081 -21.28512, -68.92172 -21.30035, -68.98939 -21.317… 2 1403 (((-68.65113 -19.77188, -68.81182 -19.74362, -68.81575 -19.733… 3 1405 (((-68.65113 -19.77188, -68.63545 -19.78062, -68.62511 -19.785… 4 1402 (((-69.31789 -19.13651, -69.2717 -19.23735, -69.14291 -19.2967… 5 1404 (((-69.39615 -19.06125, -69.40025 -19.06897, -69.40296 -19.071… 6 1107 (((-70.1095 -20.35131, -70.12438 -20.31578, -70.10985 -20.2743… 7 1101 (((-70.09894 -20.08504, -70.1023 -20.11401, -70.12001 -20.1551… 8 2104 (((-68.98863 -25.38016, -68.98731 -25.38411, -68.98696 -25.393… 9 2101 (((-70.60654 -23.43054, -70.60175 -23.43515, -70.60133 -23.438… 10 2201 (((-67.94302 -22.38175, -67.95564 -22.39233, -67.96315 -22.393… # ℹ 335 more rows A partir de este mapa, usamos el paquete {sf} para extraer los centroides de las comunas, que son el punto central de la geometría de cada polígono. Luego, a partir del centroide podemos obtener la latitud de estos puntos, para ver qué tan al norte o sur está cada comuna.\nlibrary(sf) # extraer latitud desde los polígonos comunales mapa_chile_latitud \u0026lt;- mapa_chile |\u0026gt; mutate(centroide = st_centroid(geometry), longitud = sf::st_coordinates(centroide)[,1], latitud = sf::st_coordinates(centroide)[,2]) mapa_chile_latitud # A tibble: 345 × 5 codigo_comuna geometry centroide longitud \u0026lt;dbl\u0026gt; \u0026lt;MULTIPOLYGON [°]\u0026gt; \u0026lt;POINT [°]\u0026gt; \u0026lt;dbl\u0026gt; 1 1401 (((-68.86081 -21.28512, -68… (-69.50811 -20.77126) -69.5 2 1403 (((-68.65113 -19.77188, -68… (-68.84517 -19.35443) -68.8 3 1405 (((-68.65113 -19.77188, -68… (-68.91311 -20.48006) -68.9 4 1402 (((-69.31789 -19.13651, -69… (-69.50102 -19.36999) -69.5 5 1404 (((-69.39615 -19.06125, -69… (-69.66358 -19.60151) -69.7 6 1107 (((-70.1095 -20.35131, -70.… (-70.01261 -20.18971) -70.0 7 1101 (((-70.09894 -20.08504, -70… (-70.04836 -20.92498) -70.0 8 2104 (((-68.98863 -25.38016, -68… (-69.8618 -25.30966) -69.9 9 2101 (((-70.60654 -23.43054, -70… (-69.40757 -24.27784) -69.4 10 2201 (((-67.94302 -22.38175, -67… (-68.62665 -22.16826) -68.6 # ℹ 335 more rows # ℹ 1 more variable: latitud \u0026lt;dbl\u0026gt; Con esto podemos simplemente ordenar por latitud y generar un orden numérico del 1 al 345.\norden_comunas \u0026lt;- mapa_chile_latitud |\u0026gt; arrange(desc(latitud), longitud) |\u0026gt; mutate(orden_comuna = row_number()) |\u0026gt; select(codigo_comuna, orden_comuna) orden_comunas # A tibble: 345 × 2 codigo_comuna orden_comuna \u0026lt;dbl\u0026gt; \u0026lt;int\u0026gt; 1 15202 1 2 15201 2 3 15101 3 4 15102 4 5 1403 5 6 1402 6 7 1404 7 8 1107 8 9 1405 9 10 1401 10 # ℹ 335 more rows Obtenemos una tabla con los códigos únicos territoriales y la latitud de cada comuna. Ahora podemos unir estas columnas con nuestros datos usando left_join():\n# a los datos originales, agregarle la tabla con el orden de las comunas comunas_ordenadas \u0026lt;- comunas |\u0026gt; left_join(orden_comunas, join_by(codigo_comuna)) |\u0026gt; # ordenar observaciones arrange(orden_comuna) comunas_ordenadas # A tibble: 82 × 3 nombre_comuna codigo_comuna orden_comuna \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;int\u0026gt; 1 Arica 15101 3 2 Alto Hospicio 1107 8 3 Iquique 1101 11 4 Calama 2201 15 5 Antofagasta 2101 19 6 Copiapó 3101 25 7 La Serena 4101 32 8 Coquimbo 4102 34 9 Ovalle 4301 38 10 San Felipe 5701 56 # ℹ 72 more rows Intentamos nuevamente con el gráfico a ver cómo sale:\n# intento incorrecto de gráfico ordenado comunas_ordenadas |\u0026gt; arrange(orden_comuna) |\u0026gt; ggplot() + aes(x = codigo_comuna, y = nombre_comuna, fill = codigo_comuna) + geom_col() + theme_minimal() + guides(fill = guide_none()) + scale_x_continuous(expand = c(0, 0)) Al igual como nos pasó con el ordenamiento de regiones, la variable nombre_comuna es una variable de texto (tipo caracter) y que no tiene un orden intrínseco, así que se asume que su orden es alfabético. Por lo tanto, tenemos que darle un orden a esta variable, convirtiéndola a factor con el orden de una segunda variable numérica usando fct_reorder(); en otras palabras, darle a nombre_comuna el orden que tiene la variable numérica orden_comuna:\nlibrary(forcats) # ordenar variable a partir de una segunda variable numérica comunas_ordenadas_2 \u0026lt;- comunas_ordenadas |\u0026gt; mutate(nombre_comuna = fct_reorder(nombre_comuna, orden_comuna, .desc = T)) Ahora si repetimos el mismo gráfico que antes, vemos que el orden de las comunas ahora sí es geográfico! Arica está arriba y Punta Arenas abajo, como corresponde.\n# crear un gráfico comunas_ordenadas_2 |\u0026gt; ggplot() + aes(x = orden_comuna, y = nombre_comuna, fill = orden_comuna) + geom_col() + theme_minimal() + guides(fill = guide_none()) + scale_x_continuous(expand = c(0, 0)) Aparecen visualmente de norte a sur (de arriba a abajo) porque al ordenarlas le pusimos un orden descendiente (.desc = TRUE), y ésto es necesario porque generalmente en los gráficos el origen o inicio de los ejes es en 1 y el valor de la variable aumenta a medida que se aleja del origen.\n","date":"2025-06-25T00:00:00Z","excerpt":"\u003cp\u003eComplementando el \n\u003ca href=\"../../../blog/ordenar_regiones/\"\u003epost sobre ordenar regiones de Chile de norte a sur\u003c/a\u003e, en esta publicación veremos cómo ordenar las comunas del país de norte a sur. Esto puede servirnos para mostrar datos a nivel comunal de una gran cantidad de regiones de una manera más intuitiva, en los casos donde ordenarlas por orden alfabético no entrega mucha información, o cuando queramos mostrar en nuestras visualizaciones o tablas que el factor geográfico incide en el dato principal.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/ordenar_comunas/","tags":"mapas ; Chile","title":"Ordenar las comunas de Chile de norte a sur en R"},{"content":" Los datos anómalos o outliers son datos que se alejan considerablemente de los demás. Estos datos pueden resultar problemáticos para ciertos análisis, pueden ser indicio de errores en la recolección o limpieza de datos, o pueden requerir que tomemos ciertas decisiones para corregirlos o excluirlos.\nEn este post simularemos un dataset con datos anómalos, y luego mostraremos algunas formas de visualización de datos anómalos en {ggplot2} para tomar decisiones al respecto. Al final crearemos un gráfico interactivo con {ggiraph} que permita poner el cursor sobre las observaciones para obtener más información.\nPrimero creemos un conjunto de datos que contenga números al azar, 90 números entre 1 y 100, y 10 números más grandes, para que sean nuestros outliers simulados:\nlibrary(dplyr) library(ggplot2) set.seed(1993) valor \u0026lt;- c(sample(1:100, size = 90), # números al azar sample(120:190, size = 10)) # outliers Obtenemos un vector con los dos conjuntos de números al azar.\nCon la función tibble() convertimos el vector de números en una columna de un dataframe, y usando nuevamente sample() crearemos dos columnas con palabras al azar para complementar estos datos simulados:\ndatos \u0026lt;- tibble(valor) |\u0026gt; # procesar datos por fila en vez de por la columna entera (vectorización) rowwise() |\u0026gt; # por fila, elegir tres sílabas al azar y unirlas en un sólo texto, que será el \u0026#34;nombre\u0026#34; de las observaciones mutate(nombre = sample(c(\u0026#34;ma\u0026#34;, \u0026#34;pa\u0026#34;, \u0026#34;che\u0026#34;, \u0026#34;cha\u0026#34;), 3, F) |\u0026gt; paste(collapse = \u0026#34;\u0026#34;)) |\u0026gt; # desagrupar y crear otra variable con trres valores posibles, y que por lo tanto distribuya los datos en tres grupos ungroup() |\u0026gt; mutate(grupo = sample(c(\u0026#34;ma\u0026#34;, \u0026#34;pa\u0026#34;, \u0026#34;che\u0026#34;), n(), T)) datos # A tibble: 100 × 3 valor nombre grupo \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 45 chapache pa 2 95 chechapa ma 3 83 chamapa ma 4 92 mapache pa 5 40 chamache che 6 42 pachama pa 7 75 mapacha che 8 65 machepa che 9 55 chachema che 10 18 chapama che # ℹ 90 more rows sample(c(\u0026#34;ma\u0026#34;, \u0026#34;pa\u0026#34;, \u0026#34;che\u0026#34;, \u0026#34;cha\u0026#34;), 3, F) |\u0026gt; paste(collapse = \u0026#34;\u0026#34;) [1] \u0026quot;mapache\u0026quot; Detección de datos anómalos Para identificar los outliers, utilizaremos el criterio del rango intercuartílico. El rango intercuartílico se calcula con la función IQR() y es el rango de los datos entre el primer y tercer cuartil (el percentil 25 y el percentil 75; es decir, la diferencia entre el dato ubicado en el 75% mayor y el 25% mayor de la distribución de los datos).\nEn otras palabras, el IQR es una cifra que indica qué tanta distancia existe en la \u0026ldquo;mitad\u0026rdquo; de tus datos, si los esparcieras todos en una distribución, como aparece en el siguiente gráfico1.\nLuego, el rango intercuartílico se multiplica por 1,5, y se suma al valor del tercer cuartil (percentil 75), de modo que se identifiquen como outliers los datos que sean mayores2 al tercer cuartil más 1,5 veces el rango intercuartílico. Puedes calcular cualquier percentil de un vector o columna con la función quantile(x, .75), donde el número identifica al percentil.\nAplicamos el cálculo a la variable valor:\ndatos_outliers \u0026lt;- datos |\u0026gt; mutate(umbral = quantile(valor, 0.75) + 1.5 * IQR(valor)) Esto nos dará una cifra que operará como el umbral respecto del cual se clasificarán los outliers. Es conveniente calcular este umbral dentro del dataframe, porque si queremos calcular outliers desagregados por otra variable, simplemente mantenemos la fórmula y agregamos antes un group_by() para realizar el cálculo por grupos.\nCrearemos una columna que simplemente nos diga si los valores son mayores o menores al umbral:\ndatos_outliers \u0026lt;- datos_outliers |\u0026gt; mutate(outlier = valor \u0026gt;= umbral) datos_outliers # A tibble: 100 × 5 valor nombre grupo umbral outlier \u0026lt;int\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;lgl\u0026gt; 1 45 chapache pa 167 FALSE 2 95 chechapa ma 167 FALSE 3 83 chamapa ma 167 FALSE 4 92 mapache pa 167 FALSE 5 40 chamache che 167 FALSE 6 42 pachama pa 167 FALSE 7 75 mapacha che 167 FALSE 8 65 machepa che 167 FALSE 9 55 chachema che 167 FALSE 10 18 chapama che 167 FALSE # ℹ 90 more rows Obtenemos la variable outlier con TRUE si es outlier, y FALSE si no lo es.\nAhora vamos a visualizar estos datos con el paquete {ggplot2}. Si necesitas una introducción a esta librería de visualización de datos, te recomiendo revisar este tutorial, donde explicamos en mayor detalle varias de estas visualizaciones.\nGráfico de cajas o boxplot El boxplot es una visualización que en su mismo diseño incluye la opción de mostrar los outliers, así que se trata de la opción natural para este tipo de visualizaciones exploratorias. Así que con esto concluye este tutorial. Bromita 🥰\ndatos_outliers |\u0026gt; ggplot() + aes(x = valor, y = 1) + # gráfico de boxplot geom_boxplot(alpha = 0.4, fill = \u0026#34;black\u0026#34;, outlier.color = \u0026#34;#F94C6A\u0026#34;, outlier.size = 4, outlier.alpha = 0.7) + # configuración de outliers # temas theme_minimal() + scale_y_continuous(expand = expansion(c(0.5, 0.5))) # aumentar margen del eje vertical En un boxplot, los puntos al extremo de la caja representan los casos anómalos.\nPero la gracia de este tutorial es entrenar nuestras capacidades de visualización de datos, así que veamos otras formas de visualizarlos:\nGráfico de puntos Una forma sencilla de visualizar outliers sería simplemente visualizar las observaciones del dataser como puntos, coloreando los puntos si son outliers.\ndatos_outliers |\u0026gt; ggplot() + aes(x = valor, y = 1, color = outlier) + # gráfico de puntos geom_point(size = 4, alpha = 0.6) + # escala de colores scale_color_manual(values = c(\u0026#34;black\u0026#34;, \u0026#34;#F94C6A\u0026#34;)) + # temas theme_minimal() + # ocultar leyenda guides(color = guide_none(), y = guide_none()) Puntos con dispersión Como existe una concentración densa en parte de la distribución, podemos usar la función geom_jitter() para que los puntos se dispersen verticalmente (width = 0, porque si se dispersan horizontalmente se ubicarían incorrectamente con respecto a su valor)\ndatos_outliers |\u0026gt; ggplot() + aes(x = valor, y = 1, color = outlier) + # gráfico de puntos con dispersión geom_jitter(size = 4, alpha = 0.6, width = 0) + scale_color_manual(values = c(\u0026#34;black\u0026#34;, \u0026#34;#F94C6A\u0026#34;)) + theme_minimal() + guides(color = guide_none(), y = guide_none()) Gráfico de violín El gráfico de violín también nos permite observar la distribución de los datos, pero por sí solo no nos muestra las observaciones exactas, por lo que no entrega información certera sobre los outliers, sino que nos da indicios de que la distribución de los datos tiene colas que podrían contener outliers.\ndatos_outliers |\u0026gt; ggplot() + aes(x = valor, y = 1) + # gráfico de violín geom_violin(fill = \u0026#34;black\u0026#34;, alpha = 0.4) + theme_minimal() + guides(y = guide_none()) # ocultar eje y Combinar visualizaciones Una buena opción es combinar las visualizaciones anteriores en una sola. De fondo podemos poner la distribución de los datos con geom_violin(), y encima poner los puntos de las observaciones; para las observaciones normales podemos usar geom_jitter() para dispersar los datos, y para los outliers, como son poquitos, geom_point() para que se ubiquen en concordancia con la distribución del violín.\ngrafico_outliers \u0026lt;- datos_outliers |\u0026gt; ggplot() + aes(x = valor, y = 1, color = outlier) + # gráfico de violín geom_violin(aes(y = 1, x = valor), inherit.aes = F, alpha = 0.2, lwd = 0.1, fill = \u0026#34;black\u0026#34;) + # puntos para los outliers geom_point(data = ~filter(.x, outlier), # sólo para observaciones que son outlier size = 4, alpha = 0.6) + # puntos dispersados para el resto de los datos geom_jitter(data = ~filter(.x, !outlier), # sólo para observaciones que no son outlier size = 4, alpha = 0.6) + # escala de colores scale_color_manual(values = c(\u0026#34;black\u0026#34;, \u0026#34;#F94C6A\u0026#34;)) + # temas theme_minimal() + theme(axis.title = element_blank()) + guides(color = guide_none(), y = guide_none()) + scale_y_continuous(expand = expansion(c(0.2, 0.2))) # aumentar margen del eje vertical grafico_outliers Para combinar estas visualizaciones, aprovechamos la capacidad de {ggplot2} de especificar los datos que se usan en cada capa o geometría de la visualización por medio del argumento data. Normalmente, el argumento data se rellena por defecto con los datos que entregamos a la función ggplot(), pero si especificamos el argumento podemos hacer que cada capa use datos completamente distintos. En nuestro caso, no queremos datos distintos, sino aplicar filtros a los datos de cada capa, lo que se logra con data = ~filter(.x, ...)3, donde ... sería el filtro que necesitemos. En la visualización anterior, queremos que geom_point() sólo muestre los datos que son outlier == FALSE, y que geom_jitter() sólo muestre los datos que son outlier == TRUE4.\ndata = ~filter(.x, outlier) Etiquetas de texto Al identificar outliers, una buena opción es mostrar etiquetas de texto para estos casos con geom_text(). De este modo podemos identificar exactamente a qué observaciones corresponden las anomalías. En el caso de que fueran muchas etiquetas, podemos usar geom_text_repel() del paquete {ggrepel} para que las etiquetas de texto se acomoden si es que caen encima de otras.\nlibrary(ggrepel) grafico_outliers \u0026lt;- grafico_outliers + # agregar texto al gráfico anterior ggrepel::geom_text_repel(data = ~filter(.x, outlier), # sólo para observaciones que son outlier aes(label = nombre), # variable con el texto a mostrar fontface = \u0026#34;bold\u0026#34;, size = 4, point.padding = 4, angle = 90, hjust = 0) grafico_outliers Ahora sabemos que mapacha, pachache, chepama y chachema son outliers 🤨\nDividir por grupos Otra forma de afinar el análisis es separar la visualización en los valores de la variable de agrupación que tengamos con facet_wrap(). En este caso, como la variable grupo tiene 3 valores posibles, se multiplica la visualización por tres.\ngrafico_outliers \u0026lt;- grafico_outliers + # separar gráfico en facetas según la variable grupo facet_wrap(~grupo, ncol = 1, scales = \u0026#34;fixed\u0026#34;) + theme(strip.text = element_text(face = \u0026#34;bold\u0026#34;)) # título de facetas en negrita grafico_outliers Gráfico interactivo Para mejorar la exploración de los datos podemos convertir fácilmente cualquier gráfico de {ggplot2} en gráficos interactivos gracias al paquete {ggiraph}. Principalmente, lo que agregaremos son tooltips o cajas emergentes que aparecerán cuando se pose el cursor sobre un punto del gráfico y que muestren información extra.\nCon {ggiraph} solamente se requieren cambios mínimos para volver interactivo cualquier gráfico. Entre ellos es agregar _interactive a las funciones que crean las geometrías del gráfico:\nPasar de geom_point() a geom_point_interactive() Pasar de geom_jitter() a geom_jitter_interactive() Habiendo hecho este cambio, dentro de las geometrías geom_x_interactive() ahora podremos definir la estética tooltip dentro de aes() para que aparezca contenido cuando se ponga el cursor sobre los elementos del gráfico. En este caso, haremos que los tooltips muestren el nombre de la observación y su valor, y la palabra outlier si corresponde. También podemos agregar html para dar estilo al texto; por ejemplo, las etiquetas \u0026lt;b\u0026gt; para letra negrita.\nSi queremos que los puntos del gráfico se iluminen o cambien de color al poner el cursor encima, adicionalmente tenemos que definir la estética data_id que identifique de forma única los elementos del gráfico (puede servir para hacer que varios se iluminen al mismo tiempo si elijes una variable que no identifique de forma única los valores).\nCopiamos el código anterior y hacemos los cambios apropiados al gráfico:\nlibrary(ggiraph) grafico_outliers_interactivo \u0026lt;- datos_outliers |\u0026gt; ggplot() + aes(x = valor, y = 1, color = outlier, # variable que identifica de forma única a los elementos del gráfico data_id = nombre) + geom_violin(aes(y = 1, x = valor), inherit.aes = F, alpha = 0.2, lwd = 0.1, fill = \u0026#34;black\u0026#34;) + # puntos para los outliers ggiraph::geom_point_interactive(data = ~filter(.x, outlier), # texto \u0026#34;outlier\u0026#34; con el valor aes(tooltip = paste(\u0026#34;\u0026lt;b\u0026gt;outlier:\u0026lt;/b\u0026gt; \u0026#34;, valor, sep = \u0026#34;\u0026#34;)), size = 4, alpha = 0.6) + # puntos dispersados para el resto de los datos ggiraph::geom_jitter_interactive(data = ~filter(.x, !outlier), # nombre de observación y valor aes(tooltip = paste(\u0026#34;\u0026lt;b\u0026gt;\u0026#34;, nombre, \u0026#34;:\u0026lt;/b\u0026gt; \u0026#34;, valor, sep = \u0026#34;\u0026#34;)), size = 4, alpha = 0.6) + ggrepel::geom_text_repel(data = ~filter(.x, outlier), aes(label = nombre), fontface = \u0026#34;bold\u0026#34;, size = 4, point.padding = 4, angle = 90, hjust = 0) + scale_color_manual(values = c(\u0026#34;black\u0026#34;, \u0026#34;#F94C6A\u0026#34;)) + theme_minimal() + theme(axis.title = element_blank()) + guides(color = guide_none(), y = guide_none()) + scale_y_continuous(expand = expansion(c(0.2, 0.2))) # aumentar margen del eje vertical Finalmente, para permitir la interactividad del gráfico, debemos generarlo con la función girafe(), la cual recibe el objeto con el gráfico además de varias opciones para personalizar la visualización:\ngirafe(ggobj = grafico_outliers_interactivo, # dimensiones del gráfico width_svg = 9, height_svg = 4, # opciones para los tooltips options = list( # ocultar barra de opciones opts_toolbar(hidden = \u0026#34;selection\u0026#34;, saveaspng = FALSE), # color al poner el cursor encima opts_hover(css = \u0026#34;fill: white; stroke: black;\u0026#34;), # personalizar la apariencia del tooltip opts_tooltip(opacity = 0.7, use_fill = TRUE, css = \u0026#34;font-family: sans-serif; font-size: 70%; color: white; padding: 4px; border-radius: 5px;\u0026#34;)) ) Toca o posa el cursor sobre un punto para ver la información extra! ¿Puedes encontrar el punto que dice pamache? 🦝\nEl código para este gráfico está disponible en este Gist.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nEn este ejemplo solamente buscaremos outliers que estén por sobre la distribución de los datos, pero si queremos buscar datos outliers por debajo; es decir, valores pequeños anómalos, la fórmula es la misma pero restando el 1.5 * IQR al primer cuartil.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nEsto funciona porque, al anteponer la colita de chancho (~) a la función, creamos una función lambda que reciba los datos directamente sin tener que especificar el nombre del objeto (conveniente, por ejemplo, si pasaste directo de modificar los datos a hacer el gráfico sin crear un objeto intermedio).\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nEn R, decir variable == TRUE es lo mismo que decir simplemente variable, porque variable es TRUE; o sea que puedes hacer filter(variable) en vez de filter(variable == TRUE), y de la misma forma, filter(!variable) en vez de filter(variable == FALSE).\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-06-18T00:00:00Z","excerpt":"Los datos anómalos o _outliers_ son datos que se alejan considerablemente de los demás. Crearemos un dataset simulando datos outliers y luego mostraremos algunas formas de visualizarlos en `{ggplot2}`, incluyendo un gráfico interactivo con `{ggiraph}` donde podemos poner el cursor sobre las observaciones del gráfico para obtener más información.","href":"https://bastianoleah.netlify.app/blog/2025-06-18/","tags":"limpieza de datos ; ggplot2 ; gráficos ; visualización de datos ; estadística","title":"Gráficos para identificar datos outliers o anómalos en R"},{"content":"La mayoría de las funciones de R, así como R mismo, se ejecutan en un único proceso dentro de tu computadora, dado que R es un software de un sólo hilo (single threaded).\nLos computadores modernos en general tienen entre 2 y 8 núcleos (cores), algunos incluso muchos más. Una mayor cantidad de núcleos o procesadores permite a tu computadora hacer más operaciones paralelas. Por ejemplo, si tu computador tiene 6 núcleos y R está procesando datos, R usará 1 núcleo al 100%, y te quedarán 5 núcleos en desuso, que podrían estar ejecutando otras tareas. En otras palabras, R está usando sólo el 100% de tu computador, cuando podría estar usando el 600%.\nExisten programas que son capaces de distribuir cargas de procesamiento en múltiples núcleos de tu computador, lo cual les permite terminar sus tareas más rápido o realizar tareas más complejas. Siguiendo el ejemplo, si tienes 6 núcleos y una tarea que usa 1 núcleo se demora 30 segundos en realizarse, en teoría podrías usar los 6 núcleos para realizar la tarea en una fracción del tiempo original (~5 segundos).\nPero esto asume varias cosas:\nque es posible separar en partes la tarea que las partes de la tarea se pueden procesar simultáneamente1 que procesar paralelamente la tarea trae beneficios de rendimiento2. Asumiendo que lo anterior se cumple, podemos realizar cálculos a través de múltiples procesadores (multicore) en R, aprovechando la totalidad de la capacidad de cómputo de nuestros computadores para optimizar el rendimiento de nuestros procesos.\nEjemplo Usaremos un conjunto de datos que contiene 10.000 noticias de prensa digital chilena; es decir, que contiene muchísimo texto. Para hacer el ejemplo más ilustrativo de un problema de procesamiento de grandes volúmenes de datos, multiplicaremos la base de datos para que sea 20 veces más grande, y así se entienda la necesidad de optimización.\n# cargar datos desde repositorio prensa \u0026lt;- readr::read_csv2(\u0026#34;https://github.com/bastianolea/prensa_chile/raw/main/datos/prensa_datos_muestra.csv\u0026#34;) # para más información: https://github.com/bastianolea/prensa_chile # multiplicar cantidad de datos prensa_b \u0026lt;- rep(list(prensa), 20) |\u0026gt; purrr::list_rbind() # ver datos prensa_b # A tibble: 200,000 × 4 titulo cuerpo fuente fecha \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;date\u0026gt; 1 \u0026#34;Hombre de 66 años fue asesinado en plena vía públi… \u0026#34;El h… Coope… 2024-07-21 2 \u0026#34;Revive el cuarto capítulo de Indecisos: Candidatos… \u0026#34;Este… Megan… 2024-09-03 3 \u0026#34;Menos personas circulando y miedo de los residente… \u0026#34;Poca… Megan… 2024-04-08 4 \u0026#34;CEO de Walmart Chile sostiene que si la empresa no… \u0026#34;Hace… The C… 2024-12-22 5 \u0026#34;Pdte. Boric entregó mensaje por la muerte de Piñer… \u0026#34;El p… CNN C… 2024-02-06 6 \u0026#34;Encuentran dos cadáveres en un canal de regadío en… \u0026#34;Pers… Publi… 2024-07-13 7 \u0026#34;Magisterio denuncia \u0026#39;abandono de la educación públ… \u0026#34;Carl… Coope… 2024-02-21 8 \u0026#34;“El día que la democracia triunfó sobre la dictadu… \u0026#34;Los … CNN C… 2024-10-05 9 \u0026#34;Accidente de tránsito deja un fallecido y tres her… \u0026#34;En h… T13 2024-12-22 10 \u0026#34;Dos personas resultan baleadas tras intentar evita… \u0026#34;Aton… Emol 2024-11-02 # ℹ 199,990 more rows Tenemos un dataset con 200 mil filas, donde cada fila es una noticia, y en la columna cuerpo está el texto completo de cada noticia.\nEjemplo de cálculo con 1 procesador El objetivo va a ser detectar noticias que contengan un conjunto de palabras en su cuerpo, contar cuántas palabras están presentes en cada noticia, y luego filtrar los datos para dejar sólo las noticias que tengan x cantidad de términos.\nlibrary(dplyr) library(stringr) # lista de términos a buscar términos \u0026lt;- \u0026#34;delincuen(cia|te)|crimen|criminal|inseguridad|deli(to|ctual)|hurto|robo|asalt(o|ante)|narco|homicidio|asesina(to|do)\u0026#34; tictoc::tic() prensa_delincuencia \u0026lt;- prensa_b |\u0026gt; # extraer términos desde cada noticia, si es que están presentes mutate(conceptos = str_extract_all(cuerpo, términos)) |\u0026gt; # contar cantidad de términos presentes en cada noticia mutate(n_conceptos = lengths(conceptos)) |\u0026gt; # filtrar para que tengan más de x términos filter(n_conceptos \u0026gt; 2) tictoc::toc() Medimos el tiempo que se demora en realizar esta operación usando las funciones tic() y toc() del paquete {tictoc}.\nEn mi computador R se demoró 70 segundos en buscar los términos entre las 200.000 noticias (varios millones de palabras en total) y luego hacer el conteo y filtrado.\nUso de recursos con 1 procesador Como vemos en los gráficos, solamente 1 núcleo del computador mostró uso de recursos, y el resto de los núcleos permanecieron inactivos. Esto significa que R no está aprovechando la capacidad de procesamiento de tu computador, y por lo tanto, el cálculo se demora más de lo que podría. En otras palabras, si R ocupa 1 núcleo y sobran 5 núcleos en desuso, significa que podrías ejecutar 5 sesiones de R más, de forma paralela.\nEjemplo de cálculo con 6 procesadores Para realizar el cálculo optimizando para el uso de múltiples procesadores, primero usamos la función split() para dividir los datos en varias partes iguales. Esto es equivalente a lo que mencionamos antes sobre dividir una tarea en partes.\nLuego, vamos a usar la función future_map() del paquete {furrr}, Esta función es similar a map() de {purrr}, que permite iterar sobre tus datos, pero al agregarle future_ hará que cada iteración se ejecute en un proceso separado, aprovechando los múltiples núcleos de tu computador. En otras palabras, {furrr} creará 6 sesiones de R, y cada una procesará su parte de los datos.\nCargamos los paquetes que posibilitan esta optimización:\nlibrary(purrr) library(furrr) Definimos la cantidad de procesadores que usaremos:3\nplan(multisession, workers = 6) Modificamos el código del cálculo levemente para pedirle que lo ejecute a cada una de las partes de nuestro dataset, recordando que los datos quedaron divididos en una lista que contiene 6 elementos, cada uno con una parte de los datos originales:\ntictoc::tic() # benchmark prensa_delincuencia \u0026lt;- prensa_b |\u0026gt; # separar el dataframe en una lista con x cantidad de dataframes de la misma cantidad de filas split(1:6) |\u0026gt; # calcular multiprocesador, una parte por cada procesador future_map(\\(parte) { parte |\u0026gt; mutate(conceptos = str_extract_all(cuerpo, términos)) |\u0026gt; mutate(n_conceptos = lengths(conceptos)) |\u0026gt; filter(n_conceptos \u0026gt; 2) }) |\u0026gt; # volver a unir los resultados en un solo dataframe list_rbind() tictoc::toc() El proceso se demora 18 segundos, apenas un 25% de lo que se demoró antes! En el monitor de actividad vemos claramente la diferencia en el aprovechamiento de los recursos computacionales:\nUso de recursos con 6 procesadores Notamos que todos los procesadores del computador están al 100%, a diferencia de antes. Si revisamos los procesos en el computador durante el cálculo, vemos que, aparte del proceso principal rsession, se crearon 6 procesos R que procesan los datos de forma paralela:\nEn conclusión, vimos que al usar 6 procesadores, obtenemos resultados 400%4 más rápidos, con tan solo haber agregado un par de líneas.\nNo todas las tareas son posibles de procesar de forma paralela, por ejemplo, 6 personas no se demoran menos en contar hasta 100 porque sería imposible.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nNo todas las tareas escalan de la misma forma; por ejemplo, si la tarea es lenta porque tiene que leer muchos datos, no se va a realizar más rápido por leer simultáneamente los datos, porque el cuello de botella es la velocidad de lectura y no el procesador.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nEl número de workers (subprocesos) depende de tu computador; en este caso pondré 6, pero si tu computador tiene 8 entonces pone 8, aunque hay gente que recomienda usar n-1 para dejar un núcleo libre para el sistema operativo.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n400% más rápido porque se demoró 18 segundos en vez de 70 segundos al usar 6 núcleos en vez de 1. Pero entonces, ¿por qué no 600% más rápido, si eran 6 procesadores? Esto se debe a que un procesamiento de este tipo tiene el costo extra (overhead) de tener que duplicar los datos y el entorno para cada subproceso, lo cual también puede significar un aumento en el uso de la memoria.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-06-12T00:00:00Z","excerpt":"Realiza cálculos a través de múltiples procesadores en R, aprovechando la totalidad de la capacidad de cómputo de tu computador para optimizar el rendimiento de tus procesos. Si tienes que trabajar con bases de datos muy grandes, puedes acelerar el cálculo con tan sólo un par de líneas, usando el paquete `{furrr}`.","href":"https://bastianoleah.netlify.app/blog/furrr_multiprocesador/","tags":"procesamiento de datos ; optimización ; loops ; purrr","title":"Procesamiento de datos multiprocesador en R con {furrr}"},{"content":"Hoy está de cumpleaños el paquete {ggplot2} para visualización de datos en R! Este paquete es indispensable para explorar el mundo de los datos, ya que su filosofía o gramática está muy bien lograda, y su flexibilidad posibilita crear casi lo que desees, siempre siguiendo principios firmemente sostenidos en un enfoque de ciencia de datos.\nSe trata de una herramienta que uso día a día, así que con mucho cariño (🤪) le hice un gráfico de mini celebración en 20 líneas 💜\nlibrary(ggplot2) library(dplyr) library(ggtext) library(ragg) tibble(x = sample(1:18), y = sample(1:18)) |\u0026gt; ggplot() + geom_text(aes(label = \u0026#34;⭐️\u0026#34;, y, x), size = 4) + geom_text(aes(label = \u0026#34;🎈\u0026#34;, x, y, size = y)) + annotate(\u0026#34;text\u0026#34;, y = -1, x = 18/2, hjust = 0.5, label = \u0026#34;🎂\u0026#34;, size = 12) + theme_void() + coord_cartesian(xlim = c(0, 18), ylim = c(-2, 20)) + scale_size(range = c(4, 9)) + guides(size = guide_none()) + labs(title = \u0026#34;Feliz cumpleaños \u0026lt;span style=\u0026#39;font-family: Menlo;\u0026#39;\u0026gt;{ggplot2}\u0026lt;/span\u0026gt; 🎉\u0026#34;) + theme(plot.title = element_markdown(family = \u0026#34;Helvetica\u0026#34;), plot.margin = margin(8, 8, 8, 8)) El gráfico usa números al azar para poner globos y estrellas. Los globitos crecen con respecto al eje y, mientras que la torta simplemente se ubica abajo y al centro.\nPartió de una idea mucho más sencilla que comenté en una publicación de celebración que hizo Hadley Wickham, desarrollador de {ggplot2}, {dplyr} y muchos más paquetes del Tidyverse.\nEl original era así de simple:\ntibble(a = sample(1:18, 18), b = sample(1:18, 18)) |\u0026gt; ggplot() + aes(a, b, size = b) + geom_text(aes(label = \u0026#34;🎈\u0026#34;)) + theme_void() + guides(size = guide_none()) Intenta reproducirlo en tu R! Si no te resulta, puede deberse a que el backend gráfico por defecto de R tiene problemas con los textos complejos, como los emojis En las configuraciones de RStudio, entra a General y luego en Graphics puedes cambiar el backend por uno como AGG (del paquete {ragg})\nSi quieres aprender {ggplot2} desde cero, revisa este tutorial gratuito que hice, donde te explico a usar esta útil y flexible herramienta desde lo más sencillo, con muchos ejemplos de visualizaciones aplicadas a datos reales.\n","date":"2025-06-11T00:00:00Z","excerpt":"Hoy está de cumpleaños el paquete `{ggplot2}` para visualización de datos en R! Así que le hice un gráfico de mini celebración en 20 líneas.","href":"https://bastianoleah.netlify.app/blog/2025-06-11/","tags":"ggplot2 ; gráficos","title":"Feliz cumpleaños, {ggplot2}!"},{"content":" Uno de beneficios concretos de los avances en inteligencia artificial generativa son las herramientas de autocompletado de código1. Una de estas herramientas es GitHub Copilot, la cual puede integrarse directamente en RStudio para ayudarte a programar en R. En este post mostraré algunos casos de uso real donde Copilot me ha servido.\n¿Para qué sirve? Copilot es un servicio de autocompletado de código que puede integrarse directamente en RStudio. Sirve para recibir sugerencias de código mientras programas, lo que puede ayudarte a escribir código más rápido. Esta herramienta utiliza modelos de lenguaje entrenados con una gran cantidad de código fuente disponible públicamente, lo que le permite ofrecer sugerencias contextuales basadas en el código que ya has escrito.\nInstalación En las opciones globales de RStudio, entra a el último ítem, titulado Copilot. Presiona Activar, y se te pedirá ingresar un código de verificación en el sitio web de GitHub que aparecerá indicado, usando tu cuenta de GitHub. Visita aquí para más instrucciones.\n¿Cómo funciona? Luego de activar esta herramienta, simplemente empiezas a programar, y cada vez que te detengas, el modelo intentará autocompletar lo que estés escribiendo. Frente a tu cursor aparecerá una sugerencia de código basada en lo que ya hayas escrito y en los contenidos de tu script. El modelo es capaz de predecir lo que quieres programar, según el lugar en el documento en que estés. Presionando la tecla tabulador podrás aceptar la sugerencia e incluirla instantáneamente en tu código.\nEn la práctica, esto te hará ahorrar tiempo programando tareas simples y repetitivas, ya que en la mayoría de estos casos el modelo logrará adivinar lo que intentas hacer. Pero si intentas hacer algo fuera de lo común o demasiado específico, usualmente el modelo se queda atrás. Otras veces hay un tradeoff entre pequeños errores y el beneficio de tener que escirbir menos.\nLa herramienta funciona mucho mejor si es que agregas comentarios a tu código, ya sea indicando paso por paso a lo que intentas hacer, o antecediendo los bloques de código con un comentario que describa tu objetivo. Incluso, muchas veces basta con escribir un comentario con lo que intentas hacer para que el modelo lo haga por ti.\nTambién tienes la opción de permitir que el modelo tenga acceso a todos los archivos de tu proyecto. Esto mejora significativamente las sugerencias que te hace el modelo, ya que le entrega una mayor comprensión de lo que intentas hacer. Activa esta opción en las opciones globales de RStudio (index project files).\nEjemplos Luego de varias semanas utilizándolo, puedo decir que los resultados suelen ser muy positivos.\nCopilot entiende el contexto en que escribes; por ejemplo, acá sabe que estoy creando un input para una app Shiny, y lo rellena con la columna correspondiente del dataset correcto: En este otro ejemplo, rellena las opciones de un input de Shiny con los valores que definí cientos de líneas arriba en el mismo script, ahorrándome el tener que moverme por el script para encontrar los valores: En este caso, el modelo sugiere el uso de la función get() para crear un objeto que contiene a otro objeto basado en el nombre de este segundo objeto, algo bien rebuscado pero que intuyó a partir de lo que tenía escrito al momento: Luego de lo anterior, el modelo supo que quería usar el objeto creado para seleccionar columnas de un dataframe reactivo! Acá me sorprendió mucho, porque sólo con iniciar la creación de unas columnas para la app, el modelo recomienda la estructura entera de tres columnas con tres selectores basados en las variables correspondientes, definidas en otra parte del script. Aquí se ve cómo el modelo sugiere contenido cuando declaras lo que estás haciendo con comentarios: Y en este caso, el modelo entiende que estoy dentro de un bloque de {ggplot2}, pero que además sabe que estoy usando {plotly}, por lo que me ayuda a crear correctamente el texto de los tooltips del gráfico: Resulta que todas las décadas de colaboración desinteresada y proyectos de código abierto en plataformas como GitHub, de preguntas y respuestas en StackOverflow, y miles de blogs de desarrolladores solidarios, terminaron siendo una mina de oro para entrenar modelos de lenguaje. Producto de la anterior es que hoy proliferan herramientas de inteligencia artificial con abundantes conocimientos de programación.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-06-09T00:00:00Z","excerpt":"Uno de beneficios concretos de los avances en inteligencia artificial generativa son las herramientas de autocompletado de código. Una de estas herramientas es GitHub Copilot, un servicio de autocompletado de código que puede integrarse directamente en RStudio para ayudarte a programar en R. En este post mostraré algunos casos de uso real donde Copilot me ha servido.","href":"https://bastianoleah.netlify.app/blog/github_copilot/","tags":"consejos ; inteligencia artificial","title":"Sugerencias y autocompletado de código con GitHub Copilot"},{"content":"Una de las gracias de generar documentos en Quarto es que podemos combinar la redacción con el código. Pero esto puede ir más allá que simplemente escribir un párrafo de texto y seguido de un párrafo de código. Podemos usar código para literalmente generar texto, títulos y más.\nEn otros tutoriales vimos cómo podemos incluir resultados del código dentro de nuestros párrafos de texto, por ejemplo, para que una cifra que esté dentro de una oración venga directamente del resultado de un cálculo en vez de tener que escribirle de forma manual. Pero en esta guía vamos a ver cómo podemos programar la generación masiva de títulos, párrafos y gráficos en base a una iteración, bucle o loop.\nGenerar contenido del reporte desde loops Dentro de un chunk de código, podemos usar funciones que entreguen como output código markdown. Esto nos servirá para construir un documento a partir de un loop o iteración. De este modo, podemos usar nuestros datos para generar una cantidad indeterminada de títulos, subtítulos, párrafos de textos, gráficos o más, que se generarán automáticamente.\nUsaremos el paquete {pander} para que el chunk de R retorne contenido en Markdown que Quarto interpretará como parte del documento gracias a la opción #| results: \u0026quot;asis\u0026quot; que tenemos que definir al principio del chunk:\nlibrary(dplyr) library(pander) Como primer ejemplo, tendremos un vector que contiene tres elementos de texto. Para generar el contenido de nuestro documento, queremos generar un título y un párrafo por cada elemento de texto del vector. Entonces, hacemos un loop que vaya iterando por cada elemento del vector, y para cada elemento va a crear un título, un pequeño párrafo, e insertar un emoji:\nanimales \u0026lt;- c(\u0026#34;gato\u0026#34;, \u0026#34;mapache\u0026#34;, \u0026#34;castor\u0026#34;) # unir nombres de columnas en un texto for (animal in animales) { # crear un título pandoc.header(paste(\u0026#34;Título:\u0026#34;, animal), level = 4) # crear un párrafo pandoc.p( paste(\u0026#34;Este texto fue generado por `{pander}` dentro de un _for loop_ para el animalito\u0026#34;, animal) ) # poner un animalito pandoc.p( case_when(animal == \u0026#34;gato\u0026#34;~ \u0026#34;🐈‍⬛\u0026#34;, animal == \u0026#34;mapache\u0026#34; ~ \u0026#34;🦝\u0026#34;, animal == \u0026#34;castor\u0026#34; ~ \u0026#34;🦫\u0026#34;) ) } Título: gato Este texto fue generado por {pander} dentro de un for loop para el animalito gato\n🐈‍⬛\nTítulo: mapache Este texto fue generado por {pander} dentro de un for loop para el animalito mapache\n🦝\nTítulo: castor Este texto fue generado por {pander} dentro de un for loop para el animalito castor\n🦫\nComo podemos ver, el contenido anterior se generó automáticamente como resultado del loop.\nLas funciones pandoc.header() y pandoc.p() sumadas a la opción results: asis nos ayudan a que el chunk retorne texto en markdown. Usando los mismos principios, podemos programar un loop que vaya retornando títulos, textos, y más, para que el contenido del documento se vaya escribiendo solo. Si queremos incluir gráficos, tenemos que imprimirlos explícitamente con plot().\nEn este segundo ejemplo, iteraremos por los valores de una variable de un dataframe (la variable Species de iris, que tiene 3 valores posibles) y por cada valor crearemos un título, un gráfico que destaque dicho valor, y un texto que indique una cifra relacionada al valor. Luego del código veremos el contenido resultante, generado de forma automática:\nlibrary(ggplot2) library(pander) library(dplyr) library(glue) # para pegar texto library(gghighlight) # para destacar valores en gráficos # iteración for (especie in unique(iris$Species)) { # título pandoc.header(paste(\u0026#34;Especie\u0026#34;, especie), level = 3) # definir color color_especie \u0026lt;- case_when(especie == \u0026#34;setosa\u0026#34; ~ \u0026#34;#ff006e\u0026#34;, especie == \u0026#34;virginica\u0026#34; ~ \u0026#34;#8338ec\u0026#34;, especie == \u0026#34;versicolor\u0026#34; ~ \u0026#34;#3a86ff\u0026#34;) # crear gráfico grafico \u0026lt;- iris |\u0026gt; ggplot() + aes(x = Sepal.Length, y = Sepal.Width) + geom_point(size = 3, color = color_especie) + theme_void() + gghighlight::gghighlight(Species == especie) # imprimir gráfico plot(grafico) # crear texto largo_petalo \u0026lt;- iris |\u0026gt; filter(Species == especie) |\u0026gt; slice_max(Petal.Length) |\u0026gt; pull(Petal.Length) |\u0026gt; unique() # imprimir texto pandoc.p( # paste(\u0026#34;La observación más alta en largo de pétalos en la especie\u0026#34;, especie, \u0026#34;es:\u0026#34;, largo_petalo) glue::glue(\u0026#34;La observación más alta en largo de pétalos en la especie **{especie}** es: {largo_petalo}\u0026#34;) ) # línea divisoria # pandoc.horizontal.rule() } Especie setosa La observación más alta en largo de pétalos en la especie setosa es: 1.9\nEspecie versicolor La observación más alta en largo de pétalos en la especie versicolor es: 5.1\nEspecie virginica La observación más alta en largo de pétalos en la especie virginica es: 6.9\nSi no te funciona, recuerda que el chunk tiene que tener #| results: \u0026quot;asis\u0026quot; definido al principio para que el output sea el correcto.\nUsando esta técnica podemos producir documentos muy extensos con poco código, lo que nos vuelve más eficientes y también nos ayuda para actualizar y retocar los reportes, dado que un cambio hecho una sola vez se replica en todos los elementos generados por el loop.\nEsto puede sernos útil si es que estamos generando un documento en el que estamos presentando resultados y tenemos que repetir varias veces contenido similar para los distintos valores de una variable, o si tenemos que mostrar muchos gráficos similares para distintas variables, etc.\n","date":"2025-06-08T00:00:00Z","excerpt":"\u003cp\u003eUna de las gracias de generar documentos en Quarto es que podemos combinar la redacción con el código. Pero esto puede ir más allá que simplemente escribir un párrafo de texto y seguido de un párrafo de código. Podemos usar código para literalmente generar texto, títulos y más.\u003c/p\u003e\n\u003cp\u003eEn otros tutoriales vimos cómo podemos \n\u003ca href=\"../../../blog/quarto_reportes/#c%c3%b3digo-entre-el-texto\"\u003eincluir resultados del código dentro de nuestros párrafos de texto\u003c/a\u003e, por ejemplo, para que una cifra que esté dentro de una oración venga directamente del resultado de un cálculo en vez de tener que escribirle de forma manual. Pero en esta guía vamos a ver cómo podemos programar la generación masiva de títulos, párrafos y gráficos en base a una \n\u003ca href=\"../../../blog/r_introduccion/r_intermedio/#bucles\"\u003eiteración, bucle o \u003cem\u003eloop\u003c/em\u003e.\u003c/a\u003e\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/quarto_loop/","tags":"quarto ; loops ; automatización ; gráficos","title":"Generar contenido en serie usando loops en un reporte Quarto"},{"content":" Índice Crear variable a partir de la detección de texto Cambiar la capitalización de un texto Expresiones regulares Extraer caracteres desde un texto El paquete {stringr} facilita todo tipo de trabajo que implique texto en R.\nEn general, los datos que vienen como texto suelen necesitar una limpieza previa, y adicionalmente un procesamiento para poder aprovecharlos mejor.\nSigamos un ejemplo con una columna que viene con textos sobre compras en un servicio público:\nlibrary(dplyr) library(stringr) # para trabajar con textos datos \u0026lt;- tibble( texto = c(\u0026#34;Licitación pública N°3432\u0026#34;, \u0026#34;Trato directo #3341\u0026#34;, \u0026#34;Licitación privada 876\u0026#34;, \u0026#34;LICITACION PUBLICA N3430\u0026#34;, \u0026#34;Licitacion publica 3526 concluida\u0026#34;, \u0026#34;licitación pública 2986 ok\u0026#34;, \u0026#34;sin información\u0026#34;) ) texto Licitación pública N°3432 Trato directo #3341 Licitación privada 876 LICITACION PUBLICA N3430 Licitacion publica 3526 concluida licitación pública 2986 ok sin información Como es de esperar, el texto viene sucio: escrito de distintas maneras, con y sin tildes, con y sin mayúsculas, etc.\nCrear variable a partir de la detección de texto Una primera limpieza de los datos puede ser identificar si un texto específico está o no presente en la variable de texto sucio. Para ello podemos usar la función str_detect(), que retorna TRUE o FALSE si en el texto que se le entrega como primer argumento está presente el texto en su segundo argumento. Por ejemplo:\nstr_detect(\u0026#34;un texto acá muy feo\u0026#34;, \u0026#34;feo\u0026#34;) # sí (TRUE) [1] TRUE str_detect(\u0026#34;un texto acá muy bonito\u0026#34;, \u0026#34;feo\u0026#34;) #no (FALSE) [1] FALSE Aplicamos str_detect() dentro de un mutate() para crear una columna que indique la presencia de un texto a lo largo de la columna de texto:\ndatos |\u0026gt; mutate(licitacion = str_detect(texto, \u0026#34;Licitación\u0026#34;)) # A tibble: 7 × 2 texto licitacion \u0026lt;chr\u0026gt; \u0026lt;lgl\u0026gt; 1 Licitación pública N°3432 TRUE 2 Trato directo #3341 FALSE 3 Licitación privada 876 TRUE 4 LICITACION PUBLICA N3430 FALSE 5 Licitacion publica 3526 concluida FALSE 6 licitación pública 2986 ok FALSE 7 sin información FALSE Vemos que sólo entrega dos TRUE, siendo que en las filas 4, 5 y 6 también debería encontrar coincidencias. Esto se debe a que existen diferencias en las letras minúsculas y mayúsculas.\nCambiar la capitalización de un texto Un problema común con la limpieza de texto es encontrar diferencias en las mayúsculas de las palabras. {stringr} tiene varias funciones para ayudarnos a cambiar textos a mayúsculas, minúsculas, y más.\nstr_to_lower(\u0026#34;HOLA, cómo estás?\u0026#34;) [1] \u0026quot;hola, cómo estás?\u0026quot; str_to_upper(\u0026#34;hola, cómo estás?!\u0026#34;) [1] \u0026quot;HOLA, CÓMO ESTÁS?!\u0026quot; str_to_sentence(\u0026#34;hola, cómo estás?\u0026#34;) [1] \u0026quot;Hola, cómo estás?\u0026quot; Pero a veces necesitamos corregir la capitalización de textos que en su interior contienen palabras que empiezan con mayúsculas, o siglas/acrónimos, en cuyo caso convertir a minúsculas o a oración arruinarían la gramática correcta.\n# texto que no empieza con mayúscula texto \u0026lt;- \u0026#34;mi nombre es Cecilia y trabajo en la ONU.\u0026#34; # agregar mayúscula inicial, pero se pierden las demás mayúsculas str_to_sentence(texto) [1] \u0026quot;Mi nombre es cecilia y trabajo en la onu.\u0026quot; En estos casos podemos realizar un reemplazo de texto con str_replace() que solamente reemplace la primera letra del texto por su versión mayúscula, y deje las demás intactas:\n# reemplazar sólo primera letra de la primera palabra str_replace(texto, \u0026#34;^.\u0026#34;, toupper) [1] \u0026quot;Mi nombre es Cecilia y trabajo en la ONU.\u0026quot; Aquí usamos una expresión regular (^.) para indicar que queremos reemplazar solamente la primera letra del texto (el símbolo ^ indica el inicio del texto, y . indica cualquier símbolo).\nVolviendo al ejemplo de los datos, para corregir la detección de datos con str_detect() cuando hay diferencias de mayúsculas, primero convertimos el texto a minúsculas con str_to_lower(), y buscamos el término en minúsculas:\ndatos |\u0026gt; mutate(licitacion = str_detect(str_to_lower(texto), \u0026#34;licitación\u0026#34;)) # A tibble: 7 × 2 texto licitacion \u0026lt;chr\u0026gt; \u0026lt;lgl\u0026gt; 1 Licitación pública N°3432 TRUE 2 Trato directo #3341 FALSE 3 Licitación privada 876 TRUE 4 LICITACION PUBLICA N3430 FALSE 5 Licitacion publica 3526 concluida FALSE 6 licitación pública 2986 ok TRUE 7 sin información FALSE Obtenemos una coincidencia más! Pero siguen faltando dos casos (casos 4 y 5) que deberían retornar TRUE. En estos casos, el problema está con diferencias en los tildes de las palabras. Para solucionar esto, podemos hacer una búsqueda de texto usando regex.\nExpresiones regulares Las expresiones regulares o regex son formas de escribir patrones de búsqueda, y son soportadas por todas las funciones de {stringr}. Uno de estos patrones es el operador o (|). El operador o puede usarse para encontrar coincidencias con varias palabras distintas separadas con |:\nstr_detect(c(\u0026#34;hola\u0026#34;, \u0026#34;holo\u0026#34;, \u0026#34;holi\u0026#34;), \u0026#34;hola|holi\u0026#34;) [1] TRUE FALSE TRUE En este ejemplo, se coincide con un TRUE tanto el texto hola como holi. Pero en este ejemplo ambas palabras son muy similares; 75% similares, para ser exactos 🤓☝🏼. Podemos poner entre paréntesis los caracteres específicos que varían, para que dentro de una misma palabra se acepten distintos caracteres:\nstr_detect(c(\u0026#34;hola\u0026#34;, \u0026#34;holo\u0026#34;, \u0026#34;holi\u0026#34;), \u0026#34;hol(a|i)\u0026#34;) [1] TRUE FALSE TRUE De este modo, se coincide con la palabra hol seguida tanto de a como de i; es decir, hola y holi.\nFinalmente, también podemos pedirle que coincida una palabra que dentro de ella tenga cualquier caracter:\nstr_detect(c(\u0026#34;hola\u0026#34;, \u0026#34;holo\u0026#34;, \u0026#34;holi\u0026#34;), \u0026#34;hol.\u0026#34;) [1] TRUE TRUE TRUE Siguiendo estos ejemplos para volver a nuestros datos, podemos coincidir texto con y sin tildes al mismo tiempo si usamos regex para especificar que uno o varios caracteres pueden ser distintos; en este caso, la letra o con y sin tilde:\ndatos |\u0026gt; mutate(licitacion = str_detect(str_to_lower(texto), \u0026#34;licitaci(ó|o)n\u0026#34;)) # A tibble: 7 × 2 texto licitacion \u0026lt;chr\u0026gt; \u0026lt;lgl\u0026gt; 1 Licitación pública N°3432 TRUE 2 Trato directo #3341 FALSE 3 Licitación privada 876 TRUE 4 LICITACION PUBLICA N3430 TRUE 5 Licitacion publica 3526 concluida TRUE 6 licitación pública 2986 ok TRUE 7 sin información FALSE Podemos usar el mismo código pero dentro de un ifelse() para que, en vez de TRUE y FALSE, retorne lo que queramos para las condicies verdadera y falsa:\ndatos |\u0026gt; mutate(licitacion = ifelse(str_detect(str_to_lower(texto), \u0026#34;licitaci(ó|o)n\u0026#34;), yes = \u0026#34;Licitación\u0026#34;, no = \u0026#34;Otros\u0026#34;)) # A tibble: 7 × 2 texto licitacion \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Licitación pública N°3432 Licitación 2 Trato directo #3341 Otros 3 Licitación privada 876 Licitación 4 LICITACION PUBLICA N3430 Licitación 5 Licitacion publica 3526 concluida Licitación 6 licitación pública 2986 ok Licitación 7 sin información Otros También podemos usar el operador .* de regex para indicar cualquier cantidad de caracteres entre el texto antes y después del operador. Por ejemplo:\nstr_detect(c(\u0026#34;hola\u0026#34;, \u0026#34;hooooooola\u0026#34;, \u0026#34;ho8787897la\u0026#34;, \u0026#34;hola hola\u0026#34;), \u0026#34;ho.*la\u0026#34;) [1] TRUE TRUE TRUE TRUE En el ejemplo, ho.*la significa coincidir con un texto que tenga ho, cualquier texto, y luego la; por lo tanto, coincide con hola, hooooooola, ho8787897la y cualquier otra variación.\nPodemos usar esto para hacer coincidencias más flexibles:\ndatos |\u0026gt; mutate(tipo = case_when(str_detect(str_to_lower(texto), \u0026#34;lic.*privada\u0026#34;) ~ \u0026#34;Licitación privada\u0026#34;, str_detect(str_to_lower(texto), \u0026#34;lic.*p.blica\u0026#34;) ~ \u0026#34;Licitación pública\u0026#34;, str_detect(str_to_lower(texto), \u0026#34;trato.*directo|contra.*direct\u0026#34;) ~ \u0026#34;Trato directo\u0026#34;) ) # A tibble: 7 × 2 texto tipo \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Licitación pública N°3432 Licitación pública 2 Trato directo #3341 Trato directo 3 Licitación privada 876 Licitación privada 4 LICITACION PUBLICA N3430 Licitación pública 5 Licitacion publica 3526 concluida Licitación pública 6 licitación pública 2986 ok Licitación pública 7 sin información \u0026lt;NA\u0026gt; Si combinamos los aprendizajes hasta el momento, podemos crear una columna nueva que entregue distintos valores dependiendo del texto detectado, gracias a case_when():\ndatos |\u0026gt; # limpiar el texto de antemano mutate(texto_2 = str_to_lower(texto)) |\u0026gt; # detectar licitaciones mutate(licitacion = str_detect(texto_2, \u0026#34;licitaci(ó|o)n\u0026#34;)) |\u0026gt; # detectar si son públicas, privadas, o de otro tipo mutate(tipo = case_when( # si son licitaciones, y si contiene \u0026#34;privada\u0026#34; licitacion \u0026amp; str_detect(texto_2, \u0026#34;privad(o|a)\u0026#34;) ~ \u0026#34;Licitación privada\u0026#34;, # si son licitaciones y si contiene \u0026#34;público\u0026#34; licitacion \u0026amp; str_detect(texto_2, \u0026#34;p(ú|u)blic(a|o)\u0026#34;) ~ \u0026#34;Licitación pública\u0026#34;, # otros valores str_detect(texto_2, \u0026#34;trato directo\u0026#34;) ~ \u0026#34;Trato directo\u0026#34;, # todos los demás que no coincidieron en las condiciones anteriores .default = \u0026#34;Otros\u0026#34;)) # A tibble: 7 × 4 texto texto_2 licitacion tipo \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;lgl\u0026gt; \u0026lt;chr\u0026gt; 1 Licitación pública N°3432 licitación pública n°3432 TRUE Lici… 2 Trato directo #3341 trato directo #3341 FALSE Trat… 3 Licitación privada 876 licitación privada 876 TRUE Lici… 4 LICITACION PUBLICA N3430 licitacion publica n3430 TRUE Lici… 5 Licitacion publica 3526 concluida licitacion publica 3526 co… TRUE Lici… 6 licitación pública 2986 ok licitación pública 2986 ok TRUE Lici… 7 sin información sin información FALSE Otros Extraer caracteres desde un texto Una última alternativa para limpiar estos datos sería extraer texto específico desde la variable de texto. Es decir, encontrar un tipo de texto dentro de otro texto, y solamente dejar ese texto extraído. Por ejemplo: entre un texto extenso, extraer solamente una palabra específica, si es que existe, o extraer solamente los números que esténdentro del texto.\nPara esto podemos usar la función str_extract() combinada con un operador regex para extraer secuencias de números (\\\\d+):\ndatos |\u0026gt; mutate(numero = str_extract(texto, \u0026#34;\\\\d+\u0026#34;)) # A tibble: 7 × 2 texto numero \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Licitación pública N°3432 3432 2 Trato directo #3341 3341 3 Licitación privada 876 876 4 LICITACION PUBLICA N3430 3430 5 Licitacion publica 3526 concluida 3526 6 licitación pública 2986 ok 2986 7 sin información \u0026lt;NA\u0026gt; Para finalizar, unimos todas las técnicas que vimos en este ejemplo, para terminar con una tabla de datos mucho más útil que la que teníamos al inicio!\ndatos_limpios \u0026lt;- datos |\u0026gt; # limpiar el texto de antemano mutate(texto_2 = str_to_lower(texto)) |\u0026gt; # detectar licitaciones mutate(licitacion = ifelse(str_detect(str_to_lower(texto), \u0026#34;licitaci(ó|o)n\u0026#34;), yes = \u0026#34;Licitación\u0026#34;, no = \u0026#34;Otros\u0026#34;)) |\u0026gt; # detectar si son públicas, privadas, o de otro tipo mutate(tipo = case_when( # si son licitaciones, y si contiene \u0026#34;privada\u0026#34; licitacion == \u0026#34;Licitación\u0026#34; \u0026amp; str_detect(texto_2, \u0026#34;privad(o|a)\u0026#34;) ~ \u0026#34;Licitación privada\u0026#34;, # si son licitaciones y si contiene \u0026#34;público\u0026#34; licitacion == \u0026#34;Licitación\u0026#34; \u0026amp; str_detect(texto_2, \u0026#34;p(ú|u)blic(a|o)\u0026#34;) ~ \u0026#34;Licitación pública\u0026#34;, # otros valores licitacion == \u0026#34;Otros\u0026#34; \u0026amp; str_detect(texto_2, \u0026#34;trato directo\u0026#34;) ~ \u0026#34;Trato directo\u0026#34;, # todos los demás que no coincidieron en las condiciones anteriores .default = \u0026#34;Otros\u0026#34;)) |\u0026gt; # extraer números mutate(numero = str_extract(texto, \u0026#34;\\\\d+\u0026#34;), numero = as.numeric(numero)) |\u0026gt; # convertir números a numéricos # eliminar columnas innecesarias select(-contains(\u0026#34;texto\u0026#34;)) licitacion tipo numero Licitación Licitación pública 3432 Otros Trato directo 3341 Licitación Licitación privada 876 Licitación Licitación pública 3430 Licitación Licitación pública 3526 Licitación Licitación pública 2986 Otros Otros NA ","date":"2025-06-08T00:00:00Z","excerpt":"Los datos que vienen como texto suelen necesitar una limpieza previa, y adicionalmente un procesamiento para poder aprovecharlos mejor. En este tutorial usamos el paquete `{stringr}` para limpiar y ordenar unos datos de texto.","href":"https://bastianoleah.netlify.app/blog/stringr_texto/","tags":"limpieza de datos ; texto","title":"Limpieza y recodificación de datos de texto en R con {stringr}"},{"content":" {datapasta} es un paquete para R que te ayuda a copiar y pegar datos desde y hacia R.\nInstala {datapasta} ejecutando el siguiente código:\ninstall.packages(\u0026#34;datapasta\u0026#34;, repos = c(mm = \u0026#34;https://milesmcbain.r-universe.dev\u0026#34;, getOption(\u0026#34;repos\u0026#34;))) Copiar {datapasta} puede ayudarte a compartir fácilmente datos, al convertir tus datos en texto que puedes copiar y pegar en otro lado, o editar manualmente. Usa la función dpasta() sobre un dataframe para hacer que los datos aparezcan como en texto directamente debajo de donde la ejecutaste.\nPor ejemplo, creemos una tabla pequeña:\nlibrary(dplyr) tabla \u0026lt;- iris |\u0026gt; slice_sample(n = 5) |\u0026gt; tibble() tabla # A tibble: 5 × 5 Sepal.Length Sepal.Width Petal.Length Petal.Width Species \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;fct\u0026gt; 1 6.1 3 4.9 1.8 virginica 2 5.2 4.1 1.5 0.1 setosa 3 4.9 3.1 1.5 0.2 setosa 4 5 3 1.6 0.2 setosa 5 6.5 3 5.8 2.2 virginica Imagínate que queremos corregir esta tabla, usarla como ejemplo, o compartirla con alguien. Entonces usamos la función dpasta():\ndatapasta::dpasta(tabla) ¡Magia! Aparecerá el siguiente código en nuestro script:\ntibble::tribble( ~Sepal.Length, ~Sepal.Width, ~Petal.Length, ~Petal.Width, ~Species, 6, 3, 4.8, 1.8, \u0026#34;virginica\u0026#34;, 5.7, 2.9, 4.2, 1.3, \u0026#34;versicolor\u0026#34;, 7.7, 2.8, 6.7, 2, \u0026#34;virginica\u0026#34;, 6.8, 2.8, 4.8, 1.4, \u0026#34;versicolor\u0026#34;, 4.6, 3.2, 1.4, 0.2, \u0026#34;setosa\u0026#34; ) Al ejecutar este código podemos re-crear el dataframe. Es decir, obtenemos los datos como texto que luego podemos usar para crear un nuevo dataframe. Esto tiene varias utilidades:\ntransformar los datos a texto para poder editarlos manualmente (corregir cifras, agregar observaciones) compartir pequeñas tablas con otras personas (adjuntando los datos como texto en un scirpt o correo) corregir manualmente datos pequeños para los que no valdría la pena programar soluciones (por ejemplo, cambiar faltas de ortografía) \u0026ldquo;guardar\u0026rdquo; datos en un script para eliminar la dependencia de un archivo externo, sobre todo si se trata de pocos datos (por ejemplo, para crear un diccionario de las variables o libro de códigos) Este tipo de tablas de datos almacenadas como código se llaman tribble, y su gracia es que muestran los datos tal como los veríamos en un dataframe o planilla, con la conveniencia de que podemos editarlos, incluso agregar nuevas filas o columnas si seguimos su sencilla sintaxis.\nPegar {datapasta} también nos ayuda a pegar en R datos que sacamos desde otro sitio, con la función tribble_paste(). Usando esta función podemos copiar datos de una tabla de Excel o de una página web y pegarlos como un dataframe en R.\n¡Probemos\u0026quot; Selecciona la siguiente tabla1 y cópiala:\nPaís PIB (PPP) PIB (per capita) Brazil 4958.122 23238 Mexico 3395.916 25462 Argentina 1493.423 31379 Colombia 1190.795 22421 Chile 710.195 35146 Peru 643.052 18688 Dominican Republic 336.082 30874 Ecuador 300.122 16578 Guatemala 282.833 15633 Venezuela 223.984 8397 datapasta::tribble_paste() En tu consola aparecerá el código necesario para cargar la tabla copiada en R!\ntibble::tribble( ~PAÍS, ~`PIB.(PPP)`, ~`PIB.(PER.CAPITA)`, \u0026#34;Brazil\u0026#34;, 4958.122, 23238L, \u0026#34;Mexico\u0026#34;, 3395.916, 25462L, \u0026#34;Argentina\u0026#34;, 1493.423, 31379L, \u0026#34;Colombia\u0026#34;, 1190.795, 22421L, \u0026#34;Chile\u0026#34;, 710.195, 35146L, \u0026#34;Peru\u0026#34;, 643.052, 18688L, \u0026#34;Dominican Republic\u0026#34;, 336.082, 30874L, \u0026#34;Ecuador\u0026#34;, 300.122, 16578L, \u0026#34;Guatemala\u0026#34;, 282.833, 15633L, \u0026#34;Venezuela\u0026#34;, 223.984, 8397L ) Esto nos va a resultar útil si queremos pegar datos dentro de R, pero qué pasa si queremos copiar datos desde R y pegarlos en Excel u otro software planillas de datos?\nPegar datos desde R a Excel La función write_clip() del paquete {clipr} nos permite guardar datos en el portapapeles del sistema para poder pegarlos en Excel, Numbers, Google Sheets u otros programas:\npib_latam |\u0026gt; clipr::write_clip() Los datos quedarán copiados en el portapapeles de tu sistema, y vas a poder pegarlos en Excel:\nFuente: Wikipedia\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-06-06T00:00:00Z","excerpt":"`{datapasta}` es un paquete para R que te ayuda a copiar y pegar datos desde y hacia R. Este paquete convierte tus datos en texto que puedes copiar y pegar en otro script, o editar manualmente. Usa la función `dpasta()` sobre un dataframe para hacer que los datos aparezcan como en texto directamente debajo de donde la ejecutaste. Por otro lado, `{clipr}` nos va a permitir copiar un dataframe desde R para poder pegarlo en programas como Excel.","href":"https://bastianoleah.netlify.app/blog/datapasta/","tags":"consejos ; datos","title":"Copia y pega datos en R con {datapasta} y {clipr}"},{"content":"La franja larga y angosta que es Chile tiene el beneficio de que sus regiones se ubican casi perfectamente una sobre la otra, de norte a sur. Este orgen geográfico natural de sus regiones resulta familiar para sus habitantes, por lo que se vuelve recomendable ordenar los datos a nivel regional siguiendo este orden geográfico.\nCreemos una tabla o dataframe con las regiones de Chile. Ejecuta el siguiente código en R para obtener una tabla:\nlibrary(dplyr) regiones \u0026lt;- tibble::tribble( ~codigo_region, ~nombre_region, 1, \u0026#34;Tarapacá\u0026#34;, 2, \u0026#34;Antofagasta\u0026#34;, 3, \u0026#34;Atacama\u0026#34;, 4, \u0026#34;Coquimbo\u0026#34;, 5, \u0026#34;Valparaíso\u0026#34;, 6, \u0026#34;Libertador General Bernardo O\u0026#39;Higgins\u0026#34;, 7, \u0026#34;Maule\u0026#34;, 8, \u0026#34;Biobío\u0026#34;, 9, \u0026#34;La Araucanía\u0026#34;, 10, \u0026#34;Los Lagos\u0026#34;, 11, \u0026#34;Aysén del General Carlos Ibáñez del Campo\u0026#34;, 12, \u0026#34;Magallanes y de la Antártica Chilena\u0026#34;, 13, \u0026#34;Metropolitana de Santiago\u0026#34;, 14, \u0026#34;Los Ríos\u0026#34;, 15, \u0026#34;Arica y Parinacota\u0026#34;, 16, \u0026#34;Ñuble\u0026#34; ) En esta tabla tenemos el nombre de las 16 regiones del país, y además una columna de código de región. El código de región corresponde al nombre antiguo que tenían las regiones (instaurado en dictadura), y que hoy en día no se sigue utilizando, pero que sin embargo se mantiene como código identificador de las regiones en la mayoría de las bases de datos oficiales. Si bien este código originalmente ordenaba las regiones, hoy en día la creación de nuevas regiones (como Ñuble en 2017 o Arica y Parinacota en 2007) provoca que este ordenamiento antiguo no se corresponda con el orden geográfico de las regiones.\nSi ejecutamos esta tabla de datos, veremos que las regiones no están ordenadas geográficamente:\nregiones # A tibble: 16 × 2 codigo_region nombre_region \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1 Tarapacá 2 2 Antofagasta 3 3 Atacama 4 4 Coquimbo 5 5 Valparaíso 6 6 Libertador General Bernardo O'Higgins 7 7 Maule 8 8 Biobío 9 9 La Araucanía 10 10 Los Lagos 11 11 Aysén del General Carlos Ibáñez del Campo 12 12 Magallanes y de la Antártica Chilena 13 13 Metropolitana de Santiago 14 14 Los Ríos 15 15 Arica y Parinacota 16 16 Ñuble Para ordenar geográficamente las regiones de Chile podemos crear una variable nueva, que use el código de las regiones para asignar un nuevo número que ordene las regiones de norte a sur:\nregiones_ordenadas \u0026lt;- regiones |\u0026gt; # agregar orden de región de norte a sur mutate(orden_region = case_match(codigo_region, 15 ~ 1, 1 ~ 2, 2 ~ 3, 3 ~ 4, 4 ~ 5, 5 ~ 6, 13 ~ 7, 6 ~ 8, 7 ~ 9, 16 ~ 10, 8 ~ 11, 9 ~ 12, 14 ~ 13, 10 ~ 14, 11 ~ 15, 12 ~ 16 )) Se recomienda hacer este tipo de operaciones usando el código de las regiones, para evitar problemas por las distintas formas de escribir el nombre de cada región1. Pero si lo quieres hacer por el nombre de las regiones, sería así:\nregiones_ordenadas \u0026lt;- regiones |\u0026gt; # agregar orden de región de norte a sur mutate(orden_region = case_match(nombre_region, \u0026#34;Tarapacá\u0026#34; ~ 2, \u0026#34;Antofagasta\u0026#34; ~ 3, \u0026#34;Atacama\u0026#34; ~ 4, \u0026#34;Coquimbo\u0026#34; ~ 5, \u0026#34;Valparaíso\u0026#34; ~ 6, \u0026#34;Libertador General Bernardo O\u0026#39;Higgins\u0026#34; ~ 8, \u0026#34;Maule\u0026#34; ~ 9, \u0026#34;Biobío\u0026#34; ~ 11, \u0026#34;La Araucanía\u0026#34; ~ 12, \u0026#34;Los Lagos\u0026#34; ~ 14, \u0026#34;Aysén del General Carlos Ibáñez del Campo\u0026#34; ~ 15, \u0026#34;Magallanes y de la Antártica Chilena\u0026#34; ~ 16, \u0026#34;Metropolitana de Santiago\u0026#34; ~ 7, \u0026#34;Los Ríos\u0026#34; ~ 13, \u0026#34;Arica y Parinacota\u0026#34; ~ 1, \u0026#34;Ñuble\u0026#34; ~ 10 )) Si ejecutamos la nueva tabla con la columna orden_region, vemos que sigue desordenada, así que la ordenamos:\nregiones_ordenadas |\u0026gt; arrange(orden_region) # A tibble: 16 × 3 codigo_region nombre_region orden_region \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 15 Arica y Parinacota 1 2 1 Tarapacá 2 3 2 Antofagasta 3 4 3 Atacama 4 5 4 Coquimbo 5 6 5 Valparaíso 6 7 13 Metropolitana de Santiago 7 8 6 Libertador General Bernardo O'Higgins 8 9 7 Maule 9 10 16 Ñuble 10 11 8 Biobío 11 12 9 La Araucanía 12 13 14 Los Ríos 13 14 10 Los Lagos 14 15 11 Aysén del General Carlos Ibáñez del Campo 15 16 12 Magallanes y de la Antártica Chilena 16 ¡Ahora sí aparecen ordenadas geográficamente! ¿Pero qué pasa si hacemos un gráfico con estas regiones?\nlibrary(ggplot2) regiones_ordenadas |\u0026gt; ggplot() + aes(x = codigo_region, y = nombre_region, fill = codigo_region) + geom_col() + theme_minimal() Las regiones nuevamente aparecen desordenadas. ¿Por qué? Porque {ggplot2}, la librería de visualización de datos que usamos, así como muchos otros paquetes, no se basan en en el orden de las filas de la base de datos que usemos, sino en el orden de la variable. Como la variable nombre_region es una variable de texto (tipo caracter), no tiene un orden, así que se asume que su orden es alfabético (en el gráfico vemos que abajo están las regiones que empiezan con A, porque el eje y nace desde el cero que está abajo y aumenta hacia arriba).\nLa solución sería darle un orden a la variable. En R, las variables de texto que contienen información sobre su ordenamiento se llaman factores. Un factor es una variable categórica de tipo ordinal; es decir, una variable de texto que tiene categorías de texto, pero que estas categorías tienen un ordenamiento entre ellas: por ejemplo, bajo, medio y alto deberían tener un orden de 1 a 3, de lo contrario se ordenarían alfabéticamente y alto quedaría antes que bajo.\nEl paquete {forcats} facilita todo tipo de trabajo con variables tipo factor o categóricas. Entre sus funciones está fct_reorder(), que nos permite ordenar una variable de texto en base al ordenamiento que nos da una variable numérica: en nuestro caso, ordenar el nombre_region en base al orden_region:\nlibrary(forcats) # ordenar variable a partir de una segunda variable numérica regiones_ordenadas_2 \u0026lt;- regiones_ordenadas |\u0026gt; mutate(nombre_region = forcats::fct_reorder(nombre_region, orden_region)) Si ahora volvemos a hacer el gráfico, la variable nombre_region sí aparecerá ordenada:\n# crear un gráfico regiones_ordenadas_2 |\u0026gt; ggplot() + aes(x = orden_region, y = nombre_region, fill = orden_region) + geom_col() + theme_minimal() + guides(fill = guide_none()) Naturalmente, las regiones aparecen al revés, porque el eje y parte desde abajo y aumenta hacia arriba, entonces la primera región (la de más al norte) aparece abajo. Pero sería esperable que las regiones estén ordenadas de norte a sur y de arriba hacia abajo (en el hemisferio sur), así que podemos invertir el orden del factor:\nregiones_ordenadas_2 |\u0026gt; # invertir orden de la variable mutate(nombre_region = forcats::fct_rev(nombre_region)) |\u0026gt; ggplot() + aes(x = orden_region, y = nombre_region, fill = orden_region) + geom_col() + theme_minimal() + guides(fill = guide_none()) ¡Listo! Si te sirve, acá dejo un dataframe con las regiones del país, su código y su orden, para que puedas copiarlo y pegarlo en tu script de R, y luego agregarlo a tus datos usando left_join():\n# regiones_ordenadas |\u0026gt; # datapasta::dpasta() tibble::tribble( ~codigo_region, ~nombre_region, ~orden_region, 1, \u0026#34;Tarapacá\u0026#34;, 2, 2, \u0026#34;Antofagasta\u0026#34;, 3, 3, \u0026#34;Atacama\u0026#34;, 4, 4, \u0026#34;Coquimbo\u0026#34;, 5, 5, \u0026#34;Valparaíso\u0026#34;, 6, 6, \u0026#34;Libertador General Bernardo O\u0026#39;Higgins\u0026#34;, 8, 7, \u0026#34;Maule\u0026#34;, 9, 8, \u0026#34;Biobío\u0026#34;, 11, 9, \u0026#34;La Araucanía\u0026#34;, 12, 10, \u0026#34;Los Lagos\u0026#34;, 14, 11, \u0026#34;Aysén del General Carlos Ibáñez del Campo\u0026#34;, 15, 12, \u0026#34;Magallanes y de la Antártica Chilena\u0026#34;, 16, 13, \u0026#34;Metropolitana de Santiago\u0026#34;, 7, 14, \u0026#34;Los Ríos\u0026#34;, 13, 15, \u0026#34;Arica y Parinacota\u0026#34;, 1, 16, \u0026#34;Ñuble\u0026#34;, 10 ) Por ejempo, Valparaíso puede encontrarse como Región de Valparaíso, De Valparaíso, V región de Valparaíso, etc., y para qué hablar de O\u0026rsquo;Higgins\u0026hellip;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-06-06T00:00:00Z","excerpt":"La franja larga y angosta que es Chile tiene el beneficio de que sus regiones se ubican casi perfectamente una sobre la otra, de norte a sur. Este orgen geográfico natural de sus regiones resulta familiar para sus habitantes, por lo que se vuelve recomendable ordenar los datos a nivel regional siguiendo este orden geográfico.","href":"https://bastianoleah.netlify.app/blog/ordenar_regiones/","tags":"consejos ; mapas ; Chile","title":"Ordenar las regiones de Chile de norte a sur en R"},{"content":"En este tutorial aprenderemos a crear reportes parametrizados con Quarto. Un reporte parametrizado es un reporte diseñado para que el contenido, incluyendo textos, gráficos y más, cambien al modificar una variable en su encabezado. En este sentido, por parámetro nos referimos a un valor que podremos cambiar para alterar el resultado obtenido por un mismo reporte.\nDe esta manera, podemos generar múltiples versiones de un mismo reporte simplemente cambiando el parámetro, por ejemplo, para diseñar un reporte con datos para un país, en base a un conjuntos de datos de múltiples países, y luego obtener múltiples versiones del mismo reporte pero para distintos países. O generar un reporte con datos de un año específico, y luego generar versiones del mismo reporte para distintos años.\nSi quieres aprender a crear documentos Quarto desde cero, revisa este tutorial! Si tengo un reporte donde los resultados dependen de un filtro en mis datos, definimos el valor de la variable a filtrar en el encabezado bajo la etiqueta params:\nparams: especie: \u0026#34;virginica\u0026#34; El código completo de un reporte parametrizado básico, que define una variable especie en el encabezado, sería así:\nLuego, en el código del documento, puedo acceder al contenido de especie usando params$especie. La idea de un reporte parametrizado, entonces, es que sin tocar el código del documento, puedo cambiar el valor de la variable parametrizada desde el encabezado del reporte.\nLa idea de poder hacer esto es diseñar el reporte para producir resultados en base a la variable parametrizada, para que podamos obtener múltiples reportes solamente cambiando la variable parametrizada.* De este modo, podemos obtener el reporte desde un script distinto con la función quarto_render() del paquete {quarto}:\nlibrary(quarto) quarto_render( input = \u0026#34;iris_params.qmd\u0026#34;, execute_params = list( especie = \u0026#34;setosa\u0026#34; ) ) Así solamente se necesita cambiar especie para obtener un reporte en .html listo para presentar. En otras palabras, desde un script secundario, puedo controlar la renderización del reporte, y general múltiples copias cambiando en esta función generadora de reportes el valor de la variable parametrizado, sin tener que entrar en el reporte mismo y editarlo cada vez.\nEsto podría servir para tener un script que, ejecutando varias veces la función quarto_render(), genere decenas de copias de mi reporte, pero mostrando versiones distintas o filtradas del mismo conjunto de datos.\nEl potencial de esto es que, más allá de facilitar la generación de múltiples versiones de un reporte, se pueden generar x cantidad de reportes de una vez por medio de una iteración o loop que pase por todos los valores de la variable que parametrizamos:\nwalk(c(\u0026#34;virginica\u0026#34;, \u0026#34;setosa\u0026#34;, \u0026#34;versicolor\u0026#34;), ~{ quarto_render( input = \u0026#34;iris_params.qmd\u0026#34;, output_file = paste0(\u0026#34;iris_params_\u0026#34;, .x, \u0026#34;.html\u0026#34;), execute_params = list( especie = .x ) ) }) Hay que asegurarse de que los nombres de los reportes generados sean distintos, cambiándolos en output_file = paste0(\u0026quot;iris_params_\u0026quot;, .x, \u0026quot;.html\u0026quot;) para que tengan en su nombre el valor del parámetro usado.\nTambién hay que preocuparse de que los reportes sean autocontenidos conteniendo embed-resources: true en el encabezado.\nDe esta forma obtenemos tres reportes con el trabajo de haber diseñado sólo uno. Pero si nuestra variable de parametrización tiene 10 o 100 valores, habremos obtenido 10 o 100 reportes gratis!\nEjemplo práctico: reporte parametrizado de resultados electorales Para dar un ejemplo de parametrización de reportes, crearemos un pequeño documento Quarto que carga datos de las elecciones presidenciales de 2021 y genera un reporte que muestra un gráfico, un texto redactado en base a los datos, y una tabla. Este documento estará parametrizado por la región del país de la cual se desean obtener los resultados.\nparams: region_elegida: \u0026#34;Los Ríos\u0026#34; Puedes descargar el documento Quarto en este enlace y ejecutarlo en tu propio computador, ya que los datos se descargan directamente desde el repositorio.\nEl código del documento Quarto es el siguiente:\nPodemos renderizar el documento parametrizado directamente desde RStudio presionando Render, o bien, podemos generar el resultado usando la función quarto_render(), desde la cual se puede especificar también el parámetro; es decir, la región del país de la que queremos obtener un reporte:\n# generar un solo reporte quarto_render(input = \u0026#34;reporte_elecciones_params.qmd\u0026#34;, execute_params = list(region_elegida = \u0026#34;Valparaíso\u0026#34;)) El reporte resultante:\nCon tan sólo cambiar el parámetro a una región distinta, como \u0026quot;Los Ríos\u0026quot;, obtenemos instantáneamente un nuevo reporte basado en los datos de esta región:\nCómo en nuestros datos tenemos una variable que contiene todas las regiones del país, podemos introducir todos los nombres de las regiones a un loop para que se generen tantos reportes como regiones existen en los datos:\n# cargar los datos para generar reportes en base a un parámetro que viene desde los datos # datos \u0026lt;- readr::read_csv2(\u0026#34;quarto_clases/reporte_parametrizado/datos/presidenciales_2021_region.csv\u0026#34;) datos \u0026lt;- readr::read_csv2(\u0026#34;https://github.com/bastianolea/presidenciales_2021_chile/raw/main/datos_procesados/presidenciales_2021_region.csv\u0026#34;) # todos los valores de la variable parametrizada regiones \u0026lt;- unique(datos$region) library(purrr) # loop con purrr que genere un documento por cada valor de la variable parametrizada walk(regiones, ~{ quarto_render(input = \u0026#34;reporte_elecciones_params.qmd\u0026#34;, output_file = paste0(\u0026#34;reporte_elecciones_\u0026#34;, .x, \u0026#34;.html\u0026#34;), execute_params = list(region_elegida = .x)) }) Con el código anterior obtuvimos 16 reportes, uno por cada región del país, con tan sólo haber diseñado un reporte!\n","date":"2025-04-15T00:00:00Z","excerpt":"\u003cp\u003eEn este tutorial aprenderemos a crear \n\u003ca href=\"https://posit.co/blog/parameterized-quarto/\" target=\"_blank\" rel=\"noopener\"\u003ereportes parametrizados\u003c/a\u003e con Quarto. Un reporte parametrizado es un reporte diseñado para que el contenido, incluyendo textos, gráficos y más, cambien al modificar una variable en su encabezado. En este sentido, por \u003cem\u003eparámetro\u003c/em\u003e nos referimos a un valor que podremos cambiar para alterar el resultado obtenido por un mismo reporte.\u003c/p\u003e\n\u003cp\u003eDe esta manera, podemos \u003cstrong\u003egenerar múltiples versiones de un mismo reporte simplemente cambiando el parámetro\u003c/strong\u003e, por ejemplo, para diseñar un reporte con datos para un país, en base a un conjuntos de datos de múltiples países, y luego obtener múltiples versiones del mismo reporte pero para distintos países. O generar un reporte con datos de un año específico, y luego generar versiones del mismo reporte para distintos años.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/quarto_params/","tags":"quarto ; loops","title":"Automatiza la generación de reportes creando documentos parametrizados con Quarto y R"},{"content":" Índice Crear un reporte Quarto Markdown Otras posibilidades Configuración básica Títulos Índices o tablas de contenido Títulos con numeración Temas Documento autocontenido Disposición Columnas Pestañas Contenido al margen Código Código entre el texto Quarto es una herramienta de publicación de documentos que te permite generar reportes de manera muy sencilla, utilizando bloques de código de R. En estos reportes puedes incluir tablas, gráficos, y mucho más para poder compartir tus análisis y resultados con otras personas.\nPuedes leer este mismo tutorial directamente desde un reporte Quarto en este enlace, y también puedes ver un documento de ejemplo hecho siguiendo los pasos de este tutorial en este otro enlace. Crear un reporte Quarto En RStudio, crear un nuevo archivo desde New File, y elegir Quarto Document.\nCuando hagamos los cambios que deseemos, presionamos el botón Render para generar el reporte a partir del código. El reporte se abre en el visor Viewer, y quedará guardado como un archivo .html para abrirlo con un navegador web.\nEl formato .html es el mismo formato que se usa para crear la mayoría de los sitios web, porque un reporte Quarto es básicamente una página web local. Es una forma más moderna, accesible, flexible e interactiva de crear documentos, a diferencia de formatos enfocados en la impresión como .pdf o formatos propietarios y editables como .docx.\nMarkdown Markdown es la forma de escritura principal usada en Quarto. Permite escribir texto enriquecido a partir de texto plano (código), usando símbolos de marcado para definir el estilo del texto. Más información en esta guía.\nLo que hace Markdown es ayudarnos a escribir código html con símbolos de marcado fáciles de recordar.\nAlgunos de los estilos básicos que puedes darle al texto son los siguientes:\n**negrita** *itálica* o _itálica_ \u0026lt;u\u0026gt;subrayado\u0026lt;/u\u0026gt; ~~tachado~~ `código` El uso de estos símboos de marcado resultarían en los siguientes textos:\nnegrita\nitálica o itálica\nsubrayado\ntachado\ncódigo\nLos cuales a su vez se ven así porque internamente fueron traducidos a html!\nPara crear un título en tu documento, antepone un signo gato (#) al texto. Mientras más signos gato pongas, el titular será de un nivel menor; es decir, se volverán subtítulos.\nOtras posibilidades Separadores Puedes insertar separadores escribiendo cuatro guiones. Estos resultan en una línea vertical, como la de encima del subtítulo anterior.\n---- Enlaces Para agregar un enlace en Markdown, escrirbimos el texto del enlace entre corchetes, y luego el enlace al que queremos dirigir entre paréntesis:\n[Texto](http://enlace.cl) El enlace se vería así. Naturalmente, puedes mezclar estilos de texto dentro de un enlace.\nNotas al pie de página Para agregar una nota al pie se escribe el número de la nota entre corchetes, antecedido por el símbolo de superíndice o potencia ^.\nEjemplo de una nota al pie1. También se puede agregar directamente en el texto2.\nAgregar una nota al pie[^1]. [^1]: Esta es una nota al pie También se puede agregar directamente en el texto^[De esta forma]. Imágenes Agregar una imagen es similar a la forma de hacer un enlace, excepto que la dirección del enlace debe ser a un archivo de imagen en la misma carpeta que el archivo Quarto (.qmd), y debes antecederlo por un signo de exclamación (!);\n![](mapache.jpeg) Puedes encontrar más consejos básicos sobre Markdown en esta guía.\nConfiguración básica Los documentos Quarto se configuran principalmente desde su encabezado o header. El encabezado es el código que se encuentra en las primeras líneas, escrito entre tres guiones (---). Este código está escrito en lenguaje yaml, y permite configurar casi todos los aspectos de apariencia, funcionalidad y exportación del documento.\nTítulos Puedes cambiar el título del documento en la etiqueta title.\n--- title: \u0026#34;Probando\u0026#34; --- También puedes agregar autoría y fecha, simplemente agregando las configuraciones debajo de las existentes:\n--- title: \u0026#34;Probando\u0026#34; author: \u0026#34;Bastián Olea Herrera\u0026#34; date: 2025-04-01 lang: es --- Puedes cambiar el texto que aparece arriba de alguna de las etiquetas. Sugiero buscar en internet este tipo de cosas, ya que Quarto tiene muy buena documentación.\n--- lang: es language: title-block-author-single: \u0026#34;Creado por\u0026#34; --- Puedes personalizar el titular del documento con estas etiquetas, para cambiar el color de fondo del título y el color del texto:\n--- title-block-banner: \u0026#34;#f0f3f5\u0026#34; title-block-banner-color: \u0026#34;black\u0026#34; --- Para más información sobre el bloque titular, revisa esta guía.\nÍndices o tablas de contenido A medida que agregas títulos Markdown, puedes agregar una tabla de contenidos que se actualizará automáticamente con los títulos que vayas agregando. Para más información, revisa esta otra guía.\nActiva la tabla de contenidos reemplazando el format de tu documento por uno personalizable:\nformat: html toc: true Personaliza el nivel de titulares que aparecen en la tabla de contenidos con toc-depth:\nformat: html toc: true toc-expand: 2 También poner enlaces debajo del índice, por ejemplo a tu sitio web o redes sociales. Los enlaces pueden tener un ícono personalizado.\nformat: html: other-links: - text: Enlace 1 href: https://data.nasa.gov/ - text: Enlace 2 icon: file-code href: https://data.nasa.gov/ Títulos con numeración Si tienes una estructura de títulos y subtítulos compleja, puedes agregar numeración a los títulos tanto en la tabla de contenidos como en los mismos títulos.\nPara que las secciones se numeren automáticamente en la tabla de contenidos agrega number-sections.\nformat: html toc: true toc-expand: 2 number-sections: true Temas Configura el tema de tu reporte para cambiar su apariencia completa, incluyendo colores, espaciados, tipografías y más. Revisa la lista de temas en este enlace.\nTan solo agregando theme y el nombre del tema (de la lista de temas) cambiarás la apariencia completa de tu reporte.\n--- theme: lux --- Temas Cosmo, Lux y Darkly Personaliza la tipografía y el tamaño global de las letras:\n--- mainfont: Courier fontsize: \u0026#34;20px\u0026#34; --- Documento autocontenido Los reportes Quarto son archivos web, y como tales requieren de dependencias externas, como scripts, estilos e imágenes, que se guardan como archivos. Éstos se guardan en una carpeta con el mismo nombre de tu reporte. Pero esto puede resultar inconveniente para compartir los documentos, además de generar desorden.\nPara que el documento no tenga dependencias externas, sino que sea portable y auto-contenido en un sólo archivo .html, podemos hacer que el reporte contenga todas sus dependencias dentro de sí agregando lo siguiente a la etiqueta format en el encabezado de nuestro documento:\nformat: html: embed-resources: true Recomiendo siempre usar esta opción, a pesar de que puede generar archivos más pesados.\nDisposición Veamos algunas formas de personalizar la disposición de los reportes, como columnas, pestañas y contenido en los márgenes. Para más información, consulta esta guía oficial\nColumnas Para escribir contenido en columnas, debes crear un bloque (::::) con el estilo .columns, y dentro de él, crear dos secciones con el estilo .column indicando el porcentaje del ancho que tomarán:\n:::: {.columns} ::: {.column width=\u0026#34;40%\u0026#34;} Columna izquierda ::: ::: {.column width=\u0026#34;60%\u0026#34;} Columna derecha ::: :::: Si quieres agregar un espacio entre ambas columnas, crea una columna vacía que sea más delgada:\n:::: {.columns} ::: {.column width=\u0026#34;40%\u0026#34;} Columna izquierda ::: ::: {.column width=\u0026#34;5%\u0026#34;} ::: ::: {.column width=\u0026#34;55%\u0026#34;} Columna derecha ::: :::: Pestañas Para poner contenido dentro de pestañas, crea un bloque con el estilo .panel-tabset, y dentro de éste, todos los títulos Markdown que pongas (##, ###, etc.) aparecerán en su propia pestaña:\n::: {.panel-tabset} ## Pestaña 1 Contenido de la pestaña 1 ## Pestaña 3 Contenido de la pestaña 2 ## Pestaña 3 Contenido de la pestaña 3 ::: Si usas como estilo {.panel-tabset .nav-pills}, los botones de las pestañas aparecerán rellenados.\nContenido al margen Puedes agregar cualquier contenido en los márgenes del documento, como fórmulas, aclaraciones, o incluso tablas y gráficos, usando un bloque con el estilo .aside:\n::: {.aside} \\ Esto es un ejemplo. Esto es un ejemplo. Esto es un ejemplo. Esto es un ejemplo. Esto es un ejemplo. Esto es un ejemplo. Esto es un ejemplo. Esto es un ejemplo. Esto es un ejemplo. ::: Código La gracia de Quarto es combinar código con texto. De esta forma puedes combinar tus procesos, análisis y desarrollos con la presentación de tus resultados en un mismo lugar.\nPara agregar código, creamos bloques o chunks, en los que podemos escribir código de R, y sus resultados aparecerán en el documento.\nlibrary(ggplot2) iris |\u0026gt; ggplot() + aes(x = Sepal.Length, y = Sepal.Width) + geom_point(color = \u0026#34;purple4\u0026#34;, alpha = 0.4, size = 4) + theme_minimal() Si queremos que no aparezca el código, y sólo aparezca el output, agregamos #| echo: false al inicio del bloque. De este modo sólo saldrá el resultado del código, sin el código mismo. Por ejemplo, si en un bloque pongo head(iris) pero agrego antes #| echo: false, sólo saldrá el resultado, sin el código:\nSepal.Length Sepal.Width Petal.Length Petal.Width Species 1 5.1 3.5 1.4 0.2 setosa 2 4.9 3.0 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.0 3.6 1.4 0.2 setosa 6 5.4 3.9 1.7 0.4 setosa Algunas funciones u operaciones en R entregan mensajes además del resultado esperado. Por ejemplo, cuando ocurren warnings. Para omitir que los bloques impriman mensajes y/o alertas, agregamos #| message: false y #| warning: false respectivamente.\n```{r} #| message: false #| warning: false library(dplyr) ``` Para configurar el tamaño de los gráficos, agregamos al bloque las opciones #| fig-width: 4 y/o #| fig-height: 6 para modificar el ancho o alto, respectivamente. Por defecto, los gráficos son de 7 x 5.\n```{r} #| message: false #| warning: false #| fig-width: 4 #| fig-height: 8 iris |\u0026gt; ggplot() + aes(x = Sepal.Length, y = Sepal.Width) + geom_point(color = \u0026#34;orange\u0026#34;, alpha = 0.4, size = 4) + theme_minimal() ``` Código entre el texto Para integrar código en el texto de los párrafos de tu reporte, por ejemplo, para insertar una cifra, escribimos comillas invertidas (`), la letra r, y el código cuyo output queremos que aparezca en el documento:\nDe esta forma podemos agregar texto que proviene desde nuestros datos: el dataset iris tiene 5 variables que describen 3 especies de plantas.\nAprender a generar documentos Quarto es una herramienta que puede llevar tus habilidades de análisis de datos al siguiente nivel. Con Quarto puedes presentar tu resultado de forma mucho más atractiva, pero también puedes aprender a automatizar muchísimo trabajo relacionado a reportabilidad.\nSi quieres seguir aprendiendo Quarto, por ejemplo a automatizar reportes con documentos Quarto parametrizados, o a generar contenido automáticamente para tus reportes usando loops, revisa las siguientes publicaciones de este blog en la etiqueta Quarto!\nNota al pie.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nDe esta forma\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-04-14T00:00:00Z","excerpt":"Quarto es una herramienta que te permite generar documentos y reportes de manera muy sencilla utilizando bloques de código de R. En estos reportes puedes incluir tablas, gráficos, y mucho más, de forma atractiva, para poder compartir tus análisis y resultados con otras personas. Aprender a generar documentos Quarto es una herramienta que puede llevar tus habilidades de análisis de datos al siguiente nivel!","href":"https://bastianoleah.netlify.app/blog/quarto_reportes/","tags":"quarto","title":"Tutorial: presenta los resultados de tus análisis de datos con R creando reportes y documentos Quarto"},{"content":"Amamos el castellano, con sus tildes y eñes, y nos encanta que R no tenga problemas para usar estos símbolos en cualquier parte del lenguaje. Pero hay veces en las que necesitamos deshacernos de estos símbolos especiales, como tildes, eñes, e incluso mayúsculas.\nPor ejemplo:\nSi queremos dar nombres a carpetas (porque puede dar conflictos con otros sistemas operativos o para subir archivos a internet), Si queremos buscar coincidencias de texto con stringr::str_detect() y queremos aumentar probabilidades de coincidir al omitir variaciones de los caracteres, Si queremos hacer un left_join() entre dos bases de datos con formas distintas de guardar los datos (todos en mayúscula, todos sin tilde pero con eñes, etc.) Hay muchas formas distintas de hacerlo, dependiendo de lo que necesitemos. El paquete {stringr} se enfoca en el procesamiento de texto y tiene varias funciones que nos pueden ayudar.\nlibrary(stringr) texto_esp \u0026lt;- \u0026#34;TÉxtó con PequÉññños cáráctérés roñosos\u0026#34; Eliminar caracteres específicos Podemos usar str_remove_all() para eliminar todos los caracteres problemáticos, separándolos con el operador o (|):\nstr_remove_all(texto_esp, \u0026#34;ñ|á|é|í|ó|ú\u0026#34;) [1] \u0026quot;TÉxt con PequÉos crctrs roosos\u0026quot; Pero vemos que no funciona con caracteres en mayúscula. Así que convertimos todo el texto a minúsculas primero:\nstr_remove_all(tolower(texto_esp), \u0026#34;ñ|á|é|í|ó|ú\u0026#34;) [1] \u0026quot;txt con pequos crctrs roosos\u0026quot; Sigue sin ser una buena opción, porque se pierden demasiados caracteres.\nReemplazar caracteres específicos Una mejor opción sería reemplazar los caracteres problemáticos por otros. Por ejemplo, reemplazar letras con tilde por sus versiones sin tilde. Para eso podemos usar str_replace_all(), que permite recibir un vector con las letras que queremos encontrar y un signo = con la letra que reemplazamos en casa caso:\nstr_replace_all(tolower(texto_esp), c(\u0026#34;ñ\u0026#34;=\u0026#34;n\u0026#34;, \u0026#34;á\u0026#34;=\u0026#34;a\u0026#34;, \u0026#34;é\u0026#34;=\u0026#34;e\u0026#34;, \u0026#34;í\u0026#34;=\u0026#34;i\u0026#34;, \u0026#34;ó\u0026#34;=\u0026#34;o\u0026#34;, \u0026#34;ú\u0026#34;=\u0026#34;u\u0026#34;)) [1] \u0026quot;texto con pequennnos caracteres ronosos\u0026quot; Una forma más larga pero más flexible de hacer esta limpieza sería incluir reemplazos para caracteres tanto en mayúscula como minúscula, así mantenemos la capitalización de los caracteres pero reemplazamos las versiones problemáticas por las versiones seguras:\nstr_replace_all(texto_esp, c(\u0026#34;Ñ\u0026#34;=\u0026#34;ñ\u0026#34;, \u0026#34;Á\u0026#34;=\u0026#34;A\u0026#34;, \u0026#34;É\u0026#34;=\u0026#34;E\u0026#34;, \u0026#34;Í\u0026#34;=\u0026#34;I\u0026#34;, \u0026#34;Ó\u0026#34;=\u0026#34;O\u0026#34;, \u0026#34;Ú\u0026#34;=\u0026#34;U\u0026#34;, \u0026#34;ñ\u0026#34;=\u0026#34;ñ\u0026#34;, \u0026#34;á\u0026#34;=\u0026#34;a\u0026#34;, \u0026#34;é\u0026#34;=\u0026#34;e\u0026#34;, \u0026#34;í\u0026#34;=\u0026#34;i\u0026#34;, \u0026#34;ó\u0026#34;=\u0026#34;o\u0026#34;, \u0026#34;ú\u0026#34;=\u0026#34;u\u0026#34;)) [1] \u0026quot;TExto con PequEññños caracteres roñosos\u0026quot; La gracia de este método es que tú tienes todo el control sobre los reemplazos. En este caso, reemplazamos caracteres con tilde por versiones sin tilde, pero mantenemos las eñes.\nTransliterar El paquete para procesamiento de texto {stringi} (no confundir con {stringr}) cuenta con la función stri_trans_general(), que procesa texto para convertirlo o transliterarlo a otros sistemas de escritura. Para lo que necesitamos, podemos pedirle que translitere nuestro texto a ASCII (estándar de texto plano, American Standard Code for Information Interchange):\nstringi::stri_trans_general(texto_esp, \u0026#34;Latin-ASCII\u0026#34;) # transliterar, pero remueve eñes [1] \u0026quot;TExto con PequEnnnos caracteres ronosos\u0026quot; Esta forma es más breve, pero menos flexible. Vemos, por ejemplo, que reemplaza las eñes, sin darnos mucha flexibilidad.\nOtra opción podría ser la siguiente, que transforma los símbolos especiales por versiones de varios caracteres:\niconv(texto_esp, to = \u0026#34;ASCII//translit\u0026#34;) # elimina tildes pero los reaplica como símbolos individuales [1] \u0026quot;T'Ext'o con Pequ'E~n~n~nos c'ar'act'er'es ro~nosos\u0026quot; El resultado es extraño, pero puede ser que en ciertos casos nos sirva.\nEliminar símbolos El paquete {textclean} contiene varias funciones avanzadas para limpieza de texto. Una de ellas es strip(), que elimina todos los símbolos, pero preservando tildes.\ntexto_num \u0026lt;- \u0026#34;Hoy!!! Súper ricas **empanadas** a $1.000 pesos... Ñam ñam\u0026#34; textclean::strip(texto_num) [1] \u0026quot;hoy súper ricas empanadas a pesos ñam ñam\u0026quot; El resultado es solamente texto, con tildes y con eñes, pero sin símbolos ni números. Esta función sí posee ciertas opciones para ajustar la limpieza; por ejemplo, no remover números, y dejar sólo símbolos específicos:\ntextclean::strip(texto_num, digit.remove = FALSE, char.keep = c(\u0026#34;!\u0026#34;)) [1] \u0026quot;hoy!!! súper ricas empanadas a 1000 pesos ñam ñam\u0026quot; ","date":"2025-03-31T00:00:00Z","excerpt":"Amamos el castellano, con sus tildes y eñes, y nos encanta que R no tenga problemas para usar estos símbolos en cualquier parte del lenguaje. Pero hay veces en las que necesitamos deshacernos de estos símbolos especiales, como tildes, eñes, e incluso mayúsculas. En este post te muestro varias opciones de limpieza de texto con R.","href":"https://bastianoleah.netlify.app/blog/2025-03-31/","tags":"consejos ; texto ; limpieza de datos","title":"Limpiar textos con símbolos, tildes o eñes en R"},{"content":"Cuando obtenemos una lista de palabras que obtuvimos desde nuestros datos, y queremos incluirlas en algún reporte \u0026mdash;ya sea como el título de un gráfico, el subtítulo de un capítulo, o un texto dinámico basado en los datos\u0026mdash; vamos a querer que estén redactadas correctamente: separadas por comas. La función paste() sirve para unir distintos objetos de tipo texto.\npaste(\u0026#34;primera\u0026#34;, \u0026#34;segunda\u0026#34;) [1] \u0026quot;primera segunda\u0026quot; Si queremos unir las dos palabras, podemos especificar un separador:\npaste(\u0026#34;primera\u0026#34;, \u0026#34;segunda\u0026#34;, sep = \u0026#34;, \u0026#34;) # unir palabras, separadas por comas [1] \u0026quot;primera, segunda\u0026quot; Pero si las palabras las estamos extrayendo desde los datos, lo más probable es que vengan dentro de un vector de texto, como el siguiente:\npalabras \u0026lt;- c(\u0026#34;verde\u0026#34;, \u0026#34;azul\u0026#34;, \u0026#34;morado\u0026#34;, \u0026#34;fuscia\u0026#34;) Entonces, la unión entre los elementos de un vector se realiza con el argumento collapse de paste():\npaste(palabras, collapse = \u0026#34;, \u0026#34;) [1] \u0026quot;verde, azul, morado, fuscia\u0026quot; Con esto obtenemos un vector de texto de largo 1, que contiene los elementos del vector anterior, pero unidos, separados por una coma. Pero podemos mejorar este resultado si escribimos un poco más de código para que la redacción de los elementos sea más apropiada: separando el último elemento por la palabra y. Primero obtendremos el largo del vector de palabras length(). Luego usaremos este largo para seleccionar las palabras desde la primera a la penúltima.\nlargo \u0026lt;- length(palabras) palabras_a \u0026lt;- palabras[1:largo-1] print(palabras_a) # todas las palabras menos la última [1] \u0026quot;verde\u0026quot; \u0026quot;azul\u0026quot; \u0026quot;morado\u0026quot; Volvemos a usar el largo para extraer solamente la última palabra:\npalabras_b \u0026lt;- palabras[largo] print(palabras_b) # la última palabra [1] \u0026quot;fuscia\u0026quot; Unimos el vector de las primeras palabras, separándolas con comas:\n# unir las primeras palabras separadas por comas palabras_a2 \u0026lt;- paste(palabras_a, collapse = \u0026#34;, \u0026#34;) Ahora que tenemos dos objetos, uno con las primeras palabras separadas por comas, y otro con la última palabra, unimos ambos objetos, separándolos con la palabra y:\n# agregar la última palabra separada por \u0026#34;y\u0026#34; palabras_redactadas \u0026lt;- paste(palabras_a2, palabras_b, sep = \u0026#34; y \u0026#34;) print(palabras_redactadas) [1] \u0026quot;verde, azul, morado y fuscia\u0026quot; Cómo resultado, obtenemos un código que genera la redacción de cualquier cantidad de palabras que vengan en un vector de texto. Podemos formalizar este invento creando una función que simplifique esta tarea:\nredactar_palabras \u0026lt;- function(palabras) { largo \u0026lt;- length(palabras) palabras_a \u0026lt;- palabras[1:largo-1] # primeras palabras palabras_b \u0026lt;- palabras[largo] #última palabra # unir las primeras palabras separadas por comas palabras_a2 \u0026lt;- paste(palabras_a, collapse = \u0026#34;, \u0026#34;) # agregar la última palabra separada por \u0026#34;y\u0026#34; palabras_redactadas \u0026lt;- paste(palabras_a2, palabras_b, sep = \u0026#34; y \u0026#34;) # retornar palabras redactadas return(palabras_redactadas) } Gracias a esta función, ahora podemos lograr la redacción de las palabras de forma mucho más fácil y clara:\nredactar_palabras(palabras) [1] \u0026quot;verde, azul, morado y fuscia\u0026quot; Dicho lo anterior, y gracias a lo extenso que es el ecosistema de R, quizás resulta más rápido usar una función que hace exactamente lo mismo y que viene en {glue}, un paquete del Tidyverse:\nlibrary(glue) glue::glue_collapse(palabras, sep = \u0026#34;, \u0026#34;, last = \u0026#34; y \u0026#34;) verde, azul, morado y fuscia A veces nos podemos reinventar la rueda cuando ya existen soluciones 😣 Pero esto no es tiempo perdido si es que nos permite aprender cosas nuevas y/o poner en práctica nuestras habilidades 🥰\n","date":"2025-03-30T00:00:00Z","excerpt":"Aprende a generar un texto que redacte un vector de palabras sueltas en una oración separada por comas y con el separador _y_ al final; por ejemplo: ’uno, dos y tres’. Útil para escribir programáticamente títulos, subtítulos y textos para reportes.","href":"https://bastianoleah.netlify.app/blog/2025-03-30/","tags":"texto","title":"Redactar una lista de palabras separadas por comas en R"},{"content":" Índice Crear un repositorio en GitHub para compartir código y/o datos Conectar R a GitHub Crear un repositorio local Subir el repositorio local a GitHub Crear un archivo readme.md Documentos Quarto Documento Quarto en GitHub Pages Sitios web Quarto en GitHub Pages Crear el sitio web Quarto Agregar páginas a tu sitio Cambiar temas Publicar el sitio en GitHub Pages Blogs Quarto en GitHub Pages Crear un blog Quarto Agregar posts al blog Quarto Subir el blog Quarto a GitHub Pages Blog Hugo Apps Shiny Recursos: En este tutorial veremos cuatro formas relativamente sencillas, y ordenadas de menor a mayor dificultad, para crear nuestros propios espacios en internet para compartir nuestras creaciones, aprendizajes y quiénes somos usando R, y de forma completamente gratuita.\nEn una tarde podrías tener tu propio sitio web para presentarte, para subir las cosas que has aprendido, o para destacar tu trabajo!\nSi quieres aprender a crear documentos Quarto desde cero, revisa este tutorial! Los contenidos son:\nCrear repositorios en GitGub Crear documentos Quarto Crear páginas estáticas a partir de documentos Quarto con GitHub Pages Crear sitios web con Quarto Crear blogs con Quarto Crear blogs con Hugo Apéro Lo más probable es que todas las cosas que hemos aprendido sobre R y análisis de datos fueron porque alguna persona linda y bondadosa compartió gratuita y abiertamente lo que sabía o lo que creó, para que los demás se beneficien. Uno de los aspectos más positivos de la comunidad de usuarixs de R es su disposición a compartir y ayudar a los demás 💕 ¿Por qué no devolver la mano? 🥰\nAntes que nada, todas las opciones de este tutorial requieren que sepas usar GitHub, para poder subir tus proyectos de R a GitHub. Así que, para empezar, te resumiré las instrucciones necesarias para hacerlo. Encuentra instrucciones más detalladas sobre usar git y GitHub con R en este tutorial.\nCrear un repositorio en GitHub para compartir código y/o datos Quizás la forma más sencilla de poder compartir en internet tus trabajos, desarrollos, o aprendizajes en R, es creando un repositorio de código abierto en GitHub. En los reposos dormitorios, las personas dan a conocer el código que producen, ofreciéndoselo a los demás para que puedan reutilizarlos en sus propios proyectos, ya sean utilidades, análisis de datos, clases o tutoriales, o incluso conjuntos de datos y paquetes de R.\nEn este primer paso del tutorial, aprenderemos a crear un repositorio local git para tus proyectos de R, y luego subir este repositorio local a un repositorio remoto en GitHub para poder compartirlo y que otrxs puedan encontrarlo.\nConectar R a GitHub Tutorial más detallado sobre esto escrito por mi Libro tutorial para aprender a usar git con R: https://happygitwithr.com Para poder crear tus reposos remotos, primero tenemos que darle permiso a tu sesión de R para conectarse a tu cuenta de GitHub.\nGitHub es una plataforma online donde las personas pueden subir sus repositorios de Git, permitiendo a otros acceder a su código, y contribuir a los repositorios, entre muchas otras funcionalidades.\nEl primer paso es instalar {usethis}, un paquete de R que automatiza muchas tareas repetitivas que se hacen al configurar tus proyectos.\ninstall.packages(\u0026#34;usethis\u0026#34;) Gracias a este paquete, los siguientes pasos se vuelven mucho más sencillos:\n1. Configurar nombre de usuario y correo Luego, tienes que registrar cuál es tu cuenta de GitHub.\nusethis::use_git_config(user.name = \u0026#34;Basti\u0026#34;, user.email = \u0026#34;baolea@uc.cl\u0026#34;) 2. Crear un token en GitHub para permitir el acceso de R a tu cuenta Es necesario hacer esto para permitir que tu computador pueda usar tu cuenta de GitHub. Se hace por medio de tokens para no tener que escribir tu contraseña. Es una medida más segura para poder iniciar sesión en un servicio online, porque además siempre puedes revocar la autorización desde tu cuenta de GitHub.\nusethis::create_github_token() Se abrirá una ventana de GitHub en la que podrás generar y copiar el token. Ejecuta la siguiente función, y cuando la consola te lo indique, pega el token que copiaste:\ngitcreds::gitcreds_set() 4. Confirmar que está funcionando bien: Con la siguientes función obtendremos una especie de diagnóstico sobre la conexión con nuestra cuenta, para confirmar que todo esté funcionando bien.\nusethis::git_sitrep() Crear un repositorio local Luego de haber configurado tu cuenta, puedes empezar a crear repositorios en tus proyectos de R. Recuerda que en cada proyecto puede haber un solo repositorio.\nusethis::use_git() Al ejecutar este comando, tu consola indicará cuáles son los archivos modificados, y preguntará si quieres hacer commit de tus archivos. Commit significa agregar los archivos modificados a la versión del proyecto que guardaremos/respaldaremos.\nDe esta forma, activaste el control de versiones de git en tu proyecto de RStudio, lo que significa que tienes un repositorio local.\nSubir el repositorio local a GitHub Luego de tener el repositorio local, ahora toca subirlo como repositorio remoto a GitHub, para poder compartirlo con otrxs.\nusethis::use_github() Con este comando se creará un repositorio remoto en tu cuenta de GitHub con el mismo nombre que el proyecto, y se abrirá una ventana de tu navegador con el repositorio subido. Este repositorio puedes enviárselo a otras personas, y ellos pueden clonar el repositorio en sus propias sesiones de R para poder ejecutar tu mismo código.\nCrear un archivo readme.md Si tu proyecto/repositorio tiene un archivo readme.md, aparecerá en GitHub como descripción del código. Puedes usar este documento para explicar más detalles acerca de lo que se trata el repositorio, dar instrucciones, especificar el orden de ejecución de los scripts, indicar tus fuentes, etc.\nusethis::use_readme() Con el solo hecho de tener un archivo readme.md en tu repositorio ya cuentas con una especie de sitio web donde puedes explayarte y compartir cosas más detalladas. Si bien no es la forma más atractiva de hacerlo, cumple con la función básica de poder compartir tus creaciones con el resto de la comunidad! 💜\nSi necesitas más información acerca del uso de git y GitHub con R, revisa el tutorial: Crear un repositorio Git para tu proyecto de R y comparte tu código en GitHub\nDocumentos Quarto Los documentos Quarto combinan la escritura normal con el código. La escritura, como los párrafos, títulos, y subtítulos se escriben usando la sintaxis markdown, un lenguaje de marcado que nos permite traducir textos en html usando sencillos símbolos.\nSi necesitas una guía más detallada para aprender a crear documentos Quarto desde cero, incluyendo la inclusión de código en reportes, personalización y más, revisa este tutorial! Los reportes de Quarto suelen ser en formato PDF o HTML, siendo HTML el formato más recomendado, porque además de ser más compatible, permite ciertos elementos de interacción en tu reporte, como índices, barras de menú, pestañas, selectores, enlaces, y más.\nPara crear un documento Quarto, en el menú File elige New File y luego Quarto Document.\nSe abrirá un documento de ejemplo que puedes usar como base para tus propios documentos. En este caso, agregamos un chunk con un gráfico sencillo, y presionamos el botón Render para generar el documento en html:\nComo vemos, obtenemos tres archivos en nuestro proyecto: el archivo .qmd que contiene el código que genera el documento, el documento renderizado en formato html, y una carpeta que contiene recursos necesarios para visualizar el documento: El problema es que esta carpeta, que contiene cosas como imágenes (de los gráficos), estilos y scripts, dificultan la portabilidad del documento y la posibilidad de compartirlo con otros.\nLa buena noticia es que podemos generar un reporte Quarto autocontenido; es decir, que no se requieran archivos externos al documento html. Agregamos el siguiente código al header del documento Quarto, en reemplazo del format: html:\nformat: html: embed-resources: true Hay que tener cuidado de que se respeten los espacios en blanco para que funcione bien. Si eliminas el reporte en html y la carpta _files y le das render nuevamente al documento Quarto, verás que ahora se genera solamente el reporte en html, listo para poder ser compartido!\nDocumento Quarto en GitHub Pages Si tenemos un reporte en formato html, el salto a generar una página web estática que puedas compartir con otras personas es muy sencillo de hacer. Para esto, podemos usar GitHub Pages para hacer que nuestro documento Quarto se transforme en una página de internet que otras personas pueden visitar tan sólo con entrar al enlace.\nCon un reporte publicado como página web en GitHub Pages, en vez de enviar un documento por correo o similares, podremos hacer que el documento esté disponible en una dirección web fija para que otras personas puedan verlo en línea.\nBeneficios:\nNo necesitas enviar tu reporte como un archivo adjunto No es necesario preocuparse si tu reporte contiene cientos de gráficos o es muy pesado, ya que estará online Si necesitas modificar o actualizar algo de tu reporte, puedes hacer los cambios y actualizar tu reporte cuando quieras Las personas que tengan el enlace tendrán siempre la versión actualizada de tu reporte La publicación de tu reporte es automática: subes los cambios a GitHub y tu reporte se actualizará en unos minutos El alojamiento online de tu reporte es gratuito Para hacer esto, necesitamos configurar primero el documento Quarto, subir nuestro documento Quarto a un repositorio de GitHub, y configurar el repositorio para que genere una página web estática a partir del documento. Todas estas instrucciones están detalladas en esta guía oficial, pero a continuación te resumo lo principal.\nLa configuración del documento Quarto consiste revisar el nombre del archivo, y en agregar un archivo de configuración a nuestro proyecto que hará que se guarden los archivos necesarios en una sola carpeta.\nEsto es importante, porque así GitHub Pages sabrá que éste es el documento específico que queremos que sea nuestra página web.\nRevisemos el nombre del documento Quarto*. Para que nuestro documento Quarto se publique como una página GitHub Pages, debe llamarse index.qmd (para que se genere un documento index.html), o bien, puede llamarse como queramos, pero agregando el siguiente código al header yaml del documento Quarto:\nformat: html: output-file: \u0026#34;index\u0026#34; De este modo, el documento html resultante de nuestro documento Quarto se llamará index.html.\nEl siguiente paso de configuración implica agregar un archivo de configuración al proyecto. En el panel de archivos (File) de RStudio, presionamos el botón New File y creamos un archivo de texto en blanco, llamado _quarto.yml:\nEn _quarto.yml, pegamos el siguiente código de configuración:\nproject: output-dir: docs Con esta configuración le estamos pidiendo Quarto que guarde los recursos que necesita dentro de una carpeta docs, que es lo que necesitamos para generar nuestra página web.\nSi le damos render al documento Quarto, se generará la carpeta docs con los recursos necesarios dentro.\nOtra configuración que debemos crear para GitHub Pages se hace mediante la creación de un archivo vacío. En el proyecto desde RStudio creamos un nuevo archivo que se llame .nojekyll, y que esté vacío. Este archivo es para decirle a GitHub Pages que no procese el sitio con Jekyll, porque del sitio nos encargamos nosotres.\nAhora tenemos que subir estos cambios al repositorio remoto GitHub. En la pestaña Terminal de RStudio (al lado de la consola) ejecutamos los tres siguientes comandos:\ngit add . git commit -m \u0026#34;documento quarto en docs\u0026#34; git push Con el primer comando le pedimos que todos los archivos nuevos sean considerados para el commit, con el segundo creamos el commit y le damos un mensaje, y con el tercero hacemos push para subir los cambios al repositorio remoto.\nSi vamos a GitHub debiesen estar nuestros nuevos archivos arriba. Ahora vamos a configurar GitHub para que genere una página web a partir del documento Quarto. Vamos a la seccion Settings:\nDentro de Settings, en el menú izquierdo vamos a Pages. Dentro de Pages, tenemos que seleccionar la rama del repositorio que queremos usar (usualmente main o master), y especificar que queremos apuntar a la carpeta /docs. Luego presionamos Save.\nSe tomará unos segundos o minutos en generar la página web, pero luego aparecerá el siguiente mensaje que te permitirá acceder al sitio:\n¡Listo! Ahora puedes compartir tu página con todo el mundo. El enlace será algo como https://usuario.github.io/repositorio/\nOjo que con este método sólo podremos publicar un documento Quarto por repositorio.\nPuedes ver las instrucciones completas para este proceso en esta guía oficial.\nSitios web Quarto en GitHub Pages Otra opción que tenemos para construir sitios más completos, pero también basados en documentos Quarto, donde podamos combinar texto y código, es crear un sitio web Quarto.\nCon esta modalidad de documentos Quarto podemos crear un sitio web con múltiples secciones, enlaces, página de presentación, y más, que te puede servir como un espacio en Internet para presentarte y que otras personas te encuentren, y puedan conocer tu trabajo y trayectoria.\nCrear el sitio web Quarto Al crear un nuevo proyecto desde RStudio podemos elegir la opción Quarto Website:\nSe abrirán una nueva sesión de R y veremos que nuestro proyecto ya viene con varios archivos dentro. Primero que nada, presionemos Render para previsualizar lo que tenemos como base:\nEl proyecto ya viene con un sitio web funcional, que podemos explorar. Viene con dos páginas por defecto, index.qmd y about.qmd. Ambas puedes modificarlas a tu gusto con el contenido que desees.\nEstas páginas aparecen en la barra de navegación (arriba del sitio web, o en el lado izquierdo si la pantalla/ventana es pequeña), para que tus usuarios puedan acceder a ellas.\nAgregar páginas a tu sitio Para agregar nuevas páginas al sitio, simplemente creamos nuevos documentos Quarto normalmente (New File, Quarto Document). Para hacer que sean agregados a la barra de navegación, entra al archivo de configuración de tu sitio, _quarto.yml. En este archivo, verás la configuración general de tu sitio:\nwebsite: title: \u0026#34;tutorial_sitio_web_quarto\u0026#34; navbar: left: - href: index.qmd text: Home - about.qmd Esa es la lista de páginas de tu sitio web. Al igual como sale hecho con la página about.qmd, si agregas ahí el nombre de un documento Quarto nuevo, será agregado a la barra de navegación.\nEstas páginas también pueden ser enlaces a cualquier otro sitio web. Por ejemplo, si quieres agregar un enlace a tu GitHub o a alguna red social, haz lo siguiente:\nwebsite: title: \u0026#34;tutorial_sitio_web_quarto\u0026#34; navbar: left: - href: index.qmd text: Home - about.qmd - icon: github href: https://github.com/bastianolea - icon: linkedin href: https://www.linkedin.com/in/bastianolea/ Los enlaces aparecerán con los logos de las redes sociales que definas! Sólo recuerda que los enlaces tienen que empezar con https://.\nCambiar temas Dentro del mismo documento _quarto.yml puedes cambiar el tema de tu sitio web, para darle un toque más personalizado. Puedes elegir entre 25 temas, que puedes conocer en esta guía.\nformat: html: theme: lux css: styles.css toc: true La página about.qmd también puede personalizarse.\nPublicar el sitio en GitHub Pages Para publicar el sitio en GitHub Pages tenemos que seguir las mismas instrucciones para publicar en GitHub Pages que seguimos en el paso anterior:\nPrimero, en el archivo de configuración _quarto.yml agregamos output-dir: docs debajo de project: y hacemos Render al documento index.qmd.\nTambién podemos ejecutar quarto render desde la pestaña de Terminal para reconstruir el sitio completo.\nEn el proyecto desde RStudio, creamos un nuevo archivo vacío que se llame .nojekyll, para decirle a GitHub Pages que no procese el sitio con Jekyll. Si no haces esto, cuando entres a un post del blog te aparecerá un error 404! 😨\nLuego debemos hacer que nuestro proyecto sea un repositorio git (usethis::use_git()) y subir el repositorio a GitHub (usethis::use_github()), o si ya era un repositorio git y ya estaba en GitHub, hacer git add., git commit -m \u0026quot;actualizacion\u0026quot;, y git push desde la pestaña Terminal.\nUna vez que subimos nuestros cambios al repositorio remoto, vamos al repositorio en GitHub, Settings, Pages, y configuramos el repositorio para que genere la página desde /docs:\nSiguiendo estas instrucciones ya tendrás tu sitio web básico listo! ¡Y gratis! 🥳 Ahora sólo falta hacerlo crecer agregando páginas, enlaces, y toda la información que quieras.\nBlogs Quarto en GitHub Pages Una tercera opción para presentarte al mundo por internet usando Quarto es crear un blog Quarto.\nUn blog funciona casi igual que un sitio web Quarto, con la diferencia de que el contenido está centrado en múltiples documentos Quarto que poseen más metadatos que le permiten agruparlos en categorías, en base a etiquetas, y ordenarlos por fechas. De este modo, tendrás un sitio web de presentación pero al que además podrás ir subiéndole contenido periódicamente para ir compartiendo las cosas que haces.\nRecordemos que todo lo que hemos aprendido sobre R ha sido gracias a personas que han querido compartir lo que saben, así que anímate a compartir lo que aprendes y lo que has creado!\nCrear un blog Quarto Crear un nuevo proyecto desde RStudio, elegimos la opción Quarto Blog:\nDe la misma forma que cuando creamos el sitio web Quarto, el proyecto aparecerá con los archivos necesarios para tener un blog mínimo. Si presionamos Render podremos provisionar nuestro blog:\nAgregar posts al blog Quarto El funcionamiento del blog es idéntico al del sitio web, con la distinción de que la idea es ir agregando publicaciones.\nDentro de la carpeta posts veremos que se encuentran las dos publicaciones de ejemplo que vienen con el proyecto. Si abrimos una de ellas, veremos que en su encabezado posee los metadatos que caracterizan a cada publicación:\n--- title: \u0026#34;Mi primera publicación en mi blog Quarto\u0026#34; author: \u0026#34;Bastián Olea\u0026#34; date: \u0026#34;2025-03-28\u0026#34; categories: [noticias, R, programación] image: \u0026#34;image.jpg\u0026#34; --- Entonces, para crear una nueva publicación, creamos una carpeta dentro de posts (el nombre de la carpeta será la dirección de la publicación), y dentro de la carpeta creamos un documento Quarto llamado index.qmd con un encabezado que contenga título, autor, fecha, y etiquetas.\nSi presionamos Render para generar el post, veremos que en el panel Viewer de RStudio se previsualiza nuestro blog!\nSubir el blog Quarto a GitHub Pages Nuevamente, las instrucciones para hacer que nuestro blog aparezca GitHub Pages son las mismas:\nEn _quarto.yml agregamos output-dir: docs debajo de project:. En el proyecto, creamos un nuevo archivo que se llame .nojekyll, vacío (para decirle a GitHub Pages que no procese el sitio con Jekyll) En la pestaña de terminal ejecutamos quarto render para construir el sitio completo. Creamos un repositorio git (usethis::use_git()) Subimos el repositorio a GitHub (usethis::use_github()) En GitHub, entramos a Settings, luego a Pages, y configuramos el repositorio para que genere la página desde /docs Siguiendo estas instrucciones ya tendrás tu sitio web básico listo! ¡Y gratis! 🥳 Ahora sólo falta hacerlo crecer agregando páginas, enlaces, y toda la información que quieras.\nInstrucciones para subir a GitHub: https://quarto.org/docs/publishing/github-pages.html#render-to-docs\nInstrucciones para subir a Netlify: https://beamilz.com/posts/2022-06-05-creating-a-blog-with-quarto/en/#deploy-with-netlify\nBlog Hugo Un blog Hugo es otra forma de crear un blog desde R, que también utiliza documentos Quarto, pero cuyo sistema de construcción es distinto. Al ser creados con Hugo, resultan sitios mucho más personalizables, pero por lo mismo también pueden ser más complejos de mantener.\nComo ejemplo, mi propio sitio web lo creé con Hugo, y detallé parte del proceso en un post.\nLas instrucciones de este proceso se escapan un poco al objetivo de esta guía, pero les dejo el siguiente enlace, que corresponde al tutorial oficial para crear un blog Hugo con el tema Apéro, que detalla paso por paso todas las acciones que hay que hacer para construir un blog con Hugo, personalizarlo, y publicarlo usando Netlify.\nCabe mencionar que el tutorial mismo está construido en un blog Hugo Apéro.\nTambién recomiendo este libro, Create, Publish, and Analyze Personal Websites Using R and RStudio, que detalla todas las instrucciones de crear un sitio web con R y Hugo.\nEn resumidas cuentas, las instrucciones son:\nCrear un nuevo proyecto de R install.packages(\u0026quot;blogdown\u0026quot;) blogdown::install_hugo() Y ejecutar lo siguiente para crear tu blog Hugo Apéro: library(blogdown) new_site(theme = \u0026#34;hugo-apero/hugo-apero\u0026#34;, format = \u0026#34;toml\u0026#34;, sample = FALSE, empty_dirs = TRUE) Luego ejecutas blogdown::serve_site() para previsualizar el blog creado.\nPara crear un post nuevo, tenemos una conveniente función que nos ayuda:\n# crear un post blogdown::new_post(title = \u0026#34;Nubes aleatorias en ggplot\u0026#34;, subdir = \u0026#34;blog/\u0026#34;, file = \u0026#34;blog/ggplot_nubes/index.md\u0026#34;, # define el \u0026#34;slug\u0026#34;, la dirección url del post author = \u0026#34;Bastián Olea Herrera\u0026#34;, tags = c(\u0026#34;ggplot2\u0026#34;, \u0026#34;gráficos\u0026#34;, \u0026#34;curiosidades\u0026#34;)) Apps Shiny Las aplicaciones Shiny son formas mucho más avanzadas y flexibles para poder compartir desarrollos en R con el mundo. Se trata de aplicaciones web completamente personalizables y que además son interactivas; significa que detrás de la aplicación web existe un proceso de R que está haciendo los cálculos para entregar resultados en tiempo real a sus usuarios.\nAcá te dejo dos tutoriales para aprender a usar Shiny:\nTutorial Shiny Tutorial publicar en Shinyapps Y comparto también un sitio mío (creado con Quarto y alojado en GitHub Pages) para mostrar aplicaciones Shiny qeu he creado.\nRecursos: Tutorial Git con R Tutorial GitHub Pages Tutorial Quarto Tutorial sitios web Quarto Temas Quarto Tutorial Blog Quarto Tutorial Quarto y Netlifly Este contenido fue impartido en el marco de una charla abierta organizada por el Grupo de Usuarios de R de Madrid. ¡Muchas gracias por invitarme!\n","date":"2025-03-27T00:00:00Z","excerpt":"En este tutorial veremos cuatro formas relativamente sencillas para crear nuestros propios espacios en internet con R para poder compartir nuestras creaciones y aprendizajes, de forma completamente gratuita. ¡En una tarde podrías tener tu propio sitio web para presentarte, para subir las cosas que has aprendido, o para destacar tu trabajo!","href":"https://bastianoleah.netlify.app/blog/tutorial_quarto_github_pages/","tags":"quarto ; shiny ; git ; GitHub","title":"Tutorial: crea páginas web y blogs con R+Quarto, y publícalos online con GitHub Pages"},{"content":" El paquete {patchwork} ayuda a unir y combinar gráficos de {ggplot2}. En esta guía veremos los principios del uso de este paquete, que nos permitirá construir visualizaciones más complejas\nÍndice Combinar dos gráficos lado a lado Combinar dos gráficos uno arriba del otro Combinar más de dos gráficos Poner un gráfico dentro de otro library(dplyr) library(ggplot2) library(patchwork) Primero crearemos dos gráficos de muestra, a partir del dataset iris.\ngrafico_a \u0026lt;- iris |\u0026gt; # calcular promedios por especie group_by(Species) |\u0026gt; summarize(Sepal.Length = mean(Sepal.Length)) |\u0026gt; # gráfico ggplot() + aes(x = Sepal.Length, y = Species) + # barras geom_col(fill = \u0026#34;darkslategray3\u0026#34;, width = 0.6) + theme_void() grafico_b \u0026lt;- iris |\u0026gt; # conteo por especies count(Species) |\u0026gt; # porcentaje mutate(p = n/sum(n)) |\u0026gt; # gráfico ggplot() + aes(x = 1, y = p, fill = Species) + geom_col() + # escala de color scale_fill_brewer(name = \u0026#34;Set2\u0026#34;, type = \u0026#34;qual\u0026#34;) + # gráfico de torta coord_polar(theta = \u0026#34;y\u0026#34;) + theme_void() + guides(fill = guide_legend(title = NULL)) Combinar dos gráficos lado a lado Gracias a {patchwork}, unir dos gráficos uno al lado del otro es tan sencillo como \u0026ldquo;sumarlos\u0026rdquo;:\ngrafico_a + grafico_b Si queremos ajustar las proporciones de los gráficos, agregamos la función plot_layout() para especificar las proporciones. Haremos que uno de los gráficos sea el doble de ancho que el otro:\ngrafico_a + grafico_b + plot_layout(widths = c(1, 2)) Combinar dos gráficos uno arriba del otro Para combinar dos gráficos en disposición vertical; es decir, uno arriba del otro, bsata con \u0026ldquo;dividir\u0026rdquo; los dos gráficos:\ngrafico_a / grafico_b Combinar más de dos gráficos Creemos un tercer gráfico de nuestra para probar la combinación de tres gráficos en uno solo:\ngrafico_c \u0026lt;- iris |\u0026gt; ggplot() + geom_bar(aes(Petal.Width, fill = Species)) + scale_fill_brewer(name = \u0026#34;Set2\u0026#34;, type = \u0026#34;qual\u0026#34;) + scale_y_continuous(expand = expansion(c(0, 0.1))) + theme_void() + guides(fill = guide_legend(title = NULL)) Teniendo dos gráficos, podemos disponerlos uno al lado dle otro, y el tercer gráfico debajo de los dos primeros; es decir, sumar a + b y luego dividirlos por c:\n(grafico_a + grafico_b) / grafico_c + plot_layout(guides = \u0026#34;collect\u0026#34;) En este caso agregamos una función plot_layout() para combinar las leyendas de dos de los gráficos, dado que las leyendas son iguales y sería redundante que cada gráfico las presente por separado.\nPara ajustar las proporciones en este caso, podemos primero crear la fila 1 del gráfico final, ajustando su proporción, y luego a esta fila agregarle el gráfico de abajo:\nfila_1 \u0026lt;- grafico_a + grafico_b + plot_layout(widths = c(3, 1)) fila_1 / grafico_c + plot_layout(guides = \u0026#34;collect\u0026#34;) Poner un gráfico dentro de otro grafico_d \u0026lt;- iris |\u0026gt; ggplot() + geom_bar(aes(x = 1), alpha = 0.6) + coord_polar(theta = \u0026#34;y\u0026#34;) + theme_void() También podemos necesitar insertar un gráfico dentro de otro, quizás porque uno de los gráficos representa un detalle del otro, y como tal puede que sea más conveniente disponerlo dentro del primero.\nPara insertar un gráfico dentro de otro, se agrega a un gráfico la función inset_element() con el gráfico que queremos insertar. Dentro de esta función hay que definir los argumentos top, bottom, left y right, que corresponden al perímetro en el que se ubicará el gráfico insertado.\nPara ubicar el gráfico dentro, debemos entender que el límite superior del gráfico corresponde a top = 1, y el inferior a bottom = 0, mientras que el límite izquierdo es left = 0 y el derecho es right = 1. Si usamos estos argumentos, el gráfico insertado usaría la totalidad del espacio del gráfico base:\ngrafico_c + inset_element(grafico_d, top = 1, bottom = 0, left = 0, right = 1) Notemos que las coordenadas corresponden con el centro del área del gráfico, excluyendo el área de la leyenda.\nSi ponemos que top = 0.5y right = 0.5, entonces el borde superior del gráfico insertado estará en la mitad del alto del gráfico base, y el borde derecho en la mitad del ancho; es decir, ubicándolo en la esquina inferior izquierda.\ngrafico_c + inset_element(grafico_d, top = 0.5, bottom = 0, left = 0, right = 0.5) Sabiendo esto, podemos ajustar los argumentos de inset_element() para poner el gráfico exactamente donde queremos:\ngrafico_c + inset_element(grafico_b + guides(fill = guide_none()), top = .9, bottom = .4, left = .2, right = .5) Naturalmente, podríamos combinar este gráfico con un gráfico insertado con otro gráfico más, o con la cantidad que se nos ocurra:\ngrafico_c + guides(fill = guide_legend(position = \u0026#34;inside\u0026#34;, title = NULL)) + theme(legend.position.inside = c(.8, .7)) + inset_element(grafico_b + guides(fill = guide_none()), top = 1, bottom = .4, left = .2, right = .6) + (grafico_a / grafico_a / grafico_a / grafico_a / grafico_a / grafico_a) Otra forma de insertar un gráfico dentro de otro es ubicándolo a una cierta distancia desde un borde; en este caso, insertaremos un gráfico que solo contiene un texto (el año) en al esquina superior derecha, y para ello, definiremos que el borde izquierdo de la figura insertada inset_element() se ubique en el borde izquierdo del gráfico menos 3 centímetros, el borde derecho de la figura insertada en el límite derecho del gráfico, el borde superior en el límite superior del gráfico, y el borde inferior de la figura a 2 centímetros menos del límite superior del gráfico.\n# gráfico vacío que solo contiene un texto texto_año \u0026lt;- ggplot() + annotate(\u0026#34;text\u0026#34;, x = 1, y = 1, label = 2025, size = 7) + theme_void() grafico_c + inset_element( texto_año, left = unit(1, \u0026#39;npc\u0026#39;) - unit(3, \u0026#39;cm\u0026#39;), right = 1, top = 1, bottom = unit(1, \u0026#39;npc\u0026#39;) - unit(2, \u0026#39;cm\u0026#39;), align_to = \u0026#34;full\u0026#34;) Podemos usar esta técnica para insertar cualquier texto en cualquier posición de un gráfico, incluso fuera de los límites de las escalas (que sería una limitación de hacer lo mismo con annotate().\nLa gracia de usar inset_element() es que posicionamos los elementos con respecto al tamaño del gráfico (donde el borde izquierdo es 0 y el derecho es 1, y el borde inferior es 0 y el superior es 1) y no a las coordenadas de las variables x e y, lo que nos permite ubicar los elementos de forma independiente al sistema de coordenadas del gráfico.\n","date":"2025-03-08T00:00:00Z","excerpt":"El paquete `{patchwork}` ayuda a unir y combinar múltiples gráficos de {ggplot2}. En esta guía veremos los principios del uso de este paquete, que nos permitirá construir visualizaciones más densas, por medio de la combinación de gráficos en una sola visualización, y la inserción de gráficos dentro de otros.","href":"https://bastianoleah.netlify.app/blog/patchwork/","tags":"visualización de datos ; gráficos ; ggplot2","title":"Unir y combinar gráficos `{ggplot2}` con `{patchwork}`"},{"content":"El uso del color es clave para comunicar, y el ecosistema de R tiene varios trucos convenientes para ayudarnos a usar el color de mejores formas.\nEn R, los colores se escriben como código, y a grandes rasgos pueden ser colores con nombre (por ejemplo, \u0026quot;purple\u0026quot;), colores hexadecimales (escritos como códigos de al menos 6 dígitos, como #FFFFFF), o como parte de funciones que producen paletas de colores.\nlibrary(shades) library(scales) Previsualizar colores A lo largo de este post usaremos la función swatch() del paquete {shades}, que genera un gráfico que presenta el color o la paleta de colores a partir de un vector de colores, lo que nos ayudará a visualizar nuestros colores más fácil. Una alternativa es la función show_col() de {scales}, que hace lo mismo.\ncolores \u0026lt;- c(\u0026#34;#EAD2FA\u0026#34;, \u0026#34;#9069C0\u0026#34;, \u0026#34;#6E3A98\u0026#34;) shades::swatch(colores) scales::show_col(colores) Crear colores La forma más básica de elegir un color en R es por su nombre. En R existen 657 colores con nombre. En la siguiente imagen puedes ver los principales:\nPara usarlos, simplemente usa su nombre:\ncolores \u0026lt;- c(\u0026#34;indianred\u0026#34;, \u0026#34;steelblue\u0026#34;, \u0026#34;grey60\u0026#34;) swatch(colores) Casi todos estos colores pueden ser modificados agregando un número del 1 al 4 al final del nombre; por ejemplo, mediumorchid puede hacerse levemente más claro o más oscuro:\nescala \u0026lt;- c(\u0026#34;mediumorchid\u0026#34;, \u0026#34;mediumorchid1\u0026#34;, \u0026#34;mediumorchid2\u0026#34;, \u0026#34;mediumorchid3\u0026#34;, \u0026#34;mediumorchid4\u0026#34;) swatch(escala) Los grises (gray) tienen la particularidad de que puedes ponerles un número entre 10 y 99 para ajustar su brillo:\nescala \u0026lt;- c(\u0026#34;gray10\u0026#34;, \u0026#34;gray30\u0026#34;, \u0026#34;gray50\u0026#34;, \u0026#34;gray70\u0026#34;, \u0026#34;gray90\u0026#34;) swatch(escala) También puedes escribir un color definiendo su tonalidad (hue), saturación (saturation) y brillo (value) con hsv(), entendiendo que el matiz es la posición del color en la escala de todos los colores, que va del 0 al 1, empezando y terminando con el rojo:\ncolor \u0026lt;- hsv(h = 0, s = 1, v = 1) swatch(color) Para guiarse, la siguiente gráfica muestra la tonalidad de colores entre 0 y 1,\nSiguiendo el gráfico anterior, vemos que el tono 0.8 corresponde al color morado, así que podemos crearlo con hsv():\ncolor \u0026lt;- hsv(0.85, 1, 1) swatch(color) Luego podemos modificar la saturación y brillo del color con los otros dos argumentos de hsv():\ncolor \u0026lt;- hsv(0.82, 0.5, 0.4) swatch(color) Extender paletas de colores Si tienes un vector de colores y necesitas alargarlo para tener más colores basados en la paleta original, puedes hacerlo con la función colorRampPalette(). Esta función crea otra función a partir de los colores, a la que luego le das el número de colores que necesites obtener a partir de la paleta original:\n# paleta de 5 colores paleta \u0026lt;- c(\u0026#34;#f4b43f\u0026#34;, \u0026#34;#ec6a2d\u0026#34;, \u0026#34;#cc3b7b\u0026#34;, \u0026#34;#705ce6\u0026#34;, \u0026#34;#668cf6\u0026#34;) swatch(paleta) # extender la paleta de 5 colores a 12 colores colorRampPalette(paleta)(12) |\u0026gt; swatch() También podemos usar esta función para crear con facilidad una paleta secuencial entre dos o más colores:\ncolores \u0026lt;- c(\u0026#34;#df65b2\u0026#34;, \u0026#34;#fae55f\u0026#34;) # extender la paleta a 8 colores colorRampPalette(colores)(8) |\u0026gt; swatch() Paletas de colores Varios paquetes de R contienen sus propias paletas de colores prediseñadas. Uno de los conjuntos de paletas principales en visualización de datos, sobre todo para mapas, son las de Color Brewer, a las que puedes acceder con el paquete {RColorBrewer}:\nRColorBrewer::display.brewer.all() Cuando elijas una de las paletas, puedes usarla en cualquier gráfico de {ggplot2} con la función scale_color_brewer() o scale_fill_brewer(), según corresponda:\nlibrary(ggplot2) iris |\u0026gt; ggplot() + aes(x = Sepal.Length, y = Sepal.Width, color = Species) + geom_point(size = 4, alpha = 0.7) + scale_color_brewer(palette = \u0026#34;PuRd\u0026#34;) + theme_classic() Con el paquete {colorspace} también podemos ver otras paletas disponibles:\nlibrary(colorspace) colorspace::hcl_palettes(plot = TRUE) Usar estas paletas en {ggplot2} es tan fácil como agregar la función de escala apropiada para definir los colores del gráfico:\niris |\u0026gt; ggplot() + aes(Petal.Width, Sepal.Width, color = Sepal.Length) + geom_point(size = 4, alpha = 0.7) + colorspace::scale_color_continuous_sequential(palette = \u0026#34;Sunset\u0026#34;) + scale_y_continuous(expand = expansion(c(0, 0.1))) + theme_classic() Encuentro una lista que compila todas las paletas de colores de la comunidad de R en este repositorio.\nPaletas secuenciales Las paletas secuenciales consiste en un degradado entre dos o más colores. Suelen usarse para representar una variable continua o numérica, cuyo valor va cambiando de forma cuantitativa.\nLa función sequential_hcl() del paquete {colorspace} permite crear paletas secuenciales\ncolorspace::sequential_hcl(8, h = 300) |\u0026gt; swatch() colorspace::sequential_hcl(8, h = c(300, 100)) |\u0026gt; swatch() colorspace::sequential_hcl(5, h = 260, c = c(45, 25), l = c(25, 85), power = .9) |\u0026gt; swatch() También se pueden obtener vectores de colores a partir de las paletas existentes que vienen con el paquete {colorspace}:\ncolorspace::sequential_hcl(5, palette = \u0026#34;Red-Blue\u0026#34;) |\u0026gt; swatch() colorspace::sequential_hcl(5, palette = \u0026#34;Purple-Orange\u0026#34;) |\u0026gt; swatch() Paletas cualitativas Como su nombre ética, en las paletas cualitativas los colores van saltando para maximizar la diferencia entre ellos. Se utilizan para variables cualitativas, categóricas o discretas, donde cada elemento de una secuencia es independiente de los demás, y el objetivo del uso del color es poder distinguirlos.\nLa función rainbow_hcl() de {colorspace} entrega una típica paleta de arcoíris, pero con la posibilidad de modificar sus atributos de color en sus argumentos, tales como las tonalidades (hue) de inicio o final, la intensidad (chroma) de los tonos\ncolorspace::rainbow_hcl(7, c = 70) |\u0026gt; swatch() colorspace::rainbow_hcl(7, c = 100, start = 190, end = 380) |\u0026gt; swatch() colorspace::rainbow_hcl(6, c = 60, l = 30, start = 230, end = 370) |\u0026gt; swatch() Éste tipo de paletas usualmente reúne colores en una escala tipo arcoíris, o bien reúne colores temáticos, distintos entre ellos, pero armónicos entre sí.\nTambién pueden usarse los nombres de las paredes preexistentes para generar una secuencia cualitativa con ellos.\ncolorspace::qualitative_hcl(6, palette = \u0026#34;Cold\u0026#34;, c = 80) |\u0026gt; swatch() colorspace::qualitative_hcl(6, palette = \u0026#34;Warm\u0026#34;, c = 80) |\u0026gt; swatch() Paletas divergentes Las paletas divergentes se utilizan cuando una variable expresa a dos polos, una una misma magnitud donde los extremos son separados por una brecha central.\ncolorspace::diverging_hcl(n = 5, h = c(200, 300)) |\u0026gt; swatch() colorspace::diverging_hcl(n = 7, h = c(700, 180)) |\u0026gt; swatch() colorspace::diverging_hcl(n = 7, h = c(700, 180), c = 130, alpha = .7) |\u0026gt; swatch() Modificar colores Las funciones del paquete {shades} nos permitan obtener información detallada sobre cada uno de los colores, y usar esta misma información para modificarlos con mucho detalle.\nPor ejemplo, definamos un color, y luego obtengamos el valor de su tonalidad. Recordemos que la tonalidad de los colores se expresan como grados entre 0° y 360°.\nlibrary(shades) color \u0026lt;- \u0026#34;#f65b74\u0026#34; swatch(color) hue(color) [1] 350.3226 Obtenemos que, para el color definido, el valor de su tonalidad es 350. Podemos usar esta información para modificar levemente el mismo color y así obtener una variable del mismo color levemente más anaranjada.\nswatch(c(color, hue(color, 370))) Podemos obtener mismos resultados utilizando el delta de la tonalidad del color; es decir, sumándole restándole una cantidad de grados a el valor de la tonalidad del color mismo:\nswatch(c(color, hue(color, delta(50)))) Al usar la función delta(), lo que hacemos es pedirle que cambie la tonalidad del color en 50°, volviéndose en un tono amarillo.\nPodemos obtener un resultado similar usando col_shift() del paquete {scales}:\nlibrary(scales) show_col(c(color, col_shift(color, 20))) El brillo (brighness) va de cero a uno, mientras que la claridad (lightness) va de cero a 100.\ncolor |\u0026gt; brightness(0.7) |\u0026gt; swatch() color |\u0026gt; lightness(delta(20)) |\u0026gt; swatch() Con {scales}, la función col_lighter() realiza el mismo propósito:\ncol_lighter(color, 20) |\u0026gt; show_col() Por su parte, la saturación aumenta la intensidad del color.\ncolor |\u0026gt; saturation(delta(30)) |\u0026gt; swatch() Podemos utilizar la función delta() para crear una sencilla paleta de colores a partir de un mismo color, aumentando y disminuyendo su intensidad (chroma):\nswatch( c(color |\u0026gt; chroma(delta(30)), color, color |\u0026gt; chroma(delta(-30))) ) En {scales}, la función es col_saturate():\ncol_saturate(color, -50) |\u0026gt; show_col() Podemos combinar estas técnicas para crear una paleta de colores más compleja, construida toda a partir de un solo color al cual se le va aumentando o disminuyendo sus valores de claridad e intensidad. El beneficio de hacerlo de esta manera es que luego basta con cambiar el color principal para obtener una paleta de iguales características, pero basada en una tonalidad distinta.\ncolor_principal = \u0026#34;#4D4484\u0026#34; color_fondo = color_principal |\u0026gt; lightness(13) |\u0026gt; chroma(20) color_detalle = color_principal |\u0026gt; lightness(20) |\u0026gt; chroma(40) color_destacado = color_principal |\u0026gt; lightness(50) |\u0026gt; chroma(65) color_texto = color_principal |\u0026gt; lightness(80) swatch(c(color_principal, color_fondo, color_detalle, color_destacado, color_texto)) color_principal = \u0026#34;#3170ac\u0026#34; color_fondo = color_principal |\u0026gt; lightness(13) |\u0026gt; chroma(20) color_detalle = color_principal |\u0026gt; lightness(20) |\u0026gt; chroma(40) color_destacado = color_principal |\u0026gt; lightness(50) |\u0026gt; chroma(65) color_texto = color_principal |\u0026gt; lightness(80) swatch(c(color_principal, color_fondo, color_detalle, color_destacado, color_texto)) Notar que el código es igual, y sólo se cambió el valor del color_principal. Esta estrategia es muy útil si se están produciendo visualizaciones o aplicaciones que ocupan una paleta de colores monocroma.\nMezclar colores Las funciones submix() y addmix() del paquete {shades} facilitan el mezclado de colores sustraje ctivo y aditivo, respectivamente. A partir de dos colores, entrega la mezcla de ellos, abriendo muchas posibilidades para la experimentación y creación de nuevos colores:\nswatch(c(\u0026#34;#70f1d5\u0026#34;, submix(\u0026#34;#70f1d5\u0026#34;, \u0026#34;#fae55f\u0026#34;), \u0026#34;#fae55f\u0026#34;)) swatch(c(\u0026#34;#3377f7\u0026#34;, addmix(\u0026#34;#3377f7\u0026#34;, \u0026#34;#ec4e3c\u0026#34;), \u0026#34;#ec4e3c\u0026#34;)) swatch(c(\u0026#34;#f9ce45\u0026#34;, submix(\u0026#34;#f9ce45\u0026#34;, \u0026#34;#77d671\u0026#34;, amount = 0.5), \u0026#34;#77d671\u0026#34;)) El paquete {scales} también provee una función para mezclar colores. Se puede usar esta función para tomar una paleta de colores y volverla más coherente al aplicarle una pequeña fracción de otro color, en este caso naranja:\ncol_mix(a = c(\u0026#34;#77d671\u0026#34;, \u0026#34;#70f1d5\u0026#34;, \u0026#34;#fae55f\u0026#34;, \u0026#34;#ff479c\u0026#34;), b = \u0026#34;orange2\u0026#34;, amount = 0.2) |\u0026gt; show_col() Usar paletas de colores en {ggplot2} Muchas de estos paquetes incorporan funciones de escalas de colores (scale_color_x(), scale_fill_x()) para aplicar una paleta de color fácilmente a un gráfico creado {ggplot2}.\nlibrary(ggplot2) library(dplyr) iris |\u0026gt; ggplot() + geom_bar(aes(Petal.Width, fill = Species)) + colorspace::scale_fill_discrete_qualitative(palette = \u0026#34;Dark 3\u0026#34;) + scale_y_continuous(expand = expansion(c(0, 0.1))) + theme_classic() iris |\u0026gt; ggplot() + geom_point(aes(Sepal.Width, Sepal.Length, color = Petal.Width, size = Petal.Length), alpha = .8) + colorspace::scale_color_continuous_sequential(palette = \u0026#34;Sunset\u0026#34;, na.value = \u0026#34;white\u0026#34;) + theme_classic() + guides(size = guide_legend(override.aes = list(color = \u0026#34;#784FA1\u0026#34;)), color = guide_colorsteps()) + theme(legend.title = element_blank(), axis.title = element_blank()) iris |\u0026gt; ggplot() + geom_point(aes(Petal.Length, Sepal.Width, color = Petal.Width, size = Sepal.Length), alpha = .8) + viridis::scale_colour_viridis(\u0026#34;viridis\u0026#34;, na.value = \u0026#34;white\u0026#34;) + theme_classic() + guides(size = guide_legend(override.aes = list(color = \u0026#34;#88D181\u0026#34;)), color = guide_colorsteps()) + theme(legend.title = element_blank(), axis.title = element_blank()) Algunas de las funciones para aplicar paletas de colores tienen funcionalidades extras. Por ejemplo, las funciones de {colorspace} permiten modificar sus paletas en términos de la saturación (chroma) y el brillo del color (luminance), entregándote más libertad al momento de definir una apariencia específica:\ngrafico \u0026lt;- iris |\u0026gt; ggplot() + geom_point(aes(Sepal.Width, Sepal.Length, color = Petal.Width), size = 3, alpha = .8) + theme_classic() + guides(color = guide_colorsteps()) + theme(legend.title = element_blank(), axis.title = element_blank()) grafico + colorspace::scale_color_continuous_sequential( palette = \u0026#34;TealGrn\u0026#34;, c1 = 50, # intensidad del color l1 = 60) # brillo del color grafico + colorspace::scale_color_continuous_sequential( palette = \u0026#34;TealGrn\u0026#34;, c1 = 20, # intensidad del color l1 = 30) # brillo del color Si quieres aprender visualización de datos con {ggplot2}, puedes revisar este tutorial sobre visualización de datos desde cero! Avanzado {colorspace} incluye funciones para poder visualizar secuencias de colores en proyecciones del espacio de color HCL (hue, chroma, luminance), lo que nos permite contextualizar las paletas en un espacio perceptual del color basado en estos tres parámetros.\ncolorspace::hclplot(sequential_hcl(7, h = 260, c = 80, l = c(35, 95), power = 1.5)) colorspace::hclplot(sequential_hcl(7, h = c(260, 220), c = c(50, 75, 0), l = c(30, 95), power = 1)) Fuentes y recursos https://github.com/EmilHvitfeldt/r-color-palettes https://r-graph-gallery.com/ggplot2-color.html https://www.datanovia.com/en/blog/top-r-color-palettes-to-know-for-great-data-visualization/ https://jbengler.github.io/tidyplots/articles/Color-schemes.html ","date":"2025-03-06T00:00:00Z","excerpt":"El uso del color es clave para comunicar, y el ecosistema de R tiene varios trucos convenientes para ayudarnos a usar el color de mejores formas. En este post reúno varios consejos y trucos para trabajar con colores: desde previsualizarlos, mezclarlos, combinarlos y usarlos como paletas en gráficos.","href":"https://bastianoleah.netlify.app/blog/colores/","tags":"visualización de datos ; ggplot2","title":"Creación y personalización de colores y paletas en R"},{"content":"Visualizar un mapa de Chile puede ser complicado debido a su largo. Muchas veces cuesta ubicar correctamente el mapa por el espacio vertical que requiere. Pero en ciertos casos puede ser conveniente visualizar a Chile de lado, para aprovechar el espacio horizontal.\nEn esta guía veremos cómo rotar un mapa de Chile 90° hacia la izquierda en R para que quede acostado 💤🌙\nSi necesitas aprender en profundidad la visualización de mapas con R, revisa mi tutorial de mapas y datos espaciales con {sf}. Primero cargamos los paquetes necesarios:\nlibrary(sf) # manejo de datos espaciales library(chilemapas) # mapas de Chile library(ggplot2) # visualización de datos library(dplyr) # manejo de datos tabulares library(readr) # cargar datos Obtenemos un mapa de Chile gracias al paquete {chilemapas}; en este caso un mapa del país por regiones:\n# obtener mapa mapa_region \u0026lt;- chilemapas::generar_regiones() mapa_region Simple feature collection with 16 features and 1 field Geometry type: GEOMETRY Dimension: XY Bounding box: xmin: -109.4499 ymin: -56.52511 xmax: -66.41617 ymax: -17.49778 Geodetic CRS: SIRGAS 2000 # A tibble: 16 × 2 codigo_region geometry * \u0026lt;chr\u0026gt; \u0026lt;GEOMETRY [°]\u0026gt; 1 01 POLYGON ((-68.86081 -21.28512, -68.7581 -21.21752, -68.65677 -… 2 02 MULTIPOLYGON (((-68.98863 -25.38016, -68.98522 -25.37566, -68.… 3 03 MULTIPOLYGON (((-70.68641 -26.15053, -70.68923 -26.15726, -70.… 4 04 MULTIPOLYGON (((-71.66962 -30.34526, -71.67234 -30.34574, -71.… 5 05 MULTIPOLYGON (((-71.67929 -33.44583, -71.68012 -33.448, -71.67… 6 06 POLYGON ((-71.1344 -34.78711, -71.12134 -34.80128, -71.09905 -… 7 07 POLYGON ((-72.1032 -36.12348, -72.09964 -36.12574, -72.09894 -… 8 08 MULTIPOLYGON (((-71.41259 -38.10669, -71.3922 -38.098, -71.383… 9 09 MULTIPOLYGON (((-73.35579 -38.73982, -73.35306 -38.73343, -73.… 10 10 MULTIPOLYGON (((-73.6175 -41.8142, -73.61389 -41.80392, -73.61… 11 11 MULTIPOLYGON (((-74.34857 -45.02053, -74.34886 -45.02632, -74.… 12 12 MULTIPOLYGON (((-71.18405 -52.8089, -71.17569 -52.80759, -71.1… 13 13 POLYGON ((-70.47405 -33.8624, -70.47327 -33.86269, -70.46068 -… 14 14 MULTIPOLYGON (((-71.65597 -40.35386, -71.65874 -40.34691, -71.… 15 15 POLYGON ((-70.35079 -18.8362, -70.34707 -18.83939, -70.34351 -… 16 16 POLYGON ((-72.38553 -36.91169, -72.37685 -36.91617, -72.37034 … # visualizar mapa_region |\u0026gt; ggplot(aes()) + geom_sf() + # recortar coordenadas horizontales coord_sf(xlim = c(-80, -62)) Cargamos algunos datos regionales para ponerle al mapa, sacados de mi proyecto de visualización de datos económicos de Chile:\n# obtener datos datos \u0026lt;- read_csv2(\u0026#34;https://github.com/bastianolea/economia_chile/raw/main/app/datos/pib_regional.csv\u0026#34;) # https://github.com/bastianolea/economia_chile # limpiar datos datos_2 \u0026lt;- datos |\u0026gt; group_by(serie) |\u0026gt; slice_max(año) |\u0026gt; slice_max(mes) |\u0026gt; select(nombre_region = serie, valor, año, trimestre, mes) # crear tabla de regiones regiones \u0026lt;- tribble(~codigo_region, ~nombre_region, \u0026#34;01\u0026#34;, \u0026#34;Región de Arica y Parinacota\u0026#34;, \u0026#34;02\u0026#34;, \u0026#34;Región de Tarapacá\u0026#34;, \u0026#34;03\u0026#34;, \u0026#34;Región de Antofagasta\u0026#34;, \u0026#34;04\u0026#34;, \u0026#34;Región de Atacama\u0026#34;, \u0026#34;05\u0026#34;, \u0026#34;Región de Coquimbo\u0026#34;, \u0026#34;06\u0026#34;, \u0026#34;Región de Valparaíso\u0026#34;, \u0026#34;07\u0026#34;, \u0026#34;Región Metropolitana de Santiago\u0026#34;, \u0026#34;08\u0026#34;, \u0026#34;Región del Libertador General Bernardo OHiggins\u0026#34;, \u0026#34;09\u0026#34;, \u0026#34;Región del Maule\u0026#34;, \u0026#34;10\u0026#34;, \u0026#34;Región de Ñuble\u0026#34;, \u0026#34;11\u0026#34;, \u0026#34;Región del Biobío\u0026#34;, \u0026#34;12\u0026#34;, \u0026#34;Región de La Araucanía\u0026#34;, \u0026#34;13\u0026#34;, \u0026#34;Región de Los Ríos\u0026#34;, \u0026#34;14\u0026#34;, \u0026#34;Región de Los Lagos\u0026#34;, \u0026#34;15\u0026#34;, \u0026#34;Región de Aysén del General Carlos Ibáñez del Campo\u0026#34;, \u0026#34;16\u0026#34;, \u0026#34;Región de Magallanes y de la Antártica Chilena\u0026#34;) Ahora que tenemos los datos listos, los agregamos al mapa usando un left_join():\n# agregar regiones y datos al mapa mapa_datos \u0026lt;- mapa_region |\u0026gt; left_join(regiones, by = join_by(codigo_region)) |\u0026gt; left_join(datos_2, by = join_by(nombre_region)) Finalmente, previsualizamos el mapa con los datos agregados:\n# visualizar mapa con datos mapa_datos |\u0026gt; ggplot() + aes(fill = valor) + geom_sf(linewidth = 0) + coord_sf(xlim = c(-80, -62)) + scale_fill_distiller(type = \u0026#34;seq\u0026#34;, palette = 12, labels = scales::label_comma(big.mark = \u0026#34;.\u0026#34;)) + theme_classic() + theme(axis.text = element_blank(), axis.line = element_blank(), axis.ticks = element_blank()) Ahora que tenemos un mapa de Chile con datos regionales, procedemos a rotar el mapa. Para esto, necesitamos una matriz de rotación, respecto de la cual no hay mucho que entender, salvo que nos permitirá multiplicar la geometría del mapa para obtener como resultado la misma geometría, pero rotada. El único detalle que hay que considerar es que es necesario cambiar la proyección del mapa para que la zona sur del país no se vea deformada.\n# reprojectar a CRS EPSG:5361 para evitar deformación mapa_proyectado \u0026lt;- st_transform(mapa_datos, 5361) # matriz de rotación 90° izquierda rotacion \u0026lt;- matrix(c(0, -1, 1, 0), 2, 2) # aplicar rotación al mapa proyectado mapa_rotado \u0026lt;- mapa_proyectado |\u0026gt; mutate(geometry = geometry * rotacion) Ahora visualizamos el mapa reproyectado y rotado:\nmapa_rotado |\u0026gt; ggplot() + aes(fill = valor) + geom_sf(linewidth = 0) + scale_y_continuous(labels = scales::label_number()) + coord_sf(ylim = c(800000, -100000)) + labs(title = \u0026#34;Mapa de Chile horizontal\u0026#34;, subtitle = \u0026#34;A mimir\u0026#34;) + scale_fill_distiller(type = \u0026#34;seq\u0026#34;, palette = 12, labels = scales::label_comma(big.mark = \u0026#34;.\u0026#34;)) + guides(fill = guide_legend(position = \u0026#34;bottom\u0026#34;)) + theme_classic() + theme(axis.text = element_blank(), axis.line = element_blank(), axis.ticks = element_blank()) Listo! Revisa el código completo en el siguiente botón para poder copiarlo y pegarlo en tu proyecto:\nVer código completo Fuentes DeepSeek DeepThink (R1) https://gist.github.com/ryanpeek/99c6935ae51429761f5f73cf3b027da2 https://r-spatial.github.io/sf/articles/sf3.html#affine-transformations https://en.wikipedia.org/wiki/Rotation_matrix ","date":"2025-03-04T00:00:00Z","excerpt":"Visualizar un mapa de Chile puede ser complicado debido a su largo. Muchas veces cuesta ubicar correctamente el mapa por el espacio vertical que requiere. Pero en ciertos casos puede ser conveniente visualizar a Chile _de lado_, para aprovechar el espacio horizontal. En esta guía veremos cómo rotar un mapa de Chile 90° hacia la izquierda en R para que quede acostado.","href":"https://bastianoleah.netlify.app/blog/mapa_chile_horizontal/","tags":"mapas ; visualización de datos ; Chile","title":"Rotar un mapa de Chile en R para que quede horizontal"},{"content":"Un problema común al visualizar datos georeferenciados o mapas coropléticos (con colores en las zonas geográficas que se corresponden con los datos) yace en que usamos mapas que tienen geometrías o características geográficas mucho más detalladas de lo que necesitamos. Este exceso de detalle puede jugarle en contra a la visualización que estamos intentando crear, ya sea porque dificulta la interpretación, o complejiza visualmente el gráfico.\nOtro problema de trabajar con mapas muy detallados es que la velocidad con la que se generan se ve impactada debido al detalle, lo que resulta inconveniente dado que al visualizar datos usualmente nos encontramos iterando decenas de veces una misma visualización hasta que se vea como queremos.\nEsto puede ocurrir cuando mapas desde Shapes u otras fuentes cuyo objetivo es representar fidedignamente los territorios; pero al visualizar datos generalmente necesitamos visualizaciones que no requieren exactitud milimétrica en sus polígonos.\nGeneremos un mapa de regiones de Chile, usando las geometrías del paquete {chilemapas}:\nlibrary(dplyr) library(sf) library(ggplot2) library(chilemapas) # generar mapa de regiones mapa_regiones \u0026lt;- chilemapas::mapa_comunas |\u0026gt; st_set_geometry(chilemapas::mapa_comunas$geometry) |\u0026gt; select(codigo_comuna, codigo_region, geometry) |\u0026gt; summarize(geometry = st_union(geometry), .by = codigo_region) mapa_regiones Simple feature collection with 16 features and 1 field Geometry type: GEOMETRY Dimension: XY Bounding box: xmin: -109.4499 ymin: -56.52511 xmax: -66.41617 ymax: -17.49778 Geodetic CRS: SIRGAS 2000 # A tibble: 16 × 2 codigo_region geometry \u0026lt;chr\u0026gt; \u0026lt;GEOMETRY [°]\u0026gt; 1 01 POLYGON ((-69.93023 -21.4246, -69.92376 -21.42622, -69.91932 -… 2 02 MULTIPOLYGON (((-68.0676 -24.32856, -67.91698 -24.26902, -67.8… 3 03 MULTIPOLYGON (((-71.58497 -29.02456, -71.58844 -29.02838, -71.… 4 04 MULTIPOLYGON (((-70.54551 -31.30742, -70.53877 -31.30074, -70.… 5 05 MULTIPOLYGON (((-71.33832 -33.45237, -71.33763 -33.44836, -71.… 6 06 POLYGON ((-71.5477 -34.87458, -71.54211 -34.87581, -71.53566 -… 7 07 POLYGON ((-70.41724 -35.63022, -70.41108 -35.6302, -70.40146 -… 8 08 MULTIPOLYGON (((-73.53466 -36.97378, -73.53245 -36.97829, -73.… 9 09 MULTIPOLYGON (((-73.35306 -38.73343, -73.35396 -38.72799, -73.… 10 10 MULTIPOLYGON (((-73.1691 -41.87755, -73.16135 -41.87781, -73.1… 11 11 MULTIPOLYGON (((-75.41754 -48.73857, -75.43249 -48.74372, -75.… 12 12 MULTIPOLYGON (((-70.35563 -52.94478, -70.34688 -52.93971, -70.… 13 13 POLYGON ((-70.47405 -33.8624, -70.47327 -33.86269, -70.46068 -… 14 14 MULTIPOLYGON (((-73.39503 -39.88698, -73.39672 -39.89339, -73.… 15 15 POLYGON ((-69.07223 -19.02723, -69.06394 -19.02607, -69.04748 … 16 16 POLYGON ((-72.38553 -36.91169, -72.37685 -36.91617, -72.37034 … mapa_regiones |\u0026gt; ggplot() + aes() + geom_sf() Vemos que en la zona sur del país, el detalle del mapa es tal, que los cientos de islas se vuelven en manchas grises debido a sus bordes demasiado detallados.\nmapa_regiones |\u0026gt; ggplot() + aes() + geom_sf() + coord_sf(xlim = c(-80, -65), ylim = c(-42, -56)) Podemos usar el paquete {rmapshaper} para simplificar las geometrías del mapa, bajando así el nivel de detalle de la visualización resultante:\nmapa_regiones_simple \u0026lt;- mapa_regiones |\u0026gt; # simplificar geometrías mutate(geometry = rmapshaper::ms_simplify(geometry, keep = 0.05)) mapa_regiones_simple |\u0026gt; ggplot() + aes() + geom_sf() mapa_regiones_simple |\u0026gt; ggplot() + aes() + geom_sf() + coord_sf(xlim = c(-80, -65), ylim = c(-42, -56)) En la función ms_simplify(), el valor del argumento keep define la calidad resultante del mapa. Si el valor es menor, el mapa tendrá menos detalle.\nmapa_regiones |\u0026gt; mutate(geometry = rmapshaper::ms_simplify(geometry, keep = 0.01)) |\u0026gt; ggplot() + aes() + geom_sf() + coord_sf(xlim = c(-80, -65), ylim = c(-42, -56)) En la función ms_simplify(), el valor del argumento keep define la calidad resultante del mapa. Si el valor es menor, el mapa tendrá menos detalle.\nEn estos tres mapas comparamos las diferencias entre el mapa original, con detalle al 0.1, y con detalle al 0.05:\nnormal \u0026lt;- mapa_regiones |\u0026gt; # mutate(geometry = rmapshaper::ms_simplify(geometry, keep = 0.1)) |\u0026gt; ggplot() + aes() + geom_sf() + coord_sf(xlim = c(-80, -65), ylim = c(-42, -56)) medio \u0026lt;- mapa_regiones |\u0026gt; mutate(geometry = rmapshaper::ms_simplify(geometry, keep = 0.1)) |\u0026gt; ggplot() + aes() + geom_sf() + coord_sf(xlim = c(-80, -65), ylim = c(-42, -56)) bajo \u0026lt;- mapa_regiones |\u0026gt; mutate(geometry = rmapshaper::ms_simplify(geometry, keep = 0.05)) |\u0026gt; ggplot() + aes() + geom_sf() + coord_sf(xlim = c(-80, -65), ylim = c(-42, -56)) library(patchwork) normal + medio + bajo Con respecto a la velocidad de generación de los mapas, realizamos una prueba de rendimiento que compare la velocidad de guardado de dos mapas, uno normal y uno simplificado:\nbench::mark(iterations = 20, check = FALSE, normal = ggsave(plot = mapa_regiones |\u0026gt; ggplot() + geom_sf(), filename = \u0026#34;a.jpg\u0026#34;), simple = ggsave(plot = mapa_regiones_simple |\u0026gt; ggplot() + geom_sf(), filename = \u0026#34;b.jpg\u0026#34;) ) # A tibble: 2 × 6 expression min median `itr/sec` mem_alloc `gc/sec` \u0026lt;bch:expr\u0026gt; \u0026lt;bch:tm\u0026gt; \u0026lt;bch:tm\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;bch:byt\u0026gt; \u0026lt;dbl\u0026gt; 1 normal 143.7ms 150.1ms 6.47 29.31MB 10.4 2 simple 75.2ms 78.8ms 12.6 2.46MB 11.4 Según la prueba, el mapa simplificado se genera aproximadamente el doble de rápido.\nTambién podemos compara el uso de memoria de ambos objetos:\nobject.size(mapa_regiones) |\u0026gt; print(units = \u0026#34;auto\u0026#34;) 2.1 Mb object.size(mapa_regiones_simple) |\u0026gt; print(units = \u0026#34;auto\u0026#34;) 178.2 Kb El mapa simplificado consume un 10% de memoria con respecto al mapa original.\nSi te interesa el tema de los mapas, en otros tutoriales hemos visto cómo hacer mapas de Chile con R, tanto comunales como regionales, así como también de la zona urbana de la Región Metropolitana. Además aprendimos cómo agregar características espaciales como calles y autopistas obtenidas desde Open Street Map.\n","date":"2025-02-27T00:00:00Z","excerpt":"Un problema común al visualizar datos georeferenciados o mapas coropléticos (con colores en las zonas geográficas que se corresponden con los datos) yace en que usamos mapas que tienen geometrías o características geográficas mucho más detalladas de lo que necesitamos. Este exceso de detalle puede jugarle en contra a la visualización que estamos intentando crear, ya sea porque dificulta la interpretación, o complejiza visualmente el gráfico. En esta guía aprenderemos a simplificar mapas en R para producir visualizaciones con el nivel apropiado de detalle, y hacer más rápida la generación de mapas.","href":"https://bastianoleah.netlify.app/blog/simplificar_mapas/","tags":"mapas ; visualización de datos","title":"Simplificar la geometría de los polígonos de un mapa en R"},{"content":" Hace poco conocí el paquete {mall}, que facilita mucho el uso de un un modelo de lenguaje (LLM) local como una herramienta cotidiana para el análisis y procesamiento de datos.\nEl paquete incluye varias funciones para usar un modelo LLM local en las columnas de un dataframe. {mall} te puede ayudar a :\nclasificar el contenido de una variable resumir textos extraer sentimiento a partir del texto extraer información desde el texto confirmar si algo es verdadero o falso a partir de un texto y también a aplicar cualquier prompt a una variable. Puedes encontrar instrucciones detalladas sobre configurar el uso de IA en R en esta publicación. Recientemente lo usé para un caso real, donde tenía una columna de casi 2.000 nombres, y necesitaba asignarle un género a cada una de estas personas, solamente a partir de sus nombres y apellidos.\nlibrary(dplyr) # manipulación de datos library(readr) # cargar datos library(tictoc) # medición de tiempo library(mall) # aplicar modelos de lenguaje en dataframe Los datos provienen del servicio electoral de Chile, y los nombres corresponden a 1.576 candidatos y candidatas a alcaldías.\nCon el siguiente código puedes descargar los datos listos para su uso:\narchivo_remoto \u0026lt;- \u0026#34;https://raw.githubusercontent.com/bastianolea/servel_scraping_votaciones/refs/heads/main/datos/resultados_alcaldes_2024.csv\u0026#34; candidatos \u0026lt;- readr::read_csv2(archivo_remoto) |\u0026gt; select(nombres = candidato, partido, sector) |\u0026gt; filter(nombres != \u0026#34;Nulo/Blanco\u0026#34;) candidatos # A tibble: 1,576 × 3 nombres partido sector \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Marco Antonio Gonzalez Candia RN Derecha 2 Veronica Maricel Cueto Cueto IND Independiente 3 Marcela Maritza Mansilla Potocnjak IND Independiente 4 Alejandro Felipe Ricotti Sepulveda IND Independiente 5 Carlos Orlando Tapia Aviles IND Independiente 6 Maria Luisa Hamilton Velasco IND Independiente 7 Gaston Dubournais Riveros IND Independiente 8 Marcela Chamorro Macias IND Izquierda 9 Jose Andres Arellano Blanco IND Independiente 10 Andrea Elizabeth Galvez Sepulveda REP Derecha # ℹ 1,566 more rows En esta instancia, tengo configurado {ollamar} para que use el modelo local y de código abierto Llama 3.2, de 3 billones de parámetros. Podemos instalar este modelo, o uno distinto, con estas instrucciones:\nlibrary(ollamar) ollamar::pull(\u0026#34;llama3.2\u0026#34;) # descargar e instalar un modelo mall::llm_use(\u0026#34;ollama\u0026#34;, \u0026#34;llama3.2\u0026#34;) # elegir un modelo instalado En una primera prueba, le entregamos al modelo de lenguaje la columna nombres (que contiene nombre, segundo nombre, apellido, y segundo apellido), y le pedimos al modelo que clasifique cada observación como masculino o femenino.\ntic() resultados_1 \u0026lt;- candidatos |\u0026gt; llm_classify(nombres, labels = c(\u0026#34;masculino\u0026#34;, \u0026#34;femenino\u0026#34;), pred_name = \u0026#34;genero\u0026#34;) Ollama local server running ── mall session object Backend: Ollama LLM session: model:llama3.2:latest R session: cache_folder:/var/folders/z8/61w5pwts4h5fsvhfqs1wpvk40000gn/T//RtmpntMvSc/_mall_cache127421f0326aa toc() 421.687 sec elapsed El proceso tarda aproximadamente 7 minutos en clasificar los casi 1.576 nombres, un ritmo de 0.26 segundos por cada predicción.\nresultados_1 |\u0026gt; select(genero, nombres) |\u0026gt; slice_sample(n = 15) # A tibble: 15 × 2 genero nombres \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 masculino Andres Parra Sandoval 2 femenino Leontina Del Carmen Gutierrez Rivas 3 masculino Mario Ortiz Cabezas 4 masculino Jose Esteban Alarcon Vargas 5 femenino Nancy Milena Alfaro Jurjevic 6 masculino Osvaldo Herrera Valdes 7 femenino Ninoska Villegas Lamas 8 masculino Juan Carlos Diaz Avendaño 9 masculino German Pino Maturana 10 masculino Gonzalo Rubio Fuenzalida 11 femenino Laura Correa Fuentes 12 masculino Alex Fernando Castillo Blas 13 masculino Jonathan Velasquez Ramirez 14 masculino Jorge Espinoza Cuevas 15 femenino Elizabeth Marican Rivas Para la segunda prueba, intentamos entregarle al modelo solamente los nombres, excluyendo segundos nombres y apellidos, bajo el supuesto de que el primer nombre es el mejor predictor del género, mientras que los apellidos no son un predictor del género.\nProbamos el código con una expresión regular (regex) para extraer la primera palabra de una secuencia de texto:\ncandidatos |\u0026gt; mutate(nombre = stringr::str_extract(nombres, \u0026#34;\\\\w+\u0026#34;)) |\u0026gt; select(nombre, nombres) |\u0026gt; slice_sample(n = 5) # A tibble: 5 × 2 nombre nombres \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Paola Paola Romero Valdivia 2 Alejandra Alejandra Villegas Huichaman 3 Yuliana Yuliana Ema Bustos Zapata 4 Elizabeth Elizabeth Salinas Valle 5 Freddy Freddy Antonio Ramirez Villalobos Realizamos la segunda prueba de clasificación, esta vez solamente con el primer nombre:\ntic() resultados_2 \u0026lt;- candidatos |\u0026gt; mutate(nombre = stringr::str_extract(nombres, \u0026#34;\\\\w+\u0026#34;)) |\u0026gt; # extraer nombres llm_classify(nombre, labels = c(\u0026#34;masculino\u0026#34;, \u0026#34;femenino\u0026#34;), pred_name = \u0026#34;genero\u0026#34;) toc() 139.47 sec elapsed Esta vez el proceso tarda aproximadamente 2 minutos, ¡casi 4 veces más rápido! El modelo clasifica los textos más rápido mientras menos texto tenga que analizar. Es importante mencionar que la velocidad va a depender mucho de tu computador, específicamente su GPU.\nresultados_2 |\u0026gt; select(genero, nombre) |\u0026gt; slice_sample(n = 10) # A tibble: 10 × 2 genero nombre \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 masculino Pablo 2 masculino Luis 3 femenino Sandra 4 masculino Johnny 5 masculino Miguel 6 masculino Oscar 7 masculino Luis 8 masculino Gonzalo 9 masculino Juan 10 masculino Mauricio Revisemos los resultados, obteniendo una muestra al azar de 30 nombres:\nresultados_2 |\u0026gt; filter(nombres != \u0026#34;Nulo/Blanco\u0026#34;) |\u0026gt; relocate(genero, nombre, .before = nombres) |\u0026gt; slice_sample(n = 30) |\u0026gt; print(n = Inf) # A tibble: 30 × 5 genero nombre nombres partido sector \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 masculino Christian Christian Arturo Cardenas Silva IND Independien… 2 masculino Osvaldo Osvaldo Cartagena Garcia IND Independien… 3 masculino Romelio Romelio Borquez Hidalgo IND Derecha 4 masculino Patricio Patricio Marchant Ulloa IND Centro 5 masculino Claudio Claudio Miranda Ibañez IND Independien… 6 masculino Marcos Marcos Gaete Flores IND Derecha 7 masculino Raul Raul Jaime Espejo Escobar IND Independien… 8 masculino Camilo Camilo Alejandro Rapu Riroroko IND Centro 9 femenino Clara Clara Lazcano Fernández IND Derecha 10 masculino Drazen Drazen Andre Markusovic Caceres PDG Derecha 11 femenino Denisse Denisse Tamara Leon Rojas REP Derecha 12 masculino Juan Juan Pablo Gomez Ramirez IND Independien… 13 masculino Alexis Alexis Mendez Uribe IND Independien… 14 femenino Juana Juana Sarmiento Acosta IND Independien… 15 masculino Marco Marco Antonio Gonzalez Candia RN Derecha 16 masculino Miguel Miguel Bruna Silva IND Independien… 17 masculino Hernaldo Hernaldo Andres Ahumada Chavez IND Independien… 18 femenino Nivia Nivia Del Carmen Riquelme Gutierrez IGUALDAD Izquierda 19 masculino Cristian Cristian Alejandro Sobarzo Sanhueza IND Independien… 20 masculino Cristian Cristian Andres Pozo Parraguez PS Izquierda 21 femenino Rosa Rosa Torres Escobedo IND Independien… 22 masculino Juan Juan Carlos Gonzalez Romo IND Independien… 23 femenino Paula Paula Rodriguez Valenzuela IND Independien… 24 masculino Gabriel Gabriel Silva Gonzalez IND Independien… 25 femenino Daniela Daniela Isabel Munizaga Villegas REP Derecha 26 masculino Ramon Ramon Sandoval Zurita IND Independien… 27 masculino Ramon Ramon Bahamonde Cea IND Independien… 28 masculino Claudio Claudio Arellano Cortes IND Independien… 29 femenino Sigrid Sigrid Ramirez Arias IND Derecha 30 masculino Patricio Patricio Ernesto Gonzalez Nuñez IND Independien… Revisando los nombres, al parecer las predicciones son bastante buenas, pero al hacer más pruebas nos damos cuenta de que no es 100% infalible, ya que se equivoca en algunos nombres como Aracelli, Edita, Elizabeth, o Josselyn. si se requiere aumentar la calidad de la predicción, podría instalarse un modelo de lenguaje más grande.\nOtra alternativa para mejorar el desempeño de la clasificación (es decir, que sea más certer) es agregarle al prompt más contexto sobre lo que se busca obtener, así como una alternativa para que el modelo clasifique términos que no sabe dónde clasificar:\nresultados_3 \u0026lt;- candidatos |\u0026gt; mutate(nombre = stringr::str_extract(nombres, \u0026#34;\\\\w+\u0026#34;)) |\u0026gt; # extraer nombres llm_classify(nombre, labels = c(\u0026#34;masculino\u0026#34;, \u0026#34;femenino\u0026#34;, \u0026#34;desconocido\u0026#34;), pred_name = \u0026#34;genero\u0026#34;, additional_prompt = \u0026#34;obtener el género desde nombres de personas\u0026#34;) En el argumento additional_prompt podemos especifiacr una instrucción extra para el modelo, lo que a veces es suficiente para que el modelo tenga contexto extra para poder clasificar correctamente. En mi experiencia, esto mejora considerablemente la calidad de las respuestas, y reduce la incertidumbre en algunos casos.\nEn conclusión, se trata de una herramienta muy fácil de implementar, que también es capaz de ahorrarnos bastante tiempo, por ejemplo, en el desarrollo de un algoritmo que detecte el nombre a partir de ciertas estructuras en la composición de cada uno, o bien, en tener que contrastar los nombres con alguna base de datos ya existente de nombres con un género asignado (si es que existe).\n","date":"2025-02-19T00:00:00Z","excerpt":"Aprende a usar modelos extensos de lenguaje (LLM) para clasificar datos con un caso de uso real, donde se necesita asumir el género de las personas a partir de sus nombres para poder realizar análisis con perspectiva de género. Aplicar inteligencia artificial en R para este tipo de tareas es puede ahorrarte muchísimo tiempo, y dependiendo de como ajustes los datos y el _prompt_ puede entregar buenos resultados.","href":"https://bastianoleah.netlify.app/blog/genero_nombres_llm/","tags":"procesamiento de datos ; inteligencia artificial ; análisis de texto ; género","title":"Predecir género a partir de nombres usando un modelo de lenguaje en R"},{"content":"Si creaste una aplicación Shiny y quieres compartirla con otros/as, pero tu app contiene información que no puede ser vista por cualquiera, ¡entonces sigue estos pasos! En unos minutos tendrás una aplicación que requiere de usuario y contraseña para poder usarla.\n1. Instalar {shinymanager} Instala el paquete {shinymanager}:\ninstall.packages(\u0026#34;shinymanager\u0026#34;) 2. Crear credenciales Dentro del script app.R de tu aplicación Shiny, crea un dataframe que contenga una contraseña de prueba:\n# credenciales para autenticación credentials \u0026lt;- data.frame( user = \u0026#34;usuario\u0026#34;, password = \u0026#34;usuario\u0026#34; ) 3. Aplicar autenticación a tu app Ahora, en el apartado server de tu app, agrega el siguiente código que evaluará las credenciales ingresadas y gestionará el acceso para tus usuarios:\n# autenticación res_auth \u0026lt;- secure_server(check_credentials = check_credentials(credentials)) Finalmente, antes de la línea de tu script app.R que ejecuta tu aplicación (usualmente shinyApp(ui, server)), modifica el objeto que contiene la interfaz de tu aplicación, para protegerla con la autenticación de {shinymanager}:\n# autenticación ui \u0026lt;- secure_app(ui) ¡Listo! Tu aplicación ahora solicitará una contraseña antes de ejecutarse.\nEjemplo Veamos un ejemplo con una aplicación Shiny real:\nApp sin autenticación library(shiny) library(bslib) ui \u0026lt;- page_fluid( page_fillable( card( h1(\u0026#34;Aplicación\u0026#34;), sliderInput(\u0026#34;numeros\u0026#34;, \u0026#34;Prueba\u0026#34;, 1, 10, 5) ) ) ) server \u0026lt;- function(input, output, session) { } shinyApp(ui, server) Para agregarle autenticación a esta app, debemos seguir los pasos anteriores para dejarla así:\nApp con autenticación library(shiny) library(bslib) library(shinymanager) # credenciales para autenticación credentials \u0026lt;- data.frame( user = \u0026#34;usuario\u0026#34;, password = \u0026#34;usuario\u0026#34; ) ui \u0026lt;- page_fluid( page_fillable( card( h1(\u0026#34;Aplicación\u0026#34;), sliderInput(\u0026#34;numeros\u0026#34;, \u0026#34;Prueba\u0026#34;, 1, 10, 5) ) ) ) server \u0026lt;- function(input, output, session) { # autenticación res_auth \u0026lt;- secure_server(check_credentials = check_credentials(credentials)) } # autenticación ui \u0026lt;- secure_app(ui) shinyApp(ui, server) La pantalla de autenticación se vería así:\nPersonalizar Si queremos modificar la pantalla de autenticación, podemos agregar un tema, y cambiar las etiquetas usando la función set_labels():\n# definir un tema ui \u0026lt;- secure_app(theme = shinythemes::shinytheme(\u0026#34;flatly\u0026#34;), ui) # cambiar textos de autenticación set_labels(language = \u0026#34;en\u0026#34;, \u0026#34;Please authenticate\u0026#34; = \u0026#34;Acceder\u0026#34;, \u0026#34;Username:\u0026#34; = \u0026#34;Usuario:\u0026#34;, \u0026#34;Password:\u0026#34; = \u0026#34;Contraseña:\u0026#34;, \u0026#34;Login\u0026#34; = \u0026#34;Acceder\u0026#34; ) La aplicación y todos sus contenidos han quedado protegidos tras la contraseña de usuario. El siguiente paso es guardar las contraseñas en un archivo (no en el código) que no se suba a GitHub (agregar a .gitignore), y que ojalá esté encriptado. Por ejemplo, {shinymanager} ofrece formas de guardar las credenciales en una base de datos SQlite, con encripción y hashing de las contraseñas.\nPara más información sobre el uso de {shinymanager}, consulta su documentación.\n","date":"2025-02-17T00:00:00Z","excerpt":"Si [creaste una aplicación Shiny](/blog/r_introduccion/tutorial_shiny_1) y quieres [compartirla con otros/as](/blog/r_introduccion/tutorial_shinyapps), pero tu app contiene información que no puede ser vista por cualquiera, ¡entonces sigue estos pasos! En unos minutos tendrás una aplicación que requiere de usuario y contraseña para poder usarla.","href":"https://bastianoleah.netlify.app/blog/shiny_usuarios/","tags":"shiny ; apps","title":"Protege el acceso a tus aplicaciones Shiny con contraseña"},{"content":"¿Te ha pasado que tienes una tabla con datos perdidos, y otra tabla con una columna que coincide con la primera tabla, que además cuenta con datos que quieres usar para rellenar las observaciones perdidas? Antes pensaba que esto se resolvía con left_join() y algún ajuste para reemplazar los perdidos con los datos anexados. Pero ayer conocí una función que resuelve este problema de inmediato: rows_update()!\nlibrary(dplyr) |\u0026gt; suppressPackageStartupMessages() tabla_1 \u0026lt;- tibble(ciudad = c(\u0026#34;Santiago\u0026#34;, \u0026#34;Concepción\u0026#34;, \u0026#34;Valparaíso\u0026#34;, \u0026#34;Arica\u0026#34;), poblacion = c(7123891, 1036142, 1054253, 221364), temperatura = c(NA, 17.6, NA, 19.2)) tabla_1 # A tibble: 4 × 3 ciudad poblacion temperatura \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Santiago 7123891 NA 2 Concepción 1036142 17.6 3 Valparaíso 1054253 NA 4 Arica 221364 19.2 tabla_2 \u0026lt;- tibble(ciudad = c(\u0026#34;Santiago\u0026#34;, \u0026#34;Rancagua\u0026#34;, \u0026#34;Concepción\u0026#34;, \u0026#34;Arica\u0026#34;, \u0026#34;Valparaíso\u0026#34;), temperatura = c(22.8, 14.0, 17.6, 19.2, 17.5)) tabla_2 # A tibble: 5 × 2 ciudad temperatura \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Santiago 22.8 2 Rancagua 14 3 Concepción 17.6 4 Arica 19.2 5 Valparaíso 17.5 En este ejemplo, tenemos dos tablas: la primera tiene una columna con datos perdidos o faltantes, y la segunda tabla, de datos similares, contiene las observaciones que en la primera tabla están faltando.\nUna solución sería unir ambas tablas con left_join(), lo cual resultaría en dos columnas con el mismo nombre a las que se les agregan las letras x e y para distinguirlas. Luego habría que usar la función if_else() para rellenar las filas que tienen casos perdidos con el posible valor de la segunda columna:\ntablas_unidas \u0026lt;- left_join(tabla_1, tabla_2, by = \u0026#34;ciudad\u0026#34;) tablas_unidas # A tibble: 4 × 4 ciudad poblacion temperatura.x temperatura.y \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Santiago 7123891 NA 22.8 2 Concepción 1036142 17.6 17.6 3 Valparaíso 1054253 NA 17.5 4 Arica 221364 19.2 19.2 # rellenar usando ifelse() tablas_unidas |\u0026gt; mutate(temperatura.rellenada = if_else(is.na(temperatura.x), temperatura.y, temperatura.x)) # A tibble: 4 × 5 ciudad poblacion temperatura.x temperatura.y temperatura.rellenada \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Santiago 7123891 NA 22.8 22.8 2 Concepción 1036142 17.6 17.6 17.6 3 Valparaíso 1054253 NA 17.5 17.5 4 Arica 221364 19.2 19.2 19.2 Una segunda solución se lograría usando la función de {dplyr} especializada para estos casos: coalesce(), a la cual le entregas dos o más columnas, y utiliza el primer dato no perdido entre ellas para generar la nueva columna. En otras palabras, te permite unir varias columnas en una sola, cuando estas columnas pueden tener un mismo dato pero no tiene certeza en cuál de las columnas se encuentra.\n# rellenar usando coalesce() tablas_unidas |\u0026gt; mutate(temperatura2 = coalesce(temperatura.x, temperatura.y)) # A tibble: 4 × 5 ciudad poblacion temperatura.x temperatura.y temperatura2 \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Santiago 7123891 NA 22.8 22.8 2 Concepción 1036142 17.6 17.6 17.6 3 Valparaíso 1054253 NA 17.5 17.5 4 Arica 221364 19.2 19.2 19.2 La tercera forma me resulta la más conveniente. La función rows_update() funciona casi igual que left_join(), pero la unión entre ambas tablas se realiza encima de la primera tabla, dado que se asume que ambas tablas comparten una o varias columnas con datos, y que solamente queremos rellenar la primera tabla con los datos de la segunda tabla cuando estos estén ausentes en la primera.\ntabla_1 |\u0026gt; rows_update(tabla_2, by = \u0026#34;ciudad\u0026#34;, unmatched = \u0026#34;ignore\u0026#34;) # A tibble: 4 × 3 ciudad poblacion temperatura \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Santiago 7123891 22.8 2 Concepción 1036142 17.6 3 Valparaíso 1054253 17.5 4 Arica 221364 19.2 Usando esta alternativa, nos ahorramos el paso de tener las columnas duplicadas y tenerte resolver la duplicidad manualmente, dado que rows_update() se encarga de esto.\nLa conveniencia es que de esta forma puedes rellenar múltiples columnas al mismo tiempo si es que tu segunda tabla también posee múltiples columnas con datos de reemplazo para la primera tabla. El único inconveniente es que a la segunda tabla no le deben sobrar columnas que no estén presentes en la primera tabla, dado que rows_update() está enfocada en el reemplazo de datos sobre una primera tabla, no agregar nuevas columnas, como sería en left_join().\n","date":"2025-02-14T00:00:00Z","excerpt":"¿Te ha pasado que tienes una tabla con datos perdidos, y otra tabla con una columna que coincide con la primera tabla, que además cuenta con datos que quieres usar para rellenar las observaciones perdidas? Antes pensaba que esto se resolvía con `left_join()` y algún ajuste para reemplazar los perdidos con los datos anexados. Pero ayer conocí una función que resuelve este problema de inmediato: `rows_update()`!","href":"https://bastianoleah.netlify.app/blog/2025-02-14/","tags":"dplyr ; limpieza de datos ; datos perdidos","title":"Rellenar datos perdidos usando datos de otra tabla"},{"content":"Los archivos csv (comma-separated values, valores separados por comas) suelen ser el formato más básico para guardar datos. Los beneficios que tienen los csv con respecto a compatibilidad y accesibilidad son a su vez la causa de sus desventajas: son más pesados porque sus datos no se guardan comprimidos, y suelen ser más lentos de cargar, porque los datos no vienen codificados de una forma optimizada.\nSin embargo, usualmente grandes bases de datos son guardadas en archivos csv, con varios millones de filas, lo que puede hacer que la carga de un archivo dure entre varios segundos a minutos.\nHagamos una comparación cargando un archivo csv de 2,5 GB1, con más de 16 millones de filas y 12 columnas, por medio de 3 funciones: read.csv() de R base, read_csv de {readr}, y read_csv_arrow de {arrow}.\nlibrary(readr) library(arrow) library(tictoc) # para medir el tiempo que tardan archivo \u0026lt;- \u0026#34;~/Downloads/Beneficiarios Fonasa 2023.csv\u0026#34; # ruta de la base 1. read.csv() Esta función viene por defecto en R, y es la que peor desempeño tiene, además de cargar los datos en un dataframe tradicional en lugar de un tibble:\ntic() datos \u0026lt;- read.csv(archivo, encoding = \u0026#34;latin1\u0026#34;) toc() 48.875 sec elapsed La carga del archivo demora casi un minuto con este método por defecto.\n2. readr::read_csv() El paquete {readr} es muy versátil para leer y escribir datos con un desempeño aceptable y una sintaxis consistente:\ntic() datos \u0026lt;- readr::read_csv(archivo, locale = locale(encoding = \u0026#34;latin1\u0026#34;), show_col_types = F) toc() 25.183 sec elapsed Usando este método de carga, obtenemos resultados casi tres veces más rápido que con el método de R por defecto! Además, obtenemos los datos en un cómodo y moderno tibble. Pero todavía puede ser más rápido\u0026hellip;\n3. arrow::read_csv_arrow() Los desarrolladores de la tecnología Arrow, principalmente conocida por el excelente formato de datos columnares .parquet (que supera a cualquier otro en velocidad de lectura y eficiencia de almacenamiento), ofrecen una función optimizada para la lectura de archivos csv:\ntic() datos \u0026lt;- arrow::read_csv_arrow(archivo, read_options = csv_read_options(encoding = \u0026#34;latin1\u0026#34;)) toc() 8.094 sec elapsed ¡Sólo 7 segundos! Confirmamos que el paquete {arrow} ofrece un lector optimizado de archivos csv que supera en velocidad a cualquier otro que podamos usar en R.\nConclusiones Realicemos un benchmark para contar con los datos claros sobre el veredicto final:\nresultado \u0026lt;- bench::mark(check = FALSE, iterations = 1, base = read.csv(archivo, encoding = \u0026#34;latin1\u0026#34;), readr = readr::read_csv(archivo, locale = locale(encoding = \u0026#34;latin1\u0026#34;), show_col_types = F), arrow = arrow::read_csv_arrow(archivo, read_options = csv_read_options(encoding = \u0026#34;latin1\u0026#34;)), ) resultado # A tibble: 3 × 6 expression min median `itr/sec` mem_alloc `gc/sec` \u0026lt;bch:expr\u0026gt; \u0026lt;bch:tm\u0026gt; \u0026lt;bch:tm\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;bch:byt\u0026gt; \u0026lt;dbl\u0026gt; 1 base 47s 47s 0.0213 5.71GB 0.170 2 readr 32.1s 32.1s 0.0311 1.45GB 0 3 arrow 12.6s 12.6s 0.0791 123.83MB 0 En el resultado final de este experimento, cargar un archivo csv con arrow::read_csv_arrow() resulta casi el doble de rápido que cargarlo con readr::read_csv(), y más de cinco veces más rápido que cargarlo con el típico read.csv()!\nSi revisamos la columna mem_alloc, también podemos confirmar que, además de ser más rápido, ocupa muchísima menos memoria para llevar a cabo la carga: mientras R base consume casi 6 GB de memoria, Arrow lo logra con ~120 MB!\nEn el análisis de datos, si bien la conveniencia es un factor primordial (código más sencillo, legible y fácil de escribir nos permite generar resultados mucho más rápido), siempre es posible replantearnos la forma en que trabajamos para pensar alternativas más optimizadas y veloces, si es que el volumen de datos con los que trabajamos lo amerita.\nSe trata de una base de datos de beneficiarios del Fondo Nacional de Salud de Chile (Fonasa), disponible en su sitio web de datos abiertos. Una versión agregada de los datos está disponible en mi GitHub.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-02-12T00:00:00Z","excerpt":"\u003cp\u003eLos archivos \u003ccode\u003ecsv\u003c/code\u003e (\u003cem\u003ecomma-separated values,\u003c/em\u003e valores separados por comas) suelen ser el formato más básico para guardar datos. Los beneficios que tienen los csv con respecto a compatibilidad y accesibilidad son a su vez la causa de sus desventajas: son más pesados porque sus datos no se guardan comprimidos, y suelen ser \u003cstrong\u003emás lentos de cargar\u003c/strong\u003e, porque los datos no vienen codificados de una forma optimizada.\u003c/p\u003e\n\u003cp\u003eSin embargo, usualmente grandes bases de datos son guardadas en archivos csv, con varios millones de filas, lo que puede hacer que la carga de un archivo dure entre varios segundos a \u003cstrong\u003eminutos\u003c/strong\u003e.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/2025-02-12/","tags":"consejos ; datos ; optimización","title":"Cargar archivos csv más rápido en R con Arrow"},{"content":"Acabo de publicar una nueva página donde voy a estar recopilando todos los conjuntos de datos sociales con los que trabajo o he trabajado.\nLa idea de esta página es poder compartir fácilmente datos sociales sobre Chile que vienen limpios y procesados, para facilitar el trabajo de otras personas, y también ayudarles a aprender análisis de datos.\nSe trata de una tabla que se genera automáticamente, la cual contiene una lista de repositorios enfocados en datos sociales, con clasificación según la temática del dato, y varias columnas que indican las características del conjunto de datos, como si es que el dato contiene variables de género, si está desagregado a nivel comunal, si existe una aplicación de visualización de datos asociada, la temporalidad (anual/mensual/semanal) de las observaciones, y más.\nEn cada repositorio se encuentran también scripts con el código que se utiliza para obtener los datos, procesarlos, limpiarlos, y también para generar nuevos productos a partir de estos datos, tales como gráficos, tablas, y aplicaciones.\nLa tabla se genera mediante un web scraping de mi cuenta de GitHub. Un script de web scraping en R navega por todos los repositorios de mi cuenta, y va obteniendo toda la información de cada uno de ellos. Los contenidos de la tabla se adaptan en base a las etiquetas que tiene cada repositorio, la descripción viene de la descripción de cada repo, y el título viene del readme. Las columnas con iconos también se hacen automáticamente a partir de las etiquetas. La tabla se genera con el paquete {gt}. La página se genera con Quarto y se publica automáticamente con GitHub Pages.\nLa conveniencia de este flujo de trabajo es que, por mi parte, solamente tengo que seguir subiendo repositorio, y solamente componerles las etiquetas y descripciones en GitHub, éste mini sitio se va a actualizar y reflejar la nueva información sin tener que hacer nada de trabajo extra.\nSi quieres crear tú también un mini sitio basado en R y Quarto, puedes seguir este tutorial de Quarto que hice, donde enseño lo simple que es combinar las herramientas y librerías de R para personalizar y dar contenido a los documentos creados con Quarto, incluyendo reportes en HTML, sitios web y blogs.\n","date":"2025-02-09T00:00:00Z","excerpt":"\u003cp\u003eAcabo de \n\u003ca href=\"https://bastianolea.github.io/datos_sociales/\" target=\"_blank\" rel=\"noopener\"\u003epublicar una nueva página\u003c/a\u003e donde voy a estar recopilando todos los conjuntos de datos sociales con los que trabajo o he trabajado.\u003c/p\u003e\n\u003cp\u003eLa idea de esta página es poder compartir fácilmente datos sociales sobre Chile que vienen limpios y procesados, para facilitar el trabajo de otras personas, y también ayudarles a aprender análisis de datos.\u003c/p\u003e\n\u003cp\u003eSe trata de una tabla que se genera automáticamente, la cual contiene una lista de repositorios enfocados en datos sociales, con clasificación según la temática del dato, y varias columnas que indican las características del conjunto de datos, como si es que el dato contiene variables de género, si está desagregado a nivel comunal, si existe una aplicación de visualización de datos asociada, la temporalidad (anual/mensual/semanal) de las observaciones, y más.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/datos_sociales/","tags":"blog ; datos ; tablas ; quarto ; Chile","title":"Repositorio de datos sociales"},{"content":"Los modelos de lenguaje (LLM) son herramientas muy útiles para analizar texto, y usarlos en tus análisis de datos con R es sencillo. Previamente en este blog ya he explicado cómo usar LLMs para análisis de sentimiento y otros usos. En este tutorial, te enseño a utilizar modelos de lenguaje locales, instalados en tu propio computador, para obtener resúmenes de textos. Esto puede servir por si estás analizando una base de datos de texto que contenga textos de extensión larga, para los cuales sería conveniente tener una versión más breve. Por ejemplo, al analizar resultados de web scraping, conjuntos de datos periodísticos o de noticias, datos de entrevistas, respuestas abiertas en encuestas, etc.\nPrimero, obtendremos un corpus de textos de noticias chilenas publicadas el año 2024, en forma de un dataframe con columnas con el título, cuerpo, fuente y fecha de las noticias. Los datos son obtenidos de este repositorio de obtención automatizada de textos de noticias de prensa escrita chilena.\nlibrary(dplyr) library(readr) library(stringr) url_datos \u0026lt;- \u0026#34;https://raw.githubusercontent.com/bastianolea/prensa_chile/refs/heads/main/prensa_datos_muestra.csv\u0026#34; noticias \u0026lt;- read_csv2(url_datos) Exploremos rápidamente los datos:\nset.seed(1234) noticias_muestra \u0026lt;- noticias |\u0026gt; slice_sample(n = 20) head(noticias_muestra) # A tibble: 6 × 4 titulo cuerpo fuente fecha \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;date\u0026gt; 1 “Ciclón extratropical”: los registros de su paso por… \u0026quot;En l… The C… 2024-06-02 2 Actividades con Boric, Vallejo y Bachelet: Los gesto… \u0026quot;ATON… Emol 2024-08-10 3 Choque mortal entre bus y auto en Los Ríos: Un condu… \u0026quot;Un m… Megan… 2024-04-07 4 Capturan a asaltante solitaria que robó más de $4 mi… \u0026quot;En m… Emol 2024-06-13 5 Debido a la extracción de litio: Investigación revel… \u0026quot;Univ… Emol 2024-08-23 6 Cámara acoge renuncia de Ricardo Cifuentes (DC), qui… \u0026quot;El 2… D. Fi… 2024-04-08 Confirmemos cómo llega el texto de las noticias:\nnoticias_muestra |\u0026gt; slice(1) |\u0026gt; # extraer una fila pull(cuerpo) |\u0026gt; # extraer variable como vector str_trunc(800) |\u0026gt; # recortar textos muy largos str_wrap(70) |\u0026gt; # insertar saltos de línea cada x caracteres cat() # imprimir En las últimas semanas se ha mencionado repetidamente el concepto de “ciclón extratropical” para referirse al evento meteorológico que afectará a Chile. El nombre por sí solo suena a un fenómeno climático poderoso y fuera de lo común, pero según especialistas, el país ya ha enfrentado este tipo de eventos anteriormente. El concepto de ciclón extratropical hace referencia a un sistema frontal con baja presión, y es esta baja presión la que le da el nombre de ciclón. El evento marcará la primera semana de junio y el sistema ya está ubicado en aguas abiertas del Pacífico Sur. Según reporta el sitio especializado MeteoRed, el ciclón extratropical destaca por su extraordinaria extensión según las imágenes satelitales. Aún es incierta la proyección de lluvias para la Región Metropolitana esta... Revisemos el promedio de palabras de cada noticia:\nnoticias_muestra |\u0026gt; # calcular cantidad de palabras mutate(palabras = str_split(cuerpo, pattern = \u0026#34; \u0026#34;) |\u0026gt; lengths()) |\u0026gt; # calcular cantidad de caracteres mutate(caracteres = nchar(cuerpo)) |\u0026gt; summarize(palabras = mean(palabras), caracteres = mean(caracteres)) # A tibble: 1 × 2 palabras caracteres \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 368. 2283. Cómo podemos ver, cada noticia tiene en promedio cerca de 400 palabras o más de 2000 caracteres. Usaremos un modelo de lenguaje para crear una nueva variable que contenga una versión resumida del texto de las noticias, para agilizar su comprensión.\nPero primero, tenemos que elegir e instalar un modelo de lenguaje local.\nConfiguración del modelo de lenguaje local Puedes encontrar instrucciones más detalladas sobre configurar el uso de IA en R en esta publicación. Para poder usar modelos LLM localmente con R, tenemos que instalar Ollama en nuestro equipo, que es la aplicación que nos permite obtener y usar modelos de lenguaje locales. Ollama tiene que estar abierto para poder proveer del modelo de lenguaje a nuestra sesión de R.\nLuego, ya sea desde Ollama o desde R, tenemos que instalar un modelo de lenguaje.\nPara instalar un modelo de lenguaje desde R, es tan simple como ejecutar: ollamar::pull(\u0026quot;llama3.2:3b\u0026quot;), definiendo entre comillas el modelo que hayamos elegido para instalar. Al elegir un modelo, debes considerar las capacidades de tu computador para ejecutar los modelos, y que el tamaño del modelo es directamente proporcional con la calidad de sus respuestas1.\nPara instalar un modelo desde ollama, en la línea de comandos o Terminal de tu equipo, ejecuta ollama pull llama3.2:3b y el modelo se descargará en tu equipo.\nUna vez que tengas Ollama abierto en tu computador, y un modelo previamente instalado, puedes proceder a usarlo en R en el siguiente paso.\nGenerar resúmenes de texto El primer paso para trabajar con modelos de lenguaje extensos con datos tabulares R es cargar el paquete {mall}, y especificar qué modelo queremos usar:\nlibrary(mall) # paquete para usar LLM en dataframes de R llm_use(\u0026#34;ollama\u0026#34;, \u0026#34;llama3.2:3b\u0026#34;) # indicar qué modelo usaremos ── mall session object Backend: ollama LLM session: model:llama3.2:3b R session: cache_folder:/var/folders/z8/61w5pwts4h5fsvhfqs1wpvk40000gn/T//RtmpI1xMRn/_mall_cache8da8ff9738f Posteriormente, podemos utilizar todas las funciones que empiezan con llm_ para aplicar funciones que utilizan los modelos de lenguaje sobre cada observación de la columna que especifiquemos. En este caso, para producir resúmenes de texto, usamos la función llm_summarize(), la cual contiene un prompt diseñado para resumir textos hacia la cantidad de palabras que solicitemos.\n# crear columna de resumen de los textos noticias_muestra_resumen \u0026lt;- noticias_muestra |\u0026gt; llm_summarize(cuerpo, # columna con el texto original max_words = 20, # cantidad de palabras del resumen pred_name = \u0026#34;resumen\u0026#34;, # nombre de la columna resultante additional_prompt = \u0026#34;en español\u0026#34; ) El proceso puede tardar unos minutos, dado que el modelo tiene que consumir el texto, realizar las relaciones entre todas las palabras, y generar la inferencia que produce el texto de salida. En mi computador, el procesamiento de 20 noticias se demoró un poco menos de 1 minuto, pero este tiempo puede variar en base al equipo que tengas y la cantidad de texto de cada elemento.\nSi los textos son demasiado largos, se puede usar una función como str_trunc(texto, side = \u0026quot;center\u0026quot;, width = 3000) para truncar textos demasiado largos cortando el texto sobrante desde el medio del texto.\nExploremos los resultados obtenidos:\nnoticias_muestra_resumen |\u0026gt; select(resumen) |\u0026gt; # calcular cantidad de palabras mutate(palabras = str_split(resumen, pattern = \u0026#34; \u0026#34;) |\u0026gt; lengths()) |\u0026gt; # calcular cantidad de caracteres mutate(caracteres = nchar(resumen)) |\u0026gt; summarize(palabras_prom = mean(palabras), palabras_min = min(palabras), palabras_max = max(palabras), caracteres_prom = mean(caracteres)) # A tibble: 1 × 4 palabras_prom palabras_min palabras_max caracteres_prom \u0026lt;dbl\u0026gt; \u0026lt;int\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; 1 18.4 16 23 114. noticias_muestra_resumen |\u0026gt; slice(1) |\u0026gt; pull(resumen) [1] \u0026quot;el ciclón extratropical llega a chile en la primera semana de junio con lluvias intenses en algunas regiones\u0026quot; noticias_muestra_resumen |\u0026gt; slice(4) |\u0026gt; pull(resumen) [1] \u0026quot;la mujer detenida es sospechosa de un robo en una sucursal bancaria de la zona sur de antofagasta con más de $4 millones.\u0026quot; noticias_muestra_resumen |\u0026gt; slice(3) |\u0026gt; pull(resumen) [1] \u0026quot;un bus y un automóvil chocaron, dejando un fallecido y 15 personas heridas en la ruta t-202.\u0026quot; Podemos ver que los nuevos textos mantienen coherencia con los textos originales, y en promedio consiste de resúmenes de 17 palabras, con un máximo de 21 palabras. Considerando que le pedimos al modelo que el máximo de palabra fueran 20, esto no hace recordar que los modelos de lenguaje no son deterministas, por lo tanto las instrucciones que les demos no reflejarán en un 100% los resultados que esperamos, y los resultados obtenidos nunca serán en un 100% consistentes, siempre habiendo un posible factor de azar y alucinación.\nSi los resúmenes aparecen incorrectos, o salen en inglés, se puede usar el argumento additional_prompt de llm_summarize() para indicarle al modelo explícitamente que quieres resultados en español, o siguiendo alguna otra instrucción.\nSi tu computador es muy básico (tiene poca memoria RAM), recomiendo instalar Llama 3.2 1B. Si tiene al menos 8GB de memoria, recomiendo Llama 3.2 3B. Si tienes suficiente memoria (más de 8GB), recomiendo Llama 3.1 8B.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-02-08T00:00:00Z","excerpt":"Los modelos de lenguaje (LLM) son herramientas muy útiles para analizar texto, y usarlos en tus análisis de datos con R es sencillo. En este tutorial, te enseño a utilizar modelos de lenguaje locales, instalados en tu propio computador, para obtener resúmenes de textos.","href":"https://bastianoleah.netlify.app/blog/resumir_texto_llm/","tags":"análisis de texto ; inteligencia artificial","title":"Resumir textos usando modelos de lenguaje (LLM) locales en R"},{"content":"Exposición en la Universidad Internacional de la Municipalidad de Rancagua, instancia organizada por el Programa de Gobierno Local Abierto (GOBLA) de la Municipalidad de Rancagua.\nEn esta presentación doy a conocer el visualizador de datos de Corrupción, explicando cómo fue el proceso de inspirarme a hacerlo, los desafíos que tuve durante su desarrollo, y la experiencia de colaboración y discusión en torno a un proyecto de datos que pueden ser polémicos.\nPuedes explorar la aplicación siguiendo éste enlace. Las Clases Magistrales de la UNIM promueven la participación activa de las y los ciudadanos en la creación y difusión del conocimiento, por medio de clases interdisciplinarias de diversos/as representantes del ámbito académico y del sector público, tanto locales, nacionales como internacionales.\nDiapositivas ","date":"2025-01-24T00:00:00Z","excerpt":"\u003cp\u003eExposición en la \u003cstrong\u003eUniversidad Internacional de la Municipalidad de Rancagua\u003c/strong\u003e, instancia organizada por el \u003cstrong\u003ePrograma de Gobierno Local Abierto\u003c/strong\u003e (GOBLA) de la Municipalidad de Rancagua.\u003c/p\u003e\n\u003cp\u003eEn esta presentación doy a conocer el \n\u003ca href=\"/apps/corrupcion_chile\"\u003evisualizador de datos de Corrupción\u003c/a\u003e, explicando cómo fue el proceso de inspirarme a hacerlo, los desafíos que tuve durante su desarrollo, y la experiencia de colaboración y discusión en torno a un proyecto de datos que pueden ser polémicos.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/unim/presentacion_visualizador_datos_corrupcion/","tags":"shiny ; Chile ; visualización de datos ; aplicaciones ; videos","title":"Video: Presentación de Visualizador de datos de Corrupción en Chile"},{"content":"Tuve el privilegio de exponer en la Universidad Internacional de la Municipalidad de Rancagua, un espacio de diálogo, aprendizaje y colaboración, organizado por el Programa de Gobierno Local Abierto (GOBLA) de la Municipalidad de Rancagua.\nPresenté un taller dirigido a funcionarios y funcionarias municipales sobre Aplicación de la ciencia de datos en la Gestión Municipal, donde comuniqué la importancia de manejar herramientas de progamación para le análisis de datos, favoreciendo el desarrollo de herramientas reutilizables, reproducibles, transparentes y abiertas.\nFue una instancia muy interesante, con mucha participación e interés de personas dentro y fuera del sector público, donde espero haber comunicado la importancia de las iniciativas de código y datos abiertos! Y que mucha más gente y funcionarios municipales sigan creando iniciativas de análisis de datos para el beneficio de todas y todos 💜\nLas Jornadas de Buenas Prácticas de la UNIM son un espacio de formación donde se promueve la colaboración e intercambio de experiencias exitosas en el sector público nacional e internacional. Generando una oportunidad para potenciar las competencias técnicas, el conocimiento y la capacidad de innovar.\nDiapositivas Estudio de caso 1 Análisis de datos obtenidos desde Transparencia, mostrando la capacidad de reutilizar código, y de automatizar la descarga de los datos manejando un navegador usando código.\nAnálisis de datos # fuente de los datos: # https://www.portaltransparencia.cl/PortalPdT/directorio-de-organismos-regulados/?org=MU260# # para descargar, entrar a \u0026#34;04. Personal y remuneraciones\u0026#34;, luego \u0026#34;Personal a Contrata\u0026#34; # cargar librerías library(tidyverse) # cargar datos descargados datos \u0026lt;- read_csv2(\u0026#34;caso_1_personal_contrata/TransparenciaActiva.csv\u0026#34;, locale = locale(encoding = \u0026#34;latin1\u0026#34;)) |\u0026gt; # limpiar nombres de columnas janitor::clean_names() # en este bloque de código, si cambiamos la ruta del archivo cargado, podemos re-aplicar todo el procedimiento a cualquier archivo que tengamos, permitiendo reutilizar el script completo para cada archivo de datos de la plataforma de Transparencia # conteo rápido de una variable datos |\u0026gt; count(estamento) # revisión rápida de los datos datos |\u0026gt; glimpse() # ejemplo 1a: limpiar una columna de remuneraciones datos |\u0026gt; select(starts_with(\u0026#34;remunera\u0026#34;)) |\u0026gt; mutate(remuneracion_bruta_mensualizada = parse_number(remuneracion_bruta_mensualizada, locale = locale(grouping_mark = \u0026#34;.\u0026#34;))) # ejemplo 1b: reutilizar el código anterior para limpiar todas las columnas de remuneraciones al mismo tiempo datos2 \u0026lt;- datos |\u0026gt; # select(starts_with(\u0026#34;remunera\u0026#34;)) |\u0026gt; mutate(across(starts_with(\u0026#34;remunera\u0026#34;), \\(var) parse_number(var, locale = locale(grouping_mark = \u0026#34;.\u0026#34;)))) |\u0026gt; print() # ejemplo 2a: obtener estadísticos descriptivos de una de las variables de remuneraciones datos2 |\u0026gt; group_by(estamento) |\u0026gt; summarize(mean = mean(remuneracion_liquida_mensualizada), mediana = median(remuneracion_liquida_mensualizada), min = min(remuneracion_liquida_mensualizada), max = max(remuneracion_liquida_mensualizada), x25 = quantile(remuneracion_liquida_mensualizada, .25)) # ejemplo 2b: reutilizar el código anterior para obtener estadísticos descriptivos de otra variable, agrupada por una variable distinta datos2 |\u0026gt; group_by(grado_eus_o_jornada) |\u0026gt; summarize(mean = mean(remuneracion_liquida_mensualizada), mediana = median(remuneracion_liquida_mensualizada), min = min(remuneracion_liquida_mensualizada), max = max(remuneracion_liquida_mensualizada), x25 = quantile(remuneracion_liquida_mensualizada, .25), n = n()) |\u0026gt; arrange(desc(mediana)) Obtención automatizada de los datos de Transparencia con Selenium # descarga automatizada de datos usando web scraping para controlar un navegador # cargar paquete para web scraping interactivo library(RSelenium) # crear un navegador controlable mediante código driver \u0026lt;- rsDriver(port = 4567L, browser = \u0026#34;firefox\u0026#34;, chromever = NULL) navegador \u0026lt;- driver[[\u0026#34;client\u0026#34;]] # navegar a la página navegador$navigate(\u0026#34;https://www.portaltransparencia.cl/PortalPdT/directorio-de-organismos-regulados/?org=MU260#pills-1\u0026#34;) for (i in 1:11) { # entrar a personal de contrata navegador$findElement(\u0026#39;xpath\u0026#39;, \u0026#39;//*[@id=\u0026#34;A6428:formInfo:j_idt39:0:datalist:3:j_idt43:0:j_idt47\u0026#34;]\u0026#39;)$ clickElement() Sys.sleep(1) # presionar 2024 navegador$findElement(\u0026#39;css selector\u0026#39;, \u0026#39;li.ui-datalist-item:nth-child(1)\u0026#39;)$ # cambiar año clickElement() Sys.sleep(1) # elegir un mes navegador$findElement(\u0026#39;css selector\u0026#39;, paste0(\u0026#39;#A6428\\\\:formInfo\\\\:j_idt94_list \u0026gt; li:nth-child(\u0026#39;, i, \u0026#39;)\u0026#39;))$ clickElement() # van del 1 al 11 en 2024 Sys.sleep(1) # descargar archivo navegador$findElement(\u0026#39;css selector\u0026#39;, \u0026#39;.fa-file-csv\u0026#39;)$ clickElement() Sys.sleep(1) # volver al inicio navegador$findElement(\u0026#39;xpath\u0026#39;, \u0026#39;//*[@id=\u0026#34;A6428:formInfo:j_idt64\u0026#34;]\u0026#39;)$ clickElement() Sys.sleep(1) } Estudio de caso 2 Automatización de la descarga de todos los archivos mensuales, y transformación de los archivos PDF a tablas de datos en formato CSV.\nDescarga de datos # descargar libro diario siguiendo la secuencia de los nombres de archivo # fuente de los datos: # https://www.portaltransparencia.cl/PortalPdT/directorio-de-organismos-regulados/?org=MU260# # para descargar, entrar a \u0026#34;11. Información Presupuestaria\u0026#34;, luego \u0026#34;Libro diario municipal\u0026#34; # https://transparencia.rancagua.cl/documentos/daf/2024-11-ILD.pdf # descargar un solo archivo directamente download.file(\u0026#34;https://transparencia.rancagua.cl/documentos/daf/2024-11-ILD.pdf\u0026#34;, \u0026#34;caso_2_libro_diario/2024-11-ILD.pdf\u0026#34;) # podemos cambiar el enlace para obtener un archivo correspondiente a otro mes, dado que los archivos vienen por meses y el mes está definido en el enlace mismo # siguiendo esta misma idea, podemos automatizar el proceso de desacrgar todos los archivos del año # cargar paquetes library(stringr) # crear secuencia de números con los meses que necesitamos numeros \u0026lt;- 1:11 # agregar ceros a los números si es necesario meses \u0026lt;- ifelse(numeros \u0026lt; 10, paste0(\u0026#34;0\u0026#34;, numeros), numeros) # crear los nombres de los archivos en base a la secuencia de meses archivos \u0026lt;- paste(\u0026#34;2024-\u0026#34;, meses, \u0026#34;-ILD.pdf\u0026#34;, sep = \u0026#34;\u0026#34;) # crear los enlaces en base a los nombres de archivos enlaces \u0026lt;- paste(\u0026#34;https://transparencia.rancagua.cl/documentos/daf/\u0026#34;, archivos, sep = \u0026#34;\u0026#34;) # crear las rutas donde guardaremos los archivos descargados en base a los nombres de archivos rutas \u0026lt;- paste(\u0026#34;caso_2_libro_diario/\u0026#34;, archivos, sep = \u0026#34;\u0026#34;) # descargar todos los archivos automáticamente for (mes in 1:11) { download.file(enlaces[mes], rutas[mes]) Sys.sleep(1) } Extraer datos desde un PDF # Transparencia Activa # 11. Información Presupuestaria: # Libro diario municipal # 2024 \u0026gt; NOVIEMBRE # cargar paquetes library(tidyverse) library(tabulapdf) documento \u0026lt;- \u0026#34;caso_2_libro_diario/2024-10-ILD.pdf\u0026#34; # prueba 1: # intentar extraer los datos, pero vemos que llegan muy desordenados tabulapdf::extract_tables(documento, pages = 1:3) # prueba 2: # usar este comando las veces que sea necesario para obtener las coordenadas horizontales de documento que indican el inicio y final de cada columna tabulapdf::locate_areas(documento, 3) # crear lista con coordenadas columnas \u0026lt;- list(c(35.85436, 100.69957, 143.79596, 187.50326, 240.25774, 310.74166, 491.55613, 554.61603, 608.18382)) # extraer datos especificando los cortes de cada columna tablas_0 \u0026lt;- tabulapdf::extract_tables(documento, pages = 1:3, guess = FALSE, columns = columnas ) # revisar cómo va quedando tablas_0 |\u0026gt; list_rbind() |\u0026gt; print(n=100) # limpieza inicial de las columnas tablas_1 \u0026lt;- tablas_0 |\u0026gt; list_rbind() |\u0026gt; rename(comprobante = 2, fecha = 3, documento = 4, analisis = 5, cuenta = 6, denominacion = 7, debe = 8, haber = 9) |\u0026gt; select(-1) tablas_1 # mantener sólo filas con fecha tablas_2 \u0026lt;- tablas_1 |\u0026gt; # filter(!is.na(fecha)) |\u0026gt; filter(str_detect(fecha, \u0026#34;\\\\d{4}\u0026#34;) | str_detect(fecha, \u0026#34;\\\\w+\u0026#34;)) |\u0026gt; print() # extraer la línea horizontal de texto que atravieza todas las columnas y guardarla en su propia columna tablas_3 \u0026lt;- tablas_2 |\u0026gt; mutate(titular = ifelse(str_detect(fecha, \u0026#34;\\\\w+\u0026#34;), paste(fecha, documento, analisis, cuenta, denominacion, debe) |\u0026gt; str_remove_all(\u0026#34;NA\u0026#34;), NA)) |\u0026gt; print() # rellenar hacia abajo variables que solo aparecen en una fila para cada observación tablas_4 \u0026lt;- tablas_3 |\u0026gt; tidyr::fill(titular, comprobante, .direction = \u0026#34;down\u0026#34;) |\u0026gt; filter(str_detect(fecha, \u0026#34;\\\\d{2}/\\\\d{4}\u0026#34;)) |\u0026gt; print() # limpiar columnas numéricas tablas_5 \u0026lt;- tablas_4 |\u0026gt; mutate(debe = parse_number(debe, locale = locale(grouping_mark = \u0026#34;.\u0026#34;)), haber = parse_number(haber, locale = locale(grouping_mark = \u0026#34;.\u0026#34;))) |\u0026gt; print() # revisar tablas_5 |\u0026gt; print(n=100) # obtener un resumen de los datos obtenidos tablas_5 |\u0026gt; group_by(fecha) |\u0026gt; summarize(sum(debe), sum(haber)) # luego podemos guardar los datos como csv readr::write_csv2(tablas_5, \u0026#34;caso_2_libro_diario/libro_diario.csv\u0026#34;) ","date":"2025-01-24T00:00:00Z","excerpt":"\u003cp\u003eTuve el privilegio de exponer en la \u003cstrong\u003eUniversidad Internacional de la Municipalidad de Rancagua\u003c/strong\u003e, un espacio de diálogo, aprendizaje y colaboración, organizado por el \u003cstrong\u003ePrograma de Gobierno Local Abierto\u003c/strong\u003e (GOBLA) de la Municipalidad de Rancagua.\u003c/p\u003e\n\u003cp\u003ePresenté un taller dirigido a funcionarios y funcionarias municipales sobre Aplicación de la ciencia de datos en la Gestión Municipal, donde comuniqué la importancia de manejar herramientas de progamación para le análisis de datos, favoreciendo el desarrollo de herramientas reutilizables, reproducibles, transparentes y abiertas.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/unim/taller_ciencia_de_datos_gestion_municipal/","tags":"dplyr ; web scraping ; datos ; procesamiento de datos ; limpieza de datos ; Chile ; videos","title":"Video: Taller Aplicación de la Ciencia de Datos en la Gestión Municipal"},{"content":" Actualización de app análisis de prensa 🗞️ Nuevo: gráfico de análisis de sentimiento: elige un tema y revisa si las noticias recientes fueron mayormente positivas o negativas. Compara cómo distintos medios abordan las temáticas. ¡Pronto más detalle!\nVisita la aplicación siguiendo éste enlace\nPara agregar esta funcionalidad a la app tuve que desarrollar un proceso automatizado de análisis de datos usando modelos extensos de lenguaje (LLM). Luego de que se obtienen las noticias por web scraping, se deja ejecutando el modelo de lenguaje sobre las noticias recientes para analizar el sentimiento del texto, clasificar la noticia en temáticas, y producir un resumen de su contenido.\nTuve que procesar cientos de miles de datos hacia atrás, lo que hago de manera local corriendo el modelo Llama 3.1 8B en mi computador, por lo que tomó bastantes horas. Semanalmente son aprox. 3000 las noticias nuevas, las cuales se procesan en unas 8 horas aprox., por lo que será factible dejar el computador todos los domingos por la noche analizando las noticias de la semana.\nPronto voy a poder agregar nuevas visualizaciones, como por ejemplo seleccionar un concepto (como Boric, delincuencia, Bachelet, minería, etc.) o varios conceptos a la vez, y visualizar si las noticias con esa/esas palabra/s han sido positivas o negativas, y cómo esto varía en cada medio.\n","date":"2025-01-14T00:00:00Z","excerpt":"Nuevo gráfico de análisis de sentimiento: elige un tema y revisa si las noticias recientes fueron mayormente positivas o negativas. Compara cómo distintos medios abordan las temáticas. Para agregar esta funcionalidad a la app tuve que [desarrollar un proceso automatizado de análisis de datos usando modelos extensos de lenguaje (LLM)](/blog/analisis_sentimiento_llm/). Luego de que se obtienen las noticias por web scraping, se deja ejecutando el modelo de lenguaje sobre las noticias recientes para analizar el sentimiento del texto, clasificar la noticia en temáticas, y producir un resumen de su contenido.","href":"https://bastianoleah.netlify.app/blog/2025-01-14/","tags":"web scraping ; inteligencia artificial ; visualización de datos ; shiny","title":"Actualización de app Análisis de prensa: visualización de análisis de sentimiento de noticias recientes"},{"content":" En la eterna batalla entre usuarios de tema oscuro y tema claro, yo lamentablemente soy del bando correcto: tema oscuro durante la noche, y tema claro durante el día.\nLa idea de usar un tema oscuro en una pantalla no es que se vea más bonito y profesional todo de negro, sino ayudarte a reducir el contraste de la pantalla con respecto al espacio en el que estás trabajando. En este sentido, si estás en un espacio bien iluminado, deberías usar un tema claro, para no forzar a vista a tener que adaptarse entre un entorno brillante y una pantalla oscura. Si estás en un espacio oscuro, o de iluminación baja, deberías usar un tema oscuro para adecuarte a la iluminación a tu alrededor, junto con ajustar el brillo de tu pantalla.\nLa mayoría de los sistemas operativos modernos cambian automáticamente entre el tema claro u oscuro dependiendo de la hora del día, asumiendo que durante la noche tendremos menor iluminación. Además, la mayoría de las pantallas adaptan su brillo automáticamente a la iluminación ambiente, reduciéndola en espacios oscuros. Un paso extra de cuidado de la vista e higiene del sueño es el cambio de los colores de la pantalla a un tinte cálido durante la noche, activado por defecto en sistemas operativos macOS y iOS.\nPero RStudio no se adapta solito al tema de tu computador, principalmente porque los temas son configurados por el/la usuario/a. Para resolver esto, se necesitan dos paquetes y una pequeña configuración:\nInstala {usethis}, un paquete que facilita varias operaciones convenientes en R y asociadas al desarrollo con R, Instala {rsthemes}, un paquete que contiene temas para RStudio y varias utilidades relacionadas a los temas, como la habilidad de cambiarlos por medio de funciones, usar temas aleatorios, y más. En tu consola de R, ejecuta usethis::edit_r_profile(), lo que hará que se abra tu archivo .Rprofile. Este script es un archivo especial, que se ejecuta automáticamente cada vez que inicies una sesión en R, por lo que puedes incluir en él ciertas configuraciones que siempre tengas que ejecutar, como la que incluiremos a continuación. Incluye el siguiente código en tu .Rprofile: # especificar temas para modo claro y oscuro if (interactive() \u0026amp;\u0026amp; requireNamespace(\u0026#34;rsthemes\u0026#34;, quietly = TRUE)) { rsthemes::set_theme_light(\u0026#34;Basti Purple Light\u0026#34;) # tema claro rsthemes::set_theme_dark(\u0026#34;Basti Purple Dark\u0026#34;) # tema oscuro # cambiar al tema dependiendo de la hora al iniciar una sesión en RStudio setHook(\u0026#34;rstudio.sessionInit\u0026#34;, function(isNewSession) { rsthemes::use_theme_auto(dark_start = \u0026#34;21:00\u0026#34;, # hora del tema oscuro dark_end = \u0026#34;6:00\u0026#34;) #hora del tema claro }, action = \u0026#34;append\u0026#34;) } Dentro de este bloque de código, tienes que configurar cuatro cosas: el nombre del tema claro que quieres usar durante el día, y el nombre del tema oscuro que quieres usar durante la noche, junto con la hora del día a la que quieres que se active automáticamente el modo oscuro, y la hora del día a la que quieres que se active el modo claro. En mi caso, estoy usando dos temas de RStudio morados que creé yo, basados en una paleta de colores morada y rosada. En este post explico cómo descargarlos e instalarlos en tu RStudio.\nAhora, cada vez que abras una nueva sesión de RStudio (o reinicies tu sesión), el tema de RStudio se ajustará automáticamente al tema que elegiste para el día o la noche!\nRecuerda cuidar tus ojos mientras programas mirando a la distancia con regularidad y configurando el brillo de tu pantalla para que sea similar al brillo de tu habitación!\n","date":"2025-01-12T00:00:00Z","excerpt":"La idea de usar un tema oscuro en una pantalla no es que se vea más bonito y profesional todo de negro, sino ayudarte a reducir el contraste de la pantalla con respecto al espacio en el que estás trabajando. Para resolver esto, sólo se necesitan dos paquetes y una pequeña configuración.","href":"https://bastianoleah.netlify.app/blog/2025-01-12/","tags":"consejos ; curiosidades","title":"Configura RStudio para que cambie al modo oscuro o claro automáticamente según la hora del día"},{"content":"El paquete de R {camcorder} te permite ir registrando todos los gráficos que hagas durante una sesión, y al final te entrega una animación que contiene el paso a paso de tu proceso de visualización de datos, desde el gráfico de {ggplot2} inicial hasta el producto final.\nSolo tienes que instalarlo y activarlo para que todos los gráficos que generes vayan siendo registrados automáticamente:\n# instalar install.packages(\u0026#34;camcorder\u0026#34;) library(camcorder) # empeazr grabar gráficos gg_record( dir = \u0026#34;gráficos/grabacion\u0026#34;, device = \u0026#34;jpeg\u0026#34;, 1200, 1800, scale = 1.2, units = \u0026#34;px\u0026#34;, dpi = 300 ) Al iniciar la grabación, hay que especificar las dimensiones del gráfico. Todos los gráficos que generes desde ese momento tendrán esas dimensiones, independiente de el tamaño de tu panel de gráficos en RStudio. Esto es muy útil, porque al especificar las dimensiones de tu gráfico de antemano, te permite adecuar el diseño de tus visualizaciones a su tamaño y escala desde el primer momento, en vez de tener que ajustar todos los tamaños en el momento final de exportar el gráfico (esto también es posible de hacer con el paquete {ggview}!).\nLos gráficos van a ir guardándose como imagen en la carpeta que especificaste, con un nombre de archivo secuencial.\nSi en algún momento necesitas cambiar las dimensiones de tu gráfico, puedes usar la función gg_resize_film(), por ejemplo, si la forma que está tomando tu gráfico se beneficia más de un lienzo más largo o ancho. También, si necesitas pausar la grabación, puedes usar gg_stop_recording() para que los gráficos dejen de guardarse, y luego resumirla de la misma forma en que la iniciaste.\nCuando termines de hacer tu visualización, usa la función gg_playback() para convertir todas las imágenes en una animación!\n# crear video gg_playback( last_as_first = FALSE, first_image_duration = 1, background = \u0026#34;white\u0026#34;, image_resize = 1280, playback = FALSE ) Los resultados son muy entretenidos! Me gustó poder plasmar estos procesos detallistas en una animación que expresa parte de las dificultades (y lo entretenido) de hacer gráficos.\nGracias a Nicola Rennie por el post que me hizo conocer este interesante paquete 🥰\nBonus Al generarse las animaciones en formato gif, quise convertirlas a un formato más moderno (y eficiente) como .webp, así que usé ffmpeg. Pero también me di cuenta que tenía que recortar algunos videos (porque el gráfico final quedaba con bordes en blanco, debido a la proporción de aspecto de los primeros gráficos era más alta o ancha que el del final), y achicar otros para que fueran mas livianos, así que dejo los comandos que usé para convertir gif a webp recortando y achicando:\n# achicar video ffmpeg -f gif -i \u0026#34;2025_01_11_01_24_41.gif\u0026#34; -c libwebp -vf scale=800:-1 -loop 0 camcorder1.webp # recortar video ffmpeg -f gif -i \u0026#34;2025_01_11_01_21_05.gif\u0026#34; -c libwebp -vf crop=850:1280 -loop 0 camcorder3.webp ffmpeg -f gif -i \u0026#34;2025_01_11_01_22_24.gif\u0026#34; -c libwebp -vf crop=1280:1134 -loop 0 camcorder2.webp # recortar y luego achicar: ffmpeg -f gif -i \u0026#34;2025_01_11_01_22_24.gif\u0026#34; -c libwebp -vf \u0026#34;crop=1280:1134,scale=800:-1\u0026#34; -loop 0 camcorder2b.webp ","date":"2025-01-11T00:00:00Z","excerpt":"El [paquete de R {camcorder}](https://github.com/thebioengineer/camcorder) te permite ir registrando todos los gráficos que hagas durante una sesión, y al final te entrega una animación que contiene el paso a paso de tu proceso de visualización de datos, desde el gráfico de {ggplot2} inicial hasta el producto final. Solo tienes que activarlo para que todos los gráficos que generes vayan siendo registrados, y luego puedas obtener una animación de tu proceso.","href":"https://bastianoleah.netlify.app/blog/camcorder/","tags":"curiosidades ; visualización de datos ; ggplot2","title":"Graba el proceso de tus visualizaciones de datos con `{camcorder}`"},{"content":" Visualizador de datos que busca reflejar el horror de la guerra y el exterminio que se están llevando a cabo en el territorio de Palestina por obra del estado de Israel y sus aliados. Los datos provienen de Palestine Datasets y de Armed Conflict Location \u0026amp; Event Data, organizaciones que están documentando datos sobre identificación de víctimas y registro de sucesos de relevancia política en la región.\nAccede al visualizador de datos por este enlace.\nDesarrollé un nuevo visualizador de datos sobre un tema que puede ser polémico para algunas personas. Sé que compartir este tipo de cosas me hace aún menos empleable, y me puede hacer perder oportunidades laborales. Pero fue un ejercicio de contemplación y respeto a las víctimas de un conflicto de características colonialistas y genocidas.\nSe trata de un trabajo sencillo de visualización de datos a partir de dos fuentes de información, desarrollado completamente en el lenguaje R.\nEl desarrollo tuvo un foco en la personalización de la apariencia de la aplicación usando CSS mediante funciones de R, para darle esa apariencia de interfaz gráfica militar vintage. De hecho, desarrollé primero la interfaz completa, con uno de cada uno de los elementos de prueba, para darles un estilo homogéneo, y solo después fui creando la aplicación real con esos bloques de la interfaz estilizada. Nunca había empezado a desarrollar algo por lo visual, antes de tener los datos procesados, y aprendí que se puede.\nOtro detalle que me gustó implementar, y que me gustaría hacer en su propia sección o app, es la de los nombres de víctimas que aparecen secuencialmente (es tan sólo un loop donde cada nombre tiene un retardo mayor que el anterior para su animación de entrada). En general no me gusta el uso de animaciones, porque implementar una animación te va llevando a tener que implementar animaciones en todo, pero en este caso daba un ritmo solemne a datos sensibles.\nTrabajar con datos sensibles siempre tiene un peso, en mi experiencia. Me cuesta digerir lo que veo, y me pesa el corazón. Varias veces tan sólo dictar los textos de la app me hizo nudos en la garganta. Pero creo que es importante intentar visibilizar estos temas, en memoria de las víctimas, y como un granito de arena.\nFuentes Palestine Datasets: https://data.techforpalestine.org/docs/killed-in-gaza/ Armed Conflict Location \u0026amp; Event Data (ACLED): https://acleddata.com/israel-palestine/ ","date":"2025-01-08T00:00:00Z","excerpt":"Visualizador de datos que busca reflejar el horror de la guerra y el exterminio que se están llevando a cabo en el territorio palestino por obra de Israel y sus aliados. Los datos provienen de Palestine Datasets y de Armed Conflict Location \u0026 Event Data, organizaciones que están documentando datos sobre identificación de víctimas y registro de sucesos de relevancia política en la región.","href":"https://bastianoleah.netlify.app/blog/app_palestina/","tags":"apps","title":"App: Genocidio en Palestina"},{"content":"Portafolio de una selección de las aplicaciones públicas de visualización de datos que he desarrollado. Se trata de pequeñas aplicaciones web diseñadas para hacer más accesibles y comprensibles ciertos conjuntos de datos sobre temáticas sociales, políticas y socioeconómicas. Todas estas aplicaciones web fueron desarrolladas con R, y tanto el código de fuente como sus datos están disponibles de manera pública y abierta.\nOtra versión más detallada de este portafolio está disponible en este enlace: https://bastianolea.github.io/shiny_apps/\nGenocidio en Palestina Visualizador de datos que busca reflejar el horror de la guerra y el exterminio que se están llevando a cabo en el territorio palestino por obra de Israel y sus aliados. Los datos provienen de Palestine Datasets y de Armed Conflict Location \u0026amp; Event Data, organizaciones que están documentando datos sobre identificación de víctimas y registro de sucesos de relevancia política en la región.\nTemperaturas extremas en Chile Consulta datos históricos de temperaturas extremas en el país, desde 1970 hasta 2024, y visualiza los cambios históricos en las temperaturas producto de la crisis climática.\nAnálisis de prensa chilena Aplicación de análisis de texto de prensa escrita chilena. Contiene varios gráficos que cuantifican el contenido de las noticias de Chile, semana por semana. Los gráficos permiten identificar qué palabras son las más usadas a través del tiempo, lo cual a su vez revela cómo va variando el acontecer nacional. Los datos de esta aplicación son obtenidos mediante web scraping de forma diaria, pero la app se actualiza semanalmente. La base de datos comprende más de 600 mil noticias, que suman más de 100 millones de palabras, abarcando más de 21 fuentes periodísticas distintas.\nEstadísticas de delincuencia en Chile Visualización de estadísticas oficiales de delincuencia, separadas por comuna y delito, para darle contexto y seriedad a un tema país a partir de datos objetivos. Selecciona una comuna y luego uno o varios delitos para obtener un gráfico de líneas que muestra una serie de tiempo de la cantidad de delitos, desde 2010 hasta 2023. Además, puedes visualizar la cantidad de delitos por año en la comuna seleccionada, el promedio de delitos en los gobiernos recientes, y una visualización de los tres delitos más frecuentes en cada comuna.\nProyecciones de población del Censo Aplicación web que visualiza los datos oficiales del Instituto Nacional de Estadísticas de Chile sobre proyecciones de población; es decir, estimaciones del crecimiento poblacional hacia el futuro, a partir de los datos obtenidos en los censos oficiales.\nComparador de mapas comunales de Chile Aplicación que reúne más de 170 variables urbanísticas, sociales y económicas, de nivel comunal, para todas las comunas del país, que permite al usuario elegir dos variables simultáneamente para compararlas visualmente por medio de dos mapas regionales. El visualizador entrega la posibilidad de poner a prueba relaciones entre variables tan distintas como áreas verdes y puntajes de pruebas de selección universitaria, nivel de ingresos y tasa de delitos, participación electoral y situación de las viviendas, etc., dejando al usuario la tarea de explicar los fenómenos que pueden surgir.\nIndicadores económicos de Chile Tablero que presenta +8 indicadores económicos del Banco Central de Chile, cuya presentación resumida permite analizar la situación económica del país. Los datos de esta aplicación son obtenidos de forma automática dos veces al día, garantizando que se encuentren actualizados. Además, la arquitectura de esta app facilita el proceso de añadir nuevos indicadores.\nCorrupción en Chile Catálogo y visualizador de los casos de corrupción más trascendentes del último tiempo en Chile, para poner en perspectiva los montos, responsables, y sectores políticos asociados. Los datos son recopilados manualmente para producir una tabla con la mayor información posible sobre casos de corrupción, incluyendo responsables, delitos específicos, afiliación a partidos políticos, fundaciones involucradas y más, para alientar visualizaciones interactivas que permitan a la cuidadanía comprender de dónde viene la corrupción y cómo nos afecta como país.\nMillonarios de Chile Con este visualizador puedes poner en perspectiva las fortunas individuales más grandes del país, para así dimensionar un aspecto clave de la desigualdad en Chile y el mundo. Diversas fuentes de datos permiten recopilar un listado de los empresarios más ricos de Chile. Distintas técnicas estadísticas y de visualización permiten dimensionar las enormes fortunas de estas personas, por ejemplo, comparando con los propios ingresos del usuario, o con los ingresos de toda la población del país.\nFemicidios en Chile Sitio con gráficos y tablas que expresan en cifras los datos de femicidios cometidos en Chile. Estos datos, mantenidos por la Red Chilena contra la Violencia hacia las Mujeres, expresan la brutalidad manifestada de una sociedad patriarcal donde la violencia es una realidad transversal, llevada a su extremo en la agresión y asesinato de mujeres por razones de género.\nBrechas de género Casen Visualizador que detalla brechas de género en temas sociales, de vivienda e ingresos, para analizar variables en las que las mujeres experimentan peores condiciones de vida, a nivel regional Selecciona una de las variables disponibles para generar un gráfico con todas las regiones del país, donde se detalla el porcentaje de la población femenina y masculina afectada por la variable seleccionada, o si eliges variables de vivienda o familia, el porcentaje de hogares con jefatura femenina o masculina correspondientes. Los puntos del gráfico además se detallan con las brechas o diferencias entre géneros existentes, volviendo explícitas las desigualdades o ausencia de las mismas.\nComparador de ingresos Casen Visualizador que compara distribuciones y promedios de ingresos entre las comunas de Chile, para observar las diferencias en las realidades socioeconómicas del país. Selecciona un grupo de comunas, y elige una variable de ingresos, como ingresos individuales, ingresos por hogar, ingresos per cápita o montos de pensiones/jubilación, para obtener un gráfico de densidad que describe y compara las poblaciones de las comunas, y un gráfico de dispersión que ubica los ingresos de las comunas seleccionadas en comparación a todas las demás comunas del país.\nRelacionador Casen Visualizador que permite relacionar hasta 3 variables socioeconómicas en un gráfico de dispersión por comunas, para analizar la relación entre ellas. Este visualizador permite experimentar correlaciones con numerosas variables de temas como ingresos, educación, condiciones de vida, condiciones laborales, y más, dado que permite utilizar libremente cualquiera de ellas como los ejes del gráfico, creando así visualizaciones personalizadas. Por ejemplo, se puede explorar si las comunas con bajo nivel educacional promedio son también las de menores ingresos, si es que las comunas con viviendas de menor calidad y menores ingresos se correlacionan con mayor hacinamiento o no, si las comunas de altos ingresos tienen menores afiliados a Fonasa, y más.\nGraficador encuesta CEP Aplicación web que permite visualizar gráficos de los resultados de las encuestas del Centro de Estudios Públicos. Además, permite desagregar resultados en base a categorías sociodemográficas, filtrar grupos, configurar los gráficos y descargar los datos.\n","date":"2025-01-06T00:00:00Z","excerpt":"Portafolio de una selección de las aplicaciones públicas de visualización de datos que he desarrollado. Se trata de pequeñas aplicaciones web diseñadas para hacer más accesibles y comprensibles ciertos conjuntos de datos sobre temáticas sociales, políticas y socioeconómicas. Todas estas aplicaciones web fueron desarrolladas con R, y tanto el código de fuente como sus datos están disponibles de manera pública y abierta.","href":"https://bastianoleah.netlify.app/blog/portafolio_apps/","tags":"shiny ; visualización de datos","title":"Portafolio de aplicaciones en R"},{"content":"Estoy muy feliz porque la organización SocialTIC junto a infoactivismo.org me mencionaron como una de las iniciativas de infoactivismo más destacada del año 2024! 🥳💕\nLo mejor del Infoactivismo 2024 es un recuento anual de iniciativas y proyectos dedicados a la justicia y el cambio social apoyadas en tecnologías, datos e información en Latinoamérica.\nAgradezco mucho la mención! Es un honor estar entre otras iniciativas tan destacadas. Para mi, el año 2024 fue un año en el que me di cuenta que podía combinar mis habilidades técnicas con mis intereses sociales y políticos para crear herramientas de visualización de datos que resulten útiles e interesantes para los demás. Espero que este 2025 se venga con más datos!\nEn este enlace les dejo la lista completa de iniciativas, todas ellas muy interesantes para informarse de iniciativas en la región que utilizan tecnologías y datos para visibilizar y combatir problemáticas sociales.\nMi mención la incluyeron en la sección Datos abiertos generados por organizaciones, y la comparto a continuación:\nBastián Olea. Desde Chile, reconocemos la labor ciudadana de Bastián quien ha visualizado datos sociales sobre Chile de manera atractiva y comprensible a través de R. En su blog facilita datos abiertos del sector público y comparte el código para que otras personas puedan aprender y contribuir.\nLes dejo el texto de introducción a la lista, como contexto y para que se animen a revisar el resto de iniciativas:\nComo cada fin de año, nos alegra compartir una edición más de Lo mejor del Infoactivismo 2024, un recuento de acciones por la justicia y el cambio social apoyadas en tecnologías, datos e información en Latinoamérica.\nEl contexto actual de esta edición aborda desafíos como la desinformación y la polarización, la criminalización y ataques hacia activistas, periodistas y personas defensoras de derechos humanos, la emergencia climática, la crisis de desapariciones, los conflictos armados, el avance de grupos antiderechos, el abuso de tecnologías, la vulneración de los derechos humanos y la manipulación de la información por parte de gobiernos y grupos de poder.\nLos proyectos e iniciativas que compartimos a continuación son un reflejo de las problemáticas que impactan a nivel local y regional y de la búsqueda de soluciones a los retos que enfrentamos como sociedad.\nEsta recopilación reúne más de 50 esfuerzos agrupados en 12 categorías, que por su estrategia, experimentación y creatividad en el uso de las tecnologías y la información, consideramos inspiradores para generar cambios en aspectos sociales, ambientales, políticos y culturales.\n","date":"2025-01-01T00:00:00Z","excerpt":"Estoy muy feliz porque la organización [SocialTIC](https://socialtic.org) junto a [infoactivismo.org](https://infoactivismo.org/) me mencionaron como una de las iniciativas de infoactivismo más destacada del año 2024! [_Lo mejor del Infoactivismo 2024_](https://infoactivismo.org/lo-mejor-del-infoactivismo-en-latinoamerica-2024/) es un recuento anual de iniciativas y proyectos dedicados a la justicia y el cambio social apoyadas en tecnologías, datos e información en Latinoamérica.","href":"https://bastianoleah.netlify.app/blog/2025-01-01/","tags":"blog","title":"Mención en Infoactivismo en Latinoamérica 2024"},{"content":"A largo de todo el año 2024 he estado realizando web scraping de distintas fuentes de noticias digitales de Chile. Casi todos los días ejecuto un script que a su vez se ejecuta decenas de otros scripts, que realizan el scraping diario de noticias. El resultado de estos procesos, día tras día, va aumentando la cantidad total de noticias que he ido recolectando. De vez en cuando, algún sitio cambia, o algo falla, y tengo que corregir manualmente los scripts. Cada cierto tiempo ejecuto versiones alternativas de los scripts para hacer una extracción de datos desde fechas anteriores, aumentando la cantidad total de noticias de fechas pasadas, lo que me permite rellenar vacíos en las obtenciones anteriores de noticias diarias, como también aumentar la cantidad total de noticias con datos desde años antes de que empezara este proyecto.\nEl proceso de limpieza y procesamiento del texto de estas noticias es bastante entretenido y complejo. Me gustaría poder escribir sobre él en el futuro. Pero la gracia es que yo simplemente aprieto ejecutar, y el proceso se ejecuta de principio a fin sin intervención, generando versiones actualizadas de todas las bases de datos de noticias. En total se demora unos 6 minutos en cargar y limpiar las 800 mil noticias (la extracción de la fecha sería lo más intenso en este paso), y luego hay varios scripts que tokenizan y analizan el texto, dando un total aproximado de 20 minutos de procesamiento. El proceso completo está resumido con cierto nivel de detalle en el repositorio.\nEl resultado de todo este proceso es que, a la fecha, mantengo una base de datos de más de 800.000 noticias de Chile, con su texto completo, fecha, y fuente, entre otras variables. Esta base de datos se usa para alimentar mi aplicación de análisis de prensa semanal. si en la base de datos no es de acceso público, puedes acceder a una muestra de 3.000 noticias del año 2024 seleccionadas al azar, para poder realizar análisis de texto o jugar con los datos.\nLa siguiente animación muestra la evolución del proceso de obtención de los datos (se pone buena cuando llega al 2024). Cada paso de la animación representa un mes, y las barras del gráfico representan la cantidad de datos que tenía recolectados hasta ese momento. A través de la animación, se puede ir viendo cómo van obteniéndose nuevos datos mes a mes, así como también en algunos momentos del tiempo se obtienen datos de meses anteriores, incluso años anteriores, a través de estos pasos de obtención de datos retroactivos.\nLa animación fue hecha en R con {ggplot2} y {gganimate}, y fue posible gracias a que en algún momento se me ocurrió que cada noticia extraída también registrara la fecha exacta en la que se obtuvo. Por eso, la animación puede avanzar a partir de la fecha de obtención de la noticia, de forma independiente a la fecha de publicación de la noticia.\nActualmente, y como he escrito en el blog, estoy realizando un procesamiento retroactivo de estos datos de noticias, para poder obtener información extra acerca de las noticias almacenadas. Estoy usando un modelo de lenguaje (LLM) para procesar cada una de las noticias, desde la más reciente hacia atrás, para agregar a cada noticia información de su sentimiento (positivo/negativo), su clasificación o tópico (política/economía/policial, etc.), y un resumen de máximo 40 palabras del texto de cada noticia. Ya llegué a un momento de equilibrio, en el cual he obtenido estas variables para varias decenas de miles de noticias de varios meses atrás hasta el presente, y ahora solamente restaría ir procesando estas variables para las noticias nuevas que se obtengan semana a semana, que son aproximadamente unas 3.000 noticias nuevas por semana.\nCon estos nuevos datos planeo agregar nuevas visualizaciones para la aplicación de análisis de prensa. Así que estén atentos para las novedades que se vienen.\n","date":"2024-12-31T00:00:00Z","excerpt":"\u003cp\u003eA largo de todo el año 2024 he estado realizando \n\u003ca href=\"https://bastianolea.rbind.io/tags/web-scraping/\" target=\"_blank\" rel=\"noopener\"\u003eweb scraping\u003c/a\u003e de distintas fuentes de noticias digitales de Chile. Casi todos los días ejecuto un script que a su vez se ejecuta decenas de otros scripts, que realizan el scraping diario de noticias. El resultado de estos procesos, día tras día, va aumentando la cantidad total de noticias que he ido recolectando. De vez en cuando, algún sitio cambia, o algo falla, y tengo que corregir manualmente los scripts. Cada cierto tiempo ejecuto versiones alternativas de los scripts para hacer una extracción de datos desde fechas anteriores, aumentando la cantidad total de noticias de fechas pasadas, lo que me permite rellenar vacíos en las obtenciones anteriores de noticias diarias, como también aumentar la cantidad total de noticias con datos desde años antes de que empezara este proyecto.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/2024-12-31/","tags":"blog ; ggplot2 ; web scraping ; animaciones","title":"Web scraping de noticias: avances de fin de año"},{"content":"Se denomina web scraping a un conjunto de técnicas usadas para obtener datos desde páginas web. Esto significa poder transformar la información que vemos en distintos sitios de internet en datos que podamos utilizar.\nSe usa el web scraping cuando un sitio web presenta información, cifras, datos, números, o cualquier otro elemento que nos pueda servir, pero sin facilitar acceso a los datos, como sería un enlace de descarga, una API para obtener los datos, o alguna forma de exportar la información. En estos casos tenemos que recurrir al scraping para transformar lo que vemos en la web en datos analizables.\nEn este tutorial aprenderemos a hacer web scraping en R para extraer cualquier información que veamos en un sitio web, y traerla a nuestro entorno de R para poder procesarla como deseemos.\nWeb scraping con {rvest} La primera opción a la hora de hacer web scraping de sitios web con R es el paquete {rvest} (harvest, o cosechar en español). Este paquete, parte del Tidyverse, suele ser la opción más sencilla, más popular y mejor documentada para extraer datos desde sitios web estáticos en R.\n# install.packages(\u0026#34;rvest\u0026#34;) library(rvest) Existen otros paquetes para el web scraping, como {RSelenium}, que se caracteriza por permitirnos obtener datos desde sitios web dinámicos, pero a su vez es menos intuitivo y más difícil de configurar, lo que hace que sea necesario explicarlo en un tutorial aparte.\nExtraer tablas desde un sitio web Como primer paso, extraeremos los datos de una tabla alojada en internet. Cuando las tablas están formateadas apropiadamente, como las tablas que podemos encontrar en Wikipedia, extraerlas resulta muy sencillo.\nEl primer paso es definir la dirección del sitio que queremos scrapear, y luego usamos dos funciones para obtener el código de fuente de este sitio: la función session(), que nos permite conectarnos al sitio web creando una sesión, y luego la función read_html(), que descargará el código del sitio y nos permitirá extraer información desde el mismo.\nurl \u0026lt;- \u0026#34;https://es.wikipedia.org/wiki/Chile\u0026#34; sitio_chile \u0026lt;- session(url) |\u0026gt; read_html() sitio_chile {html_document} \u0026lt;html class=\u0026quot;client-nojs vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-sticky-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-toc-available\u0026quot; lang=\u0026quot;es\u0026quot; dir=\u0026quot;ltr\u0026quot;\u0026gt; [1] \u0026lt;head\u0026gt;\\n\u0026lt;meta http-equiv=\u0026quot;Content-Type\u0026quot; content=\u0026quot;text/html; charset=UTF-8 ... [2] \u0026lt;body class=\u0026quot;skin--responsive skin-vector skin-vector-search-vue mediawik ... El objeto resultante es un documento HTML del cual podremos extraer los elementos del sitio que nos interesen.\nIntentemos extraer la tabla del sitio que posee datos sobre población indígena en el país:\nPara extraer las tablas de este sitio, usamos la función html_table(). Las funciones que extraen elementos de los sitios empiezan con html.\ntablas \u0026lt;- sitio_chile |\u0026gt; html_table() El resultado de esta extracción va a ser una lista. En R, una lista es un tipo de objeto que dentro de sí puede contener múltiples elementos de distinto tipo, forma, o tamaño. En este caso, esta lista contiene todas las tablas que habían en la página. Entonces, tenemos que elegir la tabla que nos interesa analizar.\nEn este caso, nos interesa la octava tabla, que contiene información sobre pueblos indígenas en Chile, por lo que la extraemos de la lista:\n# extraer tabla de la lista tabla_1 \u0026lt;- tablas[[8]] tabla_1 # A tibble: 12 × 3 Pueblos indígenas de Chile (2…¹ Pueblos indígenas de…² Pueblos indígenas de…³ \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Etnia Población % 2 Mapuche 1 745 147 79,8 3 Aimara 156 754 7,2 4 Diaguita 88 474 4,1 5 Quechua 33 868 1,6 6 Lican Antai 30 369 1,4 7 Colla 20 744 0,9 8 Rapanui 9399 0,4 9 Kawésqar 3448 0,1 10 Yagán 1600 0,1 11 Otro o ignorado 95 989 4,4 12 Total 2 185 792 100,0 # ℹ abbreviated names: ¹​`Pueblos indígenas de Chile (2017)[8]​`, # ²​`Pueblos indígenas de Chile (2017)[8]​`, # ³​`Pueblos indígenas de Chile (2017)[8]​` # limpiar datos de la tabla library(dplyr) Attaching package: 'dplyr' The following objects are masked from 'package:stats': filter, lag The following objects are masked from 'package:base': intersect, setdiff, setequal, union # renombrar columnas tabla_1a \u0026lt;- tabla_1 |\u0026gt; janitor::row_to_names(1) |\u0026gt; janitor::clean_names() # convertir variables a numéricas tabla_1b \u0026lt;- tabla_1a |\u0026gt; # remover espacios mutate(poblacion = stringr::str_remove_all(poblacion, \u0026#34;\\\\s+\u0026#34;)) |\u0026gt; # reemplazar comas por puntos mutate(percent = stringr::str_replace(percent, \u0026#34;,\u0026#34;, \u0026#34;.\u0026#34;)) |\u0026gt; # convertir a numéricas mutate(across(c(poblacion, percent), as.numeric)) tabla_1b # A tibble: 11 × 3 etnia poblacion percent \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Mapuche 1745147 79.8 2 Aimara 156754 7.2 3 Diaguita 88474 4.1 4 Quechua 33868 1.6 5 Lican Antai 30369 1.4 6 Colla 20744 0.9 7 Rapanui 9399 0.4 8 Kawésqar 3448 0.1 9 Yagán 1600 0.1 10 Otro o ignorado 95989 4.4 11 Total 2185792 100 ¡Listo! Extrajimos datos desde una tabla de Wikipedia, y ahora podemos usarlos para lo que necesitemos.\nExtraer datos de un sitio usando etiquetas Para scrapear elementos puntuales de un sitio, necesitamos identificarlos, y así extraer solamente lo que nos interesa. Cada elemento en un sitio web, ya sea texto, tablas o imágenes, se crea a partir de etiquetas HTML. Si identificamos las etiquetas HTML de los datos que necesitamos, podemos usarlas para extraerlos.\nProbemos con otra página de Wikipedia:\nurl \u0026lt;- \u0026#34;https://es.wikipedia.org/wiki/Mapache\u0026#34; sitio_mapache \u0026lt;- session(url) |\u0026gt; read_html() Para extraer elementos de un sitio, usamos la función html_elements(), cuyo argumento será el identificador del elemento que queremos extraer. Para extraer el título de la página, usualmente podemos usar la etiqueta HTML h1, que se usa para títulos de mayor jerarquía (existen etiquetas de títulos h1, h2\u0026hellip; hasta h6).\nsitio_mapache |\u0026gt; html_elements(\u0026#34;h1\u0026#34;) {xml_nodeset (1)} [1] \u0026lt;h1 id=\u0026quot;firstHeading\u0026quot; class=\u0026quot;firstHeading mw-first-heading\u0026quot;\u0026gt;\u0026lt;span class=\u0026quot; ... Obtenemos un elemento que empieza con \u0026lt;h1..., lo que significa que extrajimos un elemento en el sitio con dicha etiqueta h1. Para convertir este elemento a texto, usamos la función html_text():\nsitio_mapache |\u0026gt; html_elements(\u0026#34;h1\u0026#34;) |\u0026gt; html_text() [1] \u0026quot;Procyon\u0026quot; Del mismo modo, podemos extraer otros elementos del sitio, tales como los subtítulos de etiqueta h2:\nsitio_mapache |\u0026gt; html_elements(\u0026#34;h2\u0026#34;) |\u0026gt; html_text() [1] \u0026quot;Contenidos\u0026quot; \u0026quot;Descripción\u0026quot; [3] \u0026quot;Hábitat y comportamiento\u0026quot; \u0026quot;Nombre\u0026quot; [5] \u0026quot;Especies\u0026quot; \u0026quot;Especies relacionadas y similares\u0026quot; [7] \u0026quot;Véase también\u0026quot; \u0026quot;Referencias\u0026quot; [9] \u0026quot;Enlaces externos\u0026quot; Si deseamos extraer todo el texto del sitio, apuntamos a la etiqueta de párrafo, que es p:\ntexto \u0026lt;- sitio_mapache |\u0026gt; html_elements(\u0026#34;p\u0026#34;) |\u0026gt; html_text() texto[3:5] [1] \u0026quot;Tiene un tamaño mediano y puede llegar a medir entre 40 cm y 55 cm de alto. Es un poco mayor y más grueso que un gato, de pelo medianamente largo y de color gris plateado (más oscuro en el centro del lomo), el pelo de las extremidades casi blanco, cola larga y anillada (gris plateado con blanco o casi blanco) y una característica mancha de pelo negro que va desde cada mejilla a cada ojo, lo que lo hace muy reconocible, pues parece que lleva un antifaz. En ocasiones se sienta sobre sus cuartos traseros (muslos y glúteos), como hacen los osos, y, como ellos, es de patas traseras plantígradas. En sus extremidades posee cinco dedos largos y ágiles (el tacto es su sentido predominante). Puede pesar hasta 15 kg. \\n\u0026quot; [2] \u0026quot;Las manos de los mapaches tienen ciertas similitudes con las de los primates, producto de la convergencia evolutiva, ya que no están estrechamente relacionados entre sí. Su distribución de los dedos es ideal para que sea capaz de apretar y manipular objetos, aunque no presentan un pulgar oponible \\n\u0026quot; [3] \u0026quot;Es un animal de bosque, especialmente cerca de ríos, aunque ha aprendido a vivir también en áreas habitadas. En su hábitat natural come de todo, desde ranas hasta frutos, pero en las ciudades y suburbios echa mano de los contenedores de basura para comerse los restos de alimentos arrojados en ellos. Los mapaches son nocturnos, tienen un agudo sentido del olfato y son buenos trepadores.\\n\u0026quot; El objeto resultante es un vector de texto, separado en párrafos.\nExtraer datos de un sitio usando clases Hagamos otro ejemplo sobre un sitio menos estructurado que Wikipedia: queremos obtener una lista de animales endémicos de Chile desde el sitio web animalia.bio.\nEn este sitio, cada títuar de los párrafos contiene el nombre de un animal endémico. Por alguna razón, este sitio impide que le hagamos scraping si nos conectamos con la función session()1, pero también es posible extraer el código directamente del sitio, sin crear una sesión. Cuándo se hace web scraping es posible encontrarse con problemas de este tipo, como sitios que evitan entregar su información, o que lo hacen un poco más complicado de lo normal.\nurl \u0026lt;- \u0026#34;https://animalia.bio/es/endemic-lists/country/endemic-animals-of-chile\u0026#34; # extraer código de fuente del sitio sin abrir una sesión sitio_animales \u0026lt;- read_html(url) sitio_animales {html_document} \u0026lt;html lang=\u0026quot;es\u0026quot;\u0026gt; [1] \u0026lt;head\u0026gt;\\n\u0026lt;meta http-equiv=\u0026quot;Content-Type\u0026quot; content=\u0026quot;text/html; charset=UTF-8 ... [2] \u0026lt;style\u0026gt;\\n @media only screen and (max-width: 992px) {\\n .mob_banner {\\n ... [3] \u0026lt;body class=\u0026quot;page-collection \u0026quot; bucket_url=\u0026quot;https://s3.animalia.bio\u0026quot;\u0026gt;\\n ... Con este sitio web no nos resulta extraer la etiqueta h2, h3 o h4 que se usan usualmente para los titulares en sus distintos niveles, sencillamente porque este sitio web no usa esas etiquetas para crear sus titulares.\nsitio_animales |\u0026gt; html_elements(\u0026#34;h2\u0026#34;) {xml_nodeset (0)} En estos casos, tenemos que entrar al sitio web e identificar manualmente cómo se distinguen entre el resto del código los elementos que queremos extraer.\nAccedemos al sitio web con un navegador web cualquiera, y entramos al inspector web de nuestro navegador. Podemos hacer clic derecho en algún elemento del sitio, y elegir la opción Inspeccionar para abrir el inspector:\nSe abrirá un inspector web donde veremos el código de fuente del sitio al lado del sitio mismo. Si movemos nuestro cursor sobre las líneas del código, se destacarán los elementos del sitio web que corresponden a cada línea, o viceversa. De esta forma, podemos encontrar exactamente cuál línea de código se corresponde con el elemento que queremos extraer.\nEn este caso, la línea de código de los titulares de cada animal es a una etiqueta span, que corresponde a un contenedor de texto. Por sí sola, esta etiqueta no identifica de manera única a los datos que queremos extraer, por lo tanto, tenemos que encontrar otro identificador dentro de esta etiqueta que nos sirva.\nEn la mayoría de los casos, los elementos de un sitio web que comparten un mismo estilo gráfico (tipografía, tamaño de texto, color, etc.) comparten también una clase CSS. Una clase CSS es una forma de definir la apariencia de una parte de un sitio web, como un titular, un párrafo de texto, o un botón, definiendo su apariencia en una hoja de estilos CSS a partir del nombre de la clase, para luego aplicar esta misma clase a múltiples elementos del sitio. Por lo tanto, identificar la clase de un elemento web usualmente nos permite extraer múltiples elementos de un sitio que comparten una misma jerarquía o apariencia.\nEn la siguiente imagen se destacan con líneas de colores los elementos del sitio que comparten un estilo y jerarquía, y por ende podemos asumir que comparten una clase en común entre ellos:\nEn el caso de este sitio, y como vemos en el código del inspector web de nuestro navegador, todos los títulos de los animales del sitio poseen la clase collection-animal-title. Entonces, podemos usar esa clase para extraer los elementos usando la función html_elements(). Pero hay que tener en consideración que las clases CSS se escriben anteponiéndoles un punto, por lo que usamos \u0026quot;.collection-animal-title\u0026quot;:\nanimales \u0026lt;- sitio_animales |\u0026gt; html_elements(\u0026#34;.collection-animal-title\u0026#34;) |\u0026gt; html_text() animales[1:10] [1] \u0026quot;Degú\u0026quot; \u0026quot;Chinchilla de cola corta\u0026quot; [3] \u0026quot;Chinchilla de cola larga\u0026quot; \u0026quot;Zorro chilote\u0026quot; [5] \u0026quot;Delfín chileno\u0026quot; \u0026quot;Rana chilena\u0026quot; [7] \u0026quot;Remolinera costera chilena\u0026quot; \u0026quot;Cachudito de juan fernández\u0026quot; [9] \u0026quot;Turca\u0026quot; \u0026quot;Chiricoca\u0026quot; Al extraer los elementos que comparten una misma clase, {rvest} nos entrega un vector que contiene todos elementos del sitio que usan en esa clase; en este caso, los títulos de los animales.\nSi seguimos viendo el código de fuente del sitio, notamos que también hay otras clases que describen los elementos asociados a cada animal: la clase collection-desc que se aplica a los párrafos, y la clase animal-link que contiene el enlace a cada animal.\nPara extraer los textos, apuntamos a la clase .collection-desc, y usamos html_text2(), que además de convertir a texto, también ayuda removiendo caracteres en blanco como espacios o saltos de línea:\ntextos \u0026lt;- sitio_animales |\u0026gt; html_elements(\u0026#34;.collection-desc\u0026#34;) |\u0026gt; html_text2() textos[1:5] [1] \u0026quot;El degú (\u0026lt; mapudungun dewü, rata, ratón) (Octodon degus) es una especie de roedor histricomorfo de la familia Octodontidae. Es conocido también por multitud de otros nombres, como degú/ratón cola de pincel, degú/ratón de las pircas, ardilla chilena o, incluso, ratón cola de trompeta. Sin embargo, este pequeño caviomorfo endémico de Chile es usualmente denominado degú común, para distinguirlo de los otros miembros del género Octodon. El término degú ...por sí solo, no obstante, puede usarse para referirse al género Octodon o, de forma común, a Octodon degus. Otros caviomorfos con los que los degús están estrechamente relacionados son las chinchillas y los conejillos de Indias (ver infraorden Caviomorpha). Menos\u0026quot; [2] \u0026quot;La chinchilla de cola corta (Chinchilla chinchilla), también llamada chinchilla Peruana, chinchilla del altiplano, chinchilla cordillerana y chinchilla real, es una especie de roedor de la familia Chinchillidae. Habita el altiplano andino, desde el sur del Perú y el oeste de Bolivia, hasta el noreste de Chile y el norte de Argentina. Por ser su piel altamente demandada, su caza indiscriminada produjo que la población se reduzca dr ...amáticamente. La UICN estimó una reducción del 90% de la población en sólo 3 generaciones (15 años), colocándola entre las especies amenazadas.En Perú y Bolivia no existen colectas de individuos desde hace 50 años; sin embargo reportes recientes, basados en entrevistas con gente local y observaciones de guardaparques, en la Reserva nacional Eduardo Abaroa en el Departamento de Potosí, en la región del sudoeste boliviano fronteriza con la Región de Antofagasta de Chile, parecen indicar poblaciones extensas. En Bolivia, es poco probable que la especie se encuentre en otros sitios de su rango distribucional histórico. Actualmente existen registros de poblaciones en Argentina y Chile; se encuentra extinta en Perú, y su presencia en Bolivia es incierta. Menos\u0026quot; [3] \u0026quot;La chinchilla de cola larga (Chinchilla lanigera), también conocida como chinchilla chilena, costera o menor, es una especie de roedor de la familia Chinchillidae. Es una de las dos especies del género Chinchilla, la otra es Chinchilla chinchilla. Menos\u0026quot; [4] \u0026quot;El zorro chilote o zorro de Darwin (Lycalopex fulvipes) es un cánido endémico del sur de Chile. Fue descrito por primera vez en 1834 por Charles Darwin, quien lo clasificó erróneamente como una subespecie del zorro chilla (L. griseus). Se considera la especie de cánido en mayor riesgo de extinción en el mundo. No tiene subespecies. Menos\u0026quot; [5] \u0026quot;El delfín chileno (Cephalorhynchus eutropia), también conocido como delfín negro y tonina es una especie de cetáceo odontoceto de la familia Delphinidae que se encuentra mayoritariamente en las costas de Chile y muy escasamente también en la Patagonia argentina. Menos\u0026quot; Para extraer los enlaces, el proceso es levemente distinto, porque si extraemos el texto de los enlaces desde la clase .animal-link, solamente obtendremos el texto, no el enlace en sí mismo:\nsitio_animales |\u0026gt; html_elements(\u0026#34;.animal-link\u0026#34;) |\u0026gt; html_text() [1] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; [5] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; [9] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; [13] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; [17] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; [21] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; [25] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; [29] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; [33] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; [37] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; [41] \u0026quot;Descubrir más \u0026quot; \u0026quot;Descubrir más \u0026quot; Notamos que la etiqueta HTML que se usa para crear los enlaces es la etiqueta a, y dentro de esta etiqueta, además de la clase tenemos otros atributos, tales como el atributo href (hypertext reference), que contiene la dirección a la que apunta el enlace:\nsitio_animales |\u0026gt; html_elements(\u0026#34;.animal-link\u0026#34;) {xml_nodeset (42)} [1] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/degu?endemic=35\u0026quot;\u0026gt;De ... [2] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/short-tailed-chinch ... [3] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/long-tailed-chinchi ... [4] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/darwins-fox?endemic ... [5] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/chilean-dolphin?end ... [6] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/calyptocephalella?e ... [7] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/chilean-seaside-cin ... [8] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/juan-fernndez-tit-t ... [9] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/moustached-turca?en ... [10] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/crag-chilia?endemic ... [11] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/juan-fernndez-firec ... [12] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/slender-billed-para ... [13] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/chilean-tinamou?end ... [14] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/chilean-woodstar?en ... [15] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/masafuera-rayadito? ... [16] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/de-filippis-petrel? ... [17] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/white-throated-tapa ... [18] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/telmatobufo-bullock ... [19] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/telmatobufo-venustu ... [20] \u0026lt;a class=\u0026quot;animal-link\u0026quot; href=\u0026quot;https://animalia.bio/es/rhinella-atacamensi ... ... Para extraer este atributo de los elementos de etiqueta a, usamos la función html_attr(), especificando el atributo que queremos extraer:\nenlaces \u0026lt;- sitio_animales |\u0026gt; html_elements(\u0026#34;.animal-link\u0026#34;) |\u0026gt; html_attr(\u0026#34;href\u0026#34;) enlaces[1:10] [1] \u0026quot;https://animalia.bio/es/degu?endemic=35\u0026quot; [2] \u0026quot;https://animalia.bio/es/short-tailed-chinchilla?endemic=35\u0026quot; [3] \u0026quot;https://animalia.bio/es/long-tailed-chinchilla?endemic=35\u0026quot; [4] \u0026quot;https://animalia.bio/es/darwins-fox?endemic=35\u0026quot; [5] \u0026quot;https://animalia.bio/es/chilean-dolphin?endemic=35\u0026quot; [6] \u0026quot;https://animalia.bio/es/calyptocephalella?endemic=35\u0026quot; [7] \u0026quot;https://animalia.bio/es/chilean-seaside-cinclodes?endemic=35\u0026quot; [8] \u0026quot;https://animalia.bio/es/juan-fernndez-tit-tyrant?endemic=35\u0026quot; [9] \u0026quot;https://animalia.bio/es/moustached-turca?endemic=35\u0026quot; [10] \u0026quot;https://animalia.bio/es/crag-chilia?endemic=35\u0026quot; Una vez que hayamos extraído los elementos del sitio que necesitamos, y considerando que los elementos extraídos tienen todos el mismo largo (dado que cada animal del sitio tenía exactamente un título, un párrafo y un enlace), podemos unir todos los resultados en una tabla para construir un dataframe a partir de los datos scrapeados.\ntabla_animales \u0026lt;- tibble(animales, textos, enlaces) tabla_animales # A tibble: 42 × 3 animales textos enlaces \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Degú El degú (\u0026lt; mapudungun dewü, rata, ratón)… https:… 2 Chinchilla de cola corta La chinchilla de cola corta (Chinchilla … https:… 3 Chinchilla de cola larga La chinchilla de cola larga (Chinchilla … https:… 4 Zorro chilote El zorro chilote o zorro de Darwin (Lyca… https:… 5 Delfín chileno El delfín chileno (Cephalorhynchus eutro… https:… 6 Rana chilena La rana chilena (Calyptocephalella gayi)… https:… 7 Remolinera costera chilena La remolinera costera chilena (Cinclodes… https:… 8 Cachudito de juan fernández El cachudito de Juan Fernández (Anairete… https:… 9 Turca La turca o huet-huet turca (Pteroptochos… https:… 10 Chiricoca La chiricoca (Ochetorhynchus melanurus),… https:… # ℹ 32 more rows Creamos una tabla con los resultados de los tres datos que scrapeamos!\nEn general, ésta es la lógica básica del web scraping. Se debe identificar el sitio web que necesitas, luego explorar el código de fuente del sitio para entender cuáles son las clases de los elementos que quieres extraer, para finalmente extraerlos y procesarlos, de ser necesario. En muchos casos, lo que se hace es obtener una cierta cantidad de enlaces que contienen información que está estructurada de una manera homogénea, y luego se ejecuta el proceso de scraping a cada uno de los enlaces a través de un loop o iteración, por ejemplo con la función purrr::map().\nEl web scraping es una herramienta muy útil para acelerar tus procesos y automatizar obtenciones y actualizaciones de datos, y es algo que resulta muy atractivo para personas interesadas en los datos. Pero ojo, que he visto mucha gente que quiere entrar al análisis de datos empezando por aprender web scraping. En mi experiencia, si bien el web scraping de por si no es algo complejo, sí es una herramienta que requiere de un dominio de estructuras de datos complejas, así como de habilidades avanzadas de limpieza de datos. Ello debido a que los datos que obtenemos por web scraping pueden venir literalmente en cualquier formato, con infinidad de problemas inesperados, y será tarea de el/la analista resolver esos desafíos de interpretación y limpieza de datos de forma creativa, de manera que los resultados salgan correctos tanto para 1 como para 100 páginas de un mismo sitio web.\nSi te sirvió este tutorial, por favor considera hacerme una pequeña donación para poder tomarme un cafecito mientras escribo el siguiente tutorial 🥺\nLo más probable sea que este sitio identifique que estamos conectándonos desde un paquete de R que hace web scraping, y por lo tanto cancela la conexión entregando un error 403 (prohibido). Esto se debe a que cualquier interfaz que se conecte una página web entrega un user agent que los identifica, y muchos sitios usan esta información para prohibir el acceso o mostrar distintos elementos dependiendo del navegador que uses.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-12-27T00:00:00Z","excerpt":"\u003cp\u003eSe denomina web scraping a un conjunto de técnicas usadas para \u003cstrong\u003eobtener datos desde páginas web\u003c/strong\u003e. Esto significa poder transformar la información que vemos en distintos sitios de internet en datos que podamos utilizar.\u003c/p\u003e\n\u003cp\u003eSe usa el web scraping cuando un sitio web presenta información, cifras, datos, números, o cualquier otro elemento que nos pueda servir, pero sin facilitar acceso a los datos, como sería un enlace de descarga, una API para obtener los datos, o alguna forma de exportar la información. En estos casos tenemos que recurrir al scraping para transformar lo que vemos en la web en datos analizables.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/tutorial_scraping_rvest/","tags":"web scraping ; datos","title":"Tutorial: web scraping en R usando {rvest}"},{"content":"Me encuentro en la tarea de tener que procesar cientos de miles de datos, lo cual demorará varios cientos de horas, por lo que necesito que dejar mi computador trabajando durante las noches, por varios días. La idea es que, cada noche, el computador procese de la mayor cantidad de datos posibles, los resultados se guarden, y a la siguiente noche el proceso se repita con datos nuevos, hasta que en algunos días logre procesar todos los cientos de miles de datos que necesito.\nEl proceso en sí mismo consiste en utilizar modelos de lenguaje (LLM) para extraer sentimiento, clasificación y resumen de cada unidad de los datos, que corresponden a textos de entre 100 a 5000 caracteres. Este procesamiento utiliza el 100% de mi tarjeta de video, por lo que el computador alcanzar altas temperaturas, así que prefiero hacer este cálculo mientras no estoy usándolo para otras cosas.\nComo cada unidad de datos tiene una identificación única, cada noche lo que hago es extraer una muestra aleatoria del conjunto total de datos, excluyendo los datos cuyo id único ya fue procesado anteriormente. De esa manera no se repite el procesamiento de datos que ya fueron procesados.\nLas primeras noches, lo que hice fue darle al loop una muestra aleatoria de un tamaño específico. El tamaño de esta muestra dependía del tiempo promedio de ejecución de cada paso del loop, multiplicado por x, para que el tiempo total de ejecución durara un poco menos de ocho horas, de modo que cuando amaneciera, el proceso ya haya terminado y yo pudiera usar mi computador normalmente.\nPero luego vino la navidad 🎄 y yo iba a salir de mi casa sin saber en cuántas horas iba a volver. Así que no sabía cuántos datos darle al loop. Si le daba muy pocos, el computador podía terminar antes de que yo volviera, y hubieran sido muchas horas de procesamiento perdido, pero si le daba muchos datos, yo podía llegar a la casa y encontrarme con mi computador inutilizable por estar trabajando al 100% en un proceso que aún no terminaba.\nLo ideal sería poder iniciar el proceso, que el computador se ponga a calcular, y que eventualmente yo pueda detener el proceso y obtener los resultados que se alcanzaron a calcular durante ese tiempo. Pero si detienes un proceso en R, el proceso termina sin asignar su resultado. Entonces no puedo llegar y tener el proceso.\nEsto podría evitarse si, en vez de que el proceso con purrr::map() asigne sus resultados finales a un objeto de R, se use purrr::walk() para que cada paso del proceso guarde su resultado individual como un archivo por separado, y luego en otro momento se unan todos los resultados individuales. Esto lo he hecho antes con buenos resultados, pero esta vez simplemente no quería hacerlo así.\nLa solución sería ser que el proceso termine desde adentro; es decir, que algo dentro del proceso haga que éste termine cuando yo se lo pida. En un loop normal, esto se haría con el uso de un if con un stop() dentro, lo cual detendría las siguientes iteraciones. Pero el problema seguiría siendo el cómo hacerle saber al loop, desde afuera, que tiene que terminarse. Por otro lado, los loops del paquete {purrr} no tienen un equivalente a stop(), dado que no tendría sentido bajo la lógica de la programación funcional. Pero lo que sí puedan hacer los loops de {purrr} es retornar NULL con antelación, como una forma de saltarse el paso. Por lo tanto, la forma de hacer que el loop \u0026ldquo;termine\u0026rdquo; sería haciéndole que retorne nulo para todos los siguientes pasos. El loop en sí no terminaría, pero las iteraciones que retornen nulo ocuparían un tiempo de procesamiento negligible, haciendo que las miles de operaciones restantes terminen en fracción de segundo. Entonces faltaría la forma de hacer que, desde fuera, el loop supiera que tiene que empezar a saltarse todas las operaciones y retornar nulo.\nLa solución que encontré fue crear un archivo de texto que tuviera cualquier texto, pero cuando eventualmente el texto dijera algo como \u0026ldquo;stop\u0026rdquo;, el loop retornara nulo. La forma de que el loop supiera lo que contiene archivo de texto sería simplemente agregar un paso al principio de del loop que lea el archivo de texto. En la lectura del archivo, habría que confirmar si dice o no la palabra mágica, y si la dice, retornar NULL.\nif (read.delim(\u0026#34;stop.txt\u0026#34;, header = FALSE)[[1]] == \u0026#34;stop\u0026#34;) return(NULL) No encontré una forma más elegante de leer un texto y obtener su resultado como un vector limpio, porque las funciones de lectura que probé asumen que esperas un dataframe y no solamente un texto, pero es suficientemente conciso.\nEntonces, si el loop está ejecutándose, pero yo llego y edito el archivo de texto para que diga la palabra mágica, el loop empezará a retornar nulo, y terminará a la brevedad.\nUna medición de rendimiento sencilla me indica que la lectura del archivo de texto se tarda tan solo 61 millonésimas de segundo y apenas 16KB de memoria, por lo que la lectura del archivo de texto en cada paso no afectaría virtualmente en nada al rendimiento del proceso:\nbench::mark( read.delim(\u0026#34;stop.txt\u0026#34;, header = FALSE)[[1]] ) # expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc total_time result memory time gc # \u0026lt;bch:expr\u0026gt; \u0026lt;bch:\u0026gt; \u0026lt;bch:\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;bch:byt\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;bch:tm\u0026gt; \u0026lt;list\u0026gt; \u0026lt;list\u0026gt; \u0026lt;list\u0026gt; \u0026lt;list\u0026gt; # 1 \u0026#34;read.delim(\\\u0026#34;stop.… 58.1µs 61.7µs 15342. 16KB 44.7 6868 20 448ms \u0026lt;chr\u0026gt; \u0026lt;Rprofmem\u0026gt; \u0026lt;bench_tm\u0026gt; \u0026lt;tibble\u0026gt; Esta técnica dio resultado, porque ahora puedo dejar el proceso andando con una cantidad muy alta de de casos seleccionados al azar, y cuando cambio el archivo de texto para señalar que ya quiero que se termine el loop, todos los miles de pasos restantes que aún no eran procesados simplemente retornan nulo y terminan de forma instantánea, permitiendo que el loop termine correctamente y sus resultados se guarden. De esta forma no tengo que estar estimando cuántos datos voy a calcular, sino que simplemente dejo calculando y hago terminar el proceso cuando yo lo necesite.\nAsí que, durante las 24 horas que estuve fuera de casa por la navidad, el proceso alcanzó a calcular 15 mil textos, luego detuve el proceso, trabajé, y la noche siguiente a la navidad volví a dejarlo calculando y alcanzó a procesar 8 mil textos más 🥰\n","date":"2024-12-26T00:00:00Z","excerpt":"\u003cp\u003eMe encuentro en la tarea de tener que \n\u003ca href=\"/blog/2024-12-20/\"\u003eprocesar cientos de miles de datos\u003c/a\u003e, lo cual demorará varios cientos de horas, por lo que necesito que dejar mi computador trabajando durante las noches, por varios días. La idea es que, cada noche, el computador procese de la mayor cantidad de datos posibles, los resultados se guarden, y a la siguiente noche el proceso se repita con datos nuevos, hasta que en algunos días logre procesar todos los cientos de miles de datos que necesito.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/2024-12-26/","tags":"consejos ; curiosidades ; blog ; purrr ; optimización","title":"Haciendo que un loop muy largo termine sin cancelarlo"},{"content":"El análisis de sentimientos es una técnica de análisis de texto donde se aplican distintos algoritmos para poder clasificar textos de distinta longitud y complejidad en un conjunto preestablecido de categorías relacionadas al sentimiento de dichos textos. Con el sentimiento de los textos nos referimos a la información subjetiva que entregan estos textos, así como los afectos que producen. Por ejemplo, \u0026ldquo;odio a mi gato\u0026rdquo; versus \u0026ldquo;mi gatita es tan tierna\u0026rdquo; son dos textos que expresan distintos niveles de negatividad/positividad, agresividad, ternura, etcétera. Las categorías del análisis del sentimiento suelen ser positivo, neutro y negativo, u otras más complejas, como agrado (agradable/desagradable), activación (activo/pasivo), entre otros.\nEn otras palabras, la aplicación de un análisis de sentimiento a un conjunto de textos nos permitiría clasificar estos textos en categorías como positivo, neutro y negativo, en base a la forma en que describen el tema del que tratan, o bien, cualidades de las palabras que se usen dentro del texto.\nEl análisis de sentimiento usualmente es un medio para poder contar con nueva información que nos permita desarrollar nuevos análisis. En este sentido, la nueva variable de sentimiento resulta útil de ser cruzada con otras variables acerca del texto, tales como variables que identifiquen la autoría del texto, su fuente, quien enuncia el texto, de qué trata el texto, etc.\nAlgunos usos del análisis de sentimiento son:\nEvaluar si los textos dentro de un conjunto de textos o corpus son positivos o negativos, Correlacionar si los textos de distintos autores difieren en sus sentimientos, Analizar si textos sobre determinadas temáticas se correlacionan con sentimientos específicos, Estudiar si ciertos temas son tratados de una forma específica por ciertos autores, y de una forma distinta por otros Comparar si textos sobre un mismo tema son expuestos de forma distinta por distintos autores o fuentes Pensemos en el ejemplo de un conjunto de artículos de prensa o noticias, cada uno con fecha y el medio de comunicación del que proviene. Si hacemos una selección de noticias sobre un tema específico, podríamos analizar cómo cambia el sentimiento dominante con el que se plantea la temática en distintos momentos del tiempo, o si dos medios de comunicación representan una misma temática bajo distinto sentimiento.\nlibrary(dplyr) library(readr) Probemos haciendo un análisis de sentimiento a partir de un corpus de textos de noticias chilenas publicadas el año 2024. Los datos son obtenidos de este repositorio de obtención automatizada de textos de noticias de prensa escrita chilena.\n# dirección web donde se encuentran los datos url_datos \u0026lt;- \u0026#34;https://raw.githubusercontent.com/bastianolea/prensa_chile/refs/heads/main/datos/prensa_datos_muestra.csv\u0026#34; # lectura de los datos ubicados en internet noticias \u0026lt;- read_csv2(url_datos) # extracción de una marcha aleatoria de 20 noticias noticias_muestra \u0026lt;- noticias |\u0026gt; slice_sample(n = 20) En el código anterior, identificamos la ubicación de los datos que usaremos de prueba, que corresponden a un conjunto de 3000 noticias públicas del año 2024, y cargamos los datos directamente desde internet. Como se trata de una prueba, reduciremos la cantidad de noticias a tan sólo 20:\nnoticias_muestra # A tibble: 20 × 4 titulo cuerpo fuente fecha \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;date\u0026gt; 1 \u0026quot;Sismo de 7,3 de magnitud con epicentro en San Pedr… \u0026quot;El m… La Na… 2024-07-18 2 \u0026quot;Mirar, inclinar y tocar: Las recomendaciones del B… \u0026quot;Aton… Emol 2024-09-13 3 \u0026quot;Frío y lluvia en la zona central: El pronóstico de… \u0026quot;El p… Megan… 2024-04-26 4 \u0026quot;Comisión de Minería y Energía de la Cámara aprueba… \u0026quot;De m… D. Fi… 2024-11-06 5 \u0026quot;Nuevo líder de Salesforce Chile entrega detalles d… \u0026quot;En f… D. Fi… 2024-09-30 6 \u0026quot;Carabineros incauta 84 máquinas de azar y detiene … \u0026quot;Los … La Na… 2024-04-05 7 \u0026quot;Este martes se estrena la nueva temporada de Indec… \u0026quot;Este… Megan… 2024-08-13 8 \u0026quot;Trabajo activa subsidio al empleo en zonas afectad… \u0026quot;El s… D. Fi… 2024-02-20 9 \u0026quot;José Piñera aparece en misa fúnebre de su hermano … \u0026quot;El e… Megan… 2024-02-09 10 \u0026quot;Hombre de 41 años es asesinado en Viña: fue atacad… \u0026quot;El p… La Na… 2024-09-17 11 \u0026quot;Canto de ballenas jorobadas enviada al espacio ins… \u0026quot;Desd… El De… 2024-05-06 12 \u0026quot;Juan Pablo Hermosilla adelanta que entregará chats… \u0026quot;Juan… El Dí… 2024-10-23 13 \u0026quot;Eclipse Solar 2024: revisa la fecha y ciudades de … \u0026quot;El m… El Mo… 2024-10-01 14 \u0026quot;Lluvia en Santiago: ¿Hasta cuándo precipitará inte… \u0026quot;En l… Megan… 2024-06-21 15 \u0026quot;OLCA y alza de la luz: “El sistema energético está… \u0026quot;En u… El Ci… 2024-07-06 16 \u0026quot;Caso Pío Nono: fiscalía busca evitar ir a juicio t… \u0026quot;Qué … Ex-An… 2024-01-24 17 \u0026quot;“Le mintió a Chile”: Diputado Guzmán presentó ante… \u0026quot;El d… CNN C… 2024-11-18 18 \u0026quot;PDI confirma detención de \\\u0026quot;Perro Elvis\\\u0026quot; por pres… \u0026quot;Dura… 24 Ho… 2024-08-31 19 \u0026quot;Otra de Maduro: El controvertido mandatario boliva… \u0026quot;Vene… Publi… 2024-08-09 20 \u0026quot;“Ha sido un día difícil”: ministra Tohá confirma q… \u0026quot;“Ha … Publi… 2024-10-17 Los datos tienen una columna con el título, otra con el cuerpo de la noticia, y finalmente la fuente de la noticia y su fecha de publicación.\nConfiguración del modelo de lenguaje local Para poder usar modelos LLM localmente con R, tenemos que instalar Ollama en nuestro equipo, que es la aplicación que nos permite obtener y usar modelos de lenguaje locales, y ejecutar la aplicación en nuestro computador. Ollama tiene que estar abierto para poder proveer del modelo a nuestra sesión de R.\nPuedes encontrar instrucciones más detalladas sobre configurar el uso de IA en R en esta publicación. Luego, ya sea desde Ollama o desde R, tenemos que instalar un modelo de lenguaje. Desde R, es tan simple como ejecutar: ollamar::pull(\u0026quot;llama3.2:1b\u0026quot;), definiendo el modelo que queremos instalar. Al elegir un modelo, debes considerar las capacidades de tu computador para ejecutar los modelos, y que el tamaño del modelo es directamente proporcional con la calidad de sus respuestas1.\nUna vez tengas Ollama abierto en tu computador, y un modelo previamente instalado, puedes proceder a usarlo en R en el siguiente paso.\nExtracción de sentimiento a partir del texto Para realizar el análisis de sentimiento, utilizaremos la ayuda de un modelo de lenguaje de gran tamaño (LLM), los cuales están entrenados para este tipo de tareas. Cargamos el paquete {mall} para uso de LLMs en R, y {beepr} para anunciar con una campanita cuando se termine el procesamiento, porque puede tardar unos minutos.\nlibrary(mall) library(beepr) # ollamar::pull(\u0026#34;llama3.1:8b\u0026#34;) # instalar modelo llm_use(\u0026#34;ollama\u0026#34;, \u0026#34;llama3.1:8b\u0026#34;) # cargar modelo La función llm_sentiment() del paquete {mall} facilita la extracción del sentimiento a partir del texto. Tan sólo es necesario indicar la columna que contiene el texto, y las categorías de sentimiento que deseamos obtener. Por medio del siguiente código, se alimenta al modelo de lenguaje con cada uno de los textos, y se le pide obtener como respuesta si el texto es positivo, neutro o negativo.\nnoticias_sentimiento \u0026lt;- noticias_muestra |\u0026gt; llm_sentiment(cuerpo, pred_name = \u0026#34;sentimiento\u0026#34;, options = c(\u0026#34;positivo\u0026#34;, \u0026#34;neutro\u0026#34;, \u0026#34;negativo\u0026#34;)) beep() En mi computador personal, este proceso en total tomó 88 segundos, lo que significa que tarda aproximadamente cuatro segundos por cada texto que el modelo analiza y clasifica. Esto depende de las capacidades gráficas de tu computador, y del largo del texto. Las noticias suelen tener como mínimo varios cientos de palabras, lo que hace que el proceso sean un poco más lento.\nA continuación, vemos los resultados del análisis de sentimiento:\nnoticias_sentimiento |\u0026gt; select(sentimiento, titulo) # A tibble: 20 × 2 sentimiento titulo \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 neutro \u0026quot;Sismo de 7,3 de magnitud con epicentro en San Pedro de Atacama … 2 positivo \u0026quot;Mirar, inclinar y tocar: Las recomendaciones del Banco Central … 3 negativo \u0026quot;Frío y lluvia en la zona central: El pronóstico de Alejandro Se… 4 positivo \u0026quot;Comisión de Minería y Energía de la Cámara aprueba en particula… 5 positivo \u0026quot;Nuevo líder de Salesforce Chile entrega detalles de los súper a… 6 negativo \u0026quot;Carabineros incauta 84 máquinas de azar y detiene a 3 personas … 7 positivo \u0026quot;Este martes se estrena la nueva temporada de Indecisos: Candida… 8 positivo \u0026quot;Trabajo activa subsidio al empleo en zonas afectadas por los in… 9 negativo \u0026quot;José Piñera aparece en misa fúnebre de su hermano Sebastián: Le… 10 negativo \u0026quot;Hombre de 41 años es asesinado en Viña: fue atacado con “un obj… 11 positivo \u0026quot;Canto de ballenas jorobadas enviada al espacio inspira obra de … 12 positivo \u0026quot;Juan Pablo Hermosilla adelanta que entregará chats de su herman… 13 positivo \u0026quot;Eclipse Solar 2024: revisa la fecha y ciudades de Chile en las … 14 negativo \u0026quot;Lluvia en Santiago: ¿Hasta cuándo precipitará intensamente en l… 15 negativo \u0026quot;OLCA y alza de la luz: “El sistema energético está centrado en … 16 negativo \u0026quot;Caso Pío Nono: fiscalía busca evitar ir a juicio tras revés par… 17 negativo \u0026quot;“Le mintió a Chile”: Diputado Guzmán presentó antecedentes a Co… 18 negativo \u0026quot;PDI confirma detención de \\\u0026quot;Perro Elvis\\\u0026quot; por presunta responsa… 19 negativo \u0026quot;Otra de Maduro: El controvertido mandatario bolivariano ordena … 20 negativo \u0026quot;“Ha sido un día difícil”: ministra Tohá confirma que Presidente… noticias_sentimiento |\u0026gt; select(sentimiento) |\u0026gt; table() sentimiento negativo neutro positivo 11 1 8 Conclusión Revisando los resultados a la rápida, vemos que una noticia sobre un sismo es aparentemente neutra, probablemente por la forma en que esté redactada. Noticias sobre la aprobación de un proyecto de ley, la presentación de nuevas tecnologías, o beneficios para personas afectadas por incendios, son interpretadas como positivas. Por último, noticias sobre frentes de mal tiempo, operaciones policiales, funerales, crímenes y asesinatos son interpretadas como negativas. También, vemos que una noticia donde una ministra interpreta una jornada como difícil también es negativa, y otra donde se critica al mandatario de un país por sus actos también.\nEn este sentido, el desempeño de aplicar el modelo de lenguaje para obtener el sentimiento de los textos parece satisfactorio. De forma automatizada, y sin intervención ni supervisión humana, hemos obtenido valiosa información que resume un aspecto del significado codificado en los textos.\nA partir de esta nueva variable, podemos proceder, por ejemplo, a detectar noticias que contienen ciertos conceptos, y analizar si dicha temática se trata mayormente de forma positiva o negativa. O si está positividad o negatividad varía a través de las distintas fuentes de información, momentos en el tiempo, u otras particularidades del texto.\nConsideraciones Los resultados siempre van a depender de la calidad de los datos que alimentes al modelo. También es necesario considerar que, debido a que los modelos de lenguaje no son deterministas, los resultados siempre pueden variar. En otras palabras, si analiza el sentimiento de un mismo texto múltiples veces, puede que los resultados sean levemente distintos. Teniendo esto en consideración, es importante ser críticos al momento de recibir resultados de un modelo de lenguaje o de cualquier otra herramienta de inteligencia artificial, y también considerar estos resultados como convenientes intentos de aproximación a la realidad: por un lado, los modelos de lenguaje facilitan muchas tareas, pero por otro, nunca podemos fiarnos 100% de sus resultados, sobre todo cuando estamos tratando con temáticas complejas como son el significado expresado por textos que reflejan facetas complejas de la realidad social y política.\nSi este tutorial te sirvió, por favor considera hacerme una pequeña donación para poder tomarme un cafecito mientras escribo el siguiente tutorial 🥺\nSi tu computador es muy básico (tiene poca memoria RAM), recomiendo instalar Llama 3.2 1B. Si tiene al menos 8GB de memoria, recomiendo Llama 3.2 3B. Si tienes suficiente memoria (más de 8GB), recomiendo Llama 3.1 8B.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-12-22T00:00:00Z","excerpt":"\u003cp\u003eEl análisis de sentimientos es una técnica de análisis de texto donde se aplican distintos algoritmos para poder clasificar textos de distinta longitud y complejidad en un conjunto preestablecido de categorías relacionadas al sentimiento de dichos textos. Con el \u003cem\u003esentimiento\u003c/em\u003e de los textos nos referimos a la información subjetiva que entregan estos textos, así como los afectos que producen. Por ejemplo, \u0026ldquo;odio a mi gato\u0026rdquo; versus \u0026ldquo;mi gatita es tan tierna\u0026rdquo; son dos textos que expresan distintos niveles de negatividad/positividad, agresividad, ternura, etcétera. Las categorías del análisis del sentimiento suelen ser \u003cem\u003epositivo, neutro\u003c/em\u003e y \u003cem\u003enegativo,\u003c/em\u003e u otras más complejas, como agrado (agradable/desagradable), activación (activo/pasivo), entre otros.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/analisis_sentimiento_llm/","tags":"análisis de texto ; inteligencia artificial","title":"Análisis de sentimiento usando modelos de lenguaje (LLM) locales en R"},{"content":"Resumen de las actualizaciones recientes de mis trabajos. Recientemente lancé un visualizador de temperaturas extremas de Chile, que permite ver gráficamente los efectos del calentamiento global medidas por las estaciones meteorológicas del país. Además, actualicé los datos del visualizador de delincuencia, que ahora tiene datos hasta septiembre de 2024, y también del visualizador de femicidios, que también ahora tiene datos hasta la fecha.\nEn paralelo, otra aplicación que se actualiza frecuentemente es la de análisis de prensa, que se actualiza todos los lunes o martes de la semana con las noticias hasta el domingo anterior; es decir, muestra los datos de la última semana completa hacia atrás (esto porque el visualizador es de datos semanales, no diarios, entonces tienen que estar terminadas las semanas para poder incluirlas en el análisis, de lo contrario las semanas aparecerían con menos datos).\nActualización: visualizador de delitos, ahora con datos hasta septiembre de 2024 📈 Explora la evolución de la delincuencia en cualquier comuna o región de Chile, y analiza el fenómeno de la inseguridad desde datos oficiales.\nRecuerda que todas las apps que hago son de código abierto, y en sus repositorios incluyo los datos para que puedas reusarlos en tus propios análisis!\nNuevo: Visualizador de temperaturas extremas en Chile 🌤️🔥\nConsulta datos históricos de temperaturas extremas en el país, desde 1970 hasta 2024, y visualiza los cambios históricos en las temperaturas producto de la crisis climática 😞\nEste proyecto parte por dos razones: porque quería reutilizar los datos publicados en la plataforma de Datos Abiertos del Estado (muy recomendable, pero también requiere de muchas mejoras), y porque genuinamente tenía curiosidad sobre la evolución en las temperaturas que estamos viviendo.\nMe tardé solamente la tarde del domingo en hacer el scraping de datos, y parte del domingo y la mañana del lunes en hacer el dashboard. Desarrollar este tipo de proyectos con R es realmente rápido!\nLo otro que quería hacer eran estos gráficos radiales que muestran múltiples años en un anillo de meses. Nunca los había hecho! Quedan muy lindos y son muy fáciles de hacer con {ggplot2}.\nNunca había explorado ni visualizado datos de clima, así que acepto cualquier crítica ☺️\nEn el repositorio del proyecto dejé los conjuntos de datos limpios y en un solo archivo, para quienes deseen explorar!\n","date":"2024-12-21T00:00:00Z","excerpt":"\u003cp\u003eResumen de las actualizaciones recientes de mis trabajos. Recientemente lancé un \n\u003ca href=\"https://bastianolea.rbind.io/apps/temperaturas_chile/\" target=\"_blank\" rel=\"noopener\"\u003evisualizador de temperaturas extremas de Chile\u003c/a\u003e, que permite ver gráficamente los efectos del calentamiento global medidas por las estaciones meteorológicas del país. Además, actualicé los datos del \n\u003ca href=\"https://bastianolea.rbind.io/apps/delincuencia_chile/\" target=\"_blank\" rel=\"noopener\"\u003evisualizador de delincuencia\u003c/a\u003e, que ahora tiene datos hasta septiembre de 2024, y también del \n\u003ca href=\"https://bastianolea.rbind.io/apps/femicidios_chile/\" target=\"_blank\" rel=\"noopener\"\u003evisualizador de femicidios\u003c/a\u003e, que también ahora tiene datos hasta la fecha.\u003c/p\u003e\n\u003cp\u003eEn paralelo, otra aplicación que se actualiza frecuentemente es la de \n\u003ca href=\"https://bastianolea.rbind.io/apps/prensa_chile/\" target=\"_blank\" rel=\"noopener\"\u003eanálisis de prensa\u003c/a\u003e, que se actualiza todos los lunes o martes de la semana con las noticias hasta el domingo anterior; es decir, muestra los datos de la última semana completa hacia atrás (esto porque el visualizador es de datos semanales, no diarios, entonces tienen que estar terminadas las semanas para poder incluirlas en el análisis, de lo contrario las semanas aparecerían con menos datos).\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/2024-12-21/","tags":"blog","title":"Novedades: app de temperaturas extremas, actualizaciones de otras apps"},{"content":"Anoche dejé el computador procesando 5000 noticias por 8 horas usando un modelo de lenguaje (LLM) local en R para obtener clasificación, resumen y sentimiento de cada texto.\nEsto porque tengo una base de datos de más de 600 mil noticias chilenas, con su texto completo, y quiero empezar a sacarle más provecho. Por ejemplo, saber si noticias que hablan de ciertos temas son positivas o negativas (sentimiento), o simplemente clasificar de manera automatizada las noticias para separar las de política y economía de las de deportes y farándula.\nPara la inferencia usé el modelo Llama 3.1 de 8B, a través de Ollama y el paquete de R {mall}. Hice un script para cada tipo de inferencia, y están estructurados para cargar los resultados existentes de su mismo proceso, excluir los textos que ya fueron procesados, y empezar a procesar una muestra aleatoria de una cierta cantidad de nuevos textos. La cantidad aleatoria está definida por el tiempo estimado que va a tomar en procesarlas. Se demora aproximadamente una hora en procesar 1000 noticias, pero esto depende de qué tanto texto tenga la noticia, y del tipo de inferencia que se está realizando; por ejemplo, los resúmenes de texto tardan más que el análisis de sentimiento.\nEl proceso en sí mismo se ve más o menos así:\nsentimientos \u0026lt;- map(datos_limpios_split, \\(dato) { inicio \u0026lt;- now() message(paste(\u0026#34;procesando\u0026#34;, dato$id)) # obtener sentimiento sentimiento \u0026lt;- dato$texto |\u0026gt; llm_vec_sentiment(options = c(\u0026#34;positivo\u0026#34;, \u0026#34;neutral\u0026#34;, \u0026#34;negativo\u0026#34;)) # reintentar 1 vez if (is.na(sentimiento)) { sentimiento \u0026lt;- dato$texto |\u0026gt; llm_vec_sentiment(options = c(\u0026#34;positivo\u0026#34;, \u0026#34;neutral\u0026#34;, \u0026#34;negativo\u0026#34;)) } final \u0026lt;- now() if (is.na(sentimiento)) return(NULL) # resultado resultado \u0026lt;- tibble(id = dato$id, sentimiento, tiempo = final - inicio, n_palabras = dato$n_palabras ) return(resultado) }); beep() Se trata de un loop a partir de una lista de dataframes de una fila, cada elemento de la lista conteniendo los datos de cada noticia. Dentro de la alteración se extrae el sentimiento, se clasifica o se genera un resumen con una función del paquete {mall}. Luego, en caso de que la inferencia haya fallado (los modelos de lenguaje no son deterministas, por lo que pueden falla la tarea o entregar resultados inesperados de vez en cuando), se reintenta una vez, y al final se entregan los resultados con una variable que representa el tiempo que tardó el procesamiento.\nTambién hice gráficos para monitorear el avance, y si bien no es rápido (4 segundos promedio por noticia), confirmé que el computador no se sobrecalienta (tuvo un desempeño parejo luego de 8 horas). También se nota cómo los textos más largos tardan más en obtener resultados.\nDigo que el computador no se sobrecalienta, porque tengo un MacBook Air M3, que si bien tiene un GPU poderoso, no tiene ventiladores 😰 así que tuve miedo de que el desempeño bajara por la temperatura. Mi solución: dejarlo frente a un ventilador 😂\nVoy a dejarlo unas noches más procesando, y con estos nuevos datos voy a poder actualizar mi aplicación de análisis de prensa para ponerle cosas más interesantes 🥰\n","date":"2024-12-20T00:00:00Z","excerpt":"\u003cp\u003eAnoche dejé el computador procesando 5000 noticias por 8 horas usando un modelo de lenguaje (LLM) local en R para obtener clasificación, resumen y sentimiento de cada texto.\u003c/p\u003e\n\u003cp\u003eEsto porque tengo una \n\u003ca href=\"https://github.com/bastianolea/prensa_chile\" target=\"_blank\" rel=\"noopener\"\u003ebase de datos de más de 600 mil noticias chilenas\u003c/a\u003e, con su texto completo, y quiero empezar a sacarle más provecho. Por ejemplo, saber si noticias que hablan de ciertos temas son positivas o negativas (sentimiento), o simplemente clasificar de manera automatizada las noticias para separar las de política y economía de las de deportes y farándula.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/2024-12-20/","tags":"inteligencia artificial ; blog","title":"Procesando datos de texto en masa usando modelos de lenguaje (LLM)"},{"content":" Visualizador de datos de temperaturas extremas en el país. Permite consultar rápidamente datos históricos para obtener gráficos que revelan los cambios en temperaturas a través del tiempo en nuestro país. El objetivo es visibilizar los efectos del cambio climático por medio de los datos.\nEn esta plataforma puedes visualizar datos históricos sobre temperaturas extremas del país, cuyo objetivo es visibilizar los efectos del cambio climático por medio de los datos.\nLos datos se obtienen desde el Portal de Datos Abiertos del Estado de Chile mediante web scrapping. El proyecto unifica todas las fuentes de datos separadas en una sola base de datos de temperaturas, los cuales son utilizados en la plataforma de visualización interactiva.\nLos datos son obtenidos desde fuentes oficiales, y abarcan aproximadamente desde 1970 a 2024.\nEste repositorio obtiene datos desde el Portal de Datos Abiertos del Estado de Chile mediante web scrapping, unifica todas las fuentes de datos separadas en una sola base de datos de temperaturas, y produce visualizaciones y una plataforma de visualización interactiva.\nAccede al visualizador de datos por este enlace.\nEl producto principal de este código es una base de datos de temperaturas extremas en Chile, por estación meteorológica y por día, desde 1970 a 2024 disponible convenientemente en un solo archivo .csv (también disponible en .rds y en formato .parquet)\nEste proyecto parte por dos razones: porque quería reutilizar los datos publicados en la plataforma de Datos Abiertos del Estado (muy recomendable, pero también requiere de muchas mejoras), y porque genuinamente tenía curiosidad sobre la evolución en las temperaturas que estamos viviendo.\nMe tardé solamente la tarde del domingo en hacer el scraping de datos, y parte del domingo y la mañana del lunes en hacer el dashboard. Desarrollar este tipo de proyectos con R es realmente rápido!\nLo otro que quería hacer eran estos gráficos radiales que muestran múltiples años en un anillo de meses. Nunca los había hecho! Quedan muy lindos y son muy fáciles de hacer con {ggplot2}.\nNunca había explorado ni visualizado datos de clima, así que acepto cualquier crítica️! 🥰\nObtención de los datos Los datos son obtenidos de forma semi-automática usando técnicas de web scrapping con el paquete {RSelenium}. Esto debido a que el portal de datos abiertos no tiene una buena interfaz de usuario (no permite abrir resultados en nuevas pestañas, no permite copiar los enlaces de las fuentes de datos, y hay que actualizar las páginas entre 1 a 8 veces para que muestren los resultados). El script obtener_temperaturas.R simplifica tres tareas para obtener los datos: realizar una búsqueda y ampliar la cantidad de resultados mostrados en la página, entrar a cada uno de los enlaces del resultado de la búsqueda, y dentro de los conjuntos de datos, descargar todos los archivos con un solo comando. Esta ahorra muchísimo tiempo, considerando que los datos de temperatura vienen separados por semestre y año, lo que significa que hay que entrar a aproximadamente 16 conjuntos de datos distintos, y dentro de estos conjuntos de datos hay que descargar aproximadamente 6 archivos separados, mientras la plataforma dificulta abrir estos resultados por pestañas.\nFuentes Datos de la Dirección General de Aeronáutica Civil subidos a la plataforma de Datos Abiertos del Estado Dirección Meteorológica de Chile ","date":"2024-12-16T00:00:00Z","excerpt":"Consulta datos históricos de temperaturas extremas en el país, desde 1970 hasta 2024, y visualiza los cambios históricos en las temperaturas producto de la crisis climática.","href":"https://bastianoleah.netlify.app/blog/app_temperaturas_chile/","tags":"apps ; Chile","title":"App: Temperaturas extremas en Chile"},{"content":" ⚠️ este post está desactualizado, ya que es preferible usar el asistente {gander} El paquete {pal} te permite crear asistentes para programar en R, potenciados por modelos de lenguaje (LLM). La utilidad de estos asistentes es que pueden ayudarte a realizar tareas rápidamente a partir de tu código de R, o incluso a partir de un texto que describa lo que quieres hacer.\nEn este post te enseño a crear y usar asistentes de {pal} para dos tareas que realizo frecuentemente: describir lo que hace un código de R, y traducir una instrucción a código de {dplyr}.\nLa idea principal es que puedes seleccionar tu código o texto en R, y presionar una combinación de teclas para elegir tu asistente. Dependiendo del tipo de asistente, se agregará código arriba o abajo de lo que seleccionaste, o bien, se va a reemplazar lo que seleccionaste por lo que haga el asistente.\nInstalar un modelo de lenguaje local desde R Como este paquete depende de el uso de un modelo de lenguaje para funcionar, necesitas tener instalado un modelo de lenguaje en tu equipo, o tener una suscripción a un modelo de lenguaje en la nube. Personalmente prefiero usar un modelo de lenguaje local: Llama 3.1 con 8 billones de parámetros. Para instalar un modelo localmente en tu equipo desde R, primero tienes que instalar Ollama en tu equipo, abrir la aplicación, y luego instalar el paquete de R {ollamar} para descargar un modelo de lenguaje local desde R.1\nSi no tienes un modelo instalado, usa este comando para descargar e instalar un modelo desde R:\nlibrary(ollamar) pull(\u0026#34;llama3.1:8b\u0026#34;) Es necesario tener la aplicación Ollama abierta en tu computadora, dado que ésta aplicación es la que le entrega el modelo de lenguaje a R.\nCrear asistentes de IA locales El primer paso para crear los asistentes de programación es instalar el paquete [{pal}]:\n# install.packages(\u0026#34;pak\u0026#34;) pak::pak(\u0026#34;simonpcouch/pal\u0026#34;) Una vez que tengamos nuestro modelo LLM instalado, debemos indicarle a {pal} que queremos usarlo. Si tienes una suscripción a un modelo de lenguaje en la nube como Claude o ChatGPT, puedes configurarlo en este paso. Como tenemos un modelo local, le indicamos que usaremos Ollama y el nombre de nuestro modelo instalado:\noptions( .pal_fn = \u0026#34;chat_ollama\u0026#34;, .pal_args = list(model = \u0026#34;llama3.1:8b\u0026#34;) ) Ahora viene lo entretenido. Los asistentes tienen un rol, una interfaz y un contenido. Para crear nuestro asistente, en la función prompt_new() tenemos que darle un rol (que sería como el nombre de asistente), y especificar la interfaz; es decir, cómo queremos que intervenga el código que le entreguemos. Esto puede ser agregando su intervención antes del código seleccionado (prefix), después del código seleccionado (suffix), o reemplazando el código seleccionado por su aporte (replace).\nlibrary(pal) prompt_new(role = \u0026#34;nombre_de_asistente\u0026#34;, interface = \u0026#34;prefix\u0026#34;) Cuando ejecutas esta función se creará tu asistente y se abrirá un nuevo archivo .md, que contiene las instrucciones que usará tu asistente para ayudarte. Éste será el contenido de tu asistente. El archivo contiene unas instrucciones por defecto que puedes modificar, o puedes escribir tus propias instrucciones. Lo importante es que estas instrucciones sean claras, breves, y que ojalá incluyan un ejemplo de qué es lo que esperas como respuesta.\nLuego de modificar este archivo, tienes que ejecutar la función directory_load() para que {pal} actualice tus asistentes. Para volver a editar tu asistente puedes usar la función prompt_edit(\u0026quot;nombre_de_tu_asistente\u0026quot;), pero luego de editarlo debes volver a ejecutar directory_load().\nCrear un asistente de explicación de código de R Primero voy a crear un asistente que me ayude a explicar código de R. La utilidad de esta asistente para mí sería que me ayude a agregar comentarios a los scripts que entrego a mis estudiantes de R, de manera que les sea más fácil entender lo que hace cada parte de los scripts.\nDefino el nombre de mi asistente y su interacción con el código seleccionado:\nprompt_new(role = \u0026#34;explicar\u0026#34;, interface = \u0026#34;prefix\u0026#34;) Luego se abre el archivo para definir el contenido del asistente. Luego de un par de pruebas, ingresé las siguientes instrucciones o prompts:\nEres un asistente experto en ciencia de datos, diseñado para ayudar a usuarios de R a entender lo que hace un código de R. El código que debes explicar proviene principalmente de paquetes de R del Tidyverse, como dplyr, tidyr, stringr y otros. Tu tarea es explicar el código de forma breve y clara, explicando lo que hace cada función, pero sin extenderte demasiado. Responde sólo en español y en minúsculas, anteponiendo un signo # a tu respuesta, para que tu respuesta sea un párrafo de comentario de R. Inserta un salto de línea cada 64 caracteres. Responde sólo con la explicación del código, sin saltos de línea al rededor de la respuesta. Por ejemplo, si recibes el siguiente código de R: iris |\u0026gt; group_by(Species) |\u0026gt; summarize(n = mean(Sepal.Length)) |\u0026gt; arrange(desc(n)) Responde con una explicación breve, como la siguiente: # agrupa los datos del dataframe iris según la variable Species usando la función group_by(), luego calcula # el promedio de la variable Sepal.Length usando la función mean() dentro de summarize(), y # finalmente ordena las filas resultantes de mayor menor usando la función arrange() y desc() Cómo puedes ver, se trata de unas pocas instrucciones breves que definen claramente lo que espero de la asistente, junto a un ejemplo muy esquemático del resultado esperado.\nEl último paso para integrar a los asistentes a tu flujo de trabajo es especificar un atajo de teclado para invocar a tu asistente. En RStudio, si entras al menú Tools y eliges la opción Modify Keyboard Shortcuts, puede buscar Pal para especificar un atajo de teclado. En mi caso usé comando + shift + P. Si no quieres un atajo de teclado, en el menú Addins de RStudio puedes invocar Pal.\nEn el siguiente video vemos la funcionalidad en acción:\nSelecciona el código que necesites Presiona el atajo de teclado Elige el asistente que quieras usar y presiona Enter Espera a que el modelo de lenguaje inserte el texto Vemos cómo el asistente escribe una explicación del código que le entregué 🥳\nA continuación (¡y para que vean que funciona bien!) les muestro tres ejemplos donde seleccioné código de R y usé el asistente para que escriba una explicación del código:\nEjemplo 1:\n# lee una hoja de excel llamada \u0026#34;Datos Histograma\u0026#34; desde la ruta indicada usando read_excel() de readxl, # lo convierte a minúsculas con clean_names(), crea un nuevo campo \u0026#34;tmax\u0026#34; que reemplaza los valores nulos # por 17.8 y finalmente selecciona solo las columnas que no son tmax, eliminando tmedia datos_1 \u0026lt;- readxl::read_excel(\u0026#34;Datos Histograma.xlsx\u0026#34;) |\u0026gt; janitor::clean_names() |\u0026gt; mutate(tmax = replace_na(tmax, 17.8)) |\u0026gt; select(-tmedia) Ejemplo 2:\n# crea un nuevo dataframe llamado kms_año con variables año y kms a través de tribble(), luego # crea un modelo lineal usando la función lm() que relacionará kms con año, finalmente guarda las predicciones # del modelo en una nueva columna del mismo dataframe llamada pred1 utilizando predict() # el modelo busca la relación entre los años y el kilometraje kms_año \u0026lt;- tribble(~año, ~kms, 2021, 530, 2022, 930, 2023, 1260) # crear modelo lineal model \u0026lt;- lm(kms ~ año, data = kms_año) # crear predicción kms_año$pred1 \u0026lt;- predict(model) Ejemplo 3:\n# selecciona las columnas titulo e id del dataframe metadatos usando la función select() , # luego filtra los datos para incluir solo aquellas filas en las que se encuentre la cadena \u0026#34;microemprend\u0026#34; # en la variable titulo según la detección de expresión regular definida por str_detect(), # repite el proceso de filtro pero buscando ahora esta vez la cadena \u0026#34;sexo\u0026#34;, # por último extrae la columna id del dataframe resultante usando la función pull() metadatos |\u0026gt; select(titulo, id) |\u0026gt; filter(str_detect(titulo, \u0026#34;microemprend\u0026#34;)) |\u0026gt; filter(str_detect(titulo, \u0026#34;sexo\u0026#34;)) pull(id) Cómo se ven los ejemplos, el asistente entrega explicaciones bastante buenas, que con mínimos retoques resultarían útiles para orientar a usuarios principiantes de R. También podría servir para otros usuarios de R que reciban código y no lo entiendan.\nSi quieres instalar este asistente en tu equipo, ejecuta el siguiente código en tu consola de R:\npal::prompt_new(\u0026#34;explicar\u0026#34;, \u0026#34;prefix\u0026#34;, contents = \u0026#34;https://gist.github.com/bastianolea/b190b3be7477c1c48a5b06cbe7e8d441\u0026#34;) Crear un asistente de programación en {dplyr} El segundo asistente de programación que voy a crear va a ser uno que traduzca una descripción de código escrita en lenguaje natural a código de {dplyr}. Es decir, un asistente que haga lo contrario al asistente anterior: le voy a dar una explicación de un código, y el asistente va a generar el código necesario.\nPara poner a prueba la utilidad de esta herramienta y su facilidad de uso, la instrucción que le voy a dar al asistente para que funcione va a ser simplemente la misma instrucción que le di a la asistente de explicación de código, pero al revés; es decir, le voy a pedir que sea un asistente que programe en R con {dplyr}, y el ejemplo que le voy a dar de su funcionamiento va a ser el mismo ejemplo que le di al asistente anterior, pero primero la explicación del código, y después el código.\nLa instrucción quedaría así:\nEres un asistente experto en ciencia de datos, diseñado para ayudar a programar en R usando principalmente el paquete dplyr. El código que debes escribir proviene principalmente de paquetes de R del Tidyverse, como dplyr, tidyr, stringr y otros. Tu tarea es convertir la instrucción escrita en código de R usando dplyr. Responde sólo con el código de dplyr necesario para cumplir con la instrucción, sin comentarios ni explicaciones. Por ejemplo, si recibes la siguiente instrucción: agrupa los datos del dataframe iris según la variable Species, luego calcula el promedio de la variable Sepal.Length, y finalmente ordena las filas resultantes de mayor menor Responde con el siguiente código de R y dplyr: iris |\u0026gt; group_by(Species) |\u0026gt; summarize(n = mean(Sepal.Length)) |\u0026gt; arrange(desc(n)) El siguiente video retrata el funcionamiento de este nuevo asistente, y vemos que sus resultados son correctos y útiles:\nCómo podemos ver, el asistente escribe rápidamente código de {dplyr} a partir de la descripción que le dimos. Esto podría ser muy útil para usuarios principiantes que tengan una idea de lo que quieren hacer pero no sepan exactamente cómo hacerlo, o para usuarios avanzados que están cansados de escribir y quieren que una pobre máquina trabaje por ellos.\nSi quieres instalar este asistente en tu equipo, ejecuta el siguiente código en tu consola de R:\npal::prompt_new(\u0026#34;dplyr\u0026#34;, \u0026#34;suffix\u0026#34;, contents = \u0026#34;https://gist.github.com/bastianolea/a0be6c6b4471cf8e69bc1b7f30d8101a\u0026#34;) Éstos son sólo dos usos básicos que se le podría dar a este paquete para ayudarnos a desarrollar en R. Se podrían crear muchos más, como asistentes que expliquen los resultados de técnicas estadísticas, asistentes que produzcan visualizaciones de datos, y muchas otras aplicaciones de modelos de lenguaje a la programación.\nSi se te ocurren más ideas o ya probaste este paquete, ¡déjame un comentario!\nSi tu computador no es muy poderoso, te recomiendo instalar llama3.2:1b (más liviano) o llama3.2:3b (un poco mejor), pero si tienes suficiente memoria y tarjeta de video, puedes instalar llama3.1:8b.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-12-10T00:00:00Z","excerpt":"El paquete {pal} te permite crear asistentes para programar en R, potenciados por modelos de lenguaje (LLM). La utilidad de estos asistentes es que pueden ayudarte a realizar tareas rápidamente a partir de tu código de R, o incluso a partir de un texto que describa lo que quieres hacer. En este post te enseño a crear dos asistentes para tareas que realizo frecuentemente: describir lo que hace un código de R, y traducir una instrucción a código de {dplyr}","href":"https://bastianoleah.netlify.app/blog/pal_asistentes_llm/","tags":"consejos ; inteligencia artificial","title":"Crea tu propio asistente de programación en R con inteligencia artificial usando el paquete {pal}"},{"content":"No soy una persona muy cercana a la estadística, pero el día de hoy por primera vez se me ocurrió aplicar una regresión lineal para responder una pregunta de mi vida cotidiana.\nResulta que un compañero del ultraciclismo, el destacado ciclista Andrés Arias, preguntó en su Instagram si alguien podía predecir cuántos kilómetros va a recorrer este 2024 durante el desafío Rapha Festive 500, un desafío del que participamos muchos ciclistas, que consiste en recorrer 500 km en bicicleta entre el 24 y el 31 de diciembre1.\nLa pregunta la hizo Andrés luego de comentar cuánto había recorrido para el desafío en los años anteriores: 530 kilómetros en 2021, 930 kms en 2022, y 1.260 en 2023. Naturalmente noté que existía una cierta progresión lineal entre estas cifras, así que me levanté del sillón y abrí R para hacer una prueba 🤔\nLo primero fue anotar estas cifras en una tabla de datos:\nlibrary(dplyr) kms_año \u0026lt;- tribble(~año, ~kms, 2021, 530, 2022, 930, 2023, 1260) Luego visualizar los datos:\nlibrary(ggplot2) grafico_1 \u0026lt;- kms_año |\u0026gt; ggplot() + aes(x = año, y = kms) + geom_line(linewidth = 1, alpha = .4) + geom_point(size = 4, alpha = .7) + scale_x_continuous(breaks = kms_año$año) + theme_classic() + theme(panel.grid.major.x = element_line()) grafico_1 Efectivamente, la cantidad de kilómetros recorridos para este desafío en cada año ha aumentado de una manera casi lineal.\nMi idea fue crear un modelo de regresión lineal para ajustar estos datos a una recta que, al extenderla, nos permita predecir cuántos kilómetros serían recorridos en los siguientes años, si la tendencia se mantiene.\nAjustamos un modelo de regresión lineal, por medio del cual queremos predecir los kilómetros a partir de los años, dado que intuimos que un aumento en los años conlleva también un aumento en los kilómetros.\n# crear modelo lineal modelo \u0026lt;- with(kms_año, lm(kms ~ año)) # crear predicción prediccion \u0026lt;- predict(modelo) prediccion 1 2 3 541.6667 906.6667 1271.6667 # visualizar modelo grafico_1 + geom_line(aes(y = prediccion), color = \u0026#34;purple3\u0026#34;, linewidth = 1.4, linetype = \u0026#34;dashed\u0026#34;, alpha = .4) + geom_point(aes(y = prediccion), color = \u0026#34;purple3\u0026#34;, size = 4, alpha = .7) + theme(panel.grid.major.x = element_line()) La primera predicción que obtenemos es una predicción de los datos que ya conocemos, pero a partir de la recta ajusta por el modelo. Es decir, son los valores que corresponden a cada punto en el tiempo de acuerdo a la línea de regresión proyectada. Estos valores se asemejan mucho a los valores reales, pero al ser un modelo, no son exactamente iguales, ya que corresponden a una simplificación de la realidad.\nEn términos simples, un modelo de regresión lineal crea o ajusta una recta que busca cruzar todos los puntos de los datos de la manera más cercana a ellos posible. Por lo tanto, el gráfico anterior compara los datos reales con los datos representados por el modelo, que en el fondo son resumidos por una sola recta.\nA partir de esta recta, podemos predecir los valores que corresponderían a observaciones futuras (en este caso, los kilómetros recorridos en los años siguientes) asumiendo que los valores futuros seguirán ubicándose dentro de esta misma recta que predijo bastante bien los valores reales.\nLo que haremos será crear nuevos datos, en este caso nuevos años, que no estén contemplados dentro de los datos originales, que llegaban solamente hasta 2023.\n# crear nuevas observaciones para predecir predecir \u0026lt;- tibble(año = 2021:2026) Luego usamos el modelo para predecir los valores que tendrían estos nuevos datos (al extender a recta hacia los nuevos años).\n# predecir las nuevas observaciones en base al modelo ajustado prediccion \u0026lt;- predict(modelo, newdata = predecir) prediccion 1 2 3 4 5 6 541.6667 906.6667 1271.6667 1636.6667 2001.6667 2366.6667 Finalmente reunimos estos resultados en una nueva tabla de datos que contiene los años y los kilómetros predichos.\npredicho \u0026lt;- bind_cols(predecir, kms_pred = prediccion) predicho # A tibble: 6 × 2 año kms_pred \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; 1 2021 542. 2 2022 907. 3 2023 1272. 4 2024 1637. 5 2025 2002. 6 2026 2367. Teniendo estas predicciones, el último paso es unir las predicciones con los datos reales, y ponernos a analizar los resultados:\n# unir datos predichos (que tienen más años) con los datos reales kms_año_pred \u0026lt;- left_join(predicho, kms_año, by = join_by(año)) kms_año_pred # A tibble: 6 × 3 año kms_pred kms \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 2021 542. 530 2 2022 907. 930 3 2023 1272. 1260 4 2024 1637. NA 5 2025 2002. NA 6 2026 2367. NA Para visualizar los resultados, transformamos los datos a formato largo, para que en vez de tener dos columnas (los kilómetros reales y predichos) tengamos una sola columna con todos los valores, y una segunda columna que distinga los valores entre kilómetros reales y kilómetros predichos. Además, creamos una columna con los datos formateados de una manera más legible.\nlibrary(tidyr) kms_año_pred_2 \u0026lt;- kms_año_pred |\u0026gt; # transformar a largo o a tidy pivot_longer(c(kms, kms_pred), values_to = \u0026#34;valor\u0026#34;, names_to = \u0026#34;tipo\u0026#34;) |\u0026gt; # recodificar categorías mutate(tipo = recode(tipo, \u0026#34;kms\u0026#34; = \u0026#34;kms reales\u0026#34;, \u0026#34;kms_pred\u0026#34; = \u0026#34;predicción\u0026#34;)) |\u0026gt; arrange(tipo) |\u0026gt; # crear etiquetas mutate(etiqueta = round(valor, 0), etiqueta = format(etiqueta, big.mark = \u0026#34;.\u0026#34;, trim = TRUE), etiqueta = paste(etiqueta, \u0026#34;kms\u0026#34;)) kms_año_pred_2 # A tibble: 12 × 4 año tipo valor etiqueta \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 2021 kms reales 530 530 kms 2 2022 kms reales 930 930 kms 3 2023 kms reales 1260 1.260 kms 4 2024 kms reales NA NA kms 5 2025 kms reales NA NA kms 6 2026 kms reales NA NA kms 7 2021 predicción 542. 542 kms 8 2022 predicción 907. 907 kms 9 2023 predicción 1272. 1.272 kms 10 2024 predicción 1637. 1.637 kms 11 2025 predicción 2002. 2.002 kms 12 2026 predicción 2367. 2.367 kms Creamos un gráfico para analizar visualmente los resultados de nuestra predicción:\nkms_año_pred_2 |\u0026gt; ggplot() + aes(x = año, y = valor, color = tipo, linetype = tipo) + geom_line(linewidth = 1, alpha = 0.7) + geom_point(size = 3, alpha = 0.8) + geom_point(data = ~filter(.x, año == 2024), size = 3*4, alpha = 0.1, show.legend = F) + geom_text(data = ~filter(.x, tipo == \u0026#34;predicción\u0026#34;, año \u0026gt; 2023), aes(label = etiqueta), hjust = 0, nudge_x = 0.1, nudge_y = -50, size = 3, show.legend = F) + geom_text(data = ~filter(.x, tipo != \u0026#34;predicción\u0026#34;, año \u0026lt;= 2023), aes(label = etiqueta), hjust = 0, nudge_x = 0.1, nudge_y = -50, size = 3, show.legend = F) + theme_classic() + scale_color_manual(values = c(\u0026#34;kms reales\u0026#34; = \u0026#34;black\u0026#34;, \u0026#34;predicción\u0026#34; = \u0026#34;red2\u0026#34;)) + scale_y_continuous(labels = ~paste(format(.x, big.mark = \u0026#34;.\u0026#34;), \u0026#34;kms\u0026#34;)) + scale_x_continuous(expand = expansion(c(0.05, 0.1))) + guides(color = guide_legend(position = \u0026#34;inside\u0026#34;)) + labs(y = \u0026#34;kms\u0026#34;, color = NULL, linetype = NULL) + theme(panel.grid.major.x = element_line(), legend.position.inside = c(.9, .2)) Considerando los tres años de datos que tenemos de los recorridos en bicicleta de Andrés Arias durante el desafío Rapha Festive 500, podríamos predecir que este año Andrés va a recorrer 1.637 kilómetros entre el 24 y el 31 de diciembre, bajo el supuesto de que cada año Andrés ha aumentado sus kilómetros de forma lineal.\nAhora, esto choca un poco con la realidad, porque el hecho de algo aumente periódicamente no significa que en el siguiente periodo el aumento vaya a ser necesariamente el mismo que antes. Mucho menos cuando estamos hablando de datos que tienen que ver con procesos humanos. Pero de todas maneras fue un ejercicio entretenido 😊\nSí, es enfermizo, y sí, nuestras familias nos odian.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-12-03T00:00:00Z","excerpt":"Se me ocurrió aplicar estadísticas a una duda cotidiana: si se tiene un historial de kilómetros recorridos, ¿cómo podemos predecir los kilómetros que se recorrerán en el futuro? En este post aplicamos una regresión lineal en R para averiguarlo.","href":"https://bastianoleah.netlify.app/blog/regresion_ciclismo/","tags":"estadística ; ggplot2 ; curiosidades","title":"Aplicando una regresión lineal en la vida diaria: predecir kilómetros por recorrer en bicicleta"},{"content":"En esta página mantengo muestras de algunos proyectos que he realizado como analista de datos y desarrollador de R, y que consisten principalmente en aplicaciones web para la visualización y exploración de datos usando el lenguaje de programación R, la suite de paquetes para ciencia de datos Tidyverse, y el paquete de desarrollo de aplicaciones web Shiny.\nTodo lo descrito ha sido programado íntegramente por mi, como parte de distintos equipos de trabajo.\nVisualizador de resultados de encuesta de opinión sobre China Núcleo Milenio sobre los Impactos de China en América Latina\nDesarrollo de una aplicación web que visualiza los resultados de la Encuesta de opinión pública: ¿Qué piensan los chilenos sobre China?, para el Núcleo Milenio sobre los Impactos de China en América Latina (ICLAC).\nLa aplicación entrega más de 20 gráficos con distintos elementos de interactividad para presentar los resultados principales de la encuesta, y en la parte inferior de la página se encuentra un visualizador personalizado donde las y los usuarios pueden elegir cualquier pregunta de la encuesta y aplicarle filtros, desagregación, cruces de variables, visualización histórica, y más.\nEl sistema está diseñado para recibir nuevas encuestas que se agregan al visualizador existente sin problemas, mediante un proceso de limpieza y procesamiento de los datos automatizado.\nNota: en la imagen se ve el visualizador en dos columnas, dado que es muy largo\nPuedes acceder al visualizador de la encuesta aquí.\nÍndice de Saturación de Destinos Turísticos Pontificia Universidad Católica de Chile\nReporte automatizado en R y Quarto con gráficos ggplot2.\nProgramación de sistemas automatizados de obtención y procesamiento de datos, motor de cálculo estadístico para todas las comunas y destinos turísticos del país, visualizaciones interactivas de datos, reportes automatizados, coordinación de diseño y programación de plataforma web.\nProyecto FONDEF Índice de Saturación de Destinos Turísticos. Programación de sistemas automatizados de obtención y procesamiento de datos, motor de cálculo estadístico para todas las comunas y destinos turísticos del país, visualizaciones interactivas de datos, reportes automatizados, coordinación de diseño y programación de plataforma web.\nÍndice de movilidad Google Chile DataUC (Facultad de Matemáticas PUC), Pontificia Universidad Católica de Chile\nDesarrollo de una plataforma de visualización de datos que accedía a los datos periódicamente actualizados de movilidad de Google, para visualizar la información aplicada a Chile, por región y provincia. Además, relacionaba estos datos con los datos en tiempo real de coronavirus en Chile, junto a la información sobre el estado de cuarentena de las comunas correspondientes.\nAplicaciones web desarrolladas con R y Shiny con gráficos ggplot2\nRadar Legislativo Consultora Desplegar\nAnálisis de datos, desarrollo de índices e indicadores, automatización de web scraping, desarrollo de aplicaciones web de visualización de datos para la toma de decisiones empresariales. Estas aplicaciones web se alimentan por un conjunto de sistemas automatizados de obtención y procesamiento de datos, que además de entregar datos en tiempo real, permite enriquecerlos por medio de aplicaciones de uso interno.\nAplicación web desarrollada con R y Shiny, en base a web scraping en R usando rvest, procesamientos automatizados de datos, y algoritmos de predicción, y una API en plumber\nAplicación web desarrollada con R y Shiny con gráficos ggplot2\nÍndice de movilidad Transbank DataUC (Facultad de Matemáticas PUC), Pontificia Universidad Católica de Chile\nAplicaciones web desarrolladas con R y Shiny con gráficos ggplot2\nPlataforma de visualización de datos que producía un índice de movilidad a partir del procesamiento algorítmico de cientos de millones de observaciones correspondientes a las transacciones de tarjetas de débito y crédito Transbank. Los datos de transacciones eran preprocesados con algoritmos de anonimización de datos.\nGraficador Centro de Estudios Públicos (CEP) Centro de Estudios Públicos\nDesarrollo de un sistema de procesamiento de encuestas con muestreo complejo en R, además de una plataforma de visualización interactiva de datos históricos de las encuestas del Centro de Estudios Públicos (Graficador CEP), utilizando R para el procesamiento de datos, y R+Shiny para la aplicación web.\nAplicación web desarrollada con R y Shiny con gráficos ggplot2, alimentada por datos de encuestas procesados en R usando tidyverse y el paquete survey\nTarapacá intelligence DataUC (Facultad de Matemáticas PUC), Pontificia Universidad Católica de Chile Recopilación, limpieza y análisis de datos estadísticos de relevancia social.\nVisualizaciones de datos en R usando ggplot2, a partir de datos sociales procesados con R\nÍndice de Perfilamiento Social Consultora Desplegar\nAplicación web desarrollada con R y Shiny a partir del cálculo de un índice estadístico a nivel comunal\nVisualizador Covid 19 Chile DataUC (Facultad de Matemáticas PUC), Pontificia Universidad Católica de Chile\nAnálisis y visualización de datos utilizando el lenguaje de programación estadística R. Obtención automatizada de datos web (web scraping). Desarrollo de plataformas web interactivas de visualización de datos con R Shiny.\nAplicación web desarrollada con R y Shiny con gráficos ggplot2, además de una API en plumber\nDataEmprende DataUC (Facultad de Matemáticas PUC), Pontificia Universidad Católica de Chile\nAplicación web desarrollada con R y Shiny con gráficos ggplot2\n","date":"2024-11-26T00:00:00Z","excerpt":"Muestras de algunos proyectos que he realizado como analista de datos y desarrollador de R, y que consisten principalmente en aplicaciones web para la visualización y exploración de datos usando el lenguaje de programación R, la suite de paquetes para ciencia de datos Tidyverse, y el paquete de desarrollo de aplicaciones web Shiny.","href":"https://bastianoleah.netlify.app/blog/portafolio_trabajos_r/","tags":"apps ; gráficos ; shiny ; tablas ; mapas ; quarto","title":"Portafolio de trabajos previos en R"},{"content":" Índice Mapa de Chile por comunas Mapa regional de Chile Visualizar datos en el mapa Datos comunales Datos regionales Visualizar datos geográficamente es una herramienta de comunicación y análisis de datos muy potente. En este tutorial te explico cómo obtener mapas comunales y regionales de Chile en R, y cómo crear un gráficos que visualizan variables numéricas en las comunas y regiones del país. En pocos pasos puedes transformar tus datos territoriales en visualizaciones mucho más densas e informativas.\nPara crear mapas sencillos, donde una variable numérica se visualice en cada unidad territorial por medio de una escala de colores, solamente se necesitan dos cosas: la información geográfica que te permite visualizar el mapa en sí mismo, y los datos que podamos corresponder con las unidades territoriales del mapa.\nEn este breve tutorial veremos cómo obtener los mapas, como unir los datos al mapa, y cómo generar visualizaciones de estos mapas en R.\nlibrary(chilemapas) # mapas de chile library(dplyr) # manipulación de datos library(ggplot2) # visualización de datos library(scales) # utilidad para visualización de datos library(sf) # manipulación de datos geográficos Mapa de Chile por comunas El paquete {chilemapas}, desarrollado por Mauricio Vargas, ofrece una colección de mapas terrestres de Chile con topología simplificada. Simplemente llamando el objeto mapa_comunas obtenemos un dataframe con la información geográfica de cada comuna del país.\nmapa_comunas \u0026lt;- chilemapas::mapa_comunas mapa_comunas # A tibble: 345 × 4 codigo_comuna codigo_provincia codigo_region geometry \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;MULTIPOLYGON [°]\u0026gt; 1 01401 014 01 (((-68.86081 -21.28512, -68.921… 2 01403 014 01 (((-68.65113 -19.77188, -68.811… 3 01405 014 01 (((-68.65113 -19.77188, -68.635… 4 01402 014 01 (((-69.31789 -19.13651, -69.271… 5 01404 014 01 (((-69.39615 -19.06125, -69.400… 6 01107 011 01 (((-70.1095 -20.35131, -70.1243… 7 01101 011 01 (((-70.09894 -20.08504, -70.102… 8 02104 021 02 (((-68.98863 -25.38016, -68.987… 9 02101 021 02 (((-70.60654 -23.43054, -70.601… 10 02201 022 02 (((-67.94302 -22.38175, -67.955… # ℹ 335 more rows En este dataframe, la columna geometry representa los polígonos de cada comuna. Esta información ya es suficiente para visualizar el mapa con R usando {ggplot2} y {sf}, un paquete para trabajar con datos espaciales.\nPara visualizar el mapa, primero usamos sf::st_set_geometry() para asignar la geometría al dataframe. De este modo, le indicamos a R que estos datos deben graficarse en un mapa con determinadas coordenadas e información sobre proyección.\ngrafico_comunas \u0026lt;- mapa_comunas |\u0026gt; st_set_geometry(mapa_comunas$geometry) |\u0026gt; # asignar geometría ggplot() + # gráfico geom_sf() # capa geométrica grafico_comunas + theme_classic() Dado que Chile tiene islas muy alejadas del territorio continental, como Rapa Nui o Juan Fernández, el mapa queda con mucho espacio en blanco. Podemos recortar las coordenadas de la longitud del mapa (el eje x del gráfico) para que el mapa solamente abarque Chile continental:\ngrafico_comunas + coord_sf(xlim = c(-77, -65)) + theme_classic() Mapa regional de Chile Para generar un mapa de regiones, debemos crear las regiones. Esto no es complicado si pensamos que, en el fondo, las regiones son simplemente la unión de las comunas que las componen. El paquete {sf} simplifica muchísimo el trabajo con datos geográficos, en este caso la unión de distintos polígonos en uno solo.\nAgrupamos las filas del dataframe por la variable codigo_region, para que todas las comunas que pertenecen a la misma región estén agrupadas, y usamos summarize() junto a la función sf::st_union() para que los polígonos de las comunas dentro de cada región se combinen, obteniendo polígonos regionales:\nmapa_regiones \u0026lt;- mapa_comunas |\u0026gt; group_by(codigo_region) |\u0026gt; summarize(geometry = st_union(geometry)) # resumir los datos agrupados uniéndolos mapa_regiones # A tibble: 16 × 2 codigo_region geometry \u0026lt;chr\u0026gt; \u0026lt;GEOMETRY [°]\u0026gt; 1 01 POLYGON ((-69.93023 -21.4246, -69.92376 -21.42622, -69.91932 -… 2 02 MULTIPOLYGON (((-68.0676 -24.32856, -67.91698 -24.26902, -67.8… 3 03 MULTIPOLYGON (((-71.58497 -29.02456, -71.58844 -29.02838, -71.… 4 04 MULTIPOLYGON (((-70.54551 -31.30742, -70.53877 -31.30074, -70.… 5 05 MULTIPOLYGON (((-71.33832 -33.45237, -71.33763 -33.44836, -71.… 6 06 POLYGON ((-71.5477 -34.87458, -71.54211 -34.87581, -71.53566 -… 7 07 POLYGON ((-70.41724 -35.63022, -70.41108 -35.6302, -70.40146 -… 8 08 MULTIPOLYGON (((-73.53466 -36.97378, -73.53245 -36.97829, -73.… 9 09 MULTIPOLYGON (((-73.35306 -38.73343, -73.35396 -38.72799, -73.… 10 10 MULTIPOLYGON (((-73.1691 -41.87755, -73.16135 -41.87781, -73.1… 11 11 MULTIPOLYGON (((-75.41754 -48.73857, -75.43249 -48.74372, -75.… 12 12 MULTIPOLYGON (((-70.35563 -52.94478, -70.34688 -52.93971, -70.… 13 13 POLYGON ((-70.47405 -33.8624, -70.47327 -33.86269, -70.46068 -… 14 14 MULTIPOLYGON (((-73.39503 -39.88698, -73.39672 -39.89339, -73.… 15 15 POLYGON ((-69.07223 -19.02723, -69.06394 -19.02607, -69.04748 … 16 16 POLYGON ((-72.38553 -36.91169, -72.37685 -36.91617, -72.37034 … Obtenemos un dataframe con una fila por región, dado que las comunas fueron unidas en polígonos regionales. La columna geometry ahora contiene la unión de las comunas que hicimos con la función st_union(). Podemos visualizar este nuevo dataframe regional usando {ggplot2} y {sf}, igual que en el paso anterior:\ngrafico_regiones \u0026lt;- mapa_regiones |\u0026gt; st_set_geometry(mapa_regiones$geometry) |\u0026gt; # especificar la geometría del mapa ggplot() + # graficar geom_sf() + # capa geográfica coord_sf(xlim = c(-77, -65)) # recortar coordenadas grafico_regiones + theme_classic() Ahora que tenemos nuestros mapas por comunas y por regiones, la idea es poder utilizarlos para visualizar datos en ellos.\nVisualizar datos en el mapa Para visualizar los datos en un mapa, aplicaremos colores a los distintos polígonos de nuestro mapa, ya sean comunas o regiones. La intensidad del color, o su posición dentro de la escala de color elegida, representará el valor de la variable numérica que queremos visualizar.\nLa idea de fondo para entender la visualización de datos geográficos por polígonos es entender que los mapas que obtuvimos contienen en cada fila un polígono (la información geográfica para dibujar la comuna o región), y además, en cada fila contienen información que identifica al polígono correspondiente. En nuestro caso, la información de los polígonos corresponde al código único territorial de las comunas, y el código de regiones en el caso de las regiones. Éstos son códigos numéricos que identifican cada unidad territorial del país.\nLa idea es poder agregarle datos a nuestro mapa, de manera que las filas que representan unidades territoriales tengan también columnas con datos sobre dichas unidades territoriales.\nEntonces, deberíamos tener nuestro dataframe con el mapa, y otro dataframe donde tengamos los datos que queremos adjuntar al mapa. Si tenemos un mapa de comunas, tenemos que tener los datos por comuna que queremos agregarle al mapa.\nDatos comunales Obtener datos por web scraping Para visualizar un mapa de datos comunales, primero obtendremos datos comunales desde Wikipedia. Usamos el paquete para hacer un web scraping y obtener una tabla de datos de las comunas del país.\nlibrary(rvest) # dirección de wikipedia con tabla de comunas de Chile url \u0026lt;- \u0026#34;https://es.wikipedia.org/wiki/Anexo:Comunas_de_Chile\u0026#34; # obtener tabla con datos de comunas con web scraping tabla \u0026lt;- session(url) |\u0026gt; read_html() |\u0026gt; html_table(convert = FALSE) tabla[[1]] # A tibble: 346 × 12 CUT (Código Único Territori…¹ Nombre `` Provincia Región `Superficie(km²)` \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 15101 Arica \u0026quot;\u0026quot; Arica Arica… 4.799,4 2 15102 Camar… \u0026quot;\u0026quot; Arica Arica… 3.927 3 15201 Putre \u0026quot;\u0026quot; Parinaco… Arica… 5.902,5 4 15202 Gener… \u0026quot;\u0026quot; Parinaco… Arica… 2.244,4 5 01101 Iquiq… \u0026quot;\u0026quot; Iquique Tarap… 2.242,1 6 01107 Alto … \u0026quot;\u0026quot; Iquique Tarap… 572.9 7 01401 Pozo … \u0026quot;\u0026quot; Tamarugal Tarap… 13.765,8 8 01402 Camiña \u0026quot;\u0026quot; Tamarugal Tarap… 2.200,2 9 01403 Colch… \u0026quot;\u0026quot; Tamarugal Tarap… 4.015,6 10 01404 Huara \u0026quot;\u0026quot; Tamarugal Tarap… 10.474,6 # ℹ 336 more rows # ℹ abbreviated name: ¹​`CUT (Código Único Territorial)` # ℹ 6 more variables: Población2020 \u0026lt;chr\u0026gt;, `Densidad(hab./km²)` \u0026lt;chr\u0026gt;, # `IDH 2005` \u0026lt;chr\u0026gt;, `IDH 2005` \u0026lt;chr\u0026gt;, Latitud \u0026lt;chr\u0026gt;, Longitud \u0026lt;chr\u0026gt; Luego obtener los datos, realizamos una pequeña limpieza. Limpiamos los nombres de las variables, seleccionamos las variables que nos interesan, y luego las convertimos apropiadamente a valores numéricos, donde tenemos que eliminar los separadores de miles, y transformar los separadores de decimales a puntos.\nlibrary(janitor) library(stringr) # limpiar datos datos_comunas \u0026lt;- tabla[[1]] |\u0026gt; clean_names() |\u0026gt; # seleccionar y renombrar columnas select(codigo_comuna = cut_codigo_unico_territorial, nombre, region, superficie_km2, poblacion = poblacion2020) |\u0026gt; # eliminar espacios de la columna de población mutate(poblacion = str_remove_all(poblacion, \u0026#34; \u0026#34;), poblacion = as.numeric(poblacion)) |\u0026gt; # eliminar los separadores de miles mutate(superficie_km2 = str_remove_all(superficie_km2, \u0026#34;\\\\.\u0026#34;), # convertir comas a puntos superficie_km2 = str_replace(superficie_km2, \u0026#34;,\u0026#34;, \u0026#34;.\u0026#34;), superficie_km2 = as.numeric(superficie_km2)) datos_comunas # A tibble: 346 × 5 codigo_comuna nombre region superficie_km2 poblacion \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 15101 Arica Arica y Parinacota 4799. 247552 2 15102 Camarones Arica y Parinacota 3927 1233 3 15201 Putre Arica y Parinacota 5902. 2515 4 15202 General Lagos Arica y Parinacota 2244. 810 5 01101 Iquique Tarapacá 2242. 223463 6 01107 Alto Hospicio Tarapacá 5729 129999 7 01401 Pozo Almonte Tarapacá 13766. 17395 8 01402 Camiña Tarapacá 2200. 1375 9 01403 Colchane Tarapacá 4016. 1583 10 01404 Huara Tarapacá 10475. 3000 # ℹ 336 more rows Ahora que tenemos una tabla de datos que contiene la columna codigo_comuna con el código único territorial de las comunas, podemos unirla al mapa de comunas.\nAgregar datos a los mapas Esta operación de unir dos tablas de datos diferentes, pero que coinciden en una columna en común, se realiza con la función left_join(). El principio de left_join() es que tenemos dos tablas de datos, y ambas tablas de datos tienen una misma columna, que poseen los mismos valores (en nuestro caso, una columna con las comunas o los códigos únicos territoriales de las comunas). Entonces, ambas tablas se van a unir según la correspondencias de estas columnas en común.\nEjemplo:\ntabla_a \u0026lt;- tribble(~animal, ~favorito, \u0026#34;gato\u0026#34;, \u0026#34;pescado\u0026#34;, \u0026#34;mapache\u0026#34;, \u0026#34;basura\u0026#34;, \u0026#34;perro\u0026#34;, \u0026#34;carne\u0026#34;) tabla_b \u0026lt;- tribble(~animal, ~belleza, ~inteligencia, ~carisma, \u0026#34;gato\u0026#34;, 8, 6, 5, \u0026#34;perro\u0026#34;, 5, 2, 8, \u0026#34;mapache\u0026#34;, 10, 7, 2) left_join(tabla_a, tabla_b, by = \u0026#34;animal\u0026#34;) # A tibble: 3 × 5 animal favorito belleza inteligencia carisma \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 gato pescado 8 6 5 2 mapache basura 10 7 2 3 perro carne 5 2 8 En el ejemplo, tenemos dos tablas, donde las dos tienen una misma columna con los mismos datos, y otras columnas con datos distintos. Usando left_join() podemos unir ambas tablas de datos a partir de la columna que tienen en común. Como resultado obtenemos una nueva tabla que tiene todas las columnas.\nProcedemos a unir los datos con el mapa usando left_join(), especificando en el argumento by que la unión sea a partir de la columna codigo_comuna.\nmapa_comunas_2 \u0026lt;- mapa_comunas |\u0026gt; # adjuntar datos al mapa, coincidiendo por columna de código de comunas left_join(datos_comunas, by = join_by(codigo_comuna)) |\u0026gt; relocate(geometry, .after = 0) # tirar geometría al final mapa_comunas_2 # A tibble: 345 × 8 codigo_comuna codigo_provincia codigo_region nombre region superficie_km2 \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 01401 014 01 Pozo Almo… Tarap… 13766. 2 01403 014 01 Colchane Tarap… 4016. 3 01405 014 01 Pica Tarap… 8934. 4 01402 014 01 Camiña Tarap… 2200. 5 01404 014 01 Huara Tarap… 10475. 6 01107 011 01 Alto Hosp… Tarap… 5729 7 01101 011 01 Iquique Tarap… 2242. 8 02104 021 02 Taltal Antof… 20405. 9 02101 021 02 Antofagas… Antof… 30718. 10 02201 022 02 Calama Antof… 15597. # ℹ 335 more rows # ℹ 2 more variables: poblacion \u0026lt;dbl\u0026gt;, geometry \u0026lt;MULTIPOLYGON [°]\u0026gt; Como resultado, tenemos un nuevo dataframe que además de tener los datos geográficos de las comunas, también tiene nuevas columnas con datos que podemos visualizar.\nVisualizar datos comunales Para visualizar datos comunales en un mapa, especificamos la columna de geometría que contiene la información geográfica, y creamos un gráfico de {ggplot2}. En este gráfico, especificamos que el relleno de los polígonos (fill) se haga a partir de una de las variables numéricas.\nUsamos la función geom_sf() para agregar una capa de geometría a nuestro gráfico que contenga los polígonos territoriales de las comunas. Luego, definimos un tema, una paleta de colores, y corregimos la escala del eje horizontal.\nmapa_comunas_2 |\u0026gt; st_set_geometry(mapa_comunas_2$geometry) |\u0026gt; # asignar geometría ggplot() + # gráfico aes(fill = poblacion) + geom_sf(linewidth = 0) + # capa geométrica theme_classic() + scale_fill_distiller(type = \u0026#34;seq\u0026#34;, palette = 12, labels = label_comma(big.mark = \u0026#34;.\u0026#34;)) + # colores scale_x_continuous(breaks = seq(-76, -65, length.out = 3) |\u0026gt; floor()) + # escala x coord_sf(xlim = c(-77, -65)) + # recortar coordenadas theme(legend.key.width = unit(3, \u0026#34;mm\u0026#34;)) Como resultado obtenemos un mapa coroplético, o mapa de coropletas, que es un mapa donde cada región o polígono está relleno de un color que representa un valor en una escala de una variable.\nAcá hacemos otro mapa, usando el mismo código y simplemente cambiando la variable de relleno de los polígonos territoriales:\nmapa_comunas_2 |\u0026gt; st_set_geometry(mapa_comunas_2$geometry) |\u0026gt; ggplot() + aes(fill = superficie_km2) + # variable de relleno geom_sf(linewidth = 0) + theme_classic() + scale_fill_distiller(type = \u0026#34;seq\u0026#34;, palette = 11, labels = label_comma(big.mark = \u0026#34;.\u0026#34;)) + scale_x_continuous(breaks = seq(-76, -65, length.out = 3) |\u0026gt; floor()) + coord_sf(xlim = c(-77, -65)) + theme(legend.key.width = unit(3, \u0026#34;mm\u0026#34;)) Mapa de una sola región, por comunas Si queremos visualizar una sola región del país, subdividida por comunas, tan sencillo como agregar un filtro a los datos que filtre las filas que pertenecen a la región que nos interesa. En este caso, vamos a visualizar la población por comunas de la región del Libertador General Bernardo O\u0026rsquo;Higgins:\n# filtrar datos mapa_comunas_filtro \u0026lt;- mapa_comunas_2 |\u0026gt; filter(codigo_region == \u0026#34;06\u0026#34;) # mapa mapa_comunas_filtro |\u0026gt; st_set_geometry(mapa_comunas_filtro$geometry) |\u0026gt; ggplot() + aes(fill = poblacion) + geom_sf(linewidth = 0.12, color = \u0026#34;white\u0026#34;) + geom_sf_text(aes(label = comma(poblacion, big.mark = \u0026#34;.\u0026#34;)), size = 2, color = \u0026#34;white\u0026#34;, check_overlap = T) + theme_classic() + scale_fill_distiller(type = \u0026#34;seq\u0026#34;, palette = 12, labels = label_comma(big.mark = \u0026#34;.\u0026#34;)) + theme(legend.key.width = unit(3, \u0026#34;mm\u0026#34;)) + theme(axis.title = element_blank()) En este caso, agregamos también la función geom_sf_text() para agregar una capa nuestro gráfico que contiene las cifras para cada comuna. Hay que tener en consideración que poner números o textos sobre mapas suele ser complejo, porque el mapa ya es denso visualmente, y agregarle texto puede hacer que se vuelva ilegible. Hay que tener especial cuidado en resolver situaciones como textos que pasan por encima de bordes en el mapa, que se ubican incorrectamente dentro de los polígonos, o que se sobreponen uno sobre otros debido a que los polígonos se ven muy pequeños dentro del mapa.\nDatos regionales Obtener datos por web scraping Para hacer el ejemplo de visualizar datos a nivel regional, nuevamente obtendremos datos a esta escala usando web scraping. Obtendremos una tabla del Producto Interno Bruto regional desde el sitio web de el Banco Central de Chile.\nlibrary(rvest) # dirección del sitio del banco central url \u0026lt;- \u0026#34;https://si3.bcentral.cl/Siete/ES/Siete/Cuadro/CAP_CCNN/MN_CCNN76/CCNN2018_PIB_REGIONAL_N/637899740344107786\u0026#34; # obtener tabla con datos de comunas con web scraping tabla_pib \u0026lt;- session(url) |\u0026gt; read_html() |\u0026gt; html_table(convert = FALSE) Limpiamos los datos seleccionando las columnas que necesitamos, transformando la columna a tipo numérico, y filtrando las filas para quedar solamente con las que corresponden a las regiones de Chile:\ndatos_regiones \u0026lt;- tabla_pib [[1]] |\u0026gt; janitor::clean_names() |\u0026gt; select(region = serie, pib = x2023) |\u0026gt; mutate(pib = str_remove_all(pib, \u0026#34;\\\\.\u0026#34;), pib = as.numeric(pib)) |\u0026gt; filter(str_detect(region, \u0026#34;Región\u0026#34;)) datos_regiones # A tibble: 16 × 2 region pib \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 Región de Arica y Parinacota 2169 2 Región de Tarapacá 7892 3 Región de Antofagasta 31290 4 Región de Atacama 6004 5 Región de Coquimbo 9174 6 Región de Valparaíso 20275 7 Región Metropolitana de Santiago 109143 8 Región del Libertador General Bernardo OHiggins 11910 9 Región del Maule 10348 10 Región de Ñuble 4106 11 Región del Biobío 16731 12 Región de La Araucanía 7743 13 Región de Los Ríos 3561 14 Región de Los Lagos 9432 15 Región de Aysén del General Carlos Ibáñez del Campo 1573 16 Región de Magallanes y de la Antártica Chilena 2490 Crear columna de códigos regionales Los datos que obtuvimos no contienen una variable con el código único territorial de las regiones, dado que es un dato que está en desuso desde la promulgación de una ley en 2018 que prohíbe el uso de los números en comunicaciones públicas. Sin embargo, en muchas base de datos oficiales se sigue usando la numeración de las regiones, principalmente para evitar problemas de diferencias en los nombres de las regiones, por ejemplo, si contienen o no las palabras Región de o Región del para cada región, si contienen tildes o no, si contienen abreviaciones o no, si contienen símbolos como eñes o apóstrofes, etc. Todos estos problemas son resueltos por el uso de identificador numérico de las regiones.\nComo el mapa que obtuvimos con el paquete {chilemapas} utiliza el código regional, en el dato frente de nuestros datos crearemos una columna con los mismos códigos regionales, asignados a cada región a partir de la coincidencia con los textos. Por ejemplo, si el texto del nombre de la región contiene la palabra Arica, se le asigna el código 15. Esta forma de asignar los códigos regionales puede ser inexacta, pero es muy sencillo confirmar si es que la asignación de códigos funciona correctamente, y también siempre es posible utilizar expresiones regulares en {stringr} para hacer coincidencias más flexibles, como por ejemplo, coincidir una palabra con un código sin importar si la palabra tiene o no tilde, o está mal escrita.\ndatos_regiones_2 \u0026lt;- datos_regiones |\u0026gt; mutate(codigo_region = case_when( str_detect(region, \u0026#34;Arica\u0026#34;) ~ 15, str_detect(region, \u0026#34;Tarapacá\u0026#34;) ~ 1, str_detect(region, \u0026#34;Antofagasta\u0026#34;) ~ 2, str_detect(region, \u0026#34;Atacama\u0026#34;) ~ 3, str_detect(region, \u0026#34;Coquimbo\u0026#34;) ~ 4, str_detect(region, \u0026#34;Valparaíso\u0026#34;) ~ 5, str_detect(region, \u0026#34;Metropolitana\u0026#34;) ~ 13, str_detect(region, \u0026#34;Libertador General\u0026#34;) ~ 6, str_detect(region, \u0026#34;Maule\u0026#34;) ~ 7, str_detect(region, \u0026#34;Ñuble\u0026#34;) ~ 16, str_detect(region, \u0026#34;Biobío\u0026#34;) ~ 8, str_detect(region, \u0026#34;Araucanía\u0026#34;) ~ 9, str_detect(region, \u0026#34;Los Ríos\u0026#34;) ~ 14, str_detect(region, \u0026#34;Los Lagos\u0026#34;) ~ 10, str_detect(region, \u0026#34;Aysén\u0026#34;) ~ 11, str_detect(region, \u0026#34;Magallanes\u0026#34;) ~ 12 )) |\u0026gt; rename(nombre_region = region) Unir datos con mapa Luego de crear la variable de códigos regionales, podemos hacer la unión entre ambas tablas de datos, mapas y datos regionales, usando left_join():\nmapa_regiones_2 \u0026lt;- mapa_regiones |\u0026gt; mutate(codigo_region = as.numeric(codigo_region)) |\u0026gt; left_join(datos_regiones_2, by = join_by(codigo_region)) |\u0026gt; relocate(geometry, .after = 0) # tirar columna al final mapa_regiones_2 # A tibble: 16 × 4 codigo_region nombre_region pib geometry \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;GEOMETRY [°]\u0026gt; 1 1 Región de Tarapacá 7892 POLYGON ((-69.93023 -21.… 2 2 Región de Antofagasta 31290 MULTIPOLYGON (((-68.0676… 3 3 Región de Atacama 6004 MULTIPOLYGON (((-71.5849… 4 4 Región de Coquimbo 9174 MULTIPOLYGON (((-70.5455… 5 5 Región de Valparaíso 20275 MULTIPOLYGON (((-71.3383… 6 6 Región del Libertador General… 11910 POLYGON ((-71.5477 -34.8… 7 7 Región del Maule 10348 POLYGON ((-70.41724 -35.… 8 8 Región del Biobío 16731 MULTIPOLYGON (((-73.5346… 9 9 Región de La Araucanía 7743 MULTIPOLYGON (((-73.3530… 10 10 Región de Los Lagos 9432 MULTIPOLYGON (((-73.1691… 11 11 Región de Aysén del General C… 1573 MULTIPOLYGON (((-75.4175… 12 12 Región de Magallanes y de la … 2490 MULTIPOLYGON (((-70.3556… 13 13 Región Metropolitana de Santi… 109143 POLYGON ((-70.47405 -33.… 14 14 Región de Los Ríos 3561 MULTIPOLYGON (((-73.3950… 15 15 Región de Arica y Parinacota 2169 POLYGON ((-69.07223 -19.… 16 16 Región de Ñuble 4106 POLYGON ((-72.38553 -36.… Visualizar datos regionales Finalmente, visualizamos los datos en un mapa regional de la misma forma que lo hicimos con los mapas comunales. Esta vez, el mapa y los datos vienen en una fila por región, con la variable geometry que conteniendo la geometría del polígono regional. Por tanto, el código es exactamente el mismo, y solamente cambia el dataframe que usamos para generar el gráfico:\nmapa_regiones_2 |\u0026gt; st_set_geometry(mapa_regiones_2$geometry) |\u0026gt; # asignar geometría ggplot() + # gráfico aes(fill = pib) + geom_sf(linewidth = 0.12, color = \u0026#34;white\u0026#34;) + # capa geométrica theme_classic() + scale_fill_distiller(type = \u0026#34;seq\u0026#34;, palette = 18, labels = label_comma(big.mark = \u0026#34;.\u0026#34;)) + scale_x_continuous(breaks = seq(-76, -65, length.out = 3) |\u0026gt; floor()) + coord_sf(expand = F, xlim = c(-77, -65)) + # recortar coordenadas theme(legend.key.width = unit(3, \u0026#34;mm\u0026#34;)) Visualizar datos geográficamente es una herramienta de comunicación y análisis de datos muy potente. Personalmente, encuentro que el potencial de la visualización de datos en mapas radica mucho más allá de simplemente mapear una variable a un territorio, sino a incentivar que el público analice el mapa en relación a todo el conjunto de conocimientos que tenemos sobre del espacio que habitamos, sus características sociales, y las desigualdades distribuidas en el territorio.\nSi este tutorial te sirvió, por favor considera hacerme una pequeña donación para poder tomarme un cafecito mientras escribo el siguiente tutorial 🥺\n","date":"2024-11-25T00:00:00Z","excerpt":"Visualizar datos geográficamente es una herramienta de comunicación y análisis de datos muy potente. En este tutorial te explico cómo obtener mapas comunales y regionales de Chile en R, y cómo crear un gráficos que visualizan variables numéricas en las comunas y regiones del país. En pocos pasos puedes transformar tus datos territoriales en visualizaciones mucho más densas e informativas.","href":"https://bastianoleah.netlify.app/blog/tutorial_mapa_chile/","tags":"mapas ; gráficos ; ciencias sociales ; Chile","title":"Crea un mapa de Chile y visualiza datos comunales y regionales con mapas en R"},{"content":" El paquete de R {gt} (llamado así por generar great tables) permite producir tablas atractivas con R para presentar tus datos. Sus cualidades principales son que produce resultados atractivos con muy pocas líneas de código, pero al mismo tiempo ofrece una alta capacidad de personalización, teniendo opciones para personalizar prácticamente todos los aspectos de la tabla.\nOtro beneficio de usar este paquete es que contiene funciones que hacen muy fácil darle el formato correcto a las variables numéricas, como porcentajes, números grandes, cifras en dinero, etc., Y además, ofrece funciones para darle estilos personalizados a las columnas o celdas de tu tabla de forma programática. Esto permite generar ciertas reglas para que las celdas cambien de colores dependiendo de su valor, ciertas cifras cambian su tipo de letra bajo determinadas circunstancias, y mucho más.\nEn este artículo te mostraré tres ejemplos de creación de distintas tablas basadas en datos reales:\nTabla de estimación de pobreza comunal, con estilo condicional de celdas y columnas con colorización en base a datos Tabla de resultados electorales, con colorización de celdas de variables categóricas en base a una segunda variable categórica Tabla de Producto Interno Bruto regional, con colorización de múltiples columnas numéricas de forma simultánea, y estilo de celdas condicional Tabla de estimación de pobreza por comunas El primer ejemplo de tabla se basará en los datos de estimaciones de pobreza comunal del año 2022, producidos por el Ministerio de Desarrollo Social de Chile.\nEl primer paso será obtener los datos oficiales. Por conveniencia, tengo la tabla con los datos originales pre-procesada en un repositorio sobre datos de pobreza en Chile, por lo que es posible importar los datos limpios en una sola línea de código:\npobreza \u0026lt;- readr::read_csv2(\u0026#34;https://raw.githubusercontent.com/bastianolea/pobreza_chile/refs/heads/main/pobreza_comunal/datos_procesados/pobreza_comunal.csv\u0026#34;) pobreza # A tibble: 345 × 8 codigo region nombre_comuna poblacion pobreza_n pobreza_p limite_inferior \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 1101 Tarapacá Iquique 229674 41967. 0.183 0.162 2 1107 Tarapacá Alto Hospicio 138527 45162. 0.326 0.295 3 1401 Tarapacá Pozo Almonte 18290 4563. 0.250 0.199 4 1402 Tarapacá Camiña 1380 308. 0.223 0.138 5 1403 Tarapacá Colchane 1575 473. 0.300 0.187 6 1404 Tarapacá Huara 3072 1185. 0.386 0.319 7 1405 Tarapacá Pica 6184 1040. 0.168 0.119 8 2101 Antofagas… Antofagasta 438942 73103. 0.167 0.141 9 2102 Antofagas… Mejillones 15502 3078. 0.199 0.127 10 2103 Antofagas… Sierra Gorda 1790 295. 0.165 0.0743 # ℹ 335 more rows # ℹ 1 more variable: limite_superior \u0026lt;dbl\u0026gt; Podemos ver que los datos corresponden a 345 filas con información de las comunas, su población, y las cifras de pobreza en números absolutos y porcentaje.\nPara el ejemplo, primero obtendremos 7 filas al azar y seleccionaremos solamente las columnas que nos interesan. Teniendo cualquier tabla de datos en R, si le agregamos la función gt() crearemos una tabla {gt} con las configuraciones por defecto:\nlibrary(dplyr) library(gt) set.seed(1917) tabla_pobreza_1 \u0026lt;- pobreza |\u0026gt; slice_sample(n = 7) |\u0026gt; select(region, contains(\u0026#34;comuna\u0026#34;), poblacion, starts_with(\u0026#34;pobreza\u0026#34;)) |\u0026gt; gt() Esta es la tabla básica que entrega {gt}, la cual viene con líneas horizontales elegantes y un espaciado de celdas agradable. Tiene una apariencia bastante mejor que haber pegado a los datos en una planilla de Excel 🤫\nFormato de variables numéricas El primer paso para mejorar esta tabla es darle un formato de texto correcto a las variables numéricas. Esto logramos con las funciones de {gt} que empiezan con fmt_ (por formato), de las cuales usaremos fmt_number() para remover decimales de los números enteros y aplicar separadores de miles, y fmt_percent() para especificar el número de decimales y usar la coma como separador de decimales.\ntabla_pobreza_2 \u0026lt;- tabla_pobreza_1 |\u0026gt; # dar formato a números enteros fmt_number(columns = c(poblacion, pobreza_n), decimals = 0, sep_mark = \u0026#34;.\u0026#34;) |\u0026gt; # dar formato a porcentajes fmt_percent(columns = pobreza_p, decimals = 1, drop_trailing_zeros = TRUE, dec_mark = \u0026#34;,\u0026#34;) Apariencia de columnas El segundo paso va a ser cambiar el formato del texto de algunas columnas. En este caso, queremos aplicar letras negrita a la columna nombre_comuna, Y vamos a alinear la columna region hacia la derecha, para que se pegue al nombre de las comunas. Esto se lleva a cabo con la función tab_style(), cuyo primer argumento es el estilo que vamos a darle a las celdas, y el segundo es las columnas o filas que vamos a afectar. Cómo vamos a cambiar el texto de las celdas, en el argumento style se usa la función cell_text(), que contiene todos los atributos que podemos cambiar de las letras de las celdas, como color, tamaño, estilo, peso, etc.\nLa alineación del texto también podríamos haberla hecho con la función tab_style(), especificando en cell_text() que queremos que el texto sea alienado a la derecha, pero {gt} nos facilita un poco ésto con la función cols_align().\ntabla_pobreza_3 \u0026lt;- tabla_pobreza_2 |\u0026gt; # cambiar estilo de texto de una columna tab_style(style = cell_text(weight = \u0026#34;bold\u0026#34;), locations = cells_body(nombre_comuna)) |\u0026gt; # alineación de texto de una columna cols_align(align = \u0026#34;right\u0026#34;, columns = region) Para terminar de cambiar las configuraciones básicas de nuestra tabla, tenemos que mejorar los nombres de las columnas y agregar información de contexto a la tabla. Reemplazaremos los nombres de las columnas por nombres correctamente redactados, usando la función cols_label(), a la cual hay que darle los nombres de las columnas con el texto que queremos que tenga cada uno. Luego, utilizaremos la función tab_header() para agregar título y subtítulo, y la función tab_source_note() para agregar un texto de fuente debajo de la tabla.\ntabla_pobreza_4 \u0026lt;- tabla_pobreza_3 |\u0026gt; # nombres de columnas cols_label(region = \u0026#34;Región\u0026#34;, nombre_comuna = \u0026#34;Comuna\u0026#34;, poblacion = \u0026#34;Población\u0026#34;, pobreza_n = \u0026#34;Cantidad\u0026#34;, pobreza_p = \u0026#34;Porcentaje\u0026#34;) |\u0026gt; # título y subtítulo tab_header(title = \u0026#34;Pobreza comunal\u0026#34;, subtitle = \u0026#34;Cantidad y porcentaje de personas en situación de pobreza\u0026#34;) |\u0026gt; # texto fuente tab_source_note(\u0026#34;Fuente: Estimaciones de pobreza comunal 2022,\u0026#34;) |\u0026gt; tab_source_note(\u0026#34;Ministerio de Desarrollo Social\u0026#34;) En este punto ya tenemos una tabla presentable, ordenada y elegante. Podemos guardar la tabla como una imagen con el comando gtsave(), incluirla en un reporte Quarto/RMarkdown o en una app Shiny.\nEstilo condicional de celdas Podemos seguir mejorando en la tabla agregando cambios condicionales en el estilo de celdas que cumplan con una condición puntual. Por ejemplo, queremos destacar celdas de la variable poobreza_n que superen cierto valor, debido a que nos parece relevante distinguir comunas donde exista una cierta cantidad de personas en condición de pobreza.\nPara agregar estilos condicionales a las celdas de una o varias columnas, usamos la misma función tab_style() que usamos para darle un estilo a las columnas completas, pero esta vez, en el argumento de location que usamos para especificar la columna que queremos modificar, especificamos tanto una columna como un criterio para las filas. De esta forma, se indica que queremos cambiar el estilo de una columna, pero sólo si sus filas cumplen una condición determinada:\ntabla_pobreza_5 \u0026lt;- tabla_pobreza_4 |\u0026gt; tab_style(style = cell_fill(color = \u0026#34;red\u0026#34;, alpha = 0.2), locations = cells_body(columns = pobreza_n, rows = pobreza_n \u0026gt; 6000) ) Colorear columnas según su valor Podemos hacer que una columna de una variable numérica exprese su valor a través de una escala de colores. Así resulta mucho más fácil guiar la atención a las diferencias en los valores de las observaciones, ya que podemos identificar valores bajos y altos de la misma variable siguiendo el degradado de colores.\nLa función data_color() aplica colorización de celdas en base a los valores de la variable indicada en el argumento column. Si las variables numérica, especificamos que el método sea numeric, y luego tenemos varios argumentos que podemos especificar para modificar la colorización. Los principales son domain, donde se especifica el rango de la escala de colores (es decir, el valor que corresponde con el color mínimo y el valor del color máximo), y la paleta en palette, qué puede hacer un vector de colores que van desde el valor más bajo el valor más alto; es decir, el color asignado a los valores más bajos de la variable, y los colores siguientes en el degradado. En este caso, queremos que las cifras bajas sean de color blanco (igual que el fondo del gráfico), y a medida que el valor sea más alto sean de un rojo intenso y luego un rojo oscuro. El color más alto (rojo oscuro) corresponde al valor 0.3 o 30%, dado que en el contexto de medición de pobreza consideramos que eso ya es una cifra suficientemente alarmante. Los valores que queden fuera de este rango de cero a 30% van a adquirir el color de los datos perdidos (na_color), de modo que todos los valores iguales o mayores a 0.3 van a ser del color más alto.\ntabla_pobreza_6 \u0026lt;- tabla_pobreza_5 |\u0026gt; data_color(columns = pobreza_p, method = \u0026#34;numeric\u0026#34;, domain = c(0, 0.3), na_color = \u0026#34;red4\u0026#34;, palette = c(\u0026#34;white\u0026#34;, \u0026#34;red1\u0026#34;, \u0026#34;red4\u0026#34;)) Gracias al degradado de colores a las celdas tenemos una tabla que comunica mucho más que la versión anterior, dado que guía la interpretación de los datos a una variable puntual.\nEstilo avanzado de tablas Para finalizar con esta tabla, podemos personalizar con mayor detalle de todos los aspectos de la misma. En el bloque de código que viene a continuación, vamos a especificar múltiples modificaciones a la tabla:\nCambiar el tamaño del texto de las celdas Usar una tipografía o tipo de letra distinto para la tabla, usando tipografías de Google, con las funciones opt_table_font() y google_font() Poner el subtítulo de la tabla en itálica con tab_style() Cambiar el color del texto de algunas columnas, ya que queremos que pasen a segundo plano, usando tab_style() Alinear los textos de título y subtítulo hacia el lado izquierdo, usando tab_style() Cambiar el color de fondo de los nombres de columnas, para crear una jerarquía en la tabla, usando tab_style() Eliminar las líneas de borde de la tabla con opt_table_lines(\u0026quot;none\u0026quot;), para luego personalizar las líneas horizontales en tab_options(), poniéndolas de color blanco y ensanchándolas para que creen una separación entre filas Crear un espaciado entre el subtítulo con el resto de la tabla, al ensanchar la línea divisoria entre ambas, usando tab_options() Agregar un color de fondo alternante de las filas, para guiar la lectura, usando opt_row_striping() Cambiar el texto de la fuente en la parte de abajo de la tabla, y creamos un espaciado entre la fuente y la tabla ensanchando la línea divisoria entre ambas, usando tab_style() y tab_options() tabla_pobreza_7 \u0026lt;- tabla_pobreza_6 |\u0026gt; # texto de tabla más chico tab_style(style = cell_text(size = \u0026#34;small\u0026#34;), locations = list(cells_body(), cells_column_labels())) |\u0026gt; # tipografía opt_table_font(font = google_font(name = \u0026#34;Host Grotesk\u0026#34;)) |\u0026gt; # subtítulo en itálicas tab_style(style = cell_text(style = \u0026#34;italic\u0026#34;), locations = cells_title(\u0026#34;subtitle\u0026#34;)) |\u0026gt; # texto de columna tab_style(style = cell_text(\u0026#34;grey60\u0026#34;), locations = cells_body(columns = c(region, poblacion))) |\u0026gt; # alineación de texto de titulares tab_style(style = cell_text(align = \u0026#34;left\u0026#34;), locations = cells_title()) |\u0026gt; # color de fondo de títulos de columnas tab_style(style = cell_fill(\u0026#34;grey92\u0026#34;), locations = cells_column_labels()) |\u0026gt; # eliminar líneas por defecto opt_table_lines(\u0026#34;none\u0026#34;) |\u0026gt; # espaciado entre filas tab_options(table_body.hlines.style = \u0026#34;solid\u0026#34;, table_body.hlines.width = 2, table_body.hlines.color = \u0026#34;white\u0026#34;) |\u0026gt; # espaciado entre subtítulo y tabla tab_options(heading.border.bottom.style = \u0026#34;solid\u0026#34;, heading.border.bottom.color = \u0026#34;white\u0026#34;, heading.border.bottom.width = 12, heading.padding = 0) |\u0026gt; # filas con fondo alternante opt_row_striping() |\u0026gt; # texto fuente tab_options(source_notes.font.size = \u0026#34;small\u0026#34;, source_notes.padding = 0) |\u0026gt; tab_style(style = cell_text(align = \u0026#34;right\u0026#34;), locations = cells_source_notes()) |\u0026gt; # espaciado tabla y fuente tab_options(table_body.border.bottom.style = \u0026#34;solid\u0026#34;, table_body.border.bottom.color = \u0026#34;white\u0026#34;, table_body.border.bottom.width = 12) La tabla terminada tiene una apariencia mucho más personalizada y atractiva 🥰\nTabla de resultados electorales En el segundo ejemplo, haremos una pequeña tabla que resuma resultados electorales. En esta tabla, el sector político al que pertenezcan los candidatos o candidatas tendrá un color específico, pero este color se aplicará a una columna distinta al sector político.\nLos datos fueron obtenidos desde el sitio web del Servel por medio de web scraping. El procesamiento de los datos y los datos originales se encuentran en este repositorio, pero para este ejemplo, cargaremos directamente el archivo que contiene todos los resultados oficiales, procesados y limpios.\nlibrary(dplyr) library(tidyr) library(forcats) library(glue) library(lubridate) library(stringr) # cargar datos remotamente desde el repositorio datos_elecciones \u0026lt;- readr::read_csv2(\u0026#34;https://raw.githubusercontent.com/bastianolea/servel_scraping_votaciones/refs/heads/main/datos/resultados_alcaldes_2024.csv\u0026#34;) datos_elecciones # A tibble: 1,922 × 11 comuna candidato partido lista sector votos porcentaje total_votos \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 ALGARROBO Marco Antonio Go… RN Z - … Derec… 4250 0.276 15405 2 ALGARROBO Veronica Maricel… IND CAND… Indep… 2905 0.189 15405 3 ALGARROBO Marcela Maritza … IND CAND… Indep… 2677 0.174 15405 4 ALGARROBO Alejandro Felipe… IND CAND… Indep… 2425 0.157 15405 5 ALGARROBO Carlos Orlando T… IND CAND… Indep… 1089 0.0707 15405 6 ALGARROBO Maria Luisa Hami… IND CAND… Indep… 365 0.0237 15405 7 ALGARROBO Gaston Dubournai… IND F - … Indep… 332 0.0216 15405 8 ALGARROBO Nulo/Blanco \u0026lt;NA\u0026gt; \u0026lt;NA\u0026gt; Otros 1362 0.0884 15405 9 ALHUE Marcela Chamorro… IND H - … Izqui… 3490 0.598 5841 10 ALHUE Jose Andres Arel… IND CAND… Indep… 1828 0.313 5841 # ℹ 1,912 more rows # ℹ 3 more variables: mesas_escrutadas \u0026lt;dbl\u0026gt;, mesas_totales \u0026lt;dbl\u0026gt;, # mesas_porcentaje \u0026lt;dbl\u0026gt; Habiendo cargado los datos, filtramos por una comuna que nos interese, y ordenamos las columnas:\nConvertimos el sector político (variable sector) en un factor ordenado. Luego hacemos que la columna candidato también sea un factor cuyo orden depende del porcentaje obtenido. Llevamos los votos nulos a la última posición del factor de candidatos para que aparezcan al final. Ordenamos la tabla por el factor ordenado candidato. comuna_elegida = \u0026#34;PEÑALOLEN\u0026#34; tabla_elecciones_1 \u0026lt;- datos_elecciones |\u0026gt; filter(comuna == comuna_elegida) |\u0026gt; # ordenar sector político mutate(sector = as.factor(sector), sector = fct_relevel(sector, \u0026#34;Izquierda\u0026#34;, \u0026#34;Derecha\u0026#34;, \u0026#34;Independiente\u0026#34;, \u0026#34;Otros\u0026#34;)) |\u0026gt; # nulos al final de la tabla mutate(candidato = fct_reorder(candidato, porcentaje), candidato = fct_relevel(candidato, \u0026#34;Nulo/Blanco\u0026#34;,after = 0)) |\u0026gt; arrange(desc(candidato)) |\u0026gt; select(candidato, partido, votos, porcentaje, sector) tabla_elecciones_1 # A tibble: 5 × 5 candidato partido votos porcentaje sector \u0026lt;fct\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;fct\u0026gt; 1 Miguel Andres Concha Manso FA 52293 0.317 Izquierda 2 Claudia Mora Vega RN 52170 0.316 Derecha 3 Carlos Alarcon Castro IND 31217 0.189 Independiente 4 Eduardo Giesen Amtmann IND 9321 0.0565 Independiente 5 Nulo/Blanco \u0026lt;NA\u0026gt; 19928 0.121 Otros Procedemos a crear la tabla básica con {gt}, incluyendo un subtítulo en formato markdown que contiene dentro de sí el nombre de la comuna elegida a partir del objeto que usamos para filtrar los datos en el paso anterior. El texto markdown permite darle estilo enriquecido al texto de forma sencilla usando símbolos, lo cual resulta conveniente para casos como éste.\nlibrary(gt) tabla_elecciones_2 \u0026lt;- tabla_elecciones_1 |\u0026gt; gt() |\u0026gt; # titulares tab_header(title = md(\u0026#34;_Resultados parciales:_ Elecciones Municipales 2024\u0026#34;), subtitle = md(glue(\u0026#34;**{str_to_title(comuna_elegida)}**\u0026#34;))) Aplicamos el formato de porcentaje a la columna de porcentaje, y el formato numérico a la columna numérica. Luego definimos algunos aspectos del estilo de la tabla, como fondos de color para algunas columnas, una tipografía personalizada desde Google Fonts, alineaciones de textos, y estilos de textos distintos en los distintos elementos de la tabla.\ntabla_elecciones_3 \u0026lt;- tabla_elecciones_2 |\u0026gt; # formato de números fmt_percent(porcentaje, decimals = 1) |\u0026gt; fmt_number(votos, decimals = 0, sep_mark = \u0026#34;.\u0026#34;, dec_mark = \u0026#34;,\u0026#34;) |\u0026gt; # estilo fondos de todas las celdas excepto partido tab_style(locations = cells_body(column = c(candidato, votos, porcentaje)), style = cell_fill(color = \u0026#34;grey96\u0026#34;)) |\u0026gt; # tipografía opt_table_font(font = google_font(\u0026#34;Open Sans\u0026#34;)) |\u0026gt; # alineación de textos cols_align(columns = c(candidato, partido, porcentaje), align = \u0026#34;left\u0026#34;) |\u0026gt; cols_align(columns = c(partido), align = \u0026#34;center\u0026#34;) |\u0026gt; cols_hide(sector) |\u0026gt; opt_table_lines(\u0026#34;none\u0026#34;) |\u0026gt; opt_align_table_header(align = \u0026#34;left\u0026#34;) |\u0026gt; # estilo de textos tab_style(locations = cells_body(column = c(candidato, partido, porcentaje)), style = cell_text(weight = \u0026#34;bold\u0026#34;)) |\u0026gt; tab_style(locations = cells_column_labels(), style = cell_text(style = \u0026#34;italic\u0026#34;)) |\u0026gt; tab_style(locations = cells_body(rows = candidato == \u0026#34;Nulo/Blanco\u0026#34;), style = cell_text(weight = \u0026#34;normal\u0026#34;)) |\u0026gt; # etiquetas de columnas cols_label(candidato = \u0026#34;Candidato/a\u0026#34;, partido = \u0026#34;Partido\u0026#34;, votos = \u0026#34;Votos\u0026#34;, porcentaje = \u0026#34;%\u0026#34;) Colorear celdas según una variable categórica o factor Ahora viene lo entretenido. La tabla tiene una columna con una variable de partido político (partido), pero los datos contienen también una columna de sector político, que engloba a los partidos políticos. Vamos a asignar un color a las celdas del partido político dependiendo del sector político al que pertenecen; es decir, usar el contenido de una columna de la tabla para cambiar el estilo de otra columna distinta de la tabla.\nCreamos una lista con los colores que queremos usar para la variable categórica sector, que representa el sector del espectro político al cual pueden pertenecer los partidos políticos.\ncolor \u0026lt;- list(\u0026#34;izquierda\u0026#34; = \u0026#34;#DB231D\u0026#34;, \u0026#34;derecha\u0026#34; = \u0026#34;#1D76DB\u0026#34;, \u0026#34;centro\u0026#34; = \u0026#34;#B16AD2\u0026#34;, \u0026#34;independiente\u0026#34; = \u0026#34;#38B51D\u0026#34;, \u0026#34;otros\u0026#34; = \u0026#34;grey50\u0026#34;) levels(tabla_elecciones_1$sector) [1] \u0026quot;Izquierda\u0026quot; \u0026quot;Derecha\u0026quot; \u0026quot;Independiente\u0026quot; \u0026quot;Otros\u0026quot; La función data_color(), que usamos anteriormente para dar color a variables numéricas en base a sus valores, también permite asignar colores a las variables en base a sus niveles, para el caso de variables categóricas o factores que contienen niveles o categorías de respuesta nominales u ordinales.\nPara que esto funcione correctamente, es necesario que la variable esté en formato factor, lo cual hicimos en el primer paso de ordenar las columnas. La variable sector posee un determinado orden de sus niveles, que revisamos con la función levels(tabla_elecciones_1$sector), Y en base al orden de estos niveles, especificamos los colores en la paleta, en el argumento palette de data_color(). Es en esta función donde especificamos que la información sale de la columna sector, pero afecta a la columna partido, y el efecto del color se aplica al texto, no al fondo (apply_to = \u0026quot;text\u0026quot;).\nUna vez especificando esto, podemos repetir la misma operación pero aplicando el efecto al fondo (apply_to = \u0026quot;fill\u0026quot;) y dándole una transparencia de 20% (alpha = 0.2).\ntabla_elecciones_4 \u0026lt;- tabla_elecciones_3 |\u0026gt; # color texto partidos data_color(columns = sector, target_columns = partido, method = \u0026#34;factor\u0026#34;, palette = c(color$izquierda, color$derecha, color$independiente, color$otros), apply_to = \u0026#34;text\u0026#34;) |\u0026gt; # color fondo partidos data_color(columns = sector, rows = candidato != \u0026#34;Nulo/Blanco\u0026#34;, target_columns = partido, method = \u0026#34;factor\u0026#34;, palette = c(color$izquierda, color$derecha, color$independiente, color$otros), alpha = 0.2, apply_to = \u0026#34;fill\u0026#34;, autocolor_text = FALSE) Repetir el efecto de colonización al texto y al fondo por separado nos permite darle un efecto más sofisticado al color de las celdas, donde el fondo de la celda tiene el mismo color que el texto, pero más suave.\nFinalmente, aplicamos otras personalizaciones a la tabla, principalmente para crear un espacio entre las celdas y otros elementos de la tabla, y especificar el color de fondo y de texto de los votos nulos/blancos, que son menos relevantes.\ntabla_elecciones_5 \u0026lt;- tabla_elecciones_4 |\u0026gt; # color fondo nulos data_color(columns = sector, rows = candidato == \u0026#34;Nulo/Blanco\u0026#34;, target_columns = partido, palette = \u0026#34;grey96\u0026#34;, alpha = 1, apply_to = \u0026#34;fill\u0026#34;, autocolor_text = FALSE) |\u0026gt; # estilo fila nulos tab_style(locations = cells_body(rows = candidato == \u0026#34;Nulo/Blanco\u0026#34;), style = cell_text(color = \u0026#34;grey70\u0026#34;)) |\u0026gt; # notas al pie tab_options(table_body.hlines.style = \u0026#34;solid\u0026#34;, table_body.hlines.width = 8, table_body.hlines.color = \u0026#34;white\u0026#34;, table_body.vlines.style = \u0026#34;solid\u0026#34;, table_body.vlines.width = 8, table_body.vlines.color = \u0026#34;white\u0026#34;) |\u0026gt; tab_footnote(footnote = glue(\u0026#34;Fuente: Servel\u0026#34;)) |\u0026gt; tab_style(locations = cells_footnotes(), style = cell_text(align = \u0026#34;right\u0026#34;, size = px(12))) |\u0026gt; tab_options(heading.subtitle.font.size = 19, heading.padding = 1, heading.border.bottom.style = \u0026#34;solid\u0026#34;, heading.border.bottom.width = 16, heading.border.bottom.color = \u0026#34;white\u0026#34;) |\u0026gt; tab_options(table_body.border.bottom.style = \u0026#34;solid\u0026#34;, table_body.border.bottom.width = 5, table_body.border.bottom.color = \u0026#34;white\u0026#34;, footnotes.padding = 1) Obtenemos como resultado una tabla concisa pero a la vez informativa.\nTabla de Producto Interno Bruto regional El tercer y último ejemplo será una tabla con datos regionales para un rango de varios años; es decir, una tabla con una alta cantidad de celdas numéricas en cuadrícula.\nLos datos serán obtenidos directamente desde las estadísticas del Banco Central de Chile, utilizando web scraping, siguiendo el trabajo previo que hice con estadísticas económicas para un visualizador interactivo de indicadores económicos.\nCargamos el paquete {rvest}, especificamos la dirección web donde está ubicada la tabla que nos interesa, y hacemos web scraping de la tabla ubicada en esa página:\nlibrary(dplyr) library(rvest) # url con la tabla url \u0026lt;- \u0026#34;https://si3.bcentral.cl/Siete/ES/Siete/Cuadro/CAP_ESTADIST_REGIONAL/MN_REGIONAL1/CCNN2018_PIB_REGIONAL_T?cbFechaInicio=2018\u0026amp;cbFechaTermino=2024\u0026amp;cbFrecuencia=ANNUAL\u0026amp;cbCalculo=NONE\u0026amp;cbFechaBase=\u0026#34; # abrir sesión y obtener código de fuente del sitio sitio \u0026lt;- session(url) |\u0026gt; read_html() # obtener la tabla del sitio, sin convertir unidades tablas \u0026lt;- sitio |\u0026gt; html_table(convert = FALSE) # limpieza mínima tabla_pib \u0026lt;- tablas[[1]] |\u0026gt; rename(region = 2) |\u0026gt; select(-1) tabla_pib # A tibble: 19 × 8 region `2018` `2019` `2020` `2021` `2022` `2023` `2024` \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Región de Arica y Parinacota 1.433 1.419 1.331 1.554 1.665 1.696 1.680 2 Región de Tarapacá 4.355 4.573 4.392 4.791 4.868 5.022 5.225 3 Región de Antofagasta 15.986 15.983 15.697 15.560 15.562 16.089 17.257 4 Región de Atacama 3.687 3.528 3.540 3.958 3.991 4.036 4.081 5 Región de Coquimbo 5.876 5.956 5.658 6.169 6.126 6.311 6.395 6 Región de Valparaíso 13.989 13.948 13.087 14.406 15.258 15.288 15.551 7 Región Metropolitana de San… 78.215 78.931 72.170 81.945 84.131 84.396 85.941 8 Región del Libertador Gener… 7.930 7.882 7.589 8.310 8.328 8.211 8.686 9 Región del Maule 7.186 7.148 7.049 7.651 7.898 8.046 8.464 10 Región de Ñuble 2.775 2.786 2.693 3.016 3.121 3.066 3.176 11 Región del Biobío 11.337 11.499 10.865 11.864 12.202 12.917 13.241 12 Región de La Araucanía 5.241 5.325 5.075 5.708 5.956 6.072 6.255 13 Región de Los Ríos 2.498 2.503 2.403 2.639 2.709 2.706 2.821 14 Región de Los Lagos 6.422 6.563 6.225 6.722 6.957 7.047 7.156 15 Región de Aysén del General… 1.228 1.243 1.138 1.176 1.198 1.297 1.294 16 Región de Magallanes y de l… 1.843 1.934 1.689 1.813 1.912 1.943 2.052 17 Subtotal regionalizado 170.0… 171.2… 160.6… 176.9… 181.3… 183.7… 189.1… 18 Extrarregional 19.435 19.415 18.320 22.270 22.219 20.702 20.737 19 Producto Interno Bruto 189.4… 190.6… 178.9… 199.1… 203.4… 204.5… 209.9… Nos encontramos con una tabla que tiene 19 filas y seis columnas numéricas, correspondientes a los valores del Producto Interno Bruto para cada uno de los años. Procedemos a limpiar estos datos, debido a que la tabla usa puntos como divisores de miles, y esto hace que R interprete los números como si fueran decimales. Usamos stringr::str_remove() para eliminar todos los puntos en todas las columnas que empiecen con 20 (los años), y luego las convertimos a numéricas.\nlibrary(stringr) library(forcats) pib_regional \u0026lt;- tabla_pib |\u0026gt; mutate(across(starts_with(\u0026#34;20\u0026#34;), ~str_remove(.x, \u0026#34;\\\\.\u0026#34;))) |\u0026gt; mutate(across(starts_with(\u0026#34;20\u0026#34;), as.numeric)) |\u0026gt; filter(str_detect(region, \u0026#34;Región\u0026#34;)) pib_regional # A tibble: 16 × 8 region `2018` `2019` `2020` `2021` `2022` `2023` `2024` \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Región de Arica y Parinacota 1433 1419 1331 1554 1665 1696 1680 2 Región de Tarapacá 4355 4573 4392 4791 4868 5022 5225 3 Región de Antofagasta 15986 15983 15697 15560 15562 16089 17257 4 Región de Atacama 3687 3528 3540 3958 3991 4036 4081 5 Región de Coquimbo 5876 5956 5658 6169 6126 6311 6395 6 Región de Valparaíso 13989 13948 13087 14406 15258 15288 15551 7 Región Metropolitana de San… 78215 78931 72170 81945 84131 84396 85941 8 Región del Libertador Gener… 7930 7882 7589 8310 8328 8211 8686 9 Región del Maule 7186 7148 7049 7651 7898 8046 8464 10 Región de Ñuble 2775 2786 2693 3016 3121 3066 3176 11 Región del Biobío 11337 11499 10865 11864 12202 12917 13241 12 Región de La Araucanía 5241 5325 5075 5708 5956 6072 6255 13 Región de Los Ríos 2498 2503 2403 2639 2709 2706 2821 14 Región de Los Lagos 6422 6563 6225 6722 6957 7047 7156 15 Región de Aysén del General… 1228 1243 1138 1176 1198 1297 1294 16 Región de Magallanes y de l… 1843 1934 1689 1813 1912 1943 2052 Ahora podemos crear nuestra tabla básica con {gt}, especificando el formato apropiado para todas las columnas numéricas en una sola instancia fmt_number(). Esto es posible porque podemos usar selectores de {dplyr} en los argumentos de columnas de {gt}, así que podemos pedirle que aplique un formato, estilo, u otras herramientas de {gt} a una o varias columnas seleccionadas por su nombre parcial o por su tipo de datos. En este caso, aplicamos el formato a todas las columnas numéricas usando where(is.numeric):\nlibrary(gt) tabla_pib_regional_1 \u0026lt;- pib_regional |\u0026gt; gt() |\u0026gt; cols_align(\u0026#34;right\u0026#34;, region) |\u0026gt; # formato de columnas numéricas fmt_number(columns = where(is.numeric), sep_mark = \u0026#34;.\u0026#34;, decimals = 0) Con una sola línea formateamos seis columnas. Yey! 🥳\nCálculo de diferencia porcentual entre años Luego producir esta tabla, vamos a realizar una transformación en los datos para poder calcular la variación anual, entendida como la diferencia porcentual entre un año y el anterior. Para realizar este cálculo, lo más conveniente es transformar los datos a formato largo; es decir, una fila por observación, y una variable por columna. Usamos pivot_longer() para transformar las seis columnas que teníamos de datos numéricos por año, en una sola columna de datos numéricos, y otra columna que contengan los años a los que corresponde cada observación.\nlibrary(tidyr) pib_regional_long \u0026lt;- pib_regional |\u0026gt; pivot_longer(cols = where(is.numeric), names_to = \u0026#34;año\u0026#34;, values_to = \u0026#34;valor\u0026#34;) Ahora, es muy sencillo calcular la variación porcentual entre una observación y otra: agrupamos los datos por región, y calculamos cada valor dividido por el valor anterior (la celda de arriba, obtenida con lag()):\npib_regional_cambio \u0026lt;- pib_regional_long |\u0026gt; # ordenar observaciones arrange(region, año) |\u0026gt; group_by(region) |\u0026gt; # calcular cambio mutate(cambio = valor/lag(valor), cambio = 1 - cambio) |\u0026gt; ungroup() |\u0026gt; filter(!is.na(cambio)) pib_regional_cambio # A tibble: 96 × 4 region año valor cambio \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Región Metropolitana de Santiago 2019 78931 -0.00915 2 Región Metropolitana de Santiago 2020 72170 0.0857 3 Región Metropolitana de Santiago 2021 81945 -0.135 4 Región Metropolitana de Santiago 2022 84131 -0.0267 5 Región Metropolitana de Santiago 2023 84396 -0.00315 6 Región Metropolitana de Santiago 2024 85941 -0.0183 7 Región de Antofagasta 2019 15983 0.000188 8 Región de Antofagasta 2020 15697 0.0179 9 Región de Antofagasta 2021 15560 0.00873 10 Región de Antofagasta 2022 15562 -0.000129 # ℹ 86 more rows Con nuestra nueva variable cambio calculada, ahora volvemos a transformar los datos para que estén en el formato ancho; es decir, nuevamente cada celda ubicada en una columna distinta dependiendo del año al cual corresponde. Así, volvemos a obtener seis columnas de datos numéricos, una para cada año, pero esta vez con la diferencia porcentual de los valores en vez del valor absoluto.\npib_regional_cambio_wide \u0026lt;- pib_regional_cambio |\u0026gt; select(region, año, cambio) |\u0026gt; pivot_wider(names_from = año, values_from = cambio) pib_regional_cambio_wide # A tibble: 16 × 7 region `2019` `2020` `2021` `2022` `2023` `2024` \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 Región Metropolitana d… -9.15e-3 0.0857 -0.135 -2.67e-2 -0.00315 -0.0183 2 Región de Antofagasta 1.88e-4 0.0179 0.00873 -1.29e-4 -0.0339 -0.0726 3 Región de Arica y Pari… 9.77e-3 0.0620 -0.168 -7.14e-2 -0.0186 0.00943 4 Región de Atacama 4.31e-2 -0.00340 -0.118 -8.34e-3 -0.0113 -0.0111 5 Región de Aysén del Ge… -1.22e-2 0.0845 -0.0334 -1.87e-2 -0.0826 0.00231 6 Región de Coquimbo -1.36e-2 0.0500 -0.0903 6.97e-3 -0.0302 -0.0133 7 Región de La Araucanía -1.60e-2 0.0469 -0.125 -4.34e-2 -0.0195 -0.0301 8 Región de Los Lagos -2.20e-2 0.0515 -0.0798 -3.50e-2 -0.0129 -0.0155 9 Región de Los Ríos -2.00e-3 0.0400 -0.0982 -2.65e-2 0.00111 -0.0425 10 Región de Magallanes y… -4.94e-2 0.127 -0.0734 -5.46e-2 -0.0162 -0.0561 11 Región de Tarapacá -5.01e-2 0.0396 -0.0908 -1.61e-2 -0.0316 -0.0404 12 Región de Valparaíso 2.93e-3 0.0617 -0.101 -5.91e-2 -0.00197 -0.0172 13 Región de Ñuble -3.96e-3 0.0334 -0.120 -3.48e-2 0.0176 -0.0359 14 Región del Biobío -1.43e-2 0.0551 -0.0919 -2.85e-2 -0.0586 -0.0251 15 Región del Libertador … 6.05e-3 0.0372 -0.0950 -2.17e-3 0.0140 -0.0578 16 Región del Maule 5.29e-3 0.0139 -0.0854 -3.23e-2 -0.0187 -0.0520 Generamos una nueva tabla {gt} con las diferencias porcentuales entre años, especificando que estas columnas numéricas ahora son porcentajes:\ntabla_pib_regional_2 \u0026lt;- pib_regional_cambio_wide |\u0026gt; gt() |\u0026gt; cols_align(\u0026#34;right\u0026#34;, region) |\u0026gt; fmt_percent(where(is.numeric), decimals = 1, dec_mark = \u0026#34;,\u0026#34;) |\u0026gt; cols_label(region = \u0026#34;Región\u0026#34;) Colorear múltiples columnas simultáneamente Ahora que tenemos una cuadrícula de valores numéricos en nuestra tabla, resulta conveniente aplicar color a cada celda dependiendo de su valor. Esto permite una interpretación más rápida de una gran cantidad de datos, debido a que seguía la vista hacia los valores anómalos o destacables, Y se facilita la interpretación de patrones generales en los datos a partir del color.\nUsamos la función data_color() para colorizar las columnas, aprovechando las selección de columnas con la función where(is.numeric) para aplicar el color a todas las columnas numéricas de una sola vez. Usaremos la paleta viridis, que facilita la interpretación de diferencia en los datos, y además es inclusiva para personas daltónicas.\ntabla_pib_regional_3 \u0026lt;- tabla_pib_regional_2 |\u0026gt; data_color(where(is.numeric), method = \u0026#34;numeric\u0026#34;, palette = \u0026#34;viridis\u0026#34;) Otra opción es especificar que los colores se apliquen solamente al texto, para generar una tabla más liviana a la vista. Especificamos una paleta básica que va de rojo a verde pasando por negro. Usamos el método bin para segmentar los colores en rangos discretos, en vez de usar un degradado de colores, para facilitar la interpretación. Los rangos para cada uno de los colores se establece en el argumento bins, donde definimos que el color rojo se va aplicar a valores hasta -2%, luego negro del -2% al 2%, y de ahí en adelante verde.\ntabla_pib_regional_4 \u0026lt;- tabla_pib_regional_2 |\u0026gt; data_color(where(is.numeric), method = \u0026#34;bin\u0026#34;, apply_to = \u0026#34;text\u0026#34;, palette = c(\u0026#34;#bf2f2f\u0026#34;, \u0026#34;black\u0026#34;, \u0026#34;#279f2b\u0026#34;), bins = c(-Inf, -0.02, 0.02, Inf)) Usamos categorías de colores en vez de degradados para facilitar la interpretación, debido a que los degradados tendrían demasiados valores de color en una cuadrícula que ya es suficientemente compleja, dificultando que las personas asocian rápidamente diferentes sombras de rojo o de verde a interpretaciones significativas. En lugar de eso, simplificamos la interpretación al solamente colorear valores negativos y positivos, y mantener un rango de valores dentro de la normalidad que carecen de un color distintivo (negro).\nFinalmente, aplicamos más personalizaciones de estilo a la tabla para que resulte más atractiva:\nLimitar el ancho de la primera columna con cols_width() Aplicar negritas a los nombres de variables e itálicas a la primera columna con tab_style() Eliminar las líneas horizontales de arriba y abajo de la tabla Disminuir el espaciado interior de las celdas para aumentar la densidad de información con tab_options(data_row.padding = px(6)) tabla_pib_regional_5 \u0026lt;- tabla_pib_regional_4 |\u0026gt; # ancho máximo de columna región cols_width(region ~ px(220)) |\u0026gt; # filas alternas opt_row_striping() |\u0026gt; # estilo de columnas tab_style(style = cell_text(weight = \u0026#34;bold\u0026#34;), locations = cells_column_labels()) |\u0026gt; tab_style(style = cell_text(style = \u0026#34;italic\u0026#34;), locations = cells_body(columns = region)) |\u0026gt; # eliminar líneas horizontales de arriba y abajo tab_options(table.border.top.style = \u0026#34;hidden\u0026#34;, table.border.bottom.style = \u0026#34;hidden\u0026#34;) |\u0026gt; # cambiar el espaciado interno de las celdas tab_options(data_row.padding = px(6)) |\u0026gt; # título y fuentes tab_header(title = \u0026#34;Producto Interno Bruto regional\u0026#34;, subtitle = \u0026#34;Variación anual, por regiones\u0026#34;) |\u0026gt; tab_source_note(\u0026#34;Fuente: Banco Central de Chile\u0026#34;) Estilo condicional a través de varias columnas Ahora nos ubicaremos en un caso de uso más complejo. Queremos destacar los valores más extremos de la tabla a partir de una condicional: si los valores de las celdas son menores a -3%, queremos que el texto esté en negritas. Como ejemplo, aplicaremos esto a una sola columna, dejando las demás en gris para que se note el cambio:\n# una columna tabla_pib_regional_5a \u0026lt;- tabla_pib_regional_5 |\u0026gt; tab_style(style = cell_text(color = \u0026#34;grey70\u0026#34;), locations = cells_body(columns = `2019`:`2022`)) |\u0026gt; tab_style(style = cell_text(weight = \u0026#34;bold\u0026#34;), locations = cells_body(columns = `2023`, rows = `2023` \u0026lt; -0.03)) Si bien esto funciona, el problema que vamos a tener radica en que cada especificación de las columnas a las que va a aplicar el estilo dependen también de la evaluación de la condicional en la misma columna. En el fondo, que un valor adquiera o no el estilo depende de si está en la columna, y si la fila de esa misma columna cumple con la condición establecida. Entonces, si queremos aplicar lo mismo a otra columna, habría que modificar tanto la columna como la condición, porque la condición depende también de la columna.\nEntonces, ¿qué hacer si queremos aplicar lo mismo a más de una columna? Podemos repetir el llamado de tab_style() para cada uno de los años, o bien, podemos hacer un solo llamado a tab_style(), pero especificando más de una combinación de columna/fila en locations al mismo tiempo:\n# dos columnas tabla_pib_regional_5b \u0026lt;- tabla_pib_regional_5 |\u0026gt; tab_style(style = cell_text(color = \u0026#34;grey70\u0026#34;), locations = cells_body(columns = `2019`:`2021`)) |\u0026gt; tab_style(style = cell_text(weight = \u0026#34;bold\u0026#34;), locations = list(cells_body(columns = `2022`, rows = `2022` \u0026lt; -0.03), cells_body(columns = `2023`, rows = `2023` \u0026lt; -0.03)) ) Esto funciona porque se puede entregar a locations una lista con múltiples funciones que especifiquen el lugar en la tabla en que se quiere aplicar el estilo.\nSin embargo, esta solución se volvería extremadamente repetitiva si queremos aplicarlo a seis columnas o más. Habría que escribir manualmente la especificación de cells_body() para cada una de las combinaciones de años y filas.\nA continuación comparto una solución a este problema que involucra una iteración sobre los nombres de las columnas usando purrr::map(). La idea es que, si el argumento locations acepta múltiples columnas por medio de una lista (como hicimos en el paso anterior), podemos usar map() para especificar una sola vez la columna y la condición, y multiplicarla por todas las columnas que necesitemos.\nLo que estamos haciendo es una lista con múltiples instancias de cells_body(), una para cada elemento en el vector columnas. Dentro de cells_body() se usa la sintaxis !!sym() para transformar el nombre de una columna escrito como texto a un símbolo, para poder hacer la comparación apropiadamente (porque no se puede hacer \u0026quot;1\u0026quot; \u0026lt; 0, tiene que ser 1 \u0026lt; 0).\n# múltiples columnas con filas condicionales # obtener nombres de columas a las que queremos aplicar el estilo columnas \u0026lt;- pib_regional_cambio_wide |\u0026gt; select(where(is.numeric)) |\u0026gt; names() tabla_pib_regional_5c \u0026lt;- tabla_pib_regional_5 |\u0026gt; tab_style(style = cell_text(weight = \u0026#34;bold\u0026#34;), # aplicar el estilo a todas las columnas, iterando la función cells_body por los nombres de columnas locations = purrr::map(columnas, ~cells_body( columns = .x, rows = !!sym(.x) \u0026lt; -0.03) ) ) De esta forma, usamos un poco de magia de iteraciones con {purrr} y sintaxis avanzada de R con {rlang} para poder aplicar un formato condicional a múltiples columnas de una tabla.\nOtros recursos para tablas {gt} Documentación oficial: https://gt.rstudio.com https://themockup.blog/static/resources/gt-cookbook.html#table-customization https://gt.albert-rapp.de Si este tutorial te sirvió, por favor considera hacerme una pequeña donación para poder tomarme un cafecito mientras escribo el siguiente tutorial 🥺\n","date":"2024-11-19T00:00:00Z","excerpt":"El paquete de R `{gt}` genera tablas para presentar tus datos. Produce tablas atractivas con muy pocas líneas de código, y al mismo tiempo ofrece una alta capacidad de personalización de las tablas. En este artículo te mostraré tres ejemplos de creación de distintas tablas basadas en datos reales.","href":"https://bastianoleah.netlify.app/blog/tutorial_gt/","tags":"tablas ; web scraping ; Chile","title":"Tutorial: generar tablas atractivas y personalizables con {gt}"},{"content":"library(dplyr) # manipulación de datos library(lubridate) # trabajar con fechas library(RQuantLib) # paquete con calendarios library(bizdays) # paquete para contar días hábiles Hoy se me planteó un pequeño desafío que no había tenido que hacer antes. Tenía que contar la cantidad de días entre dos fechas.\nEsto no es particularmente complejo, de hecho es demasiado fácil:\nContar la cantidad de días entre dos fechas Primero definimos las dos fechas:\nfecha_hoy \u0026lt;- today() fecha_anterior \u0026lt;- today() - weeks(2) fecha_hoy [1] \u0026quot;2024-11-14\u0026quot; fecha_anterior [1] \u0026quot;2024-10-31\u0026quot; Y luego usamos una sencilla función para buscar la diferencia entre ambas fechas:\ndifftime(fecha_hoy, fecha_anterior) Time difference of 14 days Entre el 13 de noviembre y el 30 de octubre de 2024 hay 14 días. Incluso, lo anterior podría ser aún más simple:\nfecha_hoy - fecha_anterior Time difference of 14 days En R, tan sencillo como restar dos fechas para obtener la diferencia de tiempo entre ambas.\nContar la cantidad de días hábiles entre dos fechas Pero el problema no era que tenía que contar entre dos fechas, sino que tenía que contar sólo los días hábiles entre ambas.\nEsto se vuelve más complejo. Primero, porque habría que definir una forma de saltarse los fines de semana. Pero también, los días hábiles son aquellos que no son festivos, y esto implica tener una lista de todos los días festivos o feriados. Pero además, ¡los días festivos son locales! Dependen de cada país y son distintos en cada uno, e incluso cambian en el tiempo: se crean nuevos, desaparecen otros, y algunos cambian de fecha entre año y año.\nMenos mal que en R existe un paquete para todo 😌 El paquete {RQuantLib} contiene la información de calendarios de muchísimos países del mundo. A su vez, el paquete {bizdays} te permite tomar esa información de los calendarios y usarlos para el cálculo de días hábiles entre fechas:\nbizdays::load_quantlib_calendars( \u0026#34;Chile\u0026#34;, from = \u0026#34;2023-01-01\u0026#34;, to = \u0026#34;2025-12-31\u0026#34;) Calendar QuantLib/Chile loaded calendars() # confirmar que el calendario está cargado Calendars: actual, Brazil/ANBIMA, Brazil/B3, Brazil/BMF, QuantLib/Chile, weekends Ahora que tenemos un calendario de los días feriados en Chile cargado en R, usamos la función bizdays() del paquete del mismo nombre para calcular la cantidad de días hábiles entre ambas fechas, especificando el calendario que queremos usar:\nbizdays(fecha_hoy, fecha_anterior, \u0026#34;QuantLib/Chile\u0026#34;) [1] -8 En este caso, el resultado es 8, porque a los 14 días que habían entre ambas fechas le resta dos fines de semana (4 días), y además le resta dos días feriados oficiales en Chile (día de las Iglesias Evangélicas el 31 de octubre y el Día de todos los santos el 1 de noviembre).\nRepitamos el mismo ejemplo, pero ahora en una pequeña tabla de datos ficticios:\ndatos \u0026lt;- tibble::tribble( ~n, ~fecha, ~fecha_hoy, \u0026#34;16831\u0026#34;, \u0026#34;2024-02-19\u0026#34;, \u0026#34;2024-11-13\u0026#34;, \u0026#34;40003\u0026#34;, \u0026#34;2024-01-12\u0026#34;, \u0026#34;2024-11-13\u0026#34;, \u0026#34;90667\u0026#34;, \u0026#34;2024-04-06\u0026#34;, \u0026#34;2024-11-13\u0026#34;, \u0026#34;80205\u0026#34;, \u0026#34;2024-10-07\u0026#34;, \u0026#34;2024-11-13\u0026#34;, \u0026#34;14457\u0026#34;, \u0026#34;2024-08-30\u0026#34;, \u0026#34;2024-11-13\u0026#34; ) En este ejemplo, primero calculamos la diferencia normal entre días, arreglamos el resultado de la diferencia de tiempo para formar los números comunes, y luego calculamos la diferencia entre días hábiles, para poder comparar:\ndatos |\u0026gt; mutate(dias_comunes = difftime(fecha, fecha_hoy), dias_comunes = dias_comunes |\u0026gt; as.numeric() |\u0026gt; abs() |\u0026gt; round(0), dias_habiles = bizdays(fecha, fecha_hoy, \u0026#34;QuantLib/Chile\u0026#34;) ) # A tibble: 5 × 5 n fecha fecha_hoy dias_comunes dias_habiles \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 16831 2024-02-19 2024-11-13 268 181 2 40003 2024-01-12 2024-11-13 306 207 3 90667 2024-04-06 2024-11-13 221 147 4 80205 2024-10-07 2024-11-13 37 25 5 14457 2024-08-30 2024-11-13 75 48 Contar la cantidad de días de semana entre dos fechas Naturalmente, existe otra forma de hacer esto, mucho más básica. Consiste en crear una secuencia con todas las fechas que hay entre las dos fechas, e ir evaluando si cada una de esas fechas cae el día de semana o en fin de semana.\nPrimero creamos la secuencia de fechas:\nsecuencia \u0026lt;- seq.Date(fecha_anterior, fecha_hoy, by = \u0026#34;days\u0026#34;) secuencia [1] \u0026quot;2024-10-31\u0026quot; \u0026quot;2024-11-01\u0026quot; \u0026quot;2024-11-02\u0026quot; \u0026quot;2024-11-03\u0026quot; \u0026quot;2024-11-04\u0026quot; [6] \u0026quot;2024-11-05\u0026quot; \u0026quot;2024-11-06\u0026quot; \u0026quot;2024-11-07\u0026quot; \u0026quot;2024-11-08\u0026quot; \u0026quot;2024-11-09\u0026quot; [11] \u0026quot;2024-11-10\u0026quot; \u0026quot;2024-11-11\u0026quot; \u0026quot;2024-11-12\u0026quot; \u0026quot;2024-11-13\u0026quot; \u0026quot;2024-11-14\u0026quot; Luego utilizamos la función wday() para saber en qué día de la semana cae cada una de las fechas, indicando que queremos que el día lunes sea equivalente a 1, por lo que el fin de semana correspondería a los números 6 y 7:\nsecuencia_dias \u0026lt;- lubridate::wday(secuencia, week_start = 1) secuencia_dias [1] 4 5 6 7 1 2 3 4 5 6 7 1 2 3 4 En esta secuencia, cada número corresponde a una de las fechas del vector anterior, y el número representa el día de la semana: el número 4 es jueves, el número 5 es viernes, el número 6 es sábado, y así sucesivamente.\nFinalmente, sólo queda evaluar cuales de las fechas son días a menores o iguales a 5 (es decir, días de semana y no sábado ni domingo)\nsecuencia_dias \u0026lt;= 5 [1] TRUE TRUE FALSE FALSE TRUE TRUE TRUE TRUE TRUE FALSE FALSE TRUE [13] TRUE TRUE TRUE La comparación retorna verdadero cuando el número es menor igual a cinco, y falso cuando es mayor o igual. O sea, verdadero cuando es día de semana, y falso cuándo es fin de semana.\nLuego contamos la cantidad de días que coinciden con la condición dada, y listo.\nsum(secuencia_dias \u0026lt;= 5) [1] 11 Obtenemos que entre el 13 de noviembre y el 30 de octubre de 2024 hay 11 días de semana (es decir, días entre lunes y viernes).\n","date":"2024-11-13T00:00:00Z","excerpt":"En esta guía explico cómo hacer un cálculo de diferencia entre fechas, o conteo entre de días entre dos fechas, ya sea entre días corridos o solamente considerando los días hábiles.","href":"https://bastianoleah.netlify.app/blog/contar_dias_habiles/","tags":"limpieza de datos ; procesamiento de datos ; fechas","title":"Contar días hábiles entre dos fechas en R"},{"content":"En este post dejo algunos de los paquetes y funciones que uso frecuentemente al momento de limpiar datos en R. Voy a ir actualizando este post por si se me van ocurriendo más.\nlibrary(janitor) library(stringr) library(dplyr) library(lubridate) Limpiar nombres de columnas janitor::clean_names() Como su nombre lo indica, esta función cambia los nombres de las variables o columnas para estandarizar la forma en que están escritas: todo en minúsculas, sin espacios, sin símbolos, y las palabras separadas con guión bajo. Esto facilita mucho el trabajo con datos, ya que minimiza las probabilidades de qué te equivoques al escribir el nombre de una columna. Personalmente siempre ocupo esta función al momento de cargar datos, sobre todo si estos datos vienen desde Excel. Lo único malo de clean_names() es que esta función también cambia las eñes por enes.\nCorregir tablas sin nombres de columnas janitor::row_to_names() Esta función soluciona un problema bastante común al cargar datos desde Excel, que es que las planillas de datos de Excel suelen venir con filas vacías arriba. Esto porque los usuarios de Excel les gusta darle un espaciado a las tablas, pero al momento de cargar esos datos, las filas vacías producen problemas.\nEliminar filas y columnas vacías janitor::remove_empty() Una función muy conveniente para limpiar datos, porque permite eliminar con una sola instrucción las filas vacías, o las columnas vacías. En Excel es común separar distintas tablas con filas o columnas vacías, o usar filas vacías o columnas vacías para hacer espacio entre los datos. De este modo, todas las columnas y celdas de nuestra tabla de datos contendrán información.\nFiltrar datos según un patrón de texto filter(stringr::str_detect()) La combinación de estas dos funciones permite filtrar las filas que contengan un texto parcial. Se puede usar para excluir filas que contengan un cierto patrón de texto si se le antepone un signo de exclamación\nBuscar y reemplazar texto stringr::str_replace() A esta función se le entrega una columna, y dos textos: el texto que queremos detectar, y después el texto que queremos reemplazar. De esta forma, podemos encontrar ciertos patrones de texto y reemplazarlos por otros, facilitando procesos de limpieza que involucren corregir cosas mal escritas, cambiar ciertos símbolos por otros, etc.\nCreación de variables co múltiples condicionales case_when() Es una función muy poderosa para poder crear nuevas variables en base al contenido de otras variables. Su potencial radica en la facilidad que te entrega para utilizar múltiples condiciones simultáneas para especificar las categorías de la nueva variable. Por ejemplo, si una cierta celda contiene un número, puedes hacer una operación. Pero si no contiene números, puedes hacer otra. Pero si el número tiene cierto largo puede ser algo distinto, etc.\nCorregir mayúsculas de un texto stringr::str_to_sentence() El uso de mayúsculas suele ser un problema al trabajar con datos, pero esta función, junto con sus funciones hermanas, facilitan transformar las mayúsculas de cualquier texto. Con str_to_sentence() se pone solamente la primera letra de una oración con mayúscula y el resto en minúsculas. Mientras str_to_upper() y str_to_lower() transforman todo a mayúsculas o todo a minúsculas, respectivamente.\nExtraer un patrón de texto stringr::str_extract() La extracción de texto resulta muy útil cuando la combinas con expresiones regulares (regex). Te permite, por ejemplo, extraer la primera palabra de una cadena de texto, o la última palabra. Te permite extraer distintas formas de escribir un mismo concepto. Te permite extraer textos que empiecen con algo y terminen con otro término, independiente del texto que haya entre ambos extremos.\nExtraer años desde un texto stringr::str_extract(\u0026#34;\\\\d{4}\u0026#34;) Una tarea común al limpiar datos es poder extraer el año que puede venir dentro de una columna de una fecha, de una variable de texto, o de un texto distinto que incluye alguna fecha dentro de sí. Con esta expresión de regex podemos extraer una cifra numérica de cuatro dígitos, que en la mayoría de los casos corresponde al año que queremos obtener.\nConvertir fechas lubridate::dmy() Una confusión común al trabajar con R ocurre cuando intentan nacer una operación sobre una columna que no permite ese tipo de operaciones. Como sumar números que en realidad son variables de formato texto. Lo mismo pasa con las fechas, que suelen ser expresadas en texto, pero que necesitan ser interpretadas como una fecha para poder realizar operaciones propias de una fecha, como calcular rangos de tiempo, sumar o restar días, extraer meses o años, saber el día de la semana, etc. Con esta función, es tan sencillo como indicar que la fecha viene expresada en día mes año, o si viene como año mes día, usar la variante ymd().\nRenombrar todas las columnas iris |\u0026gt; rename_with(~str_trunc(.x, 7, ellipsis = \u0026#34;\u0026#34;), everything()) ¿Tienes alguna otra función o paquete de R que uses regularmente para limpiar datos? Cuéntame en los comentarios 😊\n","date":"2024-11-13T00:00:00Z","excerpt":"Algunos de los paquetes y funciones que uso frecuentemente al momento de limpiar datos en R.","href":"https://bastianoleah.netlify.app/blog/limpieza_tips/","tags":"consejos ; limpieza de datos ; texto","title":"Tips para limpieza de datos en R"},{"content":"Este post ejemplifica tres formas de cargar y explorar los datos de la encuesta Casen 2022, la Encuesta de caracterización socioeconómica nacional.\nVeremos cómo obtener resultados de la Casen a nivel de país, región y comuna, usando dos formas de aplicar el factor de expansión. El factor de expansión es necesario de aplicar para transformar los resultados de la muestra de la encuesta a cifras que tienen representación a los distintos niveles de agrupación geográfica.\nPara seguir las instrucciones, primero debes descargar los datos originales de la encuesta desde este enlace (presionar Bases de datos) en formato Stata, junto con la base de datos complementaria de provincia y comuna. Crea un nuevo proyecto de RStudio, y dentro de la carpeta del proyecto ubica ambos archivos.\nLos archivos necesarios son:\nBase de datos Casen 2022 STATA (versión 18 de marzo 2024) Base de datos provincia y comuna Casen 2022 STATA Primero, cargamos los paquetes que usaremos a través de este tutorial:\nlibrary(haven) # carga datos formato Stata (.dta) library(dplyr) # manipulación de datos library(tidyr) # ordenamiento y limpieza de datos library(srvyr) # análisis de encuestas complejas, entre otros library(scales) # crear porcentajes a partir de proporciones Cargar datos de Casen Cargar base de datos principal de la encuesta Casen, en formato Stata (.dta):\ncasen \u0026lt;- read_dta(\u0026#34;Base de datos Casen 2022 STATA.dta\u0026#34;) Explorar los datos cargados:\ncasen |\u0026gt; select(1:20) |\u0026gt; glimpse() Rows: 202,231 Columns: 20 $ id_vivienda \u0026lt;dbl\u0026gt; 1000901, 1000901, 1000901, 1000902, 1000902, 1000902, 100… $ folio \u0026lt;dbl\u0026gt; 100090101, 100090101, 100090101, 100090201, 100090201, 10… $ id_persona \u0026lt;dbl\u0026gt; 1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 1, 2, 1, 1, 2, 3, 4, 1, 2, … $ region \u0026lt;dbl+lbl\u0026gt; 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 1… $ area \u0026lt;dbl+lbl\u0026gt; 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,… $ cod_upm \u0026lt;dbl\u0026gt; 10009, 10009, 10009, 10009, 10009, 10009, 10009, 10009, 1… $ nse \u0026lt;dbl+lbl\u0026gt; 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,… $ estrato \u0026lt;dbl\u0026gt; 1630324, 1630324, 1630324, 1630324, 1630324, 1630324, 163… $ hogar \u0026lt;dbl\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, … $ expr \u0026lt;dbl\u0026gt; 43, 43, 44, 51, 51, 52, 51, 42, 42, 42, 38, 38, 33, 56, 5… $ expr_osig \u0026lt;dbl\u0026gt; 54, NA, 122, NA, 131, NA, 44, 101, 47, NA, 81, 56, 43, 84… $ varstrat \u0026lt;dbl\u0026gt; 751, 751, 751, 751, 751, 751, 751, 751, 751, 751, 751, 75… $ varunit \u0026lt;dbl\u0026gt; 12041, 12041, 12041, 12041, 12041, 12041, 12041, 12041, 1… $ fecha_entrev \u0026lt;date\u0026gt; 2023-01-28, 2023-01-28, 2023-01-28, 2022-12-29, 2022-12-… $ p1 \u0026lt;dbl+lbl\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,… $ p2 \u0026lt;dbl+lbl\u0026gt; 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,… $ p3 \u0026lt;dbl+lbl\u0026gt; 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,… $ p4 \u0026lt;dbl+lbl\u0026gt; 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,… $ p9 \u0026lt;dbl\u0026gt; 3, 3, 3, 4, 4, 4, 4, 3, 3, 3, 2, 2, 1, 4, 4, 4, 4, 2, 2, … $ p10 \u0026lt;dbl+lbl\u0026gt; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, NA, … Notamos que las columnas que corresponden a preguntas de la encuesta vienen en formato \u0026lt;bls+lbl\u0026gt;, un formato propio del paquete {haven}, que combina los valores numéricos de las variables con las etiquetas de sus categorías de respuesta.\nCargar la base de datos complementaria que contiene columnas de factor de expansión, comuna y provincia:\ncasen_comunas \u0026lt;- read_dta(\u0026#34;Base de datos provincia y comuna Casen 2022 STATA.dta\u0026#34;) Revisar contenidos del segundo archivo:\ncasen_comunas # A tibble: 202,231 × 6 folio id_persona provincia comuna expp expc \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 100090101 1 163 [Punilla] 16303 [Ñiquén] 51 52 2 100090101 2 163 [Punilla] 16303 [Ñiquén] 50 53 3 100090101 3 163 [Punilla] 16303 [Ñiquén] 51 52 4 100090201 1 163 [Punilla] 16303 [Ñiquén] 51 52 5 100090201 2 163 [Punilla] 16303 [Ñiquén] 50 52 6 100090201 3 163 [Punilla] 16303 [Ñiquén] 51 52 7 100090201 4 163 [Punilla] 16303 [Ñiquén] 51 52 8 100090301 1 163 [Punilla] 16303 [Ñiquén] 51 52 9 100090301 2 163 [Punilla] 16303 [Ñiquén] 51 52 10 100090301 3 163 [Punilla] 16303 [Ñiquén] 51 52 # ℹ 202,221 more rows Ahora que tenemos cargadas la base de datos principal de la encuesta, y la base secundaria que contiene las columnas sobre datos comunales, debemos unir ambas a partir de las variables que identifican las observaciones únicas de la encuesta: folio e id_persona.\ncasen_2 \u0026lt;- left_join(casen, casen_comunas, by = join_by(folio, id_persona)) Obtenemos un nuevo dataframe que contiene las columnas de ambas bases. Debemos confirmar que la unión se realizó correctamente, comparando que la cantidad de filas de la base resultante sea igual a la de la base original.\nObtener frecuencias y porcentajes de variables de la Casen A modelo creativo, realizaremos tres formas distintas de obtener conteos de variables a partir de la encuesta. Con esto nos referimos, por ejemplo, a obtener la cantidad de casos bajo cada nivel de pobreza económica, calcular la frecuencia de cada nivel educacional, etc.\nConteo sin aplicar factor de expansión Primero realizaremos un conteo básico de los casos de una variable, sin realizar ninguna consideración metodológica respecto al muestreo de la encuesta. Es decir, solamente contar las filas de la base de datos. Esto lo haremos a pesar de ser incorrecto porque es la forma más básica de realizar la operación, y también para poder compararlo más adelante a las formas correctas de contar los casos en la encuesta que Casen.\nContar la frecuencia de una variable a nivel nacional, sin factor de expansión (inexacto):\ncasen_2 |\u0026gt; count(pobreza) |\u0026gt; mutate(p = round(n/sum(n), 2)) |\u0026gt; mutate(porcentaje = percent(p)) # A tibble: 4 × 4 pobreza n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1 [Pobreza extrema] 4657 0.02 2% 2 2 [Pobreza no extrema] 10616 0.05 5% 3 3 [No pobreza] 186838 0.92 92% 4 NA 120 0 0% Contar la frecuencia de una variable a nivel regional, sin factor de expansión (inexacto):\ncasen_2 |\u0026gt; group_by(region) |\u0026gt; count(pobreza) |\u0026gt; mutate(p = round(n/sum(n), 2)) |\u0026gt; mutate(porcentaje = percent(p)) # A tibble: 63 × 5 # Groups: region [16] region pobreza n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1 [Región de Tarapacá] 1 [Pobreza extrema] 300 0.03 3% 2 1 [Región de Tarapacá] 2 [Pobreza no extrema] 589 0.07 7% 3 1 [Región de Tarapacá] 3 [No pobreza] 7798 0.9 90% 4 1 [Región de Tarapacá] NA 4 0 0% 5 2 [Región de Antofagasta] 1 [Pobreza extrema] 265 0.03 3% 6 2 [Región de Antofagasta] 2 [Pobreza no extrema] 462 0.05 5% 7 2 [Región de Antofagasta] 3 [No pobreza] 8300 0.92 92% 8 2 [Región de Antofagasta] NA 2 0 0% 9 3 [Región de Atacama] 1 [Pobreza extrema] 250 0.03 3% 10 3 [Región de Atacama] 2 [Pobreza no extrema] 507 0.06 6% # ℹ 53 more rows Contar la frecuencia de una variable a nivel comunal, sin factor de expansión (inexacto):\ncasen_2 |\u0026gt; filter(region == 13) |\u0026gt; select(-region) |\u0026gt; group_by(comuna) |\u0026gt; count(pobreza) |\u0026gt; mutate(p = round(n/sum(n), 2)) |\u0026gt; mutate(porcentaje = percent(p)) # A tibble: 155 × 5 # Groups: comuna [52] comuna pobreza n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 13101 [Santiago] 1 [Pobreza extrema] 50 0.03 3% 2 13101 [Santiago] 2 [Pobreza no extrema] 58 0.03 3% 3 13101 [Santiago] 3 [No pobreza] 1685 0.94 94% 4 13102 [Cerrillos] 1 [Pobreza extrema] 17 0.04 4% 5 13102 [Cerrillos] 2 [Pobreza no extrema] 8 0.02 2% 6 13102 [Cerrillos] 3 [No pobreza] 376 0.94 94% 7 13103 [Cerro Navia] 1 [Pobreza extrema] 6 0.01 1% 8 13103 [Cerro Navia] 2 [Pobreza no extrema] 7 0.01 1% 9 13103 [Cerro Navia] 3 [No pobreza] 542 0.98 98% 10 13104 [Conchalí] 1 [Pobreza extrema] 21 0.04 4% # ℹ 145 more rows Conteo usando factor de expansión Para aplicar el factor de expansión a la base de datos, usamos las columnas expr y expc, que contienen el factor de expansión regional y comunal, respectivamente. En términos sencillos, esta cifra indica la cantidad de personas que debiese representar cada observación de la encuesta, si se pretende que la encuesta represente a la población real.\nPrimero seleccionamos las columnas de la base que utilizaremos:\ncasen_3 \u0026lt;- casen_2 |\u0026gt; select(region, comuna, pobreza, starts_with(\u0026#34;exp\u0026#34;)) Aplicaremos el factor de expansión usando la función uncount() del paquete {tidyr}, que básicamente multiplica las filas por el factor de expansión indicado en una variable. Por ejemplo, si una fila de la base tiene un factor de expansión 53, entonces al aplicar uncount(), dicha fila se repetirá 53 veces.\nEsto significa que nuestra base de datos aumentará considerablemente su tamaño, potencialmente alcanzando la cantidad de filas cercana a la población nacional. Esta puede ser una cantidad de datos demasiado grande para ciertos computadores, por lo que se recomienda primero filtrar por la región o comuna que se utilizará para el análisis.\ncasen_antofagasta \u0026lt;- casen_3 |\u0026gt; filter(region == 2) Realizar expansión de las filas de la base filtrada. Nótese la cantidad de observaciones que resulta de este procedimiento.\nlibrary(tidyr) casen_antofagasta_exp \u0026lt;- casen_antofagasta |\u0026gt; # aplicar factor de expansión uncount(expc) casen_antofagasta_exp # A tibble: 710,691 × 6 region comuna pobreza expr expr_osig expp \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 2 [Región de Antofagasta] 2201 [Calama] 3 [No pobreza] 67 81 68 2 2 [Región de Antofagasta] 2201 [Calama] 3 [No pobreza] 67 81 68 3 2 [Región de Antofagasta] 2201 [Calama] 3 [No pobreza] 67 81 68 4 2 [Región de Antofagasta] 2201 [Calama] 3 [No pobreza] 67 81 68 5 2 [Región de Antofagasta] 2201 [Calama] 3 [No pobreza] 67 81 68 6 2 [Región de Antofagasta] 2201 [Calama] 3 [No pobreza] 67 81 68 7 2 [Región de Antofagasta] 2201 [Calama] 3 [No pobreza] 67 81 68 8 2 [Región de Antofagasta] 2201 [Calama] 3 [No pobreza] 67 81 68 9 2 [Región de Antofagasta] 2201 [Calama] 3 [No pobreza] 67 81 68 10 2 [Región de Antofagasta] 2201 [Calama] 3 [No pobreza] 67 81 68 # ℹ 710,681 more rows Obtener frecuencia y porcentaje de una variable de la Casen, a nivel regional, aplicando factor de expansión:\ncasen_antofagasta_exp |\u0026gt; # contar variable count(pobreza) |\u0026gt; # calcular porcentaje mutate(p = n/sum(n)) |\u0026gt; mutate(porcentaje = percent(p)) # A tibble: 4 × 4 pobreza n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1 [Pobreza extrema] 19131 0.0269 2.7% 2 2 [Pobreza no extrema] 34907 0.0491 4.9% 3 3 [No pobreza] 656560 0.924 92.4% 4 NA 93 0.000131 0.0% Obtener frecuencia y porcentaje de una variable de la Casen, a nivel comunal, aplicando factor de expansión:\ncasen_antofagasta_exp |\u0026gt; # agrupar por comuna group_by(region, comuna) |\u0026gt; # contar variable count(pobreza) |\u0026gt; # calcular porcentaje mutate(p = n/sum(n)) |\u0026gt; mutate(porcentaje = percent(p)) # A tibble: 24 × 6 # Groups: region, comuna [8] region comuna pobreza n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+l\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 2 [Región de Antofagasta] 2101 [Antofagast… 1 [Pob… 12108 0.0276 2.8% 2 2 [Región de Antofagasta] 2101 [Antofagast… 2 [Pob… 18767 0.0428 4.3% 3 2 [Región de Antofagasta] 2101 [Antofagast… 3 [No … 408071 0.930 93.0% 4 2 [Región de Antofagasta] 2102 [Mejillones] 1 [Pob… 95 0.00613 0.6% 5 2 [Región de Antofagasta] 2102 [Mejillones] 2 [Pob… 449 0.0290 2.9% 6 2 [Región de Antofagasta] 2102 [Mejillones] 3 [No … 14955 0.965 96.5% 7 2 [Región de Antofagasta] 2103 [Sierra Gor… 1 [Pob… 39 0.0218 2.2% 8 2 [Región de Antofagasta] 2103 [Sierra Gor… 2 [Pob… 65 0.0364 3.6% 9 2 [Región de Antofagasta] 2103 [Sierra Gor… 3 [No … 1684 0.942 94.2% 10 2 [Región de Antofagasta] 2104 [Taltal] 1 [Pob… 1045 0.0752 7.5% # ℹ 14 more rows Obtener frecuencia y porcentaje de una variable creada a partir de la Casen, aplicando factor de expansión. Para este ejemplo, crearemos una nueva variable que unifique los dos niveles de pobreza en uno solo, generando una variable económica pobre/no pobre:\ncasen_antofagasta_exp |\u0026gt; # crear variable dicotómica mutate(pobreza_2 = ifelse(pobreza %in% c(1, 2), \u0026#34;pobre\u0026#34;, \u0026#34;no pobre\u0026#34;)) |\u0026gt; # conteo group_by(region, comuna) |\u0026gt; count(pobreza_2) |\u0026gt; # calcular porcentaje group_by(region, comuna) |\u0026gt; mutate(p = n/sum(n)) |\u0026gt; mutate(porcentaje = percent(p)) |\u0026gt; # filtrar filter(pobreza_2 == \u0026#34;pobre\u0026#34;) # A tibble: 7 × 6 # Groups: region, comuna [7] region comuna pobreza_2 n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 2 [Región de Antofagasta] 2101 [Antofagasta] pobre 30875 0.0703 7% 2 2 [Región de Antofagasta] 2102 [Mejillones] pobre 544 0.0351 4% 3 2 [Región de Antofagasta] 2103 [Sierra Gord… pobre 104 0.0582 6% 4 2 [Región de Antofagasta] 2104 [Taltal] pobre 2423 0.174 17% 5 2 [Región de Antofagasta] 2201 [Calama] pobre 15445 0.0793 8% 6 2 [Región de Antofagasta] 2203 [San Pedro d… pobre 394 0.0363 4% 7 2 [Región de Antofagasta] 2301 [Tocopilla] pobre 4253 0.150 15% Conteo de encuestas de muestreo complejo Usando el paquete {srvyr} podemos crear un objeto de diseño de encuestas complejas, que utilice variables acerca del diseño y muestreo de la Casen para poder calcular correctamente los estadísticos que necesitemos, incluyendo la aplicación del factor de expansión.\nEstablecer el diseño de encuestas complejas para la Casen:\nlibrary(srvyr) casen_svy \u0026lt;- casen_2 |\u0026gt; as_survey(weights = expr, strata = estrato, ids = id_persona, nest = TRUE) Luego, podemos utilizar este diseño para calcular frecuencias y porcentajes, entre otros, de la forma metodológicamente correcta.\nCalcular frecuencia y porcentaje de una variable a nivel país, usando diseño de encuestas complejas, con factor de expansión:\ncasen_svy |\u0026gt; group_by(pobreza) |\u0026gt; summarize(n = survey_total(), p = survey_mean()) |\u0026gt; # select(pobreza, n, p) |\u0026gt; mutate(porcentaje = percent(p, accuracy = 0.01)) # A tibble: 4 × 6 pobreza n n_se p p_se porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1 [Pobreza extrema] 397608 12243. 0.0200 0.000432 2.00% 2 2 [Pobreza no extrema] 894216 22302. 0.0450 0.000870 4.50% 3 3 [No pobreza] 18572834 573457. 0.934 0.00118 93.43% 4 NA 13915 1817. 0.000700 0.0000932 0.07% Notamos que éste método de calcular los estadísticos también nos ofrece los errores estándar de los conteos y los porcentajes.\nCalcular frecuencia y porcentaje de una variable a nivel regional, usando diseño de encuestas complejas, con factor de expansión:\ncasen_svy |\u0026gt; filter(region == 2) |\u0026gt; group_by(pobreza) |\u0026gt; summarize(n = survey_total(), p = survey_mean()) |\u0026gt; select(pobreza, n, p) |\u0026gt; mutate(porcentaje = percent(p, accuracy = 0.01)) # A tibble: 4 × 4 pobreza n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1 [Pobreza extrema] 19618 0.0276 2.76% 2 2 [Pobreza no extrema] 34593 0.0487 4.87% 3 3 [No pobreza] 656641 0.924 92.36% 4 NA 69 0.0000971 0.01% Calcular frecuencia y porcentaje de una variable a nivel comunal, usando diseño de encuestas complejas, con factor de expansión:\ncasen_svy |\u0026gt; filter(region == 13) |\u0026gt; group_by(comuna, pobreza) |\u0026gt; summarize(n = survey_total(), p = survey_mean()) |\u0026gt; select(pobreza, n, p) |\u0026gt; mutate(porcentaje = percent(p, accuracy = 0.01)) |\u0026gt; print(n = 15) Adding missing grouping variables: `comuna` # A tibble: 155 × 5 # Groups: comuna [52] comuna pobreza n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 13101 [Santiago] 1 [Pobreza extrema] 12501 0.0253 2.53% 2 13101 [Santiago] 2 [Pobreza no extrema] 17213 0.0348 3.48% 3 13101 [Santiago] 3 [No pobreza] 465004 0.940 93.99% 4 13102 [Cerrillos] 1 [Pobreza extrema] 3232 0.0406 4.06% 5 13102 [Cerrillos] 2 [Pobreza no extrema] 1336 0.0168 1.68% 6 13102 [Cerrillos] 3 [No pobreza] 75048 0.943 94.26% 7 13103 [Cerro Navia] 1 [Pobreza extrema] 1227 0.0116 1.16% 8 13103 [Cerro Navia] 2 [Pobreza no extrema] 1442 0.0137 1.37% 9 13103 [Cerro Navia] 3 [No pobreza] 102811 0.975 97.47% 10 13104 [Conchalí] 1 [Pobreza extrema] 4409 0.0395 3.95% 11 13104 [Conchalí] 2 [Pobreza no extrema] 3544 0.0318 3.18% 12 13104 [Conchalí] 3 [No pobreza] 103635 0.929 92.87% 13 13105 [El Bosque] 1 [Pobreza extrema] 3998 0.0225 2.25% 14 13105 [El Bosque] 2 [Pobreza no extrema] 8053 0.0454 4.54% 15 13105 [El Bosque] 3 [No pobreza] 165284 0.932 93.20% # ℹ 140 more rows Repetimos el ejemplo anterior, pero creando una variable dicotómica de pobreza para agrupar los dos niveles de pobreza:\ncasen_svy |\u0026gt; filter(region == 13) |\u0026gt; # crear variable dicotómica mutate(pobreza_2 = ifelse(pobreza %in% c(1, 2), \u0026#34;pobre\u0026#34;, \u0026#34;no pobre\u0026#34;)) |\u0026gt; # calcular group_by(comuna, pobreza_2) |\u0026gt; summarize(n = survey_total(), p = survey_mean()) |\u0026gt; select(pobreza_2, comuna, n, p) |\u0026gt; mutate(porcentaje = percent(p, accuracy = 0.01)) |\u0026gt; filter(pobreza_2 == \u0026#34;pobre\u0026#34;) |\u0026gt; print(n = 15) # A tibble: 52 × 5 # Groups: comuna [52] pobreza_2 comuna n p porcentaje \u0026lt;chr\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 pobre 13101 [Santiago] 29714 0.0601 6.01% 2 pobre 13102 [Cerrillos] 4568 0.0574 5.74% 3 pobre 13103 [Cerro Navia] 2669 0.0253 2.53% 4 pobre 13104 [Conchalí] 7953 0.0713 7.13% 5 pobre 13105 [El Bosque] 12051 0.0680 6.80% 6 pobre 13106 [Estación Central] 9481 0.0454 4.54% 7 pobre 13107 [Huechuraba] 3510 0.0322 3.22% 8 pobre 13108 [Independencia] 12977 0.0703 7.03% 9 pobre 13109 [La Cisterna] 8309 0.0651 6.51% 10 pobre 13110 [La Florida] 13691 0.0289 2.89% 11 pobre 13111 [La Granja] 9140 0.0793 7.93% 12 pobre 13112 [La Pintana] 32563 0.135 13.46% 13 pobre 13113 [La Reina] 2027 0.0159 1.59% 14 pobre 13114 [Las Condes] 1720 0.00574 0.57% 15 pobre 13115 [Lo Barnechea] 705 0.00738 0.74% # ℹ 37 more rows Realizar conteos obteniendo intervalos de confianza:\ncasen_svy |\u0026gt; filter(region == 2) |\u0026gt; group_by(region, comuna, pobreza) |\u0026gt; summarize(n = survey_total(), p = survey_mean(vartype = c(\u0026#34;se\u0026#34;, \u0026#34;ci\u0026#34;))) # A tibble: 24 × 9 # Groups: region, comuna [8] region comuna pobreza n n_se p p_se p_low p_upp \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+l\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 2 [Región d… 2101 [Ant… 1 [Pob… 12195 2.83e3 0.0280 0.00455 0.0190 0.0370 2 2 [Región d… 2101 [Ant… 2 [Pob… 18947 5.09e3 0.0435 0.00778 0.0281 0.0589 3 2 [Región d… 2101 [Ant… 3 [No … 404323 1.01e5 0.928 0.0120 0.905 0.952 4 2 [Región d… 2102 [Mej… 1 [Pob… 68 1.37e1 0.00485 0.00185 0.00119 0.00851 5 2 [Región d… 2102 [Mej… 2 [Pob… 409 1.18e2 0.0292 0.00679 0.0157 0.0426 6 2 [Región d… 2102 [Mej… 3 [No … 13552 4.51e3 0.966 0.00791 0.950 0.982 7 2 [Región d… 2103 [Sie… 1 [Pob… 22 1.04e1 0.0173 0.00400 0.00936 0.0252 8 2 [Región d… 2103 [Sie… 2 [Pob… 44 1.56e1 0.0346 0.00678 0.0212 0.0480 9 2 [Región d… 2103 [Sie… 3 [No … 1207 4.23e2 0.948 0.00761 0.933 0.963 10 2 [Región d… 2104 [Tal… 1 [Pob… 1239 2.45e2 0.0768 0.0169 0.0433 0.110 # ℹ 14 more rows Repetir el ejemplo, pero utilizando otra variable:\ncasen_svy |\u0026gt; filter(region == 2) |\u0026gt; group_by(region, comuna, v4) |\u0026gt; summarize(n = survey_total(), p = survey_mean(vartype = c(\u0026#34;se\u0026#34;, \u0026#34;ci\u0026#34;))) # A tibble: 43 × 9 # Groups: region, comuna [8] region comuna v4 n n_se p p_se p_low p_upp \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl+l\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 2 [Región … 2101 [Ant… 1 [1. … 80318 22990. 0.184 2.70e-2 1.31e-1 0.238 2 2 [Región … 2101 [Ant… 2 [2. … 321605 80216. 0.739 2.16e-2 6.96e-1 0.781 3 2 [Región … 2101 [Ant… 3 [3. … 3208 779. 0.00737 7.58e-4 5.87e-3 0.00887 4 2 [Región … 2101 [Ant… 4 [4. … 11973 2714. 0.0275 2.89e-3 2.18e-2 0.0332 5 2 [Región … 2101 [Ant… 5 [5. … 17714 5218. 0.0407 8.21e-3 2.44e-2 0.0569 6 2 [Región … 2101 [Ant… 6 [6. … 647 212. 0.00149 4.79e-4 5.37e-4 0.00243 7 2 [Región … 2102 [Mej… 1 [1. … 1160 203. 0.0827 1.61e-2 5.09e-2 0.114 8 2 [Región … 2102 [Mej… 2 [2. … 12285 4296. 0.876 2.17e-2 8.33e-1 0.919 9 2 [Región … 2102 [Mej… 5 [5. … 548 140. 0.0391 6.54e-3 2.61e-2 0.0520 10 2 [Región … 2102 [Mej… 6 [6. … 36 36 0.00257 2.68e-3 -2.73e-3 0.00787 # ℹ 33 more rows Comparación de resultados Luego de haber visto tres métodos para calcular frecuencias y porcentajes desde la encuesta que hacen utilizando factor de expansión, para cerrar compararemos los resultados de los tres métodos.\nConteo simple, sin expansión:\ncasen_2 |\u0026gt; filter(region == 5) |\u0026gt; count(pobreza) |\u0026gt; mutate(p = round(n/sum(n), 4)) |\u0026gt; mutate(porcentaje = percent(p)) # A tibble: 4 × 4 pobreza n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1 [Pobreza extrema] 386 0.0188 1.9% 2 2 [Pobreza no extrema] 958 0.0466 4.7% 3 3 [No pobreza] 19202 0.934 93.4% 4 NA 6 0.0003 0.0% Conteo y porcentaje con factor de expansión:\ncasen_2 |\u0026gt; filter(region == 5) |\u0026gt; uncount(expr) |\u0026gt; count(pobreza) |\u0026gt; mutate(p = round(n/sum(n), 4)) |\u0026gt; mutate(porcentaje = percent(p, accuracy = 0.001)) # A tibble: 4 × 4 pobreza n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;int\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1 [Pobreza extrema] 38850 0.0194 1.940% 2 2 [Pobreza no extrema] 92721 0.0463 4.630% 3 3 [No pobreza] 1869211 0.934 93.400% 4 NA 495 0.0002 0.020% Conteo y porcentaje con factor de expansión y diseño de encuesta compleja:\ncasen_svy |\u0026gt; filter(region == 5) |\u0026gt; group_by(pobreza) |\u0026gt; summarize(n = survey_total(), p = survey_mean()) |\u0026gt; select(pobreza, n, p) |\u0026gt; mutate(porcentaje = percent(p, accuracy = 0.001)) # A tibble: 4 × 4 pobreza n p porcentaje \u0026lt;dbl+lbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;chr\u0026gt; 1 1 [Pobreza extrema] 38850 0.0194 1.941% 2 2 [Pobreza no extrema] 92721 0.0463 4.633% 3 3 [No pobreza] 1869211 0.934 93.401% 4 NA 495 0.000247 0.025% Los resultados de los tres métodos difieren, especialmente los del primer método con los del segundo y tercero. El segundo y tercer método solo difieren en sus decimales, pero aún así es importante comparar ambos y tomar la decisión apropiada antes de realizar el análisis:\nEl primer método no debe usarse nunca, pues no aplica correctamente la metodología de la encuesta, y sus resultados difieren de la realidad. El segundo método, usando tidyr::uncount(), si bien puede parecer más conveniente y rápido de aplicar, obtiene resultados tiene un peor desempeño al obtener los resultados, y consume una muy alta cantidad de memoria para calcular resultados a nivel nacional, o incluso regiones o comunas de gran población. En computadores con menos de 24 GB de memoria, probablemente no pueda ser posible calcular estadísticos a nivel nacional sin realizar pasos intermediarios para disminuir el consumo de memoria (como separar el cálculo por regiones y luego realizar las sumas entre ellas). Entre los tres métodos, el más correcto de utilizar es el tercero, usando el paquete {srvyr}. Además de entregar los resultados con mayor eficiencia y velocidad, aplica correctamente la metodología de muestreo especificada por la encuesta, por lo que también es el método más exacto. Si este tutorial te sirvió, por favor considera hacerme una pequeña donación para poder tomarme un cafecito mientras escribo el siguiente tutorial 🥺\n","date":"2024-11-10T00:00:00Z","excerpt":"\u003cp\u003eEste post ejemplifica tres formas de cargar y explorar los datos de la encuesta Casen 2022, la \n\u003ca href=\"https://observatorio.ministeriodesarrollosocial.gob.cl/encuesta-casen-2022\" target=\"_blank\" rel=\"noopener\"\u003eEncuesta de caracterización socioeconómica nacional\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eVeremos cómo obtener resultados de la Casen a nivel de país, región y comuna, usando dos formas de aplicar el factor de expansión. El factor de expansión es necesario de aplicar para transformar los resultados de la muestra de la encuesta a cifras que tienen representación a los distintos niveles de agrupación geográfica.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/casen_introduccion/","tags":"dplyr ; Chile ; estadística ; ciencias sociales ; datos","title":"Cargar y explorar datos de la encuesta Casen en R, usando factor de expansión"},{"content":"Cuando trabajo con R, siempre intento dejar comentarios sobre de las cosas que estoy haciendo, tanto antes como después de cada bloque de código. Así, le hago un favor a mi yo del futuro, dejando una cierta documentación de las cosas que estuve haciendo, los objetivos que tenía, y otras aclaraciones sobre los procesos realizados.\nA esta combinación de bloques de código y párrafos de textos se le llama programación literaria, o literate programming.\nOtra buena forma de poder documentar tu código, que no sea dejando comentarios en el script, es programar directamente en un notebook o documento Quarto, que combina párrafos de texto enriquecido y titulares con bloques de código.\nSin embargo, yo no acostumbro mucho a empezar mis análisis desde un documento Quarto, sino que desde scripts de R. Entonces, cuando quise transformar mis script de R a documentos Quarto para poder publicarlos en este blog, me encontré con el fastidio de tener que convertir todos los comentarios a texto, y tener que introducir todos los bloques de código dentro de bloques (chunks).\nEl paquete {convertr} permite convertir scripts R a documentos Quarto, y documentos Quarto a scripts de R:\n# instalación devtools::install_github(\u0026#34;martinasladek/convertr\u0026#34;) convertr::r_to_qmd( input_dir = \u0026#34;path/to/some_R_script.R\u0026#34;, output_dir = \u0026#34;path/to/new_converted_qmd_file.qmd\u0026#34; ) Usando la función anterior, puedo convertir automáticamente mis scripts de R con comentarios a documentos Quarto, donde los comentarios sean párrafos de texto y cada bloque de código se ubica en un chunk individual. ¡Muy conveniente!\n","date":"2024-11-10T00:00:00Z","excerpt":"\u003cp\u003eCuando trabajo con R, siempre intento dejar comentarios sobre de las cosas que estoy haciendo, tanto antes como después de cada bloque de código. Así, le hago un favor a mi \u003cem\u003eyo\u003c/em\u003e del futuro, dejando una cierta documentación de las cosas que estuve haciendo, los objetivos que tenía, y otras aclaraciones sobre los procesos realizados.\u003c/p\u003e\n\u003cp\u003eA esta combinación de bloques de código y párrafos de textos se le llama \n\u003ca href=\"https://es.wikipedia.org/wiki/Programaci%c3%b3n_literaria\" target=\"_blank\" rel=\"noopener\"\u003eprogramación literaria\u003c/a\u003e, o \u003cem\u003eliterate programming.\u003c/em\u003e\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/convertr/","tags":"consejos ; Quarto","title":"Convertir un script de R con comentarios a un documento Quarto"},{"content":"Probando un poco de arte generativo en {ggplot2}. La idea era generar gráficos que parecieran nubes o humo.\nTomé el dataframe iris, configuré algunos elementos aleatorios en el gráfico, le agregué un efecto de desenfoque a los puntos, y luego hice un loop que genera 9 gráficos con parámetros aleatorios.\nlibrary(dplyr) library(glue) library(ggplot2) library(ggfx) library(purrr) Partimos con un gráfico base usando los datos de iris, donde los puntos crecen en base a una variable, y también aumentan su transparencia en la misma medida que aumentan su tamaño. De esta forma, los puntos más grandes son también menos visibles.\niris |\u0026gt; ggplot(aes(x = Sepal.Length, y = Sepal.Width, alpha = Petal.Length, size = Petal.Length, color = Sepal.Width)) + geom_point() + theme_void() + scale_size(range = c(2, 5)) + scale_alpha(range = c(.7, .1)) + scale_color_gradient2(low = \u0026#34;red\u0026#34;, mid = \u0026#34;purple\u0026#34;, high = \u0026#34;blue\u0026#34;, midpoint = mean(iris$Sepal.Width)) + theme(legend.position = \u0026#34;none\u0026#34;) + theme(plot.margin = margin(rep(2, 4))) + # margen interior del gráfico coord_cartesian(clip = \u0026#34;off\u0026#34;) + theme(plot.background = element_rect(fill = \u0026#34;#EAD1FA\u0026#34;, linewidth = 0)) Luego que tenemos un gráfico base, hacemos una prueba aplicando geom_jitter() a los puntos, que hace que la posición de los puntos sea siempre aleatoria, y cambiamos los rangos de tamaño y de transparencia:\niris |\u0026gt; ggplot(aes(x = Sepal.Length, y = Sepal.Width, alpha = Petal.Length, size = Petal.Length, color = Sepal.Width)) + geom_jitter(width = 1, height = 1) + theme_void() + scale_size(range = c(8, 70)) + scale_alpha(range = c(.1, .0)) + scale_color_gradient2(low = \u0026#34;red\u0026#34;, mid = \u0026#34;purple\u0026#34;, high = \u0026#34;blue\u0026#34;, midpoint = mean(iris$Sepal.Width)) + theme(legend.position = \u0026#34;none\u0026#34;) + theme(plot.margin = margin(rep(60, 4))) + # margen interior del gráfico coord_cartesian(clip = \u0026#34;off\u0026#34;) + theme(plot.background = element_rect(fill = \u0026#34;#EAD1FA\u0026#34;, linewidth = 0)) Ahora que tenemos un gráfico de movimiento aleatorio, agregamos a geom_jitter() la función with_blur() del paquete {ggfx}, que desenfoca el elemento al que se la apliquemos. Así creamos el efecto de humo o nube:\niris |\u0026gt; ggplot(aes(x = Sepal.Length, y = Sepal.Width, alpha = Petal.Length, size = Petal.Length, color = Sepal.Width)) + geom_jitter(width = 1, height = 1) |\u0026gt; with_blur(sigma = 7) + theme_void() + scale_size(range = c(8, 70)) + scale_alpha(range = c(.1, .0)) + scale_color_gradient2(low = \u0026#34;red\u0026#34;, mid = \u0026#34;purple\u0026#34;, high = \u0026#34;blue\u0026#34;, midpoint = mean(iris$Sepal.Width)) + theme(legend.position = \u0026#34;none\u0026#34;) + theme(plot.margin = margin(rep(60, 4))) + # margen interior del gráfico coord_cartesian(clip = \u0026#34;off\u0026#34;) + theme(plot.background = element_rect(fill = \u0026#34;#EAD1FA\u0026#34;, linewidth = 0)) Una vez que tenemos un gráfico interesante, le agregamos más parámetros aleatorios, y lo metemos dentro de purrr::map() para generar muchos gráficos de una sola vez:\nmap(1:9, ~{ # parámetros aleatorios jitter_x = sample(seq(0.7, 1.2, 0.1), 1) # 1 jitter_y = sample(seq(0.7, 1.2, 0.1), 1) # 1 tamaño_max = sample(seq(40, 80, 5), 1) # 60 tamaño_min = sample(seq(5, 20, 5), 1) # 10 centro_color = sample(iris$Sepal.Width, 1) # generar iris |\u0026gt; ggplot(aes(x = Sepal.Length, y = Sepal.Width, alpha = Petal.Length, size = Petal.Length, color = Sepal.Width)) + geom_jitter(width = jitter_x, height = jitter_y) |\u0026gt; with_blur(sigma = 9) + theme_void() + scale_size(range = c(tamaño_min, tamaño_max)) + scale_alpha(range = c(.07, .0)) + scale_color_gradient2(low = \u0026#34;red\u0026#34;, mid = \u0026#34;purple\u0026#34;, high = \u0026#34;blue\u0026#34;, midpoint = centro_color) + theme(legend.position = \u0026#34;none\u0026#34;) + theme(plot.margin = margin(rep(60, 4))) + # margen interior del gráfico coord_cartesian(clip = \u0026#34;off\u0026#34;) + theme(plot.background = element_rect(fill = \u0026#34;#EAD1FA\u0026#34;, linewidth = 0)) # guardar # ggsave(glue(\u0026#34;nubes/orbe_{sample(111:999, 1)}.png\u0026#34;), # width = 3, height = 3, scale = 2, dpi = 200) }) [[1]] [[2]] [[3]] [[4]] [[5]] [[6]] [[7]] [[8]] [[9]] Si queremos guardar los resultados a una carpeta, agregamos el siguiente código dentro de la iteración con map() para guardar los gráficos en una carpeta, dándole a los archivos nombres aleatorios para que no se sobreescriban.\n# guardar ggsave(glue(\u0026#34;nubes_{sample(111:999, 1)}.png\u0026#34;), width = 3, height = 3, scale = 2, dpi = 200) ","date":"2024-11-08T00:00:00Z","excerpt":"Probando un poco de arte generativo en `{ggplot2}`. La idea era generar gráficos que parecieran nubes o humo. \nTomé el dataframe `iris`, configuré algunos elementos aleatorios en el gráfico, le agregué un efecto de desenfoque a los puntos, y luego hice un loop que genera 9 gráficos con parámetros aleatorios.","href":"https://bastianoleah.netlify.app/blog/ggplot_nubes/","tags":"ggplot2 ; gráficos ; curiosidades ; arte generativo","title":"¿Arte? Nubes aleatorias en `{ggplot2}`"},{"content":"Éste es un post sobre este mismo blog. Quería compartir el proceso de creación del blog, porque me pareció interesante y entretenido.\nLo creé usando Hugo, un generador de sitios web estáticos de código abierto y gratuito. Hugo se puede usar a través de R usando {blogdown} y el tema Hugo Apéro, creado por Alison Hill, y así puedes crear páginas y publicaciones usando Quarto o RMarkdown. De ese modo resulta muy fácil integrar las cosas que hagas con R en tu sitio, compartiendo el código en los posts.\nLa guía para aprender a usar Hugo Apéro y dejar tu blog operacional es muy amigable y sencilla de seguir! Lo recomiendo, solo me tomó una tarde. Tiene amplia documentación y recursos en su repositorio. También existe el libro, Create, Publish, and Analyze Personal Websites Using R and RStudio, que detalla todas las instrucciones de crear un sitio web con R y Hugo.\nEl sitio web se genera con {blogdown} dentro de un proyecto de R cada vez que hago un cambio en el código del sitio. Luego, cuando subo los cambios del sitio a GitHub, Netlifly detecta los cambios, reconstruye el sitio y lo re-publica en minutos. De esta forma, el proceso es de despliegue continuo: cada cambio local que hago, al ser subido al repositorio remoto, gatilla la reconstrucción del sitio y su actualización en la versión pública del sitio web.\nSi bien Netlify te da un dominio .netlify.app, opté por un dominio .rbind.io en la página de Rbind, una comunidad que engloba sitios y blogs sobre R bajo un mismo dominio. Además, rbind.io se ve más lindo que netlify.app 😌\nLa gracia de un sitio estático es que su contenido online no cambia, porque es un sitio web normal, y por lo tanto es liviano de cargar y debiese ser muy barato o gratis de hostear (no requiere de un servidor con capacidad computacional, como es el caso con las aplicaciones Shiny).\nLa totalidad del código de este sitio web está disponible en el repositorio blog-r en mi GitHub.\nSobre Hugo Apéro Las instrucciones de Hugo Apéro me parecieron fáciles de seguir, lo suficientemente sencillas para que cualquier persona sin experiencia en sitios web (yo) pueda seguirlas. Además, te presentan ejemplos hermosos de otros blogs que usan el tema.\nLo bueno:\nMuy simple de usar Resultados agradables y profesionales con nada de código Todo es 100% gratuito: el generador del sitio (Hugo), la IDE donde hago el sitio (RStudio), el lenguaje que uso para construir el sitio (R), el hosting (Netlify), y el dominio (Rbind). Lo malo\nPoca flexibilidad No encuentro recursos para personalizar detalles Difícil de encontrar respuestas en internet Dificultades y soluciones Algunas dificultades que tuve con el blog, y cómo las resolví:\nCrear nuevas secciones El blog viene con secciones por defecto (blog, talks, about, projects), y me costó un poco entender cómo crear nuevas o cambiarles el nombre a las existentes. Luego entendí que las carpetas dentro de content/ se vuelven en secciones si las llamas desde el archivo de configuración config.yoml, y que adquieren una apariencia por defecto, pero puedes camibiarla en el front matter de cada una (el archivo _index.md dentro de content/blog, por ejemplo). Carpeta static Carpeta con los elementos estáticos que se copian a public y que van a estar disponibles para el resto del sitio, como algunas imágenes Carpeta public Al principio no entendía por qué tenía todo duplicado, pero luego entendí que public es una carpeta que se genera cada vez que actualizas el sitio, y su objetivo es ser literalmente el sitio al cual van a acceder las personas por su navegador web. Pero a esta carpeta sólo se le agregan cosas, no se eliminan, así que si subiste algo por error tienes que borrarlo manualmente de public (por ejemplo, yo subí una carpeta con 30 imágenes siendo que solo estaba usando una) Es seguro borrar todo el contenido de public, porque al guardar un cambio en tu sitio se re-escribe toda esta carpeta desde cero. Así evitas que queden recursos innecesarios en el sitio. Redirects Las redirecciones sirven para que, luego de que consigas una nueva url para tu sitio *.netlify.app, puedas hacer que la gente que entre a la dirección *.netlify.app sea redireccionada automáticamente a tu nueva url. En mi caso, resirigí desde bastianoleah.netlify.app a https://bastianolea.rbind.io. El dominio que usé para este sitio, rbind.io, es un servicio de la comunidad R de \u0026ldquo;unir\u0026rdquo; blogs y sitios webs de usuarios/as de R, y puedes pedir en su repositorio que te cedan un subdominio para que lo uses para tu blog. Es muy fácil especificar redirecciones creando un archivo llamado _redirects en static: link original y redirección. Así evitas que los usuarios entren sin https a tu sitio, y que si entran a una url interna o antigua se les lleve a la url nueva. Quarto documents para crear posts en tu blog Para crear un post a partir de un documento Quarto, solamente tienes que poner en el yaml del documento quarto format: hugo-md. De este modo, el documento se va a renderizar en formato markdown, y si se llama index.md, va a aparecer como un post. Usar Quarto te permite usar {shiny} para construir HTML para tu sitio (por ejemplo, usando div() y otras funciones de Shiny). En los chunks donde uses Shiny debes ponerles #| output: asis para que su resultado salga como HTML y se vea en el sitio. Cambiar textos del formulario de contacto El formulario de contacto aparece en inglés por defecto, y con texto rellenado en los campos (malo para la usabilidad), pero se puede cambiar directamente, modificando el archivo themes/hugo-apero/layouts/partials/shared/contact-form.html. Traducir elementos del sitio Hay varios elementos de texto del sitio que vienen por defecto en inglés, pero pueden ser traducidos si abres los archivos html y modificas \u0026lt;p\u0026gt;El texto dentro de los tags\u0026lt;/p\u0026gt;. Los archivos están en themes/hugo-apero/layouts/. Cambiar formatos de las fechas Hay que navegar los archivos del tema del sitio, en themes/hugo-apero/layouts/, y buscar los elementos que contienen la fecha así: {{ .PublishDate.Format \u0026quot;January 2, 2006\u0026quot; }}, y cambiarlos por el formato de fecha que prefieras, consideando que el día, mes y año deben ser los que salen en ese formato (2 de enero de 2006). Yo los dejé así: {{ .PublishDate.Format \u0026quot;2/1/2006\u0026quot; }} Modificar el css En la carpeta assets/scss/ puedes modificar los archivos .scss para alterar manualmente la apariencia de tu sitio. Personalmente cambié el archivo assets/scss/_code.scss para modificar la apariencia de los outputs de consola, para que tuvieran fondo oscuro y color distinto, con el siguiente código: .pre { overflow-x: auto; overflow-y: hidden; overflow: scroll; background-color: #232137 !important; color: #8A75A3 !important; border: none !important; border-radius: 6px !important; font-weight: 200 !important; font-size: 90% !important; } Crear un post nuevo Encontraba engorroso eso de crear un nuevo archivo index.md para cada post, porque tenía que copiar y pegar las etiquetas y código que van al inicio. Pero luego me di cuenta de que hay una función de {blogdown} que te crea posts nuevos: # crear un post blogdown::new_post(title = \u0026#34;Nubes aleatorias en ggplot\u0026#34;, subdir = \u0026#34;blog/\u0026#34;, file = \u0026#34;blog/ggplot_nubes/index.md\u0026#34;, # define el \u0026#34;slug\u0026#34;, la dirección url del post author = \u0026#34;Bastián Olea Herrera\u0026#34;, tags = c(\u0026#34;ggplot2\u0026#34;, \u0026#34;gráficos\u0026#34;, \u0026#34;curiosidades\u0026#34;) ) Hacer que se vean las etiquetas en el titular de cada post Si bien las etiquetas o tags de los posts aparecen al final de cada uno, yo quería que aparecieran debajo de la fecha, en el titular de cada publicación. Siguiendo este tutorial, encontré el archivo que hay que editar, y el código que era necesario agregar: En el archivo themes/hugo-apero/layouts/_default/single.html, que controla el diseño de todos los blog posts, agregar:\n{{ with .Params.tags }} \u0026lt;dl class=\u0026#34;f6 lh-copy\u0026#34;\u0026gt; \u0026lt;em\u0026gt;\u0026lt;dd class=\u0026#34;fw5 ml0\u0026#34;\u0026gt;Tags: {{ range . }} \u0026lt;a href=\u0026#34;{{ \u0026#34;tags/\u0026#34; | absURL }}{{ . | urlize }}\u0026#34;\u0026gt;{{ . }}\u0026lt;/a\u0026gt; {{ end }}\u0026lt;/dd\u0026gt;\u0026lt;/em\u0026gt; \u0026lt;/dl\u0026gt; {{ end }} Le agregué itálicas al texto y la palabra \u0026ldquo;Tags\u0026rdquo;, pero puedes poner lo que quieras.\nAhora lo que quiero es hacer que en la página de tags, que se crea en themes/hugo-apero/layouts/taxonomy/taxonomy.html aparezca el conteo de posts por etiqueta, y se cambia en themes/hugo-apero/layouts/partials/shared/summary.html, pero no me resulta 😔\nHacer que se vean las etiquetas de cada post en la lista de posts del blog Lo mismo que en el paso anterior, solamente que en el archivo themes/hugo-apero/layouts/partials/shared/summary.html, que es el que controla los \u0026ldquo;resúmenes\u0026rdquo; de cada post que aparecen en la lista de publicaciones del blog. En este caso le saqué la palabra \u0026ldquo;Tags\u0026rdquo;. Ordenar una serie de posts por peso, mientras el resto del blog se ordene por fecha Las publicaciones normales de mi blog se ordenan por fecha, con los más nuevos arriba, que es el orden por defecto. Pero en otras categorías del blog, como la serie de posts introductorios a R, quiero que se ordenen por peso; es decir, por una forma de ordenamiento arbitraria dado que las distintas publicaciones tienen un orden de lectura desde lo más básico a lo más avanzado, independiente de la fecha de publicación. Recordemos que una serie de posts no es más que una carpeta en content/blog dentro de la cual hay más posts; en este caso, una carpeta llamada r_introduccion donde están todas las publicaciones dentro de dicha serie, junto a un front matter que hace que todas esas publicaciones sean parte de la serie \u0026ldquo;Introducción a R\u0026rdquo;. Lo primero es entender cómo se construyen las listas de publicaciones en Hugo. Los archivos dentro de la carpeta themes/hugo-apero/layouts/ son los que definen la construcción de cada página del sitio. Entre ellas, en themes/hugo-apero/layouts/blog/, los archivos que empiezan con list define en la construcción de las listas de publicaciones. Cada lista usa un layout predefinido para construirse. Por ejemplo, y por defecto, en content/blog/_index.md se define que el blog se construye con layout: list-sidebar; es decir, el archivo themes/hugo-apero/layouts/list-sidebar.html construye la lista de publicaciones del blog. Comprendiendo lo anterior, si quiero tener una lista de publicaciones distinta, como la serie de posts introductorios a R ordenados por peso y no por fecha, tengo que crear un nuevo layout y definir el front matter (_index.md) de esa serie de publicaciones para que use el nuevo layout. En este layout, definimos que las publicaciones se ordenen por peso cambiando la siguiente línea: {{ $paginator := .Paginate (where .RegularPagesRecursive \u0026quot;Type\u0026quot; \u0026quot;blog\u0026quot;).ByWeight }}. Originalmente decía .ByDate.Reverse; es decir, por fecha con más nuevos arriba. En el front matter de la serie de posts definimos que el layout sea layout: list-sidebar-weight, para que use el ordenamiento distinto. Y listo, ahora tienes dos layouts para construir páginas de listas de posts, una ordenada por fecha y otra por peso, entonces el blog se ordena por fecha, pero la serie de posts que necesites puede ordenarse por peso si especificas que use el layout distinto al por defecto. Separar logos de redes sociales de los ítems del menu superior Encontraba que los logos de redes sociales estaban muy pegados a el menú de páginas superior del blog, porque cuando uno ponía el Mouse encima de un ítem, el subrayado del ítem se ponía encima de los logos de redes sociales. El archivo themes/hugo-apero/layouts/partials/header.html construye el header de todo el sitio, así que bastó con agregar un poco de css antes de {{ partial \u0026quot;shared/social-links.html\u0026quot; . }} para darle un poco más de espacio al rededor de los iconos y que se viera mejor. Crear shortcodes Los shortcodes son atajos que puedes usar para insertar elementos en las publicaciones de tu blog. Como me gusta que las imágenes aparezcan con esquinas redondeadas y centradas en la página, en vez de aplicar este estilo CSS a cada imagen manualmente, creé un shortcode que simplemente entrega la etiqueta de imagen HTML con el estilo deseado, sin necesidad de escribir HTML. El código en los posts queda así: {{\u0026lt; imagen \u0026quot;imagen.jpeg\u0026quot;\u0026gt;}}, y funciona porque en la carpeta layouts/shortcodes/ tengo un archivo HTML llamado igual que el shortcode (imagen.html), que contiene el siguiente código:\n\u0026lt;img src=\u0026#34;{{.Get 0}}\u0026#34; style=\u0026#34;border-radius: 7px; width: 80%; max-width: 700px; display: block; margin: auto; margin-bottom: 8px; margin-top: 8px;\u0026#34;\u0026gt; Donde {{.Get 0}} es el lugar donde se entrega el texto que se pone en el shortcode la ruta de la imagen). Entonces, crear shortcodes te permite reutilizar código para construir tu sitio; en este caso, el código para dar estilo a las imágenes, o por ejemplo para poner botones bonitos en el sitio. Lo bueno es que si actualizo el shortcode, en todas las páginas que lo usé se actualiza también.\nColumnas Saqué unos shortcodes para hacer columnas en los posts de Hugo desde este ejemplo, y las instrucciones de instalación están acá, que en realidad consisten en descargar el repo y copiar los archivos html a layouts/shortcodes/.\nConsejos Mantengo un archivo .R en el inicio del blog, donde tengo todos los comandos que uso regularmente para el sitio:\nblogdown::serve_site() # previsualizar sitio blogdown::stop_server() # detener previsualización blogdown::stop_server(); blogdown::serve_site() # reiniciar previsualización # crear un post nuevo blogdown::new_post(title = \u0026#34;Nubes aleatorias en ggplot\u0026#34;, subdir = \u0026#34;blog/\u0026#34;, file = \u0026#34;blog/ggplot_nubes/index.md\u0026#34;, author = \u0026#34;Bastián Olea Herrera\u0026#34;, tags = c(\u0026#34;ggplot2\u0026#34;, \u0026#34;gráficos\u0026#34;, \u0026#34;curiosidades\u0026#34;) ) # convertir script a Quarto convertr::r_to_qmd( input_dir = \u0026#34;~/R/servel_votaciones/pruebas/genero_candidatos_LLM.R\u0026#34;, output_dir = \u0026#34;~/R/blog/blog-r/content/blog/genero_nombres_llm/codigo.qmd\u0026#34; ) # ver sitio en github usethis::browse_github() Pendientes Igual me quedaron algunas cosas pendientes que no he sabido resolver 😞\nTraducir los meses, no encontré cómo cambiarlo 🙁 Conclusiones Quizás voy a escribir un tutorial más detallado sobre cómo hacer un blog.\nCreo que es importante tener un espacio en la web que sea realmente propio, sobre todo considerando que vivimos una época en que casi la totalidad de nuestra presencia online está a merced de grandes empresas (Meta con Instagram y Facebook, Microsoft con LinkedIn, Google con todo lo demás), que además operan en esquemas de mercantilización de nuestros datos. Un sitio web personal perdurará las decisiones o quiebres de las empresas en las que actualmente confiamos, y además nos permite volver un poco a la antigua web, donde las personas eran las dueñas del contenido, sin tener que conectar todo a trackers y plataformas privadas.\n","date":"2024-11-07T00:00:00Z","excerpt":"Post explicativo de cómo hice éste sitio web usando el tema Hugo Apéro. Describo los beneficios de Hugo, las dificultades que tuve para entender, y comentarios acerca de la importancia de tener un espacio web fuera de las grandes empresas que monopolizan la internet. La totalidad del código de este sitio web [está disponible en el repositorio `blog-r` en mi GitHub.](https://github.com/bastianolea/blog-r)","href":"https://bastianoleah.netlify.app/blog/hugo_blog_nuevo/","tags":"quarto ; blog","title":"Post sobre mi nuevo blog (este mismo)"},{"content":" ⚠️ Este post está en construcción ⚠️ En este tutorial encuentras instrucciones paso a paso para crear un droplet (servidor privado) en Digital Ocean, en el cual puedes subir aplicaciones Shiny, ejecutar RStudio, dejar automatizados procesos recurrentes de análisis de datos o web scraping, y más.\nÍndice Crear una cuenta Crear un proyecto Crear un droplet Configurar droplet Clonar una aplicación Configurar Shiny Server Instalar paquetes para Shiny Crear una cuenta Puedes crearte una cuenta con este enlace para obtener $200 de crédito por 60 días, y mi me llegan $25 😊\n(necesita medio de pago)\nCrear un proyecto Crear un droplet Buscar en el Marketplace la imagen RStudio\nElegir la configuración del droplet\nConfigurar droplet Crear usuario\nadduser usuario adduser usuarioprueba usermod -aG sudo usuarioprueba Para abrir el RStudio instalado en el Droplet: {IP}:8787\nClonar una aplicación En el RStudio del droplet Nuevo proyecto Proyecto desde control de versiones https://github.com/bastianolea/estimador_ingresos_trabajo.git O en la Terminal del droplet: git clone {url}\nConfigurar Shiny Server Enlazar la carpeta de la aplicación con la carpeta de shiny-server por medio de un enlace simbólico:\nsudo ln -s ~/miaplicacion /srv/shiny-server/ Instalar paquetes para Shiny Desde la consola de Digital Ocean:\nsudo su - shiny R install.packages(\u0026#34;...\u0026#34;) Editar configuración de Shiny\nsudo nano /etc/shiny-server/shiny-server.conf Dentro de este archivo, agregar estas opciones:\npreserve_logs true; sanitize_errors false; Guardar usando control+O, cerrar usando control+W.\n","date":"2024-11-06T00:00:00Z","excerpt":"En este tutorial encuentras instrucciones paso a paso para crear un droplet (servidor privado) en Digital Ocean, en el cual puedes subir aplicaciones Shiny, ejecutar RStudio, dejar automatizados procesos recurrentes de análisis de datos o web scraping, y más.","href":"https://bastianoleah.netlify.app/blog/tutorial_digitalocean/","tags":"Shiny","title":"Tutorial: publicar una app Shiny en Digital Ocean"},{"content":"Al programar algo, siempre existen varias formas de lograr un mismo objetivo. Un criterio para elegir una forma por sobre otra puede ser el rendimiento: si hay dos formas de hacer algo, elegir la forma que se ejecute más rápido.1\nPara comparar el rendimiento de distintas expresiones en R, realizamos un benchmark, al cual le entregamos las expresiones que queremos comparar, y nos entregará un detalle de su velocidad de ejecución, consumo de memoria, y otros.\nComo ejemplo, crearemos un dataframe de 100 millones de filas, con dos variables.\nlibrary(dplyr) # crear datos datos \u0026lt;- tibble(var1 = runif(n = 1e8), var2 = runif(n = 1e8)) datos # A tibble: 100,000,000 × 2 var1 var2 \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 0.0725 0.628 2 0.664 0.358 3 0.169 0.119 4 0.0369 0.879 5 0.708 0.546 6 0.607 0.835 7 0.955 0.839 8 0.198 0.197 9 0.564 0.456 10 0.788 0.960 # ℹ 99,999,990 more rows Pongámonos en el caso de que queremos filtrar este dataframe, y en consideración de su gran tamaño, queremos ver la forma más veloz de filtrarlo. Para ello, comparamos la evaluación de un filtro con {dplyr}, y otro con {base} (las funciones por defecto de R, que usualmente son más rápidas).\nUsamos la función bench::mark, a la cual le entregamos las expresiones con un nombre para distinguirlas, y le definimos los argumentos check = FALSE para que ignore diferencias en el resultado e iterations para especificar la cantidad de veces que queremos hacer las comparaciones (con más iteraciones nos aseguramos que el desempeño sea normal y no influenciado por factores externos de nuestro computador).\n# comparar ejecución bench::mark(check = FALSE, iterations = 5, \u0026#34;dplyr\u0026#34; = datos |\u0026gt; filter(var1 \u0026gt; var2), \u0026#34;base\u0026#34; = datos[datos$var1 \u0026gt; datos$var2, ] ) # A tibble: 2 × 6 expression min median `itr/sec` mem_alloc `gc/sec` \u0026lt;bch:expr\u0026gt; \u0026lt;bch:tm\u0026gt; \u0026lt;bch:tm\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;bch:byt\u0026gt; \u0026lt;dbl\u0026gt; 1 dplyr 453ms 467ms 2.13 2.42GB 2.98 2 base 331ms 339ms 2.92 1.3GB 1.17 En la comparación vemos que dplyr::filter() es aproximadamente 100 milisegundos más lento que un filtro realizado con {base}, pero además usa casi 1GB más de memoria. También entrega datos como las iteraciones por segundo, es decir, las veces que se ejecutaría cada operación por segundo. Todas estas métricas nos podrían ayudar a decidir una opción por sobre otra.\nmi opinión personal es que, entre valorar tiempo de ejecución (que algo corra más rápido) y tiempo de desarrollo (lo que te cuesta escribirlo/entenderlo), el tiempo de desarrollo (es decir, tu tiempo como humano o trabajador) es más valioso que tener a un procesador trabajando por más tiempo. En ese sentido, usualmente considero que es mejor la opción que sea más fácil de programar, y también más fácil de leer e interpretar. Sin embargo, hay casos donde la velocidad puede primar, como es el caso de aplicaciones interactivas (donde la espera se la traspasas a tus usuarios/as), o en cálculos con cantidades grandes de datos, donde cada centésima de segundo de ejecución se puede multiplicar por millones.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-11-05T00:00:00Z","excerpt":"Para comparar el rendimiento de distintas expresiones en R, realizamos un _benchmark,_ al cual le entregamos las expresiones que queremos comparar, y nos entregará un detalle de su velocidad de ejecución. Así podemos optar por una de las operaciones en base a su mejor rendimiento.","href":"https://bastianoleah.netlify.app/blog/benchmark_ejemplo/","tags":"consejos ; procesamiento de datos ; optimización","title":"Comparar el rendimiento expresiones en R"},{"content":"El Sistema de Información del Mercado Laboral (SIMEL) es una plataforma virtual desarrollada por las instituciones que componen la Mesa para la Coordinación de las Estadísticas del Trabajo 1 con el apoyo técnico de la Organización Internacional del Trabajo (OIT).\nEl SIMEL permite obtener información objetiva y actualizada sobre el mercado del trabajo, la que estará disponible para investigadores, tomadores de decisiones y la ciudadanía en general.\nRepositorio que permite descargar los datos estadísticos de SIMEL con un solo script, obteniendo cada conjunto de datos en archivos csv individuales.\nEl script simel_scraping.R ejecuta un controlador de Selenium que utiliza Firefox para navegar por el sitio y obtener los datos. Retorna los datos en csv en la carpeta datos/{categoria}/, y los metadatos en la carpeta metadatos.\nNo usa la API porque no entendí cómo usarla 🥲\n","date":"2024-11-01T00:00:00Z","excerpt":"Repositorio de código para obtener todos los datos de SIMEL de manera automática mediante web scraping, usando `{RSelenium}`.","href":"https://bastianoleah.netlify.app/blog/simel_scraping/","tags":"web scraping ; datos ; Chile ; ciencias sociales","title":"Scraping de datos del Sistema de Información del Mercado Laboral"},{"content":"\nCon motivo de las elecciones municipales, estuve generando algunas visualizaciones ”en tiempo real” de los resultados de las elecciones de alcaldías. Los datos de conteo de votos los fui obteniendo minuto a minuto mediante web scraping con {RSelenium}, que permite programar un navegador web para que interactúe con un sitio como si fuera humano. Entonces, el navegador robot (marioneta, le llaman) iba apretando todos los botones, sin intervención de mi parte, para encontrar y copiar los resultados de cada comuna del país.\nLos nuevos resultados llegaban con frecuencia, así que había que echar a correr el proceso bajo presión, cada 10 minutos aprox. Todo iba bien: presionaba ejecutar, el proceso pasaba por todas las comunas, limpiaba los datos y retornaba visualizaciones. Hasta que, en la mitad del conteo, el sitio del Servel cambió! Por algún motivo, cambiaron a una versión similar del sitio, pero que internamente funcionaba distinto, entonces se desconfiguró todo el web scraping. Tuve que luchar contra el tiempo para reestablecerlo (terrible para mi, porque estaba aprendiendo Selenium 😭). Lo otro que me jugó en contra fue que no se me ocurrió automatizar la redacción de textos y posteo en redes sociales, que al final fue lo que me quitó más tiempo 😒\nTambién faltó hacer visualizaciones más entretenidas, pero se hizo lo que se pudo para una idea que salió a la rápida. Es gratificante hacer andar un flujo largo de procesamiento de datos solo con un par de comandos, desde la obtención de los datos hasta que te arroja decenas o cientos de salidas ✨\nPara los nerds, usé {RSelenium} para un script que recibía un vector de comunas e iteraba por ellas con {furrr}, y retornaba una lista con la tabla de resultados, el párrafo de las mesas escrutadas, y el nombre de la comuna. Luego, otro script de R cargaba el scraping más reciente y limpiaba los datos, calculaba porcentajes, coincidía partidos con sectores políticos, corregía a \u0026ldquo;independientes\u0026rdquo; que en realidad tienen sector político claro, arreglaba nombres (Servel usa eñes pero no tildes, por alguna razón), interpretaba el texto de las mesas como cifras individuales, sumaba votos nulos y blancos, entre otras cosas. Después tenía un script desde el que comandaba todos los demás pasos, que para partir borraba todas las salidas antiguas, y según las comunas que le pedía, generaba nuevos gráficos/tablas en una carpeta nueva. Sobre los gráficos y tablas, nada interesante, salvo que el alto de los gráficos dependía de la cantidad de candidatos, para que siempre mantuvieran espaciados correctos y no se deformaran si eran muchos o muy pocos candidatos/as.\nFinalmente, el flujo de procesamientos de datos en R generó 238 gráficos y 240 tablas, de las cuales les comparto algunas. Esa fue mi experiencia intentando generar reportes en tiempo real sobre datos de elecciones. Para la siguiente votación espero tener algo más elaborado!\n","date":"2024-10-30T00:00:00Z","excerpt":"Con motivo de las elecciones municipales, estuve generando algunas visualizaciones ”en tiempo real” de los resultados de las elecciones de alcaldías. Los datos de conteo de votos los fui obteniendo minuto a minuto mediante web scraping con `{RSelenium}`, que permite programar un navegador web para que interactúe con un sitio como si fuera humano. Finalmente desarrollé un sistema que, con un solo comando, ejecutaba el scraping, la limpieza y procesamiento de los datos, y retornaba tablas y gráficos listos para compartir.","href":"https://bastianoleah.netlify.app/blog/elecciones_municipales_2024/","tags":"procesamiento de datos ; web scraping ; visualización de datos ; gráficos ; tablas ; datos ; Chile","title":"Visualización y scraping de resultados en vivo de las elecciones municipales 2024"},{"content":" Recientemente se lanzó el paquete {mall}, que facilita el uso de un LLM (large language model) o modelo de lenguaje de gran tamaño para analizar texto con IA en un dataframe. Esto significa que, para cualquier dataframe que tengamos, podemos aplicar un modelo de IA a una de sus columnas y recibir sus resultados en una columna nueva.\nPuedes encontrar instrucciones más detalladas sobre configurar el uso de IA en R en esta publicación. Para poder hacer ésto, primero necesitamos tener un modelo LLM instalado localmente en nuestra computadora. Para eso, tenemos que instalar Ollama, y ejecutar la aplicación. Ollama tiene que estar abierto para poder proveer del modelo a nuestra sesión de R.\nLuego, instalamos el paquete {ollamar} en R,, que es una dependencia de {mall}. Usamos {ollamar} para descargar a nuestro equipo el modelo de lenguaje que usaremos:\nlibrary(ollamar) ollamar::pull(\u0026#34;llama3.2:3b\u0026#34;) Con eso hecho, ya puedes usar modelo directamente desde R con {ollamar}, o en un dataframe usando {mall}.\nlibrary(mall) library(dplyr) Attaching package: 'dplyr' The following objects are masked from 'package:stats': filter, lag The following objects are masked from 'package:base': intersect, setdiff, setequal, union mall::llm_use(\u0026#34;ollama\u0026#34;, \u0026#34;llama3.2:3b\u0026#34;) ── mall session object Backend: ollama LLM session: model:llama3.2:3b R session: cache_folder:/var/folders/gt/vdp_nx_x3bq5wgnlr1nphkd1zbdps_/T//RtmpzqBMog/_mall_cachee7f20d2b28 Con el siguiente código vamos a descargar un dataframe que contiene texto de noticias de Chile, para usarlo como datos de prueba. Los datos provienen de mi repositorio de web scraping y análisis de prensa de Chile.\n# obtener datos de prensa url \u0026lt;- \u0026#34;https://raw.githubusercontent.com/bastianolea/prensa_chile/refs/heads/main/prensa_datos_muestra.csv\u0026#34; datos_prensa \u0026lt;- readr::read_csv2(url, show_col_types = FALSE) ℹ Using \u0026quot;','\u0026quot; as decimal and \u0026quot;'.'\u0026quot; as grouping mark. Use `read_delim()` for more control. head(datos_prensa) # A tibble: 6 × 4 titulo cuerpo fuente fecha \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;date\u0026gt; 1 Hombre de 66 años fue asesinado en plena vía pública… \u0026quot;El h… Coope… 2024-07-21 2 Revive el cuarto capítulo de Indecisos: Candidatos a… \u0026quot;Este… Megan… 2024-09-03 3 Menos personas circulando y miedo de los residentes:… \u0026quot;Poca… Megan… 2024-04-08 4 CEO de Walmart Chile sostiene que si la empresa no d… \u0026quot;Hace… The C… 2024-12-22 5 Pdte. Boric entregó mensaje por la muerte de Piñera … \u0026quot;El p… CNN C… 2024-02-06 6 Encuentran dos cadáveres en un canal de regadío en P… \u0026quot;Pers… Publi… 2024-07-13 Probemos {mall} con 10 noticias al azar, pidiéndole al LLM que detecte el sentimiento de cada texto (si es positivo, neutro o negativo):\n# extraer sentimiento de textos datos_sentimiento \u0026lt;- datos_prensa |\u0026gt; select(titulo) |\u0026gt; slice(10:20) |\u0026gt; llm_sentiment(titulo, pred_name = \u0026#34;sentimiento\u0026#34;, options = c(\u0026#34;positivo\u0026#34;, \u0026#34;neutro\u0026#34;, \u0026#34;negativo\u0026#34;)) datos_sentimiento |\u0026gt; relocate(sentimiento, .before = titulo) # A tibble: 11 × 2 sentimiento titulo \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 negativo \u0026quot;Dos personas resultan baleadas tras intentar evitar asalto en m… 2 negativo \u0026quot;Mujer de 54 años recibió trasplante de bomba cardíaca y riñón d… 3 negativo \u0026quot;Fiscalía investiga una segunda presunta agresión de Monsalve co… 4 negativo \u0026quot;Los RUT definitivos que reciben el premio de La Suerte en Chile… 5 negativo \u0026quot;Este domingo parte el Festival de Viña 2024: Revisa el orden y … 6 negativo \u0026quot;Danesa se coronó como Miss Universo 2024: Emilia Dides quedó en… 7 positivo \u0026quot;Hassler opta por tesis de Boric por sobre la del PC en Venezuel… 8 negativo \u0026quot;Carabinero de civil disparó a delincuente que estaba robando en… 9 negativo \u0026quot;Seguridad en días del 18’: Ex PDI llama a ser precavido y a evi… 10 positivo \u0026quot;¿Quién es Janet Yellen? La Secretaria del Tesoro de EEEUU que v… 11 negativo \u0026quot;Fijan audiencia para los detenidos por el homicidio del subofic… Otro uso es pedirle que genere resúmenes de textos. Para ello, usaremos un prompt manual, donde le pedimos explícitamente \u0026quot;resumir en hasta 5 palabras\u0026quot;. El paquete aplicará dicha solicitud a cada una de las observaciones en la columna indicada, y retornará los resultados en una nueva columna llamada resumen:\n# resumir textos datos_resumidos \u0026lt;- datos_prensa |\u0026gt; select(titulo, cuerpo) |\u0026gt; slice_sample(n = 10) |\u0026gt; mutate(resumen = llm_vec_custom( cuerpo, \u0026#34;resumir en hasta 7 palabras\u0026#34;)) |\u0026gt; select(resumen, titulo) datos_resumidos # A tibble: 10 × 2 resumen titulo \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 Lo siento, no tengo información disponible. Sernageomin decreta Alerta Amari… 2 Lo siento, no tengo información disponible. Caos y golpes durante llegada de… 3 Lo siento, no tengo información disponible. Delegada Martínez y evacuaciones… 4 Lo siento, no tengo información disponible. Incendio en Copenhague destruye … 5 Lo siento, no tengo información disponible. VIDEO| “No entienden que…”: Jorg… 6 Lo siento, no tengo información disponible. Alza en los planes podría genera… 7 Lo siento, no tengo información disponible. Exmilitar venezolano presuntamen… 8 Lo siento, no tengo información disponible. ¿Hay que adelantar o retrasar el… 9 Lo siento, no tengo información disponible. Familia Imschenetzky compra part… 10 Lo siento, no tengo información disponible. Feria de vinos llega a Providenc… ","date":"2024-10-29T00:00:00Z","excerpt":"Procesa datos con IA en R, localmente! El paquete `{mall}` permite aplicar un modelo de lenguaje (LLM) local a tus datos, para así crear nuevas columnas a partir de prompts, tales como resumir, extraer sentimiento, clasificación, y más.","href":"https://bastianoleah.netlify.app/blog/introduccion_llm_mall/","tags":"análisis de texto ; inteligencia artificial","title":"Usar un modelo de lenguaje local (LLM) para analizar texto en R"},{"content":"¿Sabías que R tiene un tipo de datos para números romanos? Yo tampoco.\nlibrary(dplyr) regiones \u0026lt;- tibble(region = c(\u0026#34;I Región de Tarapacá\u0026#34;, \u0026#34;II Región de Antofagasta\u0026#34;, \u0026#34;III Región de Atacama\u0026#34;, \u0026#34;IV Región de Coquimbo\u0026#34;, \u0026#34;IX Región de La Araucanía\u0026#34;, \u0026#34;V Región de Valparaíso\u0026#34;, \u0026#34;VI Región del Libertador General Bernardo O\u0026#39;Higgins\u0026#34;, \u0026#34;VII Región del Maule\u0026#34;, \u0026#34;VIII Región del Bío Bío\u0026#34;, \u0026#34;X Región de los Lagos\u0026#34;, \u0026#34;XI Región de Aysén del General Carlos Ibañez del Campo\u0026#34;, \u0026#34;XII Región de Magallanes y la Antártica Chilena\u0026#34;, \u0026#34;XIII Región Metropolitana\u0026#34;, \u0026#34;XIV Región Los Ríos\u0026#34;, \u0026#34;XV Región Arica y Parinacota\u0026#34;, \u0026#34;XVI Ñuble\u0026#34; )) Si extraemos la primera palabra de la variable region, obtenemos solamente el número romano.\nregiones_2 \u0026lt;- regiones |\u0026gt; mutate(romano = stringr::str_extract(region, \u0026#34;\\\\w+\u0026#34;)) regiones_2 # A tibble: 16 × 2 region romano \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; 1 I Región de Tarapacá I 2 II Región de Antofagasta II 3 III Región de Atacama III 4 IV Región de Coquimbo IV 5 IX Región de La Araucanía IX 6 V Región de Valparaíso V 7 VI Región del Libertador General Bernardo O'Higgins VI 8 VII Región del Maule VII 9 VIII Región del Bío Bío VIII 10 X Región de los Lagos X 11 XI Región de Aysén del General Carlos Ibañez del Campo XI 12 XII Región de Magallanes y la Antártica Chilena XII 13 XIII Región Metropolitana XIII 14 XIV Región Los Ríos XIV 15 XV Región Arica y Parinacota XV 16 XVI Ñuble XVI Luego, convertimos le damos al número romano la clase apropiada:\nregiones_3 \u0026lt;- regiones_2 |\u0026gt; mutate(romano = as.roman(romano)) |\u0026gt; relocate(romano, .before = 1) regiones_3 # A tibble: 16 × 2 romano region \u0026lt;roman\u0026gt; \u0026lt;chr\u0026gt; 1 I I Región de Tarapacá 2 II II Región de Antofagasta 3 III III Región de Atacama 4 IV IV Región de Coquimbo 5 IX IX Región de La Araucanía 6 V V Región de Valparaíso 7 VI VI Región del Libertador General Bernardo O'Higgins 8 VII VII Región del Maule 9 VIII VIII Región del Bío Bío 10 X X Región de los Lagos 11 XI XI Región de Aysén del General Carlos Ibañez del Campo 12 XII XII Región de Magallanes y la Antártica Chilena 13 XIII XIII Región Metropolitana 14 XIV XIV Región Los Ríos 15 XV XV Región Arica y Parinacota 16 XVI XVI Ñuble class(regiones_3$romano) [1] \u0026quot;roman\u0026quot; Finalmente, vemos cómo resulta posible convertir desde números romanos a números arábigos; es decir, a la clase numeric en R.\nregiones_2 |\u0026gt; mutate(numero = as.numeric(romano)) |\u0026gt; relocate(numero, .after = romano) Warning: There was 1 warning in `mutate()`. ℹ In argument: `numero = as.numeric(romano)`. Caused by warning: ! NAs introduced by coercion # A tibble: 16 × 3 region romano numero \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; 1 I Región de Tarapacá I NA 2 II Región de Antofagasta II NA 3 III Región de Atacama III NA 4 IV Región de Coquimbo IV NA 5 IX Región de La Araucanía IX NA 6 V Región de Valparaíso V NA 7 VI Región del Libertador General Bernardo O'Higgins VI NA 8 VII Región del Maule VII NA 9 VIII Región del Bío Bío VIII NA 10 X Región de los Lagos X NA 11 XI Región de Aysén del General Carlos Ibañez del Campo XI NA 12 XII Región de Magallanes y la Antártica Chilena XII NA 13 XIII Región Metropolitana XIII NA 14 XIV Región Los Ríos XIV NA 15 XV Región Arica y Parinacota XV NA 16 XVI Ñuble XVI NA ","date":"2024-09-25T00:00:00Z","excerpt":"¿Sabías que R tiene un tipo de datos para números romanos? Yo tampoco","href":"https://bastianoleah.netlify.app/blog/numeros_romanos/","tags":"curiosidades","title":"Números romanos en R"},{"content":"Usando el paquete {purrr} para realizar iteraciones o loops en nuestro código, vamos a probar tres escenarios ante los que un loop se detendría debido a un error, y formas de solucionarlo.\nlibrary(purrr) números \u0026lt;- list(1, 2, 3, \u0026#34;error\u0026#34;, 5) En el primer escenario, el loop intenta sumar 100 a un vector de números, pero se encuentra con que uno de los números es un texto, por lo tanto la ejecución se detiene con un error:\n# loop que se detiene por un error map(números, \\(número) { message(\u0026#34;sumando \u0026#34;, número) número + 100 }) sumando 1 sumando 2 sumando 3 sumando error Error in `map()`: ℹ In index: 4. Caused by error in `número + 100`: ! non-numeric argument to binary operator En el segundo escenario, usamos try() para que, si ocurre un error, la ejecución no se detenga:\n# loop que se ejecuta a pesar del error, # pero introduce datos perdidos map(números, \\(número) { message(\u0026#34;sumando \u0026#34;, número, \u0026#34; + 100\u0026#34;) try(número + 100) |\u0026gt; as.numeric() }) |\u0026gt; unlist() sumando 1 + 100 sumando 2 + 100 sumando 3 + 100 sumando error + 100 Error in número + 100 : non-numeric argument to binary operator Warning in .f(.x[[i]], ...): NAs introduced by coercion sumando 5 + 100 [1] 101 102 103 NA 105 En el tercer escenario, usamos tryCatch(), que nos permite realizar una operación distinta cuando se detecta un error. En este caso, enviar un mensaje personalizado, y retornar un valor distinto en caso de error.\n# loop que se ejecuta a pesar del error, que emite mensaje, # y que retorna algo distinto map(números, \\(número) { message(\u0026#34;sumando \u0026#34;, número, \u0026#34; + 100\u0026#34;) tryCatch(número + 100, error = \\(e) { message(\u0026#34;!!! upsi dupsi en \u0026#39;\u0026#34;, número, \u0026#34;\u0026#39;\u0026#34;) return(NULL) }) }) |\u0026gt; unlist() sumando 1 + 100 sumando 2 + 100 sumando 3 + 100 sumando error + 100 !!! upsi dupsi en 'error' sumando 5 + 100 [1] 101 102 103 105 ","date":"2024-09-13T00:00:00Z","excerpt":"¿Tienes que hacer un loop, pero se detiene porque hay un error en uno de los pasos? Usa `try()` para que la ejecución no se detenga, o `tryCatch()` para atrapar el error y devolver algo distinto, como un mensaje y un return(NULL) para que no afecte el resultado.","href":"https://bastianoleah.netlify.app/blog/evitar_errores/","tags":"procesamiento de datos ; loops ; consejos","title":"Evitar que un loop se detenga debido a errores"},{"content":" Índice Cargar paquetes Obtener mapa de país y de región Obtener datos geográficos desde Open Street Map Visualizar mapa con calles En este tutorial crearemos un mapa de una región de Chile, y sobre el polígono geográfico aplicaremos otros elementos geográficos como calles, avenidas y carreteras, obtenidos desde Open Street Map (proveedor de mapas online abierto y comunitario).\nEsto puede servir para crear visualizaciones de datos espaciales minimalistas que de todos modos entreguen elementos urbanos de referencia para que les usuaries puedan ubicarse mejor espacialmente.\nUsaremos {dplyr} para manipular los datos, el paquete {ggplot2} para visualización de datos, {sf} para tratamiento de elementos espaciales, {rnaturalearth} para obtener mapas de cualquier país o región del mundo, y {osmdata} para obtener datos estaciales desde Open Street Map (OSM) por medio de su API pública.\nCargar paquetes library(dplyr) #manipulación de datos library(ggplot2) #gráficos library(sf) #elementos espaciales library(rnaturalearth) #mapas del mundo library(osmdata) #datos open street map Obtener mapa de país y de región El primer paso es obtener un mapa de Chile, a nivel de regiones, usando la función ne_states() del paquete {rnaturalearth}. Luego filtramos el dataframe resultante para enfocarnos en una sola región del país.\n# obtener mapa pero a nivel de regiones mapa_1 \u0026lt;- rnaturalearth::ne_states(country = \u0026#34;Chile\u0026#34;) |\u0026gt; select(name, name_es, iso_3166_2, code_hasc, geometry) mapa_2 \u0026lt;- mapa_1 |\u0026gt; filter(name == \u0026#34;Los Lagos\u0026#34;) mapa_2 Simple feature collection with 1 feature and 4 fields Geometry type: MULTIPOLYGON Dimension: XY Bounding box: xmin: -74.40722 ymin: -44.045 xmax: -71.65926 ymax: -40.27304 Geodetic CRS: WGS 84 name name_es iso_3166_2 code_hasc geometry 1 Los Lagos Los Lagos CL-LL CL.LG MULTIPOLYGON (((-71.76926 -... Previsualizamos la región obtenida usando {ggplot2}:\nmapa_2 |\u0026gt; ggplot() + geom_sf(linewidth = .4, color = \u0026#34;grey80\u0026#34;, fill = \u0026#34;grey95\u0026#34;) + theme_classic() Luego realizamos el primer paso para obtener los datos desde Open Street Map, que es definir las coordenadas de la caja delimitadora o bounding box del lugar del que vamos a solicitar datos. Por ejemplo, una ciudad específica. Si bien esto se puede hacer con la función osmdata::getbb(), los resultados no siempre coinciden con lo que esperamos (la región o ciudad puede salir más recortada de lo esperado), por lo que preferimos usar directamente la caja del mapa que queremos visualizar, usando sf::st_bbox().\n# obtener perímetro o área abarcada por el mapa caja \u0026lt;- mapa_2 |\u0026gt; st_bbox() # opcionalmente, se podría hacer con el nombre del lugar # area \u0026lt;- getbb(\u0026#34;Región de Los Lagos, Chile\u0026#34;) caja xmin ymin xmax ymax -74.40722 -44.04500 -71.65926 -40.27304 Obtener datos geográficos desde Open Street Map Teniendo estas coordenadas, se solicitan los datos haciendo un query a la API Overpass de OSM. En esta solicitud se especifica la característica del mapa que se va a obtener, y sus valores; en este caso, queremos obtener las calles de una región, por lo que la feature corresponde a highway. Dividimos la query en varias partes, para tener en objetos separados las calles de distintos tamaños (a grandes rasgos: autopistas y avenidas, calles secundarias, y calles interiores).\nConsiderar que cada query puede tomar un tiempo, dependiendo de la cantidad de datos, y que OSM es un proyecto soportado por voluntarios, por lo que se recomienda no abusar de la API (hacer la mínima cantidad de solicitudes, no hacer muchas solicitudes seguidas, etc.)\n# obtener calles dentro de la caja calles_grandes \u0026lt;- caja |\u0026gt; opq(timeout = 500) |\u0026gt; add_osm_feature(key = \u0026#34;highway\u0026#34;, value = c(\u0026#34;motorway\u0026#34;, \u0026#34;primary\u0026#34;, \u0026#34;motorway_link\u0026#34;, \u0026#34;primary_link\u0026#34;)) |\u0026gt; osmdata_sf() calles_medianas \u0026lt;- caja |\u0026gt; opq(timeout = 500) |\u0026gt; add_osm_feature(key = \u0026#34;highway\u0026#34;, value = c(\u0026#34;secondary\u0026#34;, \u0026#34;tertiary\u0026#34;, \u0026#34;secondary_link\u0026#34;, \u0026#34;tertiary_link\u0026#34;)) |\u0026gt; osmdata_sf() calles_chicas \u0026lt;- caja |\u0026gt; opq(timeout = 500) |\u0026gt; add_osm_feature(key = \u0026#34;highway\u0026#34;, value = c(\u0026#34;residential\u0026#34;, \u0026#34;living_street\u0026#34;)) |\u0026gt; osmdata_sf() calles_grandes Object of class 'osmdata' with: $bbox : -44.0449981164699,-74.4072159499999,-40.2730445289999,-71.6592646899999 $overpass_call : The call submitted to the overpass API $meta : metadata including timestamp and version numbers $osm_points : 'sf' Simple Features Collection with 29125 points $osm_lines : 'sf' Simple Features Collection with 3270 linestrings $osm_polygons : 'sf' Simple Features Collection with 0 polygons $osm_multilines : NULL $osm_multipolygons : NULL Habiendo obtenido los datos geográficos desde OSM, recibimos objetos que contienen los elementos espaciales. Extraemos las líneas de cada uno de los objetos, para quedarnos sólo con las calles. Opcionalmente, podemos recortar los elementos espaciales obtenidos con la figura del mapa, usando el mapa como figura de recorte para que las líneas de las calles no se salgan de los márgenes del mapa.\n# extraer toda la geometría calles_grandes_a \u0026lt;- calles_grandes$osm_lines calles_medianas_a \u0026lt;- calles_medianas$osm_lines calles_chicas_a \u0026lt;- calles_chicas$osm_lines # recortar geometría para que calce dentro del polígono del mapa calles_grandes_b \u0026lt;- st_intersection(calles_grandes$osm_lines, mapa_2) calles_medianas_b \u0026lt;- st_intersection(calles_medianas$osm_lines, mapa_2) calles_chicas_b \u0026lt;- st_intersection(calles_chicas$osm_lines, mapa_2) Visualizar mapa con calles Finalmente, podemos aplicar las capas obtenidas a nuestro gráfico de {ggplot2}. Podemos ajustar la apariencia de cada tipo de calle por separado dado que las ubicamos en capas distintas.\nggplot() + # capa del país entero (como fondo) geom_sf(data = mapa_1, linewidth = .4, color = \u0026#34;grey95\u0026#34;, fill = \u0026#34;grey98\u0026#34;) + # capa del mapa de la región geom_sf(data = mapa_2, linewidth = .4, color = \u0026#34;grey80\u0026#34;, fill = \u0026#34;grey95\u0026#34;) + # capas osm geom_sf(data = calles_medianas_b, linewidth = .3, alpha = .3) + geom_sf(data = calles_grandes_b, linewidth = .4, alpha = .3) + # recortar vista coord_sf(xlim = caja[c(1, 3)], ylim = c(-42.4, -40.3)) + theme_classic() Otro ejemplo del mapa visto desde más cerca, incluyendo la capa de calles interiores:\nggplot() + # capa del mapa geom_sf(data = mapa_2, fill = \u0026#34;grey95\u0026#34;) + # capas osm geom_sf(data = calles_grandes_b, linewidth = .3, alpha = 1) + geom_sf(data = calles_medianas_b, linewidth = .3, alpha = .4) + geom_sf(data = calles_chicas_b, linewidth = .2, alpha = .2) + # recortar vista coord_sf(xlim = c(-73.18, -73.09), ylim = c(-40.6, -40.55)) + theme_classic() ","date":"2024-09-03T00:00:00Z","excerpt":"\u003cdiv style = \"margin-left: -16px;\"\u003e\n  \n  \u003cdetails closed id=\"PageTableOfContents\"\u003e\n    \u003csummary\u003e\n      \u003ch2 class=\"mv0 f5 fw7 ttu tracked dib\" style = \"margin-left: 6px; font-size: 120%;\"\u003eÍndice\u003c/h2\u003e\n      \u003c/summary\u003e\n    \u003cdiv class=\"pl2 pr0 mh0\" style = \"font-size: 90%; margin-top: -8px; margin-left: 16px; margin-bottom: 32px;\"\u003e\n    \u003cnav id=\"TableOfContents\"\u003e\n  \u003cul\u003e\n    \u003cli\u003e\n      \u003cul\u003e\n        \u003cli\u003e\u003ca href=\"#cargar-paquetes\"\u003eCargar paquetes\u003c/a\u003e\u003c/li\u003e\n      \u003c/ul\u003e\n    \u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#obtener-mapa-de-país-y-de-región\"\u003eObtener mapa de país y de región\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#obtener-datos-geográficos-desde-open-street-map\"\u003eObtener datos geográficos desde Open Street Map\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#visualizar-mapa-con-calles\"\u003eVisualizar mapa con calles\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/nav\u003e\n    \u003c/div\u003e\n  \u003c/details\u003e\n  \n\u003c/div\u003e\n\u003cp\u003eEn este tutorial crearemos un mapa de una región de Chile, y sobre el polígono geográfico aplicaremos otros elementos geográficos como calles, avenidas y carreteras, obtenidos desde Open Street Map (proveedor de mapas online abierto y comunitario).\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/tutorial_mapas_osm/","tags":"mapas ; Chile","title":"Tutorial: Mapa en {ggplot2} con calles desde Open Street Map en R"},{"content":"Proyecto de ciencia de datos desarrollado en R para analizar texto de noticias chilenas. Comprende módulos para realizar web scraping de sitios web de prensa para obtener el texto de sus noticias, procesos para transformar ese texto en palabras (tokens), y procesos para analizar dicho corpus de palabras usando distintas técnicas estadísticas.\nActualmente, el corpus de noticias obtenido supera las 800.000 noticias individuales, las cuales suman un total de 140 millones (!) de palabras, abarcando más de 30 fuentes periodísticas distintas.\nAplicaciones web Análisis de prensa chilena por semanas Aplicación web desarrollada en R que muestra varios gráficos que cuantifican el contenido de las noticias de prensa escrita de Chile. Los gráficos permiten identificar qué palabras son las más usadas a través del tiempo, lo cual a su vez permite ver cómo va variando el acontecer nacional. Esta aplicación es una muestra de la utilidad del análisis de texto para resumir gran cantidad de información por medio de visualizaciones interactivas.\nAccede a la aplicación en este enlace\nDatos Los datos obtenidos mediante este repositorio van sumando más de 800 mil noticias. Los datos, por su peso, no están disponibles de forma pública. Sin embargo, puedes acceder a una muestra de 3.000 noticias del año 2024, seleccionadas al azar, en la carpeta datos de este repositorio. Esta seleccion de datos contiene texto de titulares, cuerpo de noticia, fuente y fecha.\nEstructura del código El script prensa_procesar.R es un orquestador desde el que se pueden realizar todos los pasos de obtención, procesamiento y análisis de datos. Muchos de estos procesos son altamente demandantes en términos de hardware, dado que estamos trabajando con bases de datos de cientos de miles de observaciones con columnas que contienen miles de palabras por cada fila, o bien, tablas con cientos de millones de filas que contienen los términos de todo el corpus. Por lo anterior, todos estos procesos han sido optimizados para usar las funciones más eficientes (luego de realizar varios benchmarks), reducir el costo de memoria de las operaciones (dado que la magnitud de los datos es imposible de mantener en memoria en ordenadores comunes), y aprovechar al máximo el procesamiento paralelo en múltiples núcleos de CPU.\nScraping prensa_scraping.R ejecuta múltiples procesos simultáneos (usando background jobs de RStudio), que corresponden a scripts que realizan scraping de un sitio web en específico, y guarda sus resultados en un archivo individual en resultados/{modulo}/. Se puede ejecutar diariamente para mantener actualizadas las fuentes de datos con las noticias recientes. El web scraping se realiza usando los paquetes {rvest} y {polite}, los cuales en conjunción realizan un scraping \u0026ldquo;ético\u0026rdquo;, en el sentido de que realizan solicitudes de datos eficientes y respetando los tiempos de carga de los servidores, con el objetivo de no sobrecargarlos y respetar sus políticas sobre obtención de datos. En la carpeta modulos se encuentran decenas de scripts individuales, uno por cada medio de comunicación o fuente de prensa, cada uno de los cuales realiza scraping de las noticias de dicho medio y las guarda. Actualización: los scripts de scraping fueron removidos de este repositorio por razones laborales y de seguridad. En su lugar, dejé un script de ejemplo, que puede servir para entender cómo se integran scripts de scraping a este sistema. Este script permite hacer web scraping de un sitio de noticias nacional, y puedes basarte en él para obtener datos de otros sitios. Si estás realizando un proyecto de código abierto o no remunerado y requieres acceder a todos los módulos de web scraping, por favor contactarme directamente.\nEs posible realizar el scraping de uno de los medios ejecutando estos scripts individualmente, o bien, usarlos todos de forma simultánea y paralela (todos al mismo tiempo) ejecutando las funciones del script prensa_scraping.R. Por ejemplo, para scrapear las noticias del diario CNN Chile, se cargan las librerías del script prensa_scraping.R y se cargan las funciones del proyecto con source(\u0026quot;funciones.R\u0026quot;), para así poder ejecutar la función scraping_prensa(\u0026quot;modulos/cron_cnnchile.r\u0026quot;). scraping_prensa va a ejecutar el scraping en segundo plano; es decir, un proceso que se ejecuta de forma independiente y en el fondo (sin bloquear la consola de R). Dependiendo de la línea que comentes/descomentes en la función, los scripts se ejecutarán como background job de RStudio (muestra el progreso de cada scraping en RStudio y funciona en cualquier sistema operativo) o como subprocesos de R (compatible con macOS y Linux, no muestra el progreso pero funciona sin necesidad de tener RStudio ejecutándose).\nLos resultados del scraping se guardarán en la carpeta con el nombre del medio, dentro de resultados/.\nLos scripts que en prensa_scraping.R dicen \u0026ldquo;hist\u0026rdquo; son \u0026ldquo;históricos\u0026rdquo;, es decir, dentro de cada script están comentadas líneas que permiten obtener noticias hacia atrás, y no sólo las más recientes. La capacidad de obtener noticias pasadas depende de cada fuente, ya que algunas limitan el acceso a noticias antiguas, y otras simplemente no las tienen disponibles. La mayoría de los scripts tienen comentadas líneas que permiten usar el mismo script para obtener noticias hacia atrás.\nProcesamiento Luego del scraping se realiza el procesamiento de los datos. Todos los scripts de procesamiento se pueden ejecutar secuencialmente desde prensa_procesar.R.\nprocesamiento/prensa_p1_cargar_datos.R carga los datos scrapeados obtenidos por el paso anterior, los une, limpia y guarda en una sola base de datos, una noticia por fila. La carga y unión de los resultados individuales se realiza de forma paralela usando {furrr}. La limpieza de texto es un procedimiento computacionalmente intenso (dado que implica detectar patrones en el texto para eliminar símbolos e interpretar fechas), por lo que los datos cargados son divididos en piezas más pequeñas, y cada pieza se procesa en un proceso paralelo para aprovechar múltiples procesadores, y el resultado de cada limpieza se guarda individualmente en la carpeta datos/preprocesados/ para evitar mantener los datos en memoria. Finalmente, estas piezas procesadas son vueltas a cargar y unidas como un solo archivo, datos/prensa_datos.parquet, que es la base principal que contiene todas las noticias ordenadas, limpiadas, e individualizadas con un ID único. procesamiento/prensa_p2_procesar_texto.R es el proceso en el que se tokenizan los textos de las noticias; es decir, se transforma una cadena de texto en todas las palabras individuales que conforman el cuerpo de la noticia, eliminando las stopwords o palabras que son irrelevantes para el significado del texto. Para procesar la tokenización de forma eficiente, la base de datos se divide en piezas pequeñas de igual tamaño para ser procesadas en paralelo y guardadas individualmente, igual que en el paso anterior. Esto es para reducir la cantidad de información que se debe mantener cargada en la memoria, al relevar los resultados al disco duro. De este proceso se obtiene datos/prensa_palabras.parquet, una base de datos con más de 90 millones de filas, donde cada fila es una palabra asociada al ID único de la noticia de la cual proviene. procesamiento/prensa_p3_calcular_conteo.R carga la base de datos por palabras, y cuenta la frecuencia de palabras por noticia de forma paralela, para obtener datos/prensa_palabras_conteo.parquet, una base similar a la anterior, pero cuyas palabras son únicas por cada noticia, y contiene la frecuencia de cada término por noticia. El conteo de palabras se realiza primero agrupando las palabras según su raíz (por ejemplo, \u0026ldquo;notici\u0026rdquo; es la raíz de \u0026ldquo;noticia\u0026rdquo;), de modo que palabras que comparten una misma raíz son contadas como una misma palabra (noticia, noticias, noticiario, noticioso), y luego se aplica un algoritmo que reconstruye la palabra completa a partir de la raíz, privilegiando la elección de verbos singulares, en infinitivo, y frecuentes. procesamiento/prensa_p4_calcular_correlacion.R calcula la correlación entre términos, indicando qué términos aparecen cercanos a otros a partir del conteo de palabras por noticia. Calcula tanto la correlación de las palabras en general, como la correlación de las palabras por cada fuente. Los scripts apps/prensa_semanal/prensa_semanal.R y apps/prensa_semanal/prensa_semanal_fuente.R realizan cálculos sobre los datos que producen dataframes usados por la aplicación de visualización de datos, de modo que la aplicación en sí misma realice la menor cantidad de cálculos en tiempo real. Análisis analisis/prensa_calcular_correlacion.R para calcular correlación entre palabras dentro de noticias, retornando una base con palabras y sus pares correlacionados. analisis/prensa_calcular_topicos.R donde se procesan las noticias para identificar tópicos de forma automática y no supervisada mediante machine learning. analisis/prensa_detectar_temas.R donde se evalúa la presencia de términos específicos en cada noticia, y en base al porcentaje de términos coincidentes con respecto a las palabras totales, etiquetar cada noticia con la temática que engloba a los términos; por ejemplo, noticias donde más de 3% de las palabras tienen que ver con robos, asaltos, etc., serán categorizadas como noticias sobre delincuencia. Y más\u0026hellip; Licencia Este código se rige por la licencia GNU General Public License, una licencia de código abierto, por lo que puedes usar el código libremente y modificarlo, pero debes compartir tus modificaciones bajo la misma licencia. Esto permite a los usuarios usar y modificar el software, garantizando que el código resultante seguirá siendo libre (copyleft) al imponer que subsecuentes usos o modificaciones del código deban tamnbién ser compartidas de forma abierta. Por favor, citar este repositorio, y su autor, Bastián Olea Herrera.\nPuedes ver mis otros proyectos de ciencia de datos en R en mi portafolio de visualizadores de datos sociales en R\n","date":"2024-08-08T00:00:00Z","excerpt":"Aplicación de análisis de texto de prensa escrita chilena. Contiene varios gráficos que cuantifican el contenido de las noticias de Chile, semana por semana. Los gráficos permiten identificar qué palabras son las más usadas a través del tiempo, lo cual a su vez revela cómo va variando el acontecer nacional. Los datos de esta aplicación son obtenidos mediante web scraping de forma diaria, pero la app se actualiza semanalmente. La base de datos comprende más de 600 mil noticias, que suman más de 100 millones de palabras, abarcando más de 21 fuentes periodísticas distintas.","href":"https://bastianoleah.netlify.app/blog/app_prensa_chile/","tags":"análisis de texto ; web scraping ; apps ; Chile ; gráficos","title":"App: Análisis de prensa chilena"},{"content":"En este visualizador web se presentan gráficos con datos estadísticas delictuales entregadas por el Centro de Estudio y Análisis del Delito (CEAD), quienes a su vez obtienen los datos desde reportes de Carabineros y la Policía de Investigaciones de Chile al Ministerio del Interior y Seguridad Pública.\nSegún el CEAD, cada dato de delito se compone por: denuncias formales que la ciudadanía realiza en alguna unidad policial posterior a la ocurrencia del delito, más los delitos de los que la policía toma conocimiento al efectuar una detención en flagrancia, es decir, mientras ocurre el ilícito.\nEl objetivo de esta plataforma es transparentar datos objetivos de la delincuencia en el país, otorgándoles contexto para tratar el tema con seriedad en lugar de sensacionalismo y provecho político.\nLa aplicación web está disponible en shinyapps.io, o bien, puedes clonar este repositorio en tu equipo para usarla por medio de RStudio.\nDatos Los datos se obtuvieron directamente desde CEAD haciendo uso de técnicas de web scraping en R, detalladas en este tutorial.. En este repositorio, el script obtener_datos_delincuencia.R realiza el scraping del sitio web de CEAD para los años y comunas que se le indique, y guarda los datos crudos (las tablas en formato html). Luego, el script procesar_datos_delincuencia.R carga estos datos crudos y los transforma a tablas, las limpia, y guarda los datos en formato parquet para lectura rápida, y en formato csv para compatibilidad. No se puede guardar en formato Excel porque tiene más de un millón de filas.\nLos datos limpios están disponibles en la carpeta datos_procesados.\nDescargar datos La base de datos de delitos denunciados en Chile del Centro de Estudio y Análisis del Delito (CEAD), obtenida, ordenada y limpiada mediante el código de este repositorio, se encuentra disponible en formato .parquet en este enlace.\nNota: los datos ya no están disponibles en formato .csv, porque la cantidad de observaciones aumentó y el archivo resultante pesaba más de 100 megas. En comparación, el formato .parquet, que es más eficiente y rápido, pesa sólo 1.1MB.\nActualizaciones Actualización 17/12/2024:\nDatos actualizados hasta septiembre de 2024 (máximo disponible en CEAD a la fecha) Optimización de aplicación para que use tipografías locales en vez de cargarlas desde Google Fonts Se vuelve a usar el paquete arrow para cargar los datos, porque es más rápido que nanoparquet. Correcciones mínimas. Actualización 03/07/2024:\nDatos actualizados hasta marzo de 2024 (máximo disponible en CEAD a la fecha) Actualización del código de scraping para que funcione con la actualización del sitio de CEAD Los datos ahora representan casos policiales en vez de solo denuncias. Los casos policiales \u0026ldquo;consideran las denuncias de delitos que realiza la comunidad en las unidades policiales, más las detenciones que realizan las policías ante la ocurrencia de delitos flagrantes\u0026rdquo;. Se cambia el paquete que carga los datos a nanoparquet, que tiene menos dependencias que arrow Se flexibiliza el código de la app para que se adapte a las fechas que vienen en los datos, para facilitar actualizaciones futuras ","date":"2024-07-10T00:00:00Z","excerpt":"Visualización de estadísticas oficiales de delincuencia, separadas por comuna y delito, para darle contexto y seriedad a un tema país a partir de datos objetivos. Selecciona una comuna y luego uno o varios delitos para obtener un gráfico de líneas que muestra una serie de tiempo de la cantidad de delitos, desde 2010 hasta 2023. Además, puedes visualizar la cantidad de delitos por año en la comuna seleccionada, el promedio de delitos en los gobiernos recientes, y una visualización de los tres delitos más frecuentes en cada comuna.","href":"https://bastianoleah.netlify.app/blog/app_delincuencia_chile/","tags":"apps ; datos ; Chile","title":"App: Estadísticas de delincuencia en Chile"},{"content":" Aplicación web que visualiza los datos oficiales del Instituto Nacional de Estadísticas de Chile sobre proyecciones de población; es decir, estimaciones del crecimiento poblacional hacia el futuro, a partir de los datos obtenidos en los censos oficiales.\nEl script importar_poblacion_proyecciones.R descarga directamente el archivo Excel con las proyecciones oficiales desde el INE, para usarlo en este proyecto.\nDatos En la carpeta datos_procesados se encuentran tablas de datos procesados en base a las proyecciones oficiales, incluyendo:\ncenso_proyecciones_año.csv: (11 mil filas) proyecciones de población para todas las comunas del país, por año de proyección (de 2002 a 2035) censo_proyecciones_año_edad_genero.parquet: (1.9 millones de filas) proyecciones de población para todas las comunas del país, por edad y género, por año de proyección (de 2002 a 2035) censo_proyecciones_edad_prom.csv: proyecciones de edad promedio, por comuna y año censo_proyecciones_edad_menores.csv: porcentaje de menores de edad (\u0026lt; 18), por comuna y año censo_proyecciones_edad_adultomayor.csv: porcentaje de adultos mayores (\u0026gt;= 60), por comuna y año censo_proyeccion_2024.csv: sólo la proyección para 2024 por comunas Fuentes: INE, Proyecciones de población ","date":"2024-06-17T00:00:00Z","excerpt":"Aplicación web que visualiza los datos oficiales del Instituto Nacional de Estadísticas de Chile sobre proyecciones de población; es decir, estimaciones del crecimiento poblacional hacia el futuro, a partir de los datos obtenidos en los censos oficiales.","href":"https://bastianoleah.netlify.app/blog/app_censo_proyecciones/","tags":"apps ; Chile","title":"App: Proyecciones de población del Censo"},{"content":"Aplicación web interactiva desarrollada en R. Accede a ella por este enlace\nEsta aplicación web permite visualizar interactivamente las diferencias y desigualdades territoriales de Chile a través de mapas.\nReúne más de 170 variables urbanísticas, sociales y económicas a nivel comunal, para todas las comunas del país, permitiendo al usuario elegir dos variables simultáneamente para compararlas visualmente por medio de dos mapas regionales.\nEl visualizador entrega la posibilidad de poner a prueba relaciones entre variables tan distintas como áreas verdes y puntajes de pruebas de selección universitaria, nivel de ingresos y tasa de delitos, participación electoral y situación de las viviendas, etc., dejando al usuario la tarea de explicar los fenómenos que pueden surgir.\nSeleccione una región del país, y luego elija dos variables para compararlas a nivel comunal. Por defecto, la aplicación le mostrará categorías y variables al azar, esperando que surja una relación interesante.\u0026quot;),\nSe puede elegir entre más de 170 datos sociales, organizados en 10 categorías, que incluyen datos de salud, educación, ingresos, seguridad, delincuencia, urbanismo, y otros.\nActualizaciones Actualización 21/06/24:\nAgregadas 12 variables sobre migración desde datos entregados por el Servicio Nacional de Migraciones (SERMIG). Actualización 20/06/24:\nAgregadas 71 variables sobre desarrollo urbano desde datos del Sistema de Indicadores y Estándares de Desarrollo Urbano (SIEDU) Agregadas variables de cuarteles y comisarías de Carabineros (obtenida a través de la Plataforma de Datos Abiertos de Itrend) Botón para obtener variables al azar, por si te da paja elegirlas Corrección en cálculo de aumento de delitos y de población Unidades en algunas variables (especificando que se trata de metros cuadrados, kilowatts, etc.) Estabilidad con algunos casos de error comunes Cambios estéticos leves para mejorar contraste Aspectos metodológicos La app contiene más de 170 variables individuales de datos a nivel comunal, las cuales provienen de fuentes oficiales, y que se detallan en la sección \u0026ldquo;Fuentes de los datos\u0026rdquo; y en el archivo app/fuentes.csv. Cada dato disponible en la aplicación posee su fuente oficial detallada debajo de cada mapa.\nLos datos están disponibles en este repositorio, en la carpeta app/datos/, pero recomiendo bajar hasta la sección Fuentes de este documento para encontrar la fuente original y un repositorio donde se procesan los datos originales para obtener los datos limpios.\nAspectos técnicos Se trata de una aplicación web desarrollada en el lenguaje de programación estadístico R, usando el framework para aplicaciones estadísticas interactivas Shiny. Todas las librerías son de código abierto.\nDesarrollé la aplicación para aplicar el mapa urbano de la Región Metropolitana que detallé en un tutorial previo, y para aprender el uso de módulos en Shiny.\nEl aspecto de los datos de la aplicación son controlados por una planilla llamada fuentes.csv, donde cada fila es una variable posible de visualizar en la app, y que detalla los metadatos de cada variable, como su categoría, su nombre, el nombre del objeto a cargar por la app, la escala de la variable (si es porcentaje, numero decimal o numero grande, para adecuar los gráficos), y la fuente del dato. Desde esta planilla se generan automáticamente las categorías y se rellenan los selectores de la app, facilitando el proceso de agregar o modificar variables nuevas para visualizarlas.\nFuentes de los datos Residencias temporales y residencias definitivas: Servicio Nacional de Migraciones (SERMIG). Datos procesados y limpiados en el repositorio migracion_chile.\nEstimación de extranjeros: Servicio Nacional de Migraciones (SERMIG). Datos procesados y limpiados en el repositorio migracion_chile.\nIndicadores y Estándares de Desarrollo Urbano: Sistema de Indicadores y Estándares de Desarrollo Urbano (SIEDU).. Datos limpiados y procesados en el repositorio indicadores_urbanos_siedu.\nCuarteles de Carabineros: Plataforma de Datos Abiertos de Itrend (Instituto para la Resiliencia ante Desastres). Carabineros de Chile, «Cuarteles de Carabineros». Obtenida a través de la Plataforma de Datos Abiertos de Itrend. Datos procesados en el repositorio comisarias_chile\nEstadísticas sobre delitos: Centro de Estudios y Análisis del Delito, código de scraping y procesamiento disponible en el repo delincuencia_chile, y también disponibles en detalle como aplicación web: Estadísticas de delincuencia en Chile\nResultados de los plebiscitos constitucionales: Servel, datos procesados en el repositorio plebiscitos_chile\nEstadísticas sobre resultados de prueba PAES: Mineduc, código de procesamiento de la base de datos disponible en el repositorio puntajes_prueba_paes\nPago de contribuciones por comuna: SII, datos obtenidos y procesados por Ernesto Laval\nDatos municipales: Sistema Nacional de Información Municipal (SINIM), datos procesados en el repositorio sinim_datos_comunales\nEstadísticas socioeconómicas: Encuesta CASEN 2022, datos procesados en el repositorio casen_relacionador, y también disponibles en detalle como aplicación web: Relacionador Casen\nPoblación y proyecciones: INE, datos procesados en el repositorio censo_proyecciones_poblacion, y también disponibles en detalle como aplicación web: Censo de población: proyecciones\nLista de variables disponibles variable categoria fuente Parques urbanos (metros²) Áreas verdes Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Plazas existentes (metros²) Áreas verdes Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Cantidad de plazas Áreas verdes Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Cantidad de parques urbanos Áreas verdes Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Superficie de áreas verdes con mantenimiento (metros²) Áreas verdes Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Distancia a parques públicos Áreas verdes Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Distancia a plazas públicas Áreas verdes Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Porcentaje de la superficie cubierta por vegetación total Áreas verdes Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2020) Porcentaje de la superficie cubierta por vegetación densa Áreas verdes Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2020) Porcentaje de población atendida por el sistema de parques públicos Áreas verdes Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Porcentaje de población atendida por el sistema de plazas públicas Áreas verdes Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Superficie de parques públicos por habitante que cumple estándar de distancia (3.000 metros) Áreas verdes Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Superficie de plazas públicas por habitante que cumple estándar de distancia (400 metros) Áreas verdes Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Superficie de áreas verdes públicas por habitante Áreas verdes Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Hogares en situación de hacinamiento (4 personas o más) Viviendas Casen (2022), Ministerio de Desarrollo Social Viviendas propias Viviendas Casen (2022), Ministerio de Desarrollo Social Viviendas arrendadas Viviendas Casen (2022), Ministerio de Desarrollo Social Metros cuadrados aproximados de la vivienda Viviendas Casen (2022), Ministerio de Desarrollo Social Viviendas pequeñas (30m cuadrados o menos) Viviendas Casen (2022), Ministerio de Desarrollo Social Viviendas medianas (entre 30 y 100m cuadrados) Viviendas Casen (2022), Ministerio de Desarrollo Social Viviendas grandes (más de 100m cuadrados) Viviendas Casen (2022), Ministerio de Desarrollo Social Hogares en zonas rurales Viviendas Casen (2022), Ministerio de Desarrollo Social Viviendas en mal estado Viviendas Casen (2022), Ministerio de Desarrollo Social Viviendas carentes de servicios básicos Viviendas Casen (2022), Ministerio de Desarrollo Social Numero de personas por hogar Viviendas Casen (2022), Ministerio de Desarrollo Social Porcentaje de viviendas con situación de allegamiento externo Viviendas Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Porcentaje de viviendas en situación de hacinamiento Viviendas Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Porcentaje de viviendas particulares que requieren mejoras de materialidad y/o servicios básicos Viviendas Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Requerimiento de viviendas nuevas urbanas Viviendas Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Hogares en sectores en mal estado Urbanismo Casen (2022), Ministerio de Desarrollo Social Hogares en sectores con mucho daño deliberado a la propiedad Urbanismo Casen (2022), Ministerio de Desarrollo Social Cantidad de luminarias cada 50 metros lineales de red vial Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Densidad de oferta planificada de transporte público mayor en periodo punta mañana, por persona Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Densidad de oferta planificada de transporte público menor en periodo punta mañana, por persona Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Distancia a paraderos de transporte público mayor Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Monto total per cápita, en pesos, de fondos entregados por el municipio a la comunidad vía proyectos concursables para el mejoramiento urbano Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Número de víctimas lesionadas en siniestros de tránsito por cada 100.000 habitantes Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2021) Número de víctimas mortales en siniestros de tránsito por cada 100.000 habitantes Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2021) Partición modal del transporte público (número de viajes en transporte público respecto al número total de viajes) Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Partición modal del transporte sustentable (suma de viajes en transporte público, caminata y bicicleta respecto al número total de viajes) Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Porcentaje de cobertura de la red de ciclovía sobre la red vial Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2021) Porcentaje de manzanas con veredas con buena calidad de pavimento Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Porcentaje de población atendida por la red de ciclovías Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Porcentaje de viajes originados inferiores a 5 kilómetros Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2021) Promedio de intersecciones relevantes cada 1,44 km² Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Proporción de viajes con una duración mayor o igual a 45 minutos Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Proporción de viajes de estudio y trabajo con una duración mayor o igual a 45 minutos Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Proporción de viajes totales en transporte privado con una duración mayor o igual a 45 minutos Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Proporción de viajes totales en transporte público con una duración mayor o igual a 45 minutos Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Relación entre el tiempo de viaje en hora punta respecto del tiempo de viaje fuera de hora punta Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Tiempo de viaje en hora punta mañana Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Tiempo de viaje en transporte público en hora punta mañana Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Densidad de oferta real de transporte público mayor en periodo punta mañana, por persona Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Porcentaje de personas potencialmente expuestas a niveles de ruido diurno inaceptables (Ld \u0026gt; 65 dBA OCDE) Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Porcentaje de personas potencialmente expuestas a niveles de ruido nocturno inaceptables (Ln \u0026gt; 55 dBA OCDE) Urbanismo Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Trabajadores independientes Trabajo Casen (2022), Ministerio de Desarrollo Social Personas en situación laboral inactiva Trabajo Casen (2022), Ministerio de Desarrollo Social Personas en situación laboral desocupada Trabajo Casen (2022), Ministerio de Desarrollo Social Trabaja como familiar no remunerado Trabajo Casen (2022), Ministerio de Desarrollo Social Trabaja en servicio doméstico puertas adentro o afuera Trabajo Casen (2022), Ministerio de Desarrollo Social Trabaja como empleador Trabajo Casen (2022), Ministerio de Desarrollo Social Trabajadores del hogar o servicio doméstico Trabajo Casen (2022), Ministerio de Desarrollo Social Personas en situación de pobreza Social Casen (2022), Ministerio de Desarrollo Social Personas en situación de pobreza multidimensional Social Casen (2022), Ministerio de Desarrollo Social Densidad de hogares en campamentos Social Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2022) Número de microbasurales por cada 10.000 habitantes Social Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Porcentaje de la población en situación de pobreza multidimensional Social Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Porcentaje de la población en situación de pobreza por ingresos Social Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Porcentaje de la superficie de campamentos respecto del área urbana Social Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2022) Porcentaje de unidades vecinales de la comuna que tienen entre 20% y 60% de hogares vulnerables Social Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Índice de segregación de la población vulnerable Social Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Cantidad de cámaras de vigilancia Seguridad Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Cantidad de guardias, inspectores o vigilantes municipales Seguridad Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Cantidad de automóviles de seguridad municipal o patrullaje Seguridad Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Cantidad de motos de seguridad municipal o patrullaje Seguridad Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Cantidad de delitos denunciados (total anual) Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Delitos de mayor connotación social denunciados (total anual) Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Aumento de delitos anuales reportados respecto a 2 años atrás Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Aumento de delitos anuales reportados respecto a 3 años atrás Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Aumento de delitos anuales reportados respecto a 5 años atrás Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Aumento de delitos de mayor connotación social anuales reportados respecto a 2 años atrás Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Aumento de delitos de mayor connotación social anuales reportados respecto a 3 años atrás Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Aumento de delitos de mayor connotación social anuales reportados respecto a 5 años atrás Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Porcentaje del total de delitos anuales denunciados que son de mayor connotación social Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Tasa de delitos denunciados anualmente por cada 1.000 habitantes Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Tasa de delitos de mayor connotación social denunciados anualmente por cada 1.000 habitantes Seguridad Centro de Estudios y Análisis del Delito (CEAD) (2023) Cuarteles totales de Carabineros de Chile (comisarías, retenes, subcomisarías, tenencias, etc.) Seguridad Plataforma de Datos Abiertos de Itrend (Instituto para la Resiliencia ante Desastres) Retenes de Carabineros de Chile Seguridad Plataforma de Datos Abiertos de Itrend (Instituto para la Resiliencia ante Desastres) Comisarías de Carabineros de Chile Seguridad Plataforma de Datos Abiertos de Itrend (Instituto para la Resiliencia ante Desastres) Distancia a una unidad o destacamento destinado al servicio operativo de Carabineros Seguridad Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Tasa de víctimas de delitos contra la propiedad por casos policiales cada 10.000 habitantes Seguridad Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2022) Tasa de víctimas de delitos violencia intrafamiliar por casos policiales cada 10.000 habitantes Seguridad Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2022) Tasa de víctimas de delitos violentos por casos policiales cada 10.000 habitantes Seguridad Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2022) Cantidad de Ambulancias Salud Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Cantidad de CESFAM en la comuna Salud Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Cantidad de COSAM en la comuna Salud Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Cantidad de Consultorios (urbanos y rurales) Salud Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Cantidad de SAPU en la comuna Salud Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Afiliados a previsión de salud Fonasa Salud Casen (2022), Ministerio de Desarrollo Social Afiliados a previsión de salud Isapre Salud Casen (2022), Ministerio de Desarrollo Social Cantidad de jornadas diarias completas de trabajo de médicos, en salud primaria, por cada 10.000 habitantes Salud Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Distancia a centros de salud primaria Salud Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2020) Población total al año 2017 Población Instituto Nacional de Estadísticas (INE) Población total al año 2024 Población Instituto Nacional de Estadísticas (INE) Población proyectada al año 2030 Población Instituto Nacional de Estadísticas (INE) Aumento de población en 2024 respecto a 5 años atrás Población Instituto Nacional de Estadísticas (INE) Aumento de población en 2024 respecto a 10 años atrás Población Instituto Nacional de Estadísticas (INE) Aumento de población en 2024 respecto a 20 años atrás Población Instituto Nacional de Estadísticas (INE) Hogares con personas menores de 18 años Población Casen (2022), Ministerio de Desarrollo Social Hogares con personas mayores de 60 años Población Casen (2022), Ministerio de Desarrollo Social Edad promedio Población Casen (2022), Ministerio de Desarrollo Social Personas pertenecientes a pueblos originarios Población Casen (2022), Ministerio de Desarrollo Social Consumo de energía eléctrica per cápita no residencial Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2021) Consumo de energía eléctrica per cápita residencial Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2021) Indisponibilidad de suministro eléctrico - indicador SAIDI anual Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2022) Porcentaje de inversión pública destinada a proyectos que tienen procesos de intervención de restauración de inmuebles patrimoniales sobre el total de inversión destinada a proyectos con RF Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Porcentaje de residuos municipales valorizados Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Tasa de conexiones residenciales fijas de internet por cada 1.000 viviendas particulares Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2021) Tasa de conexiones residenciales fijas de internet por cada 100 habitantes Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2021) Cantidad de residuos eliminados con disposición final per cápita Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019, 2022) Porcentaje de zonas típicas con lineamientos de intervención aprobados Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018-2019) Porcentaje de zonas típicas con lineamientos de intervención en desarrollo Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018-2019) Porcentaje de aporte de energía eléctrica proveniente de paneles solares domiciliarios Otras Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018-2019) Personas de origen extranjero Inmigración Casen (2022), Ministerio de Desarrollo Social Población estimada de migrantes internacionales por comuna Inmigración Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2020) Porcentaje de votantes de nacionalidad extranjera en las elecciones municipales Inmigración Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2022) Estimación de extranjeros totales residiendo en Chile Inmigración Servicio Nacional de Migraciones (2022) Estimación de extranjeros de origen venezolano residiendo en Chile Inmigración Servicio Nacional de Migraciones (2022) Estimación de extranjeros de origen colombiano residiendo en Chile Inmigración Servicio Nacional de Migraciones (2022) Estimación de extranjeros de origen peruano residiendo en Chile Inmigración Servicio Nacional de Migraciones (2022) Permisos de residencia definitiva para extranjeros acogidos Inmigración Servicio Nacional de Migraciones (2023) Permisos de residencia definitiva para extranjeros otorgados Inmigración Servicio Nacional de Migraciones (2023) Permisos de residencia temporal para extranjeros acogidos Inmigración Servicio Nacional de Migraciones (2023) Permisos de residencia temporal para extranjeros otorgados Inmigración Servicio Nacional de Migraciones (2023) Permisos de residencia definitiva acogidos para extranjeros de origen venezolano Inmigración Servicio Nacional de Migraciones (2023) Permisos de residencia definitiva otorgados para extranjeros de origen venezolano Inmigración Servicio Nacional de Migraciones (2023) Permisos de residencia temporal acogidos para extranjeros de origen venezolano Inmigración Servicio Nacional de Migraciones (2023) Permisos de residencia temporal otorgados para extranjeros de origen venezolano Inmigración Servicio Nacional de Migraciones (2023) Porcentaje de hogares con menos del 40% de ingresos respecto del total regional (RSH) Ingresos Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023), Registro Social de Hogares Hogares con ingresos percapita menores a la mediana ($450.000) Ingresos Casen (2022), Ministerio de Desarrollo Social Hogares con ingresos totales menores a la mediana ($1.110.000) Ingresos Casen (2022), Ministerio de Desarrollo Social Hogares con ingresos producto del trabajo menores a la mediana ($700.000) Ingresos Casen (2022), Ministerio de Desarrollo Social Ingresos promedio Ingresos Casen (2022), Ministerio de Desarrollo Social Ingreso ocupación principal Ingresos Casen (2022), Ministerio de Desarrollo Social Ingreso del trabajo Ingresos Casen (2022), Ministerio de Desarrollo Social Jubilación o pensión de vejez Ingresos Casen (2022), Ministerio de Desarrollo Social Decil autónomo regional Ingresos Casen (2022), Ministerio de Desarrollo Social Quintil autónomo regional Ingresos Casen (2022), Ministerio de Desarrollo Social Ingresos personales por ocupación principal menores a la mediana ($500.000) Ingresos Casen (2022), Ministerio de Desarrollo Social Ingresos personales por ocupación principal menores a $1.000.000 Ingresos Casen (2022), Ministerio de Desarrollo Social Ingresos personales producto del trabajo menores a la mediana ($500.000) Ingresos Casen (2022), Ministerio de Desarrollo Social Ingresos personales producto del trabajo menores a $1.000.000 Ingresos Casen (2022), Ministerio de Desarrollo Social Ingresos promedio de los hogares Ingresos Casen (2022), Ministerio de Desarrollo Social Ingreso del trabajo del hogar Ingresos Casen (2022), Ministerio de Desarrollo Social Ingreso autónomo per cápita Ingresos Casen (2022), Ministerio de Desarrollo Social Ingreso total per cápita del hogar Ingresos Casen (2022), Ministerio de Desarrollo Social Jubilación o pensión menor o igual a la mediana ($230.000) Ingresos Casen (2022), Ministerio de Desarrollo Social Jubilación o pensión menor o igual al salario mínimo ($500.000) Ingresos Casen (2022), Ministerio de Desarrollo Social Plebiscito 2022: apruebo Elecciones Servel, Resultados electorales históricos (2022) Plebiscito 2022: rechazo Elecciones Servel, Resultados electorales históricos (2022) Plebiscito 2023: en contra Elecciones Servel, Resultados electorales históricos (2023) Plebiscito 2023: a favor Elecciones Servel, Resultados electorales históricos (2023) Porcentaje de participación en las elecciones municipales por comuna Elecciones Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2022) Promedio de notas de educación media Educación Ministerio de Educación, Centro de Estudios Mineduc (2024) Puntaje PAES 2024 en la prueba de Competencia Lectora Educación Ministerio de Educación, Centro de Estudios Mineduc (2024) Puntaje PAES 2024 en la prueba de Competencia Matemática 1 Educación Ministerio de Educación, Centro de Estudios Mineduc (2024) Puntaje PAES 2024 en la prueba de Competencia Matemática 2 Educación Ministerio de Educación, Centro de Estudios Mineduc (2024) Puntaje PAES 2024 en la prueba de Historia y Ciencias Sociales Educación Ministerio de Educación, Centro de Estudios Mineduc (2024) Puntaje PAES 2024 en la prueba de Ciencias Educación Ministerio de Educación, Centro de Estudios Mineduc (2024) Docentes de aula contratados Educación Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023), Ministerio de Educación Alumnos por docente de aula Educación Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023), Ministerio de Educación Cantidad de personal docente Educación Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023), Ministerio de Educación Establecimientos de Educación Municipal Educación Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023), Ministerio de Educación Años de escolaridad promedio Educación Casen (2022), Ministerio de Desarrollo Social Personas con estudios superiores (técnico o profesional) Educación Casen (2022), Ministerio de Desarrollo Social Nivel educacional máximo: básica o menor Educación Casen (2022), Ministerio de Desarrollo Social Nivel educacional máximo: media incompleta o menor Educación Casen (2022), Ministerio de Desarrollo Social Nivel educacional máximo: estudios superiores incompletos Educación Casen (2022), Ministerio de Desarrollo Social Distancia a establecimientos de educación básica Educación Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2020) Distancia a establecimientos de educación inicial Educación Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2020) Razón entre disponibilidad efectiva de matrículas y demanda potencial por educación básica Educación Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2020) Monto percibido por Fondo Común Municipal Economía Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Participación de Ingresos Propios Permanentes en el Ingreso Total del municipio Economía Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Dependencia del Fondo Común Municipal sobre los Ingresos Propios del municipio Economía Subsecretaría de Desarrollo Regional y Administrativo, Sistema Nacional de Información Municipal (Sinim) (2023) Diferencia entre el valor de suelo más alto y el más bajo entre las áreas homogéneas urbanas definidas por el Servicio de Impuestos Internos Economía Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2018) Participación del Fondo Común Municipal (FCM) en el ingreso municipal total (descontadas las transferencias) Economía Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2022) Porcentaje de la inversión nacional a escala comunal en la que participa el municipio como institución contratante Economía Sistema de Indicadores y Estándares de Desarrollo Urbano. Consejo Nacional de Desarrollo Urbano (CNDU) y el Instituto Nacional de Estadísticas (INE) (2019) Pendientes Datos sobre género Datos sobre femicidios Datos sobre pago de contribuciones Colores a los mapas según categoría Índice de desarrollo humano Indicar unidades de las variables ","date":"2024-06-15T00:00:00Z","excerpt":"Aplicación que reúne más de 170 variables urbanísticas, sociales y económicas, de nivel comunal, para todas las comunas del país, que permite al usuario elegir dos variables simultáneamente para compararlas visualmente por medio de dos mapas regionales. El visualizador entrega la posibilidad de poner a prueba relaciones entre variables tan distintas como áreas verdes y puntajes de pruebas de selección universitaria, nivel de ingresos y tasa de delitos, participación electoral y situación de las viviendas, etc., dejando al usuario la tarea de explicar los fenómenos que pueden surgir.","href":"https://bastianoleah.netlify.app/blog/app_comparador_mapas_chile/","tags":"apps ; Chile ; datos","title":"App: Comparador de mapas comunales de Chile"},{"content":"Este tutorial de R te explicará paso a paso a cómo obtener mapas de todo Chile usando el paquete {chilemapas} desarrollado por Mauricio Vargas, y hacer gráficos con estos mapas usando {ggplot2}.\nEn la primera parte veremos cómo obtener los mapas y cómo visualizar datos comunales usando mapas en R. Si necesitas una guía sobre mapas en R, revisa este post.\nLuego, nos enfrentaremos a un problema común que se tiene al graficar un mapa de la Región Metropolitana de Santiago, que tiene que ver con la diferencia entre los límites comunales reales de cada comuna y los límites urbanos de las comunas. Es la diferencia entre tener un mapa de la RM que abarque sectores rurales como Paine y que llegue hasta Argentina, o un mapa que demarque la zona urbana de Santiago, aproximadamente correspondiente a la zona que atravieza el anillo de la autopista Américo Vespucio.\nCon un mapa de la superficie urbana de la Región Metropolitana, obtenemos una figura que es más familiar al habitante promedio de la región, y que es la que usalmente vemos en la cotidianeidad, en contraste con un mapa geográficamente correcto de todo el territorio regional.\nÍndice Introducción Paquetes Obtener un mapa regional Visualización Mapa básico de la región Mapa de la región con datos ficticios Agregar datos obtenidos desde internet al mapa Visualizar datos Mapa urbano de la región Metropolitana Excluir islas urbanas Visualizar datos en el mapa urbano Introducción Paquetes Primero cargamos los paquetes que usaremos en este tutorial. Si no tienes alguno de ellos, intálalo con install.packages().\n# datos library(dplyr) #manipulación de datos library(janitor) #limpieza de datos library(stringr) #manipulación de texto # mapas library(chilemapas) #mapas de chile library(sf) #manipulación de mapas # gráficos library(ggplot2) #visualización de datos library(viridis) #escalas de colores library(scales) #escalas numéricas # web scraping library(rvest) #obtener datos desde páginas de internet Obtener un mapa regional Primero, usaremos {chilemapas} para obtener los datos geográficos (polígonos o shapes) necesarios para producir un mapa de la Región Metropolitana:\n# obtener mapa comunal mapa_comunas \u0026lt;- chilemapas::mapa_comunas nombres_comunas \u0026lt;- chilemapas::codigos_territoriales |\u0026gt; select(matches(\u0026#34;comuna\u0026#34;)) # mapa de la región metropolitana mapa \u0026lt;- mapa_comunas |\u0026gt; # especificar la geometría st_set_geometry(mapa_comunas$geometry) |\u0026gt; # agregar nombres de comunas left_join(nombres_comunas, by = \u0026#34;codigo_comuna\u0026#34;) |\u0026gt; # filtrar la región metropolitana filter(codigo_region == \u0026#34;13\u0026#34;) mapa Simple feature collection with 52 features and 4 fields Geometry type: MULTIPOLYGON Dimension: XY Bounding box: xmin: -71.71523 ymin: -34.29093 xmax: -69.76999 ymax: -32.92194 Geodetic CRS: SIRGAS 2000 # A tibble: 52 × 5 codigo_comuna codigo_provincia codigo_region geometry * \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;MULTIPOLYGON [°]\u0026gt; 1 13404 134 13 (((-70.61396 -33.73862, -70.609… 2 13402 134 13 (((-70.61396 -33.73862, -70.623… 3 13124 131 13 (((-70.75679 -33.38348, -70.780… 4 13103 131 13 (((-70.72154 -33.43661, -70.724… 5 13301 133 13 (((-70.37256 -33.10578, -70.376… 6 13303 133 13 (((-70.72028 -32.95297, -70.723… 7 13302 133 13 (((-70.79191 -33.17296, -70.783… 8 13107 131 13 (((-70.59589 -33.33656, -70.590… 9 13104 131 13 (((-70.68968 -33.36587, -70.682… 10 13504 135 13 (((-71.27576 -33.40409, -71.263… # ℹ 42 more rows # ℹ 1 more variable: nombre_comuna \u0026lt;chr\u0026gt; Podemos ver que obtuvimos un dataframe donde cada fila es una comuna, individualizada por su nombre y su código único territorial (codigo_comuna).\nEn esta tabla de datos, la columna geometry contiene la información geográfica de cada comuna, lo que permite visualizarlas como un mapa.\nPor lo tanto, en cada fila tenemos información geográfica que representa polígonos comunales, donde cada polígono (o conjuto de polígonos) se corresponde con los datos existentes en las demás columnas, que pueden ser información como sus nombres, su población, o cualquier otra.\nVisualización Mapa básico de la región Podemos visualizar este mapa de manera con {ggplot2}:\nmapa |\u0026gt; # iniciar gráfico ggplot() + # agregar capa con el mapa geom_sf(fill = \u0026#34;grey60\u0026#34;, col = \u0026#34;white\u0026#34;) + # tema theme_void() Obtuvimos un mapa básico de todas las comunas de la Región Metropolitana de Santiago.\nMapa de la región con datos ficticios Ahora, hagamos una prueba para aprender a visualizar datos en la di este mapa. Para esto, crearemos una nueva variable donde algunas comunas tengan valores distintos. Podemos crear la nueva variable a partir de la columna nombre_comuna, aunque siempre es preferible hacerlo en base a la columna codigo_comuna, dado que los códigos únicos territoriales son identificadores únicos para cada comuna, mientras que los nombres de las comunas son más impredecibles (por ejemplo, pueden venir sin tilde, pueden venir en mayúsculas, o derechamente mal escritos).\nUsamos la función case_when() para asignar valores ficticios sobre algunas comunas, y los visualizamos en el mapa:\nmapa_datos \u0026lt;- mapa |\u0026gt; # crear una variable para comunas específicas mutate(variable = case_when(nombre_comuna == \u0026#34;Paine\u0026#34; ~ \u0026#34;Bacán\u0026#34;, nombre_comuna == \u0026#34;Buin\u0026#34; ~ \u0026#34;Penca\u0026#34;, nombre_comuna == \u0026#34;La Florida\u0026#34; ~ \u0026#34;Bacán\u0026#34;, nombre_comuna == \u0026#34;Cerrillos\u0026#34; ~ \u0026#34;Bacán\u0026#34;, nombre_comuna == \u0026#34;Nunoa\u0026#34; ~ \u0026#34;Penca\u0026#34;)) |\u0026gt; select(nombre_comuna, codigo_comuna, variable, geometry) mapa_datos Simple feature collection with 52 features and 3 fields Geometry type: MULTIPOLYGON Dimension: XY Bounding box: xmin: -71.71523 ymin: -34.29093 xmax: -69.76999 ymax: -32.92194 Geodetic CRS: SIRGAS 2000 # A tibble: 52 × 4 nombre_comuna codigo_comuna variable geometry \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;MULTIPOLYGON [°]\u0026gt; 1 Paine 13404 Bacán (((-70.61396 -33.73862, -70.60917 -33.7… 2 Buin 13402 Penca (((-70.61396 -33.73862, -70.62304 -33.7… 3 Pudahuel 13124 \u0026lt;NA\u0026gt; (((-70.75679 -33.38348, -70.78087 -33.4… 4 Cerro Navia 13103 \u0026lt;NA\u0026gt; (((-70.72154 -33.43661, -70.72426 -33.4… 5 Colina 13301 \u0026lt;NA\u0026gt; (((-70.37256 -33.10578, -70.37609 -33.1… 6 Tiltil 13303 \u0026lt;NA\u0026gt; (((-70.72028 -32.95297, -70.72329 -32.9… 7 Lampa 13302 \u0026lt;NA\u0026gt; (((-70.79191 -33.17296, -70.7833 -33.18… 8 Huechuraba 13107 \u0026lt;NA\u0026gt; (((-70.59589 -33.33656, -70.59023 -33.3… 9 Conchali 13104 \u0026lt;NA\u0026gt; (((-70.68968 -33.36587, -70.68201 -33.3… 10 Maria Pinto 13504 \u0026lt;NA\u0026gt; (((-71.27576 -33.40409, -71.26337 -33.4… # ℹ 42 more rows # visualizar mapa_datos |\u0026gt; ggplot() + aes(fill = variable) + #usamos la variable que creamos como relleno de las comunas geom_sf(col = \u0026#34;white\u0026#34;) + theme_void() Agregar datos obtenidos desde internet al mapa Ahora, pasaremos a usar datos reales sobre nuestro mapa comunal. Pero en vez de entregarles datos copiados y pegados, obtendremos directamente los datos desde internet, usando el paquete {rvest} que sirve para hacer web scraping desde páginas web; es decir, descargar datos presentes en sitios de internet para usarlos directamente en R.\nlibrary(rvest) tabla_comunas \u0026lt;- session(\u0026#34;https://es.wikipedia.org/wiki/Anexo:Comunas_de_Chile\u0026#34;) |\u0026gt; # sitio web que scrapearemos read_html() |\u0026gt; # leemos el contenido del sitio web html_table() # extraemos las tablas del sitio web tabla_comunas_2 \u0026lt;- tabla_comunas[[1]] |\u0026gt; # elegimos la primera tabla obtenida clean_names() # limpiamos los nombres de la tabla usando {janitor} tabla_comunas_3 \u0026lt;- tabla_comunas_2 |\u0026gt; filter(region == \u0026#34;Metropolitana de Santiago\u0026#34;) # filtrar la región tabla_comunas_4 \u0026lt;- tabla_comunas_3 |\u0026gt; # convertir los códigos comunales a texto rename(codigo_comuna = 1) |\u0026gt; mutate(codigo_comuna = as.character(codigo_comuna)) datos_comunas \u0026lt;- tabla_comunas_4 |\u0026gt; # limpiar variables numéricas para estén disponibles en formato numérico en vez de como texto mutate(poblacion2020 = str_remove(poblacion2020, \u0026#34; \u0026#34;), # borrar espacios poblacion2020 = as.numeric(poblacion2020)) |\u0026gt; # transformar texto a numérico # corregir superficie mutate(superficie_km2 = str_remove(superficie_km2, \u0026#34;\\\\.\u0026#34;), # borrar puntos separadores de miles superficie_km2 = str_replace(superficie_km2, \u0026#34;,\u0026#34;, \u0026#34;.\u0026#34;), # reemplazar comas por puntos superficie_km2 = as.numeric(superficie_km2)) |\u0026gt; # transformar texto a numérico # corregir densidad mutate(densidad_hab_km2 = str_remove_all(densidad_hab_km2, \u0026#34;\\\\.\u0026#34;), # borrar puntos separadores de miles densidad_hab_km2 = str_replace(densidad_hab_km2, \u0026#34;,\u0026#34;, \u0026#34;.\u0026#34;), # reemplazar comas por puntos densidad_hab_km2 = as.numeric(densidad_hab_km2)) # transformar texto a numérico datos_comunas # A tibble: 52 × 12 codigo_comuna nombre x provincia region superficie_km2 poblacion2020 \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;lgl\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;dbl\u0026gt; 1 13101 Santiago NA Santiago Metro… 232 503147 2 13102 Cerrillos NA Santiago Metro… 21 88956 3 13103 Cerro Navia NA Santiago Metro… 11 142465 4 13104 Conchalí NA Santiago Metro… 107 139195 5 13105 El Bosque NA Santiago Metro… 142 172000 6 13106 Estación C… NA Santiago Metro… 15 206792 7 13107 Huechuraba NA Santiago Metro… 448 112528 8 13108 Independen… NA Santiago Metro… 7 142065 9 13109 La Cisterna NA Santiago Metro… 10 100434 10 13110 La Florida NA Santiago Metro… 702 402433 # ℹ 42 more rows # ℹ 5 more variables: densidad_hab_km2 \u0026lt;dbl\u0026gt;, idh_2005 \u0026lt;chr\u0026gt;, idh_2005_2 \u0026lt;chr\u0026gt;, # latitud \u0026lt;chr\u0026gt;, longitud \u0026lt;chr\u0026gt; Así quedó el resultado de nuestro web scraping. A continuación, usamos la función left_join() para adjuntar estas columnas nuevas a nuestro data frame que contiene los nombres y códigos de las comunas, además de la geometría o información geográfica de las comunas, usando como columna de unión los códigos comunales:\nmapa_datos_2 \u0026lt;- mapa |\u0026gt; left_join(datos_comunas, by = \u0026#34;codigo_comuna\u0026#34;) glimpse(mapa_datos_2) Rows: 52 Columns: 16 $ codigo_comuna \u0026lt;chr\u0026gt; \u0026quot;13404\u0026quot;, \u0026quot;13402\u0026quot;, \u0026quot;13124\u0026quot;, \u0026quot;13103\u0026quot;, \u0026quot;13301\u0026quot;, \u0026quot;13303\u0026quot;,… $ codigo_provincia \u0026lt;chr\u0026gt; \u0026quot;134\u0026quot;, \u0026quot;134\u0026quot;, \u0026quot;131\u0026quot;, \u0026quot;131\u0026quot;, \u0026quot;133\u0026quot;, \u0026quot;133\u0026quot;, \u0026quot;133\u0026quot;, \u0026quot;131… $ codigo_region \u0026lt;chr\u0026gt; \u0026quot;13\u0026quot;, \u0026quot;13\u0026quot;, \u0026quot;13\u0026quot;, \u0026quot;13\u0026quot;, \u0026quot;13\u0026quot;, \u0026quot;13\u0026quot;, \u0026quot;13\u0026quot;, \u0026quot;13\u0026quot;, \u0026quot;13\u0026quot;,… $ geometry \u0026lt;MULTIPOLYGON [°]\u0026gt; MULTIPOLYGON (((-70.61396 -..., MULTIPOL… $ nombre_comuna \u0026lt;chr\u0026gt; \u0026quot;Paine\u0026quot;, \u0026quot;Buin\u0026quot;, \u0026quot;Pudahuel\u0026quot;, \u0026quot;Cerro Navia\u0026quot;, \u0026quot;Colina\u0026quot;,… $ nombre \u0026lt;chr\u0026gt; \u0026quot;Paine\u0026quot;, \u0026quot;Buin\u0026quot;, \u0026quot;Pudahuel\u0026quot;, \u0026quot;Cerro Navia\u0026quot;, \u0026quot;Colina\u0026quot;,… $ x \u0026lt;lgl\u0026gt; NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N… $ provincia \u0026lt;chr\u0026gt; \u0026quot;Maipo\u0026quot;, \u0026quot;Maipo\u0026quot;, \u0026quot;Santiago\u0026quot;, \u0026quot;Santiago\u0026quot;, \u0026quot;Chacabuco\u0026quot;… $ region \u0026lt;chr\u0026gt; \u0026quot;Metropolitana de Santiago\u0026quot;, \u0026quot;Metropolitana de Santia… $ superficie_km2 \u0026lt;dbl\u0026gt; 820, 214, 197, 11, 9712, 653, 452, 448, 107, 3935, 69… $ poblacion2020 \u0026lt;dbl\u0026gt; 82766, 109641, 253139, 142465, 180353, 21477, 126898,… $ densidad_hab_km2 \u0026lt;dbl\u0026gt; 1009.0, 512.3, 1284.9, 12951.3, 185.7, 328.0, 280.7, … $ idh_2005 \u0026lt;chr\u0026gt; \u0026quot;0.718\u0026quot;, \u0026quot;0.731\u0026quot;, \u0026quot;0.735\u0026quot;, \u0026quot;0.683\u0026quot;, \u0026quot;0.726\u0026quot;, \u0026quot;0.709\u0026quot;,… $ idh_2005_2 \u0026lt;chr\u0026gt; \u0026quot;Alto\u0026quot;, \u0026quot;Alto\u0026quot;, \u0026quot;Alto\u0026quot;, \u0026quot;Medio\u0026quot;, \u0026quot;Alto\u0026quot;, \u0026quot;Alto\u0026quot;, \u0026quot;Med… $ latitud \u0026lt;chr\u0026gt; \u0026quot;-33°48'43.2\\\u0026quot;\u0026quot;, \u0026quot;-33°43'40.8\\\u0026quot;\u0026quot;, \u0026quot;-33°26'0\\\u0026quot;\u0026quot;, \u0026quot;-33°… $ longitud \u0026lt;chr\u0026gt; \u0026quot;-70°43'22.8\\\u0026quot;\u0026quot;, \u0026quot;-70°44'20.4\\\u0026quot;\u0026quot;, \u0026quot;-70°43'0\\\u0026quot;\u0026quot;, \u0026quot;-70°… Lo que hicimos en la operación anterior fue unir dos tablas distintas en base a una variable común que ambas tablas poseen: codigo_comuna. De este modo, obtenemos un nuevo data frame que contiene tanto la información geográfica como los datos comunales que necesitamos.\nHabiendo hecho esto, ahora podemos crear gráficos comunales usando cualquier variable que queramos, siempre y cuando podamos hacer coincidir los datos con el mapa en base a los códigos comunales o los nombres de comuna.\nVisualizar datos Mapa comunal de población mapa_datos_2 |\u0026gt; ggplot() + geom_sf(aes(geometry = geometry, fill = poblacion2020), col = \u0026#34;white\u0026#34;) + viridis::scale_fill_viridis(labels = label_number(big.mark = \u0026#34;.\u0026#34;, decimal.mark = \u0026#34;,\u0026#34;)) + theme_void() + labs(fill = \u0026#34;Población\u0026#34;) Mapa comunal del índice de desarrollo humano mapa_datos_2 |\u0026gt; ggplot() + geom_sf(aes(geometry = geometry, fill = idh_2005_2), col = \u0026#34;white\u0026#34;) + scale_fill_manual(breaks = c(\u0026#34;Medio\u0026#34;, \u0026#34;Alto\u0026#34;, \u0026#34;Muy alto\u0026#34;), values = c(\u0026#34;Medio\u0026#34; = \u0026#34;olivedrab4\u0026#34;, \u0026#34;Alto\u0026#34; = \u0026#34;olivedrab3\u0026#34;, \u0026#34;Muy alto\u0026#34; = \u0026#34;olivedrab2\u0026#34;)) + geom_sf_text(aes(geometry = geometry, label = nombre_comuna), check_overlap = T, size = 2) + theme_void() + labs(fill = \u0026#34;IDH\u0026#34;) Warning in st_point_on_surface.sfc(sf::st_zm(x)): st_point_on_surface may not give correct results for longitude/latitude data Sin embargo, podemos ver que estos mapas no se ajustan perfectamente a la imagen mental que tiene un ciudadano común acerca de cómo se ve la Región Metropolitana. Por ejemplo, vemos cómo la comuna de San José de Maipo abarca una superficie enorme dado que limita en la cordillera de los Andes con Argentina, o que comunas como Lo Barnechea se expanden hacia superficies cordilleranas de gran extensión.\nEsto se debe a que ususalmente nos encontramos frente a mapas que representan el \u0026ldquo;Gran Santiago\u0026rdquo;, es decir, sólo la superficie urbana de las comunas urbanas de la región, omitiendo sectores rurales, cordilleranos o deshabitados.\nPor lo tanto, a continuación veremos cómo obtener un mapa urbano de la Región Metropolitana de Santiago.\nMapa urbano de la región Metropolitana En los siguientes pasos, pasaremos de un mapa comunal a un mapa comunal urbano; es decir, un mapa que sólo considere la superficie urbana de las comunas, en vez de la superficie total de las comunas.\nUsando {chilemapas}, podemos obtener un mapa de la Región Metropolitana con un nivel de detalle mayor, que divide internamente las comunas en superficies más pequeñas que sólo corresponden a zonas urbanas:\n# obtener mapa por zonas rural/urbano mapa_zonas_urbanas \u0026lt;- chilemapas::mapa_zonas |\u0026gt; # definir geometrías st_set_geometry(chilemapas::mapa_zonas$geometry) |\u0026gt; # filtrar región filter(codigo_region == 13) |\u0026gt; # agregar nombres de comunas left_join(chilemapas::codigos_territoriales |\u0026gt; select(matches(\u0026#34;comuna\u0026#34;))) Joining with `by = join_by(codigo_comuna)` # mapa de zonas urbanas mapa_zonas_urbanas |\u0026gt; ggplot(aes(geometry = geometry)) + geom_sf(fill = \u0026#34;grey60\u0026#34;, color = \u0026#34;white\u0026#34;) + theme_void() Podemos mejorar esta visualización uniendo con st_union() las zonas urbanas intra-comunales en sus respectivas comunas, para volver a obtener un mapa comunal, pero que recorta las comunas para que sólo consideren su la superficie urbana de cada una:\n# mapa de zonas urbanas mapa_zonas_urbanas |\u0026gt; # unir polígonos por comunas group_by(nombre_comuna, codigo_comuna) %\u0026gt;% summarise(geometry = st_union(geometry), .groups = \u0026#34;drop\u0026#34;) |\u0026gt; # visualizar ggplot() + geom_sf(fill = \u0026#34;grey60\u0026#34;, color = \u0026#34;white\u0026#34;) + theme_void() De inmediato, podemos ver que emerge una figura más familiar de lo que es el Gran Santiago, pero ahora tenemos otro problema: el mapa también contiene las zonas urbanas de comunas menos céntricas de la región, tales como Buin, Curacaví, Talagante y otras. Esto se debe a que nuestro mapa aún contiene comunas que no son mayoritariamente urbanas, dado que poseen sectores despoblados o de actividad agrícola, minera u otras, y que por consiguiente dan una apariencia discontinua a nuestro mapa.\nPara resolver esto y dejar sólo las comunas urbanas del Gran Santiago, tenemos varias opciones: podemos filtrar específicamente las comunas que queremos, o bien, podemos filtrar en base a privincias, dejando sólo las provincias Santiago y Cordillera.\nDefinir contorno urbano por comunas exactas Opción 1: seleccionar específicamente las comunas que queremos incluir:\ncomunas_urbanas \u0026lt;- c(\u0026#34;Pudahuel\u0026#34;, \u0026#34;Cerro Navia\u0026#34;, \u0026#34;Conchali\u0026#34;, \u0026#34;La Pintana\u0026#34;, \u0026#34;El Bosque\u0026#34;, \u0026#34;Estacion Central\u0026#34;, \u0026#34;Pedro Aguirre Cerda\u0026#34;, \u0026#34;Recoleta\u0026#34;, \u0026#34;Independencia\u0026#34;, \u0026#34;La Florida\u0026#34;, \u0026#34;Penalolen\u0026#34;, \u0026#34;Las Condes\u0026#34;, \u0026#34;Lo Barnechea\u0026#34;, \u0026#34;Quinta Normal\u0026#34;, \u0026#34;Maipu\u0026#34;, \u0026#34;Macul\u0026#34;, \u0026#34;Nunoa\u0026#34;, \u0026#34;Puente Alto\u0026#34;, \u0026#34;Quilicura\u0026#34;, \u0026#34;Renca\u0026#34;, \u0026#34;San Bernardo\u0026#34;, \u0026#34;San Miguel\u0026#34;, \u0026#34;La Granja\u0026#34;, \u0026#34;Providencia\u0026#34;, \u0026#34;Santiago\u0026#34;, \u0026#34;San Joaquin\u0026#34;, \u0026#34;Lo Espejo\u0026#34;, \u0026#34;La Reina\u0026#34;, \u0026#34;San Ramon\u0026#34;, \u0026#34;La Cisterna\u0026#34;, \u0026#34;Lo Prado\u0026#34;, \u0026#34;Cerrillos\u0026#34;, \u0026#34;Vitacura\u0026#34;, \u0026#34;Huechuraba\u0026#34;, \u0026#34;San Jose de Maipo\u0026#34;) # mapa de sectores urbanos, de comunas urbanas mapa_zonas_urbanas |\u0026gt; # filtrar comunas urbanas filter(nombre_comuna %in% comunas_urbanas) |\u0026gt; # unir polígonos por comunas group_by(nombre_comuna, codigo_comuna) %\u0026gt;% summarise(geometry = st_union(geometry)) |\u0026gt; # graficar ggplot() + geom_sf(fill = \u0026#34;grey60\u0026#34;, color = \u0026#34;white\u0026#34;) + theme_void() `summarise()` has grouped output by 'nombre_comuna'. You can override using the `.groups` argument. Definir contorno urbano en base a provincias Opción 2: seleccionar las dos provincias que conforman el Gran Santiago, y agregar los ajustes que sean necesarios (incluir San Bernardo, excluir Pirque)\nmapa_zonas_urbanas |\u0026gt; # dejar solo dos provincias, incluir San Bernardo y sacar Pirque filter(codigo_provincia %in% c(131, 132) | nombre_comuna == \u0026#34;San Bernardo\u0026#34;, nombre_comuna != \u0026#34;Pirque\u0026#34;) |\u0026gt; # unir polígonos por comunas group_by(nombre_comuna, codigo_comuna) %\u0026gt;% summarise(geometry = st_union(geometry)) |\u0026gt; # graficar ggplot() + geom_sf(fill = \u0026#34;grey60\u0026#34;, color = \u0026#34;white\u0026#34;) + theme_void() `summarise()` has grouped output by 'nombre_comuna'. You can override using the `.groups` argument. La decisión que tomes depende de los objetivos del usuario y de tu visualzación, pero dejo ambas aproximaciones a modo de aprendizaje.\nExcluir islas urbanas Luego de haber seleccionado las comunas urbanas que necesitamos, notamos que aún quedan algunas \u0026ldquo;islas urbanas\u0026rdquo; fuera de la zona principal del Gran Santiago. Usualmente vemos los mapas del Gran Santiago como una sola unidad geográfica contínua, sin separaciones ni islas a su alrededor. Por lo tanto, vamos a eliminar estos elementos externos a la superficie urbana contínua de forma manual.\nPara identificar los polígonos que queramos remover, podemos visualizar una fracción del mapa y agregar etiquetas para dar con sus códigos geográficos, y así poder excluirlos. Por ejemplo, aquí lo haremos con Pudahuel:\nmapa_zonas_urbanas |\u0026gt; filter(nombre_comuna == \u0026#34;Pudahuel\u0026#34;) |\u0026gt; ggplot() + geom_sf(fill = \u0026#34;lightblue\u0026#34;, color = \u0026#34;white\u0026#34;) + geom_sf_text(aes(label = geocodigo), color = \u0026#34;black\u0026#34;, size = 3) + theme_void() Warning in st_point_on_surface.sfc(sf::st_zm(x)): st_point_on_surface may not give correct results for longitude/latitude data Bastaría con anotar los geocódigos para filtrarlos.\nEntonces, en el siguiente paso removeremos estas pequeñas zonas urbanas de forma manual para obtener un mapa más contínuo.\n# vector con geocódigos que deseamor remover islas_urbanas \u0026lt;- c(\u0026#34;13124071004\u0026#34;, \u0026#34;13124071005\u0026#34;, \u0026#34;13124081001\u0026#34;, \u0026#34;13124071001\u0026#34;, \u0026#34;13124071002\u0026#34;, \u0026#34;13124071003\u0026#34;, #Pudahuel \u0026#34;13401121001\u0026#34;, #San Bernardo \u0026#34;13119131001\u0026#34;, #Maipú \u0026#34;13203031000\u0026#34;, \u0026#34;13203031001\u0026#34;, \u0026#34;13203031002\u0026#34;, \u0026#34;13203011001\u0026#34;, \u0026#34;13203011002\u0026#34; #San José de Maipo ) # crear nuevo mapa mapa_urbano \u0026lt;- mapa_zonas_urbanas |\u0026gt; # filtrar solo comunas urbanas filter(nombre_comuna %in% comunas_urbanas) |\u0026gt; # filtrar islas urbanas filter(!geocodigo %in% islas_urbanas) |\u0026gt; # unir comunas group_by(nombre_comuna, codigo_comuna) %\u0026gt;% summarise(geometry = st_union(geometry)) |\u0026gt; ungroup() `summarise()` has grouped output by 'nombre_comuna'. You can override using the `.groups` argument. # simplificar bordes del mapa (opcional) # mutate(geometry = rmapshaper::ms_simplify(geometry, keep = 0.4)) # graficar mapa_urbano |\u0026gt; ggplot() + geom_sf(fill = \u0026#34;blueviolet\u0026#34;, color = \u0026#34;white\u0026#34;) + theme_void() De esta forma ya logramos graficar un mapa del Gran Santiago mucho más definido y limpio.\nVisualizar datos en el mapa urbano Teniendo este mapa, procedemos a visualizar nuestros datos tal como hicimos al principio de este tutorial:\n# volvemos a adjuntar los datos que descargamos usando web scraping, esta vez al mapa nuevo mapa_urbano_2 \u0026lt;- mapa_urbano |\u0026gt; left_join(datos_comunas, by = \u0026#34;codigo_comuna\u0026#34;) mapa_urbano_2 |\u0026gt; ggplot() + aes(fill = densidad_hab_km2) + geom_sf(col = \u0026#34;white\u0026#34;) + viridis::scale_fill_viridis(labels = label_number(big.mark = \u0026#34;.\u0026#34;, decimal.mark = \u0026#34;,\u0026#34;), option = \u0026#34;magma\u0026#34;) + theme_void() + labs(fill = \u0026#34;Densidad poblacional\u0026#34;) Finalmente, podemos poner nuestro nuevo mapa urbano de la Región Metropolitana de Santiago sobre el mapa de la región completa:\nggplot() + geom_sf(data = mapa, fill = \u0026#34;grey95\u0026#34;, color = \u0026#34;white\u0026#34;, linewidth = 0.5) + geom_sf(data = mapa_urbano_2, aes(fill = poblacion2020), color = \u0026#34;white\u0026#34;, linewidth = 0.2) + viridis::scale_fill_viridis(labels = label_number(big.mark = \u0026#34;.\u0026#34;, decimal.mark = \u0026#34;,\u0026#34;), option = \u0026#34;mako\u0026#34;) + theme_void() + labs(fill = \u0026#34;Población\u0026#34;) + guides(fill = guide_colourbar(position = \u0026#34;inside\u0026#34;)) + theme(legend.position.inside = c(.1, .7), legend.key.width = unit(3, \u0026#34;mm\u0026#34;), legend.key.height = unit(10, \u0026#34;mm\u0026#34;), legend.ticks.length = unit(0.4, \u0026#34;mm\u0026#34;)) También podemos recortar el mapa con coord_sf() para hacerle un poco de zoom a la zona urbana dentro del contexto de la región completa:\nggplot() + geom_sf(data = mapa, aes(geometry = geometry), fill = \u0026#34;grey90\u0026#34;, color = \u0026#34;white\u0026#34;, linewidth = 0.4) + geom_sf(data = mapa_urbano_2, aes(geometry = geometry, fill = poblacion2020), color = \u0026#34;white\u0026#34;, linewidth = 0.2) + coord_sf(xlim = c(-70.95, -70.33), ylim = c(-33.75, -33.2), expand = F) + viridis::scale_fill_viridis(labels = label_number(big.mark = \u0026#34;.\u0026#34;, decimal.mark = \u0026#34;,\u0026#34;), option = \u0026#34;mako\u0026#34;) + theme_void() + labs(fill = \u0026#34;Población\u0026#34;) + theme(legend.position.inside = c(.1, .7), legend.key.width = unit(3, \u0026#34;mm\u0026#34;), legend.key.height = unit(10, \u0026#34;mm\u0026#34;), legend.ticks.length = unit(0.4, \u0026#34;mm\u0026#34;)) ","date":"2024-06-12T00:00:00Z","excerpt":"\u003cp\u003eEste tutorial de R te explicará paso a paso a cómo obtener mapas de todo Chile usando el paquete \n\u003ca href=\"https://github.com/pachadotdev/chilemapas\" target=\"_blank\" rel=\"noopener\"\u003e\u003ccode\u003e{chilemapas}\u003c/code\u003e desarrollado por Mauricio Vargas\u003c/a\u003e, y hacer gráficos con estos mapas usando \u003ccode\u003e{ggplot2}\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eEn la primera parte veremos cómo \u003cstrong\u003eobtener los mapas\u003c/strong\u003e y cómo \u003cstrong\u003evisualizar datos comunales\u003c/strong\u003e usando mapas en R. Si necesitas una guía sobre mapas en R, \n\u003ca href=\"../../../blog/mapas_sf/\"\u003erevisa este post.\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eLuego, nos enfrentaremos a un problema común que se tiene al graficar un mapa de la Región Metropolitana de Santiago, que tiene que ver con la diferencia entre los límites comunales reales de cada comuna y los \u003cstrong\u003elímites urbanos\u003c/strong\u003e de las comunas. Es la diferencia entre tener un mapa de la RM que abarque sectores rurales como Paine y que llegue hasta Argentina, o un mapa que demarque la zona urbana de Santiago, aproximadamente correspondiente a la zona que atravieza el anillo de la autopista Américo Vespucio.\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/tutorial_mapa_urbano/","tags":"mapas ; Chile","title":"Tutorial: Mapa de la zona urbana de la Región Metropolitana de Santiago en R"},{"content":" Aplicación web tipo dashboard que busca presentar de forma sencilla y compacta los principales indicadores de la economía chilena.\nLos datos de este repositorio se actualizan automáticamente cada 12 horas por medio de GitHub Actions. Estos datos se obtienen realizando web scraping al sitio web del Banco Central. usando el paquete de R {rvest}.\nIndicadores disponibles PIB IMACEC IPC IPSA UF Tasa de desempleo Índice de remuneraciones reales Inversión extranjera (nuevo) Precio del cobre (nuevo) Actualizaciones Actualización 22/07/2024 Indicador de fecha de actualización de los datos, y de dato más reciente (excluyendo la UF, que es diaria) Ajustes menores de la interfaz Actualización 11/07/2024 Nuevo indicador: inversión extranjera directa (IED) Nuevo indicador: precio del cobre Interpretación automática de las tendencias de aumento/disminución Lanzamiento 08/07/2024 Lanzamiento de la app\nFuncionamiento El script obtener_datos.R realiza el web scraping desde el Banco Central, y guarda los resultados sólo si es que encuentra diferencias con los datos preexistentes. Este script es ejecutado cada 12 horas en GitHub Actions. Toma aprox. 5 minutos en ejecutarse, y si encuentra datos nuevos, los sube al repositorio. El script de automatización, con las especificaciones del contenedor que crea, se encuentran en .github/workflows/scrapear_bancocentral.yaml.\nLa aplicación web, por su parte, carga los datos directamente desde el repositorio de GitHub, en formato .csv, y por lo tanto, la aplicación cuenta con datos actualizados sin necesidad de actualizar la aplicación misma, ya que obtiene sus datos remotamente. Si por algún motivo no se pudieran cargar los datos desde el repositorio, la app tiene versiones antiguas de los datos como plan B.\nPodrían obtenerse los datos desde la app directamente por medio de scraping usando las mismas funciones que se automatizan en el workflow de GitHub Actions, pero sería poco considerado con el servidor del Banco Central. Del mismo modo, podría optimizarse la carga de los datos, ya que actualmente cada dato se guarda en un solo archivo .csv, pero no creo que valga la pena volver más engorroso el proceso para ahorrar 4 segundos. Finalmente, otro punto de optimización sería que toda la app fuera un reporte Quarto estático.\nLa app en sí se caracteriza por estar completamente optimizada en su estructura de código, dado que todos los elementos son generados con funciones. Por lo tanto, basta con copiar y pegar aproximadamente 6 bloques breves de código para agregar un indicador nuevo, incluyendo la automatización de su obtención de datos.\nFuentes Banco Central, Base de datos estadística Banco Central, Inversión Extranjera Directa Banco Central, Indicadores diarios: Precio del Cobre, dólares por libra (Dólar) Referencias https://www.gavinrozzi.com/post/automating-scraping-gh-actions/ https://si3.bcentral.cl/SetGraficos/ ","date":"2024-05-25T00:00:00Z","excerpt":"Tablero que presenta +8 indicadores económicos del Banco Central de Chile, cuya presentación resumida permite analizar la situación económica del país. Los datos de esta aplicación son obtenidos de forma automática dos veces al día, garantizando que se encuentren actualizados. Además, la arquitectura de esta app facilita el proceso de añadir nuevos indicadores.","href":"https://bastianoleah.netlify.app/blog/app_economia_chile/","tags":"apps ; web scraping ; Chile","title":"App: Indicadores económicos de Chile"},{"content":" Aplicación web que permite visualizar datos sobre los casos más relevantes de corrupción en Chile.\nEste repositorio compila datos abiertos sobre este tema país, y produce gráficos que permiten analizar cómo y desde dónde ha operado la corrupción en Chile.\nLos casos de corrupción incluidos son aquellos donde se involucre perjuicio económico a recursos públicos, sector público, o al fisco en general.\nLos datos son recopilados manualmente, y se obtienen desde fuentes periodísticas. El criterio de inclusión para cada caso es que existan fuentes confiables de prensa que indiquen la existencia de una investigación, evidencia plausible de posible corrupción, o bien una sentencia o condena.\nCasos de corrupción por monto Tabla de casos de corrupción donde implicados tengan afiliación a partidos políticos Casos de corrupción en municipios Tabla de casos de corrupción donde implicados tengan afiliación a partidos políticos Mapa de casos de corrupción en municipalidades de la Región Metropolitana Metodología Los datos son organizados en una planilla con sus respectivas fuentes y la información disponible en casa caso. Accede a la carpeta datos para revisarlos.\nFuente de los datos Los datos están siendo compilados manualmente en este repositorio, por lo que se trata de una aplicación en constante proceso. Si quieres complementar los datos existentes, ayudar con correcciones, agregar casos nuevos, o hacer cualquier comentario, puedes encontrar los datos en el repositorio, o bien, contactarme por alguno de los medios disponibles en mi sitio web.\nOtras fuentes de datos:\nEncuesta CEP, cuyos datos son obtenidos a través del Graficador CEP, visualizador de datos de la Encuesta CEP programado por Bastián Olea Herrera como parte del equipo DataUC. Índice de percepción de la corrupción (Corruption Perceptions Index), Tranparency International Acceder a la app La aplicación web está disponible en shinyapps.io, o bien, puedes clonar este repositorio en tu equipo para usarla por medio de RStudio.\nSobre la app El sistema de análisis de datos está programado en R, y la app misma está desarrollada en Shiny, un paquete de R para el desarrollo de aplicaciones web interactivas.\nAlgunas peculiaridades de la app:\nGráfico de barras conformadas por puntos, y divisibles en múltiples barras por caso: Se aplica un procesamiento peculiar a los datos de montos de corrupción para generar el gráfico de pseudo-barras, que usa puntos en lugar de barras. Por un lado, se deben transformar los montos a una escala común, donde cada unidad es representada por un punto, para así obtener un gráfico de pictogramas. El escalamiento es sencillo, y en este caso se hace entre 1 y 15, donde el mayor monto sería representado por 15 círculos. Pero luego, y ya que algunos montos son demasiado grandes en comparación con otros, estas barras constituidas por círculos son divididas en múltiples barras, para que las barras grandes no ocupen tanto espacio en el gráfico. Dado que esta no es una funcionalidad nativa al paquete de visualización de datos que se usa, ggplot2, usamos ingenisamente el sistema de facetas para que cada caso se ubique en una faceta del gráfico, y así puedan haber múltiples barras que correspondan a un solo caso. Finalmente, se realizan varios procedimientos para que las etiquetas, nombres de los casos, y otros elementos queden correctamente alineadas con estas barras triples, dado que de lo contrario un caso con tres barras tendría su nombre puesto tres veces.\nGráfico de comparación de montos en su equivalente a precios de otras cosas, que mantiene sus proporciones: El gráfico que permite comparar montos de casos de corrupción con su equivalente en vehículos, propiedades o incluso hospitales, tiene la particularidad de que presenta cientos o miles de cuadraditos pequeños. Sin embargo, estos cuadraditos siempre se ven equidistantes y ordenados, sin importar el ancho de la ventana del usuario. Esto se hace definiendo una cantidad de cuadraditos por fila, y luego se calcula cuantos pixeles usarían esa cantidad de cuadraditos en el tamaño de ventana del usuario, para usar esa proporción al momento de definir el largo del gráfico. De esa forma, los cuadraditos usarán el tamaño de la ventana o pantalla del usuario para un efecto más impactante, pero siempre se verán ordenados.\nCarga de elementos en base al desplazamiento del usuario: Dado que se decidió que la aplicación sea larga hacia abajo, resulta que hay demasiados elementos por cargarse al abrir el sitio, sobre todo algunos gráficos y tablas que son de tamaño considerable. Para agilizar la carga del sitio, se implementó código en JavaScript que permite obtener la posición vertical del usuario en la ventana como un input para Shiny; es decir, podemos saber si el usuario está en la parte superior de la app, o si se ha desplazado hacia abajo. De este modo, solamente cargamos los elementos grandes al final del sitio si el usuario ha hecho scroll lo suficiente como para ver dichos elementos.\nPantallazos Estructura del proyecto Esta breve sección explica a grandes rasgos cómo funciona este proyecto, en términos programáticos.\nTodo el contenido de este repositorio depende de un archivo de datos en formato Excel, datos/casos_corrupcion_chile.xlsx. Este archivo es producido a partir de datos/casos_corrupcion_chile.numbers, que es una planilla de Apple Numbers, pero también podría editarse directamente el Excel. En esta planilla se almacenan manualmente todos los datos de la plataforma.\nLos datos de la plataforma se procesan ejecutando el script corrupcion_procesar.R, que procesa los datos desde el archivo Excel para dejarlos en formato .rds (nativo de R) en el archivo app/corrupcion_datos.rds. Este archivo es el que alimenta la aplicación y todos los gráficos y tablas.\nEl script corrupcion_graficar.R ejecuta varios otros scripts que producen gráficos, tablas y mapas a partir de los datos procesados, app/corrupcion_datos.rds. Los gráficos, tablas y mapas se guardan en las carpetas graficos, tablas y mapas, respectivamente, con nombres de archivo con o sin fecha. Los archivos sin fecha son usados en este readme.md para mantener actualizadas las visualizaciones estáticas, y los con fecha se guardan sólo por temas de archivación de versiones anteriores.\nDiseñado y programado en R por Bastián Olea Herrera. Magíster en Sociología, data scientist.\nhttps://bastianolea.rbind.io\nbastianolea@gmail.com\nAgradecimientos:\nMiguel Oyarzo, por sus comentarios, contribución de datos nuevos e ideas Rodrigo Rettig, por sus contribución de datos nuevos y difusión Citar:\nOlea H., B. (2024). Corrupción en Chile. Obtenido desde https://github.com/bastianolea/corrupcion_chile\nhttps://osf.io/xmnbd/\n","date":"2024-05-20T00:00:00Z","excerpt":"Catálogo y visualizador de los casos de corrupción más trascendentes del último tiempo en Chile, para poner en perspectiva los montos, responsables, y sectores políticos asociados. Los datos son recopilados manualmente para producir una tabla con la mayor información posible sobre casos de corrupción, incluyendo responsables, delitos específicos, afiliación a partidos políticos, fundaciones involucradas y más, para alientar visualizaciones interactivas que permitan a la cuidadanía comprender de dónde viene la corrupción y cómo nos afecta como país.","href":"https://bastianoleah.netlify.app/blog/app_corrupcion_chile/","tags":"apps ; datos ; Chile ; gráficos ; tablas","title":"App: Corrupción en Chile"},{"content":" Aplicación web interactiva sobre las fortunas de los empresarios más ricos de Chile.\nEn toda economía de mercado existen personajes que acaparan vastas riquezas, ya sea por el éxito de sus negocios, por poseer recursos clave, haber recibido herencias o ser sucesores de otros magnates, o bien, por haber ejercido estrategias cuestionables para el enriquecimiento propio.\nCon este visualizador puedes poner en perspectiva las fortunas de los millonarios mas grandes del país, comparando sus fortunas con los ingresos de los chilenos, la teletón, tu propio sueldo, y más, para así dimensionar un aspecto clave de la desigualdad en Chile y el mundo.\nFuentes Los datos usados en el visualizador se obtienen de diversas fuentes, principalmente la tabla de Billonarios de Forbes obtenida desde Kaggle, pero posteriormente compilados de forma manual en el archivo millonarios_chile en la carpeta datos.\nLos datos sobre los ingresos del país, deciles de ingresos y otros se obtienen de la encuesta Casen 2022, cuyos datos son dercargados en el script casen2022_importar.R, y luego preprocesados y calculados en los scripts casen2022_procesar.R y casen2022_calcular.R.\nLa población de Chile se obtiene desde las proyecciones del Censo, cuyos datos se descargan, limpian y procesan en el script poblacion_obtener.R.\nEl precio del dólar se obtiene mediante web scraping del sitio del Banco Central, por medio de la función obtener_dolar() en funciones.R.\nFuentes de datos Lista de billonarios de Forbes 2023, obtenido desde Kaggle El Mostrador Rankia El Desconcierto Ciper Chile Ciper Chile Bio Bío Chile Diseñado y programado en R por Bastián Olea Herrera. Magíster en Sociología, data scientist.\n","date":"2024-03-13T00:00:00Z","excerpt":"Con este visualizador puedes poner en perspectiva las fortunas individuales más grandes del país, para así dimensionar un aspecto clave de la desigualdad en Chile y el mundo. Diversas fuentes de datos permiten recopilar un listado de los empresarios más ricos de Chile. Distintas técnicas estadísticas y de visualización permiten dimensionar las enormes fortunas de estas personas, por ejemplo, comparando con los propios ingresos del usuario, o con los ingresos de toda la población del país.","href":"https://bastianoleah.netlify.app/blog/app_millonarios_chile/","tags":"apps ; Chile","title":"App: Millonarios de Chile"},{"content":" Aplicación web para la visualización de datos del registro de femicidios realizado por la Red Chilena contra la Violencia hacia las Mujeres desde 2010 en adelante.\nEste proyecto incluye scripts de R para descargar todos los datos oficiales recopilados por la Red (obteniendo los enlaces usando web scraping y descargándolos desde Google Docs), limpiarlos, y procesarlos para su visualización, además de la web app de visualización, desarrollada con Shiny.\nLa georeferenciación de los datos se basa en la información de localidad o comuna, y cuando no se entrega información de comuna, se aplica la capital regional de la región indicada.\nLos datos se encuentran actualizados a octubre de 2024.\nSobre los femicidios, la Red Chilena contra la Violencia hacia las Mujeres plantea:\nLa teoría feminista conceptualiza el femicidio como un crimen misógino que refleja, en grado extremo, el sentido de propiedad, dominación y control que ejercen los hombres hacia las mujeres en las sociedades patriarcales.\nAccede a la aplicación web en este enlace\nMapa georeferenciado de femicidios en Chile Tabla de femicidios en Chile, año 2024 Fuentes Red Chilena contra la Violencia hacia las Mujeres, quienes en su exhaustiva recopilación plantean \u0026ldquo;esta base de datos es elaborada y administrada por la Red Chilena contra la Violencia hacia las Mujeres. El uso de los datos está a disposición de todas, todos y todes, citando la fuente.\u0026rdquo; Puedes acceder al registro realizado por la Red en este enlace. ![](pantallazos/Femicidios en Chile.jpg) ![](pantallazos/Femicidios en Chile 2.jpg)\n","date":"2024-02-24T00:00:00Z","excerpt":"Sitio con gráficos y tablas que expresan en cifras los datos de femicidios cometidos en Chile. Estos datos, mantenidos por la Red Chilena contra la Violencia hacia las Mujeres, expresan la brutalidad manifestada de una sociedad patriarcal donde la violencia es una realidad transversal, llevada a su extremo en la agresión y asesinato de mujeres por razones de género.","href":"https://bastianoleah.netlify.app/blog/app_femicidios_chile/","tags":"apps ; datos ; Chile ; género","title":"App: Femicidios en Chile"},{"content":" Aplicación web que permite visualiza variables desde la encuesta Casen 2022, explicitando las brechas de género existentes en Chile mediante un gráfico de puntos por género y región.\nEste visualizador permite explorar con facilidad las múltiples desigualdades de género que son medibles empíricamente utilizando los datos más recientes de la Encuesta de caracterización socioeconómica nacional (Casen) 2022.\nLas variables fueron seleccionadas por el interés general que puedan tener, y no por la existencia o no de brechas de género en ellas. Si se te ocurren otras variables que incluir, puedes contactarme!\nTanto la aplicación como el procesamiento de los datos, incluida la descarga de los datos oficiales de la Casen, están programadas en R y disponibles en este repositorio.\nLa aplicación web está disponible en shinyapps.io, o bien, puedes clonar este repositorio en tu equipo para reproducir todos los cálculos y usarla por medio de RStudio.\nDiseñado y programado en R por Bastián Olea Herrera. Magíster en Sociología, data scientist.\nhttps://bastianolea.rbind.io\nbastianolea arroba gmail punto com\n","date":"2024-01-31T00:00:00Z","excerpt":"Visualizador que detalla brechas de género en temas sociales, de vivienda e ingresos, para analizar variables en las que las mujeres experimentan peores condiciones de vida, a nivel regional Selecciona una de las variables disponibles para generar un gráfico con todas las regiones del país, donde se detalla el porcentaje de la población femenina y masculina afectada por la variable seleccionada, o si eliges variables de vivienda o familia, el porcentaje de hogares con jefatura femenina o masculina correspondientes. Los puntos del gráfico además se detallan con las brechas o diferencias entre géneros existentes, volviendo explícitas las desigualdades o ausencia de las mismas.","href":"https://bastianoleah.netlify.app/blog/app_casen_genero_brechas/","tags":"apps ; Chile ; género","title":"App: Brechas de género Casen"},{"content":" Aplicación web para generar gráficos de densidad que representan los ingresos de la población de cualquier comuna del país, o de varias comunas a la vez, para poder comparar las realidades económicas de sus habitantes.\nUn gráfico de densidad indica cómo se distribuye una población con respecto a una variable indicada en el eje horizontal. En este caso, el eje horizontal corresponde a una escala de ingresos, y el eje vertical es la proporción de la población.\nLa visualización indica en qué tramos de ingresos se ubican las personas de la comuna seleccionada, donde la altura de la curva representa a una mayor proporción de las personas que perciben los ingresos que indica el eje horizontal. Por ejemplo, un gráfico con mucha altura en la parte inferior de la escala (izquierda) significa que la mayoría de la población percibe ingresos bajos, o un gráfico con mucha altura simultáneamente en los extremos izquierdo y derecho representaría una población con alta desigualdad y polarización de ingresos.\nLos datos provienen de la Encuesta de caracterización socioeconómica nacional (Casen) 2022.\nLa aplicación web está disponible en shinyapps.io, o bien, puedes clonar este repositorio en tu equipo para usarla por medio de RStudio.\n","date":"2024-01-21T00:00:00Z","excerpt":"Visualizador que compara distribuciones y promedios de ingresos entre las comunas de Chile, para observar las diferencias en las realidades socioeconómicas del país. Selecciona un grupo de comunas, y elige una variable de ingresos, como ingresos individuales, ingresos por hogar, ingresos per cápita o montos de pensiones/jubilación, para obtener un gráfico de densidad que describe y compara las poblaciones de las comunas, y un gráfico de dispersión que ubica los ingresos de las comunas seleccionadas en comparación a todas las demás comunas del país.","href":"https://bastianoleah.netlify.app/blog/app_casen_comparador_ingresos/","tags":"apps ; datos ; Chile","title":"App: Comparador de ingresos Casen"},{"content":" Aplicación web que permite comparar visualmente múltiples variables desde la encuesta Casen 2022, las cuales se visualizan en un gráfico de dispersión por comunas o regiones. Por defecto, al abrir la app se eligen variables al azar.\nEl visualizador permite analizar la relación entre múltiples datos socioeconómicos de las comunas del país, en base a los datos de la Encuesta de caracterización socioeconómica nacional (Casen) 2022.\nEl gráfico expresa cómo se posicionan las comunas entre dos ejes que pueden representar ingresos, condiciones de vida, o situaciones de vulnerabilidad, expresando así la relación entre las desigualdades y condiciones de vida del país.\nTanto la aplicación como el procesamiento de los datos, incluida la descarga de los datos oficiales de la Casen, están programadas en R y disponibles en este repositorio.\nLa aplicación web está disponible en shinyapps.io, o bien, puedes clonar este repositorio en tu equipo para usarla por medio de RStudio.\nInformación técnica Para producir los datos, hay que ejecutar los scripts en el siguiente orden:\ncasen2022_importar.r: descarga la base de datos original de la encuesta Casen desde su fuente casen2022_preprocesar.r: carga la base de datos descargada en el paso anterior y la deja guardada en un formato más eficiente casen2022_calcular.r: determina las variables a utilizar en el visualizador, y luego calcula conteos, promedios, medianas y porcentajes, según corresponda para cada variable. Las variables de ingresos son representadas por la mediana comunal, las demás variables numéricas son promedios, y las que no son numéricas (por ejemplo, nacionalidad, pobreza, etc.) son calculadas como conteo de la población comunal, y luego transformado en promedio de la población comunal. Diseñado y programado en R por Bastián Olea Herrera. Magíster en Sociología, data scientist.\n","date":"2024-01-14T00:00:00Z","excerpt":"Visualizador que permite relacionar hasta 3 variables socioeconómicas en un gráfico de dispersión por comunas, para analizar la relación entre ellas. Este visualizador permite experimentar correlaciones con numerosas variables de temas como ingresos, educación, condiciones de vida, condiciones laborales, y más, dado que permite utilizar libremente cualquiera de ellas como los ejes del gráfico, creando así visualizaciones personalizadas. Por ejemplo, se puede explorar si las comunas con bajo nivel educacional promedio son también las de menores ingresos, si es que las comunas con viviendas de menor calidad y menores ingresos se correlacionan con mayor hacinamiento o no, si las comunas de altos ingresos tienen menores afiliados a Fonasa, y más.","href":"https://bastianoleah.netlify.app/blog/app_casen_relacionador/","tags":"apps ; datos ; Chile ; ciencias sociales","title":"App: Relacionador de datos sociales Casen"},{"content":"En este script detallaré cómo descargar datos de estadísticas delictuales del Centro de Estudios y Análisis del Delito (CEAD) de Chile utilizando técnicas de web scraping en R. Las estadísticas disponibles en el sitio web de CEAD corresponden a los siguientes datos oficiales: Estadísticas Oficiales de Delitos de Mayor Connotación Social (DMCS), Violencia Intrafamiliar (VIF), Incivilidades y otros hechos informados por Carabineros y la Policía de Investigaciones de Chile al Ministerio del Interior y Seguridad Pública.\nEl proceso implica analizar el funcionamiento del sitio web de CEAD para poder replicar su método de solicitud de datos en R, y así acceder de forma directa en R a los datos que necesitemos desde el sitio, sin necesidad de entrar al sitio web ni realizar operaciones manuales. Esto resulta conveniente dado que puede ser engorroso utilizar la interfaz del sitio web de CEAD para obtener los datos manualmente, o bien, si se necesita obtener un gran volumen de datos desde el sitio. Por lo demás, obtener los datos de forma manual mediante el sitio web no es una práctica reproducible, mientras que siguiendo estas instrucciones obtenemos un conjunto de funciones que hacen que el proceso de obtención de estadísticas delictuales sea automático, reproducible y sencillo.\nFuncionamiento del sitio web del CEAD El sitio web del CEAD ofrece una interfaz web donde se pueden obtener tablas con datos delictuales por comuna o región, para los delitos o grupos de delitos especificados, y para los años que se le indique. Luego de hacer selecciones para obtener los datos en el sitio de CEAD, podemos ingresar al inspector web de nuestro navegador, y en el apartado de red podemos identificar en el documento get_estadisticas_delictuales.php que las selecciones que hicimos se traducen en una solicitud XML que se envía al servidor de CEAD y retorna los datos correspondientes.\nA modo de ejemplo, realicemos una selección sencilla en la web de CEAD:\nEn el inspector web del navegador (en este caso Safari), vamos a red, identificamos get_estadisticas_delictuales.php, y en el menú contextual elegimos copiar como cURL para ver la solicitud que se envió al servidor de CEAD para obtener los datos:\nAdjunto un ejemplo de cURL:\ncurl \u0026#39;https://cead.spd.gov.cl/wp-content/themes/gobcl-wp-master/data/get_estadisticas_delictuales.php\u0026#39;-X \u0026#39;POST\u0026#39;-H \u0026#39;Content-Type: application/x-www-form-urlencoded; charset=UTF-8\u0026#39;-H \u0026#39;Accept: /\u0026#39;-H \u0026#39;Sec-Fetch-Site: same-origin\u0026#39;-H \u0026#39;Accept-Language: es-419,es;q=0.9\u0026#39;-H \u0026#39;Accept-Encoding: gzip, deflate, br\u0026#39;-H \u0026#39;Sec-Fetch-Mode: cors\u0026#39;-H \u0026#39;Host: cead.spd.gov.cl\u0026#39;-H \u0026#39;Origin: https://cead.spd.gov.cl\u0026#39;-H \u0026#39;Content-Length: 674\u0026#39;-H \u0026#39;User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15\u0026#39;-H \u0026#39;Referer: https://cead.spd.gov.cl/estadisticas-delictuales/\u0026#39;-H \u0026#39;Connection: keep-alive\u0026#39;-H \u0026#39;Sec-Fetch-Dest: empty\u0026#39;-H \u0026#39;Cookie: _ga=GA1.3.359881599.1695818071; _gid=GA1.3.1925657744.1695818071; PHPSESSID=p580t8bid8coo6gk46bg7i1162\u0026#39;-H \u0026#39;X-Requested-With: XMLHttpRequest\u0026#39;–data \u0026#39;medida=1\u0026amp;tipoVal=2\u0026amp;anio%5B%5D=2022\u0026amp;delitos_agrupados%5B%5D=2\u0026amp;delitos_agrupados%5B%5D=4\u0026amp;delitos_agrupados%5B%5D=3\u0026amp;delitos_agrupados_nombres%5B%5D=Abusos+sexuales+y+otros+delitos+sexuales\u0026amp;delitos_agrupados_nombres%5B%5D=Incivilidades\u0026amp;delitos_agrupados_nombres%5B%5D=Delitos+de+mayor+connotaci%C3%B3n+social\u0026amp;delitos%5B%5D=17\u0026amp;delitos%5B%5D=3\u0026amp;delitos%5B%5D=4\u0026amp;delitos%5B%5D=5\u0026amp;delitos%5B%5D=20\u0026amp;delitos_nombres%5B%5D=Consumo+alcohol+v%C3%ADa+p%C3%BAblica\u0026amp;delitos_nombres%5B%5D=Homicidios\u0026amp;delitos_nombres%5B%5D=Hurtos\u0026amp;delitos_nombres%5B%5D=Lesiones+leves\u0026amp;delitos_nombres%5B%5D=Ebriedad\u0026amp;region%5B%5D=15\u0026amp;region_nombres%5B%5D=Regi%C3%B3n+de+Arica+y+Parinacota\u0026amp;seleccion=2\u0026amp;descarga=false\u0026#39; Nuestro objetivo, entonces, es replicar esta solicitud en R para obtener los datos sin tener que usar la interfaz web de CEAD.\nCrear solicitud de datos Podemos notar que la solicitud cURL se divide, a grandes rasgos, en una primera sección con el año seleccionado (\u0026amp;anio%5B%5D=2022), luego los delitos elegidos (\u0026amp;delitos_agrupados_nombres%5B%5D=Abusos+sexuales+y+otros+delitos+sexuales...), y una sección final donde se define la unidad geográfica correspondiente (\u0026amp;region%5B%5D=15\u0026amp;region_nombres%5B%5D=Regi%C3%B3n+de+Arica+y+Parinacota). Por lo tanto, utilizando R podemos replicar una solicitud de un conjunto de delitos copiando el texto que genera la aplicación web de CEAD, y cambiando los parámetros que necesitemos; en este caso, el año y la comuna.\nRealizando previamente una solicitud en el sitio de CEAD, podemos seleccionar los delitos que nos interesen, y copiar la solicitud generada, aislando el texto que corresponde a los delitos, como vemos a continuación:\n\u0026amp;delitos_agrupados%5B%5D=3\u0026amp;delitos_agrupados%5B%5D=7\u0026amp;delitos_agrupados_nombres%5B%5D=Delitos+de+mayor+connotaci%C3%B3n+social\u0026amp;delitos_agrupados_nombres%5B%5D=Robo+frustrado\u0026amp;delitos%5B%5D=8\u0026amp;delitos%5B%5D=10\u0026amp;delitos%5B%5D=9\u0026amp;delitos%5B%5D=11\u0026amp;delitos%5B%5D=12\u0026amp;delitos%5B%5D=30\u0026amp;delitos%5B%5D=13\u0026amp;delitos%5B%5D=7\u0026amp;delitos%5B%5D=4\u0026amp;delitos_nombres%5B%5D=Robo+con+violencia+o+intimidaci%C3%B3n\u0026amp;delitos_nombres%5B%5D=Robo+de+veh%C3%ADculo+motorizado\u0026amp;delitos_nombres%5B%5D=Robo+de+objetos+de+o+desde+veh%C3%ADculo\u0026amp;delitos_nombres%5B%5D=Robo+en+lugar+habitado\u0026amp;delitos_nombres%5B%5D=Robo+en+lugar+no+habitado\u0026amp;delitos_nombres%5B%5D=Robo+frustrado\u0026amp;delitos_nombres%5B%5D=Robo+por+sorpresa\u0026amp;delitos_nombres%5B%5D=Otros+robos+con+fuerza\u0026amp;delitos_nombres%5B%5D=Hurtos Puedes usar este mismo texto para realizar tu propia request de delitos relacionados a robos, o bien, puedes realizar tu propia selección de delitos/infracciones/incivilidades en el sitio del CEAD y copiar el texto usando el inspector web de tu navegador, teniendo cuidado de extraer el texto correcto y completo, es decir, desde que dice \u0026amp;delitos_agrupados después del año, hasta que diga el último delito antes de decir \u0026amp;region.\nPor ejemplo, otra string que seleccionaría los delitos de violencia intrafamiliar y violencia sexual sería la siguiente: \u0026amp;delitos_agrupados%5B%5D=2\u0026amp;delitos_agrupados%5B%5D=8\u0026amp;delitos_agrupados_nombres%5B%5D=Abusos+sexuales+y+otros+delitos+sexuales\u0026amp;delitos_agrupados_nombres%5B%5D=Violencia+intrafamiliar.\nCon esta información, creamos una función en R donde repliquemos la solicitud/request que nos interesa, definiendo que queremos obtener la información a escala mensual, con argumentos para poder definir el año y comuna que necesitamos:\ncead_generar_request \u0026lt;- function(año_elegido, comuna_numero) { # todos los meses del año request_fechas = \u0026#34;\u0026amp;trimestre%5B%5D=1\u0026amp;trimestre%5B%5D=2\u0026amp;trimestre%5B%5D=3\u0026amp;trimestre%5B%5D=4\u0026amp;mes%5B%5D=1\u0026amp;mes%5B%5D=2\u0026amp;mes%5B%5D=3\u0026amp;mes%5B%5D=4\u0026amp;mes%5B%5D=5\u0026amp;mes%5B%5D=6\u0026amp;mes%5B%5D=7\u0026amp;mes%5B%5D=8\u0026amp;mes%5B%5D=9\u0026amp;mes%5B%5D=10\u0026amp;mes%5B%5D=11\u0026amp;mes%5B%5D=12\u0026amp;mes_nombres%5B%5D=Enero\u0026amp;mes_nombres%5B%5D=Febrero\u0026amp;mes_nombres%5B%5D=Marzo\u0026amp;mes_nombres%5B%5D=Abril\u0026amp;mes_nombres%5B%5D=Mayo\u0026amp;mes_nombres%5B%5D=Junio\u0026amp;mes_nombres%5B%5D=Julio\u0026amp;mes_nombres%5B%5D=Agosto\u0026amp;mes_nombres%5B%5D=Septiembre\u0026amp;mes_nombres%5B%5D=Octubre\u0026amp;mes_nombres%5B%5D=Noviembre\u0026amp;mes_nombres%5B%5D=Diciembre\u0026#34; # delitos copiados desde una request anterior request_delitos = \u0026#34;\u0026amp;delitos_agrupados%5B%5D=3\u0026amp;delitos_agrupados%5B%5D=7\u0026amp;delitos_agrupados_nombres%5B%5D=Delitos+de+mayor+connotaci%C3%B3n+social\u0026amp;delitos_agrupados_nombres%5B%5D=Robo+frustrado\u0026amp;delitos%5B%5D=8\u0026amp;delitos%5B%5D=10\u0026amp;delitos%5B%5D=9\u0026amp;delitos%5B%5D=11\u0026amp;delitos%5B%5D=12\u0026amp;delitos%5B%5D=30\u0026amp;delitos%5B%5D=13\u0026amp;delitos%5B%5D=7\u0026amp;delitos%5B%5D=4\u0026amp;delitos_nombres%5B%5D=Robo+con+violencia+o+intimidaci%C3%B3n\u0026amp;delitos_nombres%5B%5D=Robo+de+veh%C3%ADculo+motorizado\u0026amp;delitos_nombres%5B%5D=Robo+de+objetos+de+o+desde+veh%C3%ADculo\u0026amp;delitos_nombres%5B%5D=Robo+en+lugar+habitado\u0026amp;delitos_nombres%5B%5D=Robo+en+lugar+no+habitado\u0026amp;delitos_nombres%5B%5D=Robo+frustrado\u0026amp;delitos_nombres%5B%5D=Robo+por+sorpresa\u0026amp;delitos_nombres%5B%5D=Otros+robos+con+fuerza\u0026amp;delitos_nombres%5B%5D=Hurtos\u0026#34; # unir fragmentos en una sola request request \u0026lt;- paste0(\u0026#34;medida=1\u0026amp;tipoVal=2\u0026#34;, \u0026#34;\u0026amp;anio%5B%5D=\u0026#34;, año_elegido, request_fechas, request_delitos, \u0026#34;\u0026amp;comuna%5B%5D=\u0026#34;, comuna_numero, \u0026#34;\u0026amp;seleccion=2\u0026amp;descarga=false\u0026#34; ) return(request) } A continuación, realizamos una prueba con esta función para crear una solicitud para el año 2022, usando el código único territorial o CUT de una comuna de prueba, en este caso, La Florida. Los códigos únicos territoriales de las comunas podemos obtenerlos del sitio web de la Infraestructura de Datos Geoespaciales de Chile (IDE Chile) u otros (personalmente, suelo crear una función que al ingreasrle el nombre de una comuna me retorne el CUT de la misma).\nrequest_prueba = cead_generar_request(año_elegido = 2022, comuna_numero = 13110) print(request_prueba) ## [1] \u0026#34;medida=1\u0026amp;tipoVal=2\u0026amp;anio%5B%5D=2022\u0026amp;trimestre%5B%5D=1\u0026amp;trimestre%5B%5D=2\u0026amp;trimestre%5B%5D=3\u0026amp;trimestre%5B%5D=4\u0026amp;mes%5B%5D=1\u0026amp;mes%5B%5D=2\u0026amp;mes%5B%5D=3\u0026amp;mes%5B%5D=4\u0026amp;mes%5B%5D=5\u0026amp;mes%5B%5D=6\u0026amp;mes%5B%5D=7\u0026amp;mes%5B%5D=8\u0026amp;mes%5B%5D=9\u0026amp;mes%5B%5D=10\u0026amp;mes%5B%5D=11\u0026amp;mes%5B%5D=12\u0026amp;mes_nombres%5B%5D=Enero\u0026amp;mes_nombres%5B%5D=Febrero\u0026amp;mes_nombres%5B%5D=Marzo\u0026amp;mes_nombres%5B%5D=Abril\u0026amp;mes_nombres%5B%5D=Mayo\u0026amp;mes_nombres%5B%5D=Junio\u0026amp;mes_nombres%5B%5D=Julio\u0026amp;mes_nombres%5B%5D=Agosto\u0026amp;mes_nombres%5B%5D=Septiembre\u0026amp;mes_nombres%5B%5D=Octubre\u0026amp;mes_nombres%5B%5D=Noviembre\u0026amp;mes_nombres%5B%5D=Diciembre\u0026amp;delitos_agrupados%5B%5D=3\u0026amp;delitos_agrupados%5B%5D=7\u0026amp;delitos_agrupados_nombres%5B%5D=Delitos+de+mayor+connotaci%C3%B3n+social\u0026amp;delitos_agrupados_nombres%5B%5D=Robo+frustrado\u0026amp;delitos%5B%5D=8\u0026amp;delitos%5B%5D=10\u0026amp;delitos%5B%5D=9\u0026amp;delitos%5B%5D=11\u0026amp;delitos%5B%5D=12\u0026amp;delitos%5B%5D=30\u0026amp;delitos%5B%5D=13\u0026amp;delitos%5B%5D=7\u0026amp;delitos%5B%5D=4\u0026amp;delitos_nombres%5B%5D=Robo+con+violencia+o+intimidaci%C3%B3n\u0026amp;delitos_nombres%5B%5D=Robo+de+veh%C3%ADculo+motorizado\u0026amp;delitos_nombres%5B%5D=Robo+de+objetos+de+o+desde+veh%C3%ADculo\u0026amp;delitos_nombres%5B%5D=Robo+en+lugar+habitado\u0026amp;delitos_nombres%5B%5D=Robo+en+lugar+no+habitado\u0026amp;delitos_nombres%5B%5D=Robo+frustrado\u0026amp;delitos_nombres%5B%5D=Robo+por+sorpresa\u0026amp;delitos_nombres%5B%5D=Otros+robos+con+fuerza\u0026amp;delitos_nombres%5B%5D=Hurtos\u0026amp;comuna%5B%5D=13110\u0026amp;seleccion=2\u0026amp;descarga=false\u0026#34; De este modo, creamos una solicitud que simula a un usuario que accedió al sitio web de CEAD, realizó selecciones en los distintos apartados de delitos, comunas y años, y presionó el botón \u0026ldquo;Buscar\u0026rdquo;.\nRealizar solicitud de datos Teniendo nuestra solicitud hecha, basta con enviarla al servidor para recibir los datos de vuelta, tal como si hubiésemos entrado al sitio, seleccionado las opciones y presionado el botón para obtener datos. Para ello, creamos una nueva función que envía al servidor la solicitud que creamos:\ncead_realizar_request \u0026lt;- function(request) { RCurl::getURL(url = \u0026#34;https://cead.spd.gov.cl/wp-content/themes/gobcl-wp-master/data/get_estadisticas_delictuales.php\u0026#34;, postfields = request, httpheader = c(Connection = \u0026#34;close\u0026#34;, \u0026#39;Content-Type\u0026#39; = \u0026#34;application/x-www-form-urlencoded; charset=UTF-8\u0026#34;, \u0026#39;Content-length\u0026#39; = nchar(request) ) ) } Entonces, aplicamos la solicitud que creamos a la función para realizar la solicitud, y así obtendremos datos desde el servidor de CEAD. Cuidado, que esta función lleva a cabo una conexión web con el servidor de CEAD, por lo que debemos ser responsables y precavidos con la cantidad de veces que la ejecutamos, y con no solicitar demasiados datos, dado que estamos obteniendo datos de manera gratuita y no queremos causar inconvenientes para otros usuarios, por ejemplo, realizando cientos de requests por minuto.\ndatos_prueba \u0026lt;- cead_realizar_request(request_prueba) #ejecutar solo cuando sea necesario, para no saturar el servidor CEAD datos_prueba |\u0026gt; substr(1, 400) ## [1] \u0026#34;\u0026lt;div class=\\\u0026#34;cont-reportes col-md-12\\\u0026#34;\u0026gt;\u0026lt;table width=\\\u0026#34;100%\\\u0026#34; border=\\\u0026#34;1\\\u0026#34; style=\\\u0026#34;border:1px solid #ccc;min-width: 100px;\\\u0026#34;\u0026gt;\\n\\t\\t\\t\\t \u0026lt;thead\u0026gt;\\n\\t\\t\\t\\t \u0026lt;tr\u0026gt;\\n\\t\\t\\t\\t\\t\u0026lt;th colspan=\\\u0026#34;1\\\u0026#34; style=\\\u0026#34;border:1px solid #ccc;min-width: 100px;background:#1d89c8;\\\u0026#34;\u0026gt;\u0026lt;/th\u0026gt;\u0026lt;th colspan=\\\u0026#34;12\\\u0026#34; style=\\\u0026#34;border:1px solid #ccc;background:#1d89c8;color:#ffffff;\\\u0026#34;\u0026gt;\u0026lt;center\u0026gt;2022\u0026lt;/center\u0026gt;\u0026lt;/th\u0026gt;\\n\\t\\t\\t\\t \u0026lt;/tr\u0026gt;\u0026lt;tr\u0026gt;\\n\\t\\t\\t\\t\\t\u0026lt;th colspan=\\\u0026#34;1\\\u0026#34; style=\\\u0026#34;border:1px so\u0026#34; Sin embargo, los datos que recibimos vienen en formato HTML. Para transformarlos en datos legibles, usamos el paquete {rvest} para transformar el HTML a un data frame:\n#obtener y limpiar datos html usando rvest datos_prueba_2 \u0026lt;- datos_prueba |\u0026gt; rvest::read_html() |\u0026gt; #leer datos html rvest::html_table() |\u0026gt; #extraer tabla desde el código purrr::pluck(1) |\u0026gt; #sacar de la lista janitor::row_to_names(2) |\u0026gt; #usar segunda fila como nombre de columnas rename(delitos = 1) #renombrar primera columna print(datos_prueba_2) #| eval: FALSE ## # A tibble: 10 × 13 ## delitos `1` `2` `3` `4` `5` `6` `7` `8` `9` `10` `11` ## \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; ## 1 Delitos de… 594 551 661 653 705 702 784 854 827 753 775 ## 2 Hurtos 90 68 84 95 100 105 134 125 116 113 93 ## 3 Otros robo… 11 8 14 14 13 9 13 12 11 14 7 ## 4 Robo con v… 142 157 155 153 166 164 177 216 165 188 205 ## 5 Robo de ob… 90 86 118 118 141 137 140 158 165 123 128 ## 6 Robo de ve… 61 49 70 70 53 57 47 52 60 41 47 ## 7 Robo en lu… 46 43 42 49 49 43 66 47 56 49 62 ## 8 Robo en lu… 41 31 28 42 32 43 52 44 40 48 46 ## 9 Robo por s… 48 52 57 33 70 69 90 115 104 89 84 ## 10 Robo frust… 13 10 16 17 23 26 20 28 10 18 22 ## # ℹ 1 more variable: `12` \u0026lt;chr\u0026gt; Listo! Hemos obtenido los datos de delitos directamente del sitio del CEAD desde R. Para finalizar, realizamos una pequeña manipulación de los datos para transformarlos a formato tidy, es decir, una observación por fila:\ndatos_prueba_3 \u0026lt;- datos_prueba_2 |\u0026gt; tidyr::pivot_longer(cols = 2:length(datos_prueba_2), names_to = \u0026#34;mes\u0026#34;, values_to = \u0026#34;cifra\u0026#34;) |\u0026gt; #pivotar a formato long mutate(fecha = lubridate::ymd(paste(\u0026#34;2022\u0026#34;, mes, \u0026#34;1\u0026#34;))) |\u0026gt; #crear fecha a partir del mes select(fecha, delito = delitos, delito_n = cifra) |\u0026gt; mutate(delito_n = as.numeric(delito_n)) |\u0026gt; arrange(delito, desc(fecha)) head(datos_prueba_3, 20) #| eval: FALSE ## # A tibble: 20 × 3 ## fecha delito delito_n ## \u0026lt;date\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; ## 1 2022-12-01 Delitos de mayor connotación social 834 ## 2 2022-11-01 Delitos de mayor connotación social 775 ## 3 2022-10-01 Delitos de mayor connotación social 753 ## 4 2022-09-01 Delitos de mayor connotación social 827 ## 5 2022-08-01 Delitos de mayor connotación social 854 ## 6 2022-07-01 Delitos de mayor connotación social 784 ## 7 2022-06-01 Delitos de mayor connotación social 702 ## 8 2022-05-01 Delitos de mayor connotación social 705 ## 9 2022-04-01 Delitos de mayor connotación social 653 ## 10 2022-03-01 Delitos de mayor connotación social 661 ## 11 2022-02-01 Delitos de mayor connotación social 551 ## 12 2022-01-01 Delitos de mayor connotación social 594 ## 13 2022-12-01 Hurtos 110 ## 14 2022-11-01 Hurtos 93 ## 15 2022-10-01 Hurtos 113 ## 16 2022-09-01 Hurtos 116 ## 17 2022-08-01 Hurtos 125 ## 18 2022-07-01 Hurtos 134 ## 19 2022-06-01 Hurtos 105 ## 20 2022-05-01 Hurtos 100 Ejecutar para varias comunas y/o años Ahora, probablemente queramos obtener datos para un rango de años más amplio, y quizás para varias comunas. Para ello, podemos realizar un loop que aplique la función que creamos consecutivamente a partir de un vector de años y de comunas, a modo de realizar varias solicitudes de una sola vez.\nUsando como ejemplo 4 códigos únicos territoriales y un vector de 5 años, realizamos un loop usando {purrr} que realice las solicitudes al CEAD, procurando mantener un ritmo de scraping que sea respetuoso de la fuente de datos. Notamos que el loop tiene dos niveles: por cada comuna, realiza otro loop para todos los años. Esto también se puede realizar en un solo loop con la función map2()` , pero personalmente encuentro que usar un loop dentro de otro es más intuitivo.\n#definir comunas y años que queremos scrapear comunas_por_calcular = c(14104, 13110, 13112, 05604) |\u0026gt; set_names() años_elegidos = 2018:2022 |\u0026gt; set_names() #loop por cada comuna datos_cead \u0026lt;- map(comunas_por_calcular, \\(comuna_numero) { message(\u0026#34;inciando comuna \u0026#34;, comuna_numero) #loop dentro del loop: para la comuna, por cada año especificado data_comuna \u0026lt;- map(años_elegidos, \\(año) { message(\u0026#34;año \u0026#34;, año) #generar request xml.request = cead_generar_request(año_elegido = año, comuna_numero) #obtener datos usando la función de request inicio = Sys.time() data_año = cead_realizar_request(xml.request) final = Sys.time() #dar tiempo de espera en base al tiempo que tomó descargar los datos, para no saturar el servidor Sys.sleep((final-inicio)*10) return(data_año) }) return(data_comuna) }) datos_cead |\u0026gt; substr(1, 400) Esto puede tomar un par de minutos. En todo caso, el código va arrojando mensajes en la consola para que no pensemos que se quedó pegado.\nPosteriormente, realizamos el mismo proceso de limpieza de datos que antes, obteniendo las tablas desde el código html que retornó el CEAD, y ordenamos los datos en formato tidy, donde cada variable es una columna y cada observación es una fila. Ojo, que este loop recorre los datos obtenidos en el paso anterior en base al vector de comunas que usamos para obtener los datos, comunas_por_calcular, ya que los datos fueron recibidos como una lista donde cada elemento de la lista tiene como título el código de la comuna.\n#por cada comuna cead_limpiada \u0026lt;- map_df(comunas_por_calcular, \\(.comuna) { # message(\u0026#34;obteniendo comuna \u0026#34;, .comuna) #extraer la comuna desde los datos usando el nombre de cada elemento de la lista cead_comuna \u0026lt;- datos_cead |\u0026gt; pluck(as.character(.comuna)) #por cada año, usando los nombres del objeto anterior, dado que cada comuna es, a su vez, una lista cuyos elementos son los años obtenidos para esa comuna datos_comuna_año \u0026lt;- map(names(cead_comuna), \\(.año) { # message(\u0026#34;obteniendo año \u0026#34;, .año) #obtener tablas cead_comuna_año \u0026lt;- cead_comuna |\u0026gt; pluck(.año) |\u0026gt; rvest::read_html() |\u0026gt; rvest::html_table() |\u0026gt; purrr::pluck(1) |\u0026gt; janitor::row_to_names(2) |\u0026gt; rename(delitos = 1) #pivotar a formato tidy (una observación por fila) cead_datos \u0026lt;- cead_comuna_año |\u0026gt; tidyr::pivot_longer(cols = 2:length(cead_comuna_año), names_to = \u0026#34;mes\u0026#34;, values_to = \u0026#34;cifra\u0026#34;) |\u0026gt; mutate(año = .año, comuna = .comuna) |\u0026gt; mutate(fecha = lubridate::ymd(paste(año, mes, \u0026#34;1\u0026#34;))) return(cead_datos) }) return(datos_comuna_año) }) head(cead_limpiada) ## # A tibble: 6 × 6 ## delitos mes cifra año comuna fecha ## \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;chr\u0026gt; \u0026lt;dbl\u0026gt; \u0026lt;date\u0026gt; ## 1 Delitos de mayor connotación social 1 34 2018 14104 2018-01-01 ## 2 Delitos de mayor connotación social 2 25 2018 14104 2018-02-01 ## 3 Delitos de mayor connotación social 3 26 2018 14104 2018-03-01 ## 4 Delitos de mayor connotación social 4 37 2018 14104 2018-04-01 ## 5 Delitos de mayor connotación social 5 20 2018 14104 2018-05-01 ## 6 Delitos de mayor connotación social 6 20 2018 14104 2018-06-01 Listo, hemos obtenido los datos para las comunas que definimos y en los años que fijamos, y lo mejor es que podemos volver a usar este proceso para obtener los datos de cualquier comuna, o incluso de todas las comunas, de forma rápida y reproducible. Si bien hay otras formas de automatizar este tipo de procesos, considero que usando R resulta más confiable dado que nos comunicamos directamente con el funcionamiento de la plataforma web, y es bastante conveniente tener estos scripts en caso de que necesites replicar el proceso para otras comunas o rangos temporales.\nBastián Olea Herrera\nMagíster en sociología, data scientist\nhttps://bastianolea.rbind.io\nContacto: bastianolea arroba gmail punto com\n","date":"2023-09-27T00:00:00Z","excerpt":"\u003cp\u003eEn este script detallaré cómo descargar datos de estadísticas delictuales del Centro de Estudios y Análisis del Delito (CEAD) de Chile utilizando técnicas de web scraping en R. Las estadísticas disponibles en el sitio web de CEAD corresponden a los siguientes datos oficiales: \u003cem\u003eEstadísticas Oficiales de Delitos de Mayor Connotación Social (DMCS), Violencia Intrafamiliar (VIF), Incivilidades y otros hechos informados por Carabineros y la Policía de Investigaciones de Chile al Ministerio del Interior y Seguridad Pública.\u003c/em\u003e\u003c/p\u003e","href":"https://bastianoleah.netlify.app/blog/tutorial_delitos_cead/","tags":"web scraping ; Chile ; datos ; ciencias sociales","title":"Tutorial: Scraping de estadísticas delictuales del Centro de Estudios y Análisis del Delito con R"},{"content":"Aplicación web que permite visualizar gráficos de los resultados de las encuestas del Centro de Estudios Públicos. Además, permite desagregar resultados en base a categorías sociodemográficas, filtrar grupos, configurar los gráficos y descargar los datos.\n","date":"2023-06-15T00:00:00Z","excerpt":"Aplicación web que permite visualizar gráficos de los resultados de las encuestas del Centro de Estudios Públicos. Además, permite desagregar resultados en base a categorías sociodemográficas, filtrar grupos, configurar los gráficos y descargar los datos.","href":"https://bastianoleah.netlify.app/blog/app_cep_graficador/","tags":"apps ; Chile ; ciencias sociales","title":"App: Graficador encuesta CEP"},{"content":"** No content below YAML for the talk _index. This file provides front matter for the listing page layout and sidebar content. It is also a branch bundle, and all settings under cascade provide front matter for all pages inside talk/. You may still override any of these by changing them in a page\u0026rsquo;s front matter.**\n","date":null,"excerpt":"\u003cp\u003e** No content below YAML for the talk _index. This file provides front matter for the listing page layout and sidebar content. It is also a branch bundle, and all settings under \u003ccode\u003ecascade\u003c/code\u003e provide front matter for all pages inside talk/. You may still override any of these by changing them in a page\u0026rsquo;s front matter.**\u003c/p\u003e","href":"https://bastianoleah.netlify.app/tutoriales/","tags":"","title":"Tutoriales de R"}]