자동차 시험 데이터의 km/h, m/s, g, m/s² 단위를 변환하고 검증하기
합성 차량 속도와 가속도 데이터를 일관된 SI 단위로 변환한 뒤 역변환하여 계산을 확인합니다. 원본 CSV는 그대로 유지하고 변환된 데이터는 새 출력 폴더에 저장합니다.
합성 가속도 데이터에 3샘플 후행 이동평균을 적용하고 모든 필터 값을 손으로 검증하며, 필터의 1샘플 정렬 지연을 명시적으로 확인합니다. 원본 CSV는 변경하지 않고 처리 결과를 새 폴더에 저장합니다.
이런 분께 맞아요자동차 또는 시험 센서 데이터에 간단한 1차 평활화를 적용하면서 필터가 만드는 시간 지연까지 이해하려는 사람을 위한 안내입니다.
이 예제에서는 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초입니다.
아래 데이터는 이 글을 위해 작성한 합성 데이터입니다. sensor_data.csv로 저장하세요. 샘플 간격은 0.1초입니다. 신호는 0 부근에서 시작한 뒤 9 m/s^2 부근으로 상승하며, 두 구간 모두에 의도적인 변동을 넣었습니다.
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입니다. 계산을 쉽게 확인하기 위한 단순한 합성 데이터이며 특정 차량 센서나 실제 노이즈 분포를 나타낸다고 주장하지 않습니다.
첫 번째 완전한 구간은 0.2초에서 끝납니다. 계산은 (0 + 3 - 3) / 3 = 0입니다. 다음 구간은 (3 - 3 + 0) / 3 = 0입니다. 원본 신호가 0.4초에서 처음 9가 되었을 때 구간 값은 -3, 0, 9이므로 필터 결과는 9가 아니라 2입니다.
| 출력 시각 | 세 샘플 | MA3 | 구간 중심 | 정렬 지연 |
|---|---|---|---|---|
| 0.2 s | 0, 3, -3 | 0.000 | 0.1 s | 0.1 s |
| 0.3 s | 3, -3, 0 | 0.000 | 0.2 s | 0.1 s |
| 0.4 s | -3, 0, 9 | 2.000 | 0.3 s | 0.1 s |
| 0.5 s | 0, 9, 12 | 7.000 | 0.4 s | 0.1 s |
| 0.6 s | 9, 12, 6 | 9.000 | 0.5 s | 0.1 s |
| 0.7 s | 12, 6, 9 | 9.000 | 0.6 s | 0.1 s |
변화 구간에서 필터 출력은 0에서 바로 9로 이동하지 않고 0, 2, 7, 9처럼 여러 샘플에 걸쳐 상승합니다. 이것이 평활화 효과이며, 동시에 다른 센서 채널과 비교할 때 시간 정렬을 고려해야 하는 이유입니다.
다음 스크립트를 moving_average_filter.py로 저장하세요. 타임스탬프가 증가하며 0.1초의 일정한 간격을 갖는지 검사하고, 3샘플 후행 평균을 계산해 새 CSV에 저장합니다. 기존 출력 폴더는 재사용하지 않습니다.
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()
처음 두 행은 완전한 3샘플 구간이 없으므로 필터 열이 비어 있어야 합니다. 이후 여섯 필터 값은 0.000, 0.000, 2.000, 7.000, 9.000, 9.000이어야 합니다.
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
아래 예상 콘솔 출력은 합성 데이터와 코드를 바탕으로 손으로 도출한 결과이며 실제 실행 로그가 아닙니다.
Input samples: 8.
MA3 outputs with full windows: 6.
Nominal alignment delay: 0.1 s.
Output: outputs/moving_average_result/filtered_sensor_data.csv| 문제 | 확인할 사항 |
|---|---|
| 예상하지 못한 샘플 간격 | 누락 샘플, 중복 타임스탬프, 잘못된 EXPECTED_DT 값을 확인하세요. |
| 필터 신호가 늦어 보임 | 후행 구간은 출력 타임스탬프보다 앞선 중심 시각의 데이터를 요약한다는 점을 확인하세요. |
| 중요한 피크가 작아짐 | 이동평균은 짧은 이벤트를 주변 샘플로 분산시킬 수 있습니다. |
| 평활화가 너무 강함 | 구간 길이를 줄이고 노이즈 감소와 시간 특성을 다시 비교하세요. |
| 평활화가 부족함 | 더 긴 구간이 도움이 될 수 있지만 시간 퍼짐과 명목 지연도 커집니다. |
이동평균은 단순한 저역통과형 평활화 방법이며 모든 센서 문제에 적합한 필터는 아닙니다. 빠른 변동을 줄일 수 있지만 피크를 낮추고 짧은 이벤트의 시작과 끝을 흐릴 수도 있습니다. 진동, 충격, 고장 검출, 임계값 기반 분석에서는 특히 중요합니다.
여기서 말하는 0.1초 지연은 0.1초 간격으로 샘플링한 3샘플 후행 구간에서 구간 중심 시각과 결과를 사용할 수 있는 출력 시각의 차이입니다. 모든 신호 특징이나 모든 필터 구현에 동일하게 적용되는 보편적인 지연으로 해석하면 안 됩니다.
실제 자동차 데이터에는 비균일 샘플링, 누락 샘플, 센서 바이어스, 포화, 여러 동기화 채널, 특정 주파수 성분 등이 있을 수 있습니다. 이러한 경우 샘플링 주파수와 필요한 대역폭을 기준으로 필터를 설계해야 할 수 있습니다. 공학적 결론을 내리기 전에 원본 신호를 보존하고 필터 방법을 기록하세요.
2026-09-20 · 예제 수동 검토 · 대상: Python 3.12 · 표준 라이브러리: csv, pathlib · 실행하지 않음
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.