학습 목표
- 그래프를 그리기 전 질문, x/y 변수, 단위, 고정 조건을 문장으로 정의한다
- 제목 · 축 라벨 · 단위 · 축 범위를 갖춘 matplotlib 그래프를 작성한다
- NumPy 배열로 데이터를 생성하고 처리한 뒤 시각화 전 self-check를 수행한다
- 데이터 질문에 맞는 그래프 종류(선/산점/막대)를 선택하고, 부적절한 선택도 설명한다
- 축 절단, 라벨 누락, 전처리 은폐, 표본 수 누락 등 오해 유발 요소를 식별하고 수정한다
- 고차원 조건을 2D 그래프로 줄일 때 숨은 변수와 해석 한계를 밝힌다
- 결과를 관찰–원인 가설–시사점–한계 4문장으로 정리한다
그래프 설계 quickstart: 코드 전에 이 6가지를 먼저 답하기
그래프를 그리기 전에 아래 6가지 질문에 먼저 답하세요. 이 답이 없으면 코드가 맞아도 해석이 쉽게 흔들립니다.
- 이 그래프가 답하려는 질문은 무엇인가?
- 무엇이 변하는가? → x축 변수와 단위
- 무엇을 측정하거나 비교하는가? → y축 변수와 단위
- 비교를 공정하게 만들기 위해 어떤 조건을 고정했는가?
- 축 범위, log scale, 확대 표시가 결론을 과장하거나 숨기지 않는가?
- 이 그래프만으로 말할 수 없는 해석 한계는 무엇인가?
1) 공학 그래프 설계 사양서
그래프를 보고서에 넣기 전, 먼저 아래 사양서를 채운다고 생각하세요. 사양서는 코드를 길게 만드는 문서가 아니라 그래프가 오해를 만들지 않도록 막는 안전장치입니다.
| 항목 | 작성할 내용 | 빠지면 생기는 위험 |
|---|---|---|
| 질문 | 이 그림이 답하는 한 문장 | 그림은 있는데 결론이 없음 |
| x/y 변수 | 독립 변수와 측정·비교할 값 | 원인과 결과를 혼동함 |
| 단위 | s, V, dBm, Hz, Ω 등 | 수치 크기 해석 불가 |
| 축 범위/스케일 | 0 기준, 확대, log 여부 | 변화가 과장되거나 묻힘 |
| 고정 조건 | 온도, 거리, 부하, 샘플링 조건 등 | 다른 조건을 같은 데이터처럼 비교함 |
| 전처리 | 평균, 필터링, 이상치 제거 여부 | 원본과 처리 결과를 혼동함 |
| 표본 수/불확실성 | n, 반복 수, 노이즈 범위 | 우연한 차이를 설계 결론으로 오해함 |
| 해석 한계 | 이 그래프로 말할 수 없는 것 | 과잉 결론을 내림 |
2) 설계 사양을 matplotlib 코드로 옮기기
아래 코드는 단순히 선을 그리는 예제가 아닙니다. title, xlabel, ylabel, ylim, legend는 질문, 단위, 축 범위, 계열 구분을 코드로 옮긴 결과입니다.
import matplotlib.pyplot as pltimport numpy as np
t = np.linspace(0, 1, 100) # 시간축 (s)v = 3.3 * np.sin(2 * np.pi * 5 * t) # 5 Hz 정현파 (V)
title = '5 Hz Sine Wave — fixed 0-1 s interval'x_label = 'Time (s)'y_label = 'Voltage (V)'axis_reason = 'y축은 ±3.3 V 신호가 잘리지 않도록 -4~4 V로 설정'fixed_condition = '주파수 5 Hz, 관측 구간 0-1 s'
print('labels_ready:', all([title, x_label, y_label]))print('same_length:', len(t) == len(v))print('axis_reason:', axis_reason)print('fixed_condition:', fixed_condition)
plt.figure(figsize=(7, 4))plt.plot(t, v, linewidth=2, color='royalblue', label='V(t)')plt.title(title)plt.xlabel(x_label)plt.ylabel(y_label)plt.ylim(-4, 4)plt.grid(alpha=0.3)plt.legend()plt.show()코드 전후 체크할 6가지
x와y길이가 같은지 먼저 확인한다 (len(x) == len(y))- 제목은 그래프가 답하는 질문 또는 고정 조건을 드러내게 쓴다
- 축 라벨에는 단위를 반드시 포함한다
- 축 범위, log scale, 확대 표시를 썼다면 이유를 설명한다
- 원본/전처리 결과, 고정 조건, 비교 조건을 구분한다
- 그래프 뒤에 해석 문장과 한계 문장을 붙인다
3) 질문에 맞는 그래프 선택
| 데이터 질문 | 권장 그래프 | 이유 |
|---|---|---|
| “시간에 따라 값이 어떻게 변하나?” | 선 그래프(Line) | 순서, 추세, 기울기 확인 용이 |
| “두 변수의 관계는?” | 산점도(Scatter) | 상관, 분산, 이상점 파악 용이 |
| “범주별 크기 비교는?” | 막대 그래프(Bar) | 카테고리 비교 명확 |
| “값 분포가 어떻게 생겼나?” | 히스토그램(Histogram) | 빈도 분포 확인 가능 |
| “조건이 여러 개인가?” | 작은 여러 그래프 또는 색/마커 1개 추가 | 숨은 변수를 분리해 비교 가능 |
4) 차원의 저주: 2D 그래프가 숨기는 변수
실제 공학 데이터는 한두 개 숫자로 끝나지 않습니다. RSSI를 예로 들면 거리뿐 아니라 안테나 종류, 장애물, 측정 시간, 주변 전파 환경, 송신 출력, 측정 위치가 함께 영향을 줍니다. 하지만 2D 그래프는 보통 x축과 y축 두 변수만 보여 줍니다. 이때 나머지 조건이 섞이면 그럴듯하지만 틀린 상관관계가 생길 수 있습니다.
여기서는 차원의 저주를 많은 조건을 2D 그림 하나로 줄일 때 정보가 사라지는 문제라는 실무적 의미로 사용합니다.
고차원 데이터를 2D 그래프로 줄일 때의 원칙
- 한 그래프는 한 질문만 답하게 한다.
- 비교하지 않는 조건은 제목, 캡션, 출력 문장에 고정 조건으로 적는다.
- 세 번째 변수는 색 또는 마커 하나까지만 조심해서 쓴다.
- 조건이 많으면 한 그림에 몰아넣지 말고 작은 여러 그래프나 표와 그래프로 나눈다.
- 결론 문장에는 “이 조건에서”라는 범위를 붙인다.
- 원인 확정이 아니라 관찰이면 “~때문으로 보인다”처럼 가설로 표현한다.
5) Bad vs Good 그래프 예시
Bad 예시 — 오해 유발
# 예시 코드import matplotlib.pyplot as plt
x = [1, 2, 3, 4]y = [51, 52, 53, 54]
plt.plot(x, y, color='red')plt.ylim(50.8, 54.2) # y축을 과도하게 잘라 변화가 과장됨plt.title('Result') # 제목이 모호함# 축 라벨·단위 없음plt.show()문제점: 축 라벨이 없고, 제목이 모호하며, y축 절단 때문에 작은 변화가 크게 보입니다. 이 그래프만 보면 51V에서 54V로 변한 차이가 심각한 설계 실패처럼 보일 수 있습니다.
matplotlib 저장 결과
아래 이미지는 위 코드처럼 y축을 좁게 잘라 저장한 결과입니다. 실제 변화량보다 변화가 크게 보이는지 확인하세요.
Good 예시 — 신뢰 가능
# 예시 코드import matplotlib.pyplot as plt
x = [1, 2, 3, 4]y = [51, 52, 53, 54]
plt.figure(figsize=(6, 4))plt.plot(x, y, marker='o', linewidth=2, color='royalblue', label='측정값')plt.title('출력 전압 vs 테스트 단계')plt.xlabel('테스트 단계 (-)')plt.ylabel('출력 전압 (V)')plt.ylim(0, 60) # 맥락을 보존하는 축 범위plt.grid(alpha=0.3)plt.legend()plt.show()개선점: 제목이 질문을 드러내고, 축 라벨에 단위가 있으며, y축 범위가 데이터 맥락을 보존합니다. 확대가 필요하다면 전체 범위 그림과 확대 그림을 함께 제시하거나, 축을 잘랐다는 사실을 명시해야 합니다.
matplotlib 저장 결과
아래 이미지는 위 코드처럼 제목, 축 라벨, 단위, y축 범위를 갖춰 저장한 결과입니다. 같은 데이터라도 축 범위와 라벨이 해석을 어떻게 바꾸는지 비교하세요.
6) 엔드-투-엔드 파이프라인: 설계 사양 → 데이터 → 처리 → 그래프 작성
공학 시각화는 보통 다음 4단계로 진행합니다.
- 설계 사양: 질문, 단위, 고정 조건, 해석 한계를 먼저 쓴다
- 데이터 생성/로드: NumPy로 배열 생성 또는 측정값 로드
- 처리와 검증: 정규화, 통계, 필터링을 수행하고 길이·범위를 점검
- 그래프 작성과 해석:
plt.plot,plt.title,plt.xlabel,plt.ylabel로 그림을 완성하고 해석 문장을 붙인다
matplotlib 코드 체크리스트
| 코드 | 역할 | 확인할 점 |
|---|---|---|
plt.figure(figsize=(...)) | 새 그림 영역 만들기 | 너무 작거나 길쭉하지 않은가 |
plt.plot(x, y, ...) | 선 그래프 그리기 | len(x) == len(y)인가 |
plt.scatter(x, y, ...) | 산점도 그리기 | 점의 흩어짐을 봐야 하는 질문인가 |
plt.title(...) | 그래프 제목 | 질문 또는 고정 조건이 드러나는가 |
plt.xlabel(...), plt.ylabel(...) | 축 라벨과 단위 | 단위가 포함되어 있는가 |
plt.ylim(...), plt.xscale(...) | 축 범위와 스케일 | 확대·log scale 이유를 설명할 수 있는가 |
plt.grid(...), plt.legend() | 읽기 보조 요소 | 여러 선이 있을 때 색 외에도 구분되는가 |
plt.show() | 그림 표시 | 그림 뒤에 해석과 한계 문장을 붙였는가 |
import numpy as npimport matplotlib.pyplot as plt
# 1) 설계 사양title = '10 Hz Sine Signal — Raw vs Smoothed'x_label = 'Time (s)'y_label = 'Voltage (V)'fixed_condition = '관측 구간 0-1 s, 같은 난수 seed=42'preprocess_note = '이동 평균 window=10으로 스무딩'interpretation_limit = '시뮬레이션 데이터이므로 실제 센서 잡음 원인을 확정할 수 없음'
# 2) 데이터 생성 (NumPy)rng = np.random.default_rng(42)t = np.linspace(0, 1, 200) # 시간 (s)v = 5.0 * np.sin(2 * np.pi * 10 * t) # 10 Hz 신호 (V)v_noisy = v + 0.4 * rng.normal(size=t.size) # 잡음 추가
# 3) 처리 — 이동 평균 스무딩window = 10v_smooth = np.convolve(v_noisy, np.ones(window) / window, mode='same')
# 4) self-checkprint('labels_ready:', all([title, x_label, y_label]))print('same_length:', len(t) == len(v_noisy) == len(v_smooth))print('fixed_condition:', fixed_condition)print('preprocess_note:', preprocess_note)
# 5) 시각화plt.figure(figsize=(8, 4))plt.plot(t, v_noisy, alpha=0.4, color='gray', linestyle='--', label='Raw noisy')plt.plot(t, v_smooth, linewidth=2, color='royalblue', label='Smoothed')plt.title(title)plt.xlabel(x_label)plt.ylabel(y_label)plt.legend()plt.grid(alpha=0.3)plt.show()
print('한계:', interpretation_limit)7) 해석 문장 템플릿 (4문장)
결과를 정리할 때 아래 4문장 구조를 사용하세요. 관찰과 원인 가설을 분리하면, 그래프를 보고 바로 단정하는 실수를 줄일 수 있습니다.
- 관찰: “A에서 B로 변할 때, C가 증가/감소했다.”
- 원인 가설: “이는 D 특성 때문으로 보인다.”
- 공학적 시사점: “따라서 E 조건에서 운용/설계를 권장한다.”
- 한계: “다만 F 조건은 확인하지 않았으므로, 이 그래프만으로는 G를 확정할 수 없다.”
예시:
- 같은 안테나와 같은 측정 환경에서 거리가 1m에서 6m로 증가할 때, RSSI가 -38 dBm에서 -60 dBm으로 감소했다.
- 이는 자유공간 경로손실이 거리의 로그에 비례해 증가하기 때문으로 보인다.
- 따라서 안정적인 수신을 위해 이 조건에서는 송수신 거리를 3m 이내로 유지하는 설계를 권장한다.
- 다만 장애물, 안테나 방향, 주변 전파 환경을 바꾸어 반복 측정하지 않았으므로 거리 외 요인의 영향은 확정할 수 없다.
예제 1
matplotlib 저장 결과
위 예제의 데이터를 matplotlib 코드로 저장하면 아래와 같습니다. 이 이미지는 예제 1과 같은 50개 점, 같은 전압값을 사용합니다.
예제 1 줄별 해설
- NumPy로 등간격 시간축과 정현파를 생성합니다. W11에서 배운
np.linspace와 vectorization을 그대로 활용합니다. - 제목·축 라벨·데이터 길이를 먼저 점검한 뒤
plt.plot으로 그리면 실수를 줄일 수 있습니다. all([…])로 라벨이 모두 비어 있지 않은지 한 번에 검사합니다.- 플로팅 전에
min/max/mean으로 데이터 범위를 확인하면 축 범위 설정에 도움이 됩니다. axis_reason과fixed_condition을 출력해 축 범위와 실험 조건을 그래프 밖에서도 확인합니다.assert로 x/y 길이와 라벨 존재 여부를 자동 확인하고, 마지막에 해석 한계를 적습니다.
예제 2에서 새로 등장하는 개념
| 개념 | 역할 | 예시 |
|---|---|---|
np.diff(v) / np.diff(t) | 구간 기울기(변화율) 계산 | RSSI 감소율 (dB/m) |
| f-string 출력 | 해석 문장을 코드로 자동 생성 | f"평균 기울기: {slope:.2f}" |
zip(a, b) | 두 배열을 같은 순서의 쌍으로 순회 | for d, r in zip(distance_m, rssi_dbm): |
zip() 이해하기: 인덱스 루프와 비교
두 리스트나 배열을 함께 순회할 때, 인덱스로 접근하는 방식과 zip을 사용하는 방식을 비교해 봅시다.
names = ["sensor_A", "sensor_B", "sensor_C"]values = [22.5, 23.1, 21.8]
# 방법 1: 인덱스로 접근 (이미 알고 있는 방식)for i in range(len(names)): print(names[i], values[i])
# 방법 2: zip 으로 쌍을 꺼내기 (같은 결과, 더 간결)for name, val in zip(names, values): print(name, val)zip(a, b)는 a와 b에서 같은 순서의 원소를 묶어 줍니다. 인덱스 변수 i가 필요 없어 실수가 줄어듭니다.
예제 2
예제 2 matplotlib 정적 그림
아래 정적 그림은 예제 2의 기준 결과입니다. 콘솔에서는 거리·RSSI 표, labels_ready, same_length, fixed_condition, hidden_variable_note, 해석 문장이 서로 맞는지 점검하세요.
예제 2 줄별 해설
- 측정 데이터를 NumPy 배열로 정의합니다.
dtype=float을 명시해 이후 나누기 연산이 정확하게 됩니다. np.diff로 인접 구간의 변화량을 구하고 거리 차이로 나눠 기울기(dB/m)를 얻습니다.plt.plot에 거리 배열과 RSSI 배열을 넣어 처리 결과를 그래프로 확인합니다.zip(distance_m, rssi_dbm)으로 인덱스 없이 두 배열을 동시에 순회합니다.- 관찰–원인 가설–시사점–한계 4문장 해석 템플릿을 f-string과 일반 출력으로 완성합니다.
자주 나오는 함정 표
| 실수 | 겉으로 보이는 문제 | 왜 위험한가 | 수정 방법 |
|---|---|---|---|
| 라벨·단위 누락 | x, y만 보임 | 물리적 의미 해석 불가 | Time (s), Voltage (V)처럼 명시 |
| y축 절단 미표기 | 작은 차이가 크게 보임 | 결론 왜곡 | 0 기준 또는 확대 표시 명시 |
| x/y 길이 불일치 | 런타임 오류 또는 이상한 그래프 | 데이터 오정렬 | assert len(x) == len(y) |
| 원본 없이 스무딩 결과만 표시 | 그래프가 깔끔해 보임 | 노이즈와 이상치가 숨겨짐 | 원본과 처리 결과를 함께 표시 |
| 서로 다른 조건을 한 선으로 연결 | 추세가 있는 것처럼 보임 | 존재하지 않는 인과관계 생성 | 조건별로 나누거나 고정 조건 명시 |
| 색만으로 계열 구분 | 화면에서는 보이지만 인쇄나 색각 차이에서는 불명확 | 비교 대상 혼동 | marker, linestyle, legend를 함께 사용 |
| 표본 수/반복 수 누락 | 평균값만 보임 | 우연한 차이를 일반화 | n, 반복 수, 불확실성 범위 표시 |
| 해석 문장 없음 | 그림만 있음 | 의사결정 근거 없음 | 관찰–원인 가설–시사점–한계 작성 |
| 라벨 빈 문자열 | "" 가 그대로 출력됨 | 독자에게 정보 없음 | all([title, x_label, y_label]) 점검 |
로컬에서도 같은 그림을 저장하고 싶다면
보고서나 발표 자료에 그래프를 넣을 때는 파일로 저장합니다. 로컬 Python, Jupyter, Colab에서는 아래 패턴을 사용하세요.
import matplotlib.pyplot as plt
plt.figure(figsize=(7, 4))plt.plot(x, y, marker="o")plt.title(title)plt.xlabel(x_label)plt.ylabel(y_label)plt.grid(alpha=0.3)plt.tight_layout()plt.savefig("my_plot.png", dpi=160)plt.show()실습 문제 · 직접 코딩
목표: 시간에 따라 증가하는 전압 데이터를 공학 그래프 설계 기준에 맞게 표현합니다.
해야 할 일: 그래프 질문, 제목, 축 라벨, 단위, 축 범위 이유를 정한 뒤 plt.plot으로 그리세요.
완료 조건: labels_ready, same_length, axis_reason, fixed_condition이 출력되고 그래프가 표시되어야 합니다.
목표: 같은 데이터라도 질문에 따라 차트 타입이 달라지고, 부적절한 차트가 오해를 만들 수 있음을 확인합니다.
해야 할 일: 같은 데이터를 plt.plot 선 그래프와 plt.scatter 산점도로 각각 그리고, 어떤 질문에 적합/부적합한지 쓰세요.
완료 조건: 두 그래프가 모두 표시되고, 선택 이유와 부적절한 사용 사례가 출력되어야 합니다.
목표: 전압-시간 데이터에서 구간 기울기를 계산하고, 축 범위가 추세 해석에 미치는 영향을 설명합니다.
해야 할 일: np.diff(v) / np.diff(t)로 구간별 기울기를 만들고 평균 기울기와 축 범위 설명을 출력하세요.
완료 조건: 기울기 배열, 평균 기울기, 추세 판정, 축 범위 설명이 출력되고 전압-시간 그래프가 표시되어야 합니다.
목표: 그래프를 보고 끝내지 않고 관찰–원인 가설–시사점–한계 4문장으로 해석합니다.
해야 할 일: 온도와 저항 데이터를 분석해 평균 기울기를 구하고, 4문장 해석을 f-string으로 출력하세요.
완료 조건: 관찰, 원인 가설, 시사점, 한계 문장이 각각 한 줄씩 출력되고 온도-저항 그래프가 표시되어야 합니다.
목표: 두 안테나의 수신 전력 데이터를 비교하고, 고정 조건과 숨은 변수를 함께 문장으로 판단합니다.
해야 할 일: 안테나 A/B의 평균 RSSI를 계산하고, 두 계열을 한 그래프에 그린 뒤 비교 조건과 해석 한계를 출력하세요.
완료 조건: 두 평균값, 라벨 검증 결과, “어느 안테나가 더 강한가”, 고정 조건, 숨은 변수 문장이 출력되어야 합니다.
제출 전 자기점검
- 질문: 이 그래프가 답하는 질문이 제목이나 캡션에 드러나는가?
- 단위: x축과 y축 라벨에 단위가 있는가?
- 스케일: 축 범위, 확대, log scale이 결론을 과장하지 않는가?
- 데이터 정렬:
len(x) == len(y)와 조건별 데이터 매칭을 확인했는가? - 조건: 비교 조건과 고정 조건을 구분했는가?
- 전처리: 원본, 스무딩, 필터링, 이상치 제거 여부를 표시했는가?
- 차원: 2D 그래프에서 숨은 변수나 빠진 조건을 설명했는가?
- 해석: 관찰–원인 가설–시사점–한계를 모두 작성했는가?
