카테고리 없음

센서 데이터 3가지 오염 패턴 — 코드로 직접 탐지하는 법

린코더 2026. 5. 14. 11:11

 

이전 글: 센서 데이터는 어디서 오염되는가 — 제조 현장의 3가지 함정

 

#1에서 드리프트, 전환점, 누락 — 세 가지 오염 패턴을 봤습니다. 이번 글에서는 그것을 직접 찾아봅니다.

우리 공장 데이터에도 이런 패턴이 있을까요?
있다면 — 언제부터, 어느 구간에서 시작됐을까요?

3라인 온도 센서 3개월치 데이터 — 세 패턴이 모두 들어있습니다. 코드로 읽고, 각각이 현장에서 무엇을 의미하는지 함께 확인합니다.


탐지 순서에 대해 — 먼저 짚고 갑니다

#1 현장 관찰에서 확인한 것처럼 — 세 패턴이 동시에 존재할 때는 순서가 있습니다.

1단계   누락부터 확인합니다 — 없는 데이터를 먼저 파악해야 있는 데이터를 올바르게 해석할 수 있습니다

2단계   전환점을 확인합니다 — 기준 자체가 이동했다면, 이전과 이후를 같은 기준으로 비교할 수 없습니다

3단계   드리프트 추세를 봅니다 — 누락과 전환점을 제거한 구간에서 방향성이 있는 변화를 읽습니다

이 순서대로 코드를 실행합니다.


탐지 1 — 누락 시간을 찾습니다

데이터를 열었을 때 행이 몇 개인지 세는 것만으로는 부족합니다. 중요한 것은 타임스탬프 사이의 간격입니다. 정상 수집 주기보다 긴 간격이 있다면 — 그 구간에 데이터가 없는 것입니다.

import pandas as pd

df = pd.read_csv('sensor_temp_line3_2024Q1.csv', parse_dates=['timestamp'])
df = df.sort_values('timestamp').reset_index(drop=True)

# 타임스탬프 간격 계산 (분 단위)
df['gap_min'] = df['timestamp'].diff().dt.total_seconds() / 60

# 정상 수집 주기: 1분. 2배 이상이면 누락 의심
threshold = 2.0
missing_gaps = df[df['gap_min'] > threshold][['timestamp', 'gap_min']]

print(f"누락 의심 구간: {len(missing_gaps)}건")
print(missing_gaps)

코드가 보여주는 것은 간단합니다 — 타임스탬프 간격이 비정상적으로 긴 지점의 목록입니다.

현장에서 읽는 법

간격이 30분이면 — 설비 정지였을 수 있습니다. 5분이면 — 네트워크 순단이었을 수 있습니다. 코드는 시점과 길이를 알려줍니다. 그 시간대에 무슨 일이 있었는지는 — 현장 기록(작업일보, 정지이력)과 대조해 볼 수 있습니다.

누락 구간을 파악하면 — 이후 분석에서 그 구간을 제외하거나 별도로 표시할 수 있습니다. 없는 데이터를 모르고 분석하는 것과, 알고 처리하는 것은 다릅니다.

린코더 범위 안: 누락 구간 탐지 및 시점 확인
린코더 범위 밖: 누락 원인 제거 (네트워크 인프라, 설비 하드웨어)

수집 주기가 설비마다 다르다면 — threshold를 설비별로 따로 정의하는 것이 좋습니다.

탐지 2 — 기준 이동 (Changepoint)을 찾습니다

기준 이동은 교대 교체, 원자재 로트 변경, 설비 교체처럼 — 기준 자체가 이동하는 순간입니다. 값의 수준이 갑자기 달라집니다. 개별 값은 이상하지 않지만, 이전 구간과 이후 구간의 평균이 다릅니다.

# 이동 평균으로 전환점 탐지
window = 30  # 30분 이동 윈도우
df['rolling_mean'] = df['temperature'].rolling(window=window, center=True).mean()
df['rolling_std'] = df['temperature'].rolling(window=window, center=True).std()

# 이동 평균의 급격한 변화 탐지
df['mean_change'] = df['rolling_mean'].diff().abs()
change_threshold = df['rolling_std'].mean() * 2

changepoints = df[df['mean_change'] > change_threshold][['timestamp', 'temperature', 'mean_change']]
print(f"전환점 의심 구간: {len(changepoints)}건")
print(changepoints.head(10))

현장에서 읽는 법

이동평균 방식은 변화가 크고 명확할 때 잘 작동합니다. 변화가 완만하거나 노이즈가 많으면 놓칠 수 있습니다. 출발점으로 사용합니다.

기준 이동이 감지된 시점이 교대 교체 시간과 일치한다면 — 작업자 간 측정 방식 차이를 의심합니다. 원자재 입고 기록과 일치한다면 — 로트 변경이 원인일 수 있습니다. 코드는 시점을 알려줍니다. 원인은 현장 기록과 대조해 볼 수 있습니다.

기준 이동이 확인되면 — 이전 구간과 이후 구간을 별도로 분석하는 것이 좋습니다. 기준 이동을 무시하고 전체 구간을 하나로 보면 — 평균과 추세가 모두 왜곡됩니다.

린코더 범위 안: 기준 이동 시점 탐지 및 구간 분리
린코더 범위 밖: 기준 이동 원인 제거 (작업 표준화, 공급망 관리)

탐지 3 — 드리프트 (Drift) 추세를 읽습니다

누락 구간을 제외하고, 전환점으로 구간을 나눈 다음 — 각 구간 안에서 값이 한 방향으로 지속적으로 이동하고 있는지 확인합니다. 드리프트는 추세입니다. 하루 이틀의 변동이 아니라, 주 단위로 같은 방향으로 움직이는 경향입니다.

from scipy import stats

# sensor_temp_line3_2024Q1.csv 로드
df = pd.read_csv('sensor_temp_line3_2024Q1.csv', parse_dates=['timestamp'])
df = df.sort_values('timestamp').reset_index(drop=True)

# CP2(03/01) 이후 구간 — 누락 제외
segment = df[(df['timestamp'] >= '2024-03-01') & df['temperature'].notna()].copy()

# 선형 회귀로 추세 방향과 기울기 확인
x = (segment['timestamp'] - segment['timestamp'].min()).dt.total_seconds()
slope, intercept, r_value, p_value, std_err = stats.linregress(x, segment['temperature'])

print(f"기울기: {slope * 3600:.4f} °C/시간")
print(f"R²:     {r_value**2:.3f}")
print(f"p-value: {p_value:.4f}")

if p_value < 0.05 and abs(r_value) > 0.3:
    direction = "상승" if slope > 0 else "하락"
    print(f"→ 드리프트 감지: {direction} 추세")
else:
    print(f"→ 유의미한 추세 없음")

# 출력 결과
# 기울기: +0.0120 °C/시간
# R²: 0.986
# p-value: 0.0000
# → 드리프트 감지: 상승 추세

현장에서 읽는 법

기울기 +0.0120°C/시간 — 하루 0.29°C, 한 달이면 약 8.6°C 상승합니다. R² 0.986은 온도 변화의 98.6%가 시간 흐름으로 설명된다는 뜻입니다. 추세가 매우 일관됩니다. p-value 0.0000은 이 추세가 우연이 아닐 가능성이 매우 높다는 의미입니다. 세 숫자가 동시에 이 방향을 가리킨다면 — Calibration 점검 주기를 재검토하는 근거가 될 수 있습니다.

린코더 범위 안: 드리프트 방향·속도·시점 정량화
린코더 범위 밖: 센서 Calibration 실행 (측정 담당자, 설비 엔지니어)

전환점이 여러 개라면 — 각 구간마다 반복 실행합니다. 전환점을 확정하는 방법은 다음 글에서 다룹니다.

코드는 완전한 답을 주지 않습니다.
어디를 봐야 하는지 — 방향을 가리킵니다.
그것만으로도 현장에서는 충분한 출발점이 됩니다.

드리프트가 멈추지 않으면 — 기준값 자체가 밀립니다. 그 순간부터 센서는 정상이라고 말하지만, 공정은 이미 다른 곳에 있습니다. 다음 글에서 확인합니다.

린코더 · Series 2

Prologue  |  #0  |  참고 (MSA)  |  #1  |  #2 (현재)  |  #3