Código Python
Código Python
import pandas as pd
import math
#
==========================================
===============
# CONFIGURACIÓN
#
==========================================
===============
CARPETA_SALIDA = RUTA_ARCHIVO.parent
TABLAS_REQUERIDAS = {
"xpu_ProyectoMesTag": [
"CodigoIniciativa",
"Empresa",
"NombreCorto",
"Tipologia",
"Ciclo",
"Comentario",
"Ppto2026",
"Mes",
"MesIdx",
"Tag",
"FuenteTabla",
"ProyectoMesKey",
"ProyectoMesTagKey",
],
"xpu_RolReglas": [
"Rol",
"TagsVisibles",
"TipologiasPermitidas",
"ExcluyeTipologia",
"Activo",
"OrdenVisual",
"Comentario",
],
"xpu_CapacidadRolEmpresa": [
"Empresa",
"Rol",
"CapacidadParalelo",
"Activo",
"Comentario",
],
"xpu_CatalogoMeses": [
"Mes",
"MesIdx",
"Activo",
"Comentario",
],
"xpu_ParametrosModelo": [
"Parametro",
"Valor",
"TipoDato",
"Activo",
"Comentario",
],
"xpu_DotacionActual": [
"Empresa",
"Rol",
"DotacionActual",
"Activo",
"Comentario",
],
"xpu_RuteoBalanceRol": [
"Empresa",
"RolBase",
"RolApoyo",
"Tag",
"Tipologia",
"TipoRuteo",
"PrioridadRuteo",
"LimitarBasePorDotacion",
"HeatmapApoyoSinTopeDotacion",
"PermiteDesborde",
"Activo",
"Comentario",
],
}
PRIORIDAD_TIPOLOGIA = {
"Distribución": 1,
"Transmisión": 2,
"Generación": 3,
"Edificaciones": 4,
"TIC": 5,
"Innovación": 6,
#
==========================================
===============
# UTILIDADES
#
==========================================
===============
def limpiar_texto(valor):
if [Link](valor):
return None
return str(valor).strip()
df = [Link]()
if df[col].dtype == "object":
df[col] = df[col].apply(limpiar_texto)
return df
return []
wb = load_workbook(ruta_archivo, data_only=True)
tabla_encontrada = None
ws_tabla = None
for ws in [Link]:
if nombre_tabla in [Link]:
tabla_encontrada = [Link][nombre_tabla]
ws_tabla = ws
break
ref = tabla_encontrada.ref
filas = ws_tabla[ref]
df = [Link](registros, columns=encabezados)
df = normalizar_dataframe(df)
return df
#
==========================================
===============
# VALIDACIONES
#
==========================================
===============
errores = []
columnas_reales = list([Link])
if faltantes:
return errores
def validar_vacios_criticos(tablas: Dict[str, [Link]]) ->
List[str]:
errores = []
df = tablas["xpu_ProyectoMesTag"]
nulos = df[col].isna().sum()
if nulos > 0:
df = tablas["xpu_CapacidadRolEmpresa"]
nulos = df[col].isna().sum()
if nulos > 0:
[Link](f"[xpu_CapacidadRolEmpresa] La columna
'{col}' tiene {nulos} nulos.")
df = tablas["xpu_RuteoBalanceRol"]
nulos = df[col].isna().sum()
if nulos > 0:
return errores
errores = []
df = tablas["xpu_CapacidadRolEmpresa"]
if dup > 0:
df = tablas["xpu_DotacionActual"]
if dup > 0:
df = tablas["xpu_RuteoBalanceRol"]
if dup > 0:
return errores
parametros = {}
nombre = row["Parametro"]
valor = row["Valor"]
tipo = str(row["TipoDato"]).strip().lower() if not
[Link](row["TipoDato"]) else "text"
if tipo == "int":
try:
valor = int(valor)
except Exception:
pass
try:
valor = float(valor)
except Exception:
pass
else:
valor = limpiar_texto(valor)
parametros[nombre] = valor
return parametros
print("-" * 70)
print("-" * 70)
#
==========================================
===============
#
==========================================
===============
filas = []
rol = row["Rol"]
tags = partir_lista_pipe(row["TagsVisibles"])
tips = partir_lista_pipe(row["TipologiasPermitidas"])
excluye = limpiar_texto([Link]("ExcluyeTipologia"))
orden_visual = [Link]("OrdenVisual")
comentario = limpiar_texto([Link]("Comentario"))
continue
[Link](
"Rol": rol,
"TagVisible": limpiar_texto(tag),
"TipologiaPermitida": limpiar_texto(tip),
"ExcluyeTipologia": excluye,
"OrdenVisual": orden_visual,
"ComentarioRegla": comentario,
"ReglaAplicada": f"{rol}|{tag}|{tip}",
df = [Link](filas)
if not [Link]:
df = normalizar_dataframe(df)
df = df.sort_values(
na_position="last"
).reset_index(drop=True)
return df
filas = []
empresa = row["Empresa"]
rol_base = row["RolBase"]
rol_apoyo = row["RolApoyo"]
tags = partir_lista_pipe(row["Tag"])
tips = partir_lista_pipe(row["Tipologia"])
tipo_ruteo = row["TipoRuteo"]
prioridad = row["PrioridadRuteo"]
limitar = row["LimitarBasePorDotacion"]
heatmap = row["HeatmapApoyoSinTopeDotacion"]
desborde = row["PermiteDesborde"]
comentario = row["Comentario"]
continue
[Link](
"Empresa": limpiar_texto(empresa),
"RolBase": limpiar_texto(rol_base),
"RolApoyo": limpiar_texto(rol_apoyo),
"Tag": limpiar_texto(tag),
"Tipologia": limpiar_texto(tip),
"TipoRuteo": limpiar_texto(tipo_ruteo),
"PrioridadRuteo": prioridad,
"LimitarBasePorDotacion": limitar,
"HeatmapApoyoSinTopeDotacion": heatmap,
"PermiteDesborde": desborde,
"Comentario": limpiar_texto(comentario),
"ClaveRuteoExpandida": f"{empresa}|{rol_base}|
{tag}|{tip}",
df = [Link](filas)
if not [Link]:
df = normalizar_dataframe(df)
df = df.sort_values(
na_position="last"
).reset_index(drop=True)
return df
def excluir_roles_apoyo_directos(
df_candidatos: [Link],
df_ruteo_expandido: [Link],
) -> [Link]:
"""
"""
apoyo = df_ruteo_expandido.copy()
apoyo = apoyo[
apoyo["TipoRuteo"].astype(str).[Link]().eq("Desborde")
apoyo["EsApoyoCondicionado"] = 1
salida = df_candidatos.merge(
apoyo,
how="left"
salida = salida[salida["EsApoyoCondicionado"].isna()].copy()
salida = [Link](columns=["EsApoyoCondicionado"])
return salida
def restringir_candidatos_a_roles_base_explicitos(
df_candidatos: [Link],
df_ruteo_expandido: [Link],
) -> [Link]:
"""
"""
candidatos = df_candidatos.copy()
combos_existentes = (
.drop_duplicates()
.copy()
)
combos_existentes["__flag_ruteo_explicito__"] = 1
combos_roles_base = (
.drop_duplicates()
.copy()
.rename(columns={"RolBase": "__rol_base_permitido__"})
salida = [Link](
combos_existentes,
how="left"
salida = [Link](
combos_roles_base,
how="left"
# 5) Regla:
salida = salida[
(salida["__flag_ruteo_explicito__"].isna())
| (salida["__rol_base_permitido__"].notna())
].copy()
salida = [Link](columns=["__flag_ruteo_explicito__",
"__rol_base_permitido__"])
return salida
#
==========================================
===============
#
==========================================
===============
def generar_candidatos(
df_proyecto_mes_tag: [Link],
df_reglas_expandidas: [Link],
df_ruteo_expandido: [Link],
) -> [Link]:
incluir_tipologias_no_configuradas =
int([Link]("incluir_tipologias_no_configuradas", 0)) == 1
base = df_proyecto_mes_tag.copy()
if not usar_tags_visibles:
reglas = df_reglas_expandidas.copy()
df = [Link](
reglas,
left_on=["Tag", "Tipologia"],
right_on=["TagVisible", "TipologiaPermitida"],
how="inner",
if "ExcluyeTipologia" in [Link]:
mask_excluir = (
df["ExcluyeTipologia"].notna()
& (df["Tipologia"].astype(str).[Link]() ==
df["ExcluyeTipologia"].astype(str).[Link]())
df = [Link][~mask_excluir].copy()
if incluir_tipologias_no_configuradas:
tips_base =
set(base["Tipologia"].dropna().astype(str).[Link]().unique())
tips_reglas =
set(reglas["TipologiaPermitida"].dropna().astype(str).[Link]().unique(
))
print("\nALERTA DE CONTROL")
print("-" * 70)
for t in tips_no_config:
print(f"- {t}")
print("-" * 70)
df["ProyectoCuenta"] = 1
df["ProyectoMesRolKey"] = (
df["CodigoIniciativa"].astype(str)
+ "|"
+ df["Empresa"].astype(str)
+ "|"
+ df["Mes"].astype(str)
+ "|"
+ df["Rol"].astype(str)
columnas_salida = [
"CodigoIniciativa",
"Empresa",
"NombreCorto",
"Tipologia",
"Ciclo",
"Comentario",
"Ppto2026",
"Mes",
"MesIdx",
"Tag",
"Rol",
"ProyectoCuenta",
"ReglaAplicada",
"OrdenVisual",
"ComentarioRegla",
"FuenteTabla",
"ProyectoMesKey",
"ProyectoMesTagKey",
"ProyectoMesRolKey",
df = df[columnas_salida].copy()
# Corrección 1:
df = excluir_roles_apoyo_directos(df, df_ruteo_expandido)
# Corrección 2:
df = restringir_candidatos_a_roles_base_explicitos(df,
df_ruteo_expandido)
df = df.sort_values(
na_position="last"
).reset_index(drop=True)
return df
#
==========================================
===============
#
==========================================
===============
def agregar_capacidad_y_dotacion(
df_candidatos: [Link],
df_capacidad: [Link],
df_dotacion: [Link],
) -> [Link]:
df = df_candidatos.copy()
if usar_999:
df["EsSinRestriccion"] = df["CapacidadParalelo"].apply(
df["EsTagCargaTrabajo"] = df["Tag"].apply(lambda x: 1 if x in
{"EP", "ETO", "OB"} else 0)
df["CapacidadFuente"] = "xpu_CapacidadRolEmpresa"
df["DotacionFuente"] = "xpu_DotacionActual"
return df
def calcular_cupos_apoyo_proporcional(
gg: [Link],
"""
"""
if limite_base is None:
return {}
total_grupo = len(gg)
return {}
conteo_tip = (
[Link]("Tipologia", dropna=False)
.size()
.reset_index(name="ProyectosGrupo")
conteo_tip["ProporcionGrupo"] = conteo_tip["ProyectosGrupo"] /
total_grupo
conteo_tip["ExcesoIdeal"] = conteo_tip["ProporcionGrupo"] *
exceso_total
conteo_tip["ExcesoEntero"] =
conteo_tip["ExcesoIdeal"].apply([Link]).astype(int)
conteo_tip["ParteDecimal"] = conteo_tip["ExcesoIdeal"] -
conteo_tip["ExcesoEntero"]
conteo_tip["PrioridadTipologia"] =
conteo_tip["Tipologia"].map(PRIORIDAD_TIPOLOGIA).fillna(999).astype
(int)
asignado_entero = int(conteo_tip["ExcesoEntero"].sum())
if saldo > 0:
conteo_tip = conteo_tip.sort_values(
na_position="last"
).reset_index(drop=True)
conteo_tip["Extra"] = 0
for i in range(saldo):
conteo_tip.loc[i, "Extra"] = 1
conteo_tip["ExcesoAsignadoApoyo"] =
conteo_tip["ExcesoEntero"] + conteo_tip["Extra"]
else:
conteo_tip["ExcesoAsignadoApoyo"] =
conteo_tip["ExcesoEntero"]
conteo_tip["ExcesoAsignadoApoyo"] = conteo_tip.apply(
lambda r: min(int(r["ExcesoAsignadoApoyo"]),
int(r["ProyectosGrupo"])),
axis=1
cupos = dict(
zip(
conteo_tip["Tipologia"].astype(str),
conteo_tip["ExcesoAsignadoApoyo"].astype(int),
return cupos
def asignar_grupo_desborde_proporcional(
gg: [Link],
rol_base: str,
rol_apoyo: str,
limite_base: int | None,
permite_desborde: int,
) -> [Link]:
"""
"""
g = [Link]()
g["PrioridadTipologia"] =
g["Tipologia"].map(PRIORIDAD_TIPOLOGIA).fillna(999).astype(int)
g["RondaBarrido"] = None
g["RolOriginal"] = g["Rol"]
g["FlagRuteado"] = 1
g["LimiteBaseGrupo"] = limite_base
if limite_base is None:
g["RolAsignadoFinal"] = rol_base
g["OrigenAsignacion"] = "RuteoBaseSinLimite"
return g
if permite_desborde != 1:
g["RolAsignadoFinal"] = rol_base
g["OrigenAsignacion"] = "RuteoBaseExcesoSinDesborde"
return g
secuencia_global = 1
sub = sub.sort_values(
by=["CodigoIniciativa", "ProyectoMesTagKey"],
ascending=[True, True],
na_position="last"
).reset_index(drop=True)
total_tip = len(sub)
sub["SecuenciaGrupo"] = range(secuencia_global,
secuencia_global + total_tip)
secuencia_global += total_tip
sub["RolAsignadoFinal"] = [Link].to_series().apply(
sub["OrigenAsignacion"] = [Link].to_series().apply(
)
sub["CupoApoyoTipologia"] = apoyo_tip
sub["CupoBaseTipologia"] = base_tip
[Link](sub)
if not piezas:
return [Link][0:0].copy()
salida = salida.sort_values(
by=["PrioridadTipologia", "CodigoIniciativa",
"ProyectoMesTagKey"],
na_position="last"
).reset_index(drop=True)
return salida
def aplicar_ruteo_base_apoyo(
df_con_capacidad: [Link],
df_ruteo_expandido: [Link],
) -> [Link]:
base = df_con_capacidad.copy()
ruteo = df_ruteo_expandido.copy()
df = [Link](
ruteo,
how="left",
suffixes=("", "_rut"),
sin_regla = df[df["TipoRuteo"].isna()].copy()
sin_regla["RolOriginal"] = sin_regla["Rol"]
sin_regla["RolAsignadoFinal"] = sin_regla["Rol"]
sin_regla["OrigenAsignacion"] = "VisibleSinRuteo"
sin_regla["FlagRuteado"] = 0
sin_regla["SecuenciaGrupo"] = None
sin_regla["PrioridadTipologia"] =
sin_regla["Tipologia"].map(PRIORIDAD_TIPOLOGIA).fillna(999).astype(i
nt)
sin_regla["RondaBarrido"] = None
sin_regla["LimiteBaseGrupo"] = None
sin_regla["CupoApoyoTipologia"] = None
sin_regla["CupoBaseTipologia"] = None
con_regla = df[df["TipoRuteo"].notna()].copy()
resultados = []
if not con_regla.empty:
con_regla["PrioridadRuteo"] =
pd.to_numeric(con_regla["PrioridadRuteo"], errors="coerce")
con_regla["LimitarBasePorDotacion"] =
pd.to_numeric(con_regla["LimitarBasePorDotacion"],
errors="coerce").fillna(0).astype(int)
con_regla["PermiteDesborde"] =
pd.to_numeric(con_regla["PermiteDesborde"],
errors="coerce").fillna(0).astype(int)
con_regla["DotacionActual"] =
pd.to_numeric(con_regla["DotacionActual"], errors="coerce")
con_regla["CapacidadParalelo"] =
pd.to_numeric(con_regla["CapacidadParalelo"], errors="coerce")
con_regla["PrioridadTipologia"] =
con_regla["Tipologia"].map(PRIORIDAD_TIPOLOGIA).fillna(999).astype(
int)
cols_group = [
"Empresa",
"Mes",
"MesIdx",
"Tag",
"RolBase",
"RolApoyo",
"TipoRuteo",
"PrioridadRuteo",
gg = [Link]()
tipo_ruteo = limpiar_texto(gg["TipoRuteo"].iloc[0])
rol_base = limpiar_texto(gg["RolBase"].iloc[0])
rol_apoyo = limpiar_texto(gg["RolApoyo"].iloc[0])
permite_desborde = int(gg["PermiteDesborde"].iloc[0]) if
[Link](gg["PermiteDesborde"].iloc[0]) else 0
limitar_base_por_dotacion =
int(gg["LimitarBasePorDotacion"].iloc[0]) if
[Link](gg["LimitarBasePorDotacion"].iloc[0]) else 0
capacidad_base = gg["CapacidadParalelo"].dropna()
dotacion_base = gg["DotacionActual"].dropna()
limite_base = None
if tipo_ruteo == "SinApoyo":
limite_base = None
else:
limite_base = int(capacidad_base)
else:
limite_base = None
if tipo_ruteo == "SinApoyo":
gg["RolOriginal"] = gg["Rol"]
gg["FlagRuteado"] = 1
gg["RolAsignadoFinal"] = rol_base
gg["OrigenAsignacion"] = "RuteoSinApoyo"
gg["LimiteBaseGrupo"] = limite_base
gg["CupoApoyoTipologia"] = None
gg["CupoBaseTipologia"] = None
[Link](gg)
gg_res = asignar_grupo_desborde_proporcional(
gg=gg,
rol_base=rol_base,
rol_apoyo=rol_apoyo,
limite_base=limite_base,
permite_desborde=permite_desborde,
[Link](gg_res)
else:
gg["RolOriginal"] = gg["Rol"]
gg["FlagRuteado"] = 1
gg["RolAsignadoFinal"] = rol_base
gg["OrigenAsignacion"] = "RuteoBaseDefault"
gg["RondaBarrido"] = None
gg["LimiteBaseGrupo"] = limite_base
gg["CupoApoyoTipologia"] = None
gg["CupoBaseTipologia"] = None
[Link](gg)
con_regla_res = [Link](resultados, ignore_index=True) if
resultados else [Link](columns=[Link]())
detalle["Rol"] = detalle["RolAsignadoFinal"]
detalle["ProyectoMesRolKey"] = (
detalle["CodigoIniciativa"].astype(str)
+ "|"
+ detalle["Empresa"].astype(str)
+ "|"
+ detalle["Mes"].astype(str)
+ "|"
+ detalle["Rol"].astype(str)
columnas_finales = [
"CodigoIniciativa",
"Empresa",
"NombreCorto",
"Tipologia",
"Ciclo",
"Comentario",
"Ppto2026",
"Mes",
"MesIdx",
"Tag",
"Rol",
"RolOriginal",
"RolAsignadoFinal",
"ProyectoCuenta",
"EsTagCargaTrabajo",
"CapacidadParalelo",
"DotacionActual",
"EsSinRestriccion",
"CapacidadFuente",
"DotacionFuente",
"TipoRuteo",
"PrioridadRuteo",
"PrioridadTipologia",
"RondaBarrido",
"SecuenciaGrupo",
"OrigenAsignacion",
"FlagRuteado",
"LimiteBaseGrupo",
"CupoBaseTipologia",
"CupoApoyoTipologia",
"FuenteTabla",
"ProyectoMesKey",
"ProyectoMesTagKey",
"ProyectoMesRolKey",
detalle[col] = None
detalle = detalle[columnas_finales].copy()
detalle = detalle.sort_values(
na_position="last"
).reset_index(drop=True)
return detalle
"""
"""
orden_tipologias = [
"Distribución",
"Transmisión",
"Generación",
"Edificaciones",
"TIC",
"Innovación",
grupos = {}
for tip in orden_tipologias:
sub = sub.sort_values(
by=["CodigoIniciativa", "ProyectoMesTagKey"],
ascending=[True, True],
na_position="last"
).reset_index(drop=True)
grupos[tip] = sub
piezas = []
contador_global = 1
ronda = 1
while True:
hubo_fila = False
sub = grupos[tip]
fila["RondaBarrido"] = ronda
fila["SecuenciaGrupo"] = contador_global
fila["PrioridadTipologia"] = PRIORIDAD_TIPOLOGIA.get(tip,
999)
[Link](fila)
contador_global += 1
hubo_fila = True
if not hubo_fila:
break
ronda += 1
if not piezas:
return [Link][0:0].copy()
salida = salida.reset_index(drop=True)
return salida
def clasificar_cobertura_capacidad(df_detalle_preliminar:
[Link]) -> [Link]:
df = df_detalle_preliminar.copy()
df["EstadoCapacidad"] = "ConCapacidad"
[Link][df["CapacidadParalelo"].isna(), "EstadoCapacidad"] =
"SinCapacidad"
[Link][
"EstadoCapacidad"
] = "SinRestriccion999"
return df
def construir_detalle_calculo(df_detalle_preliminar: [Link]) ->
[Link]:
df = clasificar_cobertura_capacidad(df_detalle_preliminar)
df_calculo = df[
(df["EstadoCapacidad"] == "ConCapacidad")
| (df["EstadoCapacidad"] == "SinRestriccion999")
].copy()
def calcular_bolsa_rol_mes(
df_detalle_calculo: [Link],
) -> [Link]:
df = df_detalle_calculo.copy()
df = df[df["EsTagCargaTrabajo"] == 1].copy()
modo_conteo = int([Link]("modo_conteo_proyecto_mes",
1))
mostrar_trab_999 =
int([Link]("mostrar_trabajadores_requeridos_en_cap999", 0))
if modo_conteo != 1:
raise ValueError("La versión actual espera
modo_conteo_proyecto_mes = 1.")
agrupado = (
.agg(
TotalProyectosCargaRolMes=("CodigoIniciativa", lambda s:
[Link]().astype(str).nunique()),
CapacidadParalelo=("CapacidadParalelo", "max"),
EsSinRestriccion=("EsSinRestriccion", "max"),
.reset_index()
def calcular_trabajadores(row):
cap = row["CapacidadParalelo"]
total = row["TotalProyectosCargaRolMes"]
sinr = row["EsSinRestriccion"]
if [Link](cap):
return None
if sinr == 1:
if float(cap) <= 0:
return None
def calcular_indicador(row):
if row["EsSinRestriccion"] == 1:
return round(float(row["TotalProyectosCargaRolMes"]), 2)
return row["TrabajadoresRequeridosRolMes"]
agrupado["IndicadorDashboardRolMes"] =
[Link](calcular_indicador, axis=1)
agrupado["TipoIndicadorDashboard"] =
agrupado["EsSinRestriccion"].apply(
agrupado["MesOrden"] = agrupado["MesIdx"].astype(str).[Link](3)
+ "|" + agrupado["Mes"].astype(str)
agrupado["FlagCalculoValido"] =
agrupado["CapacidadParalelo"].apply(
agrupado = agrupado.sort_values(
na_position="last"
).reset_index(drop=True)
return agrupado
def calcular_bolsa_rol_mes_tipologia(
df_detalle_calculo: [Link],
) -> [Link]:
df = df_detalle_calculo.copy()
df = df[df["EsTagCargaTrabajo"] == 1].copy()
modo_conteo = int([Link]("modo_conteo_proyecto_mes",
1))
mostrar_trab_999 =
int([Link]("mostrar_trabajadores_requeridos_en_cap999", 0))
if modo_conteo != 1:
agrupado = (
.agg(
TotalProyectosCargaRolMesTipologia=("CodigoIniciativa",
lambda s: [Link]().astype(str).nunique()),
CapacidadParalelo=("CapacidadParalelo", "max"),
EsSinRestriccion=("EsSinRestriccion", "max"),
.reset_index()
)
def calcular_indicador(row):
cap = row["CapacidadParalelo"]
total = row["TotalProyectosCargaRolMesTipologia"]
sinr = row["EsSinRestriccion"]
if [Link](cap):
return None
if sinr == 1:
return round(float(total), 2)
if float(cap) <= 0:
return None
agrupado["IndicadorDashboardRolMesTipologia"] =
[Link](calcular_indicador, axis=1)
agrupado["PrioridadTipologia"] =
agrupado["Tipologia"].map(PRIORIDAD_TIPOLOGIA).fillna(999).astype(
int)
agrupado["TipologiaOrdenHeatmap"] =
agrupado["PrioridadTipologia"].astype(str).[Link](2) + " - " +
agrupado["Tipologia"].astype(str)
agrupado = agrupado.sort_values(
na_position="last"
).reset_index(drop=True)
return agrupado
def calcular_necesidad_mensual(
df_detalle_calculo: [Link],
df_bolsa_rol_mes: [Link],
) -> [Link]:
df = df_detalle_calculo.copy()
df = df[df["EsTagCargaTrabajo"] == 1].copy()
agrupado_tipologia = (
.agg(
ProyectosVisiblesTipologia=("CodigoIniciativa", lambda s:
[Link]().astype(str).nunique())
.reset_index()
salida = agrupado_tipologia.merge(
df_bolsa_rol_mes,
how="left"
salida = salida.sort_values(
).reset_index(drop=True)
return salida
def resumen_cobertura_capacidad(df_detalle_clasificado:
[Link]) -> [Link]:
resumen = (
df_detalle_clasificado.groupby("EstadoCapacidad",
dropna=False)
.size()
.reset_index(name="Filas")
.sort_values(by="EstadoCapacidad")
.reset_index(drop=True)
return resumen
filas = []
gg = g.sort_values(
by=["TotalProyectosCargaRolMes", "MesIdx"],
ascending=[False, True],
na_position="last"
).reset_index(drop=True)
trab = gg["TrabajadoresRequeridosRolMes"].dropna()
sinr = gg["EsSinRestriccion"].dropna()
[Link](
"Empresa": empresa,
"Rol": rol,
"MesPico": mes_pico,
"PromedioTrabajadoresRequeridos": round([Link](), 2) if
not [Link] else None,
"MesesConActividad": int(meses_activos),
df = [Link](filas)
return df
def calcular_resumen_rol_empresa_tipologia(df_necesidad_mensual:
[Link]) -> [Link]:
filas = []
proy_tip = g["ProyectosVisiblesTipologia"].dropna()
proy_bolsa = g["TotalProyectosCargaRolMes"].dropna()
trab = g["TrabajadoresRequeridosRolMes"].dropna()
sinr = g["EsSinRestriccion"].dropna()
[Link](
"Empresa": empresa,
"Rol": rol,
"Tipologia": tipologia,
"PromedioProyectosCargaTipologia": round(proy_tip.mean(),
2) if not proy_tip.empty else None,
"MesesConActividad": int(meses_act),
"PromedioTrabajadoresRequeridosRolMes":
round([Link](), 2) if not [Link] else None,
"EsSinRestriccion": [Link]() if not [Link] else None,
df = [Link](filas)
return df
def calcular_plazas_requeridas(
df_bolsa_rol_mes: [Link],
df_resumen_rol_empresa: [Link],
) -> [Link]:
bolsa = (
.agg(
CapacidadParalelo=("CapacidadParalelo", "max"),
EsSinRestriccion=("EsSinRestriccion", "max"),
PicoProyectosCarga=("TotalProyectosCargaRolMes", "max"),
PromedioProyectosCarga=("TotalProyectosCargaRolMes",
"mean"),
.reset_index()
salida = [Link](
on=["Empresa", "Rol"],
how="left"
)
def tipo_regla_plaza(rol):
return "BIM_2"
if rol in {
"Esp. Servidumbre",
"Esp. Ambiental",
"Esp. Legal",
}:
return "FIJA_1"
return "CAPACIDAD"
salida["TipoReglaPlaza"] = salida["Rol"].apply(tipo_regla_plaza)
def plazas(row):
regla = row["TipoReglaPlaza"]
cap = row["CapacidadParalelo"]
pico = row["PicoProyectosCarga"]
if regla == "BIM_2":
return 2
if regla == "FIJA_1":
return 1
return None
salida["PromedioProyectosCarga"] =
salida["PromedioProyectosCarga"].round(2)
return salida
def calcular_asignacion_proyecto_plaza(
df_detalle_calculo: [Link],
df_plazas_requeridas: [Link],
) -> [Link]:
source = df_detalle_calculo.copy()
proyecto_visible = (
.agg(
MesInicioVisibleIdx=("MesIdx", "min"),
MesFinVisibleIdx=("MesIdx", "max"),
DuracionVisibleMeses=("MesIdx", lambda s:
[Link]().nunique()),
.reset_index()
)
proyecto_carga = (
source[source["EsTagCargaTrabajo"] == 1]
.agg(
MesInicioCargaIdx=("MesIdx", "min"),
MesFinCargaIdx=("MesIdx", "max"),
DuracionCargaMeses=("MesIdx", lambda s:
[Link]().nunique()),
.reset_index()
salida = proyecto_visible.merge(
proyecto_carga,
how="left"
salida["DuracionCargaMeses"] =
salida["DuracionCargaMeses"].fillna(0)
def tag_principal(tags):
for t in prioridad:
if t in tags:
return t
salida = [Link](
on=["Empresa", "Rol"],
how="left"
grupos = []
gg = [Link]()
regla = gg["TipoReglaPlaza"].dropna()
plazas = gg["PlazasRequeridas"].dropna()
if regla == "FIJA_1":
gg = gg.sort_values(
by=["Tipologia", "CodigoIniciativa"],
ascending=[True, True],
na_position="last"
gg = gg.sort_values(
by=["Tipologia", "TagPrincipalBIM", "DuracionVisibleMeses",
"CodigoIniciativa"],
na_position="last"
else:
gg = gg.sort_values(
by=["DuracionCargaMeses", "MesInicioCargaIdx",
"MesFinCargaIdx", "CodigoIniciativa"],
na_position="last"
gg = gg.reset_index(drop=True)
def plaza_n(n):
return None
if regla == "FIJA_1":
return 1
gg["PlazaTeoricaN"] = gg["OrdenAsignacion"].apply(plaza_n)
gg["PlazaTeorica"] = gg["PlazaTeoricaN"].apply(
[Link](gg)
asignacion = [Link](grupos, ignore_index=True) if grupos else
[Link]()
asignacion = [Link](
detalle_mes,
how="left"
asignacion = asignacion.sort_values(
na_position="last"
).reset_index(drop=True)
return asignacion
def escribir_salidas_excel(
ruta_salida: Path,
df_detalle_preliminar: [Link],
df_detalle_clasificado: [Link],
df_detalle_calculo: [Link],
df_bolsa_rol_mes: [Link],
df_bolsa_rol_mes_tipologia: [Link],
df_necesidad_mensual: [Link],
df_resumen_rol_empresa: [Link],
df_resumen_rol_empresa_tipologia: [Link],
df_plazas_requeridas: [Link],
df_asignacion_proyecto_plaza: [Link],
df_matriz_rol_plaza: [Link],
) -> None:
df_detalle_preliminar.to_excel(writer,
sheet_name="out_DetallePreliminar", index=False)
df_detalle_clasificado.to_excel(writer,
sheet_name="out_DetalleClasificado", index=False)
df_detalle_calculo.to_excel(writer,
sheet_name="out_DetalleCalculo", index=False)
df_bolsa_rol_mes.to_excel(writer,
sheet_name="out_BolsaRolMes", index=False)
df_bolsa_rol_mes_tipologia.to_excel(writer,
sheet_name="out_BolsaRolMesTipologia", index=False)
df_necesidad_mensual.to_excel(writer,
sheet_name="out_NecesidadMensual", index=False)
df_resumen_rol_empresa.to_excel(writer,
sheet_name="out_ResumenRolEmpresa", index=False)
df_resumen_rol_empresa_tipologia.to_excel(writer,
sheet_name="out_ResumenRolTipologia", index=False)
df_plazas_requeridas.to_excel(writer,
sheet_name="out_PlazasRequeridas", index=False)
df_asignacion_proyecto_plaza.to_excel(writer,
sheet_name="out_AsignacionPlaza", index=False)
df_matriz_rol_plaza.to_excel(writer,
sheet_name="out_MatrizRolPlaza", index=False)
def construir_matriz_rol_plaza(
df_asignacion_proyecto_plaza: [Link],
df_catalogo_meses: [Link],
) -> [Link]:
source = df_asignacion_proyecto_plaza.copy()
meses_ordenados = (
df_catalogo_meses[["Mes", "MesIdx"]]
.drop_duplicates()
.sort_values("MesIdx")
lista_meses = meses_ordenados["Mes"].tolist()
base = source[
"Empresa",
"Rol",
"PlazaTeorica",
"PlazaTeoricaN",
"OrdenAsignacion",
"CodigoIniciativa",
"NombreCorto",
"Tipologia",
"Mes",
"Tag",
].copy()
matriz = base.pivot_table(
index=[
"Empresa",
"Rol",
"PlazaTeorica",
"PlazaTeoricaN",
"OrdenAsignacion",
"CodigoIniciativa",
"NombreCorto",
"Tipologia",
],
columns="Mes",
values="Tag",
aggfunc="first",
).reset_index()
columnas_fijas = [
"Empresa",
"Rol",
"PlazaTeorica",
"PlazaTeoricaN",
"OrdenAsignacion",
"CodigoIniciativa",
"NombreCorto",
"Tipologia",
matriz = matriz[columnas_finales].copy()
matriz = matriz.sort_values(
na_position="last"
).reset_index(drop=True)
return matriz
#
==========================================
===============
# MAIN
#
==========================================
===============
def main():
print(f"Archivo: {RUTA_ARCHIVO}")
if not RUTA_ARCHIVO.exists():
return
errores: List[str] = []
# 1. Lectura
try:
df = obtener_tabla_excel(RUTA_ARCHIVO, nombre_tabla)
tablas[nombre_tabla] = df
[Link](validar_columnas(nombre_tabla, df,
columnas_esperadas))
except Exception as e:
if tablas:
imprimir_resumen_tablas(tablas)
parametros = {}
if "xpu_ParametrosModelo" in tablas:
parametros =
construir_parametros(tablas["xpu_ParametrosModelo"])
print("\nPARAMETROS ACTIVOS")
print("-" * 70)
for k, v in [Link]():
print(f"{k}: {v}")
print("-" * 70)
[Link](validar_vacios_criticos(tablas))
[Link](validar_duplicados(tablas))
print("\nRESULTADO DE VALIDACIÓN")
print("-" * 70)
if errores:
print(f"- {err}")
print("-" * 70)
print("Se detiene la ejecución por errores de estructura.")
return
else:
print("-" * 70)
# 2. Expansión
df_reglas_expandidas =
expandir_reglas_rol(tablas["xpu_RolReglas"])
df_ruteo_expandido =
expandir_ruteo(tablas["xpu_RuteoBalanceRol"])
print("\nRESUMEN DE EXPANSIÓN")
print("-" * 70)
print("-" * 70)
# 3. Candidatos
df_candidatos = generar_candidatos(
df_proyecto_mes_tag=tablas["xpu_ProyectoMesTag"],
df_reglas_expandidas=df_reglas_expandidas,
df_ruteo_expandido=df_ruteo_expandido,
parametros=parametros,
print("\nRESUMEN DE CANDIDATOS")
print("-" * 70)
print(f"Candidatos generados: {df_candidatos.shape[0]} filas x
{df_candidatos.shape[1]} columnas")
print(f"Empresas en candidatos:
{df_candidatos['Empresa'].nunique()}")
print("-" * 70)
control_elcto_generacion = df_candidatos[
].copy()
print("-" * 70)
if control_elcto_generacion.empty:
else:
print(control_elcto_generacion["Rol"].value_counts(dropna=False).to_s
tring())
print("-" * 70)
# 4. Capacidad y ruteo
df_con_capacidad = agregar_capacidad_y_dotacion(
df_candidatos=df_candidatos,
df_capacidad=tablas["xpu_CapacidadRolEmpresa"],
df_dotacion=tablas["xpu_DotacionActual"],
parametros=parametros,
print("-" * 70)
print("-" * 70)
df_detalle_preliminar = aplicar_ruteo_base_apoyo(
df_con_capacidad=df_con_capacidad,
df_ruteo_expandido=df_ruteo_expandido,
print("-" * 70)
print(f"Roles finales :
{df_detalle_preliminar['Rol'].nunique()}")
print(f"Filas ruteadas :
{int((df_detalle_preliminar['FlagRuteado'] == 1).sum())}")
print(f"Filas no ruteadas :
{int((df_detalle_preliminar['FlagRuteado'] == 0).sum())}")
print(f"Asignadas a apoyo :
{int((df_detalle_preliminar['OrigenAsignacion'] ==
'RuteoApoyo').sum())}")
print("-" * 70)
control_estudios_elcto_oct26 = df_detalle_preliminar[
].copy()
print("-" * 70)
if control_estudios_elcto_oct26.empty:
else:
print(
control_estudios_elcto_oct26[
"CodigoIniciativa",
"Tipologia",
"RondaBarrido",
"SecuenciaGrupo",
"RolAsignadoFinal",
"OrigenAsignacion",
.sort_values(["SecuenciaGrupo", "CodigoIniciativa"],
ascending=[True, True])
.head(40)
.to_string(index=False)
print("-" * 70)
control_desborde_elcto = df_detalle_preliminar[
].copy()
print("-" * 70)
if control_desborde_elcto.empty:
else:
print(
control_desborde_elcto[
"CodigoIniciativa",
"Tipologia",
"RolAsignadoFinal",
"OrigenAsignacion",
"CupoBaseTipologia",
"CupoApoyoTipologia",
]
.sort_values(["Tipologia", "CodigoIniciativa"], ascending=[True,
True])
.head(60)
.to_string(index=False)
print("-" * 70)
df_detalle_clasificado, df_detalle_calculo =
construir_detalle_calculo(df_detalle_preliminar)
df_resumen_cobertura =
resumen_cobertura_capacidad(df_detalle_clasificado)
print("-" * 70)
print(f"{row['EstadoCapacidad']:<20}: {row['Filas']}")
print("-" * 70)
print("-" * 70)
# 6. Bolsa rol-mes
df_bolsa_rol_mes = calcular_bolsa_rol_mes(
df_detalle_calculo=df_detalle_calculo,
parametros=parametros,
print("-" * 70)
print(f"Bolsa generada : {df_bolsa_rol_mes.shape[0]} filas
x {df_bolsa_rol_mes.shape[1]} columnas")
print(f"Empresas en bolsa :
{df_bolsa_rol_mes['Empresa'].nunique()}")
print(f"Roles en bolsa :
{df_bolsa_rol_mes['Rol'].nunique()}")
print("-" * 70)
df_bolsa_rol_mes_tipologia = calcular_bolsa_rol_mes_tipologia(
df_detalle_calculo=df_detalle_calculo,
parametros=parametros,
print("-" * 70)
print(f"Roles en resultado :
{df_bolsa_rol_mes_tipologia['Rol'].nunique()}")
print(f"Tipologías en resultado :
{df_bolsa_rol_mes_tipologia['Tipologia'].nunique()}")
print("-" * 70)
# 8. Necesidad mensual
df_necesidad_mensual = calcular_necesidad_mensual(
df_detalle_calculo=df_detalle_calculo,
df_bolsa_rol_mes=df_bolsa_rol_mes,
print("-" * 70)
print(f"Tipologías en resultado :
{df_necesidad_mensual['Tipologia'].nunique()}")
print(f"Roles en resultado :
{df_necesidad_mensual['Rol'].nunique()}")
print("-" * 70)
df_resumen_rol_empresa =
calcular_resumen_rol_empresa(df_bolsa_rol_mes)
print("-" * 70)
print(f"Roles en resumen :
{df_resumen_rol_empresa['Rol'].nunique()}")
print("-" * 70)
df_resumen_rol_empresa_tipologia =
calcular_resumen_rol_empresa_tipologia(df_necesidad_mensual)
print(f"Tipologías en resumen :
{df_resumen_rol_empresa_tipologia['Tipologia'].nunique()}")
print("-" * 70)
df_plazas_requeridas = calcular_plazas_requeridas(
df_bolsa_rol_mes=df_bolsa_rol_mes,
df_resumen_rol_empresa=df_resumen_rol_empresa,
print("-" * 70)
print("-" * 70)
df_asignacion_proyecto_plaza =
calcular_asignacion_proyecto_plaza(
df_detalle_calculo=df_detalle_calculo,
df_plazas_requeridas=df_plazas_requeridas,
print(f"Asignación generada :
{df_asignacion_proyecto_plaza.shape[0]} filas x
{df_asignacion_proyecto_plaza.shape[1]} columnas")
print("-" * 70)
df_matriz_rol_plaza = construir_matriz_rol_plaza(
df_asignacion_proyecto_plaza=df_asignacion_proyecto_plaza,
df_catalogo_meses=tablas["xpu_CatalogoMeses"],
print("-" * 70)
print(f"Empresas en matriz :
{df_matriz_rol_plaza['Empresa'].nunique()}")
print(f"Roles en matriz :
{df_matriz_rol_plaza['Rol'].nunique()}")
print("-" * 70)
exportar_debug(df_matriz_rol_plaza, "debug_matriz_rol_plaza.csv")
escribir_salidas_excel(
ruta_salida=ruta_excel_salida,
df_detalle_preliminar=df_detalle_preliminar,
df_detalle_clasificado=df_detalle_clasificado,
df_detalle_calculo=df_detalle_calculo,
df_bolsa_rol_mes=df_bolsa_rol_mes,
df_bolsa_rol_mes_tipologia=df_bolsa_rol_mes_tipologia,
df_necesidad_mensual=df_necesidad_mensual,
df_resumen_rol_empresa=df_resumen_rol_empresa,
df_resumen_rol_empresa_tipologia=df_resumen_rol_empresa_tipologi
a,
df_plazas_requeridas=df_plazas_requeridas,
df_asignacion_proyecto_plaza=df_asignacion_proyecto_plaza,
df_matriz_rol_plaza=df_matriz_rol_plaza,
print("\nSALIDA EXCEL")
print("-" * 70)
print("-" * 70)
exportar_debug(df_resumen_rol_empresa,
"debug_resumen_rol_empresa.csv")
exportar_debug(df_resumen_rol_empresa_tipologia,
"debug_resumen_rol_empresa_tipologia.csv")
exportar_debug(df_plazas_requeridas,
"debug_plazas_requeridas.csv")
exportar_debug(df_asignacion_proyecto_plaza,
"debug_asignacion_proyecto_plaza.csv")
# 16. Export debug
exportar_debug(df_detalle_clasificado,
"debug_detalle_clasificado.csv")
exportar_debug(df_detalle_calculo, "debug_detalle_calculo.csv")
exportar_debug(df_bolsa_rol_mes, "debug_bolsa_rol_mes.csv")
exportar_debug(df_necesidad_mensual,
"debug_necesidad_mensual.csv")
# Export debug
exportar_debug(df_reglas_expandidas,
"debug_reglas_expandidas.csv")
exportar_debug(df_ruteo_expandido, "debug_ruteo_expandido.csv")
exportar_debug(df_candidatos, "debug_candidatos.csv")
exportar_debug(df_con_capacidad, "debug_con_capacidad.csv")
exportar_debug(df_detalle_preliminar,
"debug_detalle_preliminar.csv")
exportar_debug(df_detalle_clasificado,
"debug_detalle_clasificado.csv")
exportar_debug(df_detalle_calculo, "debug_detalle_calculo.csv")
exportar_debug(df_bolsa_rol_mes, "debug_bolsa_rol_mes.csv")
exportar_debug(df_necesidad_mensual,
"debug_necesidad_mensual.csv")
exportar_debug(df_resumen_rol_empresa,
"debug_resumen_rol_empresa.csv")
exportar_debug(df_resumen_rol_empresa_tipologia,
"debug_resumen_rol_empresa_tipologia.csv")
exportar_debug(df_plazas_requeridas,
"debug_plazas_requeridas.csv")
exportar_debug(df_asignacion_proyecto_plaza,
"debug_asignacion_proyecto_plaza.csv")
exportar_debug(df_bolsa_rol_mes_tipologia,
"debug_bolsa_rol_mes_tipologia.csv")
if __name__ == "__main__":
main()