개요
정비 부품 수요예측 모델을 개선하다가, 운용시간으로 “기대 수량”을 계산해서 모델에 칸 하나로 넣어봤더니 수량 정확도가 올랐다.
재밌었던 건 같은 재료(과거 수량, 운용시간)를 그냥 칸으로 따로 넣었을 땐 아무 효과가 없었다는 점이다. 계산 과정이랑 왜 그랬는지를 정리해 둔다.
실제 데이터는 다룰 수 없어서 아래 숫자는 전부 가상의 부품 A로 설명한다.
배경
모델은 흔한 2단계 구조다.
- 1단계: 내년에 이 부품이 나가냐 안 나가냐 (CatBoost 분류)
- 2단계: 나간다면 몇 개냐 (톱다운 + LightGBM 회귀)
데이터를 보다 보니 몇 년 사이에 장비 전체 운용시간이 많이 줄었고, 부품 소모도 같이 줄어 있었다. 연도별로 보면 운용시간과 부품 소모량의 전년 대비 증감 상관이 0.8 정도.
운용을 덜 하면 부품도 덜 나가는 거야 당연한데, 모델이 이걸 제대로 쓰고 있는 것 같지는 않았다.
부품 A
| 연도 | 그해 전체 운용시간 | 부품 A 소모 |
|---|---|---|
| 2022 | 4,400 시간 | 2 개 |
| 2023 | 8,000 시간 | 4 개 |
| 2024 (작년) | 4,450 시간 | 2 개 |
2025년에 몇 개 나갈지 맞히고 싶은 상황.
수량만 보면 작년 2개, 재작년 4개라 2~4개 사이 어딘가로 보인다. 근데 2023년 4개는 운용을 거의 두 배 했던 해의 기록이다. 이걸 그대로 믿으면 많이 잡게 된다.
수량 말고 고장률로 보기
고장률 \(\lambda\) 는 장비를 1시간 운용할 때 그 부품이 평균 몇 개 나가는지다. 값이 너무 작아서 보통 “1만 시간당 몇 개”로 읽는다.
\[\lambda = \frac{\text{소모 수량}}{\text{그동안 운용한 시간}}\]부품 A를 해마다 따로 계산해 보면 2022년 4.5, 2023년 5.0, 2024년 4.5 (1만 시간당). 수량은 2 → 4 → 2로 출렁였는데 고장률로 보면 거의 그대로다.
수량은 그해 운용량을 따라가지만 고장률은 부품 자체 성질에 가까워서 그렇다. 그래서 생각한 게 이거다.
\[q_t \;\xrightarrow{\;\div H_t\;}\; \lambda \;\xrightarrow{\;\times H_{t_0}\;}\; \mu\]과거 수량을 그해 운용시간으로 나눠서 고장률로 모으고, 지금 운용시간을 다시 곱해서 수량으로 돌려놓자.
참고로 MTBF(평균 고장 간격)는 부품 1개가 평균 몇 시간마다 고장 나는지라서 고장률은 그 역수다. 장비에 같은 부품이 \(n\) 개 달려 있으면 \(n\) 배.
\[\lambda = \frac{n}{\mathrm{MTBF}}\]계산
실제로는 최근 10년을 본다. 예측 연도가 \(Y\) 면 기준 연도(작년)는 \(t_0 = Y-1\) 이고, \(t_0\) 까지 기록만 쓴다.
연도 가중치
최근 기록을 더 믿으려고 오래된 해일수록 작게 곱한다. 작년 1, 재작년 0.8, 3년 전 0.64 이런 식.
\[w_t = 0.8^{\,t_0 - t}\]가중 수량, 가중 운용시간
\[c = \sum_t w_t\, q_t, \qquad e = \sum_t w_t\, H_t\]부품 A로 계산하면
- \(c = 2\times1 + 4\times0.8 + 2\times0.64 \approx 6.5\) 개
- \(e = 4450\times1 + 8000\times0.8 + 4400\times0.64 = 13666\) 시간
고장률
\[\lambda = \frac{c}{e} = \frac{6.5}{13666} \approx 0.00047\]1만 시간당 4.7개 정도.
기록 적은 부품 보정
이게 감마-포아송(경험적 베이즈)이라는 건데, 이름이 거창해서 그렇지 하는 일은 단순하다. 기록이 적은 부품은 전체 평균 쪽으로 조금 당긴다.
10년 동안 딱 1번 나간 부품이면 그게 원래 그런 건지 우연인지 알 수가 없어서, 그 값만 믿으면 위험하다.
전체 부품 고장률의 평균을 \(m\), 분산을 \(s^2\) 이라 하면
\[\beta = \frac{m}{s^2 - m/e}, \qquad \alpha = m\beta, \qquad \hat\lambda = \frac{c + \alpha}{e + \beta}\]식을 풀어 보면 결국 자기 값과 평균을 섞는 거다.
\[\hat\lambda = Z\cdot\frac{c}{e} + (1-Z)\cdot m, \qquad Z = \frac{e}{e+\beta}\]\(Z\) 가 자기 기록을 얼마나 믿을지인데, 기록이 길면 1에 가까워지고 짧으면 0 쪽으로 간다.
내년 기대 수량
\[\mu = \hat\lambda \cdot H_{t_0}\]내년 운용시간은 모르니까 작년 값을 쓴다. 부품 A는 \(4.7 \times 0.445 \approx 2.1\), 내년에 평균 2.1개 정도 나간다는 얘기다.
포아송은 그래서 뭘 하는 건데
처음에 나도 헷갈렸던 부분이다. 포아송 분포가 예측을 하는 건 아니다.
평균이 2.1개라고 해서 딱 2개가 나오는 건 아니고, 0개일 때도 4개일 때도 있다. 그 퍼진 정도를 확률로 보여주는 게 포아송이다.
\[P(A = k) = \frac{\mu^{k}\, e^{-\mu}}{k!}\]\(\mu = 2.1\) 이면 대충 이렇다.
| 개수 | 확률 |
|---|---|
| 0 | 12% |
| 1 | 26% |
| 2 | 27% |
| 3 | 19% |
| 4 | 10% |
| 5 이상 | 6% |
정확히 1개 vs 1개 이상
\(\frac{2.1\, e^{-2.1}}{1!} \approx 0.26\) 은 “정확히 1개” 나올 확률이다.
모델에 넣고 싶었던 건 “나가긴 하냐”, 즉 1개 이상이다. 1개, 2개, 3개 … 를 다 더하는 대신 0개일 확률만 빼면 된다.
\[P(A \ge 1) = 1 - P(A = 0) = 1 - e^{-\mu}\]부품 A는 \(1 - e^{-2.1} \approx 0.88\), 88%.
k! 은 왜 붙어 있나
1년을 엄청 잘게 \(n\) 칸으로 쪼개고 칸마다 아주 작은 확률 \(p = \mu/n\) 로 고장 난다고 하면, \(k\) 번 고장 날 확률은 이항분포다.
\[P(A=k) = \binom{n}{k} p^{k}(1-p)^{n-k}, \qquad \binom{n}{k} = \frac{n(n-1)\cdots(n-k+1)}{k!}\]여기서 \(k!\) 은 순서 중복을 없애는 거다. 3월이랑 7월에 고장이 났으면 3월부터 세든 7월부터 세든 같은 결과니까 2!로 나눈다.
\(n\) 을 무한대로 보내면 포아송 식이 나온다. \(k!\) 덕분에 확률을 다 더하면 1이 되기도 하고.
\[\sum_{k=0}^{\infty}\frac{\mu^{k}}{k!} = e^{\mu}\]모델에 넣기
게이트(나가냐 안 나가냐)는 그대로 두고, 수량 쪽 LightGBM 입력에만 칸 3개를 추가했다.
- 기대 수량: \(\ln(1+\mu)\)
- 1개 이상 나갈 확률: \(1 - e^{-\mu}\)
- 고장률: \(\log_{10}\hat\lambda\)
# 품목별, 기준 연도 cy 까지 기록만 사용
ys = list(range(cy - 9, cy + 1))
w = np.array([0.8 ** (cy - y) for y in ys])
c = (qty_by_year[ys] * w).sum(axis=1) # 품목별 가중 수량
e = float((w * hours_by_year.reindex(ys).to_numpy()).sum()) # 전체 가중 운용시간
lam = c / e
m, v = lam.mean(), lam.var()
beta = m / max(v - m / e, 1e-12)
alpha = m * beta
lam_hat = (c + alpha) / (e + beta)
mu = lam_hat * hours_by_year[cy]
feats["po_mu"] = np.log1p(mu)
feats["po_p"] = 1 - np.exp(-mu)
feats["po_rate"] = np.log10(lam_hat)
학습할 때도 과거 각 연도마다 그 시점까지 기록으로만 같은 칸을 만들어야 한다. 안 그러면 미래 정보가 섞인다.
설계 MTBF가 있는 부품은 같은 계산을 MTBF로도 해서, 기록 적은 부품을 전체 평균 대신 설계값 쪽으로 당기는 데 썼다. 설계 MTBF만 단독으로 넣었을 땐 효과가 없었다. 실제 소모랑 순위가 잘 안 맞았다.
결과
두 평가 연도 모두 수량 정확도가 올랐다. 한 해는 1.6 정도, 다른 해는 조금. 나가냐 안 나가냐는 안 건드렸으니까 그쪽 정확도는 그대로다.
근데 왜 모델이 못 찾았지
재료는 원래 다 있었다. 그래서 식은 빼고 재료만 넣어 봤다.
| 넣은 것 | 수량 정확도 변화 |
|---|---|
| 재료만 (가중 수량, 가중 운용시간, 작년 운용시간) | 거의 0 |
| 포아송으로 계산한 값 | +1.6 정도 |
| 재료 + 계산한 값 | +0.3 정도 |
재료만으로는 아무 일도 안 일어났고, 같이 넣으면 오히려 효과가 묽어졌다.
생각해 보면 이유는 이렇다.
- 트리 모델(GBM)은 “이 값이 몇 이상이면” 하는 식으로 구간을 나눠서 근사한다. 나누기나 곱하기 같은 관계는 데이터가 엄청 많아야 겨우 흉내 낸다.
- 전체 운용시간은 한 시점에서 모든 부품이 같은 값이다. 부품끼리 구분하는 데 못 쓰고, 해마다 다른 값은 학습 연도 수(10개 남짓)만큼밖에 없다.
- 식으로 묶어서 주면 부품마다 다른 숫자 하나가 되니까 모델이 바로 쓸 수 있다.
결국 “고장은 운용한 만큼 난다”는 걸 사람이 식으로 넣어준 게 핵심이었다.
예전엔 왜 안 됐었나
사실 다른 데이터에서 포아송을 세 번 해봤다가 다 실패했었다.
- 부품별 분배 비율을 고장률 × 운용시간으로 잡기: 결국 과거 사용량이랑 같은 값이라 새 정보가 없었다. 신뢰성 데이터북의 이론 고장률은 실제 수요랑 안 맞아서 더 나빠졌다.
- 발생률을 포아송으로 보정해서 1단계(나가냐)에 섞기: 포아송은 발생률이 일정하다고 가정하는데, 실제로는 부품마다 시간이 지나면서 줄어들어서 과하게 높게 잡았다.
- 운용시간 노출로 1단계에 섞기: 그 데이터는 운용 강도가 거의 일정해서 운용시간이 사실상 달력 개월 수랑 같았다(상관 0.95). 운용시간에 정보가 없었다.
이번에 된 이유는 두 가지라고 본다.
- 넣은 곳: 이번에도 1단계에 넣은 건 똑같이 떨어졌다. 효과는 2단계(몇 개냐)에서만 났다.
- 데이터: 기간이 길고 운용시간 자체가 몇 배씩 변한 데이터였다. 운용이 일정하면 시간당으로 바꿔 봤자 달라질 게 없다.
같은 아이디어라도 어디에 넣느냐, 데이터에 그 변화가 있느냐에 따라 결과가 완전히 달라진다는 걸 다시 느꼈다.