"""Anomaly detection module""" import statistics from collections import defaultdict from datetime import datetime, timedelta from typing import Dict, List, Optional, Tuple, Set from ..data.csv_handler import CSVHandler from ..common.utils import safe_float class AnomalyDetector: """异常检测器""" def __init__(self, warning_threshold: float = 1.5, critical_threshold: float = 2.0, min_cost_threshold: float = 100.0): """初始化异常检测器 Args: warning_threshold: 警告阈值倍数 critical_threshold: 严重阈值倍数 min_cost_threshold: 最小成本阈值 """ self.warning_threshold = warning_threshold self.critical_threshold = critical_threshold self.min_cost_threshold = min_cost_threshold self.csv_handler = CSVHandler() def calculate_account_baseline(self, historical_records: List[Dict]) -> Dict: """计算账户费用基线 Args: historical_records: 历史记录列表 Returns: 基线统计信息 """ # 按日期聚合费用 daily_costs = defaultdict(float) for record in historical_records: cost = safe_float(record.get('cost', 0)) daily_costs[record['date']] += cost costs = [cost for cost in daily_costs.values() if cost > 0] if not costs: return { 'avg_cost': 0, 'median_cost': 0, 'std_cost': 0, 'max_cost': 0, 'min_cost': 0, 'sample_size': 0 } return { 'avg_cost': statistics.mean(costs), 'median_cost': statistics.median(costs), 'std_cost': statistics.stdev(costs) if len(costs) > 1 else 0, 'max_cost': max(costs), 'min_cost': min(costs), 'sample_size': len(costs) } def calculate_service_baselines(self, historical_records: List[Dict], ignored_services: Optional[Set[str]] = None) -> Dict[str, Dict]: """计算服务费用基线 Args: historical_records: 历史记录列表 ignored_services: 要忽略的服务集合 Returns: 服务基线字典 """ if ignored_services is None: ignored_services = set() service_daily_costs = defaultdict(lambda: defaultdict(float)) # 按服务和日期聚合费用 for record in historical_records: service = record.get('product', 'Unknown') if service in ignored_services: continue date = record['date'] cost = safe_float(record.get('cost', 0)) service_daily_costs[service][date] += cost baselines = {} for service, daily_costs in service_daily_costs.items(): costs = [cost for cost in daily_costs.values() if cost > 0] if costs: baselines[service] = { 'avg_cost': statistics.mean(costs), 'median_cost': statistics.median(costs), 'std_cost': statistics.stdev(costs) if len(costs) > 1 else 0, 'max_cost': max(costs), 'min_cost': min(costs), 'sample_size': len(costs) } return baselines def calculate_tag_baselines(self, historical_records: List[Dict], tag_field: str = 'tag_usedby', min_cost: float = 1.0) -> Dict[str, Dict]: """计算标签费用基线 Args: historical_records: 历史记录列表 tag_field: 标签字段名 min_cost: 最小成本阈值 Returns: 标签基线字典 """ tag_daily_costs = defaultdict(lambda: defaultdict(float)) # 按标签和日期聚合费用 for record in historical_records: tag_value = record.get(tag_field, '').strip() if not tag_value: continue date = record['date'] cost = safe_float(record.get('cost', 0)) if cost >= min_cost: tag_daily_costs[tag_value][date] += cost baselines = {} for tag, daily_costs in tag_daily_costs.items(): costs = [cost for cost in daily_costs.values() if cost > 0] if costs and len(costs) >= 3: # 至少需要3天数据 baselines[tag] = { 'avg_cost': statistics.mean(costs), 'median_cost': statistics.median(costs), 'std_cost': statistics.stdev(costs) if len(costs) > 1 else 0, 'max_cost': max(costs), 'min_cost': min(costs), 'sample_size': len(costs) } return baselines def detect_account_anomalies(self, current_cost: float, baseline: Dict, account_id: str) -> Optional[Dict]: """检测账户级别异常 Args: current_cost: 当前成本 baseline: 基线数据 account_id: 账户ID Returns: 异常信息字典,如果没有异常则返回None """ if baseline['avg_cost'] <= 0 or current_cost < self.min_cost_threshold: return None ratio = current_cost / baseline['avg_cost'] severity = None if ratio >= self.critical_threshold: severity = 'critical' elif ratio >= self.warning_threshold: severity = 'warning' if severity: return { 'type': 'account', 'account_id': account_id, 'severity': severity, 'current_cost': current_cost, 'baseline_avg': baseline['avg_cost'], 'ratio': ratio, 'threshold': (self.critical_threshold if severity == 'critical' else self.warning_threshold), 'increase_amount': current_cost - baseline['avg_cost'], 'increase_percentage': (ratio - 1) * 100 } return None def detect_service_anomalies(self, current_services: Dict[str, float], baselines: Dict[str, Dict], account_id: str) -> List[Dict]: """检测服务级别异常 Args: current_services: 当前服务成本字典 baselines: 服务基线字典 account_id: 账户ID Returns: 异常信息列表 """ anomalies = [] for service, current_cost in current_services.items(): if service not in baselines: continue baseline = baselines[service] if baseline['avg_cost'] <= 0 or current_cost < self.min_cost_threshold: continue ratio = current_cost / baseline['avg_cost'] severity = None if ratio >= self.critical_threshold: severity = 'critical' elif ratio >= self.warning_threshold: severity = 'warning' if severity: anomalies.append({ 'type': 'service', 'account_id': account_id, 'service': service, 'severity': severity, 'current_cost': current_cost, 'baseline_avg': baseline['avg_cost'], 'ratio': ratio, 'threshold': (self.critical_threshold if severity == 'critical' else self.warning_threshold), 'increase_amount': current_cost - baseline['avg_cost'], 'increase_percentage': (ratio - 1) * 100 }) return anomalies def detect_tag_anomalies(self, current_tags: Dict[str, float], baselines: Dict[str, Dict], account_id: str, tag_field: str = 'tag_usedby') -> List[Dict]: """检测标签级别异常 Args: current_tags: 当前标签成本字典 baselines: 标签基线字典 account_id: 账户ID tag_field: 标签字段名 Returns: 异常信息列表 """ anomalies = [] for tag, current_cost in current_tags.items(): if tag not in baselines: continue baseline = baselines[tag] if baseline['avg_cost'] <= 0 or current_cost < 1.0: continue ratio = current_cost / baseline['avg_cost'] severity = None if ratio >= self.critical_threshold: severity = 'critical' elif ratio >= self.warning_threshold: severity = 'warning' if severity: anomalies.append({ 'type': 'tag', 'account_id': account_id, 'tag_field': tag_field, 'tag_value': tag, 'severity': severity, 'current_cost': current_cost, 'baseline_avg': baseline['avg_cost'], 'ratio': ratio, 'threshold': (self.critical_threshold if severity == 'critical' else self.warning_threshold), 'increase_amount': current_cost - baseline['avg_cost'], 'increase_percentage': (ratio - 1) * 100 }) return anomalies def detect_new_tags(self, current_tags: Dict[str, float], known_tags: Set[str], new_tag_threshold: float = 5.0) -> List[Dict]: """检测新出现的标签 Args: current_tags: 当前标签成本字典 known_tags: 已知标签集合 new_tag_threshold: 新标签成本阈值 Returns: 新标签信息列表 """ new_tags = [] for tag, cost in current_tags.items(): if tag not in known_tags and cost >= new_tag_threshold: new_tags.append({ 'type': 'new_tag', 'tag_value': tag, 'cost': cost, 'threshold': new_tag_threshold }) return new_tags def load_historical_data(self, file_paths: List[str], days_back: int = 30) -> List[Dict]: """加载历史数据 Args: file_paths: CSV文件路径列表 days_back: 回溯天数 Returns: 历史记录列表 """ all_records = [] cutoff_date = datetime.now() - timedelta(days=days_back) for file_path in file_paths: records = self.csv_handler.read_file(file_path) # 过滤日期范围 for record in records: try: record_date = datetime.strptime(record['date'], '%Y-%m-%d') if record_date >= cutoff_date: all_records.append(record) except (ValueError, KeyError): continue return all_records