docs: 재고 분석 통계 최적화 노트 추가
- 추세 분석 로직 변천사 기록 - 롤백 포인트 (커밋 hash) 정리 - 대안 로직들 (전월대비, 이동평균, 선형회귀) 정리 - TODO 리스트
This commit is contained in:
parent
0b81999cb4
commit
2ad4ad05f3
@ -1366,13 +1366,17 @@
|
|||||||
const stockChangePercent = firstStock > 0 ? Math.round((stockChange / firstStock) * 100) : 0;
|
const stockChangePercent = firstStock > 0 ? Math.round((stockChange / firstStock) * 100) : 0;
|
||||||
const stockTrend = stockChange > 0 ? 'increasing' : (stockChange < 0 ? 'decreasing' : 'stable');
|
const stockTrend = stockChange > 0 ? 'increasing' : (stockChange < 0 ? 'decreasing' : 'stable');
|
||||||
|
|
||||||
// 사용량 추세 계산 (전반부 vs 후반부)
|
// 사용량 추세 계산 (최근 3개 기간 vs 이전 3개 기간 평균 비교)
|
||||||
const half = Math.floor(items.length / 2);
|
const recentCount = Math.min(3, Math.floor(items.length / 2));
|
||||||
const firstHalfUsage = items.slice(0, half).reduce((sum, i) => sum + i.rx_usage, 0);
|
const recentItems = items.slice(-recentCount);
|
||||||
const secondHalfUsage = items.slice(half).reduce((sum, i) => sum + i.rx_usage, 0);
|
const previousItems = items.slice(-recentCount * 2, -recentCount);
|
||||||
const usageChange = secondHalfUsage - firstHalfUsage;
|
|
||||||
const usageChangePercent = firstHalfUsage > 0 ? Math.round((usageChange / firstHalfUsage) * 100) : 0;
|
const recentAvg = recentItems.length > 0 ? recentItems.reduce((sum, i) => sum + i.rx_usage, 0) / recentItems.length : 0;
|
||||||
const usageTrend = usageChange > firstHalfUsage * 0.1 ? 'increasing' : (usageChange < -firstHalfUsage * 0.1 ? 'decreasing' : 'stable');
|
const previousAvg = previousItems.length > 0 ? previousItems.reduce((sum, i) => sum + i.rx_usage, 0) / previousItems.length : 0;
|
||||||
|
|
||||||
|
const usageChange = Math.round(recentAvg - previousAvg);
|
||||||
|
const usageChangePercent = previousAvg > 0 ? Math.round((usageChange / previousAvg) * 100) : 0;
|
||||||
|
const usageTrend = usageChangePercent > 10 ? 'increasing' : (usageChangePercent < -10 ? 'decreasing' : 'stable');
|
||||||
|
|
||||||
// 해석 메시지 및 상태 결정
|
// 해석 메시지 및 상태 결정
|
||||||
let interpretation = '';
|
let interpretation = '';
|
||||||
@ -1452,7 +1456,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="forecast-row">
|
<div class="forecast-row">
|
||||||
<span class="forecast-label">사용량 추세</span>
|
<span class="forecast-label">사용량 추세 (최근 평균)</span>
|
||||||
<span class="forecast-value" style="color: ${trendColor[usageTrend]};">
|
<span class="forecast-value" style="color: ${trendColor[usageTrend]};">
|
||||||
${trendIcon[usageTrend]} ${usageChange >= 0 ? '+' : ''}${usageChange.toLocaleString()} (${usageChangePercent >= 0 ? '+' : ''}${usageChangePercent}%)
|
${trendIcon[usageTrend]} ${usageChange >= 0 ? '+' : ''}${usageChange.toLocaleString()} (${usageChangePercent >= 0 ? '+' : ''}${usageChangePercent}%)
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
153
docs/통계최적화노트.md
Normal file
153
docs/통계최적화노트.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# 📊 재고 분석 통계 최적화 노트
|
||||||
|
|
||||||
|
> 재고량 vs 사용량 비교 그래프의 추세 분석 로직을 최적화하는 과정 기록
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 커밋 히스토리 (롤백 포인트)
|
||||||
|
|
||||||
|
| 커밋 | Hash | 설명 | 특징 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 재고 변화만 | `2ca35cd` | 재고 변화 추이 그래프 (단일 Y축) | 보라색 라인만, 깔끔함 |
|
||||||
|
| 이중 Y축 v1 | `0b81999` | 재고 + 사용량 비교 (전반부/후반부 합) | 피크에 민감한 문제 |
|
||||||
|
| 이중 Y축 v2 | *(현재)* | 재고 + 사용량 비교 (최근 3개월 평균) | 피크 영향 줄임 |
|
||||||
|
|
||||||
|
### 롤백 방법
|
||||||
|
```bash
|
||||||
|
# 재고 변화만 있던 깔끔한 버전으로 돌아가기
|
||||||
|
git checkout 2ca35cd -- backend/templates/admin_stock_analytics.html backend/app.py
|
||||||
|
|
||||||
|
# 이중 Y축 v1으로 돌아가기
|
||||||
|
git checkout 0b81999 -- backend/templates/admin_stock_analytics.html backend/app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 추세 분석 로직 변천사
|
||||||
|
|
||||||
|
### v1: 전반부 합 vs 후반부 합 (❌ 폐기)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 데이터를 반으로 나눠서 총합 비교
|
||||||
|
const half = Math.floor(items.length / 2);
|
||||||
|
const firstHalfUsage = items.slice(0, half).reduce((sum, i) => sum + i.rx_usage, 0);
|
||||||
|
const secondHalfUsage = items.slice(half).reduce((sum, i) => sum + i.rx_usage, 0);
|
||||||
|
const usageChange = secondHalfUsage - firstHalfUsage;
|
||||||
|
```
|
||||||
|
|
||||||
|
**문제점:**
|
||||||
|
- 19개월 데이터 → 앞 9개월 vs 뒤 10개월
|
||||||
|
- 후반부에 피크(예: 2025-06)가 하나만 있어도 "증가 추세"로 판정
|
||||||
|
- 최근 3개월이 계속 떨어져도 피크 하나 때문에 잘못된 결과
|
||||||
|
|
||||||
|
**실제 사례 (테라펜세미정):**
|
||||||
|
- 눈으로 보면: 명백한 감소 추세 (6,500 → 1,000)
|
||||||
|
- 로직 결과: "+13,457 (+37%)" 증가 추세 ← 틀림!
|
||||||
|
- 원인: 2025-06 피크(~7,000)가 후반부 총합을 뻥튀기
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### v2: 최근 3개월 평균 vs 이전 3개월 평균 (현재)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 최근 3개 기간 vs 그 이전 3개 기간의 평균 비교
|
||||||
|
const recentCount = Math.min(3, Math.floor(items.length / 2));
|
||||||
|
const recentItems = items.slice(-recentCount);
|
||||||
|
const previousItems = items.slice(-recentCount * 2, -recentCount);
|
||||||
|
|
||||||
|
const recentAvg = recentItems.reduce((sum, i) => sum + i.rx_usage, 0) / recentItems.length;
|
||||||
|
const previousAvg = previousItems.reduce((sum, i) => sum + i.rx_usage, 0) / previousItems.length;
|
||||||
|
|
||||||
|
const usageChange = Math.round(recentAvg - previousAvg);
|
||||||
|
const usageChangePercent = previousAvg > 0 ? Math.round((usageChange / previousAvg) * 100) : 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
**장점:**
|
||||||
|
- 중간의 피크에 영향 안 받음
|
||||||
|
- "최근" 변화를 정확히 감지
|
||||||
|
- 직관적인 결과
|
||||||
|
|
||||||
|
**단점:**
|
||||||
|
- 월별 분석 시 6개월만 봄 (더 긴 추세 놓칠 수 있음)
|
||||||
|
- 계절성 반영 안 됨
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 고려 중인 대안들
|
||||||
|
|
||||||
|
### 옵션 A: 전월 대비 (MoM)
|
||||||
|
```javascript
|
||||||
|
const lastMonth = items[items.length - 1].rx_usage;
|
||||||
|
const prevMonth = items[items.length - 2].rx_usage;
|
||||||
|
const change = lastMonth - prevMonth;
|
||||||
|
```
|
||||||
|
- ✅ 가장 직관적 ("지난달 대비 얼마?")
|
||||||
|
- ✅ 비즈니스에서 표준
|
||||||
|
- ❌ 단기 변동에 민감
|
||||||
|
|
||||||
|
### 옵션 B: 이동평균 (Moving Average)
|
||||||
|
```javascript
|
||||||
|
// 3개월 이동평균 계산 후 기울기 비교
|
||||||
|
const ma3 = items.map((item, i, arr) => {
|
||||||
|
if (i < 2) return null;
|
||||||
|
return (arr[i].rx_usage + arr[i-1].rx_usage + arr[i-2].rx_usage) / 3;
|
||||||
|
}).filter(v => v !== null);
|
||||||
|
```
|
||||||
|
- ✅ 노이즈 제거
|
||||||
|
- ✅ 그래프에 추세선으로 표시 가능
|
||||||
|
- ❌ 구현 복잡
|
||||||
|
|
||||||
|
### 옵션 C: 선형 회귀 기울기
|
||||||
|
```javascript
|
||||||
|
// 최소자승법으로 기울기 계산
|
||||||
|
function linearRegression(data) {
|
||||||
|
const n = data.length;
|
||||||
|
const sumX = data.reduce((s, _, i) => s + i, 0);
|
||||||
|
const sumY = data.reduce((s, v) => s + v, 0);
|
||||||
|
const sumXY = data.reduce((s, v, i) => s + i * v, 0);
|
||||||
|
const sumX2 = data.reduce((s, _, i) => s + i * i, 0);
|
||||||
|
return (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- ✅ 통계적으로 정확
|
||||||
|
- ✅ 전체 데이터 활용
|
||||||
|
- ❌ 학술적, 직관성 떨어짐
|
||||||
|
|
||||||
|
### 옵션 D: 복합 (추천 예정)
|
||||||
|
- **전월 대비**: 카드에 숫자로 표시
|
||||||
|
- **3개월 이동평균**: 그래프에 추세선으로 표시
|
||||||
|
- **추세 판정**: 이동평균 기울기로
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 TODO
|
||||||
|
|
||||||
|
- [ ] 전월 대비 수치 추가
|
||||||
|
- [ ] 이동평균 추세선 그래프에 표시
|
||||||
|
- [ ] 계절성 고려 (전년 동월 대비)
|
||||||
|
- [ ] 일별 분석 시 7일 이동평균 적용
|
||||||
|
- [ ] 추세 판정 기준값 튜닝 (현재 ±10%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 변경 내용 |
|
||||||
|
|------|-----------|
|
||||||
|
| 2026-03-13 | 이중 Y축 그래프 최초 구현 (전반부/후반부 합) |
|
||||||
|
| 2026-03-13 | 추세 로직 수정 (최근 3개월 평균으로 변경) |
|
||||||
|
| 2026-03-13 | 최적화 노트 문서 생성 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖼️ 참고: 이전 버전 그래프
|
||||||
|
|
||||||
|
### 재고 변화만 (단일 Y축) - `2ca35cd`
|
||||||
|
- 보라색 라인 하나로 재고 추이만 표시
|
||||||
|
- 깔끔하고 심플함
|
||||||
|
- 사용량 정보 없음
|
||||||
|
|
||||||
|
### 이중 Y축 v1 - `0b81999`
|
||||||
|
- 왼쪽 Y축: 재고량 (보라색 라인)
|
||||||
|
- 오른쪽 Y축: 처방 사용량 (파란색 바)
|
||||||
|
- 추세 분석: 전반부/후반부 합 비교 (문제 있음)
|
||||||
Loading…
Reference in New Issue
Block a user