자동차·데이터 실무

이동평균으로 합성 센서 데이터를 평활화하고 지연 확인하기

합성 가속도 데이터에 3샘플 후행 이동평균을 적용하고 모든 필터 값을 손으로 검증하며, 필터의 1샘플 정렬 지연을 명시적으로 확인합니다. 원본 CSV는 변경하지 않고 처리 결과를 새 폴더에 저장합니다.

목차 보기

이런 분께 맞아요자동차 또는 시험 센서 데이터에 간단한 1차 평활화를 적용하면서 필터가 만드는 시간 지연까지 이해하려는 사람을 위한 안내입니다.

준비할 것
  • Python 3.12와 해당 버전을 실행하는 터미널 명령이 준비되어 있어야 합니다.
  • UTF-8 CSV와 Python 파일을 저장할 수 있는 텍스트 편집기가 필요합니다.
  • 센서 값이 일정한 시간 간격의 샘플로 기록된다는 기본 개념을 알고 있어야 합니다.
  • Python 표준 라이브러리인 csv와 pathlib만 사용합니다.

01이동평균과 시간 기준 정의하기

이 예제에서는 3샘플 후행 이동평균을 사용합니다. 샘플 n의 필터 값은 현재 샘플과 직전 두 샘플의 평균으로 계산합니다. 즉 ma[n] = (x[n] + x[n-1] + x[n-2]) / 3입니다. 따라서 처음 두 샘플에는 완전한 3샘플 구간이 없습니다.

이동평균은 빠른 샘플 간 변동을 줄이지만 시간 관계도 바꿉니다. 0.2, 0.3, 0.4초의 세 샘플을 포함하는 구간의 중심은 0.3초지만, 인과적인 후행 방식에서는 0.4초 샘플이 들어온 뒤 결과를 계산할 수 있습니다. 샘플 간격이 0.1초라면 구간 중심과 출력 시각의 정렬 차이는 0.1초입니다.

02작은 합성 가속도 신호 만들기

아래 데이터는 이 글을 위해 작성한 합성 데이터입니다. sensor_data.csv로 저장하세요. 샘플 간격은 0.1초입니다. 신호는 0 부근에서 시작한 뒤 9 m/s^2 부근으로 상승하며, 두 구간 모두에 의도적인 변동을 넣었습니다.

csv
time_s,accel_m_s2
0.0,0
0.1,3
0.2,-3
0.3,0
0.4,9
0.5,12
0.6,6
0.7,9

샘플은 총 8개입니다. 0.4초 전의 원본 값은 0, 3, -3, 0이고, 0.4초부터는 9, 12, 6, 9입니다. 계산을 쉽게 확인하기 위한 단순한 합성 데이터이며 특정 차량 센서나 실제 노이즈 분포를 나타낸다고 주장하지 않습니다.

03필터 값을 손으로 계산하기

첫 번째 완전한 구간은 0.2초에서 끝납니다. 계산은 (0 + 3 - 3) / 3 = 0입니다. 다음 구간은 (3 - 3 + 0) / 3 = 0입니다. 원본 신호가 0.4초에서 처음 9가 되었을 때 구간 값은 -3, 0, 9이므로 필터 결과는 9가 아니라 2입니다.

출력 시각세 샘플MA3구간 중심정렬 지연
0.2 s0, 3, -30.0000.1 s0.1 s
0.3 s3, -3, 00.0000.2 s0.1 s
0.4 s-3, 0, 92.0000.3 s0.1 s
0.5 s0, 9, 127.0000.4 s0.1 s
0.6 s9, 12, 69.0000.5 s0.1 s
0.7 s12, 6, 99.0000.6 s0.1 s

변화 구간에서 필터 출력은 0에서 바로 9로 이동하지 않고 0, 2, 7, 9처럼 여러 샘플에 걸쳐 상승합니다. 이것이 평활화 효과이며, 동시에 다른 센서 채널과 비교할 때 시간 정렬을 고려해야 하는 이유입니다.

04필터를 적용하고 정렬 지연 기록하기

다음 스크립트를 moving_average_filter.py로 저장하세요. 타임스탬프가 증가하며 0.1초의 일정한 간격을 갖는지 검사하고, 3샘플 후행 평균을 계산해 새 CSV에 저장합니다. 기존 출력 폴더는 재사용하지 않습니다.

python
import csv
from pathlib import Path

SOURCE = Path("sensor_data.csv")
OUTPUT_DIR = Path("outputs") / "moving_average_result"
OUTPUT = OUTPUT_DIR / "filtered_sensor_data.csv"
WINDOW = 3
EXPECTED_DT = 0.1
TOLERANCE = 1e-9


def main() -> None:
    if not SOURCE.is_file():
        raise FileNotFoundError(f"Source CSV not found: {SOURCE}")
    if OUTPUT_DIR.exists():
        raise FileExistsError(f"Output folder already exists: {OUTPUT_DIR}")
    if WINDOW < 1 or WINDOW % 2 == 0:
        raise ValueError("WINDOW must be a positive odd integer.")

    samples = []
    with SOURCE.open("r", encoding="utf-8-sig", newline="") as stream:
        reader = csv.DictReader(stream)
        required = {"time_s", "accel_m_s2"}
        if reader.fieldnames is None or not required.issubset(reader.fieldnames):
            raise ValueError("CSV is missing a required column.")

        for row in reader:
            samples.append((float(row["time_s"]), float(row["accel_m_s2"])))

    if len(samples) < WINDOW:
        raise ValueError("Not enough samples for the moving-average window.")

    for index in range(1, len(samples)):
        dt = samples[index][0] - samples[index - 1][0]
        if abs(dt - EXPECTED_DT) > TOLERANCE:
            raise ValueError(f"Unexpected sample interval at row {index + 2}: {dt}")

    half_window = WINDOW // 2
    output_rows = []

    for index, (time_s, raw_value) in enumerate(samples):
        if index < WINDOW - 1:
            average_text = ""
            center_text = ""
            delay_text = ""
        else:
            window_values = [value for _, value in samples[index - WINDOW + 1:index + 1]]
            average = sum(window_values) / WINDOW
            center_time = samples[index - half_window][0]
            alignment_delay = time_s - center_time
            average_text = f"{average:.3f}"
            center_text = f"{center_time:.1f}"
            delay_text = f"{alignment_delay:.1f}"

        output_rows.append({
            "time_s": f"{time_s:.1f}",
            "raw_accel_m_s2": f"{raw_value:g}",
            "ma3_accel_m_s2": average_text,
            "window_center_time_s": center_text,
            "alignment_delay_s": delay_text,
        })

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()
    fields = [
        "time_s", "raw_accel_m_s2", "ma3_accel_m_s2",
        "window_center_time_s", "alignment_delay_s"
    ]
    with OUTPUT.open("x", encoding="utf-8", newline="") as stream:
        writer = csv.DictWriter(stream, fieldnames=fields)
        writer.writeheader()
        writer.writerows(output_rows)

    valid_outputs = len(samples) - WINDOW + 1
    print(f"Input samples: {len(samples)}.")
    print(f"MA{WINDOW} outputs with full windows: {valid_outputs}.")
    print(f"Nominal alignment delay: {half_window * EXPECTED_DT:.1f} s.")
    print(f"Output: {OUTPUT.as_posix()}")


if __name__ == "__main__":
    main()

05예상 결과와 비교하기

처음 두 행은 완전한 3샘플 구간이 없으므로 필터 열이 비어 있어야 합니다. 이후 여섯 필터 값은 0.000, 0.000, 2.000, 7.000, 9.000, 9.000이어야 합니다.

csv
time_s,raw_accel_m_s2,ma3_accel_m_s2,window_center_time_s,alignment_delay_s
0.0,0,,,
0.1,3,,,
0.2,-3,0.000,0.1,0.1
0.3,0,0.000,0.2,0.1
0.4,9,2.000,0.3,0.1
0.5,12,7.000,0.4,0.1
0.6,6,9.000,0.5,0.1
0.7,9,9.000,0.6,0.1

아래 예상 콘솔 출력은 합성 데이터와 코드를 바탕으로 손으로 도출한 결과이며 실제 실행 로그가 아닙니다.

text
Input samples: 8.
MA3 outputs with full windows: 6.
Nominal alignment delay: 0.1 s.
Output: outputs/moving_average_result/filtered_sensor_data.csv

06결과와 자주 발생하는 오류 확인하기

  • 첫 완전 평균을 직접 확인합니다. (0 + 3 - 3) / 3 = 0입니다.
  • 0.4초 변화 구간을 확인합니다. (-3 + 0 + 9) / 3 = 2입니다.
  • 8개 샘플에 3샘플 창을 적용했을 때 완전한 출력이 6개인지 확인합니다.
  • 모든 구간 중심과 출력 시각의 차이가 0.1초인지 확인합니다.
  • OUTPUT_DIR을 바꾸지 않고 다시 실행합니다. 기존 결과를 덮어쓰지 않고 FileExistsError로 중단되어야 합니다.
문제확인할 사항
예상하지 못한 샘플 간격누락 샘플, 중복 타임스탬프, 잘못된 EXPECTED_DT 값을 확인하세요.
필터 신호가 늦어 보임후행 구간은 출력 타임스탬프보다 앞선 중심 시각의 데이터를 요약한다는 점을 확인하세요.
중요한 피크가 작아짐이동평균은 짧은 이벤트를 주변 샘플로 분산시킬 수 있습니다.
평활화가 너무 강함구간 길이를 줄이고 노이즈 감소와 시간 특성을 다시 비교하세요.
평활화가 부족함더 긴 구간이 도움이 될 수 있지만 시간 퍼짐과 명목 지연도 커집니다.

07실제 시험 데이터에 적용하기 전 한계 이해하기

이동평균은 단순한 저역통과형 평활화 방법이며 모든 센서 문제에 적합한 필터는 아닙니다. 빠른 변동을 줄일 수 있지만 피크를 낮추고 짧은 이벤트의 시작과 끝을 흐릴 수도 있습니다. 진동, 충격, 고장 검출, 임계값 기반 분석에서는 특히 중요합니다.

여기서 말하는 0.1초 지연은 0.1초 간격으로 샘플링한 3샘플 후행 구간에서 구간 중심 시각과 결과를 사용할 수 있는 출력 시각의 차이입니다. 모든 신호 특징이나 모든 필터 구현에 동일하게 적용되는 보편적인 지연으로 해석하면 안 됩니다.

실제 자동차 데이터에는 비균일 샘플링, 누락 샘플, 센서 바이어스, 포화, 여러 동기화 채널, 특정 주파수 성분 등이 있을 수 있습니다. 이러한 경우 샘플링 주파수와 필요한 대역폭을 기준으로 필터를 설계해야 할 수 있습니다. 공학적 결론을 내리기 전에 원본 신호를 보존하고 필터 방법을 기록하세요.

실행·검증 기록

2026-09-20 · 예제 수동 검토 · 대상: Python 3.12 · 표준 라이브러리: csv, pathlib · 실행하지 않음

  • 합성 가속도 샘플 8개와 일정한 0.1초 샘플 간격을 수동으로 확인했습니다.
  • 완전한 3샘플 이동평균 6개를 0, 0, 2, 7, 9, 9로 수동 계산했습니다.
  • 3샘플 구간의 중심이 후행 출력 타임스탬프보다 한 샘플 앞에 있음을 수동으로 확인했습니다.
  • 명목상 중심-출력 정렬 차이를 1샘플 × 0.1초 = 0.1초로 계산했습니다.
  • 입력 열 검사, 샘플 간격 검사, 출력 충돌 방지, 원본 CSV 보존 로직을 코드로 검토했습니다.
  • 예상 출력 CSV와 콘솔 내용을 손으로 도출했습니다.
검증 한계
  • 이 응답의 작성자는 코드를 실행하지 않았으며, 필터링된 CSV를 실제로 만들지 않았습니다.
  • 합성 신호는 특정 차량, 가속도 센서, 실제 노이즈 분포를 나타낸다고 주장하지 않습니다.
  • 임의 주파수에서의 주파수 응답과 위상 응답, 비균일 샘플링, 누락 샘플, 대규모 데이터는 시험하지 않았습니다.
  • 공식 문서 URL은 알려진 문서 위치를 사용했지만 실시간으로 접속해 확인하지 않았습니다.

사이트 전체 작성·검증 원칙

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.