Ajustes en la conexión front-back y eliminación datos demo
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
"class": "beyond_metrics.dimensions.SatisfactionExperience.SatisfactionExperienceMetrics",
|
||||
"enabled": true,
|
||||
"metrics": [
|
||||
"csat_global",
|
||||
"csat_avg_by_skill_channel",
|
||||
"nps_avg_by_skill_channel",
|
||||
"ces_avg_by_skill_channel",
|
||||
|
||||
@@ -7,7 +7,7 @@ import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.axes import Axes
|
||||
|
||||
import math
|
||||
|
||||
REQUIRED_COLUMNS_OP: List[str] = [
|
||||
"interaction_id",
|
||||
@@ -165,21 +165,57 @@ class OperationalPerformanceMetrics:
|
||||
# ------------------------------------------------------------------ #
|
||||
def fcr_rate(self) -> float:
|
||||
"""
|
||||
FCR = % de interacciones resueltas en el primer contacto.
|
||||
FCR proxy = 100 - escalation_rate.
|
||||
|
||||
Definido como % de filas con is_resolved == True.
|
||||
Si la columna no existe, devuelve NaN.
|
||||
Usamos la métrica de escalación ya calculada a partir de transfer_flag.
|
||||
Si no se puede calcular escalation_rate, intentamos derivarlo
|
||||
directamente de la columna transfer_flag. Si todo falla, devolvemos NaN.
|
||||
"""
|
||||
try:
|
||||
esc = self.escalation_rate()
|
||||
except Exception:
|
||||
esc = float("nan")
|
||||
|
||||
# Si escalation_rate es válido, usamos el proxy simple
|
||||
if esc is not None and not math.isnan(esc):
|
||||
fcr = 100.0 - esc
|
||||
return float(max(0.0, min(100.0, round(fcr, 2))))
|
||||
|
||||
# Fallback: calcular directamente desde transfer_flag
|
||||
df = self.df
|
||||
if "is_resolved" not in df.columns:
|
||||
if "transfer_flag" not in df.columns or len(df) == 0:
|
||||
return float("nan")
|
||||
|
||||
col = df["transfer_flag"]
|
||||
|
||||
# Normalizar a booleano: TRUE/FALSE, 1/0, etc.
|
||||
if col.dtype == "O":
|
||||
col_norm = (
|
||||
col.astype(str)
|
||||
.str.strip()
|
||||
.str.lower()
|
||||
.map({
|
||||
"true": True,
|
||||
"t": True,
|
||||
"1": True,
|
||||
"yes": True,
|
||||
"y": True,
|
||||
})
|
||||
).fillna(False)
|
||||
transfer_mask = col_norm
|
||||
else:
|
||||
transfer_mask = pd.to_numeric(col, errors="coerce").fillna(0) > 0
|
||||
|
||||
total = len(df)
|
||||
if total == 0:
|
||||
transfers = int(transfer_mask.sum())
|
||||
|
||||
esc_rate = transfers / total if total > 0 else float("nan")
|
||||
if math.isnan(esc_rate):
|
||||
return float("nan")
|
||||
|
||||
resolved = df["is_resolved"].sum()
|
||||
return float(round(resolved / total * 100, 2))
|
||||
fcr = 100.0 - esc_rate * 100.0
|
||||
return float(max(0.0, min(100.0, round(fcr, 2))))
|
||||
|
||||
|
||||
def escalation_rate(self) -> float:
|
||||
"""
|
||||
|
||||
@@ -160,6 +160,26 @@ class SatisfactionExperienceMetrics:
|
||||
.round(2)
|
||||
)
|
||||
return pivot
|
||||
|
||||
def csat_global(self) -> float:
|
||||
"""
|
||||
CSAT medio global (todas las interacciones).
|
||||
|
||||
Usa la columna opcional `csat_score`:
|
||||
- Si no existe, devuelve NaN.
|
||||
- Si todos los valores son NaN / vacíos, devuelve NaN.
|
||||
"""
|
||||
df = self.df
|
||||
if "csat_score" not in df.columns:
|
||||
return float("nan")
|
||||
|
||||
series = pd.to_numeric(df["csat_score"], errors="coerce").dropna()
|
||||
if series.empty:
|
||||
return float("nan")
|
||||
|
||||
mean = series.mean()
|
||||
return float(round(mean, 2))
|
||||
|
||||
|
||||
def csat_aht_correlation(self) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
1001
backend/data/example/call_center_dataset.csv
Normal file
1001
backend/data/example/call_center_dataset.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -58,7 +58,8 @@ const DataInputRedesigned: React.FC<DataInputRedesignedProps> = ({
|
||||
{ name: 'wrap_up_time', type: 'Segundos', example: '30', required: true },
|
||||
{ name: 'agent_id', type: 'String', example: 'Agente_045', required: true },
|
||||
{ name: 'transfer_flag', type: 'Boolean', example: 'TRUE / FALSE', required: true },
|
||||
{ name: 'caller_id', type: 'String (hash)', example: 'Hash_99283', required: false }
|
||||
{ name: 'caller_id', type: 'String (hash)', example: 'Hash_99283', required: false },
|
||||
{ name: 'csat_score', type: 'Float', example: '4', required: false }
|
||||
];
|
||||
|
||||
const handleDownloadTemplate = () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
export type TierKey = 'gold' | 'silver' | 'bronze';
|
||||
export type AnalysisSource = 'synthetic' | 'backend' | 'fallback';
|
||||
|
||||
export interface Tier {
|
||||
name: string;
|
||||
@@ -269,4 +270,5 @@ export interface AnalysisData {
|
||||
benchmarkData: BenchmarkDataPoint[]; // Actualizado de benchmarkReport
|
||||
agenticReadiness?: AgenticReadinessResult; // v2.0: Nuevo campo
|
||||
staticConfig?: StaticConfig; // v2.0: Configuración estática usada
|
||||
source?: AnalysisSource;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ import { RoadmapPhase } from '../types';
|
||||
import { BarChartHorizontal, Zap, Smile, DollarSign, Target, Globe } from 'lucide-react';
|
||||
import { calculateAgenticReadinessScore, type AgenticReadinessInput } from './agenticReadinessV2';
|
||||
import { callAnalysisApiRaw } from './apiClient';
|
||||
import { mapBackendResultsToAnalysisData } from './backendMapper';
|
||||
import {
|
||||
mapBackendResultsToAnalysisData,
|
||||
buildHeatmapFromBackend,
|
||||
} from './backendMapper';
|
||||
|
||||
|
||||
|
||||
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
@@ -568,8 +572,18 @@ export const generateAnalysis = async (
|
||||
mapped.recommendations = generateRecommendationsFromTemplates();
|
||||
mapped.opportunities = generateOpportunityMatrixData();
|
||||
mapped.roadmap = generateRoadmapData();
|
||||
mapped.benchmarkData = generateBenchmarkData();
|
||||
mapped.heatmapData = generateHeatmapData(costPerHour, avgCsat, segmentMapping);
|
||||
|
||||
// Benchmark: de momento no tenemos datos reales -> no lo generamos en modo backend
|
||||
mapped.benchmarkData = [];
|
||||
|
||||
// Heatmap: ahora se construye a partir de datos reales del backend
|
||||
mapped.heatmapData = buildHeatmapFromBackend(
|
||||
raw,
|
||||
costPerHour,
|
||||
avgCsat,
|
||||
segmentMapping
|
||||
);
|
||||
|
||||
|
||||
console.log('✅ Usando resultados del backend mapeados + findings/benchmark del frontend');
|
||||
return mapped;
|
||||
@@ -734,6 +748,7 @@ const generateSyntheticAnalysis = (
|
||||
economicModel: generateEconomicModelData(),
|
||||
roadmap: generateRoadmapData(),
|
||||
benchmarkData: generateBenchmarkData(),
|
||||
source: 'synthetic',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
} from '../types';
|
||||
import type { BackendRawResults } from './apiClient';
|
||||
import { BarChartHorizontal, Zap, DollarSign } from 'lucide-react';
|
||||
import type { HeatmapDataPoint, CustomerSegment } from '../types';
|
||||
|
||||
|
||||
function safeNumber(value: any, fallback = 0): number {
|
||||
const n = typeof value === 'number' ? value : Number(value);
|
||||
@@ -577,17 +579,67 @@ export function mapBackendResultsToAnalysisData(
|
||||
if (performanceDimension) dimensions.push(performanceDimension);
|
||||
if (economyDimension) dimensions.push(economyDimension);
|
||||
|
||||
// KPIs de resumen
|
||||
const op = raw?.operational_performance;
|
||||
const cs = raw?.customer_satisfaction;
|
||||
|
||||
// FCR: viene ya como porcentaje 0-100
|
||||
const fcrPctRaw = safeNumber(op?.fcr_rate, NaN);
|
||||
const fcrPct =
|
||||
Number.isFinite(fcrPctRaw) && fcrPctRaw >= 0
|
||||
? Math.min(100, Math.max(0, fcrPctRaw))
|
||||
: undefined;
|
||||
|
||||
const csatAvg = computeCsatAverage(cs);
|
||||
|
||||
// CSAT global (opcional)
|
||||
const csatGlobalRaw = safeNumber(cs?.csat_global, NaN);
|
||||
const csatGlobal =
|
||||
Number.isFinite(csatGlobalRaw) && csatGlobalRaw > 0
|
||||
? csatGlobalRaw
|
||||
: undefined;
|
||||
|
||||
|
||||
// KPIs de resumen (los 4 primeros son los que se ven en "Métricas de Contacto")
|
||||
const summaryKpis: Kpi[] = [];
|
||||
|
||||
// 1) Interacciones Totales (volumen backend)
|
||||
summaryKpis.push({
|
||||
label: 'Volumen total (estimado)',
|
||||
label: 'Interacciones Totales',
|
||||
value:
|
||||
totalVolume > 0
|
||||
? totalVolume.toLocaleString('es-ES')
|
||||
: 'N/A',
|
||||
: 'N/D',
|
||||
});
|
||||
|
||||
// 2) AHT Promedio (P50 de distribución de AHT)
|
||||
const ahtP50 = safeNumber(op?.aht_distribution?.p50, 0);
|
||||
summaryKpis.push({
|
||||
label: 'AHT Promedio',
|
||||
value: ahtP50
|
||||
? `${Math.round(ahtP50)}s`
|
||||
: 'N/D',
|
||||
});
|
||||
|
||||
// 3) Tasa FCR
|
||||
summaryKpis.push({
|
||||
label: 'Tasa FCR',
|
||||
value:
|
||||
fcrPct !== undefined
|
||||
? `${Math.round(fcrPct)}%`
|
||||
: 'N/D',
|
||||
});
|
||||
|
||||
// 4) CSAT
|
||||
summaryKpis.push({
|
||||
label: 'CSAT',
|
||||
value:
|
||||
csatGlobal !== undefined
|
||||
? `${csatGlobal.toFixed(1)}/5`
|
||||
: 'N/D',
|
||||
});
|
||||
|
||||
// --- KPIs adicionales, usados en otras secciones ---
|
||||
|
||||
if (numChannels > 0) {
|
||||
summaryKpis.push({
|
||||
label: 'Canales analizados',
|
||||
@@ -607,7 +659,7 @@ export function mapBackendResultsToAnalysisData(
|
||||
value: `${arScore.toFixed(1)}/10`,
|
||||
});
|
||||
|
||||
// KPIs de economía
|
||||
// KPIs de economía (backend)
|
||||
const econ = raw?.economy_costs;
|
||||
const totalAnnual = safeNumber(
|
||||
econ?.cost_breakdown?.total_annual,
|
||||
@@ -649,5 +701,277 @@ export function mapBackendResultsToAnalysisData(
|
||||
benchmarkData: [],
|
||||
agenticReadiness,
|
||||
staticConfig: undefined,
|
||||
source: 'backend',
|
||||
};
|
||||
}
|
||||
|
||||
export function buildHeatmapFromBackend(
|
||||
raw: BackendRawResults,
|
||||
costPerHour: number,
|
||||
avgCsat: number,
|
||||
segmentMapping?: {
|
||||
high_value_queues: string[];
|
||||
medium_value_queues: string[];
|
||||
low_value_queues: string[];
|
||||
}
|
||||
): HeatmapDataPoint[] {
|
||||
const volumetry = raw?.volumetry;
|
||||
const volumeBySkill = volumetry?.volume_by_skill;
|
||||
|
||||
const rawSkillLabels =
|
||||
volumeBySkill?.labels ??
|
||||
volumeBySkill?.skills ??
|
||||
volumeBySkill?.skill_names ??
|
||||
[];
|
||||
|
||||
const skillLabels: string[] = Array.isArray(rawSkillLabels)
|
||||
? rawSkillLabels.map((s: any) => String(s))
|
||||
: [];
|
||||
|
||||
const skillVolumes: number[] = Array.isArray(volumeBySkill?.values)
|
||||
? volumeBySkill.values.map((v: any) => safeNumber(v, 0))
|
||||
: [];
|
||||
|
||||
const op = raw?.operational_performance;
|
||||
const econ = raw?.economy_costs;
|
||||
const cs = raw?.customer_satisfaction;
|
||||
|
||||
const talkHoldAcwBySkill = Array.isArray(
|
||||
op?.talk_hold_acw_p50_by_skill
|
||||
)
|
||||
? op.talk_hold_acw_p50_by_skill
|
||||
: [];
|
||||
|
||||
const globalEscalation = safeNumber(op?.escalation_rate, 0);
|
||||
const globalFcrPct = Math.max(
|
||||
0,
|
||||
Math.min(100, 100 - globalEscalation)
|
||||
);
|
||||
|
||||
const csatGlobalRaw = safeNumber(cs?.csat_global, NaN);
|
||||
const csatGlobal =
|
||||
Number.isFinite(csatGlobalRaw) && csatGlobalRaw > 0
|
||||
? csatGlobalRaw
|
||||
: undefined;
|
||||
const csatMetric0_100 = csatGlobal
|
||||
? Math.max(
|
||||
0,
|
||||
Math.min(100, Math.round((csatGlobal / 5) * 100))
|
||||
)
|
||||
: 0;
|
||||
|
||||
const ineffBySkill = Array.isArray(
|
||||
econ?.inefficiency_cost_by_skill_channel
|
||||
)
|
||||
? econ.inefficiency_cost_by_skill_channel
|
||||
: [];
|
||||
|
||||
const COST_PER_SECOND = costPerHour / 3600;
|
||||
|
||||
if (!skillLabels.length) return [];
|
||||
|
||||
// Para normalizar la repetitividad según volumen
|
||||
const volumesForNorm = skillVolumes.filter((v) => v > 0);
|
||||
const minVol =
|
||||
volumesForNorm.length > 0
|
||||
? Math.min(...volumesForNorm)
|
||||
: 0;
|
||||
const maxVol =
|
||||
volumesForNorm.length > 0
|
||||
? Math.max(...volumesForNorm)
|
||||
: 0;
|
||||
|
||||
const heatmap: HeatmapDataPoint[] = [];
|
||||
|
||||
for (let i = 0; i < skillLabels.length; i++) {
|
||||
const skill = skillLabels[i];
|
||||
const volume = safeNumber(skillVolumes[i], 0);
|
||||
|
||||
const talkHold = talkHoldAcwBySkill[i] || {};
|
||||
const talk_p50 = safeNumber(talkHold.talk_p50, 0);
|
||||
const hold_p50 = safeNumber(talkHold.hold_p50, 0);
|
||||
const acw_p50 = safeNumber(talkHold.acw_p50, 0);
|
||||
|
||||
const aht_mean = talk_p50 + hold_p50 + acw_p50;
|
||||
|
||||
// Coste anual aproximado
|
||||
const annual_volume = volume * 12;
|
||||
const annual_cost = Math.round(
|
||||
annual_volume * aht_mean * COST_PER_SECOND
|
||||
);
|
||||
|
||||
const ineff = ineffBySkill[i] || {};
|
||||
const aht_p50_backend = safeNumber(ineff.aht_p50, aht_mean);
|
||||
const aht_p90_backend = safeNumber(ineff.aht_p90, aht_mean);
|
||||
|
||||
// Variabilidad proxy: aproximamos CV a partir de P90-P50
|
||||
let cv_aht = 0;
|
||||
if (aht_p50_backend > 0) {
|
||||
cv_aht =
|
||||
(aht_p90_backend - aht_p50_backend) / aht_p50_backend;
|
||||
}
|
||||
|
||||
// Dimensiones agentic similares a las que tenías en generateHeatmapData,
|
||||
// pero usando valores reales en lugar de aleatorios.
|
||||
|
||||
// 1) Predictibilidad (menor CV => mayor puntuación)
|
||||
const predictability_score = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
10,
|
||||
10 - ((cv_aht - 0.3) / 1.2) * 10
|
||||
)
|
||||
);
|
||||
|
||||
// 2) Complejidad inversa (usamos la tasa global de escalación como proxy)
|
||||
const transfer_rate = globalEscalation; // %
|
||||
const complexity_inverse_score = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
10,
|
||||
10 - ((transfer_rate / 100 - 0.05) / 0.25) * 10
|
||||
)
|
||||
);
|
||||
|
||||
// 3) Repetitividad (según volumen relativo)
|
||||
let repetitivity_score = 5;
|
||||
if (maxVol > minVol && volume > 0) {
|
||||
repetitivity_score =
|
||||
((volume - minVol) / (maxVol - minVol)) * 10;
|
||||
} else if (volume === 0) {
|
||||
repetitivity_score = 0;
|
||||
}
|
||||
|
||||
const agentic_readiness_score =
|
||||
predictability_score * 0.4 +
|
||||
complexity_inverse_score * 0.35 +
|
||||
repetitivity_score * 0.25;
|
||||
|
||||
let readiness_category:
|
||||
| 'automate_now'
|
||||
| 'assist_copilot'
|
||||
| 'optimize_first';
|
||||
if (agentic_readiness_score >= 8.0) {
|
||||
readiness_category = 'automate_now';
|
||||
} else if (agentic_readiness_score >= 5.0) {
|
||||
readiness_category = 'assist_copilot';
|
||||
} else {
|
||||
readiness_category = 'optimize_first';
|
||||
}
|
||||
|
||||
const automation_readiness = Math.round(
|
||||
agentic_readiness_score * 10
|
||||
); // 0-100
|
||||
|
||||
// Métricas normalizadas 0-100 para el color del heatmap
|
||||
const ahtMetric = aht_mean
|
||||
? Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
100,
|
||||
Math.round(
|
||||
100 - ((aht_mean - 240) / 310) * 100
|
||||
)
|
||||
)
|
||||
)
|
||||
: 0;
|
||||
|
||||
const holdMetric = hold_p50
|
||||
? Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
100,
|
||||
Math.round(
|
||||
100 - (hold_p50 / 120) * 100
|
||||
)
|
||||
)
|
||||
)
|
||||
: 0;
|
||||
|
||||
const transferMetric = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
100,
|
||||
Math.round(100 - transfer_rate)
|
||||
)
|
||||
);
|
||||
|
||||
// Clasificación por segmento (si nos pasan mapeo)
|
||||
let segment: CustomerSegment | undefined;
|
||||
if (segmentMapping) {
|
||||
const normalizedSkill = skill.toLowerCase();
|
||||
if (
|
||||
segmentMapping.high_value_queues.some((q) =>
|
||||
normalizedSkill.includes(q.toLowerCase())
|
||||
)
|
||||
) {
|
||||
segment = 'high';
|
||||
} else if (
|
||||
segmentMapping.low_value_queues.some((q) =>
|
||||
normalizedSkill.includes(q.toLowerCase())
|
||||
)
|
||||
) {
|
||||
segment = 'low';
|
||||
} else {
|
||||
segment = 'medium';
|
||||
}
|
||||
}
|
||||
|
||||
heatmap.push({
|
||||
skill,
|
||||
segment,
|
||||
volume,
|
||||
aht_seconds: aht_mean,
|
||||
metrics: {
|
||||
fcr: Math.round(globalFcrPct),
|
||||
aht: ahtMetric,
|
||||
csat: csatMetric0_100,
|
||||
hold_time: holdMetric,
|
||||
transfer_rate: transferMetric,
|
||||
},
|
||||
annual_cost,
|
||||
variability: {
|
||||
cv_aht: Math.round(cv_aht * 100), // %
|
||||
cv_talk_time: 0,
|
||||
cv_hold_time: 0,
|
||||
transfer_rate,
|
||||
},
|
||||
automation_readiness,
|
||||
dimensions: {
|
||||
predictability: Math.round(predictability_score * 10) / 10,
|
||||
complexity_inverse:
|
||||
Math.round(complexity_inverse_score * 10) / 10,
|
||||
repetitivity: Math.round(repetitivity_score * 10) / 10,
|
||||
},
|
||||
readiness_category,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('📊 Heatmap backend generado:', {
|
||||
length: heatmap.length,
|
||||
firstItem: heatmap[0],
|
||||
});
|
||||
|
||||
return heatmap;
|
||||
}
|
||||
|
||||
function computeCsatAverage(customerSatisfaction: any): number | undefined {
|
||||
const arr = customerSatisfaction?.csat_avg_by_skill_channel;
|
||||
if (!Array.isArray(arr) || !arr.length) return undefined;
|
||||
|
||||
const values: number[] = arr
|
||||
.map((item: any) =>
|
||||
safeNumber(
|
||||
item?.csat ??
|
||||
item?.value ??
|
||||
item?.score,
|
||||
NaN
|
||||
)
|
||||
)
|
||||
.filter((v) => Number.isFinite(v));
|
||||
|
||||
if (!values.length) return undefined;
|
||||
|
||||
const sum = values.reduce((a, b) => a + b, 0);
|
||||
return sum / values.length;
|
||||
}
|
||||
|
||||
8
notas.md
8
notas.md
@@ -9,4 +9,10 @@ python -m uvicorn beyond_api.main:app --reload --port 8000
|
||||
|
||||
|
||||
# Frontend
|
||||
npm run dev
|
||||
npm run dev
|
||||
|
||||
# Siguientes pasos: que revise todo el código y quitar todo lo random para que utilice datos reales
|
||||
# Comparar los sintéticos con la demo y ver que ofrecen los mismos datos. Faltan cosas
|
||||
# Hacer que funcione de alguna manera el selector de JSON
|
||||
# Dockerizar
|
||||
# Limpieza de código
|
||||
Reference in New Issue
Block a user