Initial commit - production deployment
This commit is contained in:
701
services/procurement/app/ml/supplier_performance_predictor.py
Normal file
701
services/procurement/app/ml/supplier_performance_predictor.py
Normal file
@@ -0,0 +1,701 @@
|
||||
"""
|
||||
Supplier Performance Predictor
|
||||
Predicts supplier reliability, delivery delays, and quality issues
|
||||
Generates insights for procurement risk management
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import structlog
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
from sklearn.ensemble import GradientBoostingClassifier, GradientBoostingRegressor
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class SupplierPerformancePredictor:
|
||||
"""
|
||||
Predicts supplier performance metrics for procurement risk management.
|
||||
|
||||
Capabilities:
|
||||
1. Delivery delay probability prediction
|
||||
2. Quality issue likelihood scoring
|
||||
3. Supplier reliability scoring (0-100)
|
||||
4. Alternative supplier recommendations
|
||||
5. Procurement risk assessment
|
||||
6. Insight generation for high-risk suppliers
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.delay_model = None
|
||||
self.quality_model = None
|
||||
self.reliability_scores = {}
|
||||
self.scaler = StandardScaler()
|
||||
self.feature_columns = []
|
||||
|
||||
async def analyze_supplier_performance(
|
||||
self,
|
||||
tenant_id: str,
|
||||
supplier_id: str,
|
||||
order_history: pd.DataFrame,
|
||||
min_orders: int = 10
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze historical supplier performance and generate insights.
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant identifier
|
||||
supplier_id: Supplier identifier
|
||||
order_history: Historical orders with columns:
|
||||
- order_date
|
||||
- expected_delivery_date
|
||||
- actual_delivery_date
|
||||
- order_quantity
|
||||
- received_quantity
|
||||
- quality_issues (bool)
|
||||
- quality_score (0-100)
|
||||
- order_value
|
||||
min_orders: Minimum orders required for analysis
|
||||
|
||||
Returns:
|
||||
Dictionary with performance metrics and insights
|
||||
"""
|
||||
logger.info(
|
||||
"Analyzing supplier performance",
|
||||
tenant_id=tenant_id,
|
||||
supplier_id=supplier_id,
|
||||
orders=len(order_history)
|
||||
)
|
||||
|
||||
if len(order_history) < min_orders:
|
||||
logger.warning(
|
||||
"Insufficient order history",
|
||||
supplier_id=supplier_id,
|
||||
orders=len(order_history),
|
||||
required=min_orders
|
||||
)
|
||||
return self._insufficient_data_response(tenant_id, supplier_id)
|
||||
|
||||
# Calculate performance metrics
|
||||
metrics = self._calculate_performance_metrics(order_history)
|
||||
|
||||
# Calculate reliability score
|
||||
reliability_score = self._calculate_reliability_score(metrics)
|
||||
|
||||
# Predict future performance
|
||||
predictions = self._predict_future_performance(order_history, metrics)
|
||||
|
||||
# Assess procurement risk
|
||||
risk_assessment = self._assess_procurement_risk(
|
||||
metrics, reliability_score, predictions
|
||||
)
|
||||
|
||||
# Generate insights
|
||||
insights = self._generate_supplier_insights(
|
||||
tenant_id, supplier_id, metrics, reliability_score,
|
||||
risk_assessment, predictions
|
||||
)
|
||||
|
||||
# Store reliability score
|
||||
self.reliability_scores[supplier_id] = reliability_score
|
||||
|
||||
logger.info(
|
||||
"Supplier performance analysis complete",
|
||||
supplier_id=supplier_id,
|
||||
reliability_score=reliability_score,
|
||||
insights_generated=len(insights)
|
||||
)
|
||||
|
||||
return {
|
||||
'tenant_id': tenant_id,
|
||||
'supplier_id': supplier_id,
|
||||
'analyzed_at': datetime.utcnow().isoformat(),
|
||||
'orders_analyzed': len(order_history),
|
||||
'metrics': metrics,
|
||||
'reliability_score': reliability_score,
|
||||
'predictions': predictions,
|
||||
'risk_assessment': risk_assessment,
|
||||
'insights': insights
|
||||
}
|
||||
|
||||
def _calculate_performance_metrics(
|
||||
self,
|
||||
order_history: pd.DataFrame
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate comprehensive supplier performance metrics.
|
||||
|
||||
Args:
|
||||
order_history: Historical order data
|
||||
|
||||
Returns:
|
||||
Dictionary of performance metrics
|
||||
"""
|
||||
# Ensure datetime columns
|
||||
order_history['order_date'] = pd.to_datetime(order_history['order_date'])
|
||||
order_history['expected_delivery_date'] = pd.to_datetime(order_history['expected_delivery_date'])
|
||||
order_history['actual_delivery_date'] = pd.to_datetime(order_history['actual_delivery_date'])
|
||||
|
||||
# Calculate delivery delays
|
||||
order_history['delivery_delay_days'] = (
|
||||
order_history['actual_delivery_date'] - order_history['expected_delivery_date']
|
||||
).dt.days
|
||||
|
||||
order_history['is_delayed'] = order_history['delivery_delay_days'] > 0
|
||||
order_history['is_early'] = order_history['delivery_delay_days'] < 0
|
||||
|
||||
# Calculate quantity accuracy
|
||||
order_history['quantity_accuracy'] = (
|
||||
order_history['received_quantity'] / order_history['order_quantity']
|
||||
)
|
||||
|
||||
order_history['is_short_delivery'] = order_history['quantity_accuracy'] < 1.0
|
||||
order_history['is_over_delivery'] = order_history['quantity_accuracy'] > 1.0
|
||||
|
||||
metrics = {
|
||||
# Delivery metrics
|
||||
'total_orders': int(len(order_history)),
|
||||
'on_time_orders': int((~order_history['is_delayed']).sum()),
|
||||
'delayed_orders': int(order_history['is_delayed'].sum()),
|
||||
'on_time_rate': float((~order_history['is_delayed']).mean() * 100),
|
||||
'avg_delivery_delay_days': float(order_history[order_history['is_delayed']]['delivery_delay_days'].mean()) if order_history['is_delayed'].any() else 0.0,
|
||||
'max_delivery_delay_days': int(order_history['delivery_delay_days'].max()),
|
||||
'delivery_delay_std': float(order_history['delivery_delay_days'].std()),
|
||||
|
||||
# Quantity accuracy metrics
|
||||
'avg_quantity_accuracy': float(order_history['quantity_accuracy'].mean() * 100),
|
||||
'short_deliveries': int(order_history['is_short_delivery'].sum()),
|
||||
'short_delivery_rate': float(order_history['is_short_delivery'].mean() * 100),
|
||||
|
||||
# Quality metrics
|
||||
'quality_issues': int(order_history['quality_issues'].sum()) if 'quality_issues' in order_history.columns else 0,
|
||||
'quality_issue_rate': float(order_history['quality_issues'].mean() * 100) if 'quality_issues' in order_history.columns else 0.0,
|
||||
'avg_quality_score': float(order_history['quality_score'].mean()) if 'quality_score' in order_history.columns else 100.0,
|
||||
|
||||
# Consistency metrics
|
||||
'delivery_consistency': float(100 - order_history['delivery_delay_days'].std() * 10), # Lower variance = higher consistency
|
||||
'quantity_consistency': float(100 - (order_history['quantity_accuracy'].std() * 100)),
|
||||
|
||||
# Recent trend (last 30 days vs overall)
|
||||
'recent_on_time_rate': self._calculate_recent_trend(order_history, 'is_delayed', days=30),
|
||||
|
||||
# Cost metrics
|
||||
'total_order_value': float(order_history['order_value'].sum()) if 'order_value' in order_history.columns else 0.0,
|
||||
'avg_order_value': float(order_history['order_value'].mean()) if 'order_value' in order_history.columns else 0.0
|
||||
}
|
||||
|
||||
# Ensure all metrics are valid (no NaN)
|
||||
for key, value in metrics.items():
|
||||
if isinstance(value, float) and np.isnan(value):
|
||||
metrics[key] = 0.0
|
||||
|
||||
return metrics
|
||||
|
||||
def _calculate_recent_trend(
|
||||
self,
|
||||
order_history: pd.DataFrame,
|
||||
metric_column: str,
|
||||
days: int = 30
|
||||
) -> float:
|
||||
"""Calculate recent trend for a metric."""
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
recent_orders = order_history[order_history['order_date'] >= cutoff_date]
|
||||
|
||||
if len(recent_orders) < 3:
|
||||
return 0.0 # Not enough recent data
|
||||
|
||||
if metric_column == 'is_delayed':
|
||||
return float((~recent_orders['is_delayed']).mean() * 100)
|
||||
else:
|
||||
return float(recent_orders[metric_column].mean() * 100)
|
||||
|
||||
def _calculate_reliability_score(
|
||||
self,
|
||||
metrics: Dict[str, Any]
|
||||
) -> int:
|
||||
"""
|
||||
Calculate overall supplier reliability score (0-100).
|
||||
|
||||
Factors:
|
||||
- On-time delivery rate (40%)
|
||||
- Quantity accuracy (20%)
|
||||
- Quality score (25%)
|
||||
- Consistency (15%)
|
||||
"""
|
||||
# On-time delivery score (40 points)
|
||||
on_time_score = metrics['on_time_rate'] * 0.40
|
||||
|
||||
# Quantity accuracy score (20 points)
|
||||
quantity_score = min(100, metrics['avg_quantity_accuracy']) * 0.20
|
||||
|
||||
# Quality score (25 points)
|
||||
quality_score = metrics['avg_quality_score'] * 0.25
|
||||
|
||||
# Consistency score (15 points)
|
||||
# Average of delivery and quantity consistency
|
||||
consistency_score = (
|
||||
(metrics['delivery_consistency'] + metrics['quantity_consistency']) / 2
|
||||
) * 0.15
|
||||
|
||||
total_score = on_time_score + quantity_score + quality_score + consistency_score
|
||||
|
||||
# Penalties
|
||||
# Severe penalty for high quality issue rate
|
||||
if metrics['quality_issue_rate'] > 10:
|
||||
total_score *= 0.8 # 20% penalty
|
||||
|
||||
# Penalty for high short delivery rate
|
||||
if metrics['short_delivery_rate'] > 15:
|
||||
total_score *= 0.9 # 10% penalty
|
||||
|
||||
return int(round(max(0, min(100, total_score))))
|
||||
|
||||
def _predict_future_performance(
|
||||
self,
|
||||
order_history: pd.DataFrame,
|
||||
metrics: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Predict future supplier performance based on trends.
|
||||
|
||||
Args:
|
||||
order_history: Historical order data
|
||||
metrics: Calculated performance metrics
|
||||
|
||||
Returns:
|
||||
Dictionary of predictions
|
||||
"""
|
||||
# Simple trend-based predictions
|
||||
# For production, could use ML models trained on multi-supplier data
|
||||
|
||||
predictions = {
|
||||
'next_order_delay_probability': 0.0,
|
||||
'next_order_quality_issue_probability': 0.0,
|
||||
'predicted_delivery_days': 0,
|
||||
'confidence': 0
|
||||
}
|
||||
|
||||
# Delay probability based on historical rate and recent trend
|
||||
historical_delay_rate = metrics['delayed_orders'] / max(1, metrics['total_orders'])
|
||||
recent_on_time_rate = metrics['recent_on_time_rate'] / 100
|
||||
|
||||
# Weight recent performance higher
|
||||
predicted_on_time_prob = (historical_delay_rate * 0.3) + ((1 - recent_on_time_rate) * 0.7)
|
||||
predictions['next_order_delay_probability'] = float(min(1.0, max(0.0, predicted_on_time_prob)))
|
||||
|
||||
# Quality issue probability
|
||||
if metrics['quality_issues'] > 0:
|
||||
quality_issue_prob = metrics['quality_issue_rate'] / 100
|
||||
predictions['next_order_quality_issue_probability'] = float(quality_issue_prob)
|
||||
|
||||
# Predicted delivery days (expected delay)
|
||||
if metrics['avg_delivery_delay_days'] > 0:
|
||||
predictions['predicted_delivery_days'] = int(round(metrics['avg_delivery_delay_days']))
|
||||
|
||||
# Confidence based on data quantity and recency
|
||||
if metrics['total_orders'] >= 50:
|
||||
predictions['confidence'] = 90
|
||||
elif metrics['total_orders'] >= 30:
|
||||
predictions['confidence'] = 80
|
||||
elif metrics['total_orders'] >= 20:
|
||||
predictions['confidence'] = 70
|
||||
else:
|
||||
predictions['confidence'] = 60
|
||||
|
||||
return predictions
|
||||
|
||||
def _assess_procurement_risk(
|
||||
self,
|
||||
metrics: Dict[str, Any],
|
||||
reliability_score: int,
|
||||
predictions: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Assess overall procurement risk for this supplier.
|
||||
|
||||
Risk levels: low, medium, high, critical
|
||||
"""
|
||||
risk_factors = []
|
||||
risk_score = 0 # 0-100, higher = more risky
|
||||
|
||||
# Low reliability
|
||||
if reliability_score < 60:
|
||||
risk_factors.append('Low reliability score')
|
||||
risk_score += 30
|
||||
elif reliability_score < 75:
|
||||
risk_factors.append('Medium reliability score')
|
||||
risk_score += 15
|
||||
|
||||
# High delay probability
|
||||
if predictions['next_order_delay_probability'] > 0.5:
|
||||
risk_factors.append('High delay probability')
|
||||
risk_score += 25
|
||||
elif predictions['next_order_delay_probability'] > 0.3:
|
||||
risk_factors.append('Moderate delay probability')
|
||||
risk_score += 15
|
||||
|
||||
# Quality issues
|
||||
if metrics['quality_issue_rate'] > 15:
|
||||
risk_factors.append('High quality issue rate')
|
||||
risk_score += 25
|
||||
elif metrics['quality_issue_rate'] > 5:
|
||||
risk_factors.append('Moderate quality issue rate')
|
||||
risk_score += 10
|
||||
|
||||
# Quantity accuracy issues
|
||||
if metrics['short_delivery_rate'] > 20:
|
||||
risk_factors.append('Frequent short deliveries')
|
||||
risk_score += 15
|
||||
elif metrics['short_delivery_rate'] > 10:
|
||||
risk_factors.append('Occasional short deliveries')
|
||||
risk_score += 8
|
||||
|
||||
# Low consistency
|
||||
if metrics['delivery_consistency'] < 60:
|
||||
risk_factors.append('Inconsistent delivery timing')
|
||||
risk_score += 10
|
||||
|
||||
# Determine risk level
|
||||
if risk_score >= 70:
|
||||
risk_level = 'critical'
|
||||
elif risk_score >= 50:
|
||||
risk_level = 'high'
|
||||
elif risk_score >= 30:
|
||||
risk_level = 'medium'
|
||||
else:
|
||||
risk_level = 'low'
|
||||
|
||||
return {
|
||||
'risk_level': risk_level,
|
||||
'risk_score': min(100, risk_score),
|
||||
'risk_factors': risk_factors,
|
||||
'recommendation': self._get_risk_recommendation(risk_level, risk_factors)
|
||||
}
|
||||
|
||||
def _get_risk_recommendation(
|
||||
self,
|
||||
risk_level: str,
|
||||
risk_factors: List[str]
|
||||
) -> str:
|
||||
"""Generate risk mitigation recommendation."""
|
||||
if risk_level == 'critical':
|
||||
return 'URGENT: Consider switching to alternative supplier. Current supplier poses significant operational risk.'
|
||||
elif risk_level == 'high':
|
||||
return 'HIGH PRIORITY: Increase safety stock and have backup supplier ready. Monitor closely.'
|
||||
elif risk_level == 'medium':
|
||||
return 'MONITOR: Keep standard safety stock. Review performance quarterly.'
|
||||
else:
|
||||
return 'LOW RISK: Supplier performing well. Maintain current relationship.'
|
||||
|
||||
def _generate_supplier_insights(
|
||||
self,
|
||||
tenant_id: str,
|
||||
supplier_id: str,
|
||||
metrics: Dict[str, Any],
|
||||
reliability_score: int,
|
||||
risk_assessment: Dict[str, Any],
|
||||
predictions: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate actionable insights for procurement team.
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
supplier_id: Supplier ID
|
||||
metrics: Performance metrics
|
||||
reliability_score: Overall reliability (0-100)
|
||||
risk_assessment: Risk assessment results
|
||||
predictions: Future performance predictions
|
||||
|
||||
Returns:
|
||||
List of insight dictionaries
|
||||
"""
|
||||
insights = []
|
||||
|
||||
# Insight 1: Low reliability alert
|
||||
if reliability_score < 60:
|
||||
insights.append({
|
||||
'type': 'alert',
|
||||
'priority': 'critical' if reliability_score < 50 else 'high',
|
||||
'category': 'procurement',
|
||||
'title': f'Low Supplier Reliability: {reliability_score}/100',
|
||||
'description': f'Supplier {supplier_id} has low reliability score of {reliability_score}. On-time rate: {metrics["on_time_rate"]:.1f}%, Quality: {metrics["avg_quality_score"]:.1f}. Consider alternative suppliers.',
|
||||
'impact_type': 'operational_risk',
|
||||
'impact_value': 100 - reliability_score,
|
||||
'impact_unit': 'risk_points',
|
||||
'confidence': 85,
|
||||
'metrics_json': {
|
||||
'supplier_id': supplier_id,
|
||||
'reliability_score': reliability_score,
|
||||
'on_time_rate': round(metrics['on_time_rate'], 2),
|
||||
'quality_score': round(metrics['avg_quality_score'], 2),
|
||||
'quality_issue_rate': round(metrics['quality_issue_rate'], 2),
|
||||
'delayed_orders': metrics['delayed_orders'],
|
||||
'total_orders': metrics['total_orders']
|
||||
},
|
||||
'actionable': True,
|
||||
'recommendation_actions': [
|
||||
{
|
||||
'label': 'Find Alternative Supplier',
|
||||
'action': 'search_alternative_suppliers',
|
||||
'params': {'current_supplier_id': supplier_id}
|
||||
},
|
||||
{
|
||||
'label': 'Increase Safety Stock',
|
||||
'action': 'adjust_safety_stock',
|
||||
'params': {'supplier_id': supplier_id, 'multiplier': 1.5}
|
||||
}
|
||||
],
|
||||
'source_service': 'procurement',
|
||||
'source_model': 'supplier_performance_predictor'
|
||||
})
|
||||
|
||||
# Insight 2: High delay probability
|
||||
if predictions['next_order_delay_probability'] > 0.4:
|
||||
delay_prob_pct = predictions['next_order_delay_probability'] * 100
|
||||
insights.append({
|
||||
'type': 'prediction',
|
||||
'priority': 'high' if delay_prob_pct > 60 else 'medium',
|
||||
'category': 'procurement',
|
||||
'title': f'High Delay Risk: {delay_prob_pct:.0f}% Probability',
|
||||
'description': f'Supplier {supplier_id} has {delay_prob_pct:.0f}% probability of delaying next order. Expected delay: {predictions["predicted_delivery_days"]} days. Plan accordingly.',
|
||||
'impact_type': 'operational_risk',
|
||||
'impact_value': delay_prob_pct,
|
||||
'impact_unit': 'probability_percent',
|
||||
'confidence': predictions['confidence'],
|
||||
'metrics_json': {
|
||||
'supplier_id': supplier_id,
|
||||
'delay_probability': round(delay_prob_pct, 2),
|
||||
'predicted_delay_days': predictions['predicted_delivery_days'],
|
||||
'historical_delay_rate': round(metrics['delayed_orders'] / max(1, metrics['total_orders']) * 100, 2),
|
||||
'avg_delay_days': round(metrics['avg_delivery_delay_days'], 2)
|
||||
},
|
||||
'actionable': True,
|
||||
'recommendation_actions': [
|
||||
{
|
||||
'label': 'Order Earlier',
|
||||
'action': 'adjust_order_lead_time',
|
||||
'params': {
|
||||
'supplier_id': supplier_id,
|
||||
'additional_days': predictions['predicted_delivery_days'] + 2
|
||||
}
|
||||
},
|
||||
{
|
||||
'label': 'Increase Safety Stock',
|
||||
'action': 'adjust_safety_stock',
|
||||
'params': {'supplier_id': supplier_id, 'multiplier': 1.3}
|
||||
}
|
||||
],
|
||||
'source_service': 'procurement',
|
||||
'source_model': 'supplier_performance_predictor'
|
||||
})
|
||||
|
||||
# Insight 3: Quality issues
|
||||
if metrics['quality_issue_rate'] > 10:
|
||||
insights.append({
|
||||
'type': 'alert',
|
||||
'priority': 'high',
|
||||
'category': 'procurement',
|
||||
'title': f'Quality Issues: {metrics["quality_issue_rate"]:.1f}% of Orders',
|
||||
'description': f'Supplier {supplier_id} has quality issues in {metrics["quality_issue_rate"]:.1f}% of orders ({metrics["quality_issues"]} of {metrics["total_orders"]}). This impacts product quality and customer satisfaction.',
|
||||
'impact_type': 'quality_risk',
|
||||
'impact_value': metrics['quality_issue_rate'],
|
||||
'impact_unit': 'percentage',
|
||||
'confidence': 90,
|
||||
'metrics_json': {
|
||||
'supplier_id': supplier_id,
|
||||
'quality_issue_rate': round(metrics['quality_issue_rate'], 2),
|
||||
'quality_issues': metrics['quality_issues'],
|
||||
'avg_quality_score': round(metrics['avg_quality_score'], 2)
|
||||
},
|
||||
'actionable': True,
|
||||
'recommendation_actions': [
|
||||
{
|
||||
'label': 'Review Supplier Quality',
|
||||
'action': 'schedule_supplier_review',
|
||||
'params': {'supplier_id': supplier_id, 'reason': 'quality_issues'}
|
||||
},
|
||||
{
|
||||
'label': 'Increase Inspection',
|
||||
'action': 'increase_quality_checks',
|
||||
'params': {'supplier_id': supplier_id}
|
||||
}
|
||||
],
|
||||
'source_service': 'procurement',
|
||||
'source_model': 'supplier_performance_predictor'
|
||||
})
|
||||
|
||||
# Insight 4: Excellent performance (positive insight)
|
||||
if reliability_score >= 90:
|
||||
insights.append({
|
||||
'type': 'insight',
|
||||
'priority': 'low',
|
||||
'category': 'procurement',
|
||||
'title': f'Excellent Supplier Performance: {reliability_score}/100',
|
||||
'description': f'Supplier {supplier_id} demonstrates excellent performance with {reliability_score} reliability score. On-time: {metrics["on_time_rate"]:.1f}%, Quality: {metrics["avg_quality_score"]:.1f}. Consider expanding partnership.',
|
||||
'impact_type': 'positive_performance',
|
||||
'impact_value': reliability_score,
|
||||
'impact_unit': 'score',
|
||||
'confidence': 90,
|
||||
'metrics_json': {
|
||||
'supplier_id': supplier_id,
|
||||
'reliability_score': reliability_score,
|
||||
'on_time_rate': round(metrics['on_time_rate'], 2),
|
||||
'quality_score': round(metrics['avg_quality_score'], 2)
|
||||
},
|
||||
'actionable': True,
|
||||
'recommendation_actions': [
|
||||
{
|
||||
'label': 'Increase Order Volume',
|
||||
'action': 'adjust_supplier_allocation',
|
||||
'params': {'supplier_id': supplier_id, 'increase_pct': 20}
|
||||
},
|
||||
{
|
||||
'label': 'Negotiate Better Terms',
|
||||
'action': 'initiate_negotiation',
|
||||
'params': {'supplier_id': supplier_id, 'reason': 'volume_increase'}
|
||||
}
|
||||
],
|
||||
'source_service': 'procurement',
|
||||
'source_model': 'supplier_performance_predictor'
|
||||
})
|
||||
|
||||
# Insight 5: Performance decline
|
||||
if metrics['recent_on_time_rate'] > 0 and metrics['recent_on_time_rate'] < metrics['on_time_rate'] - 15:
|
||||
insights.append({
|
||||
'type': 'alert',
|
||||
'priority': 'medium',
|
||||
'category': 'procurement',
|
||||
'title': 'Supplier Performance Decline Detected',
|
||||
'description': f'Supplier {supplier_id} recent performance ({metrics["recent_on_time_rate"]:.1f}% on-time) is significantly worse than historical average ({metrics["on_time_rate"]:.1f}%). Investigate potential issues.',
|
||||
'impact_type': 'performance_decline',
|
||||
'impact_value': metrics['on_time_rate'] - metrics['recent_on_time_rate'],
|
||||
'impact_unit': 'percentage_points',
|
||||
'confidence': 75,
|
||||
'metrics_json': {
|
||||
'supplier_id': supplier_id,
|
||||
'recent_on_time_rate': round(metrics['recent_on_time_rate'], 2),
|
||||
'historical_on_time_rate': round(metrics['on_time_rate'], 2),
|
||||
'decline': round(metrics['on_time_rate'] - metrics['recent_on_time_rate'], 2)
|
||||
},
|
||||
'actionable': True,
|
||||
'recommendation_actions': [
|
||||
{
|
||||
'label': 'Contact Supplier',
|
||||
'action': 'schedule_supplier_meeting',
|
||||
'params': {'supplier_id': supplier_id, 'reason': 'performance_decline'}
|
||||
},
|
||||
{
|
||||
'label': 'Monitor Closely',
|
||||
'action': 'increase_monitoring_frequency',
|
||||
'params': {'supplier_id': supplier_id}
|
||||
}
|
||||
],
|
||||
'source_service': 'procurement',
|
||||
'source_model': 'supplier_performance_predictor'
|
||||
})
|
||||
|
||||
logger.info(
|
||||
"Generated supplier insights",
|
||||
supplier_id=supplier_id,
|
||||
insights=len(insights)
|
||||
)
|
||||
|
||||
return insights
|
||||
|
||||
def _insufficient_data_response(
|
||||
self,
|
||||
tenant_id: str,
|
||||
supplier_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Return response when insufficient data available."""
|
||||
return {
|
||||
'tenant_id': tenant_id,
|
||||
'supplier_id': supplier_id,
|
||||
'analyzed_at': datetime.utcnow().isoformat(),
|
||||
'orders_analyzed': 0,
|
||||
'metrics': {},
|
||||
'reliability_score': None,
|
||||
'predictions': {},
|
||||
'risk_assessment': {
|
||||
'risk_level': 'unknown',
|
||||
'risk_score': None,
|
||||
'risk_factors': ['Insufficient historical data'],
|
||||
'recommendation': 'Collect more order history before assessing supplier performance.'
|
||||
},
|
||||
'insights': []
|
||||
}
|
||||
|
||||
def compare_suppliers(
|
||||
self,
|
||||
suppliers_analysis: List[Dict[str, Any]],
|
||||
product_category: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Compare multiple suppliers and provide recommendations.
|
||||
|
||||
Args:
|
||||
suppliers_analysis: List of supplier analysis results
|
||||
product_category: Optional product category filter
|
||||
|
||||
Returns:
|
||||
Comparison report with recommendations
|
||||
"""
|
||||
if not suppliers_analysis:
|
||||
return {'error': 'No suppliers to compare'}
|
||||
|
||||
# Sort by reliability score
|
||||
ranked_suppliers = sorted(
|
||||
suppliers_analysis,
|
||||
key=lambda x: x.get('reliability_score', 0),
|
||||
reverse=True
|
||||
)
|
||||
|
||||
comparison = {
|
||||
'analyzed_at': datetime.utcnow().isoformat(),
|
||||
'suppliers_compared': len(ranked_suppliers),
|
||||
'product_category': product_category,
|
||||
'top_supplier': ranked_suppliers[0]['supplier_id'],
|
||||
'top_supplier_score': ranked_suppliers[0]['reliability_score'],
|
||||
'bottom_supplier': ranked_suppliers[-1]['supplier_id'],
|
||||
'bottom_supplier_score': ranked_suppliers[-1]['reliability_score'],
|
||||
'ranked_suppliers': [
|
||||
{
|
||||
'supplier_id': s['supplier_id'],
|
||||
'reliability_score': s['reliability_score'],
|
||||
'risk_level': s['risk_assessment']['risk_level']
|
||||
}
|
||||
for s in ranked_suppliers
|
||||
],
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
# Generate comparison insights
|
||||
if len(ranked_suppliers) >= 2:
|
||||
score_gap = ranked_suppliers[0]['reliability_score'] - ranked_suppliers[-1]['reliability_score']
|
||||
|
||||
if score_gap > 30:
|
||||
comparison['recommendations'].append({
|
||||
'recommendation': f'Consider consolidating orders with top supplier {ranked_suppliers[0]["supplier_id"]} (score: {ranked_suppliers[0]["reliability_score"]})',
|
||||
'reason': f'Significant performance gap ({score_gap} points) from lowest performer'
|
||||
})
|
||||
|
||||
# Check for high-risk suppliers
|
||||
high_risk = [s for s in ranked_suppliers if s['risk_assessment']['risk_level'] in ['high', 'critical']]
|
||||
if high_risk:
|
||||
comparison['recommendations'].append({
|
||||
'recommendation': f'URGENT: Replace {len(high_risk)} high-risk supplier(s)',
|
||||
'reason': 'Significant operational risk from unreliable suppliers',
|
||||
'affected_suppliers': [s['supplier_id'] for s in high_risk]
|
||||
})
|
||||
|
||||
return comparison
|
||||
|
||||
def get_supplier_reliability_score(self, supplier_id: str) -> Optional[int]:
|
||||
"""Get cached reliability score for a supplier."""
|
||||
return self.reliability_scores.get(supplier_id)
|
||||
Reference in New Issue
Block a user