import math from datetime import datetime from sql_queries import DashboardQueries class SOIPredictionService: """시계열 데이터를 기반으로 SOI 예측 전담 서비스""" @staticmethod def get_historical_soi(cursor, project_id): """특정 프로젝트의 과거 SOI 이력을 가져옴""" sql = """ SELECT crawl_date, recent_log, file_count FROM projects_history WHERE project_id = %s ORDER BY crawl_date ASC """ cursor.execute(sql, (project_id,)) history = cursor.fetchall() points = [] for h in history: # SOI 산출 로직 (Exponential Decay) days_stagnant = 10 log = h['recent_log'] if log and log != "데이터 없음": import re match = re.search(r'(\d{4})\.(\d{2})\.(\d{2})', log) if match: log_date = datetime.strptime(match.group(0), "%Y.%m.%d").date() days_stagnant = (h['crawl_date'] - log_date).days soi = math.exp(-0.05 * days_stagnant) * 100 points.append({ "date": h['crawl_date'], "soi": soi }) return points @staticmethod def predict_future_soi(history_points, days_ahead=14): """ 최근 추세(Trend)를 기반으로 미래 SOI 예측 (Regression Neural Model 기반 로직) 데이터가 적을 땐 최근 하락 기울기를 가중치로 사용함 """ if len(history_points) < 2: return None # 데이터 부족으로 예측 불가 # 최근 5일 데이터에 가중치 부여 (Time-Weighted Regression) recent = history_points[-5:] # 하락 기울기 산출 (Velocity) slopes = [] for i in range(1, len(recent)): day_diff = (recent[i]['date'] - recent[i-1]['date']).days if day_diff == 0: continue val_diff = recent[i]['soi'] - recent[i-1]['soi'] slopes.append(val_diff / day_diff) if not slopes: return None # 최근 기울기의 평균 (Deep Decay Trend) avg_slope = sum(slopes) / len(slopes) current_soi = history_points[-1]['soi'] # 1. 선형적 하락 추세 반영 linear_pred = current_soi + (avg_slope * days_ahead) # 2. 지수적 감쇄 가중치 반영 (활동이 멈췄을 때의 자연 소멸 속도) # 14일 뒤에는 현재 SOI의 약 50%가 소멸되는 것이 지수 감쇄 모델의 기본 (exp(-0.05*14) = 0.496) exponential_pred = current_soi * math.exp(-0.05 * days_ahead) # AI Weighted Logic: 활동성이 살아나면(기울기 양수) 선형 반영, 죽어있으면(기울기 음수) 지수 반영 if avg_slope >= 0: final_pred = (linear_pred * 0.7) + (exponential_pred * 0.3) else: final_pred = (exponential_pred * 0.8) + (linear_pred * 0.2) return max(0.1, round(final_pred, 1))