Una empresa retail con 1,200 empleados en Panamá y Costa Rica presentaba una tasa de rotación del 24% anual, el doble del promedio de la industria. RR.HH. no tenía claridad sobre qué departamentos o factores causaban las salidas. Se construyó un análisis completo con SQL y Python para identificar patrones, calcular un score de riesgo por empleado y proponer acciones concretas de retención.
-- Dataset completo: activos + salientes -- JOIN de 4 tablas con variable objetivo SELECT e.id_empleado, e.edad, e.genero, e.departamento, e.cargo, DATEDIFF( COALESCE(e.fecha_salida, CURDATE()), e.fecha_ingreso ) / 365.0 AS anos_antiguedad, s.salario_mensual, s.ultimo_aumento_pct, d.nivel_satisfaccion, d.calificacion_desempeno, d.proyectos_completados, a.horas_extra_mes_prom, CASE WHEN e.fecha_salida IS NOT NULL THEN 1 ELSE 0 END AS salio FROM empleados e LEFT JOIN salarios s ON e.id_empleado = s.id_empleado LEFT JOIN desempeno d ON e.id_empleado = d.id_empleado LEFT JOIN asistencia a ON e.id_empleado = a.id_empleado WHERE e.fecha_ingreso >= '2021-01-01';
-- Tasa de rotación por departamento -- CTE + RANK() para ordenar por riesgo WITH totales AS ( SELECT departamento, COUNT(*) AS total, SUM(salio) AS salidas FROM empleados_full GROUP BY departamento ), rotacion AS ( SELECT departamento, total, salidas, ROUND( salidas * 100.0 / total, 2 ) AS tasa_pct FROM totales ) SELECT *, RANK() OVER( ORDER BY tasa_pct DESC ) AS ranking_riesgo FROM rotacion ORDER BY ranking_riesgo;
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # ── 1. CARGA ─────────────────────────────────────────────────────── df = pd.read_csv('empleados_full.csv') # Imputar nulos con mediana por departamento df['salario_mensual'] = df.groupby('departamento')['salario_mensual'].\ transform(lambda x: x.fillna(x.median())) df['horas_extra_mes_prom'] = df['horas_extra_mes_prom'].fillna(0) # Variable objetivo df['salio'] = df['fecha_salida'].notna().astype(int) # ── 2. CORRELACIONES ─────────────────────────────────────────────── vars_num = [ 'anos_antiguedad', 'salario_mensual', 'nivel_satisfaccion', 'horas_extra_mes_prom', 'calificacion_desempeno', 'proyectos_completados' ] corr = df[vars_num + ['salio']].corr()['salio'].\ drop('salio').sort_values() print("Top factores de riesgo:\n", corr) # ── 3. SCORE DE RIESGO ───────────────────────────────────────────── p25 = df['salario_mensual'].quantile(0.25) df['score_riesgo'] = ( (df['salario_mensual'] < p25) .astype(int) * 3 + (df['horas_extra_mes_prom'] > 60) .astype(int) * 2 + (df['anos_antiguedad'] < 1) .astype(int) * 2 + (df['nivel_satisfaccion'] < 3) .astype(int) * 2 + (df['calificacion_desempeno'] < 3) .astype(int) * 1 ) df['nivel_riesgo'] = pd.cut( df['score_riesgo'], bins=[-1, 2, 5, 10], labels=['Bajo', 'Medio', 'Alto'] ) # ── 4. EXPORTAR ──────────────────────────────────────────────────── df_riesgo = df[df['salio'] == 0].sort_values( 'score_riesgo', ascending=False ) df_riesgo.to_csv('empleados_riesgo.csv', index=False) alto = (df['nivel_riesgo'] == 'Alto').sum() print(f"Empleados riesgo alto : {alto}") print(f"Costo potencial : ${alto * 3500:,}")
| ID | Departamento | Cargo | Antigüedad | Salario | Horas Extra | Satisfacción | Score | Riesgo |
|---|---|---|---|---|---|---|---|---|
| EMP-0412 | Ventas | Asesor Comercial | 0.8 años | $620 | 72 hrs | 2 / 5 | 9 | Alto |
| EMP-0287 | Ventas | Supervisor Zona | 1.1 años | $710 | 58 hrs | 2 / 5 | 7 | Alto |
| EMP-0534 | Logística | Coordinador | 1.4 años | $850 | 68 hrs | 3 / 5 | 6 | Alto |
| EMP-0619 | At. al Cliente | Agente Senior | 2.1 años | $780 | 55 hrs | 3 / 5 | 4 | Medio |
| EMP-0103 | TI | Analista de Datos | 3.5 años | $1,200 | 38 hrs | 4 / 5 | 1 | Bajo |