Improve the frontend 3

This commit is contained in:
Urtzi Alfaro
2025-10-30 21:08:07 +01:00
parent 36217a2729
commit 63f5c6d512
184 changed files with 21512 additions and 7442 deletions

View File

@@ -0,0 +1,168 @@
"""
Circuit Breaker Pattern Implementation
Prevents cascading failures by stopping requests to failing services
and allowing them time to recover.
"""
import asyncio
import time
from enum import Enum
from typing import Callable, Any, Optional
from datetime import datetime, timedelta
import logging
logger = logging.getLogger(__name__)
class CircuitState(str, Enum):
"""Circuit breaker states"""
CLOSED = "closed" # Normal operation
OPEN = "open" # Circuit is open, requests fail immediately
HALF_OPEN = "half_open" # Testing if service has recovered
class CircuitBreakerOpenError(Exception):
"""Raised when circuit breaker is open"""
pass
class CircuitBreaker:
"""
Circuit Breaker implementation for protecting service calls.
States:
- CLOSED: Normal operation, requests pass through
- OPEN: Too many failures, requests fail immediately
- HALF_OPEN: Testing recovery, limited requests allowed
Args:
failure_threshold: Number of failures before opening circuit
timeout_duration: Seconds to wait before attempting recovery
success_threshold: Successful calls needed in HALF_OPEN to close circuit
expected_exceptions: Tuple of exceptions that count as failures
"""
def __init__(
self,
failure_threshold: int = 5,
timeout_duration: int = 60,
success_threshold: int = 2,
expected_exceptions: tuple = (Exception,)
):
self.failure_threshold = failure_threshold
self.timeout_duration = timeout_duration
self.success_threshold = success_threshold
self.expected_exceptions = expected_exceptions
self._state = CircuitState.CLOSED
self._failure_count = 0
self._success_count = 0
self._last_failure_time: Optional[datetime] = None
self._next_attempt_time: Optional[datetime] = None
@property
def state(self) -> CircuitState:
"""Get current circuit state"""
if self._state == CircuitState.OPEN and self._should_attempt_reset():
self._state = CircuitState.HALF_OPEN
self._success_count = 0
logger.info(f"Circuit breaker entering HALF_OPEN state")
return self._state
def _should_attempt_reset(self) -> bool:
"""Check if enough time has passed to attempt reset"""
if self._next_attempt_time is None:
return False
return datetime.now() >= self._next_attempt_time
async def call(self, func: Callable, *args, **kwargs) -> Any:
"""
Execute function with circuit breaker protection.
Args:
func: Function to execute
*args: Positional arguments for func
**kwargs: Keyword arguments for func
Returns:
Result of func execution
Raises:
CircuitBreakerOpenError: If circuit is open
Exception: Original exception from func if circuit is closed
"""
if self.state == CircuitState.OPEN:
raise CircuitBreakerOpenError(
f"Circuit breaker is OPEN. Next attempt at {self._next_attempt_time}"
)
try:
# Execute the function
if asyncio.iscoroutinefunction(func):
result = await func(*args, **kwargs)
else:
result = func(*args, **kwargs)
# Success
self._on_success()
return result
except self.expected_exceptions as e:
# Expected failure
self._on_failure()
raise
def _on_success(self):
"""Handle successful call"""
if self._state == CircuitState.HALF_OPEN:
self._success_count += 1
if self._success_count >= self.success_threshold:
self._close_circuit()
else:
# In CLOSED state, reset failure count on success
self._failure_count = 0
def _on_failure(self):
"""Handle failed call"""
self._failure_count += 1
self._last_failure_time = datetime.now()
if self._state == CircuitState.HALF_OPEN:
# Failure in HALF_OPEN returns to OPEN
self._open_circuit()
elif self._failure_count >= self.failure_threshold:
# Too many failures, open the circuit
self._open_circuit()
def _open_circuit(self):
"""Open the circuit"""
self._state = CircuitState.OPEN
self._next_attempt_time = datetime.now() + timedelta(seconds=self.timeout_duration)
logger.warning(
f"Circuit breaker opened after {self._failure_count} failures. "
f"Next attempt at {self._next_attempt_time}"
)
def _close_circuit(self):
"""Close the circuit"""
self._state = CircuitState.CLOSED
self._failure_count = 0
self._success_count = 0
self._next_attempt_time = None
logger.info(f"Circuit breaker closed after successful recovery")
def reset(self):
"""Manually reset circuit breaker to CLOSED state"""
self._close_circuit()
logger.info(f"Circuit breaker manually reset")
def get_stats(self) -> dict:
"""Get circuit breaker statistics"""
return {
"state": self.state.value,
"failure_count": self._failure_count,
"success_count": self._success_count,
"last_failure_time": self._last_failure_time.isoformat() if self._last_failure_time else None,
"next_attempt_time": self._next_attempt_time.isoformat() if self._next_attempt_time else None
}

View File

@@ -0,0 +1,438 @@
"""
Optimization Utilities
Provides optimization algorithms for procurement planning including
MOQ rounding, economic order quantity, and multi-objective optimization.
"""
import math
from decimal import Decimal
from typing import List, Tuple, Dict, Optional
from dataclasses import dataclass
@dataclass
class OrderOptimizationResult:
"""Result of order quantity optimization"""
optimal_quantity: Decimal
order_cost: Decimal
holding_cost: Decimal
total_cost: Decimal
orders_per_year: float
reasoning: str
def calculate_economic_order_quantity(
annual_demand: float,
ordering_cost: float,
holding_cost_per_unit: float
) -> float:
"""
Calculate Economic Order Quantity (EOQ).
EOQ = sqrt((2 × D × S) / H)
where:
- D = Annual demand
- S = Ordering cost per order
- H = Holding cost per unit per year
Args:
annual_demand: Annual demand in units
ordering_cost: Cost per order placement
holding_cost_per_unit: Annual holding cost per unit
Returns:
Optimal order quantity
"""
if annual_demand <= 0 or ordering_cost <= 0 or holding_cost_per_unit <= 0:
return 0.0
eoq = math.sqrt((2 * annual_demand * ordering_cost) / holding_cost_per_unit)
return eoq
def optimize_order_quantity(
required_quantity: Decimal,
annual_demand: float,
ordering_cost: float = 50.0,
holding_cost_rate: float = 0.25,
unit_price: float = 1.0,
min_order_qty: Optional[Decimal] = None,
max_order_qty: Optional[Decimal] = None
) -> OrderOptimizationResult:
"""
Optimize order quantity considering EOQ and constraints.
Args:
required_quantity: Quantity needed for current period
annual_demand: Estimated annual demand
ordering_cost: Fixed cost per order
holding_cost_rate: Annual holding cost as % of unit price
unit_price: Cost per unit
min_order_qty: Minimum order quantity (MOQ)
max_order_qty: Maximum order quantity (storage limit)
Returns:
OrderOptimizationResult with optimal quantity and costs
"""
holding_cost_per_unit = unit_price * holding_cost_rate
# Calculate EOQ
eoq = calculate_economic_order_quantity(
annual_demand,
ordering_cost,
holding_cost_per_unit
)
# Start with EOQ or required quantity, whichever is larger
optimal_qty = max(float(required_quantity), eoq)
reasoning = f"Base EOQ: {eoq:.2f}, Required: {required_quantity}"
# Apply minimum order quantity
if min_order_qty and Decimal(optimal_qty) < min_order_qty:
optimal_qty = float(min_order_qty)
reasoning += f", Applied MOQ: {min_order_qty}"
# Apply maximum order quantity
if max_order_qty and Decimal(optimal_qty) > max_order_qty:
optimal_qty = float(max_order_qty)
reasoning += f", Capped at max: {max_order_qty}"
# Calculate costs
orders_per_year = annual_demand / optimal_qty if optimal_qty > 0 else 0
annual_ordering_cost = orders_per_year * ordering_cost
annual_holding_cost = (optimal_qty / 2) * holding_cost_per_unit
total_annual_cost = annual_ordering_cost + annual_holding_cost
return OrderOptimizationResult(
optimal_quantity=Decimal(str(optimal_qty)),
order_cost=Decimal(str(annual_ordering_cost)),
holding_cost=Decimal(str(annual_holding_cost)),
total_cost=Decimal(str(total_annual_cost)),
orders_per_year=orders_per_year,
reasoning=reasoning
)
def round_to_moq(
quantity: Decimal,
moq: Decimal,
round_up: bool = True
) -> Decimal:
"""
Round quantity to meet minimum order quantity.
Args:
quantity: Desired quantity
moq: Minimum order quantity
round_up: If True, always round up to next MOQ multiple
Returns:
Rounded quantity
"""
if quantity <= 0 or moq <= 0:
return quantity
if quantity < moq:
return moq
# Calculate how many MOQs needed
multiples = quantity / moq
if round_up:
return Decimal(math.ceil(float(multiples))) * moq
else:
return Decimal(round(float(multiples))) * moq
def round_to_package_size(
quantity: Decimal,
package_size: Decimal,
allow_partial: bool = False
) -> Decimal:
"""
Round quantity to package size.
Args:
quantity: Desired quantity
package_size: Size of one package
allow_partial: If False, always round up to full packages
Returns:
Rounded quantity
"""
if quantity <= 0 or package_size <= 0:
return quantity
packages_needed = quantity / package_size
if allow_partial:
return quantity
else:
return Decimal(math.ceil(float(packages_needed))) * package_size
def apply_price_tier_optimization(
base_quantity: Decimal,
unit_price: Decimal,
price_tiers: List[Dict]
) -> Tuple[Decimal, Decimal, str]:
"""
Optimize quantity to take advantage of price tiers.
Args:
base_quantity: Base quantity needed
unit_price: Current unit price
price_tiers: List of dicts with 'min_quantity' and 'unit_price'
Returns:
Tuple of (optimized_quantity, unit_price, reasoning)
"""
if not price_tiers:
return base_quantity, unit_price, "No price tiers available"
# Sort tiers by min_quantity
sorted_tiers = sorted(price_tiers, key=lambda x: x['min_quantity'])
# Calculate cost at base quantity
base_cost = base_quantity * unit_price
# Find current tier
current_tier_price = unit_price
for tier in sorted_tiers:
if base_quantity >= Decimal(str(tier['min_quantity'])):
current_tier_price = Decimal(str(tier['unit_price']))
# Check if moving to next tier would save money
best_quantity = base_quantity
best_price = current_tier_price
best_savings = Decimal('0')
reasoning = f"Current tier price: ${current_tier_price}"
for tier in sorted_tiers:
tier_min_qty = Decimal(str(tier['min_quantity']))
tier_price = Decimal(str(tier['unit_price']))
if tier_min_qty > base_quantity:
# Calculate cost at this tier
tier_cost = tier_min_qty * tier_price
# Calculate savings
savings = base_cost - tier_cost
if savings > best_savings:
# Additional quantity needed
additional_qty = tier_min_qty - base_quantity
# Check if savings justify additional inventory
# Simple heuristic: savings should be > 10% of additional cost
additional_cost = additional_qty * tier_price
if savings > additional_cost * Decimal('0.1'):
best_quantity = tier_min_qty
best_price = tier_price
best_savings = savings
reasoning = f"Upgraded to tier {tier_min_qty}+ for ${savings:.2f} savings"
return best_quantity, best_price, reasoning
def aggregate_requirements_for_moq(
requirements: List[Dict],
moq: Decimal
) -> List[Dict]:
"""
Aggregate multiple requirements to meet MOQ efficiently.
Args:
requirements: List of requirement dicts with 'quantity' and 'date'
moq: Minimum order quantity
Returns:
List of aggregated orders
"""
if not requirements:
return []
# Sort requirements by date
sorted_reqs = sorted(requirements, key=lambda x: x['date'])
orders = []
current_batch = []
current_total = Decimal('0')
for req in sorted_reqs:
req_qty = Decimal(str(req['quantity']))
# Check if adding this requirement would exceed reasonable aggregation
# (e.g., don't aggregate more than 30 days worth)
if current_batch:
days_span = (req['date'] - current_batch[0]['date']).days
if days_span > 30:
# Finalize current batch
if current_total > 0:
orders.append({
'quantity': round_to_moq(current_total, moq),
'date': current_batch[0]['date'],
'requirements': current_batch.copy()
})
current_batch = []
current_total = Decimal('0')
current_batch.append(req)
current_total += req_qty
# If we've met MOQ, finalize this batch
if current_total >= moq:
orders.append({
'quantity': round_to_moq(current_total, moq),
'date': current_batch[0]['date'],
'requirements': current_batch.copy()
})
current_batch = []
current_total = Decimal('0')
# Handle remaining requirements
if current_batch:
orders.append({
'quantity': round_to_moq(current_total, moq),
'date': current_batch[0]['date'],
'requirements': current_batch
})
return orders
def calculate_order_splitting(
total_quantity: Decimal,
suppliers: List[Dict],
max_supplier_capacity: Optional[Decimal] = None
) -> List[Dict]:
"""
Split large order across multiple suppliers.
Args:
total_quantity: Total quantity needed
suppliers: List of supplier dicts with 'id', 'capacity', 'reliability'
max_supplier_capacity: Maximum any single supplier should provide
Returns:
List of allocations with 'supplier_id' and 'quantity'
"""
if not suppliers:
return []
# Sort suppliers by reliability (descending)
sorted_suppliers = sorted(
suppliers,
key=lambda x: x.get('reliability', 0.5),
reverse=True
)
allocations = []
remaining = total_quantity
for supplier in sorted_suppliers:
if remaining <= 0:
break
supplier_capacity = Decimal(str(supplier.get('capacity', float('inf'))))
# Apply max capacity constraint
if max_supplier_capacity:
supplier_capacity = min(supplier_capacity, max_supplier_capacity)
# Allocate to this supplier
allocated = min(remaining, supplier_capacity)
allocations.append({
'supplier_id': supplier['id'],
'quantity': allocated,
'reliability': supplier.get('reliability', 0.5)
})
remaining -= allocated
# If still remaining, distribute across suppliers
if remaining > 0:
# Distribute remaining proportionally to reliability
total_reliability = sum(s.get('reliability', 0.5) for s in sorted_suppliers)
for i, supplier in enumerate(sorted_suppliers):
if total_reliability > 0:
proportion = supplier.get('reliability', 0.5) / total_reliability
additional = remaining * Decimal(str(proportion))
allocations[i]['quantity'] += additional
return allocations
def calculate_buffer_stock(
lead_time_days: int,
daily_demand: float,
demand_variability: float,
service_level: float = 0.95
) -> Decimal:
"""
Calculate buffer stock based on demand variability.
Buffer Stock = Z × σ × √(lead_time)
where:
- Z = service level z-score
- σ = demand standard deviation
- lead_time = lead time in days
Args:
lead_time_days: Supplier lead time in days
daily_demand: Average daily demand
demand_variability: Coefficient of variation (CV = σ/μ)
service_level: Target service level (0-1)
Returns:
Buffer stock quantity
"""
if lead_time_days <= 0 or daily_demand <= 0:
return Decimal('0')
# Z-scores for common service levels
z_scores = {
0.90: 1.28,
0.95: 1.65,
0.975: 1.96,
0.99: 2.33,
0.995: 2.58
}
# Get z-score for service level
z_score = z_scores.get(service_level, 1.65) # Default to 95%
# Calculate standard deviation
stddev = daily_demand * demand_variability
# Buffer stock formula
buffer = z_score * stddev * math.sqrt(lead_time_days)
return Decimal(str(buffer))
def calculate_reorder_point(
daily_demand: float,
lead_time_days: int,
safety_stock: Decimal
) -> Decimal:
"""
Calculate reorder point.
Reorder Point = (Daily Demand × Lead Time) + Safety Stock
Args:
daily_demand: Average daily demand
lead_time_days: Supplier lead time in days
safety_stock: Safety stock quantity
Returns:
Reorder point
"""
lead_time_demand = Decimal(str(daily_demand * lead_time_days))
return lead_time_demand + safety_stock

View File

@@ -0,0 +1,293 @@
"""
Saga Pattern Implementation
Provides distributed transaction coordination with compensation logic
for microservices architecture.
"""
import asyncio
import uuid
from typing import Callable, List, Dict, Any, Optional, Tuple
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
import logging
logger = logging.getLogger(__name__)
class SagaStepStatus(str, Enum):
"""Status of a saga step"""
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
FAILED = "failed"
COMPENSATING = "compensating"
COMPENSATED = "compensated"
class SagaStatus(str, Enum):
"""Overall saga status"""
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
FAILED = "failed"
COMPENSATING = "compensating"
COMPENSATED = "compensated"
@dataclass
class SagaStep:
"""
A single step in a saga with compensation logic.
Args:
name: Human-readable step name
action: Async function to execute
compensation: Async function to undo the action
action_args: Arguments for the action function
action_kwargs: Keyword arguments for the action function
"""
name: str
action: Callable
compensation: Optional[Callable] = None
action_args: tuple = field(default_factory=tuple)
action_kwargs: dict = field(default_factory=dict)
# Runtime state
status: SagaStepStatus = SagaStepStatus.PENDING
result: Any = None
error: Optional[Exception] = None
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
@dataclass
class SagaExecution:
"""Tracks execution state of a saga"""
saga_id: str
status: SagaStatus = SagaStatus.PENDING
steps: List[SagaStep] = field(default_factory=list)
current_step: int = 0
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
error: Optional[Exception] = None
class SagaCoordinator:
"""
Coordinates saga execution with automatic compensation on failure.
Example:
```python
saga = SagaCoordinator()
saga.add_step(
"create_order",
action=create_order,
compensation=delete_order,
action_args=(order_data,)
)
saga.add_step(
"reserve_inventory",
action=reserve_inventory,
compensation=release_inventory,
action_args=(order_id, items)
)
result = await saga.execute()
```
"""
def __init__(self, saga_id: Optional[str] = None):
self.execution = SagaExecution(
saga_id=saga_id or str(uuid.uuid4())
)
self._completed_steps: List[SagaStep] = []
def add_step(
self,
name: str,
action: Callable,
compensation: Optional[Callable] = None,
action_args: tuple = (),
action_kwargs: dict = None
):
"""
Add a step to the saga.
Args:
name: Human-readable step name
action: Async function to execute
compensation: Async function to undo the action (optional)
action_args: Arguments for the action function
action_kwargs: Keyword arguments for the action function
"""
step = SagaStep(
name=name,
action=action,
compensation=compensation,
action_args=action_args,
action_kwargs=action_kwargs or {}
)
self.execution.steps.append(step)
logger.debug(f"Added step '{name}' to saga {self.execution.saga_id}")
async def execute(self) -> Tuple[bool, Optional[Any], Optional[Exception]]:
"""
Execute all saga steps in sequence.
Returns:
Tuple of (success: bool, final_result: Any, error: Optional[Exception])
"""
self.execution.status = SagaStatus.IN_PROGRESS
self.execution.started_at = datetime.now()
logger.info(
f"Starting saga {self.execution.saga_id} with {len(self.execution.steps)} steps"
)
try:
# Execute each step
for idx, step in enumerate(self.execution.steps):
self.execution.current_step = idx
success = await self._execute_step(step)
if not success:
# Step failed, trigger compensation
logger.error(
f"Saga {self.execution.saga_id} failed at step '{step.name}': {step.error}"
)
await self._compensate()
self.execution.status = SagaStatus.COMPENSATED
self.execution.completed_at = datetime.now()
self.execution.error = step.error
return False, None, step.error
# Step succeeded
self._completed_steps.append(step)
# All steps completed successfully
self.execution.status = SagaStatus.COMPLETED
self.execution.completed_at = datetime.now()
# Return the result of the last step
final_result = self.execution.steps[-1].result if self.execution.steps else None
logger.info(f"Saga {self.execution.saga_id} completed successfully")
return True, final_result, None
except Exception as e:
logger.exception(f"Unexpected error in saga {self.execution.saga_id}: {e}")
await self._compensate()
self.execution.status = SagaStatus.FAILED
self.execution.completed_at = datetime.now()
self.execution.error = e
return False, None, e
async def _execute_step(self, step: SagaStep) -> bool:
"""
Execute a single saga step.
Returns:
True if step succeeded, False otherwise
"""
step.status = SagaStepStatus.IN_PROGRESS
step.started_at = datetime.now()
logger.info(f"Executing saga step '{step.name}'")
try:
# Execute the action
if asyncio.iscoroutinefunction(step.action):
result = await step.action(*step.action_args, **step.action_kwargs)
else:
result = step.action(*step.action_args, **step.action_kwargs)
step.result = result
step.status = SagaStepStatus.COMPLETED
step.completed_at = datetime.now()
logger.info(f"Saga step '{step.name}' completed successfully")
return True
except Exception as e:
step.error = e
step.status = SagaStepStatus.FAILED
step.completed_at = datetime.now()
logger.error(f"Saga step '{step.name}' failed: {e}")
return False
async def _compensate(self):
"""
Execute compensation logic for all completed steps in reverse order.
"""
if not self._completed_steps:
logger.info(f"No steps to compensate for saga {self.execution.saga_id}")
return
self.execution.status = SagaStatus.COMPENSATING
logger.info(
f"Starting compensation for saga {self.execution.saga_id} "
f"({len(self._completed_steps)} steps to compensate)"
)
# Compensate in reverse order
for step in reversed(self._completed_steps):
if step.compensation is None:
logger.warning(
f"Step '{step.name}' has no compensation function, skipping"
)
continue
step.status = SagaStepStatus.COMPENSATING
try:
logger.info(f"Compensating step '{step.name}'")
# Execute compensation with the result from the original action
compensation_args = (step.result,) if step.result is not None else ()
if asyncio.iscoroutinefunction(step.compensation):
await step.compensation(*compensation_args)
else:
step.compensation(*compensation_args)
step.status = SagaStepStatus.COMPENSATED
logger.info(f"Step '{step.name}' compensated successfully")
except Exception as e:
logger.error(f"Failed to compensate step '{step.name}': {e}")
# Continue compensating other steps even if one fails
logger.info(f"Compensation completed for saga {self.execution.saga_id}")
def get_execution_summary(self) -> Dict[str, Any]:
"""Get summary of saga execution"""
return {
"saga_id": self.execution.saga_id,
"status": self.execution.status.value,
"total_steps": len(self.execution.steps),
"current_step": self.execution.current_step,
"completed_steps": len(self._completed_steps),
"started_at": self.execution.started_at.isoformat() if self.execution.started_at else None,
"completed_at": self.execution.completed_at.isoformat() if self.execution.completed_at else None,
"error": str(self.execution.error) if self.execution.error else None,
"steps": [
{
"name": step.name,
"status": step.status.value,
"has_compensation": step.compensation is not None,
"error": str(step.error) if step.error else None
}
for step in self.execution.steps
]
}

View File

@@ -0,0 +1,536 @@
"""
Time Series Utilities
Provides utilities for time-series analysis, projection, and calculations
used in forecasting and inventory planning.
"""
import statistics
from datetime import date, datetime, timedelta
from typing import List, Dict, Tuple, Optional
from decimal import Decimal
import math
def generate_date_range(
start_date: date,
end_date: date,
include_end: bool = True
) -> List[date]:
"""
Generate a list of dates between start and end.
Args:
start_date: Start date (inclusive)
end_date: End date
include_end: Whether to include end date
Returns:
List of dates
"""
dates = []
current = start_date
while current < end_date or (include_end and current == end_date):
dates.append(current)
current += timedelta(days=1)
return dates
def generate_future_dates(
start_date: date,
num_days: int
) -> List[date]:
"""
Generate a list of future dates starting from start_date.
Args:
start_date: Starting date
num_days: Number of days to generate
Returns:
List of dates
"""
return [start_date + timedelta(days=i) for i in range(num_days)]
def calculate_moving_average(
values: List[float],
window_size: int
) -> List[float]:
"""
Calculate moving average over a window.
Args:
values: List of values
window_size: Size of moving window
Returns:
List of moving averages
"""
if len(values) < window_size:
return []
moving_averages = []
for i in range(len(values) - window_size + 1):
window = values[i:i + window_size]
moving_averages.append(sum(window) / window_size)
return moving_averages
def calculate_standard_deviation(values: List[float]) -> float:
"""
Calculate standard deviation of values.
Args:
values: List of values
Returns:
Standard deviation
"""
if len(values) < 2:
return 0.0
return statistics.stdev(values)
def calculate_variance(values: List[float]) -> float:
"""
Calculate variance of values.
Args:
values: List of values
Returns:
Variance
"""
if len(values) < 2:
return 0.0
return statistics.variance(values)
def calculate_mean(values: List[float]) -> float:
"""
Calculate mean of values.
Args:
values: List of values
Returns:
Mean
"""
if not values:
return 0.0
return statistics.mean(values)
def calculate_median(values: List[float]) -> float:
"""
Calculate median of values.
Args:
values: List of values
Returns:
Median
"""
if not values:
return 0.0
return statistics.median(values)
def calculate_percentile(values: List[float], percentile: float) -> float:
"""
Calculate percentile of values.
Args:
values: List of values
percentile: Percentile to calculate (0-100)
Returns:
Percentile value
"""
if not values:
return 0.0
sorted_values = sorted(values)
k = (len(sorted_values) - 1) * percentile / 100
f = math.floor(k)
c = math.ceil(k)
if f == c:
return sorted_values[int(k)]
d0 = sorted_values[int(f)] * (c - k)
d1 = sorted_values[int(c)] * (k - f)
return d0 + d1
def calculate_coefficient_of_variation(values: List[float]) -> float:
"""
Calculate coefficient of variation (CV = stddev / mean).
Args:
values: List of values
Returns:
Coefficient of variation
"""
if not values:
return 0.0
mean = calculate_mean(values)
if mean == 0:
return 0.0
stddev = calculate_standard_deviation(values)
return stddev / mean
def aggregate_by_date(
data: List[Tuple[date, float]],
aggregation: str = "sum"
) -> Dict[date, float]:
"""
Aggregate time-series data by date.
Args:
data: List of (date, value) tuples
aggregation: Aggregation method ('sum', 'mean', 'max', 'min')
Returns:
Dictionary mapping date to aggregated value
"""
by_date: Dict[date, List[float]] = {}
for dt, value in data:
if dt not in by_date:
by_date[dt] = []
by_date[dt].append(value)
result = {}
for dt, values in by_date.items():
if aggregation == "sum":
result[dt] = sum(values)
elif aggregation == "mean":
result[dt] = calculate_mean(values)
elif aggregation == "max":
result[dt] = max(values)
elif aggregation == "min":
result[dt] = min(values)
else:
result[dt] = sum(values)
return result
def fill_missing_dates(
data: Dict[date, float],
start_date: date,
end_date: date,
fill_value: float = 0.0
) -> Dict[date, float]:
"""
Fill missing dates in time-series data.
Args:
data: Dictionary mapping date to value
start_date: Start date
end_date: End date
fill_value: Value to use for missing dates
Returns:
Dictionary with all dates filled
"""
date_range = generate_date_range(start_date, end_date)
filled_data = {}
for dt in date_range:
filled_data[dt] = data.get(dt, fill_value)
return filled_data
def calculate_trend(
values: List[float]
) -> Tuple[float, float]:
"""
Calculate linear trend (slope and intercept) using least squares.
Args:
values: List of values
Returns:
Tuple of (slope, intercept)
"""
if len(values) < 2:
return 0.0, values[0] if values else 0.0
n = len(values)
x = list(range(n))
y = values
# Calculate means
x_mean = sum(x) / n
y_mean = sum(y) / n
# Calculate slope
numerator = sum((x[i] - x_mean) * (y[i] - y_mean) for i in range(n))
denominator = sum((x[i] - x_mean) ** 2 for i in range(n))
if denominator == 0:
return 0.0, y_mean
slope = numerator / denominator
intercept = y_mean - slope * x_mean
return slope, intercept
def project_value(
historical_values: List[float],
periods_ahead: int,
method: str = "mean"
) -> List[float]:
"""
Project future values based on historical data.
Args:
historical_values: Historical values
periods_ahead: Number of periods to project
method: Projection method ('mean', 'trend', 'last')
Returns:
List of projected values
"""
if not historical_values:
return [0.0] * periods_ahead
if method == "mean":
# Use historical mean
projected_value = calculate_mean(historical_values)
return [projected_value] * periods_ahead
elif method == "last":
# Use last value
return [historical_values[-1]] * periods_ahead
elif method == "trend":
# Use trend projection
slope, intercept = calculate_trend(historical_values)
n = len(historical_values)
return [slope * (n + i) + intercept for i in range(periods_ahead)]
else:
# Default to mean
projected_value = calculate_mean(historical_values)
return [projected_value] * periods_ahead
def calculate_cumulative_sum(values: List[float]) -> List[float]:
"""
Calculate cumulative sum of values.
Args:
values: List of values
Returns:
List of cumulative sums
"""
cumulative = []
total = 0.0
for value in values:
total += value
cumulative.append(total)
return cumulative
def calculate_rolling_sum(
values: List[float],
window_size: int
) -> List[float]:
"""
Calculate rolling sum over a window.
Args:
values: List of values
window_size: Size of rolling window
Returns:
List of rolling sums
"""
if len(values) < window_size:
return []
rolling_sums = []
for i in range(len(values) - window_size + 1):
window = values[i:i + window_size]
rolling_sums.append(sum(window))
return rolling_sums
def normalize_values(
values: List[float],
method: str = "minmax"
) -> List[float]:
"""
Normalize values to a standard range.
Args:
values: List of values
method: Normalization method ('minmax' or 'zscore')
Returns:
List of normalized values
"""
if not values:
return []
if method == "minmax":
# Scale to [0, 1]
min_val = min(values)
max_val = max(values)
if max_val == min_val:
return [0.5] * len(values)
return [(v - min_val) / (max_val - min_val) for v in values]
elif method == "zscore":
# Z-score normalization
mean = calculate_mean(values)
stddev = calculate_standard_deviation(values)
if stddev == 0:
return [0.0] * len(values)
return [(v - mean) / stddev for v in values]
else:
return values
def detect_outliers(
values: List[float],
method: str = "iqr",
threshold: float = 1.5
) -> List[bool]:
"""
Detect outliers in values.
Args:
values: List of values
method: Detection method ('iqr' or 'zscore')
threshold: Threshold for outlier detection
Returns:
List of booleans indicating outliers
"""
if not values:
return []
if method == "iqr":
# Interquartile range method
q1 = calculate_percentile(values, 25)
q3 = calculate_percentile(values, 75)
iqr = q3 - q1
lower_bound = q1 - threshold * iqr
upper_bound = q3 + threshold * iqr
return [v < lower_bound or v > upper_bound for v in values]
elif method == "zscore":
# Z-score method
mean = calculate_mean(values)
stddev = calculate_standard_deviation(values)
if stddev == 0:
return [False] * len(values)
z_scores = [(v - mean) / stddev for v in values]
return [abs(z) > threshold for z in z_scores]
else:
return [False] * len(values)
def interpolate_missing_values(
values: List[Optional[float]],
method: str = "linear"
) -> List[float]:
"""
Interpolate missing values in a time series.
Args:
values: List of values with possible None values
method: Interpolation method ('linear', 'forward', 'backward')
Returns:
List with interpolated values
"""
if not values:
return []
result = []
if method == "forward":
# Forward fill
last_valid = None
for v in values:
if v is not None:
last_valid = v
result.append(last_valid if last_valid is not None else 0.0)
elif method == "backward":
# Backward fill
next_valid = None
for v in reversed(values):
if v is not None:
next_valid = v
result.insert(0, next_valid if next_valid is not None else 0.0)
else: # linear
# Linear interpolation
result = list(values)
for i in range(len(result)):
if result[i] is None:
# Find previous and next valid values
prev_idx = None
next_idx = None
for j in range(i - 1, -1, -1):
if values[j] is not None:
prev_idx = j
break
for j in range(i + 1, len(values)):
if values[j] is not None:
next_idx = j
break
if prev_idx is not None and next_idx is not None:
# Linear interpolation
x0, y0 = prev_idx, values[prev_idx]
x1, y1 = next_idx, values[next_idx]
result[i] = y0 + (y1 - y0) * (i - x0) / (x1 - x0)
elif prev_idx is not None:
# Forward fill
result[i] = values[prev_idx]
elif next_idx is not None:
# Backward fill
result[i] = values[next_idx]
else:
# No valid values
result[i] = 0.0
return result