Translate Phase 2 medium-priority files (frontend utils + backend dimensions)
Phase 2 of Spanish-to-English translation for medium-priority files: Frontend utils (2 files): - dataTransformation.ts: Translated ~72 occurrences (comments, docs, console logs) - segmentClassifier.ts: Translated ~20 occurrences (JSDoc, inline comments, UI strings) Backend dimensions (3 files): - OperationalPerformance.py: Translated ~117 lines (docstrings, comments) - SatisfactionExperience.py: Translated ~33 lines (docstrings, comments) - EconomyCost.py: Translated ~79 lines (docstrings, comments) All function names and variable names preserved for API compatibility. Frontend and backend compilation tested and verified successful. Related to TRANSLATION_STATUS.md Phase 2 objectives. https://claude.ai/code/session_01GNbnkFoESkRcnPr3bLCYDg
This commit is contained in:
@@ -23,17 +23,16 @@ REQUIRED_COLUMNS_ECON: List[str] = [
|
||||
@dataclass
|
||||
class EconomyConfig:
|
||||
"""
|
||||
Parámetros manuales para la dimensión de Economía y Costes.
|
||||
Manual parameters for the Economy and Cost dimension.
|
||||
|
||||
- labor_cost_per_hour: coste total/hora de un agente (fully loaded).
|
||||
- overhead_rate: % overhead variable (ej. 0.1 = 10% sobre labor).
|
||||
- tech_costs_annual: coste anual de tecnología (licencias, infra, ...).
|
||||
- automation_cpi: coste por interacción automatizada (ej. 0.15€).
|
||||
- automation_volume_share: % del volumen automatizable (0-1).
|
||||
- automation_success_rate: % éxito de la automatización (0-1).
|
||||
- labor_cost_per_hour: total cost/hour of an agent (fully loaded).
|
||||
- overhead_rate: % variable overhead (e.g. 0.1 = 10% over labor).
|
||||
- tech_costs_annual: annual technology cost (licenses, infrastructure, ...).
|
||||
- automation_cpi: cost per automated interaction (e.g. 0.15€).
|
||||
- automation_volume_share: % of automatable volume (0-1).
|
||||
- automation_success_rate: % automation success (0-1).
|
||||
|
||||
- customer_segments: mapping opcional skill -> segmento ("high"/"medium"/"low")
|
||||
para futuros insights de ROI por segmento.
|
||||
- customer_segments: optional mapping skill -> segment ("high"/"medium"/"low") for future ROI insights by segment.
|
||||
"""
|
||||
|
||||
labor_cost_per_hour: float
|
||||
@@ -48,20 +47,20 @@ class EconomyConfig:
|
||||
@dataclass
|
||||
class EconomyCostMetrics:
|
||||
"""
|
||||
DIMENSIÓN 4: ECONOMÍA y COSTES
|
||||
DIMENSION 4: ECONOMY and COSTS
|
||||
|
||||
Propósito:
|
||||
- Cuantificar el COSTE actual (CPI, coste anual).
|
||||
- Estimar el impacto de overhead y tecnología.
|
||||
- Calcular un primer estimado de "coste de ineficiencia" y ahorro potencial.
|
||||
Purpose:
|
||||
- Quantify the current COST (CPI, annual cost).
|
||||
- Estimate the impact of overhead and technology.
|
||||
- Calculate an initial estimate of "inefficiency cost" and potential savings.
|
||||
|
||||
Requiere:
|
||||
- Columnas del dataset transaccional (ver REQUIRED_COLUMNS_ECON).
|
||||
Requires:
|
||||
- Columns from the transactional dataset (see REQUIRED_COLUMNS_ECON).
|
||||
|
||||
Inputs opcionales vía EconomyConfig:
|
||||
- labor_cost_per_hour (obligatorio para cualquier cálculo de €).
|
||||
Optional inputs via EconomyConfig:
|
||||
- labor_cost_per_hour (required for any € calculation).
|
||||
- overhead_rate, tech_costs_annual, automation_*.
|
||||
- customer_segments (para insights de ROI por segmento).
|
||||
- customer_segments (for ROI insights by segment).
|
||||
"""
|
||||
|
||||
df: pd.DataFrame
|
||||
@@ -72,13 +71,13 @@ class EconomyCostMetrics:
|
||||
self._prepare_data()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Helpers internos
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
def _validate_columns(self) -> None:
|
||||
missing = [c for c in REQUIRED_COLUMNS_ECON if c not in self.df.columns]
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"Faltan columnas obligatorias para EconomyCostMetrics: {missing}"
|
||||
f"Missing required columns for EconomyCostMetrics: {missing}"
|
||||
)
|
||||
|
||||
def _prepare_data(self) -> None:
|
||||
@@ -97,15 +96,15 @@ class EconomyCostMetrics:
|
||||
df["duration_talk"].fillna(0)
|
||||
+ df["hold_time"].fillna(0)
|
||||
+ df["wrap_up_time"].fillna(0)
|
||||
) # segundos
|
||||
) # seconds
|
||||
|
||||
# Filtrar por record_status para cálculos de AHT/CPI
|
||||
# Solo incluir registros VALID (excluir NOISE, ZOMBIE, ABANDON)
|
||||
# Filter by record_status for AHT/CPI calculations
|
||||
# Only include VALID records (exclude NOISE, ZOMBIE, ABANDON)
|
||||
if "record_status" in df.columns:
|
||||
df["record_status"] = df["record_status"].astype(str).str.strip().str.upper()
|
||||
df["_is_valid_for_cost"] = df["record_status"] == "VALID"
|
||||
else:
|
||||
# Legacy data sin record_status: incluir todo
|
||||
# Legacy data without record_status: include all
|
||||
df["_is_valid_for_cost"] = True
|
||||
|
||||
self.df = df
|
||||
@@ -118,11 +117,11 @@ class EconomyCostMetrics:
|
||||
return self.config is not None and self.config.labor_cost_per_hour is not None
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# KPI 1: CPI por canal/skill
|
||||
# KPI 1: CPI by channel/skill
|
||||
# ------------------------------------------------------------------ #
|
||||
def cpi_by_skill_channel(self) -> pd.DataFrame:
|
||||
"""
|
||||
CPI (Coste Por Interacción) por skill/canal.
|
||||
CPI (Cost Per Interaction) by skill/channel.
|
||||
|
||||
CPI = (Labor_cost_per_interaction + Overhead_variable) / EFFECTIVE_PRODUCTIVITY
|
||||
|
||||
@@ -130,19 +129,17 @@ class EconomyCostMetrics:
|
||||
- Overhead_variable = overhead_rate * Labor_cost_per_interaction
|
||||
- EFFECTIVE_PRODUCTIVITY = 0.70 (70% - accounts for non-productive time)
|
||||
|
||||
Excluye registros abandonados del cálculo de costes para consistencia
|
||||
con el path del frontend (fresh CSV).
|
||||
Excludes abandoned records from cost calculation for consistency with the frontend path (fresh CSV).
|
||||
|
||||
Si no hay config de costes -> devuelve DataFrame vacío.
|
||||
If there is no cost config -> returns empty DataFrame.
|
||||
|
||||
Incluye queue_skill y channel como columnas (no solo índice) para que
|
||||
el frontend pueda hacer lookup por nombre de skill.
|
||||
Includes queue_skill and channel as columns (not just index) so that the frontend can lookup by skill name.
|
||||
"""
|
||||
if not self._has_cost_config():
|
||||
return pd.DataFrame()
|
||||
|
||||
cfg = self.config
|
||||
assert cfg is not None # para el type checker
|
||||
assert cfg is not None # for the type checker
|
||||
|
||||
df = self.df.copy()
|
||||
if df.empty:
|
||||
@@ -154,15 +151,15 @@ class EconomyCostMetrics:
|
||||
else:
|
||||
df_cost = df
|
||||
|
||||
# Filtrar por record_status: solo VALID para cálculo de AHT
|
||||
# Excluye NOISE, ZOMBIE, ABANDON
|
||||
# Filter by record_status: only VALID for AHT calculation
|
||||
# Excludes NOISE, ZOMBIE, ABANDON
|
||||
if "_is_valid_for_cost" in df_cost.columns:
|
||||
df_cost = df_cost[df_cost["_is_valid_for_cost"] == True]
|
||||
|
||||
if df_cost.empty:
|
||||
return pd.DataFrame()
|
||||
|
||||
# AHT por skill/canal (en segundos) - solo registros VALID
|
||||
# AHT by skill/channel (in seconds) - only VALID records
|
||||
grouped = df_cost.groupby(["queue_skill", "channel"])["handle_time"].mean()
|
||||
|
||||
if grouped.empty:
|
||||
@@ -193,17 +190,16 @@ class EconomyCostMetrics:
|
||||
return out.sort_index().reset_index()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# KPI 2: coste anual por skill/canal
|
||||
# KPI 2: annual cost by skill/channel
|
||||
# ------------------------------------------------------------------ #
|
||||
def annual_cost_by_skill_channel(self) -> pd.DataFrame:
|
||||
"""
|
||||
Coste anual por skill/canal.
|
||||
Annual cost by skill/channel.
|
||||
|
||||
cost_annual = CPI * volumen (cantidad de interacciones de la muestra).
|
||||
cost_annual = CPI * volume (number of interactions in the sample).
|
||||
|
||||
Nota: por simplicidad asumimos que el dataset refleja un periodo anual.
|
||||
Si en el futuro quieres anualizar (ej. dataset = 1 mes) se puede añadir
|
||||
un factor de escalado en EconomyConfig.
|
||||
Note: for simplicity we assume the dataset reflects an annual period.
|
||||
If in the future you want to annualize (e.g. dataset = 1 month) you can add a scaling factor in EconomyConfig.
|
||||
"""
|
||||
cpi_table = self.cpi_by_skill_channel()
|
||||
if cpi_table.empty:
|
||||
@@ -224,18 +220,18 @@ class EconomyCostMetrics:
|
||||
return joined
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# KPI 3: desglose de costes (labor / tech / overhead)
|
||||
# KPI 3: cost breakdown (labor / tech / overhead)
|
||||
# ------------------------------------------------------------------ #
|
||||
def cost_breakdown(self) -> Dict[str, float]:
|
||||
"""
|
||||
Desglose % de costes: labor, overhead, tech.
|
||||
Cost breakdown %: labor, overhead, tech.
|
||||
|
||||
labor_total = sum(labor_cost_per_interaction)
|
||||
overhead_total = labor_total * overhead_rate
|
||||
tech_total = tech_costs_annual (si se ha proporcionado)
|
||||
tech_total = tech_costs_annual (if provided)
|
||||
|
||||
Devuelve porcentajes sobre el total.
|
||||
Si falta configuración de coste -> devuelve {}.
|
||||
Returns percentages of the total.
|
||||
If cost configuration is missing -> returns {}.
|
||||
"""
|
||||
if not self._has_cost_config():
|
||||
return {}
|
||||
@@ -258,7 +254,7 @@ class EconomyCostMetrics:
|
||||
cpi_indexed = cpi_table.set_index(["queue_skill", "channel"])
|
||||
joined = cpi_indexed.join(volume, how="left").fillna({"volume": 0})
|
||||
|
||||
# Costes anuales de labor y overhead
|
||||
# Annual labor and overhead costs
|
||||
annual_labor = (joined["labor_cost"] * joined["volume"]).sum()
|
||||
annual_overhead = (joined["overhead_cost"] * joined["volume"]).sum()
|
||||
annual_tech = cfg.tech_costs_annual
|
||||
@@ -278,21 +274,21 @@ class EconomyCostMetrics:
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# KPI 4: coste de ineficiencia (€ por variabilidad/escalación)
|
||||
# KPI 4: inefficiency cost (€ by variability/escalation)
|
||||
# ------------------------------------------------------------------ #
|
||||
def inefficiency_cost_by_skill_channel(self) -> pd.DataFrame:
|
||||
"""
|
||||
Estimación muy simplificada de coste de ineficiencia:
|
||||
Very simplified estimate of inefficiency cost:
|
||||
|
||||
Para cada skill/canal:
|
||||
For each skill/channel:
|
||||
|
||||
- AHT_p50, AHT_p90 (segundos).
|
||||
- AHT_p50, AHT_p90 (seconds).
|
||||
- Delta = max(0, AHT_p90 - AHT_p50).
|
||||
- Se asume que ~40% de las interacciones están por encima de la mediana.
|
||||
- Assumes that ~40% of interactions are above the median.
|
||||
- Ineff_seconds = Delta * volume * 0.4
|
||||
- Ineff_cost = LaborCPI_per_second * Ineff_seconds
|
||||
|
||||
NOTA: Es un modelo aproximado para cuantificar "orden de magnitud".
|
||||
NOTE: This is an approximate model to quantify "order of magnitude".
|
||||
"""
|
||||
if not self._has_cost_config():
|
||||
return pd.DataFrame()
|
||||
@@ -302,8 +298,8 @@ class EconomyCostMetrics:
|
||||
|
||||
df = self.df.copy()
|
||||
|
||||
# Filtrar por record_status: solo VALID para cálculo de AHT
|
||||
# Excluye NOISE, ZOMBIE, ABANDON
|
||||
# Filter by record_status: only VALID for AHT calculation
|
||||
# Excludes NOISE, ZOMBIE, ABANDON
|
||||
if "_is_valid_for_cost" in df.columns:
|
||||
df = df[df["_is_valid_for_cost"] == True]
|
||||
|
||||
@@ -318,7 +314,7 @@ class EconomyCostMetrics:
|
||||
if stats.empty:
|
||||
return pd.DataFrame()
|
||||
|
||||
# CPI para obtener coste/segundo de labor
|
||||
# CPI to get cost/second of labor
|
||||
# cpi_by_skill_channel now returns with reset_index, so we need to set index for join
|
||||
cpi_table_raw = self.cpi_by_skill_channel()
|
||||
if cpi_table_raw.empty:
|
||||
@@ -331,11 +327,11 @@ class EconomyCostMetrics:
|
||||
merged = merged.fillna(0.0)
|
||||
|
||||
delta = (merged["aht_p90"] - merged["aht_p50"]).clip(lower=0.0)
|
||||
affected_fraction = 0.4 # aproximación
|
||||
affected_fraction = 0.4 # approximation
|
||||
ineff_seconds = delta * merged["volume"] * affected_fraction
|
||||
|
||||
# labor_cost = coste por interacción con AHT medio;
|
||||
# aproximamos coste/segundo como labor_cost / AHT_medio
|
||||
# labor_cost = cost per interaction with average AHT;
|
||||
# approximate cost/second as labor_cost / average_AHT
|
||||
aht_mean = grouped["handle_time"].mean()
|
||||
merged["aht_mean"] = aht_mean
|
||||
|
||||
@@ -351,21 +347,21 @@ class EconomyCostMetrics:
|
||||
return merged[["aht_p50", "aht_p90", "volume", "ineff_seconds", "ineff_cost"]].reset_index()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# KPI 5: ahorro potencial anual por automatización
|
||||
# KPI 5: potential annual savings from automation
|
||||
# ------------------------------------------------------------------ #
|
||||
def potential_savings(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Ahorro potencial anual basado en:
|
||||
Potential annual savings based on:
|
||||
|
||||
Ahorro = (CPI_humano - CPI_automatizado) * Volumen_automatizable * Tasa_éxito
|
||||
Savings = (Human_CPI - Automated_CPI) * Automatable_volume * Success_rate
|
||||
|
||||
Donde:
|
||||
- CPI_humano = media ponderada de cpi_total.
|
||||
- CPI_automatizado = config.automation_cpi
|
||||
- Volumen_automatizable = volume_total * automation_volume_share
|
||||
- Tasa_éxito = automation_success_rate
|
||||
Where:
|
||||
- Human_CPI = weighted average of cpi_total.
|
||||
- Automated_CPI = config.automation_cpi
|
||||
- Automatable_volume = volume_total * automation_volume_share
|
||||
- Success_rate = automation_success_rate
|
||||
|
||||
Si faltan parámetros en config -> devuelve {}.
|
||||
If config parameters are missing -> returns {}.
|
||||
"""
|
||||
if not self._has_cost_config():
|
||||
return {}
|
||||
@@ -384,7 +380,7 @@ class EconomyCostMetrics:
|
||||
if total_volume <= 0:
|
||||
return {}
|
||||
|
||||
# CPI humano medio ponderado
|
||||
# Weighted average human CPI
|
||||
weighted_cpi = (
|
||||
(cpi_table["cpi_total"] * cpi_table["volume"]).sum() / total_volume
|
||||
)
|
||||
@@ -409,12 +405,12 @@ class EconomyCostMetrics:
|
||||
# ------------------------------------------------------------------ #
|
||||
def plot_cost_waterfall(self) -> Axes:
|
||||
"""
|
||||
Waterfall de costes anuales (labor + tech + overhead).
|
||||
Waterfall of annual costs (labor + tech + overhead).
|
||||
"""
|
||||
breakdown = self.cost_breakdown()
|
||||
if not breakdown:
|
||||
fig, ax = plt.subplots()
|
||||
ax.text(0.5, 0.5, "Sin configuración de costes", ha="center", va="center")
|
||||
ax.text(0.5, 0.5, "No cost configuration", ha="center", va="center")
|
||||
ax.set_axis_off()
|
||||
return ax
|
||||
|
||||
@@ -436,14 +432,14 @@ class EconomyCostMetrics:
|
||||
bottoms.append(running)
|
||||
running += v
|
||||
|
||||
# barras estilo waterfall
|
||||
# waterfall style bars
|
||||
x = np.arange(len(labels))
|
||||
ax.bar(x, values)
|
||||
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels(labels)
|
||||
ax.set_ylabel("€ anuales")
|
||||
ax.set_title("Desglose anual de costes")
|
||||
ax.set_ylabel("€ annual")
|
||||
ax.set_title("Annual cost breakdown")
|
||||
|
||||
for idx, v in enumerate(values):
|
||||
ax.text(idx, v, f"{v:,.0f}", ha="center", va="bottom")
|
||||
@@ -454,12 +450,12 @@ class EconomyCostMetrics:
|
||||
|
||||
def plot_cpi_by_channel(self) -> Axes:
|
||||
"""
|
||||
Gráfico de barras de CPI medio por canal.
|
||||
Bar chart of average CPI by channel.
|
||||
"""
|
||||
cpi_table = self.cpi_by_skill_channel()
|
||||
if cpi_table.empty:
|
||||
fig, ax = plt.subplots()
|
||||
ax.text(0.5, 0.5, "Sin configuración de costes", ha="center", va="center")
|
||||
ax.text(0.5, 0.5, "No cost configuration", ha="center", va="center")
|
||||
ax.set_axis_off()
|
||||
return ax
|
||||
|
||||
@@ -474,7 +470,7 @@ class EconomyCostMetrics:
|
||||
cpi_indexed = cpi_table.set_index(["queue_skill", "channel"])
|
||||
joined = cpi_indexed.join(volume, how="left").fillna({"volume": 0})
|
||||
|
||||
# CPI medio ponderado por canal
|
||||
# Weighted average CPI by channel
|
||||
per_channel = (
|
||||
joined.reset_index()
|
||||
.groupby("channel")
|
||||
@@ -486,9 +482,9 @@ class EconomyCostMetrics:
|
||||
fig, ax = plt.subplots(figsize=(6, 4))
|
||||
per_channel.plot(kind="bar", ax=ax)
|
||||
|
||||
ax.set_xlabel("Canal")
|
||||
ax.set_ylabel("CPI medio (€)")
|
||||
ax.set_title("Coste por interacción (CPI) por canal")
|
||||
ax.set_xlabel("Channel")
|
||||
ax.set_ylabel("Average CPI (€)")
|
||||
ax.set_title("Cost per interaction (CPI) by channel")
|
||||
ax.grid(axis="y", alpha=0.3)
|
||||
|
||||
return ax
|
||||
|
||||
Reference in New Issue
Block a user