개요

정비 부품 수요예측 모델을 개선하다가, 운용시간으로 “기대 수량”을 계산해서 모델에 칸 하나로 넣어봤더니 수량 정확도가 올랐다.

재밌었던 건 같은 재료(과거 수량, 운용시간)를 그냥 칸으로 따로 넣었을 땐 아무 효과가 없었다는 점이다. 계산 과정이랑 왜 그랬는지를 정리해 둔다.

실제 데이터는 다룰 수 없어서 아래 숫자는 전부 가상의 부품 A로 설명한다.


배경

모델은 흔한 2단계 구조다.

데이터를 보다 보니 몇 년 사이에 장비 전체 운용시간이 많이 줄었고, 부품 소모도 같이 줄어 있었다. 연도별로 보면 운용시간과 부품 소모량의 전년 대비 증감 상관이 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로 계산하면

고장률

\[\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개를 추가했다.

# 품목별, 기준 연도 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 정도

재료만으로는 아무 일도 안 일어났고, 같이 넣으면 오히려 효과가 묽어졌다.

생각해 보면 이유는 이렇다.

결국 “고장은 운용한 만큼 난다”는 걸 사람이 식으로 넣어준 게 핵심이었다.


예전엔 왜 안 됐었나

사실 다른 데이터에서 포아송을 세 번 해봤다가 다 실패했었다.

이번에 된 이유는 두 가지라고 본다.

같은 아이디어라도 어디에 넣느냐, 데이터에 그 변화가 있느냐에 따라 결과가 완전히 달라진다는 걸 다시 느꼈다.