잡음과 오래된 값
85분 안팎
학습 목표
길이 3 이동평균을 구하고 300ms 지난 샘플을 STALE로 표시합니다.
개념
평균값과 신선도는 따로 판단합니다
온도가 1000·3000·5000밀리도로 흔들리면 마지막 숫자만 표시하는 화면은 자주 바뀝니다. 최근 세 번의 성공 값을 평균하면 변동을 완화할 수 있습니다. 그러나 센서가 멈추어도 평균값은 메모리에 남습니다. 이번 레슨은 평균을 계산하는 창과 마지막 성공 시각을 함께 관리합니다. 숫자를 부드럽게 만드는 일과 지금 사용할 수 있는지 판단하는 일을 분리해야 연결 장애를 숨기지 않습니다.
필터 입력은 온도 원시 바이트가 아니라 앞 단계에서 변환한 정수 밀리도입니다. 허용 범위는 SENSOR-48 표현에 맞추어 -128000~127996입니다. 이동평균 창은 최대 세 개이며 새 성공 값이 들어오면 가장 오래된 값을 제거합니다. 시작할 때는 받은 개수만으로 나눕니다. 아직 두 개만 받았는데 항상 3으로 나누면 값이 작아집니다. 부족한 칸에 0을 넣거나 첫 값으로 채우는 다른 정책과 혼동하지 않습니다.
처음 값 1000은 평균 1000, 다음 3000을 받으면 2000, 다음 5000이면 3000입니다. 네 번째 7000이 오면 1000을 빼고 3000·5000·7000의 평균 5000입니다. 세 값이 모두 7000으로 교체되어야 출력도 7000이 됩니다. 계단 변화에서는 과거 값 때문에 출력이 늦게 따라갑니다. 창을 크게 한다고 모든 상황의 판단이 좋아지는 것은 아니며 반응 요구와 함께 선택합니다.
정수 평균은 0 방향으로 버립니다. 합 -3을 두 값으로 나누면 -1이며 Python의 -3 // 2 결과 -2와 다릅니다. abs(total) // count를 구한 뒤 원래 합이 음수면 부호를 붙입니다. float를 거치지 않으므로 정수 계약이 드러납니다. C 미션은 int64_t 합을 만들어 int64_t 개수로 나눕니다. 합과 분모의 부호 타입이 섞여 음수가 unsigned로 바뀌지 않도록 확인합니다.
신선도 기준은 마지막 성공 샘플로부터 300ms 미만이면 FRESH, 300ms 이상이면 STALE입니다. 정확히 300에 오래됨으로 바뀌는 자체 정책입니다. 평균에 들어 있는 세 값 중 가장 오래된 시각이 아니라 마지막 새 값의 시각으로 판단합니다. 따라서 FRESH라고 모든 창 원소가 300ms 미만이라는 보장은 없습니다. 프로젝트 요구가 각 원소의 나이 제한이라면 창 원소에도 시각을 저장하고 다른 검사를 추가해야 합니다.
샘플이 한 번도 없거나 RESET으로 비운 상태는 EMPTY입니다. 0밀리도는 정상 숫자이므로 EMPTY를 0으로 표시하지 않습니다. STALE도 0으로 바꾸지 않습니다. 데이터가 없는 상태, 오래된 상태, 현재 값이 0인 상태가 서로 구별되어야 합니다. 이 레슨에서는 FRESH 뒤에만 평균 정수를 출력합니다. 화면 색이나 숫자 모양 대신 상태 표식을 채점 가능한 계약으로 사용합니다.
브라우저는 줄마다 명령을 처리합니다. S 시각 값은 성공한 샘플을 추가하고 Q 시각은 창을 바꾸지 않고 조회하며 RESET 시각은 창과 마지막 성공 시각을 비웁니다. 각 유효 명령은 현재 상태 한 줄을 출력합니다. 예를 들어 S 100 -500, Q 399, Q 400은 FRESH -500, FRESH -500, STALE입니다. 실패한 센서 읽기는 S로 입력하지 않는다는 계약을 지킵니다. 실패를 온도 0으로 보내면 평균을 오염시킵니다.
시각은 십진 정수 0~4294967295이고 입력 줄 사이에 감소하지 않습니다. 같은 시각의 여러 성공 값은 허용합니다. 이 Python 연습은 순환하지 않는 한 구간을 다루며 C 미션의 uint32 순환 계산과 범위가 다릅니다. 잘못된 숫자·명령·토큰 수·범위·역행은 INVALID를 출력하고 창과 마지막 처리 시각을 보존합니다. invalid 뒤 다음 줄이 정상 처리되는지 검사합니다.
새 S가 왔을 때 직전 성공과 간격이 300ms 이상이면 기존 창을 먼저 비웁니다. 긴 단절 뒤 새 10000을 받았는데 과거 1000과 평균 5500으로 시작하지 않으려는 정책입니다. Q가 STALE을 출력해도 창을 바꾸지 않으며 다음 S의 간격 검사에서 초기화합니다. RESET은 명시적으로 즉시 비웁니다. 자동 간격 초기화와 분리·재연결의 RESET을 같은 결과를 내는 두 조건으로 설명합니다.
입력 검사는 값 추가보다 먼저 끝냅니다. 시각이 정상인데 값이 문자이거나 범위 밖이면 previous도 갱신하지 않습니다. 유효한 명령만 마지막 처리 시각을 전진시킵니다. 오류를 잡는 except가 넓으면 개발 중 다른 버그까지 INVALID로 숨길 수 있으므로 ValueError만 처리합니다. IndexError가 보이면 토큰 수 검사가 빠진 부분을 찾습니다. 정상 입력을 더 많이 넣는 것으로 경계 오류가 해결되지 않습니다.
자료 구조는 window 리스트, last 성공 시각, previous 마지막 유효 명령 시각 세 개입니다. last를 Q마다 now로 바꾸면 새 측정이 없는데 계속 FRESH가 됩니다. previous를 last처럼 쓰면 역행 검사와 신선도 검사가 섞입니다. 값 목록만 두고 시각을 저장하지 않는 구현은 STALE을 계산할 수 없습니다. 변수마다 누가 언제 쓰는지 적으면 반복 조회가 데이터를 갱신하는 오류를 예방합니다.
starter는 모든 유효해 보이는 줄에 EMPTY를 출력합니다. 첫 S와 이후 Q를 처리하는 상태를 추가하고 실제 개수 평균, 창 길이 제한, 나이 경계를 차례로 구현합니다. 처음에는 양수 네 값으로 창 이동을 확인하고 다음에 -1·-2로 버림 방향을 확인합니다. 기대가 -1인데 -2가 나오면 Python //를 그대로 쓴 부분을 확인합니다. STALE이 301에야 나오면 크거나 같음 경계를 다시 읽습니다.
시험 입력에는 초기 Q, RESET 뒤 Q, 299/300ms 경계, 네 번째 샘플, 음수 평균, 긴 간격 뒤 새 값이 들어갑니다. 정상 값 0도 별도로 넣습니다. 역행 실패 뒤 조회에서 기존 값이 보존되는지 확인하면 입력 오류의 부작용까지 검사할 수 있습니다. 모든 테스트를 서로 다른 독립 실행으로 두어 한 테스트의 마지막 상태가 다음 테스트에 섞이지 않게 합니다. 한 입력 안의 줄들은 의도적으로 상태를 공유합니다.
따라하기에서는 값 배열을 직접 출력하여 어느 값이 빠지는지 관찰합니다. 평균 숫자만 보면 창에 네 개를 남겨 둔 오류를 놓칠 수 있습니다. 마지막 성공 100과 조회 399·400을 대조하면 나이 계산에 조회 시각을 사용했는지 확인됩니다. 새 S 400 10000에서 창이 한 개로 재시작하는 이유도 확인합니다. 출력 표식 뒤의 정수는 밀리도이며 1000을 1.0으로 바꾸면 계약 단위가 달라집니다.
미션의 이동평균은 성공한 MEASURE에서 갱신하고 읽기 실패·정지·분리·재연결에서 비웁니다. TRANSMIT이 늦어 300ms를 넘으면 그 pending 샘플을 공개하지 않습니다. 숫자가 매끄럽다는 이유로 전송 제한을 건너뛰지 않습니다. 기존 UART 배열은 이전 정상 기록으로 보존하지만 새 프레임 공개 여부는 별도 길이 증가로 판단합니다. 저장된 과거 데이터와 현재 사용 가능한 데이터를 구별합니다.
필터 시험은 실제 센서 잡음의 통계 특성을 증명하지 않습니다. 고정된 자체 입력에서 창 이동과 경계를 검증합니다. 실물에는 샘플 주기, 교정, 급변 시 반응, 허용 오차를 추가 측정해야 합니다. 더 읽기의 필터 장은 이동평균과 지수평균의 차이를 비교하는 데 사용하며 NumPy 코드를 여기 옮기지 않습니다. 이 실습은 표준 라이브러리와 정수 연산으로 데이터 계약을 직접 구현합니다.
따라하기
최근 세 값 이동
처음에는 실제 개수로 나누고 네 번째부터 가장 오래된 값을 뺍니다.
window = []
for value in (1000, 3000, 5000, 7000):
window.append(value)
window = window[-3:]
print(window, sum(window) // len(window))
실행 결과
[1000] 1000 [1000, 3000] 2000 [1000, 3000, 5000] 3000 [3000, 5000, 7000] 5000
신선도 포함 경계
마지막 성공은 100입니다. 조회를 새 측정 시각으로 저장하지 않습니다.
last = 100
for now in (399, 400, 401):
print(now, "FRESH" if now - last < 300 else "STALE")
실행 결과
399 FRESH 400 STALE 401 STALE
음수 평균 버림
-1·-2의 정수 평균은 0 방향으로 -1입니다. 내림 결과와 대조합니다.
total, count = -3, 2
print("floor", total // count)
print("toward-zero", -(abs(total) // count))
실행 결과
floor -2 toward-zero -1
단절 뒤 창 재시작
300ms 간격 뒤에는 이전 값과 섞지 않습니다.
window, last, now, value = [1000], 0, 300, 10000
if now - last >= 300:
window.clear()
window.append(value)
print("FRESH", sum(window) // len(window))
실행 결과
FRESH 10000
확인 문제
실습
줄별 S 시각 밀리도, Q 시각, RESET 시각을 처리합니다. 최근 성공 3개를 실제 개수로 나누어 0 방향 버림합니다. 나이 300ms 이상은 STALE, 값 없음은 EMPTY, 그 외 FRESH 정수입니다. 새 S의 성공 간격 300ms 이상과 RESET은 창을 비웁니다. 시각은 0~4294967295이며 감소하지 않습니다. 밀리도 -128000~127996. 형식·범위·역행은 INVALID이며 상태를 보존합니다. 입력 줄이 하나도 없으면 출력이 없고 빈 줄은 INVALID입니다.
모범 답안
import sys
window = []
last = None
previous = -1
for line in sys.stdin:
v = line.split()
try:
if len(v) not in (2, 3) or v[0] not in {"S", "Q", "RESET"}:
raise ValueError()
now = int(v[1])
if not 0 <= now <= 4294967295 or now < previous:
raise ValueError()
if v[0] == "S" and len(v) == 3:
value = int(v[2])
if not -128000 <= value <= 127996:
raise ValueError()
if last is not None and now - last >= 300:
window.clear()
window.append(value)
window = window[-3:]
last = now
elif v[0] == "RESET" and len(v) == 2:
window.clear()
last = None
elif v[0] != "Q" or len(v) != 2:
raise ValueError()
previous = now
if last is None:
print("EMPTY")
elif now - last >= 300:
print("STALE")
else:
total = sum(window)
avg = abs(total) // len(window)
if total < 0:
avg = -avg
print("FRESH", avg)
except ValueError:
print("INVALID")
더 읽기
면접 질문
- 센서 기록기의 상태 머신을 설명해 주시면 됩니다.