Files
BeyondCXAnalytics-Demo/backend/tests/test_volumetria.py
2026-02-04 11:08:21 +01:00

222 lines
6.0 KiB
Python

import math
from datetime import datetime
import matplotlib
import pandas as pd
from beyond_metrics.dimensions.Volumetria import VolumetriaMetrics
# Usamos backend "Agg" para que matplotlib no intente abrir ventanas
matplotlib.use("Agg")
def _sample_df() -> pd.DataFrame:
"""
DataFrame de prueba con el nuevo esquema de columnas:
Campos usados por VolumetriaMetrics:
- interaction_id
- datetime_start
- queue_skill
- channel
5 interacciones:
- 3 por canal "voz", 2 por canal "chat"
- 3 en skill "ventas", 2 en skill "soporte"
- 3 en enero, 2 en febrero
"""
data = [
{
"interaction_id": "id1",
"datetime_start": datetime(2024, 1, 1, 9, 0),
"queue_skill": "ventas",
"channel": "voz",
},
{
"interaction_id": "id2",
"datetime_start": datetime(2024, 1, 1, 9, 30),
"queue_skill": "ventas",
"channel": "voz",
},
{
"interaction_id": "id3",
"datetime_start": datetime(2024, 1, 1, 10, 0),
"queue_skill": "soporte",
"channel": "voz",
},
{
"interaction_id": "id4",
"datetime_start": datetime(2024, 2, 1, 10, 0),
"queue_skill": "ventas",
"channel": "chat",
},
{
"interaction_id": "id5",
"datetime_start": datetime(2024, 2, 2, 11, 0),
"queue_skill": "soporte",
"channel": "chat",
},
]
return pd.DataFrame(data)
# ----------------------------------------------------------------------
# VALIDACIÓN BÁSICA
# ----------------------------------------------------------------------
def test_init_validates_required_columns():
df = _sample_df()
# No debe lanzar error con las columnas por defecto
vm = VolumetriaMetrics(df)
assert not vm.is_empty
# Si falta alguna columna requerida, debe lanzar ValueError
for col in ["interaction_id", "datetime_start", "queue_skill", "channel"]:
df_missing = df.drop(columns=[col])
try:
VolumetriaMetrics(df_missing)
assert False, f"Debería fallar al faltar la columna: {col}"
except ValueError:
pass
# ----------------------------------------------------------------------
# VOLUMEN Y DISTRIBUCIONES
# ----------------------------------------------------------------------
def test_volume_by_channel_and_skill():
df = _sample_df()
vm = VolumetriaMetrics(df)
vol_channel = vm.volume_by_channel()
vol_skill = vm.volume_by_skill()
# Canales
assert vol_channel.sum() == len(df)
assert vol_channel["voz"] == 3
assert vol_channel["chat"] == 2
# Skills
assert vol_skill.sum() == len(df)
assert vol_skill["ventas"] == 3
assert vol_skill["soporte"] == 2
def test_channel_and_skill_distribution_pct():
df = _sample_df()
vm = VolumetriaMetrics(df)
dist_channel = vm.channel_distribution_pct()
dist_skill = vm.skill_distribution_pct()
# 3/5 = 60%, 2/5 = 40%
assert math.isclose(dist_channel["voz"], 60.0, rel_tol=1e-6)
assert math.isclose(dist_channel["chat"], 40.0, rel_tol=1e-6)
assert math.isclose(dist_skill["ventas"], 60.0, rel_tol=1e-6)
assert math.isclose(dist_skill["soporte"], 40.0, rel_tol=1e-6)
# ----------------------------------------------------------------------
# HEATMAP Y SAZONALIDAD
# ----------------------------------------------------------------------
def test_heatmap_24x7_shape_and_values():
df = _sample_df()
vm = VolumetriaMetrics(df)
heatmap = vm.heatmap_24x7()
# 7 días x 24 horas
assert heatmap.shape == (7, 24)
# Comprobamos algunas celdas concretas
# 2024-01-01 es lunes (dayofweek=0), llamadas a las 9h (2) y 10h (1)
assert heatmap.loc[0, 9] == 2
assert heatmap.loc[0, 10] == 1
# 2024-02-01 es jueves (dayofweek=3), 10h
assert heatmap.loc[3, 10] == 1
# 2024-02-02 es viernes (dayofweek=4), 11h
assert heatmap.loc[4, 11] == 1
def test_monthly_seasonality_cv():
df = _sample_df()
vm = VolumetriaMetrics(df)
cv = vm.monthly_seasonality_cv()
# Volumen mensual: [3, 2]
# mean = 2.5, std (ddof=1) ≈ 0.7071 -> CV ≈ 28.28%
assert math.isclose(cv, 28.28, rel_tol=1e-2)
def test_peak_offpeak_ratio():
df = _sample_df()
vm = VolumetriaMetrics(df)
ratio = vm.peak_offpeak_ratio()
# Horas pico definidas en la clase: 10-19
# Pico: 10h,10h,11h -> 3 interacciones
# Valle: 9h,9h -> 2 interacciones
# Ratio = 3/2 = 1.5
assert math.isclose(ratio, 1.5, rel_tol=1e-6)
def test_concentration_top20_skills_pct():
df = _sample_df()
vm = VolumetriaMetrics(df)
conc = vm.concentration_top20_skills_pct()
# Skills: ventas=3, soporte=2, total=5
# Top 20% de skills (ceil(0.2 * 2) = 1 skill) -> ventas=3
# 3/5 = 60%
assert math.isclose(conc, 60.0, rel_tol=1e-6)
# ----------------------------------------------------------------------
# CASO DATAFRAME VACÍO
# ----------------------------------------------------------------------
def test_empty_dataframe_behaviour():
df_empty = pd.DataFrame(
columns=["interaction_id", "datetime_start", "queue_skill", "channel"]
)
vm = VolumetriaMetrics(df_empty)
assert vm.is_empty
assert vm.volume_by_channel().empty
assert vm.volume_by_skill().empty
assert math.isnan(vm.monthly_seasonality_cv())
assert math.isnan(vm.peak_offpeak_ratio())
assert math.isnan(vm.concentration_top20_skills_pct())
# ----------------------------------------------------------------------
# PLOTS
# ----------------------------------------------------------------------
def test_plot_methods_return_axes():
df = _sample_df()
vm = VolumetriaMetrics(df)
ax1 = vm.plot_heatmap_24x7()
ax2 = vm.plot_channel_distribution()
ax3 = vm.plot_skill_pareto()
from matplotlib.axes import Axes
assert isinstance(ax1, Axes)
assert isinstance(ax2, Axes)
assert isinstance(ax3, Axes)