Unidad 2 - Manejo de datos con tidyverse
Curso Nivelatorio de R · CIENFI · Universidad Icesi
1 Objetivo de la unidad
En la Unidad 1 aprendimos el idioma: objetos, vectores, data frames y proyectos reproducibles. En esta unidad empieza el trabajo real. Un secreto a voces de la investigación aplicada: el manejo de datos ocupa alrededor del 80% del tiempo de un proyecto empírico. Estimar la regresión toma minutos; dejar la base lista para estimarla toma días. Esta unidad existe para que esos días sean los menos posibles.
¿Por qué esto importa en economía aplicada?
Las bases con las que trabajará en la Maestría —encuestas de hogares como la GEIH, paneles de firmas tipo EDIT o Supersociedades, registros administrativos de salud, impuestos o educación— nunca llegan listas: traen categorías escritas de tres formas distintas, números leídos como texto, faltantes sin documentar, duplicados y valores imposibles, y la información suele venir repartida en varias tablas que hay que cruzar. La calidad de cualquier resultado —una descriptiva, un gráfico, un coeficiente— nunca supera la calidad de la base sobre la que se calcula.
El ciclo de manejo de datos (el mapa de esta unidad)
Importar el archivo a un data frame → diagnosticar la base antes de tocarla (glimpse(), skim()) → limpiar duplicados, texto, tipos, faltantes y outliers → transformar (filtrar, crear variables, agrupar y resumir con dplyr) → cruzar tablas con una llave común (left_join()). No es una lista para memorizar: es el orden natural en el que trabajaremos de aquí en adelante, y el índice de este documento.
1.1 Lo que aprenderemos en esta unidad
- Importar y diagnosticar: leer archivos con
import()(rio),read_csv()yread_excel(); radiografiar conglimpse()yskim(); estandarizar nombres conclean_names() - Transformar con dplyr: encadenar con el pipe
%>%; usarselect(),rename(),filter(),arrange()ymutate()(conifelse()ycase_when()) - Limpiar una base sucia: un flujo de 5 pasos —duplicados, texto, tipos, faltantes y outliers— con cada decisión justificada y documentada
- Agrupar, resumir y cruzar: tablas por grupo con
group_by()+summarise(),count()yn_distinct(); uniones conleft_join(); reorganización conpivot_longer()/pivot_wider()
2 Importar datos y primer diagnóstico
En la Unidad 1 escribimos los datos a mano con c() y data.frame(). En la práctica, los datos viven en archivos —un .csv descargado del DANE, un Excel enviado por un ministerio— y el primer paso de todo análisis es importarlos al Environment.
2.1 La base del curso: innovacion_empresas.csv
Conozca la base que nos acompañará toda la unidad
innovacion_empresas.csv contiene microdatos ficticios a nivel de firma, inspirados en encuestas de innovación tipo EDIT (DANE): una fila por empresa, con su sector, departamento, tamaño, empleados, ventas, gasto en I+D e indicadores de innovación y exportación. La base está sucia a propósito: la misma categoría escrita de varias formas (Si, si, SI, 1), columnas numéricas que llegan como texto, faltantes, valores imposibles y filas duplicadas. No es un capricho pedagógico: así llegan las bases reales.
La importamos en RStudio, dentro del proyecto del curso y con rutas relativas (regla de oro de la Unidad 1). El estándar del curso es import() (paquete rio), que detecta el formato por la extensión (.csv, .xlsx, .dta, .rds, …) y elige el lector adecuado; las alternativas específicas de cada formato también son válidas. Este bloque se corre en RStudio (no es ejecutable aquí):
## [RStudio] llamar/instalar librerias
require(pacman)
p_load(tidyverse, rio, janitor, skimr)
## importar la base del curso (ruta relativa a la raiz del proyecto)
firmas <- import("datos/originales/innovacion_empresas.csv")
## equivalentes segun el formato:
firmas <- read_csv("datos/originales/innovacion_empresas.csv") # readr (tidyverse)
firmas <- read_excel("datos/originales/firmas.xlsx", sheet = 1) # readxl, para Excel2.2 Primer diagnóstico: radiografiar antes de tocar
Importar sin diagnosticar es como recibir un paciente sin examinarlo. Tres funciones “lupa” dan la radiografía:
glimpse()(dplyr): la versión mejorada delstr()de la Unidad 1: filas, columnas, tipo de cada variable y primeros valores. Aquí salta la primera alarma de nuestra base:ventas_millonesynum_empleadosllegan como<chr>(texto).skim()(skimr): resumen por variable: faltantes (n_missing), valores distintos (n_unique) y, para las numéricas, mínimo, máximo y percentiles.clean_names()(janitor): estandariza los nombres de columnas asnake_case(Ventas (millones $)pasa aventas_millones). Con Excel institucionales le ahorrará muchos dolores de cabeza.
## [RStudio] diagnostico de la base recien importada
glimpse(firmas)
skim(firmas)
firmas <- clean_names(firmas)Cómo leer la salida de skim()
Léala de arriba a abajo buscando lo que no cuadra: un n_unique mayor de lo esperado en una categórica delata variantes de escritura (¿15 valores para 7 sectores?); una columna de números en el bloque character delata un problema de tipo; un n_missing alto exige entender los faltantes antes de tratarlos; y un p0 o p100 absurdo delata valores imposibles u outliers. Cada hallazgo se anota: ese listado es el insumo del flujo de limpieza de la sección 5.
La rutina completa al importar —estándar del curso— es siempre la misma: import() con ruta relativa → glimpse() → skim() → clean_names() → anotar los problemas en el script. Cinco minutos de diagnóstico evitan horas de depurar resultados absurdos río abajo.
2.3 La mini base de los ejercicios interactivos
Los bloques interactivos de este documento corren con webR en su navegador, que no puede leer archivos de su disco. Por eso los ejercicios usan una versión miniatura de la base del curso (8 firmas, nombres abreviados), ya construida con data.frame() dentro de este documento. Ejecute el bloque para verla y radiografiarla:
2.4 Mini-ejercicio: Diagnóstico exprés
Responda con código, no mirando la tabla:
Resultados esperados: 8 filas y 8 columnas; 2 faltantes en gasto_id; cuatro tamaños de firma.
3 El pipe (%>%)
Antes de los verbos de dplyr, el conector que los une. Suponga que quiere el promedio de ventas, redondeado. La forma tradicional anida funciones y se lee al revés de como se piensa: la primera operación (mean) queda en el centro y la última (round) al principio. El pipe %>% endereza la lectura:
Cómo leerlo: el pipe toma lo que está a su izquierda y lo pasa como primer argumento de la función de la derecha. En voz alta, %>% se lee “luego”: “tome las ventas, luego calcule el promedio, luego redondee”. El código se convierte en una oración.
Con data frames el pipe muestra todo su poder. Un anticipo de la próxima sección (no se preocupe aún por los verbos; fíjese en cómo se lee):
“Tome las firmas, luego quédese con las que innovan, luego seleccione tres columnas, luego ordénelas de mayores a menores ventas”. Cuatro pasos, cero objetos intermedios, y cualquier lector —incluido usted en seis meses— entiende el flujo de un vistazo.
El estándar del curso es %>%
Existen dos pipes en R: %>% (del paquete magrittr, se carga automáticamente con dplyr/tidyverse) y |> (el nativo de R desde la versión 4.1). Para lo que haremos aquí son equivalentes, pero el estándar del curso es %>%: es el que verá en todo nuestro material y en la mayoría de la ayuda en línea. Si encuentra |> en otro material, léalo igual: “luego”.
Dos hábitos que adoptaremos desde ya: el atajo Ctrl + Shift + M (Windows/Linux) o Cmd + Shift + M (Mac) escribe %>% en RStudio; y después de cada %>% se salta de línea — un paso del flujo por línea, para que los scripts se lean como recetas.
4 Transformaciones básicas con dplyr
dplyr es el paquete central del tidyverse para manipular data frames. Su filosofía: en vez de la notación de corchetes de la Unidad 1, ofrece verbos —funciones cuyo nombre dice exactamente qué hacen— que comparten una misma lógica: resultado <- datos %>% verbo(argumentos).
Los verbos NO modifican el objeto original
Todos devuelven una copia transformada. Si no guarda el resultado con <-, la transformación se imprime en la consola y se pierde: firmas seguirá intacta en el Environment hasta que usted la sobreescriba explícitamente. Es una red de seguridad: puede experimentar sin miedo a dañar los datos.
4.1 select(): elegir columnas
Devuelve solo las columnas pedidas, en el orden pedido; con el signo - excluye:
4.2 rename(): renombrar columnas
Cambia el nombre sin tocar el contenido; la sintaxis es nuevo_nombre = nombre_viejo:
¿Cuándo renombrar? Nombres poco descriptivos (x1, var2), con mayúsculas o espacios, o para unificar nomenclatura antes de cruzar bases. El estándar sigue siendo snake_case.
4.3 filter(): filtrar filas
Devuelve solo las filas que cumplen una condición; las demás se descartan:
Los operadores de comparación son los de la Unidad 1 (==, !=, >, >=, <, <=), más uno nuevo: %in% pregunta si el valor pertenece a un conjunto. Las condiciones se combinan con & (ambas) y | (al menos una):
Error clásico: = no es ==
filter(firmas, cod_sector = "C") falla: = asigna, == compara. Dentro de filter(), comparar es siempre con dos signos igual. R se lo recuerda en el mensaje de error (Did you mean cod_sector == "C"?): léalo.
De la Unidad 1 a filter()
En la Unidad 1 filtramos con indexación lógica: personas[personas$ingreso > 1500000, ]. filter(firmas, ventas > 1000) hace exactamente lo mismo: construye un vector de TRUE/FALSE y conserva las filas con TRUE. Solo cambia la sintaxis: sin corchetes, sin repetir el nombre de la base, y encadenable con %>%. Si entendió la indexación lógica, ya entendió filter().
4.4 arrange(): ordenar filas
Reorganiza las filas según una o más columnas; es el paso final típico antes de presentar una tabla:
Un detalle útil del ejercicio: arrange() manda los NA siempre al final, con o sin desc().
4.5 mutate(): crear variables
Añade columnas calculadas; las originales se conservan y el número de filas no cambia:
Observe las firmas E004 y E007: como gasto_id es NA, la intensidad también queda NA. El NA se propaga en los cálculos — no desaparece solo, y eso es deseable: nos obliga a decidir qué hacer con él (sección 5).
Clasificar con ifelse(). Para una variable de dos categorías: “para cada fila, si la condición se cumple escriba lo primero; si no, lo segundo”.
4.5.1 Clasificar con case_when(): ramas planas
Con tres o más categorías, los ifelse() anidados se vuelven ilegibles. case_when() los reemplaza con ramas planas, una condición por línea:
Cómo leer (y escribir) un case_when()
Cada rama es condicion ~ valor y se evalúan en orden: la primera que se cumple gana (una firma con ventas de 3.000 es “grande”, no “mediana”, aunque cumpla ambas). La última rama, TRUE ~ "pequena", recoge “todo lo demás”; sin ella, esas filas quedarían en NA. Estilo del curso: ramas planas, una por línea, alineadas — un case_when() bien escrito se lee como una tabla de reglas.
4.6 Mini-ejercicio: Construyendo variables de análisis
Complete los espacios ___:
Pista y verificación: la última rama debe capturar “todo lo demás”. Resultado esperado: 2 firmas grandes (E001, E006), 3 medianas (E003, E005, E008) y 3 pequeñas. Revise el panel Environment: firmas_mut debe existir con 10 columnas y firmas seguir intacta con 8.
Hasta aquí hemos visto: los verbos transforman una base que se supone confiable. La realidad es menos amable: antes de transformar hay que limpiar.
5 Limpiar una base sucia
Esta sección es el corazón de la unidad. Trabajaremos con firmas_sucia, una mini base que concentra —en 9 filas— los mismos problemas de la base real del curso y que ya viene cargada en este documento. Ejecute el bloque para verla y obsérvela con ojo clínico:
Los cinco problemas de esta base (los mismos de la base real)
- Una fila duplicada: la firma E103 aparece dos veces, idéntica
- Texto inconsistente:
sectoreinnovamezclan mayúsculas y espacios invisibles ("industria "," no") - Números como texto:
ventasescharacter, con un"$1500"y un"ND"(no disponible) - Faltantes:
gasto_idtieneNAque hay que interpretar antes de tratar - Un outlier: una firma reporta 9.000 empleados — sospechoso, por decir lo menos
Los corregiremos en este orden: primero filas (duplicados), luego columnas (texto, tipos) y al final las decisiones difíciles (faltantes, outliers). Los pasos son secuenciales: ejecute los bloques en orden.
Primero, el diagnóstico con herramientas que ya conocemos — cinco líneas, cinco problemas confirmados:
Ahora sí: partimos de firmas_sucia (que no tocaremos — regla de la Unidad 1: los datos originales son intocables) y construiremos firmas_limpia paso a paso.
5.1 Paso 1: eliminar duplicados con distinct()
distinct() compara cada fila completa con las anteriores y conserva solo la primera aparición: si el resultado es 1, eliminamos exactamente el duplicado de E103. ¿Por qué es lo primero? Porque una fila duplicada infla todo lo que calcule después: sumas, promedios, conteos. Para declarar duplicados usando solo algunas columnas: distinct(firmas_sucia, id_empresa, .keep_all = TRUE).
5.2 Paso 2: estandarizar el texto
“Industria” ≠ “industria” ≠ ” Industria”
Para R son tres categorías distintas — y dos se diferencian por un espacio que usted no puede ver. Si agrupa por sector sin estandarizar, obtendrá 8 grupos en vez de 3, los totales no cuadrarán y R no emitirá ningún error. Es el error más silencioso y frecuente del trabajo con encuestas.
Tres funciones de base R resuelven casi todo: tolower() / toupper() unifican mayúsculas y minúsculas; trimws() elimina espacios al inicio y al final; gsub(patron, reemplazo, texto) busca un patrón y lo reemplaza (la usaremos en el paso 3).
Lea la línea de adentro hacia afuera: primero trimws() quita espacios, luego tolower() pasa a minúsculas. La verificación con table() no es opcional: cada paso de limpieza termina con un chequeo.
5.3 Paso 3: corregir los tipos
ventas llegó como texto por culpa de dos valores: "ND" (texto puro) y "$1500" (número con símbolo). La estrategia: declarar el texto-basura como NA, quitar el símbolo y solo entonces convertir:
Tres funciones nuevas, cada una con su rol: na_if(x, "ND") convierte un valor específico en NA (la forma explícita y documentada de declarar un faltante); gsub("\\$", "", x) reemplaza el símbolo $ por nada (se escribe \\$ porque $ tiene significado especial en expresiones regulares); as.numeric() convierte texto a número — lo que no pueda convertir queda en NA con un warning: si aparece, revise qué valor se le escapó.
En la base real hay un problema adicional: decimales con coma ("857,3"). Para eso, parse_number() de readr es la navaja suiza — entiende monedas, separadores de miles y comas decimales:
## [RStudio] convertir ventas_millones con decimales de coma ("857,3")
firmas <- firmas %>%
mutate(ventas_millones = parse_number(ventas_millones, locale = locale(decimal_mark = ",")))5.4 Paso 4: tratar los faltantes
Ya sabemos que un NA se propaga: mean() de una columna con faltantes devuelve NA, salvo con na.rm = TRUE. Pero antes de “removerlos” hay que hacerse una pregunta que es de economía, no de R:
¿Este NA es información o es un error?
Un faltante puede tener sentido económico. En las encuestas de innovación, la firma que no invierte en I+D simplemente no responde el módulo de gasto: su NA en gasto_id significa, casi con certeza, “no invierte”, y recodificarlo a 0 es defendible. En cambio, el "ND" de ventas es no respuesta: la firma vende (está viva), pero no sabemos cuánto; ponerle un 0 sería inventar un dato falso — ahí el NA debe quedarse y manejarse con na.rm = TRUE. Misma sintaxis, decisiones opuestas: la diferencia no está en el código, está en entender cómo se generaron los datos.
Aplicamos las dos decisiones, cada una con su justificación escrita:
5.5 Paso 5: detectar y decidir sobre los outliers
El máximo es 9.000 empleados — en una base donde la mediana ronda los 60 y esa misma firma reporta ventas de 1.500 millones. Una firma de 9.000 empleados facturaría órdenes de magnitud más: es casi con certeza un error de digitación (¿eran 90? ¿900?). Sin evidencia para corregirlo, la decisión conservadora es convertirlo en NA:
El “acta de limpieza”: decisiones que quedan escritas
Ante un valor extremo hay tres opciones: corregirlo (solo con evidencia), convertirlo en NA (si es implausible pero el resto de la fila sirve) o dejarlo (si es raro pero posible: en ingresos y ventas, los valores extremos reales son la norma). Cualquiera que elija, la regla del curso es una sola: toda decisión de limpieza queda documentada como comentario en el script — problema encontrado, decisión y justificación, como el bloque ## ACTA DE LIMPIEZA de arriba. Ese registro permite que un coautor (o usted en seis meses) reproduzca y defienda la base final. En la Maestría, “¿qué le hizo a los datos?” es una pregunta que le harán siempre.
5.6 Mini-ejercicio: Su primer flujo de limpieza completo
Otra base sucia —firmas_b, ya cargada en este documento—, mismos vicios. Ejecute el bloque para verla y complete el flujo (los ___) encadenando todo con %>%:
Verificación: 5 filas (había 1 duplicado), ciudad con 2 valores, exporta con 2, ventas numérica con 1 NA y promedio de 306.25. Cinco problemas, un solo pipe.
6 Agrupar y resumir
Con la base limpia empieza la parte gratificante: convertir microdatos en estadísticas que respondan preguntas. ¿Cuál es la tasa de innovación por sector? ¿Las ventas promedio? Ese es el trabajo del dúo group_by() + summarise(). Volvemos a la mini base limpia firmas.
6.1 summarise(): colapsar la base en un resumen
A diferencia de mutate() (que conserva las filas), summarise() colapsa todas las filas en una sola, con los estadísticos que le pidamos:
Qué pasó: 8 filas entraron, 1 salió. n() es una función especial que cuenta las filas del grupo (aquí, toda la base). Y note el na.rm = TRUE en gasto_id: la lección del paso 4, aplicada.
6.2 group_by() + summarise(): el dúo central
Un resumen global esconde la heterogeneidad — y la heterogeneidad es donde vive la economía. group_by() marca la base como agrupada; el summarise() siguiente calcula dentro de cada grupo:
Cómo leer el resultado: una fila por sector, con su número de firmas, sus ventas promedio y su tasa de innovación. Este es el hilo conductor de la unidad: la misma tabla, sobre la base real, es el entregable de la práctica. Dos detalles importantes:
mean(innova == "si")es una proporción. La comparación produceTRUE/FALSE, que R trata como1/0; su promedio es la fracción de firmas innovadoras. Es el mismo truco de la Unidad 1 (sum()de una comparación cuenta;mean()promedia) y lo usará constantemente..groups = "drop"desagrupa el resultado. Sin él, la tabla queda “marcada” como agrupada y puede producir resultados inesperados después. Estándar del curso: todosummarise()congroup_by()cierra con.groups = "drop".
Granularidad: ¿a qué nivel está cada tabla?
summarise() sin agrupar produce 1 fila (toda la base); con group_by(cod_sector), 1 fila por sector; con group_by(cod_sector, tamano), 1 fila por cada combinación sector × tamaño. Antes de calcular, pregúntese: ¿mi unidad de análisis es la firma o el sector? Media base de los errores de interpretación viene de confundir el nivel de la tabla.
6.3 count() y n_distinct(): los atajos de conteo
Cuando solo necesita frecuencias, count() abrevia el group_by() + summarise(n = n()); cuando necesita el número de valores distintos, n_distinct() dentro de summarise():
Hacia la Unidad 3: de la tabla al gráfico
Las tablas agrupadas que acabamos de construir son el insumo directo de la visualización. En la Unidad 3, la tabla de tasa de innovación por sector se convertirá en un gráfico de barras de ggplot2 en cinco líneas. El flujo estándar del trabajo empírico es exactamente este: base limpia → group_by() + summarise() → gráfico. Lo que hicimos en esta unidad es la mitad invisible de cada figura que verá en un paper.
6.4 Mini-ejercicio: Preguntas al estilo de un informe
Pistas y verificación: P1: tres departamentos; Valle del Cauca y Bogotá D.C. empatan con 3 firmas. P2: summarise(ventas_prom = mean(ventas), empleo_total = sum(empleados), .groups = "drop"). P3: el sector J (todas sus firmas innovan: tasa = 1); si su tabla dice otra cosa, revise si escribió mean(innova == "si") con ==.
7 Cruzar bases: joins
Hasta ahora toda la información vivía en una sola tabla. En la práctica casi nunca es así: los microdatos de firmas están en una base y las características de los sectores (crecimiento, valor agregado, referentes nacionales) en otra. Para analizarlas juntas hay que cruzarlas.
La llave: la columna que conecta dos tablas
Un join une dos tablas emparejando filas que comparten el valor de una llave (key): una o más columnas presentes en ambas. En nuestra base, la llave natural es cod_sector: identifica al sector en los microdatos de firmas y en la base agregada sectorial. Antes de cruzar, pregúntese siempre: ¿cuál es la llave? ¿está limpia en ambas tablas? (Un "c " con espacio jamás emparejará con "C" — por eso la limpieza va primero.)
7.1 left_join(): el estándar del curso
left_join(x, y, by = "llave") conserva todas las filas de la tabla izquierda (x) y les pega las columnas de y donde encuentra pareja. El cruce típico: indicadores por sector (calculados con lo que ya sabemos) más una tabla de características sectoriales (info_sector, que ya viene cargada en este documento):
Lea el resultado con cuidado — ahí está toda la teoría de joins: los sectores C y G encontraron pareja y recibieron las columnas nuevas; J e I no existen en info_sector, así que sus filas sobreviven (es un left join) pero con NA en las columnas nuevas — un NA inesperado tras un join es síntoma de llaves que no emparejaron; y el sector M, que existe solo en la tabla derecha, fue descartado silenciosamente.
7.2 El peligro: llaves duplicadas multiplican filas
¿Y si la llave se repite en la tabla derecha? Cada fila de la izquierda se empareja con todas sus parejas — y la base se infla:
Regla de oro: verifique el número de filas antes y después del join
Si cruza una base de 500 firmas con una tabla sectorial y termina con 1.480 filas, no ganó datos: duplicó firmas, y todo promedio posterior quedará contaminado sin que R proteste. El chequeo cuesta una línea (nrow() antes y después) y es obligatorio en este curso. Si las filas aumentaron, la llave está duplicada en la tabla derecha: revísela con count(tabla, llave) %>% filter(n > 1).
Las variantes: inner y full. Dos parientes de left_join() que conviene reconocer: inner_join(x, y) conserva solo las filas con pareja en ambas tablas (útil cuando únicamente sirven los casos completos) y full_join(x, y) conserva todas las filas de ambas (útil para auditar qué hay y qué falta en cada tabla). En la duda, left_join() con la base de análisis a la izquierda: perder observaciones sin darse cuenta es peor que cargar unos NA visibles.
7.3 El cruce en la base real del curso
Con la base real, el flujo completo —limpiar, agregar, cruzar— se ve así (se corre en RStudio):
## [RStudio] indicadores de la muestra por sector
resumen_sector <- firmas %>%
group_by(cod_sector) %>%
summarise(n_firmas = n(),
tasa_innova = mean(innova_producto == "si", na.rm = TRUE),
.groups = "drop")
## importar la base agregada sectorial y cruzar por cod_sector
sectores <- import("datos/originales/sectores_agregado.csv")
resumen_full <- resumen_sector %>% left_join(sectores, by = "cod_sector")
## verificacion obligatoria: el cruce no debe cambiar el numero de filas
nrow(resumen_sector) == nrow(resumen_full)El premio: resumen_full permite comparar la tasa de innovación de nuestra muestra con la tasa_innovacion_sectorial de referencia nacional, sector por sector. Esa comparación —imposible con las tablas separadas— es el tipo de pregunta que justifica todo el cruce.
7.4 Mini-ejercicio: Un cruce con trampa
Verificación: 4 filas (las mismas de resumen_sector) y el sector J con salario_prom = NA, porque no aparece en la tabla de salarios. Ningún NA de un join se ignora: se explica.
8 Reorganizar con tidyr
A veces el problema no es el contenido de la base sino su forma. Compare dos maneras de guardar las ventas anuales de dos firmas:
Formato ancho (una columna por año):
| id_empresa | ventas_2021 | ventas_2022 | ventas_2023 |
|---|---|---|---|
| E001 | 4800 | 5200 | 5600 |
| E002 | 430 | 480 | 510 |
Formato largo (una fila por firma-año):
| id_empresa | anio | ventas |
|---|---|---|
| E001 | 2021 | 4800 |
| E001 | 2022 | 5200 |
| E001 | 2023 | 5600 |
| … | … | … |
La información es idéntica; la utilidad no. El formato largo es el que esperan ggplot2, los verbos de dplyr (¿cómo calcularía el crecimiento promedio por año en la tabla ancha?) y los modelos de datos panel de la Maestría (la unidad firma-año es una fila). El ancho es cómodo para leer y típico de los Excel institucionales. El paquete tidyr (parte del tidyverse) traduce entre ambos; como no está disponible en webR, estos bloques se corren en RStudio:
## [RStudio] de ancho a largo: pivot_longer()
ventas_largas <- ventas_anchas %>%
pivot_longer(cols = starts_with("ventas_"), # columnas a apilar
names_to = "anio", # sus nombres -> nueva columna
names_prefix = "ventas_", # quitar el prefijo
values_to = "ventas") # sus valores -> nueva columna
## [RStudio] el viaje de vuelta: pivot_wider()
ventas_anchas <- ventas_largas %>%
pivot_wider(names_from = anio, values_from = ventas, names_prefix = "ventas_")Regla práctica: largo para analizar, ancho para presentar
Si va a graficar, agrupar, resumir o estimar: largo. Si va a pegar una tabla compacta en un informe: ancho. El síntoma clásico de que necesita pivot_longer() es tener información en los nombres de las columnas (ventas_2021, ventas_2022, …): los años son datos, y los datos van en celdas, no en encabezados.
9 Errores comunes
Los cinco errores que más tiempo cuestan en esta unidad. Reconocerlos a la primera es media depuración.
9.1 1. Confundir = con ==
firmas %>% filter(cod_sector = "C") # Error: `=` asigna
firmas %>% filter(cod_sector == "C") # Correcto: `==` comparaDentro de filter() (y de cualquier condición), comparar es con dos signos igual. El mensaje de error de dplyr incluso sugiere la corrección: léalo antes de pedir ayuda.
9.2 2. Olvidar guardar el resultado
firmas %>% mutate(ventas_empleado = ventas / empleados) # INCORRECTO: se imprime y se pierde
firmas <- firmas %>% mutate(ventas_empleado = ventas / empleados) # CORRECTO: se asigna con <-Los verbos de dplyr nunca modifican el objeto original: devuelven una copia. Si tras “crear” una variable R dice object 'ventas_empleado' not found, casi seguro faltó el <-. Verifíquelo en el panel Environment.
9.3 3. filter() descarta los NA silenciosamente
El más traicionero. Una condición evaluada sobre NA no da TRUE ni FALSE: da NA, y filter() solo conserva los TRUE:
Ni error ni warning: las firmas E004 y E007 (con gasto_id = NA) se esfumaron sin dejar rastro. Regla práctica: si una columna tiene faltantes, decida explícitamente si las filas con NA entran o salen del filtro.
9.4 4. Un join con llave duplicada multiplica filas
Lo vimos en la sección de joins y merece repetirse: si la llave se repite en la tabla derecha, cada fila de la izquierda se empareja con todas sus copias y la base crece sin avisar. El antídoto cuesta una línea: nrow() antes y después del join; si el número cambió, busque la llave duplicada en la tabla derecha.
9.5 5. Creer que group_by() “hace algo” por sí solo
Mismas 8 filas, mismos valores: lo único nuevo es la marca # Groups: cod_sector [4] sobre la tabla. group_by() no calcula ni colapsa nada — solo prepara el terreno para que el siguiente summarise() (o mutate()) trabaje grupo por grupo. Y su reverso: un objeto que quedó agrupado sin que usted lo recuerde puede alterar cálculos posteriores. Por eso el estándar del curso cierra cada resumen con .groups = "drop".
10 Checklist de salida
Antes de pasar a la Unidad 3, verifique que puede:
Si algún punto le genera dudas, vuelva a la sección correspondiente y ejecute los mini-ejercicios de nuevo.
11 Preguntas de comprensión
- Una colega le dice que su base “tiene 5 regiones”, pero el diccionario de datos afirma que son 3. ¿Qué pudo haber pasado, con qué funciones lo diagnosticaría y con cuáles lo corregiría?
- En la base del curso,
gasto_id_millonestiene cerca del 40% de valores faltantes. Un compañero propone recodificarlos todos a 0; otro propone eliminar esas filas. ¿Qué necesita saber sobre cómo se recolectaron los datos para decidir, y por qué la misma regla no aplicaría a los faltantes deventas_millones? filter(firmas, ventas > 1000)devuelve menos filas de las que usted esperaba y ningún mensaje de error. ¿Qué pasó con las firmas cuyo valor deventasesNAy cómo cambiaría el código para conservarlas?- Después de un
left_join(), su base pasó de 500 a 1.480 filas. Explique qué ocurrió, por qué contamina cualquier promedio posterior y qué verificación de una línea lo habría detectado a tiempo. mutate(prom = mean(ventas))ysummarise(prom = mean(ventas))usan la misma fórmula, pero producen resultados de tamaños distintos. ¿Qué devuelve cada uno y en qué situación usaría cada cual?
12 Recursos de aprendizaje de la unidad
- Video (asincrónico): Transformación de datos con dplyr — Eduard Martínez
- Lectura guía: R for Data Science (2.ª ed.) — caps. Data transformation y Joins (versión en español, 1.ª ed.)
- Referencia rápida: Cheatsheets de Posit — dplyr (data transformation) y tidyr (data tidying)
- Material ampliado: Semana 4 y Semana 6 del curso de Business Analytics — transformación de datos y calidad/limpieza
- Guías del curso: Guía de estilo de scripts · Guía de organización de proyectos · Guía de uso de IA