Fix Gitea MCP ESM resolution error, update configuration, and include project tests/migration files
This commit is contained in:
@@ -1,97 +1,97 @@
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
|
||||
class SOIPredictionService:
|
||||
"""학습형 시계열 예측 및 피처 추출 엔진"""
|
||||
|
||||
@staticmethod
|
||||
def get_historical_soi(cursor, project_id):
|
||||
"""DB에서 프로젝트의 과거 SOI 히스토리를 시퀀스로 추출"""
|
||||
cursor.execute("""
|
||||
SELECT crawl_date, file_count, recent_log
|
||||
FROM projects_history
|
||||
WHERE project_id = %s
|
||||
ORDER BY crawl_date ASC
|
||||
""", (project_id,))
|
||||
return cursor.fetchall()
|
||||
|
||||
@staticmethod
|
||||
def extract_vitality_features(history):
|
||||
"""딥러닝 학습을 위한 4대 핵심 피처 추출 (Feature Engineering)"""
|
||||
if len(history) < 2:
|
||||
return {"velocity": 0, "acceleration": 0, "consistency": 0.5, "density": 0.1}
|
||||
|
||||
# 실제 데이터 구조에 맞게 보정
|
||||
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
|
||||
|
||||
# 2. 활동 가속도 (Acceleration): 최근 활동이 빨라지는지 느려지는지
|
||||
acceleration = np.diff(np.diff(counts)).mean() if len(counts) > 2 else 0
|
||||
|
||||
# 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
|
||||
|
||||
# 4. 관리 일관성 (Consistency): 업데이트 간격의 표준편차 (낮을수록 좋음)
|
||||
# (현재 데이터는 일일 크롤링이므로 로그 텍스트 변화 시점을 기준으로 간격 계산 가능)
|
||||
|
||||
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):
|
||||
"""기존 점수와 시계열 피처를 결합하여 미래 점수 예측"""
|
||||
# 데이터가 너무 적으면 무조건 보수적 감쇄 (14일 기준 약 -2.1점)
|
||||
if not history or len(history) < 3:
|
||||
return round(max(0, min(100, current_soi - (0.15 * days_ahead))), 1)
|
||||
|
||||
features = SOIPredictionService.extract_vitality_features(history)
|
||||
current_val = float(current_soi)
|
||||
|
||||
# [정밀 정체 분석]
|
||||
# 1. 파일 수 변화 확인 (최근 5개 샘플)
|
||||
recent_counts = [int(h['file_count'] or 0) for h in history[-5:]]
|
||||
is_hard_stagnant = len(set(recent_counts)) <= 1 # 파일 수 변동이 전혀 없음
|
||||
|
||||
# 2. 최근 로그 상태 확인
|
||||
last_log = history[-1]['recent_log']
|
||||
is_no_activity = last_log is None or last_log == "데이터 없음" or "폴더자동삭제" in last_log
|
||||
|
||||
# [모멘텀 산출 로직 개편]
|
||||
if is_hard_stagnant:
|
||||
# 파일 변화가 없다면 아무리 로그가 있어도 '유지 관리'일 뿐 '성장'이 아님
|
||||
# 오히려 시간이 갈수록 기술 부채와 데이터 노후화가 진행된다고 판단 (강력 패널티)
|
||||
momentum_factor = -2.5 if is_no_activity else -1.0
|
||||
else:
|
||||
# 실질적인 파일 수 변화(Velocity)가 있을 때만 긍정적 모멘텀 검토
|
||||
v_gain = features['velocity'] * 0.5
|
||||
d_gain = features['density'] * 0.8
|
||||
momentum_factor = v_gain + d_gain - 0.5 # 기본적으로 하향 압력 부여
|
||||
|
||||
# 예측 로직: 현재값 + 모멘텀 - (시간에 따른 자연 부식)
|
||||
# 정체 시 momentum_factor가 -1.0~-2.5이므로 감쇄가 매우 빠름
|
||||
decay_constant = 0.08
|
||||
predicted = current_val + momentum_factor - (decay_constant * days_ahead)
|
||||
|
||||
# [최종 방어 로직]
|
||||
# 실질적 파일 증가(velocity > 0)가 포착되지 않았다면 예보는 현재값보다 클 수 없음
|
||||
if features['velocity'] <= 0 and predicted > current_val:
|
||||
predicted = current_val - 1.5 # 강제 하락
|
||||
|
||||
# 사망 선고 (AVI가 이미 낮고 정체 중이면 0에 수렴하도록 가속)
|
||||
if current_val < 20 and is_hard_stagnant:
|
||||
predicted = max(0, predicted - 5.0)
|
||||
|
||||
return round(max(0, min(100, predicted)), 1)
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
|
||||
class SOIPredictionService:
|
||||
"""학습형 시계열 예측 및 피처 추출 엔진"""
|
||||
|
||||
@staticmethod
|
||||
def get_historical_avi(cursor, project_id):
|
||||
"""DB에서 프로젝트의 과거 AVI 히스토리를 시퀀스로 추출"""
|
||||
cursor.execute("""
|
||||
SELECT crawl_date, file_count, recent_log
|
||||
FROM projects_history
|
||||
WHERE project_id = %s
|
||||
ORDER BY crawl_date ASC
|
||||
""", (project_id,))
|
||||
return cursor.fetchall()
|
||||
|
||||
@staticmethod
|
||||
def extract_vitality_features(history):
|
||||
"""딥러닝 학습을 위한 4대 핵심 피처 추출 (Feature Engineering)"""
|
||||
if len(history) < 2:
|
||||
return {"velocity": 0, "acceleration": 0, "consistency": 0.5, "density": 0.1}
|
||||
|
||||
# 실제 데이터 구조에 맞게 보정
|
||||
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
|
||||
|
||||
# 2. 활동 가속도 (Acceleration): 최근 활동이 빨라지는지 느려지는지
|
||||
acceleration = np.diff(np.diff(counts)).mean() if len(counts) > 2 else 0
|
||||
|
||||
# 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
|
||||
|
||||
# 4. 관리 일관성 (Consistency): 업데이트 간격의 표준편차 (낮을수록 좋음)
|
||||
# (현재 데이터는 일일 크롤링이므로 로그 텍스트 변화 시점을 기준으로 간격 계산 가능)
|
||||
|
||||
return {
|
||||
"velocity": float(velocity),
|
||||
"acceleration": float(acceleration),
|
||||
"density": float(density),
|
||||
"sample_count": len(history)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def predict_future_avi(current_avi, history, days_ahead=14):
|
||||
"""기존 점수와 시계열 피처를 결합하여 미래 점수 예측"""
|
||||
# 데이터가 너무 적으면 무조건 보수적 감쇄 (14일 기준 약 -2.1점)
|
||||
if not history or len(history) < 3:
|
||||
return round(max(0, min(100, current_avi - (0.15 * days_ahead))), 1)
|
||||
|
||||
features = SOIPredictionService.extract_vitality_features(history)
|
||||
current_val = float(current_avi)
|
||||
|
||||
# [정밀 정체 분석]
|
||||
# 1. 파일 수 변화 확인 (최근 5개 샘플)
|
||||
recent_counts = [int(h['file_count'] or 0) for h in history[-5:]]
|
||||
is_hard_stagnant = len(set(recent_counts)) <= 1 # 파일 수 변동이 전혀 없음
|
||||
|
||||
# 2. 최근 로그 상태 확인
|
||||
last_log = history[-1]['recent_log']
|
||||
is_no_activity = last_log is None or last_log == "데이터 없음" or "폴더자동삭제" in last_log
|
||||
|
||||
# [모멘텀 산출 로직 개편]
|
||||
if is_hard_stagnant:
|
||||
# 파일 변화가 없다면 아무리 로그가 있어도 '유지 관리'일 뿐 '성장'이 아님
|
||||
# 오히려 시간이 갈수록 기술 부채와 데이터 노후화가 진행된다고 판단 (강력 패널티)
|
||||
momentum_factor = -2.5 if is_no_activity else -1.0
|
||||
else:
|
||||
# 실질적인 파일 수 변화(Velocity)가 있을 때만 긍정적 모멘텀 검토
|
||||
v_gain = features['velocity'] * 0.5
|
||||
d_gain = features['density'] * 0.8
|
||||
momentum_factor = v_gain + d_gain - 0.5 # 기본적으로 하향 압력 부여
|
||||
|
||||
# 예측 로직: 현재값 + 모멘텀 - (시간에 따른 자연 부식)
|
||||
# 정체 시 momentum_factor가 -1.0~-2.5이므로 감쇄가 매우 빠름
|
||||
decay_constant = 0.08
|
||||
predicted = current_val + momentum_factor - (decay_constant * days_ahead)
|
||||
|
||||
# [최종 방어 로직]
|
||||
# 실질적 파일 증가(velocity > 0)가 포착되지 않았다면 예보는 현재값보다 클 수 없음
|
||||
if features['velocity'] <= 0 and predicted > current_val:
|
||||
predicted = current_val - 1.5 # 강제 하락
|
||||
|
||||
# 사망 선고 (AVI가 이미 낮고 정체 중이면 0에 수렴하도록 가속)
|
||||
if current_val < 20 and is_hard_stagnant:
|
||||
predicted = max(0, predicted - 5.0)
|
||||
|
||||
return round(max(0, min(100, predicted)), 1)
|
||||
|
||||
Reference in New Issue
Block a user