🦉 LaborInsightAnalytics

1 Mercado Laboral Colombiano

1.0.1 Análisis Exhaustivo · DANE · 2010–2025 · Desagregación por Género y Ciudad

Equipo: TAD 12 · Ficha 3314045

Fuente: DANE — GEIH

Período: 2010–2025

Fecha: 01 jul. 2026


2 Introducción

📋

Contexto del Análisis

Este informe analiza el mercado laboral colombiano utilizando los datos de la Encuesta Continua de Hogares (GEIH) del DANE. El dataset unificado integra ocho fuentes con distintas granularidades: cifras nacionales mensuales desagregadas por género, datos anuales de salario mínimo y fuerza de trabajo, indicadores de pobreza monetaria, y cifras de 37 ciudades del país entre 2021 y 2025.

El objetivo es construir una visión completa, con perspectiva de género y territorial, del comportamiento del empleo colombiano a lo largo de 15 años que incluyeron ciclos de crecimiento, una pandemia global y una recuperación desigual.

# ---- KPIs de descripción ----
n_gen   <- nrow(df_gen)
n_smlv  <- nrow(df_smlv)
n_pob   <- nrow(df_pob)
n_ing   <- nrow(df_ing)
n_total <- n_gen + n_smlv + n_pob + n_ing
📊

Hoja Género

384 registros.
Desagregación mensual F/M de los 4 indicadores laborales principales.
Serie 2010–2025.

💼

Hoja SMLV & Fuerza de Trabajo

16 registros anuales.
SMLV histórico, tasas nacionales y población ocupada/desocupada.

💰

Hoja Pobreza Monetaria

14 registros anuales.
Tasa de pobreza nacional.

🏙️

Hoja Ingresos 5 Ciudades

25 registros.
Distribución de asalariados vs. independientes.


3 Descripción General del Dataset

🗂️

Estructura y Tipos de Variables

<span class="card-ico">📊</span>
<h4>Hoja Género</h4>
<p><strong>384</strong> registros. Desagregación mensual F/M de los 4 indicadores laborales principales. Serie 2010–2025.</p>
<span class="card-ico">💼</span>
<h4>Hoja SMLV & Fuerza de Trabajo</h4>
<p><strong>16</strong> registros anuales. SMLV histórico, tasas nacionales y población ocupada/desocupada 2010–2025.</p>
<span class="card-ico">💰</span>
<h4>Hoja Pobreza Monetaria</h4>
<p><strong>14</strong> registros anuales. Tasa de pobreza nacional y personas en pobreza 2012–2025.</p>
<span class="card-ico">🏙️</span>
<h4>Hoja Ingresos 5 Ciudades</h4>
<p><strong>25</strong> registros. Distribución asalariados vs. independientes en 5 ciudades vulnerables 2021–2025.</p>

4 Calidad de los Datos

🔍

Diagnóstico de Calidad

# Análisis de nulos
nulos_gen  <- sum(is.na(df_gen[, c("TGP","TO","TD","TS")]))
nulos_smlv <- sum(is.na(df_smlv[, c("TGP","TO","TD","Salario")]))
dup_gen    <- sum(duplicated(df_gen))
dup_smlv   <- sum(duplicated(df_smlv))

# Rango de valores
tgp_rng <- range(df_gen$TGP, na.rm = TRUE)
to_rng  <- range(df_gen$TO,  na.rm = TRUE)
td_rng  <- range(df_gen$TD,  na.rm = TRUE)

# Consistencia: TO siempre debe ser < TGP
inconsistente <- df_gen %>% filter(TO > TGP) %>% nrow()
Nulos (género)
0
Dataset limpio
Duplicados
0
Sin duplicados
📐
TO > TGP
0
Inconsistencias lógicas
📈
Rango TGP
42–82.9%
Mín–Máx histórico
# Detección de outliers con IQR en TD
q1  <- quantile(df_gen$TD, 0.25, na.rm = TRUE)
q3  <- quantile(df_gen$TD, 0.75, na.rm = TRUE)
iqr <- q3 - q1
outliers_td <- df_gen %>% filter(TD < q1 - 1.5*iqr | TD > q3 + 1.5*iqr)

p_out <- ggplot(df_gen, aes(x = Género, y = TD * 100, fill = Género)) +
  geom_boxplot(outlier.color = pal$gold, outlier.size = 2.5, alpha = 0.85, width = 0.5) +
  scale_fill_manual(values = c("FEMENINO" = pal$gold, "MASCULINO" = pal$navy)) +
  labs(
    title    = "Distribución de la Tasa de Desocupación por Género",
    subtitle = paste0("Período 2010–2025 · Outliers detectados (IQR): ", nrow(outliers_td)),
    x = NULL, y = "TD (%)",
    caption = "Fuente: DANE — GEIH"
  ) +
  theme_labor() +
  theme(legend.position = "none")

ggplotly(p_out) %>%
  plotly_layout("Distribución TD por Género",
                paste0("Período 2010–2025 · Outliers IQR: ", nrow(outliers_td)))
🎯 Por qué un boxplotcomparar distribuciones

El boxplot es el estándar para comparar la distribución completa (mediana, dispersión y valores atípicos) de una variable continua entre dos grupos categóricos. Aquí se necesitaba ver no solo el promedio de la TD por género, sino su variabilidad y la presencia de outliers, algo que un gráfico de barras o de línea no puede mostrar.

🔎 Lectura diagnóstica

La caja femenina se ubica más arriba y es más ancha que la masculina, evidenciando mayor nivel y mayor dispersión de desempleo en mujeres. Los puntos fuera de los bigotes corresponden en su mayoría a 2020, confirmando que la pandemia generó valores atípicos reales y no errores de captura.

Diagnóstico de calidad: El dataset presenta una calidad excelente. No se detectaron valores nulos en los indicadores principales ni registros duplicados. La única anomalía esperada son los valores de 2020, que representan un evento real (pandemia COVID-19) y no errores de datos. La tasa de desocupación femenina presenta mayor variabilidad que la masculina, lo que es coherente con la estructura del mercado laboral colombiano.


5 Estadística Descriptiva

📊

Resumen Estadístico Completo

# Función para calcular estadísticas descriptivas completas
desc_stats <- function(x, nombre) {
  x <- x[!is.na(x)] * 100
  data.frame(
    Variable   = nombre,
    N          = length(x),
    Media      = round(mean(x), 2),
    Mediana    = round(median(x), 2),
    Moda       = round(as.numeric(names(sort(table(round(x,1)), decreasing=TRUE)[1])), 2),
    SD         = round(sd(x), 2),
    CV         = paste0(round(sd(x)/mean(x)*100, 1), "%"),
    Mín        = round(min(x), 2),
    Máx        = round(max(x), 2),
    P25        = round(quantile(x, 0.25), 2),
    P75        = round(quantile(x, 0.75), 2),
    Asimetría  = round(mean((x - mean(x))^3) / sd(x)^3, 3)
  )
}

# Separar por género
tgp_f <- df_gen %>% filter(Género == "FEMENINO") %>% pull(TGP)
tgp_m <- df_gen %>% filter(Género == "MASCULINO") %>% pull(TGP)
to_f  <- df_gen %>% filter(Género == "FEMENINO") %>% pull(TO)
to_m  <- df_gen %>% filter(Género == "MASCULINO") %>% pull(TO)
td_f  <- df_gen %>% filter(Género == "FEMENINO") %>% pull(TD)
td_m  <- df_gen %>% filter(Género == "MASCULINO") %>% pull(TD)

stats_df <- bind_rows(
  desc_stats(tgp_f, "TGP — Femenino"),
  desc_stats(tgp_m, "TGP — Masculino"),
  desc_stats(to_f,  "TO  — Femenino"),
  desc_stats(to_m,  "TO  — Masculino"),
  desc_stats(td_f,  "TD  — Femenino"),
  desc_stats(td_m,  "TD  — Masculino")
)

reactable(
  stats_df,
  theme = reactableTheme(
    headerStyle = list(background = "#0B2545", color = "white", fontSize = "0.75rem"),
    cellStyle = list(fontSize = "0.83rem")
  ),
  columns = list(
    Variable = colDef(minWidth = 140),
    Asimetría = colDef(cell = function(value) {
      color <- if (abs(value) < 0.5) "#27AE60" else if (abs(value) < 1) "#C2790F" else "#C0392B"
      div(value, style = list(color = color, fontWeight = "700"))
    })
  ),
  striped = TRUE, highlight = TRUE, searchable = TRUE, defaultPageSize = 6
)
# Violin plots comparativos
df_long <- df_gen %>%
  select(Género, TGP, TO, TD, TS) %>%
  pivot_longer(cols = c(TGP, TO, TD, TS), names_to = "Indicador", values_to = "Valor") %>%
  mutate(Valor = Valor * 100)

p_violin <- ggplot(df_long, aes(x = Indicador, y = Valor, fill = Género)) +
  geom_violin(alpha = 0.7, position = position_dodge(width = 0.8), width = 0.8) +
  geom_boxplot(alpha = 0.6, width = 0.2, position = position_dodge(width = 0.8),
               outlier.size = 1.5, outlier.alpha = 0.5) +
  scale_fill_manual(values = c("FEMENINO" = pal$gold, "MASCULINO" = pal$navy)) +
  labs(title = "Distribución de Indicadores por Género (2010–2025)",
       x = NULL, y = "Valor (%)", fill = "Género") +
  theme_labor()

ggplotly(p_violin) %>%
  plotly_layout("Distribución de Indicadores por Género", "Violín + Boxplot · 2010–2025")
🎯 Por qué violín + boxplotforma de la distribución

El violín agrega a la comparación de grupos la forma de la densidad (dónde se concentran los valores), algo que un boxplot solo no revela. Combinarlo con un boxplot interno mantiene los estadísticos de referencia (mediana, cuartiles) mientras se ven posibles distribuciones bimodales o asimétricas en los cuatro indicadores a la vez.

🔎 Lectura diagnóstica

La TD femenina muestra una forma más ensanchada hacia valores altos (cola derecha), coherente con la asimetría positiva identificada más adelante. La TGP y la TO tienen violines más simétricos y compactos, indicando un comportamiento más estable a lo largo de la serie.


6 Análisis Exploratorio (EDA)

🧭

Exploración Visual Completa

6.1 Evolución Histórica Nacional

df_smlv_plot <- df_smlv %>% filter(!is.na(TO))

p_nac <- plot_ly(df_smlv_plot, x = ~Año) %>%
  add_lines(y = ~TGP * 100, name = "TGP", line = list(color = pal$navy,  width = 3)) %>%
  add_lines(y = ~TO  * 100, name = "TO",  line = list(color = pal$gold,  width = 3)) %>%
  add_lines(y = ~TD  * 100, name = "TD",  line = list(color = pal$teal,  width = 3, dash = "dash")) %>%
  add_lines(y = ~TS  * 100, name = "TS",  line = list(color = pal$amber, width = 2, dash = "dot")) %>%
  add_annotations(
    x = 2020, y = 54, text = "🦠 COVID-19",
    showarrow = TRUE, arrowcolor = pal$red,
    font = list(size = 11, color = pal$red)
  )

p_nac %>% plotly_layout(
  "Evolución de los Cuatro Indicadores Laborales (2010–2025)",
  "Promedios anuales nacionales · Fuente: DANE"
) %>%
  layout(yaxis = list(title = "(%)", ticksuffix = "%"),
         xaxis = list(title = "Año"))
🎯 Por qué un gráfico de líneasserie temporal

Con cuatro indicadores medidos año a año durante 15 años, la línea es el gráfico natural para mostrar tendencia y evolución continua en el tiempo. Usar cuatro líneas superpuestas (en vez de cuatro gráficos separados) permite comparar directamente su comportamiento relativo, incluyendo el quiebre puntual de 2020, señalado con una anotación.

🔎 Lectura diagnóstica

TGP y TO se mueven prácticamente en paralelo, lo cual es consistente con su alta correlación. La TD (línea discontinua) muestra el pico más agudo en 2020 y una recuperación gradual, mientras que la TS se mantiene relativamente estable en niveles bajos durante todo el período.

6.2 Histogramas de Distribución

# Ggplot2 version más controlada
df_hist <- df_gen %>%
  select(Género, TGP, TO, TD, TS) %>%
  pivot_longer(
    -Género,
    names_to = "Indicador",
    values_to = "Valor"
  ) %>%
  mutate(Valor = Valor * 100)

p_h <- ggplot(df_hist,
              aes(x = Valor,
                  fill = Género,
                  color = Género)) +
  geom_histogram(
    bins = 25,
    alpha = 0.7,
    position = "identity"
  ) +
  scale_fill_manual(values = c(
    "FEMENINO" = pal$gold,
    "MASCULINO" = pal$navy
  )) +
  scale_color_manual(values = c(
    "FEMENINO" = "#8a6508",
    "MASCULINO" = "#071A33"
  )) +
  facet_wrap(~Indicador, scales = "free", ncol = 2) +
  labs(
    title = "Histogramas de distribución por indicador y género",
    x = "Valor (%)",
    y = "Frecuencia"
  ) +
  theme_labor() +
  theme(
    strip.background = element_rect(fill = pal$navy),
    strip.text = element_text(color = "white",
                              face = "bold",
                              size = 10)
  )

ggplotly(p_h) %>%
  plotly_layout(
    "Histogramas por Indicador y Género",
    "Distribución de frecuencias · 2010–2025"
  )
🎯 Por qué histogramasforma y sesgo

El histograma es el gráfico correcto para examinar cómo se distribuyen los valores individuales de una variable continua: si son simétricos, sesgados, uni o multimodales. Se usó facet_wrap por indicador para no mezclar escalas distintas (TGP, TO, TD y TS) en un mismo eje, y superposición por color para comparar género dentro de cada panel.

🔎 Lectura diagnóstica

Los histogramas de TD muestran una cola derecha más marcada en el género femenino, es decir, más meses con desempleo elevado. TGP y TO presentan formas más cercanas a la normal, con las distribuciones femenina y masculina desplazadas entre sí pero de forma similar, evidencia visual de la brecha estructural sin cruce de curvas.

6.3 Análisis Mensual y Estacionalidad

meses_ord <- c("ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO",
               "JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE")

estac <- df_gen %>%
  group_by(Mes_n, Género) %>%
  summarise(
    TGP_m = mean(TGP, na.rm = TRUE) * 100,
    TO_m  = mean(TO,  na.rm = TRUE) * 100,
    TD_m  = mean(TD,  na.rm = TRUE) * 100,
    .groups = "drop"
  ) %>%
  filter(!is.na(Mes_n)) %>%
  mutate(Mes = factor(Mes_n, labels = substr(meses_ord, 1, 3)))

p_est <- ggplot(estac, aes(x = Mes, y = TD_m, group = Género, color = Género)) +
  geom_line(size = 1.3) +
  geom_point(size = 2.5) +
  scale_color_manual(values = c("FEMENINO" = pal$gold, "MASCULINO" = pal$navy)) +
  labs(title = "Estacionalidad de la Tasa de Desocupación por Mes",
       subtitle = "Promedio histórico por mes y género · 2010–2025",
       x = "Mes", y = "TD promedio (%)", color = "Género") +
  theme_labor()

ggplotly(p_est) %>%
  plotly_layout("Patrón Estacional de la Tasa de Desocupación",
                "Promedio por mes · 2010–2025")
🎯 Por qué línea por mesestacionalidad

Cuando el eje X es una variable cíclica ordenada (los 12 meses del año) y se busca detectar un patrón repetitivo, la línea conectada por puntos es más legible que barras, porque resalta la forma de la curva (subidas y bajadas) en vez de comparar magnitudes aisladas.

🔎 Lectura diagnóstica

Ambas líneas suben en enero-febrero y bajan hacia octubre-noviembre, confirmando un patrón estacional consistente en 15 años de datos. La línea femenina se mantiene sistemáticamente por encima de la masculina en todos los meses, mostrando que la brecha de género no es estacional sino estructural.

Patrón estacional confirmado: Enero y febrero son sistemáticamente los meses con mayor desempleo en todos los años de la serie. Los meses de octubre y noviembre presentan los valores más bajos. Este patrón se mantiene consistente en ambos géneros, aunque es más pronunciado en las mujeres.

6.4 Scatter Plots — Relaciones entre Variables

df_scat <- df_gen %>%
  mutate(TGP_p = TGP*100, TO_p = TO*100, TD_p = TD*100)

p_sc <- plot_ly(df_scat, x = ~TGP_p, y = ~TO_p,
                color = ~Género,
                colors = c("FEMENINO" = pal$gold, "MASCULINO" = pal$navy),
                size = ~(TD_p),
                text = ~paste0("Año: ", Año, "<br>Mes: ", Mes_n,
                               "<br>TGP: ", round(TGP_p,1), "%",
                               "<br>TO: ",  round(TO_p,1),  "%",
                               "<br>TD: ",  round(TD_p,1),  "%"),
                hoverinfo = "text", type = "scatter", mode = "markers",
                marker = list(opacity = 0.65, sizemode = "diameter")) %>%
  plotly_layout("TGP vs TO — Relación por Género",
                "Tamaño de burbuja proporcional a la TD · 2010–2025") %>%
  layout(xaxis = list(title = "TGP (%)", ticksuffix = "%"),
         yaxis = list(title = "TO (%)", ticksuffix = "%"))

p_sc
🎯 Por qué scatter de burbujastres variables a la vez

Se necesitaban mostrar simultáneamente tres variables numéricas (TGP, TO y TD) más el género: un scatter plot ubica dos de ellas en los ejes, y el tamaño de la burbuja (tercer canal visual) codifica la TD, evitando construir un gráfico separado solo para esa relación.

🔎 Lectura diagnóstica

Se observa una relación lineal positiva clara entre TGP y TO en ambos géneros, con las burbujas femeninas más grandes en la zona de menor TO, es decir, mayor desempleo justo donde menos participan las mujeres del mercado laboral — un doble efecto negativo simultáneo.


7 Correlaciones

🔗

Matriz de Correlaciones

# Correlaciones entre indicadores
df_cor <- df_gen %>%
  select(TGP, TO, TD, TS) %>%
  mutate(across(everything(), as.numeric)) %>%
  filter(complete.cases(.))

cor_mat <- cor(df_cor * 100)

# Heatmap interactivo
cor_df <- as.data.frame(as.table(cor_mat)) %>%
  rename(Var1 = Var1, Var2 = Var2, Correlación = Freq)

p_cor <- plot_ly(
  z = cor_mat,
  x = colnames(cor_mat),
  y = colnames(cor_mat),
  type = "heatmap",
  colorscale = list(
    list(0,   pal$red),
    list(0.5, "white"),
    list(1,   pal$navy)
  ),
  zmid = 0, zmin = -1, zmax = 1,
  text = matrix(round(cor_mat, 2), nrow = 4),
  texttemplate = "%{text}",
  hovertemplate = "<b>%{x}</b> vs <b>%{y}</b><br>r = %{z:.3f}<extra></extra>"
) %>%
  plotly_layout("Matriz de Correlaciones — Indicadores Laborales",
                "Pearson · Dataset completo 2010–2025")

p_cor
🎯 Por qué un heatmapmatriz de correlación

Cuando se comparan todas las combinaciones posibles entre varias variables numéricas (4×4 en este caso), el heatmap es más eficiente que una serie de scatter plots: codifica la fuerza y dirección de cada correlación con color e intensidad, permitiendo detectar patrones de un vistazo en toda la matriz.

🔎 Lectura diagnóstica

El bloque azul intenso entre TGP y TO confirma la correlación casi perfecta esperada conceptualmente. La TD aparece en tonos rojizos frente a TO, es decir, correlación negativa: a mayor ocupación, menor desempleo, tal como predice la teoría del mercado laboral.

# Ranking de correlaciones
cor_rank <- cor_df %>%
  filter(as.character(Var1) < as.character(Var2)) %>%
  mutate(
    Fuerza = case_when(
      abs(Correlación) >= 0.9 ~ "Muy alta",
      abs(Correlación) >= 0.7 ~ "Alta",
      abs(Correlación) >= 0.5 ~ "Moderada",
      abs(Correlación) >= 0.3 ~ "Baja",
      TRUE ~ "Muy baja"
    ),
    Dirección = if_else(Correlación > 0, "Positiva ↑", "Negativa ↓")
  ) %>%
  arrange(desc(abs(Correlación)))

reactable(
  cor_rank,
  theme = reactableTheme(
    headerStyle = list(background = "#0B2545", color = "white")
  ),
  columns = list(
    Correlación = colDef(
      format = colFormat(digits = 4),
      cell = function(value) {
        color <- if (value > 0.7) pal$navy else if (value > 0) pal$teal else pal$red
        div(round(value, 4), style = list(color = color, fontWeight = "700"))
      }
    ),
    Fuerza    = colDef(cell = function(v) span(v, style = list(background = "#0B254520", padding = "2px 8px", borderRadius = "10px"))),
    Dirección = colDef(cell = function(v) span(v, style = list(color = if (grepl("↑", v)) pal$teal else pal$red, fontWeight = "700")))
  ),
  striped = TRUE, highlight = TRUE
)

Interpretación: La TGP y la TO presentan una correlación muy alta positiva (r > 0.95), lo esperado conceptualmente: a mayor participación laboral, mayor ocupación. La TD muestra correlación moderada negativa con la TO, confirmando que cuando aumenta el empleo efectivo disminuye el desempleo. La pandemia de 2020 introduce un punto de ruptura que intensifica estas relaciones.


8 Análisis por Género

Brecha de Género Histórica

# Brecha anual
brecha <- fem %>%
  inner_join(mas, by = "Año", suffix = c("_F", "_M")) %>%
  mutate(
    Brecha_TGP = (TGP_M - TGP_F) * 100,
    Brecha_TO  = (TO_M  - TO_F)  * 100,
    Brecha_TD  = (TD_F  - TD_M)  * 100  # Desocupación: femenina supera
  )

p_brecha <- plot_ly(brecha, x = ~Año) %>%
  add_bars(y = ~Brecha_TO, name = "Brecha TO (pp)",
           marker = list(color = pal$navy, opacity = 0.85)) %>%
  add_lines(y = ~Brecha_TD, name = "Brecha TD (pp)",
            line = list(color = pal$gold, width = 3, dash = "dot"),
            yaxis = "y2") %>%
  layout(
    yaxis  = list(title = "Brecha TO (pp)"),
    yaxis2 = list(title = "Brecha TD (pp)", overlaying = "y", side = "right"),
    barmode = "group"
  ) %>%
  plotly_layout("Evolución de la Brecha de Género 2010–2025",
                "Barras: diferencia en TO (M-F) · Línea: diferencia en TD (F-M)")

p_brecha
🎯 Por qué barras + línea combinadasdoble eje

Se combinan dos indicadores de brecha (TO y TD) que tienen magnitudes y significados distintos. Las barras destacan la evolución año a año de la brecha en TO (comparaciones discretas por período), mientras que la línea en un segundo eje resalta la tendencia continua de la brecha en TD, sin forzar ambas series a la misma escala.

🔎 Lectura diagnóstica

Las barras de brecha en TO se mantienen persistentemente positivas durante toda la serie, sin ningún año de paridad. La línea de brecha en TD se dispara en los años de crisis (2020), mostrando que las mujeres absorben desproporcionadamente el impacto de los choques económicos.

# Evolución TGP, TO, TD por género
gen_plot <- anual_gen %>%
  pivot_longer(cols = c(TGP, TO, TD), names_to = "Indicador", values_to = "Valor") %>%
  mutate(Valor = Valor * 100)

p_gen <- ggplot(gen_plot, aes(x = Año, y = Valor, color = interaction(Indicador, Género),
                               linetype = Género, shape = Indicador)) +
  geom_line(size = 1.3) +
  geom_point(size = 2.5) +
  scale_color_manual(values = c(
    "TGP.FEMENINO"  = pal$gold,  "TGP.MASCULINO"  = pal$navy,
    "TO.FEMENINO"   = "#D4A017", "TO.MASCULINO"   = "#13335E",
    "TD.FEMENINO"   = pal$amber, "TD.MASCULINO"   = pal$teal
  )) +
  scale_linetype_manual(values = c("FEMENINO" = "dashed", "MASCULINO" = "solid")) +
  facet_wrap(~Indicador, scales = "free_y", ncol = 3) +
  labs(title = "TGP, TO y TD por Género — Serie Anual 2010–2025",
       x = "Año", y = "(%)", color = NULL, linetype = "Género") +
  theme_labor() +
  theme(strip.background = element_rect(fill = pal$navy),
        strip.text = element_text(color = "white", face = "bold"))

ggplotly(p_gen) %>%
  plotly_layout("Indicadores por Género 2010–2025", "Promedios anuales · DANE")
🎯 Por qué líneas facetadastres indicadores en paralelo

En vez de saturar un solo panel con seis líneas (3 indicadores × 2 géneros) de escalas distintas, se usó facet_wrap para separar TGP, TO y TD en paneles independientes con eje Y libre, manteniendo el color por indicador y el tipo de línea (sólida/discontinua) por género para una comparación limpia dentro de cada panel.

🔎 Lectura diagnóstica

En los tres paneles la línea femenina (discontinua) se ubica de forma consistente por debajo (TGP, TO) o por encima (TD) de la masculina, sin que las curvas se crucen en ningún año, lo que confirma visualmente que la brecha de género es estructural y no coyuntural.

# KPIs de género último año
ult_fem <- fem %>% filter(Año == max(Año))
ult_mas <- mas %>% filter(Año == max(Año))
brecha_to_ult <- (ult_mas$TO - ult_fem$TO) * 100
👩
TO Femenina 2025
46.7%
Promedio anual
👨
TO Masculina 2025
71.4%
Promedio anual
⚠️
Brecha de Género (TO)
24.7 pp
Diferencia M - F
📉
TD Femenina 2025
11.4%
vs 7% masculina

9 Análisis de Pobreza Monetaria

💰

Evolución de la Pobreza 2012–2025

p_pob <- plot_ly(df_pob, x = ~Año) %>%
  add_bars(y = ~Personas_mil, name = "Personas en pobreza (miles)",
           marker = list(
             color = ~ifelse(Año == 2020, pal$red, pal$navy),
             opacity = 0.85
           )) %>%
  add_lines(y = ~TasaPobreza * 100, name = "Tasa de Pobreza (%)",
            line = list(color = pal$gold, width = 3),
            yaxis = "y2") %>%
  layout(
    yaxis  = list(title = "Personas en pobreza (miles)"),
    yaxis2 = list(title = "Tasa de Pobreza (%)", overlaying = "y", side = "right",
                  ticksuffix = "%")
  ) %>%
  plotly_layout("Pobreza Monetaria Nacional 2012–2025",
                "Barras: personas (miles) · Línea dorada: tasa de pobreza · Rojo: 2020 pandemia")

p_pob
🎯 Por qué barras + línea combinadasvolumen vs. tasa

Las barras muestran el volumen absoluto de personas en pobreza (miles), mientras que la línea en eje secundario muestra la tasa relativa (%). Ambas lecturas son necesarias porque el tamaño de la población cambia con el tiempo: un volumen alto no siempre implica una tasa alta, y viceversa.

🔎 Lectura diagnóstica

La barra de 2020 resaltada en rojo coincide con el pico tanto en volumen como en tasa, confirmando el impacto de la pandemia. Después de 2020 ambas series descienden de forma sostenida, señal de una recuperación consistente y no de un simple efecto de crecimiento poblacional.

pob_2020 <- df_pob %>% filter(Año == 2020)
pob_ult  <- df_pob %>% filter(Año == max(Año))
pob_2012 <- df_pob %>% filter(Año == min(Año))
🦠
Pico pandemia (2020)
43.1%
21,059k personas
📉
Mínimo histórico (2025)
28%
14,447k personas
👥
Reducción vs pico 2020
15.1 pp
En 5 años de recuperación
🏁
Reducción 2012–2025
13 pp
Tendencia estructural

10 Salario Mínimo y Mercado Laboral

💼

SMLV 2010–2025

p_smlv <- plot_ly(df_smlv %>% filter(!is.na(Salario)), x = ~Año) %>%
  add_bars(y = ~Salario / 1000, name = "SMLV (COP miles)",
           marker = list(color = pal$navy, opacity = 0.8)) %>%
  add_lines(y = ~VarSMLV * 100, name = "Variación Anual (%)",
            line = list(color = pal$gold, width = 3),
            yaxis = "y2") %>%
  layout(
    yaxis  = list(title = "SMLV (miles COP)", tickprefix = "$"),
    yaxis2 = list(title = "Variación (%)", overlaying = "y", side = "right",
                  ticksuffix = "%")
  ) %>%
  plotly_layout("Salario Mínimo y su Variación Anual 2010–2025",
                "Barras: valor SMLV (miles COP) · Línea: % incremento anual")

p_smlv
🎯 Por qué barras + línea combinadasnivel vs. variación

El SMLV en pesos y su variación porcentual anual son magnitudes de naturaleza distinta (nivel acumulado vs. tasa de cambio). Las barras comunican el nivel del salario en cada año, mientras la línea en eje secundario aísla el ritmo de crecimiento, que es lo que suele generar debate sobre su efecto en el empleo.

🔎 Lectura diagnóstica

Las barras crecen de forma sostenida y casi monotónica, mientras la línea de variación anual oscila sin una tendencia clara al alza o a la baja. Esto permite contrastar visualmente los años de mayor incremento porcentual contra la evolución de TGP/TO/TD para evaluar si hubo o no efectos adversos observables.

tbl_smlv <- df_smlv %>%
  filter(!is.na(Salario)) %>%
  select(Año, Salario, VarSMLV, TGP, TO, TD) %>%
  mutate(
    Salario  = paste0("$", format(round(Salario), big.mark = ",")),
    VarSMLV  = paste0(round(VarSMLV * 100, 1), "%"),
    TGP      = paste0(round(TGP * 100, 1), "%"),
    TO       = paste0(round(TO  * 100, 1), "%"),
    TD       = paste0(round(TD  * 100, 1), "%")
  ) %>%
  rename(
    `SMLV` = Salario, `Var. SMLV` = VarSMLV,
    `TGP` = TGP, `TO` = TO, `TD` = TD
  )

reactable(
  tbl_smlv,
  theme = reactableTheme(
    headerStyle = list(background = "#0B2545", color = "white", fontSize = "0.78rem")
  ),
  striped = TRUE, highlight = TRUE, defaultPageSize = 16,
  fullWidth = TRUE
)

11 Análisis de 5 Ciudades Vulnerables

🏙️

Empleo por Tipo — Ciudades con Mayor Informalidad

ing_2025 <- df_ing %>% filter(Año == 2025) %>% filter(!is.na(Ciudad))

p_ing <- plot_ly(ing_2025, x = ~Ciudad) %>%
  add_bars(y = ~PctAsal * 100, name = "% Asalariados",
           marker = list(color = pal$navy, opacity = 0.9)) %>%
  add_bars(y = ~PctIndep * 100, name = "% Independientes",
           marker = list(color = pal$gold, opacity = 0.9)) %>%
  layout(barmode = "group",
         yaxis = list(title = "%", ticksuffix = "%")) %>%
  plotly_layout("Distribución Asalariados vs Independientes — 2025",
                "5 ciudades con mayor informalidad laboral · Fuente: DANE")

p_ing
🎯 Por qué barras agrupadascomparación entre categorías

Con una variable categórica (ciudad) en el eje X y dos porcentajes que se quieren comparar lado a lado (% asalariados vs. % independientes) para un mismo año, las barras agrupadas son la opción más clara: permiten comparar tanto entre ciudades como entre los dos tipos de empleo dentro de cada ciudad.

🔎 Lectura diagnóstica

Las ciudades muestran proporciones distintas entre asalariados e independientes, evidenciando que la informalidad no es homogénea: algunas ciudades dependen mucho más del empleo por cuenta propia, lo que sugiere estructuras económicas locales diferentes.

p_tend_ing <- ggplot(df_ing %>% filter(!is.na(PctAsal)),
                     aes(x = Año, y = PctAsal * 100, color = Ciudad)) +
  geom_line(size = 1.3) +
  geom_point(size = 2.5) +
  scale_color_viridis_d(option = "D", end = 0.9) +
  labs(title = "Evolución del % Asalariados por Ciudad (2021–2025)",
       subtitle = "Ciudades con mercados laborales más vulnerables",
       x = "Año", y = "% Asalariados", color = "Ciudad") +
  scale_y_continuous(labels = scales::percent_format(scale = 1)) +
  theme_labor()

ggplotly(p_tend_ing) %>%
  plotly_layout("Evolución del % Asalariados por Ciudad", "2021–2025")
🎯 Por qué líneas multicategoríaevolución por grupo

Con cinco ciudades observadas a lo largo de varios años, la línea permite seguir la trayectoria individual de cada una en el tiempo. Se usó una paleta viridis (colores distinguibles y accesibles) en vez de la paleta corporativa dorado/navy, porque aquí hay más de dos categorías y se necesita diferenciarlas claramente.

🔎 Lectura diagnóstica

Las líneas no son paralelas: algunas ciudades ganan participación asalariada mientras otras la pierden, lo que indica que la tendencia hacia la formalización o informalización del empleo depende del contexto local y no responde a una dinámica nacional uniforme.


12 Dashboard Interactivo

📟

Exploración Interactiva del Dataset Completo

# Tabla interactiva completa
df_tabla <- df_gen %>%
  select(Año, Mes_n, Género, TGP, TO, TD, TS) %>%
  mutate(
    Mes  = factor(Mes_n, labels = substr(meses_ord, 1, 3)),
    TGP  = round(TGP * 100, 2),
    TO   = round(TO  * 100, 2),
    TD   = round(TD  * 100, 2),
    TS   = round(TS  * 100, 2)
  ) %>%
  select(-Mes_n) %>%
  filter(!is.na(TGP)) %>%
  arrange(Año, desc(Género))

reactable(
  df_tabla,
  theme = reactableTheme(
    headerStyle = list(background = "#0B2545", color = "white", fontSize = "0.78rem"),
    cellStyle = list(fontSize = "0.83rem")
  ),
  columns = list(
    Género = colDef(cell = function(v) {
      bg <- if (v == "FEMENINO") pal$gold else pal$navy
      span(v, style = list(
        background = bg, color = "white",
        padding = "2px 9px", borderRadius = "12px",
        fontSize = "0.72rem", fontWeight = "700"
      ))
    }),
    TGP = colDef(format = colFormat(suffix = "%", digits = 2)),
    TO  = colDef(format = colFormat(suffix = "%", digits = 2)),
    TD  = colDef(
      format = colFormat(suffix = "%", digits = 2),
      style = function(value) {
        if (!is.na(value) && value > 15) list(color = "#C0392B", fontWeight = "700")
        else if (!is.na(value) && value > 12) list(color = "#C2790F", fontWeight = "600")
        else list()
      }
    ),
    TS = colDef(format = colFormat(suffix = "%", digits = 2))
  ),
  filterable = TRUE,
  searchable = TRUE,
  highlight  = TRUE,
  striped    = TRUE,
  defaultPageSize = 15,
  defaultSorted = list(Año = "desc")
)

13 Relaciones entre Variables

🔄

Relaciones Multivariables

# TO vs TD por género y año — bubbles
df_rel <- anual_gen %>%
  mutate(TGP_p = TGP*100, TO_p = TO*100, TD_p = TD*100, TS_p = TS*100)

p_rel <- plot_ly(df_rel, x = ~TO_p, y = ~TD_p,
                 color = ~Género,
                 colors = c("FEMENINO" = pal$gold, "MASCULINO" = pal$navy),
                 size  = ~TGP_p,
                 frame = ~Año,
                 text  = ~paste0(Género, "<br>Año: ", Año,
                                 "<br>TO: ", round(TO_p,1), "%",
                                 "<br>TD: ", round(TD_p,1), "%",
                                 "<br>TGP: ", round(TGP_p,1), "%"),
                 hoverinfo = "text",
                 type = "scatter", mode = "markers",
                 marker = list(opacity = 0.75, sizemode = "diameter")) %>%
  animation_opts(frame = 600, easing = "linear", redraw = FALSE) %>%
  animation_slider(currentvalue = list(prefix = "Año: ")) %>%
  plotly_layout("Animación: TO vs TD por Género (2010–2025)",
                "Tamaño proporcional a TGP · Usa el control para ver la evolución año por año") %>%
  layout(xaxis = list(title = "Tasa de Ocupación (%)", ticksuffix = "%"),
         yaxis = list(title = "Tasa de Desocupación (%)", ticksuffix = "%"))

p_rel
🎯 Por qué burbujas animadas4 dimensiones + tiempo

Aquí se cruzan cinco dimensiones a la vez: TO, TD, TGP (tamaño), género (color) y año (tiempo). Un gráfico estático no podría mostrar la evolución sin saturarse de puntos superpuestos; el control de animación por año (frame) convierte el tiempo en una dimensión navegable, dejando el scatter limpio en cada instante.

🔎 Lectura diagnóstica

Al recorrer los años se observa cómo las burbujas femeninas y masculinas viajan juntas en la fase de crecimiento (2010–2019), se dispersan bruscamente en 2020 (TD sube, TO cae) y luego convergen de nuevo en la recuperación, sin llegar a cerrar completamente la brecha entre grupos.

# Relación SMLV vs TD
df_rel2 <- df_smlv %>% filter(!is.na(Salario), !is.na(TD))

p_r2 <- ggplot(df_rel2, aes(x = Salario / 1000, y = TD * 100, label = Año)) +
  geom_point(size = 4, color = pal$navy, alpha = 0.8) +
  geom_smooth(method = "loess", se = TRUE, color = pal$gold, fill = paste0(pal$gold, "30"), size = 1.3) +
  geom_text(nudge_y = 0.3, size = 3, color = pal$muted, fontface = "bold") +
  scale_x_continuous(labels = scales::dollar_format(prefix = "$", suffix = "k")) +
  labs(title = "Relación entre SMLV y Tasa de Desocupación (2010–2025)",
       subtitle = "Línea suavizada LOESS · Cada punto es un año",
       x = "SMLV (miles COP)", y = "TD (%)") +
  theme_labor()

ggplotly(p_r2) %>%
  plotly_layout("SMLV vs Tasa de Desocupación", "Relación histórica 2010–2025")
🎯 Por qué scatter + suavizado LOESSrelación no lineal

Para explorar si existe relación entre el nivel del salario mínimo y la tasa de desocupación, cada año se representa como un punto (scatter), y se ajusta una curva LOESS en vez de una recta de regresión porque no se asume de antemano que la relación sea lineal; la banda de confianza sombreada comunica la incertidumbre de esa curva.

🔎 Lectura diagnóstica

La curva LOESS no muestra una pendiente ascendente sostenida entre SMLV y TD, lo que sugiere que, en la serie histórica analizada, los incrementos del salario mínimo no se han traducido en un aumento sistemático del desempleo, apoyando el hallazgo automático reportado más adelante.


14 Insights Automáticos

💡

Conclusiones Derivadas de los Datos

# Calcular todos los insights automáticamente
brecha_media      <- mean(brecha$Brecha_TO, na.rm = TRUE)
año_min_to_f      <- fem$Año[which.min(fem$TO)]
min_to_f          <- min(fem$TO, na.rm = TRUE) * 100
año_max_brecha    <- brecha$Año[which.max(brecha$Brecha_TO)]
max_brecha        <- max(brecha$Brecha_TO, na.rm = TRUE)
caida_covid_f     <- (fem$TO[fem$Año==2019] - fem$TO[fem$Año==2020]) * 100
caida_covid_m     <- (mas$TO[mas$Año==2019] - mas$TO[mas$Año==2020]) * 100
aumento_smlv_max  <- df_smlv$Año[which.max(df_smlv$VarSMLV)]
var_max_smlv      <- max(df_smlv$VarSMLV, na.rm = TRUE) * 100
td_f_mean         <- mean(td_f * 100, na.rm = TRUE)
td_m_mean         <- mean(td_m * 100, na.rm = TRUE)
pob_reduccion     <- (pob_2020$TasaPobreza - pob_ult$TasaPobreza) * 100

# Asimetría de TD femenina
skew_td_f <- mean((td_f*100 - mean(td_f*100, na.rm=TRUE))^3, na.rm=TRUE) /
             sd(td_f*100, na.rm=TRUE)^3
tipo_asim <- if_else(skew_td_f > 0.5, "asimétrica positiva (cola hacia valores altos)", 
                     if_else(skew_td_f < -0.5, "asimétrica negativa (cola hacia valores bajos)", 
                             "aproximadamente simétrica"))
📊

Brecha estructural de género

La tasa de ocupación masculina superó a la femenina en promedio 25.4 puntos porcentuales durante 2010–2025. La mayor brecha se registró en 2021 con 27.2 pp.

🦠

COVID-19: impacto diferencial

En 2020 la TO femenina cayó 7.6 pp mientras la masculina cayó 6.9 pp. Las mujeres absorbieron un golpe 11% mayor que los hombres.

📉

Mínimo histórico de empleo femenino

La tasa de ocupación femenina más baja de toda la serie fue 38.1%, registrada en 2020 como consecuencia directa de la pandemia.

💰

Mayor alza real del SMLV

El mayor incremento del salario mínimo fue en 2023 con un 16% de aumento. Dicho año no generó efectos adversos observables sobre la tasa de ocupación.

📈

Desocupación femenina persistente

La TD media femenina fue 14% vs 8.5% masculina a lo largo de todo el período. La distribución de la TD femenina es asimétrica positiva (cola hacia valores altos).

🏁

Recuperación de la pobreza

Desde el pico pandémico de 2020 (43.1%), la tasa de pobreza se redujo 15.1 pp hasta alcanzar 28% en 2025, el mínimo histórico de la serie.


15 Tablas Interactivas Completas

📋

Datos Completos Exportables

15.1 Indicadores Anuales Nacionales

tbl_anual <- df_smlv %>%
  filter(!is.na(TGP)) %>%
  mutate(
    `TGP (%)` = round(TGP * 100, 2),
    `TO (%)`  = round(TO  * 100, 2),
    `TD (%)`  = round(TD  * 100, 2),
    `TS (%)`  = round(TS  * 100, 2),
    `SMLV (COP)` = Salario,
    `Var. SMLV`  = paste0(round(VarSMLV * 100, 1), "%")
  ) %>%
  select(Año, `TGP (%)`, `TO (%)`, `TD (%)`, `TS (%)`, `SMLV (COP)`, `Var. SMLV`)

DT::datatable(
  tbl_anual,
  extensions = c("Buttons", "Scroller"),
  options = list(
    dom = "Bfrtip",
    buttons = list("excel", "csv", "pdf"),
    scrollY = "350px",
    scroller = TRUE,
    pageLength = 16
  ),
  rownames = FALSE,
  class = "cell-border stripe hover"
) %>%
  DT::formatCurrency("SMLV (COP)", currency = "$", digits = 0, mark = ",") %>%
  DT::formatStyle(
    "TD (%)",
    backgroundColor = DT::styleInterval(c(10, 13),
      values = c("rgba(15,107,92,0.1)", "rgba(194,121,15,0.15)", "rgba(192,57,43,0.15)"))
  )

15.2 Ingresos por Ciudad

tbl_ing <- df_ing %>%
  mutate(
    `% Asalariados` = scales::percent(PctAsal, accuracy = 0.1),
    `% Independientes` = scales::percent(PctIndep, accuracy = 0.1)
  ) %>%
  select(
    Ciudad,
    Año,
    PobOcup,
    Asal,
    Indep,
    `% Asalariados`,
    `% Independientes`
  )

DT::datatable(
  tbl_ing,
  colnames = c(
    "Ciudad",
    "Año",
    "Pob. Ocupada (k)",
    "Asalariados (k)",
    "Independientes (k)",
    "% Asalariados",
    "% Independientes"
  ),
  extensions = "Buttons",
  options = list(
    dom = "Bfrtip",
    buttons = c("excel", "csv"),
    pageLength = 10
  ),
  rownames = FALSE,
  class = "cell-border stripe hover"
)