이동평균으로 합성 센서 데이터를 평활화하고 지연 확인하기
합성 가속도 데이터에 3샘플 후행 이동평균을 적용하고 모든 필터 값을 손으로 검증하며, 필터의 1샘플 정렬 지연을 명시적으로 확인합니다. 원본 CSV는 변경하지 않고 처리 결과를 새 폴더에 저장합니다.
합성 차량 속도와 가속도 데이터를 일관된 SI 단위로 변환한 뒤 역변환하여 계산을 확인합니다. 원본 CSV는 그대로 유지하고 변환된 데이터는 새 출력 폴더에 저장합니다.
이런 분께 맞아요속도와 가속도 단위가 섞인 차량 또는 시험 데이터를 분석하기 전에 간단한 단위 변환과 검증을 수행하려는 사람을 위한 안내입니다.
이 예제에서는 속도를 m/s, 가속도를 m/s²로 통일합니다. 1 km는 1000 m이고 1시간은 3600초이므로 km/h 값을 3.6으로 나누면 m/s가 됩니다. 반대로 m/s에 3.6을 곱하면 km/h가 됩니다.
가속도는 관례적인 표준중력 값 g0 = 9.80665 m/s²를 사용합니다. g 단위의 값에 9.80665를 곱하면 m/s²가 됩니다. 반대로 m/s²를 9.80665로 나누면 g 단위가 됩니다.
| 변환 | 계산식 |
|---|---|
| km/h → m/s | km/h ÷ 3.6 |
| m/s → km/h | m/s × 3.6 |
| g → m/s² | g × 9.80665 |
| m/s² → g | m/s² ÷ 9.80665 |
아래 데이터는 이 글을 위해 작성한 합성 데이터입니다. unit_test_data.csv로 저장하세요. 속도는 손으로 변환하기 쉬운 값을 사용하고, 가속도에는 양수, 음수, 소수 g 값을 포함했습니다.
sample_id,speed_kmh,accel_g
T001,0,0
T002,36,1
T003,72,-0.5
T004,90,0.25
T005,108,-1.25
레코드는 5개입니다. 음수 가속도는 변환 후에도 음수로 유지됩니다. 단위 변환은 단위만 바꾸며 부호를 바꾸지 않습니다. 이 데이터는 수치 예제일 뿐 특정 차량 거동이나 센서를 나타내는 값이 아닙니다.
속도 변환은 직접 계산할 수 있습니다. 예를 들어 72 km/h ÷ 3.6 = 20 m/s이고, 90 km/h ÷ 3.6 = 25 m/s입니다. 다시 변환하면 20 × 3.6 = 72 km/h, 25 × 3.6 = 90 km/h가 됩니다.
가속도는 1 g가 9.80665 m/s²입니다. -0.5 g는 -4.903325 m/s², 0.25 g는 2.4516625 m/s², -1.25 g는 -12.2583125 m/s²가 됩니다.
| sample_id | speed_kmh | 예상 speed_m_s | accel_g | 예상 accel_m_s2 |
|---|---|---|---|---|
| T001 | 0 | 0.000000 | 0 | 0.000000 |
| T002 | 36 | 10.000000 | 1 | 9.806650 |
| T003 | 72 | 20.000000 | -0.5 | -4.903325 |
| T004 | 90 | 25.000000 | 0.25 | 2.451663 |
| T005 | 108 | 30.000000 | -1.25 | -12.258313 |
표의 가속도 값은 소수점 이하 6자리로 표시했습니다. 실제 계산에서는 이미 반올림된 출력 문자열을 다시 사용하지 말고 9.80665의 전체 값을 사용해야 합니다.
다음 스크립트를 unit_conversion_check.py로 저장하세요. 원본 값을 읽고 두 종류의 정방향 변환을 수행한 뒤 다시 원래 단위로 역변환하여 작은 수치 허용오차 안에서 일치하는지 확인합니다. 원본 CSV는 다시 저장하지 않습니다.
import csv
import math
from pathlib import Path
SOURCE = Path("unit_test_data.csv")
OUTPUT_DIR = Path("outputs") / "unit_conversion_result"
OUTPUT = OUTPUT_DIR / "converted_test_data.csv"
STANDARD_GRAVITY = 9.80665
TOLERANCE = 1e-12
def kmh_to_ms(value: float) -> float:
return value / 3.6
def ms_to_kmh(value: float) -> float:
return value * 3.6
def g_to_ms2(value: float) -> float:
return value * STANDARD_GRAVITY
def ms2_to_g(value: float) -> float:
return value / STANDARD_GRAVITY
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}")
output_rows = []
with SOURCE.open("r", encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream)
required = {"sample_id", "speed_kmh", "accel_g"}
if reader.fieldnames is None or not required.issubset(reader.fieldnames):
raise ValueError("CSV is missing a required column.")
seen_ids = set()
for row in reader:
sample_id = row["sample_id"].strip()
if not sample_id:
raise ValueError("sample_id must not be blank.")
if sample_id in seen_ids:
raise ValueError(f"Duplicate sample_id: {sample_id}")
seen_ids.add(sample_id)
speed_kmh = float(row["speed_kmh"])
accel_g = float(row["accel_g"])
speed_ms = kmh_to_ms(speed_kmh)
accel_ms2 = g_to_ms2(accel_g)
speed_roundtrip = ms_to_kmh(speed_ms)
accel_roundtrip = ms2_to_g(accel_ms2)
speed_ok = math.isclose(
speed_kmh, speed_roundtrip,
rel_tol=TOLERANCE, abs_tol=TOLERANCE
)
accel_ok = math.isclose(
accel_g, accel_roundtrip,
rel_tol=TOLERANCE, abs_tol=TOLERANCE
)
if not speed_ok or not accel_ok:
raise RuntimeError(f"Round-trip check failed: {sample_id}")
output_rows.append({
"sample_id": sample_id,
"speed_kmh": f"{speed_kmh:g}",
"speed_m_s": f"{speed_ms:.6f}",
"speed_roundtrip_kmh": f"{speed_roundtrip:.6f}",
"accel_g": f"{accel_g:g}",
"accel_m_s2": f"{accel_ms2:.6f}",
"accel_roundtrip_g": f"{accel_roundtrip:.6f}",
"roundtrip_check": "PASS",
})
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir()
fields = [
"sample_id", "speed_kmh", "speed_m_s", "speed_roundtrip_kmh",
"accel_g", "accel_m_s2", "accel_roundtrip_g", "roundtrip_check"
]
with OUTPUT.open("x", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=fields)
writer.writeheader()
writer.writerows(output_rows)
print(f"Converted records: {len(output_rows)}.")
print(f"Round-trip checks passed: {len(output_rows)}.")
print(f"Standard gravity used: {STANDARD_GRAVITY} m/s^2.")
print(f"Output: {OUTPUT.as_posix()}")
if __name__ == "__main__":
main()
모든 행은 역변환 후 원래 속도와 가속도로 돌아와야 합니다. 아래에는 소수점 이하 6자리로 표시한 예상 결과가 있습니다. 내부 계산은 문자열로 서식화하기 전의 부동소수점 값을 사용합니다.
sample_id,speed_kmh,speed_m_s,speed_roundtrip_kmh,accel_g,accel_m_s2,accel_roundtrip_g,roundtrip_check
T001,0,0.000000,0.000000,0,0.000000,0.000000,PASS
T002,36,10.000000,36.000000,1,9.806650,1.000000,PASS
T003,72,20.000000,72.000000,-0.5,-4.903325,-0.500000,PASS
T004,90,25.000000,90.000000,0.25,2.451663,0.250000,PASS
T005,108,30.000000,108.000000,-1.25,-12.258313,-1.250000,PASS
아래 예상 콘솔 출력은 합성 데이터와 스크립트를 바탕으로 손으로 도출한 결과이며 실제 실행 로그가 아닙니다.
Converted records: 5.
Round-trip checks passed: 5.
Standard gravity used: 9.80665 m/s^2.
Output: outputs/unit_conversion_result/converted_test_data.csv| 문제 | 확인할 사항 |
|---|---|
| 속도가 약 3.6배 차이남 | m/s 값을 km/h로 잘못 처리했거나 그 반대인지 확인하세요. |
| 가속도가 약 9.8배 차이남 | g와 m/s²를 혼동했는지 확인하세요. |
| 음수 가속도가 양수로 바뀜 | 분석에서 크기만 필요한 경우가 아니라면 절댓값을 적용하지 마세요. |
| 예상하지 못한 반올림 차이 | 계산에는 충분한 정밀도를 유지하고 표시나 내보내기 단계에서만 반올림하세요. |
| 역변환은 통과하지만 원본 단위 라벨이 틀림 | 수학적으로 가역적인 변환은 원본의 잘못된 단위 표기를 탐지할 수 없습니다. |
단위 변환이 정상적으로 끝났다는 것은 계산과 선언된 단위가 내부적으로 일관된다는 뜻일 뿐입니다. 원본 센서가 제대로 교정되었는지, 단위 라벨이 정확했는지, 기록된 값이 의도한 물리량을 나타내는지는 증명하지 않습니다.
여기서는 g 변환에 관례적인 표준중력 9.80665 m/s²를 사용합니다. 실제 지역의 중력가속도는 이 관례적인 기준과 다를 수 있습니다. 많은 공학 데이터에서는 g를 정규화된 가속도 단위로 사용하므로 어떤 기준을 사용했는지 문서화해야 합니다.
실제 차량 시험 데이터에는 휠 속도, 차량 속도, 회전속도, 힘, 압력, 온도 등 여러 단위가 함께 있을 수 있습니다. 각 채널의 물리 차원을 데이터 사전에 기록하고 그에 맞게 변환하세요. 숫자의 크기만 보고 단위를 추정해서는 안 됩니다.
2026-09-20 · 예제 수동 검토 · 대상: Python 3.12 · 표준 라이브러리: csv, math, pathlib · 실행하지 않음
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.