feat: Implementar PWA, Analytics, Reportes PDF y mejoras OCR
FASE 1 - PWA y Frontend: - Crear templates/base.html, dashboard.html, analytics.html, executive.html - Crear static/css/main.css con diseño responsivo - Agregar static/js/app.js, pwa.js, camera.js, charts.js - Implementar manifest.json y service-worker.js para PWA - Soporte para captura de tickets desde cámara móvil FASE 2 - Analytics: - Crear módulo analytics/ con predictions.py, trends.py, comparisons.py - Implementar predicción básica con promedio móvil + tendencia lineal - Agregar endpoints /api/analytics/trends, predictions, comparisons - Integrar Chart.js para gráficas interactivas FASE 3 - Reportes PDF: - Crear módulo reports/ con pdf_generator.py - Implementar SalesReportPDF con generar_reporte_diario y ejecutivo - Agregar comando /reporte [diario|semanal|ejecutivo] - Agregar endpoints /api/reports/generate y /api/reports/download FASE 4 - Mejoras OCR: - Crear módulo ocr/ con processor.py, preprocessor.py, patterns.py - Implementar AmountDetector con patrones múltiples de montos - Agregar preprocesador adaptativo con pipelines para diferentes condiciones - Soporte para corrección de rotación (deskew) y threshold Otsu Dependencias agregadas: - reportlab, matplotlib (PDF) - scipy, pandas (analytics) - imutils, deskew, cachetools (OCR) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
241
sales-bot/analytics/predictions.py
Normal file
241
sales-bot/analytics/predictions.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
Sales prediction algorithms for Sales Bot
|
||||
Uses moving average and linear regression for basic predictions
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Try to import scipy for linear regression
|
||||
try:
|
||||
from scipy import stats
|
||||
import numpy as np
|
||||
SCIPY_AVAILABLE = True
|
||||
except ImportError:
|
||||
SCIPY_AVAILABLE = False
|
||||
logger.warning("scipy not available, using basic prediction methods")
|
||||
|
||||
|
||||
def prediccion_basica(ventas_diarias: List[float], dias_prediccion: int = 7) -> Dict:
|
||||
"""
|
||||
Genera predicciones combinando promedio móvil con tendencia lineal.
|
||||
|
||||
Args:
|
||||
ventas_diarias: Lista de montos de ventas por día
|
||||
dias_prediccion: Número de días a predecir
|
||||
|
||||
Returns:
|
||||
dict con predicciones, tendencia y confianza
|
||||
"""
|
||||
if not ventas_diarias or len(ventas_diarias) < 3:
|
||||
return {
|
||||
'predicciones': [],
|
||||
'next_day': 0,
|
||||
'next_week': 0,
|
||||
'tendencia': 'stable',
|
||||
'confidence': 0,
|
||||
'error': 'Datos insuficientes para predicción'
|
||||
}
|
||||
|
||||
try:
|
||||
# Calcular promedio móvil (últimos 7 días o menos si no hay suficientes)
|
||||
ventana = min(7, len(ventas_diarias))
|
||||
promedio_movil = sum(ventas_diarias[-ventana:]) / ventana
|
||||
|
||||
# Calcular tendencia
|
||||
if SCIPY_AVAILABLE:
|
||||
# Usar regresión lineal con scipy
|
||||
x = np.arange(len(ventas_diarias))
|
||||
y = np.array(ventas_diarias)
|
||||
slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)
|
||||
confianza = abs(r_value)
|
||||
else:
|
||||
# Método básico sin scipy
|
||||
slope, intercept, confianza = _calcular_regresion_simple(ventas_diarias)
|
||||
|
||||
# Generar predicciones
|
||||
predicciones = []
|
||||
for i in range(1, dias_prediccion + 1):
|
||||
# Predicción lineal
|
||||
pred_lineal = slope * (len(ventas_diarias) + i) + intercept
|
||||
|
||||
# Combinar promedio móvil con tendencia lineal
|
||||
pred_combinada = (promedio_movil + pred_lineal) / 2
|
||||
|
||||
# No permitir predicciones negativas
|
||||
predicciones.append(max(0, pred_combinada))
|
||||
|
||||
# Determinar tendencia
|
||||
if slope > 0.5:
|
||||
tendencia = 'increasing'
|
||||
elif slope < -0.5:
|
||||
tendencia = 'decreasing'
|
||||
else:
|
||||
tendencia = 'stable'
|
||||
|
||||
return {
|
||||
'predicciones': predicciones,
|
||||
'next_day': round(predicciones[0], 2) if predicciones else 0,
|
||||
'next_week': round(sum(predicciones[:7]), 2) if len(predicciones) >= 7 else round(sum(predicciones), 2),
|
||||
'tendencia': tendencia,
|
||||
'trend': tendencia, # Alias for frontend
|
||||
'confidence': round(confianza, 2),
|
||||
'promedio_movil': round(promedio_movil, 2),
|
||||
'slope': round(slope, 4)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error en predicción: {str(e)}")
|
||||
return {
|
||||
'predicciones': [],
|
||||
'next_day': 0,
|
||||
'next_week': 0,
|
||||
'tendencia': 'stable',
|
||||
'trend': 'stable',
|
||||
'confidence': 0,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
|
||||
def _calcular_regresion_simple(datos: List[float]) -> tuple:
|
||||
"""
|
||||
Calcula regresión lineal simple sin scipy.
|
||||
|
||||
Returns:
|
||||
(slope, intercept, r_value)
|
||||
"""
|
||||
n = len(datos)
|
||||
if n < 2:
|
||||
return 0, datos[0] if datos else 0, 0
|
||||
|
||||
x = list(range(n))
|
||||
y = datos
|
||||
|
||||
# Calcular medias
|
||||
x_mean = sum(x) / n
|
||||
y_mean = sum(y) / n
|
||||
|
||||
# Calcular slope
|
||||
numerador = sum((x[i] - x_mean) * (y[i] - y_mean) for i in range(n))
|
||||
denominador = sum((x[i] - x_mean) ** 2 for i in range(n))
|
||||
|
||||
if denominador == 0:
|
||||
return 0, y_mean, 0
|
||||
|
||||
slope = numerador / denominador
|
||||
intercept = y_mean - slope * x_mean
|
||||
|
||||
# Calcular R-squared (coeficiente de determinación)
|
||||
y_pred = [slope * xi + intercept for xi in x]
|
||||
ss_res = sum((y[i] - y_pred[i]) ** 2 for i in range(n))
|
||||
ss_tot = sum((y[i] - y_mean) ** 2 for i in range(n))
|
||||
|
||||
r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
|
||||
r_value = r_squared ** 0.5 if r_squared >= 0 else 0
|
||||
|
||||
return slope, intercept, r_value
|
||||
|
||||
|
||||
def calcular_tendencia(ventas_diarias: List[float]) -> Dict:
|
||||
"""
|
||||
Analiza la tendencia de los datos de ventas.
|
||||
|
||||
Returns:
|
||||
dict con información de tendencia
|
||||
"""
|
||||
if not ventas_diarias or len(ventas_diarias) < 2:
|
||||
return {
|
||||
'direccion': 'stable',
|
||||
'cambio_porcentual': 0,
|
||||
'volatilidad': 0
|
||||
}
|
||||
|
||||
# Dividir en dos mitades
|
||||
mitad = len(ventas_diarias) // 2
|
||||
primera_mitad = ventas_diarias[:mitad] if mitad > 0 else ventas_diarias[:1]
|
||||
segunda_mitad = ventas_diarias[mitad:] if mitad > 0 else ventas_diarias[1:]
|
||||
|
||||
promedio_primera = sum(primera_mitad) / len(primera_mitad) if primera_mitad else 0
|
||||
promedio_segunda = sum(segunda_mitad) / len(segunda_mitad) if segunda_mitad else 0
|
||||
|
||||
# Calcular cambio porcentual
|
||||
if promedio_primera > 0:
|
||||
cambio_pct = ((promedio_segunda - promedio_primera) / promedio_primera) * 100
|
||||
else:
|
||||
cambio_pct = 0
|
||||
|
||||
# Determinar dirección
|
||||
if cambio_pct > 5:
|
||||
direccion = 'increasing'
|
||||
elif cambio_pct < -5:
|
||||
direccion = 'decreasing'
|
||||
else:
|
||||
direccion = 'stable'
|
||||
|
||||
# Calcular volatilidad (desviación estándar relativa)
|
||||
media = sum(ventas_diarias) / len(ventas_diarias)
|
||||
if media > 0:
|
||||
varianza = sum((x - media) ** 2 for x in ventas_diarias) / len(ventas_diarias)
|
||||
desviacion = varianza ** 0.5
|
||||
volatilidad = (desviacion / media) * 100
|
||||
else:
|
||||
volatilidad = 0
|
||||
|
||||
return {
|
||||
'direccion': direccion,
|
||||
'cambio_porcentual': round(cambio_pct, 2),
|
||||
'volatilidad': round(volatilidad, 2),
|
||||
'promedio_anterior': round(promedio_primera, 2),
|
||||
'promedio_actual': round(promedio_segunda, 2)
|
||||
}
|
||||
|
||||
|
||||
def generar_prediccion_extendida(
|
||||
ventas_diarias: List[float],
|
||||
dias_historico: int = 30,
|
||||
dias_futuro: int = 14
|
||||
) -> Dict:
|
||||
"""
|
||||
Genera predicción extendida con más detalles.
|
||||
|
||||
Args:
|
||||
ventas_diarias: Datos históricos
|
||||
dias_historico: Días de historia a considerar
|
||||
dias_futuro: Días a predecir
|
||||
|
||||
Returns:
|
||||
dict con predicción completa
|
||||
"""
|
||||
# Limitar datos al histórico solicitado
|
||||
datos = ventas_diarias[-dias_historico:] if len(ventas_diarias) > dias_historico else ventas_diarias
|
||||
|
||||
# Obtener predicción básica
|
||||
prediccion = prediccion_basica(datos, dias_futuro)
|
||||
|
||||
# Agregar análisis de tendencia
|
||||
tendencia_info = calcular_tendencia(datos)
|
||||
|
||||
# Calcular estadísticas adicionales
|
||||
if datos:
|
||||
maximo = max(datos)
|
||||
minimo = min(datos)
|
||||
promedio = sum(datos) / len(datos)
|
||||
mejor_dia_idx = datos.index(maximo)
|
||||
else:
|
||||
maximo = minimo = promedio = 0
|
||||
mejor_dia_idx = 0
|
||||
|
||||
return {
|
||||
**prediccion,
|
||||
'tendencia_detalle': tendencia_info,
|
||||
'estadisticas': {
|
||||
'maximo': round(maximo, 2),
|
||||
'minimo': round(minimo, 2),
|
||||
'promedio': round(promedio, 2),
|
||||
'mejor_dia_indice': mejor_dia_idx,
|
||||
'dias_analizados': len(datos)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user