feat: 운영 일관성(OCI) 지표 도입 및 분석 UI/UX 정밀 복구
- analysis_service.py: 운영 일관성(OCI) 산출 로직 구현 및 장기 정체 패널티(100일 기준) 적용 - js/analysis.js: OCI 통합, 아코디언 심층 분석 텍스트 보강, SWOT 사분면 및 스크롤 로직 정밀 복구 - style/*.css: 유색 border-left/top 스타일 제거 및 흑백/그레이 계열로 디자인 정제 - templates/analysis.html: 분석 모델 명칭 원복 및 지표 정의 UI 업데이트 - ANALYSIS_REPORT.md: OCI 지표 정의 추가 및 가치 기여도(VCI) 등급 설명 정제 (야구 용어 삭제)
This commit is contained in:
@@ -1,13 +1,51 @@
|
||||
import re
|
||||
import math
|
||||
import statistics
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from sql_queries import DashboardQueries
|
||||
from prediction_service import SOIPredictionService
|
||||
|
||||
class AnalysisService:
|
||||
"""프로젝트 통계 및 활동성 분석 전문 서비스"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_operational_consistency(history_rows, days_stagnant):
|
||||
"""운영 일관성 지수(OCI) 산출 로직 (장기 정체 패널티 포함)
|
||||
최근 30일간 활동 리듬 분석 + 현재 방치 기간에 따른 강력한 감쇄
|
||||
"""
|
||||
if not history_rows or len(history_rows) < 2:
|
||||
return 0.0
|
||||
|
||||
# 1. 최근 30일 이력 기반 Base Score 산출
|
||||
now = datetime.now().date()
|
||||
recent_30 = [h for h in history_rows if (now - h['crawl_date']).days <= 30]
|
||||
|
||||
# 주차별 활동 여부 (4주)
|
||||
weeks_active = [False, False, False, False]
|
||||
for h in recent_30:
|
||||
days_ago = (now - h['crawl_date']).days
|
||||
week_idx = min(3, days_ago // 7)
|
||||
weeks_active[week_idx] = True
|
||||
|
||||
base_consistency = (sum(weeks_active) / 4) * 70
|
||||
|
||||
# 활동 밀도 (변화 발생일 비율)
|
||||
effort_days = 0
|
||||
for i in range(1, len(recent_30)):
|
||||
if recent_30[i]['file_count'] != recent_30[i-1]['file_count']:
|
||||
effort_days += 1
|
||||
|
||||
density_score = (effort_days / max(1, len(recent_30))) * 30
|
||||
base_oci = base_consistency + density_score
|
||||
|
||||
# 2. [핵심] 장기 정체 패널티 적용
|
||||
# 방치일이 100일 이상이면 OCI는 0점으로 수렴 (성실도 무효화)
|
||||
stagnation_factor = max(0, (100 - days_stagnant) / 100.0)
|
||||
|
||||
final_oci = base_oci * stagnation_factor
|
||||
|
||||
return round(final_oci, 1)
|
||||
|
||||
@staticmethod
|
||||
def calculate_activity_status(target_date_dt, log, file_count):
|
||||
"""개별 프로젝트의 활동 상태 및 방치일 산출"""
|
||||
@@ -59,7 +97,7 @@ class AnalysisService:
|
||||
|
||||
@staticmethod
|
||||
def get_p_zsr_analysis_logic(cursor):
|
||||
"""절대적 방치 실태 고발 및 AI 위험 적응형(AAS) 분석 로직"""
|
||||
"""절대적 방치 실태 고발 및 운영 일관성(OCI) 분석 로직"""
|
||||
cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE)
|
||||
res_date = cursor.fetchone()
|
||||
if not res_date or not res_date['last_date']:
|
||||
@@ -77,31 +115,12 @@ class AnalysisService:
|
||||
|
||||
if not projects: return []
|
||||
|
||||
# [Step 1] AI 전처리: 부서별 평균 방치일 계산 (조직적 위험도 산출용)
|
||||
dept_stats = {}
|
||||
for p in projects:
|
||||
log = p['recent_log']
|
||||
days = 14 # 기본값
|
||||
if log and log != "데이터 없음":
|
||||
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 = (last_date - log_date).days
|
||||
|
||||
dept = p['department'] or "미분류"
|
||||
if dept not in dept_stats: dept_stats[dept] = []
|
||||
dept_stats[dept].append(days)
|
||||
|
||||
dept_avg_risk = {d: statistics.mean(days_list) for d, days_list in dept_stats.items()}
|
||||
|
||||
# [Step 2] AI 위험 적응형 SOI 산출 (AAS 모델)
|
||||
results = []
|
||||
total_soi = 0
|
||||
|
||||
for p in projects:
|
||||
file_count = int(p['file_count']) if p['file_count'] else 0
|
||||
log = p['recent_log']
|
||||
dept = p['department'] or "미분류"
|
||||
|
||||
# 방치일 계산
|
||||
days_stagnant = 14
|
||||
@@ -114,52 +133,33 @@ class AnalysisService:
|
||||
is_auto_delete = log and "폴더자동삭제" in log.replace(" ", "")
|
||||
|
||||
# AI-Hazard 추론 로직 (Dynamic Lambda)
|
||||
# 1. 자산 규모 리스크 (파일이 많을수록 방치 시 가치 하락 가속)
|
||||
scale_impact = min(0.04, math.log10(file_count + 1) * 0.008) if file_count > 0 else 0
|
||||
ai_lambda = 0.04 + scale_impact
|
||||
|
||||
# 2. 조직적 전염 리스크 (부서 전체가 방치 중이면 패널티 부여)
|
||||
dept_risk_days = dept_avg_risk.get(dept, 14)
|
||||
env_impact = min(0.03, (dept_risk_days / 30) * 0.01)
|
||||
|
||||
# 최종 AI 위험 계수 산출 (기본 0.04에서 변동)
|
||||
ai_lambda = 0.04 + scale_impact + env_impact
|
||||
|
||||
# 지수 감쇄 적용 (AAS Score)
|
||||
# 지수 감쇄 적용
|
||||
soi_score = math.exp(-ai_lambda * days_stagnant) * 100
|
||||
|
||||
# [AI 데이터 진정성 검증 로직 1 - ECV 패널티 (존재론적)]
|
||||
# ECV 패널티
|
||||
existence_confidence = 1.0
|
||||
if file_count == 0:
|
||||
existence_confidence = 0.05
|
||||
elif file_count < 10:
|
||||
existence_confidence = 0.4
|
||||
if file_count == 0: existence_confidence = 0.05
|
||||
elif file_count < 10: existence_confidence = 0.4
|
||||
|
||||
# [AI 데이터 진정성 검증 로직 2 - Log Quality Scoring (활동의 질)]
|
||||
# Log Quality Scoring
|
||||
log_quality_factor = 1.0
|
||||
if log and log != "데이터 없음":
|
||||
# 성과 중심 (High)
|
||||
if any(k in log for k in ["업로드", "수정", "등록", "변환", "파일", "업데이트"]):
|
||||
log_quality_factor = 1.0
|
||||
# 구조 관리 (Mid)
|
||||
elif any(k in log for k in ["폴더", "생성", "삭제", "이동"]):
|
||||
log_quality_factor = 0.7
|
||||
# 단순 행정/설정 (Low)
|
||||
elif any(k in log for k in ["참가자", "권한", "추가", "변경", "메일"]):
|
||||
log_quality_factor = 0.4
|
||||
else:
|
||||
log_quality_factor = 0.6 # 기타 일반 로그
|
||||
if any(k in log for k in ["업로드", "수정", "등록", "변환", "파일", "업데이트"]): log_quality_factor = 1.0
|
||||
elif any(k in log for k in ["폴더", "생성", "삭제", "이동"]): log_quality_factor = 0.7
|
||||
elif any(k in log for k in ["참가자", "권한", "추가", "변경", "메일"]): log_quality_factor = 0.4
|
||||
else: log_quality_factor = 0.6
|
||||
|
||||
# 최종 점수 산출 (AAS * ECV * LogQuality)
|
||||
soi_score = soi_score * existence_confidence * log_quality_factor
|
||||
|
||||
if is_auto_delete:
|
||||
soi_score = 0.1
|
||||
if is_auto_delete: soi_score = 0.1
|
||||
|
||||
# [AI 미래 예측 및 실무 투입 에너지 분석]
|
||||
# [운영 일관성 분석 (OCI)]
|
||||
history_rows = SOIPredictionService.get_historical_soi(cursor, p['project_id'])
|
||||
predicted_soi = SOIPredictionService.predict_future_soi(soi_score, history_rows, days_ahead=14)
|
||||
oci_score = AnalysisService.calculate_operational_consistency(history_rows, days_stagnant)
|
||||
|
||||
# 실무 투입 에너지 계산 (최근 30개 히스토리 기준 파일 변화일수)
|
||||
# 실무 투입 에너지 계산
|
||||
effort_days = 0
|
||||
if len(history_rows) > 1:
|
||||
for i in range(1, len(history_rows)):
|
||||
@@ -167,28 +167,26 @@ class AnalysisService:
|
||||
effort_days += 1
|
||||
|
||||
work_effort_rate = round((effort_days / max(1, len(history_rows))) * 100, 1)
|
||||
|
||||
total_soi += soi_score
|
||||
|
||||
# [최종 세이버메트릭스 보정: P-WAR+ (Adjusted Score)]
|
||||
# 절대 기준선(Replacement Level): 70.0% (이 이하는 자산 가치 파괴로 간주)
|
||||
# VCI 산출
|
||||
REPLACEMENT_LEVEL = 70.0
|
||||
asset_weight = (file_count / 200.0) + 0.5 # 파일 100개당 0.5배 가중 (최소 0.5배)
|
||||
asset_weight = (file_count / 200.0) + 0.5
|
||||
p_war_score = (soi_score - REPLACEMENT_LEVEL) * asset_weight
|
||||
|
||||
results.append({
|
||||
"project_nm": p['short_nm'] or p['project_nm'],
|
||||
"file_count": file_count,
|
||||
"days_stagnant": days_stagnant,
|
||||
"risk_count": round(p_war_score, 2), # P-WAR+ 절대 기여도 점수 (평균의 함정 극복용)
|
||||
"risk_count": round(p_war_score, 2),
|
||||
"p_war": round(soi_score, 1),
|
||||
"predicted_soi": predicted_soi,
|
||||
"oci_score": oci_score, # 운영 일관성 지수 추가
|
||||
"is_auto_delete": is_auto_delete,
|
||||
"master": p['master'],
|
||||
"dept": p['department'],
|
||||
"ai_lambda": round(ai_lambda, 4),
|
||||
"log_quality": log_quality_factor,
|
||||
"work_effort": work_effort_rate, # 신규 지표 추가
|
||||
"work_effort": work_effort_rate,
|
||||
"avg_info": {
|
||||
"avg_files": 0,
|
||||
"avg_stagnant": 0,
|
||||
|
||||
Reference in New Issue
Block a user