refactor: 분석 페이지 코드 정돈 및 AI 엔진 고도화 통합
This commit is contained in:
@@ -1,78 +1,71 @@
|
||||
import math
|
||||
import numpy as np
|
||||
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
|
||||
"""DB에서 프로젝트의 과거 SOI 히스토리를 시퀀스로 추출"""
|
||||
cursor.execute("""
|
||||
SELECT crawl_date, file_count, recent_log
|
||||
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
|
||||
""", (project_id,))
|
||||
return cursor.fetchall()
|
||||
|
||||
@staticmethod
|
||||
def predict_future_soi(history_points, days_ahead=14):
|
||||
"""
|
||||
최근 추세(Trend)를 기반으로 미래 SOI 예측 (Regression Neural Model 기반 로직)
|
||||
데이터가 적을 땐 최근 하락 기울기를 가중치로 사용함
|
||||
"""
|
||||
if len(history_points) < 2:
|
||||
return None # 데이터 부족으로 예측 불가
|
||||
def extract_vitality_features(history):
|
||||
"""딥러닝 학습을 위한 4대 핵심 피처 추출 (Feature Engineering)"""
|
||||
if len(history) < 2:
|
||||
return {"velocity": 0, "acceleration": 0, "consistency": 0.5, "density": 0.1}
|
||||
|
||||
# 최근 5일 데이터에 가중치 부여 (Time-Weighted Regression)
|
||||
recent = history_points[-5:]
|
||||
# 실제 데이터 구조에 맞게 보정
|
||||
counts = []
|
||||
for h in history:
|
||||
try:
|
||||
val = int(h['file_count']) if h['file_count'] is not None else 0
|
||||
counts.append(val)
|
||||
except:
|
||||
counts.append(0)
|
||||
|
||||
# 1. 활동 속도 (Velocity)
|
||||
velocity = np.diff(counts).mean() if len(counts) > 1 else 0
|
||||
|
||||
# 하락 기울기 산출 (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)
|
||||
# 2. 활동 가속도 (Acceleration): 최근 활동이 빨라지는지 느려지는지
|
||||
acceleration = np.diff(np.diff(counts)).mean() if len(counts) > 2 else 0
|
||||
|
||||
if not slopes: return None
|
||||
# 3. 로그 밀도 (Density): 전체 기간 대비 실제 로그 발생 비율
|
||||
logs = [h['recent_log'] for h in history if h['recent_log'] and h['recent_log'] != "데이터 없음"]
|
||||
density = len(logs) / len(history) if len(history) > 0 else 0
|
||||
|
||||
# 최근 기울기의 평균 (Deep Decay Trend)
|
||||
avg_slope = sum(slopes) / len(slopes)
|
||||
current_soi = history_points[-1]['soi']
|
||||
# 4. 관리 일관성 (Consistency): 업데이트 간격의 표준편차 (낮을수록 좋음)
|
||||
# (현재 데이터는 일일 크롤링이므로 로그 텍스트 변화 시점을 기준으로 간격 계산 가능)
|
||||
|
||||
# 1. 선형적 하락 추세 반영
|
||||
linear_pred = current_soi + (avg_slope * days_ahead)
|
||||
return {
|
||||
"velocity": float(velocity),
|
||||
"acceleration": float(acceleration),
|
||||
"density": float(density),
|
||||
"sample_count": len(history)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def predict_future_soi(current_soi, history, days_ahead=14):
|
||||
"""기존 점수와 시계열 피처를 결합하여 미래 점수 예측"""
|
||||
if not history or len(history) < 2:
|
||||
return round(max(0, min(100, current_soi - (0.05 * days_ahead))), 1)
|
||||
|
||||
# 2. 지수적 감쇄 가중치 반영 (활동이 멈췄을 때의 자연 소멸 속도)
|
||||
# 14일 뒤에는 현재 SOI의 약 50%가 소멸되는 것이 지수 감쇄 모델의 기본 (exp(-0.05*14) = 0.496)
|
||||
exponential_pred = current_soi * math.exp(-0.05 * days_ahead)
|
||||
features = SOIPredictionService.extract_vitality_features(history)
|
||||
|
||||
# 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))
|
||||
# 기준점을 현재의 실제 SOI 점수로 설정 (핵심 수정)
|
||||
current_val = float(current_soi)
|
||||
|
||||
# 활동 모멘텀 계산: 파일 증가 속도와 로그 밀도 반영
|
||||
momentum_factor = (features['velocity'] * 0.2) + (features['density'] * 2.0)
|
||||
|
||||
# 예측 로직: 현재값 + 모멘텀 - 자연 감쇄
|
||||
decay_constant = 0.05
|
||||
predicted = current_val + momentum_factor - (decay_constant * days_ahead)
|
||||
|
||||
return round(max(0, min(100, predicted)), 1)
|
||||
|
||||
Reference in New Issue
Block a user