Construcción de base, tablas y exportación
Universidad Católica de Temuco
2026-06-01
Construir una base de remuneraciones municipales a partir de archivos descargados desde Transparencia Activa.
En este laboratorio vamos a:
bind_rows()left_join()La carpeta del proyecto debería tener una estructura parecida a esta:
Recordar primero instalar en caso de no contar en la biblioteca con los paquetes antes mencionados.
Los archivos vienen separados por punto y coma, por eso usaremos read_csv2().
planta <- read_csv2("datos_raw/planta_marzo_2026.csv", locale = locale(encoding = "Latin1")) |>
clean_names()
contrata <- read_csv2("datos_raw/contrata_marzo_26.csv", locale = locale(encoding = "Latin1")) |>
clean_names()
cdt <- read_csv2("datos_raw/cdt_marzo_26.csv", locale = locale(encoding = "Latin1")) |>
clean_names()
honorarios <- read_csv2("datos_raw/honorarios_26.csv", locale = locale(encoding = "Latin1")) |>
clean_names()Antes de limpiar, debemos mirar qué columnas trae cada archivo.
Antes de limpiar, debemos mirar qué columnas trae cada archivo.
[1] "ano"
[2] "mes"
[3] "estamento"
[4] "nombre_completo"
[5] "cargo_o_funcion"
[6] "grado_eus_o_jornada"
[7] "calificacion_profesional_o_formacion"
[8] "region"
[9] "asignaciones_especiales_del_mes_inc_en_rem_bruta"
[10] "remuneracion_bruta_del_mes_incluye_bonos_e_incentivos_asig_especiales_horas_extras"
[11] "remuneracion_liquida_del_mes"
[12] "rem_adicionales_del_mes_no_inc_en_rem_bruta"
[13] "remuneracion_bonos_incentivos_del_mes_inc_en_rem_bruta"
[14] "derecho_a_horas_extraordinarias"
[15] "montos_y_horas_extraordinarias_diurnas_del_mes_inc_en_rem_bruta"
[16] "montos_y_horas_extraordinarias_nocturnas_del_mes_inc_en_rem_bruta"
[17] "montos_y_horas_extraordinarias_festivas_del_mes_inc_en_rem_bruta"
[18] "fecha_de_inicio_dd_mm_aa"
[19] "fecha_de_termino_dd_mm_aa"
[20] "observaciones"
[21] "viaticos_del_mes_no_inc_en_rem_bruta"
[22] "x22"
Antes de limpiar, debemos mirar qué columnas trae cada archivo.
[1] "ano"
[2] "mes"
[3] "estamento"
[4] "nombre_completo"
[5] "cargo_o_funcion"
[6] "grado_eus_o_jornada"
[7] "calificacion_profesional_o_formacion"
[8] "region"
[9] "asignaciones_especiales_del_mes_inc_en_rem_bruta"
[10] "remuneracion_bruta_del_mes_incluye_bonos_e_incentivos_asig_especiales_horas_extras"
[11] "remuneracion_liquida_del_mes"
[12] "rem_adicionales_del_mes_no_inc_en_rem_bruta"
[13] "remuneracion_bonos_incentivos_del_mes_inc_en_rem_bruta"
[14] "derecho_a_horas_extraordinarias"
[15] "montos_y_horas_extraordinarias_diurnas_del_mes_inc_en_rem_bruta"
[16] "montos_y_horas_extraordinarias_nocturnas_del_mes_inc_en_rem_bruta"
[17] "montos_y_horas_extraordinarias_festivas_del_mes_inc_en_rem_bruta"
[18] "fecha_de_inicio_dd_mm_aa"
[19] "fecha_de_termino_dd_mm_aa"
[20] "viaticos_del_mes_no_inc_en_rem_bruta"
[21] "observaciones"
[22] "x22"
Antes de limpiar, debemos mirar qué columnas trae cada archivo.
[1] "ano"
[2] "mes"
[3] "nombre_completo"
[4] "cargo_o_funcion"
[5] "grado_eus_o_jornada"
[6] "calificacion_profesional_o_formacion"
[7] "region"
[8] "asignaciones_especiales_del_mes_inc_en_rem_bruta"
[9] "remuneracion_bruta_del_mes_incluye_bonos_e_incentivos_asig_especiales_horas_extras"
[10] "remuneracion_liquida_del_mes"
[11] "rem_adicionales_del_mes_no_inc_en_rem_bruta"
[12] "remuneracion_bonos_incentivos_del_mes_inc_en_rem_bruta"
[13] "monto_de_desvinculacion_no_inc_en_rem_bruta"
[14] "derecho_a_horas_extraordinarias"
[15] "montos_y_horas_extraordinarias_del_mes_inc_en_rem_bruta"
[16] "viaticos_del_mes_no_inc_en_rem_bruta"
[17] "fecha_de_inicio_dd_mm_aaaa"
[18] "fecha_de_termino_dd_mm_aaaa"
[19] "observaciones"
[20] "x20"
Antes de limpiar, debemos mirar qué columnas trae cada archivo.
[1] "ano"
[2] "mes"
[3] "nombre_completo"
[4] "grado_eus_si_corresponde"
[5] "descripcion_de_la_funcion"
[6] "calificacion_profesional_o_formacion"
[7] "region"
[8] "honorario_total_bruto_del_mes"
[9] "honorario_total_liquido_del_mes"
[10] "tipo_de_pago"
[11] "descripcion_pago"
[12] "numero_de_cuotas"
[13] "fecha_de_inicio"
[14] "fecha_de_termino"
[15] "observaciones"
[16] "enlace_funciones_desarrolladas"
[17] "viaticos"
[18] "x18"
Rows: 72
Columns: 22
$ ano <dbl> …
$ mes <chr> …
$ estamento <chr> …
$ nombre_completo <chr> …
$ cargo_o_funcion <chr> …
$ grado_eus_o_jornada <dbl> …
$ calificacion_profesional_o_formacion <chr> …
$ region <chr> …
$ asignaciones_especiales_del_mes_inc_en_rem_bruta <chr> …
$ remuneracion_bruta_del_mes_incluye_bonos_e_incentivos_asig_especiales_horas_extras <chr> …
$ remuneracion_liquida_del_mes <chr> …
$ rem_adicionales_del_mes_no_inc_en_rem_bruta <chr> …
$ remuneracion_bonos_incentivos_del_mes_inc_en_rem_bruta <chr> …
$ derecho_a_horas_extraordinarias <chr> …
$ montos_y_horas_extraordinarias_diurnas_del_mes_inc_en_rem_bruta <chr> …
$ montos_y_horas_extraordinarias_nocturnas_del_mes_inc_en_rem_bruta <chr> …
$ montos_y_horas_extraordinarias_festivas_del_mes_inc_en_rem_bruta <chr> …
$ fecha_de_inicio_dd_mm_aa <chr> …
$ fecha_de_termino_dd_mm_aa <chr> …
$ observaciones <chr> …
$ viaticos_del_mes_no_inc_en_rem_bruta <chr> …
$ x22 <lgl> …
Rows: 56
Columns: 22
$ ano <dbl> …
$ mes <chr> …
$ estamento <chr> …
$ nombre_completo <chr> …
$ cargo_o_funcion <chr> …
$ grado_eus_o_jornada <dbl> …
$ calificacion_profesional_o_formacion <chr> …
$ region <chr> …
$ asignaciones_especiales_del_mes_inc_en_rem_bruta <chr> …
$ remuneracion_bruta_del_mes_incluye_bonos_e_incentivos_asig_especiales_horas_extras <chr> …
$ remuneracion_liquida_del_mes <chr> …
$ rem_adicionales_del_mes_no_inc_en_rem_bruta <chr> …
$ remuneracion_bonos_incentivos_del_mes_inc_en_rem_bruta <chr> …
$ derecho_a_horas_extraordinarias <chr> …
$ montos_y_horas_extraordinarias_diurnas_del_mes_inc_en_rem_bruta <chr> …
$ montos_y_horas_extraordinarias_nocturnas_del_mes_inc_en_rem_bruta <chr> …
$ montos_y_horas_extraordinarias_festivas_del_mes_inc_en_rem_bruta <chr> …
$ fecha_de_inicio_dd_mm_aa <chr> …
$ fecha_de_termino_dd_mm_aa <chr> …
$ viaticos_del_mes_no_inc_en_rem_bruta <chr> …
$ observaciones <chr> …
$ x22 <lgl> …
Rows: 5
Columns: 20
$ ano <dbl> …
$ mes <chr> …
$ nombre_completo <chr> …
$ cargo_o_funcion <chr> …
$ grado_eus_o_jornada <chr> …
$ calificacion_profesional_o_formacion <chr> …
$ region <chr> …
$ asignaciones_especiales_del_mes_inc_en_rem_bruta <chr> …
$ remuneracion_bruta_del_mes_incluye_bonos_e_incentivos_asig_especiales_horas_extras <chr> …
$ remuneracion_liquida_del_mes <chr> …
$ rem_adicionales_del_mes_no_inc_en_rem_bruta <chr> …
$ remuneracion_bonos_incentivos_del_mes_inc_en_rem_bruta <chr> …
$ monto_de_desvinculacion_no_inc_en_rem_bruta <chr> …
$ derecho_a_horas_extraordinarias <chr> …
$ montos_y_horas_extraordinarias_del_mes_inc_en_rem_bruta <chr> …
$ viaticos_del_mes_no_inc_en_rem_bruta <chr> …
$ fecha_de_inicio_dd_mm_aaaa <chr> …
$ fecha_de_termino_dd_mm_aaaa <chr> …
$ observaciones <chr> …
$ x20 <lgl> …
Rows: 204
Columns: 18
$ ano <dbl> 2026, 2026, 2026, 2026, 2026, 202…
$ mes <chr> "Marzo", "Marzo", "Marzo", "Marzo…
$ nombre_completo <chr> "Acuña Cerda, Jenifer Haydee", "A…
$ grado_eus_si_corresponde <chr> "No asimilado a grado", "No asimi…
$ descripcion_de_la_funcion <chr> "Prestacion de servicios Programa…
$ calificacion_profesional_o_formacion <chr> "Profesora Educación Fisica", "En…
$ region <chr> "Región de La Araucanía", "Región…
$ honorario_total_bruto_del_mes <chr> "$ 1.462.759", "$ 636.008", "$ 22…
$ honorario_total_liquido_del_mes <chr> "-", "-", "-", "-", "-", "-", "-"…
$ tipo_de_pago <chr> "Pago mensual", "Pago mensual", "…
$ descripcion_pago <chr> "-", "-", "-", "-", "-", "-", "-"…
$ numero_de_cuotas <chr> "-", "-", "-", "-", "-", "-", "-"…
$ fecha_de_inicio <chr> "02/01/2026", "01/01/2026", "05/0…
$ fecha_de_termino <chr> "31/12/2026", "31/12/2026", "30/0…
$ observaciones <chr> "decreto 415; decreto 874 17/02/2…
$ enlace_funciones_desarrolladas <chr> "No publica", "No publica", "No p…
$ viaticos <chr> "No informa", "No informa", "No i…
$ x18 <lgl> NA, NA, NA, NA, NA, NA, NA, NA, N…
Cada archivo debe conservar información sobre su origen.
Los archivos no tienen exactamente las mismas columnas. Por eso construiremos una versión simplificada de cada base.
planta_simple <- planta |>
transmute(
municipio,
unidad,
tipo_personal,
anio,
mes,
mes_num,
periodo,
nombre = nombre_completo,
estamento = estamento,
funcion = cargo_o_funcion,
grado_jornada = grado_eus_o_jornada,
formacion = calificacion_profesional_o_formacion,
region = region,
monto_bruto = remuneracion_bruta_del_mes_incluye_bonos_e_incentivos_asig_especiales_horas_extras,
monto_liquido = remuneracion_liquida_del_mes,
fecha_inicio = fecha_de_inicio_dd_mm_aa,
fecha_termino = fecha_de_termino_dd_mm_aa,
observaciones = observaciones
)contrata_simple <- contrata |>
transmute(
municipio,
unidad,
tipo_personal,
anio,
mes,
mes_num,
periodo,
nombre = nombre_completo,
estamento = estamento,
funcion = cargo_o_funcion,
grado_jornada = grado_eus_o_jornada,
formacion = calificacion_profesional_o_formacion,
region = region,
monto_bruto = remuneracion_bruta_del_mes_incluye_bonos_e_incentivos_asig_especiales_horas_extras,
monto_liquido = remuneracion_liquida_del_mes,
fecha_inicio = fecha_de_inicio_dd_mm_aa,
fecha_termino = fecha_de_termino_dd_mm_aa,
observaciones = observaciones
)cdt_simple <- cdt |>
transmute(
municipio,
unidad,
tipo_personal,
anio,
mes,
mes_num,
periodo,
nombre = nombre_completo,
estamento = NA_character_,
funcion = cargo_o_funcion,
grado_jornada = grado_eus_o_jornada,
formacion = calificacion_profesional_o_formacion,
region = region,
monto_bruto = remuneracion_bruta_del_mes_incluye_bonos_e_incentivos_asig_especiales_horas_extras,
monto_liquido = remuneracion_liquida_del_mes,
fecha_inicio = fecha_de_inicio_dd_mm_aaaa,
fecha_termino = fecha_de_termino_dd_mm_aaaa,
observaciones = observaciones
)honorarios_simple <- honorarios |>
transmute(
municipio,
unidad,
tipo_personal,
anio,
mes,
mes_num,
periodo,
nombre = nombre_completo,
estamento = NA_character_,
funcion = descripcion_de_la_funcion,
grado_jornada = grado_eus_si_corresponde,
formacion = calificacion_profesional_o_formacion,
region = region,
monto_bruto = honorario_total_bruto_del_mes,
monto_liquido = honorario_total_liquido_del_mes,
fecha_inicio = fecha_de_inicio,
fecha_termino = fecha_de_termino,
observaciones = observaciones
)Antes de apilar las bases, conviene asegurar que las columnas comunes tengan el mismo tipo. En estos archivos, por ejemplo, grado_jornada puede venir como número en una base y como texto en otra. Eso hace que bind_rows() se detenga.
Primero generaremos un conjunto, con los nombres de las variables que debiesen ser reconocidas como texto
Luego vamos a aplicar para cada elemento de esa lista de variables, la opción de ser conocida como texto (character)
planta_simple <- planta_simple |>
mutate(across(any_of(columnas_texto), as.character))
contrata_simple <- contrata_simple |>
mutate(across(any_of(columnas_texto), as.character))
cdt_simple <- cdt_simple |>
mutate(across(any_of(columnas_texto), as.character))
honorarios_simple <- honorarios_simple |>
mutate(across(any_of(columnas_texto), as.character))bind_rows()Como queremos apilar registros, usamos bind_rows().
Rows: 337
Columns: 18
$ municipio <chr> "Vilcún", "Vilcún", "Vilcún", "Vilcún", "Vilcún", "Vilcú…
$ unidad <chr> "Municipalidad", "Municipalidad", "Municipalidad", "Muni…
$ tipo_personal <chr> "planta", "planta", "planta", "planta", "planta", "plant…
$ anio <dbl> 2026, 2026, 2026, 2026, 2026, 2026, 2026, 2026, 2026, 20…
$ mes <chr> "marzo", "marzo", "marzo", "marzo", "marzo", "marzo", "m…
$ mes_num <dbl> 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,…
$ periodo <chr> "2026-03", "2026-03", "2026-03", "2026-03", "2026-03", "…
$ nombre <chr> "ALFARO PULGAR, PABLO ESTEBAN", "ALVIAL RIVERA, VIVIANA…
$ estamento <chr> "Técnico", "Profesional", "Técnico", "Administrativo", "…
$ funcion <chr> "TÉCNICO ADMINISTRACIÓN MUNICIPAL", "PROFESIONAL OBRAS M…
$ grado_jornada <chr> "14", "11", "16", "17", "7", "9", "15", "17", "17", "7",…
$ formacion <chr> "Técnico en Amplificación", "Ingeniero Civil", "Técnico …
$ region <chr> "Región de La Araucanía", "Región de La Araucanía", "Reg…
$ monto_bruto <chr> "$ 1.737.854", "$ 2.426.876", "$ 1.314.239", "$ 892.019"…
$ monto_liquido <chr> "$", "$", "$", "$", "$", "$", "$", "$", "$", "$", "$", "…
$ fecha_inicio <chr> "01/06/2015", "03/11/2020", "25/03/2019", "17/11/2025", …
$ fecha_termino <chr> "Indefinido", "Indefinido", "Indefinido", "30/06/2026", …
$ observaciones <chr> "Sin Observaciones", "Sin Observaciones", "Sin Observaci…
| Acción | Qué hace | Ejemplo |
|---|---|---|
| Recodificar | Cambia o agrupa valores de una variable existente. | 1 = Hombre, 2 = Mujer |
| Crear variable derivada | Construye una nueva variable usando información existente. | Obtener sexo desde nombre. |
| Corrección manual | Ajusta casos puntuales luego de revisar errores. | Cambiar fila 25 de No clasificado a Mujer. |
Los montos vienen como texto. Necesitamos convertirlos a números.
Como este proceso lo tendremos que repetir, generaremos una función, así podemos replicar este proceso solo escribiendo el nombre de la función posteriormente.
Min. 1st Qu. Median Mean 3rd Qu. Max.
47334 500000 885765 1015447 1294552 7008009
# A tibble: 1 × 3
casos sin_monto_bruto sin_monto_liquido
<int> <int> <int>
1 337 0 337
Vamos a crear una variable sexo para identificar diferencias entre tipos de funcionarios.
Con la información que tenemos… ¿cómo podríamos hacerlo?
¡Exacto! A partir de la variable nombre
remuneraciones. La columna con el nombre completo se llama nombrePrimero crea una variable nombre_limpio, que tiene todas las letras en minúscula. Esto evita que JUAN, Juan y juan sean tratados como valores distintos.
Luego, str_squish elimina los espacios innecesarios que pueen evistir al inicio, al final o entre palabras.
Por último, word(nombre_limpio, 3)Extrae la tercer palabra de nombre_limpio. Esto nos permitirá clasificar ese nombre según sexo.
Construir un diccionario de clasificación
Supongamos quiero corregir un nombre en particular:
remuneraciones <- remuneraciones |>
mutate(
sexo = if_else(
primer_nombre == "carla",
"Mujer",
sexo
)
)
#Si necesitas hacerlo para más casos
remuneraciones <- remuneraciones |>
mutate(
sexo = case_when(
primer_nombre == "carlos" ~ "Hombre",
primer_nombre == "constanza" ~ "Mujer",
primer_nombre == "alexis" ~ "Hombre",
TRUE ~ sexo
)
)Podemos también hacerlo a filas especificas. Por ejemplo, si quieres cambiar el sexo de la observación 25:
La lógica es: Si la fila es la número 25, cambia sexo a “Mujer”. PAra todas las demás filas, deja el valor original de sexo
remuneraciones <- remuneraciones |>
mutate(
formacion_limpia = str_to_lower(formacion),
formacion_limpia = str_squish(formacion_limpia),
nivel_formacion = case_when(
str_detect(formacion_limpia, "enseñanza básica|ensenanza basica|estudios básicos|estudios basicos") ~
"Enseñanza básica",
str_detect(formacion_limpia, "enseñanza media|ensenanza media|estudios medios|segundo año enseñanza media|media incompleta") ~
"Enseñanza media",
str_detect(formacion_limpia, "abogad|arquitect|asistente social|trabajador social|trabajadora social|cientista político|cientista politico|comunicador social|constructor civil|contador auditor|contador público|contador publico|educadora|fonoaudiolog|geolog|ingenier|kinesiolog|m[eé]dico|medico|odontolog|periodista|planificador social|profesor|profesora|psicolog|psicopedagog|sociolog|terapeuta ocupacional|veterinario|veterinaria") ~
"Profesional universitario",
str_detect(formacion_limpia, "técnico|tecnico|téc\\.|tec\\.|nivel medio|nivel superior") ~
"Técnico",
str_detect(formacion_limpia, "estudiante|egresad|estudios universitarios|estudios de|estudios pedagogía|estudios pedagogia") ~
"Estudiante / estudios incompletos",
str_detect(formacion_limpia, "auxiliar|maestro|monitor|instructor|mecánico|mecanico|soldador|secretaria") ~
"Oficio / apoyo administrativo",
TRUE ~ "Otro / no clasificado"
),
tiene_postgrado_diplomado = case_when(
str_detect(formacion_limpia, "magister|magíster|diplomado|postitulo|postítulo") ~ "Sí",
TRUE ~ "No"
)
)# A tibble: 4 × 3
tipo_personal registros porcentaje
<chr> <int> <dbl>
1 codigo_trabajo 5 1.5
2 contrata 56 16.6
3 honorarios 204 60.5
4 planta 72 21.4
tabla_remuneraciones <- remuneraciones |>
group_by(tipo_personal) |>
summarise(
registros = n(),
promedio_bruto = round(mean(monto_bruto, na.rm = TRUE), 0),
mediana_bruto = round(median(monto_bruto, na.rm = TRUE), 0),
minimo_bruto = min(monto_bruto, na.rm = TRUE),
maximo_bruto = max(monto_bruto, na.rm = TRUE),
sin_monto = sum(is.na(monto_bruto)),
.groups = "drop"
)# A tibble: 4 × 7
tipo_personal registros promedio_bruto mediana_bruto minimo_bruto maximo_bruto
<chr> <int> <dbl> <dbl> <dbl> <dbl>
1 codigo_traba… 5 446342 120448 47334 976221
2 contrata 56 1109228 917666 525133 2540591
3 honorarios 204 662758 636008 53340 1890000
4 planta 72 1981312 1582105 86621 7008009
# ℹ 1 more variable: sin_monto <int>
# A tibble: 4 × 7
`Tipo de personal` Registros `Promedio bruto` `Mediana bruto` `Mínimo bruto`
<chr> <int> <dbl> <dbl> <dbl>
1 codigo_trabajo 5 446342 120448 47334
2 contrata 56 1109228 917666 525133
3 honorarios 204 662758 636008 53340
4 planta 72 1981312 1582105 86621
# ℹ 2 more variables: `Máximo bruto` <dbl>, `Sin monto informado` <int>
flextableTipo de personal | Registros | Promedio bruto | Mediana bruto | Mínimo bruto | Máximo bruto | Sin monto informado |
|---|---|---|---|---|---|---|
codigo_trabajo | 5 | 446,342 | 120,448 | 47,334 | 976,221 | 0 |
contrata | 56 | 1,109,228 | 917,666 | 525,133 | 2,540,591 | 0 |
honorarios | 204 | 662,758 | 636,008 | 53,340 | 1,890,000 | 0 |
planta | 72 | 1,981,312 | 1,582,105 | 86,621 | 7,008,009 | 0 |
dir.create("outputs", showWarnings = FALSE)
doc <- read_docx()
doc <- doc |>
body_add_par("Resultados descriptivos preliminares", style = "heading 1") |>
body_add_par("Tabla 1. Remuneraciones brutas por tipo de personal", style = "heading 2") |>
body_add_flextable(tabla_flex) |>
body_add_par(
"Fuente: elaboración propia a partir de Transparencia Activa, Municipalidad de Vilcún, marzo 2026.",
style = "Normal"
)
print(doc, target = "outputs/tabla_remuneraciones_vilcun.docx")gttabla_nivel_formacion <- remuneraciones |>
count(nivel_formacion, sort = TRUE) |>
mutate(
porcentaje = round(n / sum(n) * 100, 1)
)
ft_nivel_formacion <- tabla_nivel_formacion |>
flextable() |>
set_header_labels(
nivel_formacion = "Nivel de formación",
n = "Frecuencia",
porcentaje = "Porcentaje"
) |>
colformat_num(
j = "porcentaje",
digits = 1,
suffix = "%"
) |>
theme_booktabs() |>
autofit()
ft_nivel_formacionNivel de formación | Frecuencia | Porcentaje |
|---|---|---|
Profesional universitario | 137 | 40.7% |
Técnico | 102 | 30.3% |
Enseñanza media | 67 | 19.9% |
Oficio / apoyo administrativo | 12 | 3.6% |
Otro / no clasificado | 9 | 2.7% |
Estudiante / estudios incompletos | 6 | 1.8% |
Enseñanza básica | 4 | 1.2% |
tabla_formacion_personal <- remuneraciones |>
count(nivel_formacion, tipo_personal) |>
group_by(nivel_formacion) |>
mutate(
porcentaje = round(n / sum(n) * 100, 1)
) |>
ungroup()
ft_formacion_personal <- tabla_formacion_personal |>
flextable() |>
set_header_labels(
nivel_formacion = "Nivel de formación",
tipo_personal = "Tipo de personal",
n = "Frecuencia",
porcentaje = "Porcentaje dentro del nivel"
) |>
colformat_num(
j = "porcentaje",
digits = 1,
suffix = "%"
) |>
theme_vanilla() |>
autofit()
ft_formacion_personalNivel de formación | Tipo de personal | Frecuencia | Porcentaje dentro del nivel |
|---|---|---|---|
Enseñanza básica | contrata | 1 | 25.0% |
Enseñanza básica | honorarios | 2 | 50.0% |
Enseñanza básica | planta | 1 | 25.0% |
Enseñanza media | codigo_trabajo | 1 | 1.5% |
Enseñanza media | contrata | 20 | 29.9% |
Enseñanza media | honorarios | 27 | 40.3% |
Enseñanza media | planta | 19 | 28.4% |
Estudiante / estudios incompletos | contrata | 1 | 16.7% |
Estudiante / estudios incompletos | honorarios | 5 | 83.3% |
Oficio / apoyo administrativo | honorarios | 10 | 83.3% |
Oficio / apoyo administrativo | planta | 2 | 16.7% |
Otro / no clasificado | honorarios | 8 | 88.9% |
Otro / no clasificado | planta | 1 | 11.1% |
Profesional universitario | codigo_trabajo | 4 | 2.9% |
Profesional universitario | contrata | 11 | 8.0% |
Profesional universitario | honorarios | 91 | 66.4% |
Profesional universitario | planta | 31 | 22.6% |
Técnico | contrata | 23 | 22.5% |
Técnico | honorarios | 61 | 59.8% |
Técnico | planta | 18 | 17.6% |
left_join()Ahora imaginemos que queremos agregar información externa del municipio.
Usamos left_join() porque queremos agregar columnas, no filas.
left_join()Revisamos:
# A tibble: 6 × 5
municipio provincia region_administrativa tipo_comuna tipo_personal
<chr> <chr> <chr> <chr> <chr>
1 Vilcún Cautín La Araucanía Municipio de referenc… planta
2 Vilcún Cautín La Araucanía Municipio de referenc… planta
3 Vilcún Cautín La Araucanía Municipio de referenc… planta
4 Vilcún Cautín La Araucanía Municipio de referenc… planta
5 Vilcún Cautín La Araucanía Municipio de referenc… planta
6 Vilcún Cautín La Araucanía Municipio de referenc… planta
El número de filas no cambia, porque left_join() agregó columnas de contexto.
Cada grupo debe adaptar el script a su municipio.
Debe entregar una carpeta con:
Escriba un párrafo de 5 a 7 líneas que responda:
El objetivo de este laboratorio no es solo producir una tabla.
El objetivo es construir una base trazable, ordenada y reproducible, que luego pueda alimentar el póster descriptivo.
La semana anterior trabajamos el paso desde archivos descargados a una base analizable:
Esta semana avanzamos al siguiente paso:
El objetivo de esta clase no es solamente aprender comandos de ggplot2.
El objetivo es aprender a transformar resultados descriptivos en gráficos claros, interpretables y útiles para un póster.
Idea central
Un gráfico no es decoración.
Un gráfico es una forma de comunicar un patrón de los datos.
La Evaluación 2 será un póster científico descriptivo. Debe incluir:
| Semana | Actividad |
|---|---|
| Semana 11 | Gráficos descriptivos y avance del póster |
| Semana 12 | Avance en la construcción de póster |
| Semana 13 | Entrega digital final del póster y respaldo técnico |
| Semana 14 | Jornada de pósters impresos abierta a la comunidad |
Antes de hacer cualquier gráfico, debemos tener claro:
El póster debe partir situando la comuna.
No basta con mostrar remuneraciones. También hay que indicar en qué tipo de comuna se está observando el fenómeno.
La sección de contexto comunal debe incluir:
El contexto comunal permite interpretar los datos con más cuidado.
No es lo mismo observar remuneraciones en:
Importante
Estas variables contextualizan. No deben ser tratadas como causas en este ejercicio.
| Situación | Mejor opción |
|---|---|
| Mostrar cifras exactas | Tabla |
| Comparar categorías | Gráfico de barras |
| Mostrar evolución temporal | Gráfico de líneas |
| Mostrar distribución de montos | Histograma o boxplot |
| Mostrar pocos indicadores contextuales | Ficha o tabla breve |
| Mostrar ubicación territorial | Mapa |
| Sintetizar resultados principales | Hallazgos con cifras |
ggplot2Un gráfico en ggplot2 se construye por capas.
| Elemento | Función |
|---|---|
data |
Base de datos que se usará |
aes() |
Variables que se asignan a los ejes o atributos visuales |
geom_col() |
Barras con valores ya calculados |
geom_line() |
Línea para evolución temporal |
geom_boxplot() |
Distribución por grupo |
geom_histogram() |
Distribución de una variable numérica |
labs() |
Títulos, ejes y fuente |
ggsave() |
Exportar gráfico |
Sirve para comparar categorías.
Ejemplo de pregunta:
¿Qué tipo de vínculo contractual concentra más registros en la Municipalidad de Vilcún?
library(tidyverse)
remuneraciones |>
count(tipo_personal) |>
ggplot(aes(x = tipo_personal, y = n)) +
geom_col() +
labs(
title = "Registros por tipo de personal",
subtitle = "Municipalidad de Vilcún, marzo 2026",
x = "Tipo de personal",
y = "Número de registros",
caption = "Fuente: Transparencia Activa."
)Recomendación, visitar el repositorio de ggplot2 https://r-graph-gallery.com/
También podemos modificar los colores.
En ggplot2, se puede identificar con el nombre, pero también con un Hex code. Los códigos hexadecimal se ven así: #2765F5
Para encontrar su Hex code, pueden visitar: https://htmlcolorcodes.com
library(tidyverse)
library(scales)
grafico_tipo_personal <- remuneraciones |>
count(tipo_personal) |>
mutate(
porcentaje = n / sum(n),
tipo_personal = fct_reorder(tipo_personal, n)
) |>
ggplot(aes(x = tipo_personal, y = n)) +
geom_col(
fill = "#1F4E79",
width = 0.68
) +
geom_text(
aes(
label = paste0(n, " registros\n", percent(porcentaje, accuracy = 0.1))
),
hjust = -0.10,
size = 3.8,
lineheight = 0.9
) +
coord_flip() +
scale_y_continuous(
expand = expansion(mult = c(0, 0.18))
) +
labs(
title = "Registros por tipo de personal",
subtitle = "Municipalidad de Vilcún, marzo 2026",
x = NULL,
y = "Número de registros",
caption = "Fuente: elaboración propia con datos de Transparencia Activa."
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(
face = "bold",
size = 18
),
plot.subtitle = element_text(
size = 12,
color = "grey30"
),
axis.text.y = element_text(
size = 11,
face = "bold"
),
axis.text.x = element_text(
size = 10,
color = "grey35"
),
axis.title.x = element_text(
size = 11,
margin = margin(t = 8)
),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
plot.caption = element_text(
size = 9,
color = "grey35",
hjust = 0
),
plot.margin = margin(12, 25, 12, 12)
)
grafico_tipo_personalgeom_col() y geom_bar()Hay que distinguir:
| Comando | Cuándo usarlo |
|---|---|
geom_bar() |
Cuando quiero que R cuente automáticamente los casos |
geom_col() |
Cuando ya calculé el valor que quiero graficar |
Para este curso, muchas veces usaremos geom_col(), porque antes construiremos tablas con count() o summarise().
Si la pregunta es por composición, no basta con contar.
Conviene calcular porcentajes.
tabla_tipo <- remuneraciones |>
count(tipo_personal, name = "registros") |>
mutate(
porcentaje = registros / sum(registros) * 100
)
ggplot(tabla_tipo, aes(x = tipo_personal, y = porcentaje)) +
geom_col() +
labs(
title = "Composición porcentual del personal informado",
subtitle = "Municipalidad de Vilcún, marzo 2026",
x = "Tipo de personal",
y = "Porcentaje del total de registros",
caption = "Fuente: Transparencia Activa."
)Un gráfico de barras permite decir:
No permite decir:
Sirve para comparar la distribución de una variable numérica entre grupos.
Ejemplo de pregunta:
¿Cómo se distribuyen los montos brutos informados según tipo de vínculo?
ggplot(remuneraciones, aes(x = tipo_personal, y = monto_bruto)) +
geom_boxplot() +
scale_y_continuous(
labels = label_number(
prefix = "$",
big.mark = ".",
decimal.mark = ",",
accuracy = 1
)
) +
labs(
title = "Distribución de remuneraciones brutas por tipo de personal",
subtitle = "Municipalidad de Vilcún, marzo 2026",
x = "Tipo de personal",
y = "Monto bruto informado",
caption = "Fuente: Transparencia Activa."
)library(tidyverse)
library(scales)
grafico_boxplot_remuneraciones_horizontal <- remuneraciones |>
mutate(
tipo_personal = case_when(
tipo_personal == "codigo_trabajo" ~ "Código del Trabajo",
tipo_personal == "contrata" ~ "Contrata",
tipo_personal == "planta" ~ "Planta",
tipo_personal == "honorarios" ~ "Honorarios",
TRUE ~ tipo_personal
),
tipo_personal = fct_reorder(tipo_personal, monto_bruto, .fun = median, na.rm = TRUE)
) |>
ggplot(aes(x = tipo_personal, y = monto_bruto)) +
geom_boxplot(
fill = "#1F4E79",
color = "grey20",
alpha = 0.80,
width = 0.60,
outlier.color = "#C00000",
outlier.alpha = 0.55,
outlier.size = 1.8
) +
coord_flip() +
scale_y_continuous(
labels = label_number(
prefix = "$",
big.mark = ".",
decimal.mark = ",",
accuracy = 1
)
) +
labs(
title = "Distribución de remuneraciones brutas por tipo de personal",
subtitle = "Municipalidad de Vilcún, marzo 2026",
x = NULL,
y = "Monto bruto informado",
caption = "Fuente: elaboración propia con datos de Transparencia Activa."
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 17, color = "grey15"),
plot.subtitle = element_text(size = 12, color = "grey35", margin = margin(b = 10)),
axis.text.y = element_text(face = "bold", size = 10, color = "grey25"),
axis.text.x = element_text(size = 10, color = "grey35"),
axis.title.x = element_text(size = 11, margin = margin(t = 8)),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
plot.caption = element_text(hjust = 0, size = 9, color = "grey40"),
plot.margin = margin(12, 18, 12, 12)
)
grafico_boxplot_remuneraciones_horizontalUn boxplot permite observar:
Advertencia
En este caso, las diferencias entre tipos de personal deben interpretarse con cautela, porque no necesariamente corresponden a cargos, jornadas o funciones equivalentes.
Sirve para observar la distribución general de una variable numérica.
library(tidyverse)
library(scales)
grafico_hist_remuneraciones <- ggplot(remuneraciones, aes(x = monto_bruto)) +
geom_histogram(
bins = 30,
fill = "#7675BF",
color = "white",
linewidth = 0.3,
alpha = 0.85
) +
geom_vline(
aes(xintercept = median(monto_bruto, na.rm = TRUE)),
linetype = "dashed",
linewidth = 0.9,
color = "#C00000"
) +
scale_x_continuous(
labels = label_number(
prefix = "$",
big.mark = ".",
decimal.mark = ",",
accuracy = 1
)
) +
labs(
title = "Distribución de remuneraciones brutas informadas",
subtitle = "Municipalidad de Vilcún, marzo 2026",
x = "Monto bruto informado",
y = "Número de registros",
caption = "Fuente: elaboración propia con datos de Transparencia Activa.\nNota: la línea roja segmentada indica la mediana."
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(
face = "bold",
size = 17,
color = "grey15"
),
plot.subtitle = element_text(
size = 12,
color = "grey35",
margin = margin(b = 10)
),
axis.text.x = element_text(
size = 10,
color = "grey35",
angle = 25,
hjust = 1
),
axis.text.y = element_text(
size = 10,
color = "grey35"
),
axis.title.x = element_text(
size = 11,
margin = margin(t = 8)
),
axis.title.y = element_text(
size = 11,
margin = margin(r = 8)
),
panel.grid.minor = element_blank(),
panel.grid.major.y = element_line(color = "grey88"),
panel.grid.major.x = element_blank(),
plot.caption = element_text(
hjust = 0,
size = 9,
color = "grey40"
),
plot.margin = margin(12, 18, 12, 12)
)
grafico_hist_remuneracionesCada hallazgo debería tener:
Ejemplo:
El personal a contrata concentra XX registros, equivalente al XX% del total informado. Esto sugiere que, dentro de los registros disponibles, este vínculo tiene un peso relevante en la estructura municipal observada. Sin embargo, este resultado depende de los archivos disponibles y del periodo analizado.
| Error | Por qué es problema |
|---|---|
| No poner fuente | El lector no sabe de dónde vienen los datos |
| No indicar periodo | El resultado queda descontextualizado |
| Usar gráfico circular con muchas categorías | Se vuelve difícil de leer |
| Graficar montos sin limpiarlos | R puede ordenar o calcular mal |
| Usar nombres técnicos en ejes | El público no entiende la variable |
| Saturar el gráfico con etiquetas | Pierde legibilidad |
| Interpretar causalmente | Excede el objetivo descriptivo |
| Usar colores sin criterio | Confunde más de lo que ayuda |
Para el póster:
Todo gráfico que vaya al póster debe tener:
| Elemento | Pregunta que responde |
|---|---|
| Título | ¿Qué muestra el gráfico? |
| Subtítulo | ¿De qué comuna y periodo se trata? |
| Ejes claros | ¿Qué representa cada dimensión? |
| Fuente | ¿De dónde vienen los datos? |
| Nota si corresponde | ¿Qué cuidado debe tener el lector? |
Cada gráfico debe cumplir una función.
| Elemento del póster | Función |
|---|---|
| Contexto comunal | Situar el caso |
| Pregunta descriptiva | Orientar la lectura |
| Fuente y datos | Explicar de dónde vienen los datos |
| Tabla clave | Mostrar cifras precisas |
| Gráfico 1 | Mostrar composición |
| Gráfico 2 | Mostrar evolución o comparación |
| Hallazgos | Sintetizar mensajes principales |
| Limitaciones | Reconocer alcances del análisis |
Mensaje final
Un buen gráfico no es el que se ve más complejo.
Es el que permite entender rápidamente un resultado descriptivo, con fuente clara, periodo definido y una interpretación prudente.
Sirve para mostrar evolución temporal.
Ejemplo de pregunta:
¿Cómo ha evolucionado la remuneración bruta mediana por tipo de personal entre marzo de 2023 y marzo de 2026?
tabla_tiempo <- remuneraciones |>
group_by(fecha_periodo, tipo_personal) |>
summarise(
mediana_bruto = median(monto_bruto, na.rm = TRUE),
.groups = "drop"
)
ggplot(tabla_tiempo, aes(x = fecha_periodo, y = mediana_bruto, group = tipo_personal)) +
geom_line() +
geom_point() +
labs(
title = "Evolución de la remuneración bruta mediana",
subtitle = "Según tipo de personal",
x = "Periodo",
y = "Monto bruto mediano",
caption = "Fuente: Transparencia Activa."
)Para gráficos de evolución, el periodo debe estar bien ordenado.
No basta con tener mes = "marzo" o periodo = "2026-03" como texto.
Conviene crear una fecha:
Esto permite que R ordene los meses correctamente.
tabla_tiempo <- remuneraciones |>
group_by(fecha_periodo, tipo_personal) |>
summarise(
mediana_bruto = median(monto_bruto, na.rm = TRUE),
.groups = "drop"
)
ggplot(tabla_tiempo, aes(x = fecha_periodo, y = mediana_bruto, group = tipo_personal)) +
geom_line() +
geom_point() +
labs(
title = "Evolución de la remuneración bruta mediana",
subtitle = "Según tipo de personal",
x = "Periodo",
y = "Monto bruto mediano",
caption = "Fuente: Transparencia Activa."
)Mensaje final
Un buen gráfico no es el que se ve más complejo.
Es el que permite entender rápidamente un resultado descriptivo, con fuente clara, periodo definido y una interpretación prudente.