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:
Claude
2026-02-07 11:03:00 +00:00
parent 94178eaaae
commit 8c7f5fa827
5 changed files with 325 additions and 335 deletions

View File

@@ -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