La Fase 1 establece las bases analíticas del proyecto SIGA, capturando la realidad operativa actual, identificando patrones de pérdida y implementando mejoras rápidas que demuestran valor inmediato.
| Métrica | Target | Importancia |
|---|---|---|
| Decisiones capturadas | >95% | Crítica |
| Patrones identificados | Top 10 | Alta |
| Quick wins implementados | 3-5 | Alta |
| Ahorro demostrado | >€5,000/mes | Crítica |
| Adopción usuarios | >70% | Media |
# Setup inicial de base de datos
class DataCaptureSetup:
"""Configuración inicial para captura de datos"""
def __init__(self):
self.db_config = {
'host': 'siga-analytics.db',
'database': 'siga_operations',
'user': 'siga_admin',
'connection_pool_size': 20
}
def create_capture_schema(self):
"""Crea esquema para captura de decisiones"""
schema = """
-- Tabla principal de decisiones
CREATE TABLE IF NOT EXISTS decision_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
dispatcher_id VARCHAR(50) NOT NULL,
vehicle_id VARCHAR(20) NOT NULL,
current_position JSONB NOT NULL,
available_options JSONB NOT NULL,
decision_taken JSONB NOT NULL,
reasoning TEXT,
expected_outcome JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabla de resultados reales
CREATE TABLE IF NOT EXISTS decision_outcomes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
decision_id UUID REFERENCES decision_log(id),
actual_outcome JSONB NOT NULL,
variance_analysis JSONB,
captured_at TIMESTAMPTZ DEFAULT NOW()
);
-- Índices para análisis rápido
CREATE INDEX idx_decision_timestamp ON decision_log(timestamp);
CREATE INDEX idx_decision_vehicle ON decision_log(vehicle_id);
CREATE INDEX idx_decision_dispatcher ON decision_log(dispatcher_id);
"""
return schema
// Interfaz de captura en tiempo real
interface DecisionCapture {
id: string;
timestamp: Date;
context: {
vehicle: VehicleState;
dispatcher: DispatcherInfo;
marketConditions: MarketSnapshot;
};
options: DecisionOption[];
selected: DecisionOption;
reasoning: string;
expectedOutcome: ExpectedResult;
}
// Componente de logging simplificado
const DecisionLogger: React.FC = () => {
const [currentDecision, setCurrentDecision] = useState<DecisionCapture | null>(null);
const logDecision = async (decision: DecisionCapture) => {
// Validar completitud
if (!validateDecision(decision)) {
showError("Faltan datos para registrar decisión");
return;
}
// Enviar a backend
try {
await api.post('/decisions/log', decision);
showSuccess("Decisión registrada");
// Reset form
setCurrentDecision(null);
} catch (error) {
showError("Error al registrar decisión");
console.error(error);
}
};
return (
<Card title="Registrar Decisión">
<VehicleSelector onChange={handleVehicleSelect} />
<OptionsCapture onAdd={handleAddOption} />
<DecisionForm onSubmit={logDecision} />
</Card>
);
};
class SystemIntegration:
"""Integración con sistemas legacy de la empresa"""
def __init__(self):
self.connectors = {
'fleetbase': FleetbaseConnector(),
'tms': TMSConnector(),
'erp': ERPConnector()
}
async def sync_vehicle_positions(self):
"""Sincroniza posiciones actuales de vehículos"""
positions = {}
# Obtener de Fleetbase
fleet_data = await self.connectors['fleetbase'].get_vehicles()
for vehicle in fleet_data:
positions[vehicle['id']] = {
'lat': vehicle['last_position']['latitude'],
'lng': vehicle['last_position']['longitude'],
'status': vehicle['status'],
'last_update': vehicle['last_seen'],
'driver': vehicle['driver_id']
}
# Enriquecer con datos del TMS
tms_data = await self.connectors['tms'].get_active_trips()
for trip in tms_data:
if trip['vehicle_id'] in positions:
positions[trip['vehicle_id']]['current_load'] = {
'id': trip['load_id'],
'destination': trip['destination'],
'eta': trip['estimated_arrival']
}
return positions
def analyze_empty_kilometers():
"""Análisis detallado de patrones de km vacíos"""
query = """
WITH empty_trips AS (
SELECT
t.vehicle_id,
t.origin_zone,
t.destination_zone,
t.empty_km,
t.trip_date,
EXTRACT(DOW FROM t.trip_date) as day_of_week,
EXTRACT(HOUR FROM t.trip_start) as hour_of_day,
t.empty_km * c.cost_per_km as empty_cost
FROM trips t
JOIN cost_factors c ON t.vehicle_type = c.vehicle_type
WHERE t.empty_km > 0
AND t.trip_date >= CURRENT_DATE - INTERVAL '90 days'
)
SELECT
origin_zone,
destination_zone,
day_of_week,
COUNT(*) as frequency,
AVG(empty_km) as avg_empty_km,
SUM(empty_cost) as total_cost,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY empty_km) as median_km
FROM empty_trips
GROUP BY origin_zone, destination_zone, day_of_week
HAVING COUNT(*) >= 3 -- Mínimo 3 ocurrencias para ser patrón
ORDER BY total_cost DESC
LIMIT 50;
"""
patterns = db.execute(query)
# Análisis adicional
insights = []
for pattern in patterns:
insight = {
'route': f"{pattern['origin_zone']} → {pattern['destination_zone']}",
'pattern': identify_temporal_pattern(pattern),
'monthly_cost': pattern['total_cost'] * 4.33,
'recommendation': generate_recommendation(pattern),
'quick_win_potential': calculate_quick_win_potential(pattern)
}
insights.append(insight)
return insights
class QuickWinIdentifier:
"""Identifica e implementa mejoras rápidas"""
def identify_quick_wins(self, patterns: List[Pattern]) -> List[QuickWin]:
"""Encuentra oportunidades de mejora inmediata"""
quick_wins = []
# 1. Alertas automáticas para rutas problemáticas
problem_routes = self._find_problem_routes(patterns)
for route in problem_routes:
quick_win = QuickWin(
type='automatic_alert',
title=f"Alerta: No enviar vacío {route.origin}→{route.destination}",
implementation=self._create_alert_rule(route),
expected_saving=route.monthly_cost * 0.7,
effort='low',
risk='low'
)
quick_wins.append(quick_win)
# 2. Reglas de redistribución
imbalance_zones = self._find_imbalanced_zones(patterns)
for zone in imbalance_zones:
quick_win = QuickWin(
type='redistribution_rule',
title=f"Regla: Límite {zone.max_vehicles} camiones en {zone.name}",
implementation=self._create_redistribution_rule(zone),
expected_saving=zone.imbalance_cost * 0.5,
effort='low',
risk='medium'
)
quick_wins.append(quick_win)
# 3. Optimización de timing
timing_issues = self._find_timing_patterns(patterns)
for issue in timing_issues:
quick_win = QuickWin(
type='timing_optimization',
title=f"Optimizar: Salidas {issue.time_window} desde {issue.zone}",
implementation=self._create_timing_guide(issue),
expected_saving=issue.timing_cost * 0.6,
effort='medium',
risk='low'
)
quick_wins.append(quick_win)
# Priorizar por ROI
quick_wins.sort(key=lambda x: x.expected_saving / x.effort_score, reverse=True)
return quick_wins[:5] # Top 5 quick wins
def create_pattern_dashboard():
"""Crea visualización de patrones identificados"""
dashboard_config = {
'title': 'SIGA - Análisis de Patrones Fase 1',
'panels': [
{
'type': 'heatmap',
'title': 'Mapa de Calor - Km Vacíos',
'data_source': 'empty_km_by_route',
'size': 'large'
},
{
'type': 'time_series',
'title': 'Evolución Semanal',
'data_source': 'weekly_empty_km_trend',
'size': 'medium'
},
{
'type': 'top_list',
'title': 'Top 10 Rutas Problemáticas',
'data_source': 'problem_routes',
'size': 'medium'
},
{
'type': 'savings_tracker',
'title': 'Quick Wins - Ahorro Estimado',
'data_source': 'quick_wins_savings',
'size': 'small'
}
]
}
return dashboard_config
class AlertSystem:
"""Sistema de alertas para prevenir decisiones subóptimas"""
def __init__(self):
self.rules = []
self.notification_channels = ['email', 'sms', 'app']
def add_route_alert(self, origin: str, destination: str,
conditions: Dict, message: str):
"""Añade alerta para ruta específica"""
rule = AlertRule(
type='route_based',
trigger_conditions={
'origin_zone': origin,
'destination_zone': destination,
**conditions
},
message=message,
severity='high',
actions=['notify_dispatcher', 'suggest_alternatives']
)
self.rules.append(rule)
# Activar inmediatamente
self._activate_rule(rule)
def check_decision(self, decision: Decision) -> List[Alert]:
"""Verifica si una decisión activa alertas"""
triggered_alerts = []
for rule in self.rules:
if rule.evaluate(decision):
alert = Alert(
rule=rule,
decision=decision,
timestamp=datetime.now(),
message=rule.format_message(decision)
)
triggered_alerts.append(alert)
return triggered_alerts
// Configuración de reglas en la UI
const RuleConfiguration: React.FC = () => {
const [rules, setRules] = useState<BusinessRule[]>([]);
const addRule = (rule: BusinessRule) => {
// Validar regla
if (!validateBusinessRule(rule)) {
showError("Regla inválida");
return;
}
// Guardar en backend
api.post('/rules', rule)
.then(response => {
setRules([...rules, response.data]);
showSuccess(`Regla "${rule.name}" activada`);
})
.catch(error => {
showError("Error al crear regla");
});
};
return (
<div className="rule-configuration">
<h2>Configurar Reglas de Negocio</h2>
<RuleBuilder onSave={addRule} />
<ActiveRulesList
rules={rules}
onToggle={handleToggleRule}
onDelete={handleDeleteRule}
/>
<RuleImpactPreview rules={rules} />
</div>
);
};
class ImpactMeasurement:
"""Mide el impacto real de las mejoras implementadas"""
def measure_quick_win_impact(self, quick_win_id: str,
period_days: int = 14) -> ImpactReport:
"""Mide impacto de un quick win específico"""
# Obtener métricas antes/después
baseline = self._get_baseline_metrics(quick_win_id)
current = self._get_current_metrics(quick_win_id, period_days)
# Calcular mejoras
improvements = {
'empty_km_reduction': (
(baseline['avg_empty_km'] - current['avg_empty_km']) /
baseline['avg_empty_km'] * 100
),
'cost_savings': baseline['daily_cost'] - current['daily_cost'],
'decision_quality': current['optimal_decisions'] / current['total_decisions'],
'user_adoption': current['rules_followed'] / current['applicable_situations']
}
# Proyección mensual
monthly_projection = {
'savings': improvements['cost_savings'] * 30,
'km_saved': (baseline['avg_empty_km'] - current['avg_empty_km']) * 30,
'roi': (improvements['cost_savings'] * 30) / quick_win_cost * 100
}
return ImpactReport(
quick_win_id=quick_win_id,
period=period_days,
improvements=improvements,
projection=monthly_projection,
confidence=self._calculate_confidence(period_days, sample_size)
)
# Reporte Fase 1 - Análisis de Operaciones SIGA
## Resumen Ejecutivo
- Período analizado: 90 días
- Decisiones capturadas: 2,847
- Patrones identificados: 47
- Quick wins implementados: 5
- Ahorro mensual proyectado: €8,500
## Hallazgos Principales
### 1. Patrones de Kilómetros Vacíos
- 32% de km totales son vacíos
- Costo mensual: €45,000
- Principales rutas problema:
- Valencia → Barcelona (viernes tarde): €3,200/mes
- Madrid → Zaragoza (lunes mañana): €2,800/mes
- Sevilla → Málaga (general): €2,100/mes
### 2. Desequilibrios de Flota
- Lunes: 70% flota en zonas industriales
- Viernes: 65% flota en zonas urbanas
- Costo del desequilibrio: €12,000/mes
### 3. Decisiones Subóptimas
- 23% de decisiones podrían mejorarse
- Principal causa: falta de información
- Potencial de mejora: €15,000/mes
## Quick Wins Implementados
1. **Alerta Valencia-Barcelona Viernes**
- Ahorro: €2,240/mes
- Adopción: 95%
2. **Límite 3 camiones zona puerto**
- Ahorro: €1,800/mes
- Adopción: 88%
3. **Guía timing salidas Madrid**
- Ahorro: €1,500/mes
- Adopción: 72%
4. **Dashboard zonas calientes**
- Ahorro: €1,200/mes
- Adopción: 100%
5. **Checklist decisión post-entrega**
- Ahorro: €1,760/mes
- Adopción: 81%
## Próximos Pasos
- Continuar con Fase 2
- Expandir sistema de alertas
- Entrenar dispatchers en nuevas herramientas
# Estadísticas del sistema de captura
capture_stats = {
'total_decisions_logged': 2847,
'capture_rate': 0.96, # 96%
'avg_capture_time': 45, # segundos
'data_quality_score': 0.89,
'missing_fields_rate': 0.04,
'user_satisfaction': 4.2 # de 5
}
# API endpoints activos
endpoints = [
'POST /api/decisions/log',
'GET /api/decisions/search',
'GET /api/patterns/analysis',
'GET /api/quickwins/status',
'GET /api/metrics/dashboard'
]
┌─────────────────────────────────────────────────────────┐
│ QUICK WINS - PANEL DE CONTROL │
├─────────────────────────────────────────────────────────┤
│ │
│ Ahorro Total Mensual: €8,500 💰 │
│ │
│ Por Quick Win: │
│ ├─ 🚨 Alerta Valencia-BCN €2,240 ████████▌ 26% │
│ ├─ 📍 Límite zona puerto €1,800 ███████ 21% │
│ ├─ ⏰ Timing Madrid €1,500 ██████ 18% │
│ ├─ 🗺️ Dashboard zonas €1,200 █████ 14% │
│ └─ ✅ Checklist decisión €1,760 ███████ 21% │
│ │
│ Adopción Promedio: 87% 📈 │
│ ROI Fase 1: 283% 🚀 │
│ │
│ [Ver Detalles] [Exportar Reporte] [Siguiente Fase] │
└─────────────────────────────────────────────────────────┘
Decisión: ✅ PROCEDER A FASE 2
⬅️ Volver a Implementación | ➡️ Siguiente: Fase 2 - Herramientas