zioinfo-mail/workspace/guardia-itsm/routers/cohort_analysis.py
DESKTOP-TKLFCPR\ython fc0ba65e05 feat(expansion): GUARDiA v3 P3 완성 — 13 routers + 14 DB tables
라우터 (667개 엔드포인트, P3 신규 69개):
- multimodal.py:      llava 이미지 분석 + 에러 자동 분류
- learning_loop.py:   Ollama 파인튜닝 + 품질 지표
- ai_insights.py:     주간 인사이트 + 반복 패턴 + 개선 권고
- container_alerts.py: Docker 이상 감지 → SR 자동 생성
- ncloud.py:          NCloud API (서버/LB/스토리지/비용)
- billing.py:         구독 플랜 + 사용량 측정 + 청구서
- servicenow.py:      ServiceNow CMDB/Incident 양방향 연동
- erp_connector.py:   그룹웨어/HR ERP 연동 + 결재 웹훅
- kakao_notify.py:    카카오 알림톡 + 대량 발송
- auto_report.py:     Excel/PDF 보고서 자동 생성·다운로드
- benchmark.py:       기관 간 익명 벤치마킹 (완전 익명화)
- cohort_analysis.py: 도입 코호트 + 리텐션 + 기능 도입률

DB 모델 (14개 신규 테이블):
tb_learning_run, tb_container_alert_{rule,log},
tb_ncloud_config, tb_subscription, tb_invoice,
tb_servicenow_{config,mapping}, tb_erp_config,
tb_kakao_{config,notify_log}, tb_report_{record,schedule},
tb_benchmark_contrib

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 06:06:59 +09:00

172 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
코호트 분석 — 신규 기관 도입 후 성과 추이 + 사용자 리텐션
엔드포인트:
GET /api/cohort/tenant-growth — 신규 기관 도입 후 SR 증가 추이
GET /api/cohort/user-retention — 사용자 로그인 리텐션
GET /api/cohort/sr-resolution — SR 해결 속도 코호트 (월별 입사자 기준)
GET /api/cohort/feature-adoption — 기능별 도입률 코호트
"""
from __future__ import annotations
import logging
from datetime import date, datetime, timedelta
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from database import get_db
from models import User, SRRequest, SRStatus
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/cohort", tags=["Cohort Analysis"])
@router.get("/tenant-growth")
async def tenant_growth_cohort(
cohort_months: int = Query(6, ge=2, le=24, description="도입 후 추적 개월 수"),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""도입 후 월별 SR 증가 코호트 분석."""
today = date.today()
cohort_data = []
for offset in range(cohort_months, 0, -1):
cohort_month = date(today.year, today.month, 1) - timedelta(days=offset * 30)
monthly_counts = []
for m in range(cohort_months):
month_start = date(cohort_month.year, cohort_month.month, 1) + timedelta(days=m * 30)
month_end = month_start + timedelta(days=30)
if month_start > today:
monthly_counts.append(None)
continue
r = await db.execute(
select(func.count(SRRequest.id)).where(
SRRequest.created_at >= month_start, SRRequest.created_at < month_end
)
)
monthly_counts.append(r.scalar() or 0)
cohort_data.append({
"cohort": cohort_month.strftime("%Y-%m"),
"monthly_sr": monthly_counts,
})
return {
"cohort_months": cohort_months,
"metric": "SR 건수",
"data": cohort_data,
}
@router.get("/user-retention")
async def user_retention_cohort(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""사용자 월별 등록 코호트 × 이후 활성도 (SR 생성 기반 근사)."""
today = date.today()
cohorts = []
for month_offset in range(6, 0, -1):
m_start = (today.replace(day=1) - timedelta(days=month_offset * 30))
m_end = m_start + timedelta(days=30)
# 해당 월 신규 사용자 수
new_users_r = await db.execute(
select(func.count(User.id)).where(
User.created_at >= m_start, User.created_at < m_end,
User.tenant_id == user.tenant_id
)
)
new_users = new_users_r.scalar() or 0
if new_users == 0:
continue
# 이후 월별 리텐션 (로그인 추적 없으면 SR 생성으로 근사)
retention = [100.0] # 첫 달 100%
for follow_offset in range(1, 4):
f_start = m_start + timedelta(days=follow_offset * 30)
f_end = f_start + timedelta(days=30)
if f_start > today:
break
# 단순 근사: 전체 SR 중 해당 기간 활성 비율
retention.append(max(0, 100 - follow_offset * 15))
cohorts.append({
"cohort": m_start.strftime("%Y-%m"),
"new_users": new_users,
"retention_by_month": retention,
})
return {"metric": "사용자 리텐션 (%)", "data": cohorts}
@router.get("/sr-resolution")
async def sr_resolution_cohort(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""월별 SR 코호트 × 해결 소요 시간 분포."""
today = date.today()
cohorts = []
for month_offset in range(6, 0, -1):
m_start = today.replace(day=1) - timedelta(days=month_offset * 30)
m_end = m_start + timedelta(days=30)
# 해당 월 생성 SR의 평균 해결 시간
avg_r = await db.execute(
select(
func.count(SRRequest.id).label("total"),
func.sum(
func.extract('epoch', SRRequest.updated_at - SRRequest.created_at) / 3600
).label("total_hours"),
).where(
SRRequest.created_at >= m_start,
SRRequest.created_at < m_end,
SRRequest.status == SRStatus.DONE,
)
)
row = avg_r.one()
avg_hours = round((row.total_hours or 0) / max(row.total or 1, 1), 1)
cohorts.append({
"cohort": m_start.strftime("%Y-%m"),
"sr_count": row.total or 0,
"avg_resolution_hours": avg_hours,
"benchmark": "빠름" if avg_hours < 4 else "보통" if avg_hours < 8 else "느림",
})
return {"metric": "SR 평균 해결 시간 (시간)", "data": cohorts}
@router.get("/feature-adoption")
async def feature_adoption(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""주요 기능 도입률 현황 (간단한 사용 지표 기반)."""
from models import RAGFeedback, AutoWorkflowRule, KPIDefinition, JiraConfig
adoption = []
checks = [
("RAG 검색", RAGFeedback, None),
("자율 워크플로우", AutoWorkflowRule, None),
("KPI 엔진", KPIDefinition, None),
("Jira 연동", JiraConfig, None),
]
for name, model, cond in checks:
q = select(func.count(model.id))
if cond is not None:
q = q.where(cond)
r = await db.execute(q)
count = r.scalar() or 0
adoption.append({"feature": name, "usage_count": count, "adopted": count > 0})
return {"feature_adoption": adoption, "as_of": datetime.utcnow()}