표의 정제 규칙과 지도 인코딩 규칙을 하나의 실행 흐름으로 연결하기
오늘의 학습 목표
- Colab에서 pandas와 Folium을 준비하고 수업용 CSV를 DataFrame으로 불러올 수 있습니다.
raw_df를 보존하고clean_df작업 사본에서만 값을 정리할 수 있습니다.pd.to_numeric(),dropna(),drop_duplicates(),between()이 각각 어떤 문제를 처리하는지 설명할 수 있습니다.folium.Map()에서 지도 중심, 확대 수준과 바탕지도를 정할 수 있습니다.- 함수와 반복문을 이용해 수치를 반지름으로, 범주를 색으로 변환한
CircleMarker를 만들 수 있습니다. - 정제된 행 수와 마커 수를 비교하고 정제 CSV와 인터랙티브 지도 HTML을 저장할 수 있습니다.
- 0-5분1교시 회상
한 행과 한 마커의 대응을 예측합니다.
- 5-12분환경과 원본
Folium을 준비하고 CSV를 불러옵니다.
- 12-24분데이터 정제
자료형·결측·중복·좌표 범위를 처리합니다.
- 24-32분지도 그릇
중심점·확대 수준·바탕지도를 정합니다.
- 32-43분마커 인코딩
함수와 반복문으로 24개 마커를 만듭니다.
- 43-50분저장과 검증
CSV·HTML을 저장하고 미션 계약을 확인합니다.
- 50-60분확장 비교
지점 지도와 단계구분도의 차이를 판단합니다.
수업 운영: 기본 50분과 확장 60분
기본 50분에는 아래 CELL 1부터 CELL 7까지 순서대로 실행하여 정제 CSV와 지도 HTML을 완성합니다. 모든 개별 확인이 빠르게 끝난 경우에만 50-60분 확장에서 지점 지도와 단계구분도의 데이터 구조를 비교합니다. GeoJSON 코드는 필수 범위에 포함하지 않습니다.
코드를 읽는 한 문장
29행 원본을 보존하고 문제 5개를 근거와 함께 제외한 뒤, 남은 24행을 반복하여 24개의 마커와 하나의 HTML 지도를 만듭니다.
0-5분 · 한 행이 마커 하나가 되는지 먼저 예측하기
1교시에는 위도·경도가 위치, 프로그램 수가 마커 크기, 시설 범주가 색상, 장소명과 실제 값이 문자 정보가 된다고 정했습니다. 코드를 실행하기 전에 입력과 출력의 수를 예측합니다.
| 단계 | 행 또는 마커 수 | 설명 |
|---|---|---|
| 원본 CSV | 29행 | 유효한 시설 24개와 문제 행 5개 |
| 숫자 변환·결측 처리 후 | 27행 | 위도 결측과 프로그램 수 변환 실패 제외 |
| 중복 처리 후 | 26행 | 같은 place_id 한 행 제외 |
| 좌표 범위 처리 후 | 24행 | 위도 오류와 경도 오류 각 한 행 제외 |
| 최종 지도 | 24개 마커 | 유효한 한 행마다 원형 마커 하나 |
중복을 제거하지 않으면 마커는 몇 개인가?
숫자 변환과 좌표 범위 문제만 처리하고 중복 장소를 그대로 두었을 때 남는 행과 마커 수를 적습니다. 정답을 확인하려고 코드를 먼저 바꾸지 않습니다.
정답: 중복을 그대로 두면 몇 개의 마커가 생기는가?
25개입니다. 유효한 실제 장소는 24개이지만 C004 행이 두 번 남아 같은 좌표에 마커가 겹칩니다. 화면에서는 하나처럼 보일 수도 있으므로 행 수와 마커 수뿐 아니라 식별자 중복도 확인해야 합니다.
5-12분 · CELL 1-2: 환경을 준비하고 원본을 불러오기
Google Colab의 새 노트북을 열고 아래 셀을 위에서 아래로 실행합니다. pandas는 CSV와 DataFrame을 다루고, Folium은 Python에서 Leaflet 기반 지도를 구성해 노트북에 표시하거나 HTML로 저장합니다.
CELL 1 · Folium과 필요한 도구 준비
!pip -q install folium
from pathlib import Path
import folium
import pandas as pd
from google.colab import files
from IPython.display import display
print("pandas version:", pd.__version__)
print("folium version:", folium.__version__)
첫 줄의 느낌표는 Colab 환경에 Python 패키지를 설치하는 셸 명령입니다. 설치가 끝난 뒤 import folium으로 현재 런타임에서 Folium을 사용할 수 있게 합니다. 세션을 새로 시작하면 설치 셀부터 다시 실행합니다.
CELL 2 · CSV 업로드와 원본 점검
uploaded = files.upload()
source_path = "week-10-public-facilities-practice.csv"
assert source_path in uploaded, "지정된 수업용 CSV를 업로드하세요."
raw_df = pd.read_csv(source_path)
print("원본 크기:", raw_df.shape)
print("열 이름:", list(raw_df.columns))
print("열별 자료형:")
print(raw_df.dtypes)
print("열별 결측값:")
print(raw_df.isna().sum())
print("place_id 중복 수:", raw_df.duplicated(subset=["place_id"]).sum())
display(raw_df.head())
raw_df.shape의 결과는 (29, 6)이어야 합니다. 29는 행 수, 6은 열 수입니다. program_count에 unknown이 들어 있기 때문에 처음에는 숫자형이 아니라 문자열이 섞인 object로 읽힐 수 있습니다.
원본에서 문제 값을 직접 덮어쓰지 않는다
raw_df는 처음 불러온 상태를 보존합니다. 셀을 수정하거나 문제 행을 삭제하는 작업은 다음 단계의 clean_df에서만 수행합니다. 그래야 정제 전후를 비교하고 다시 시작할 수 있습니다.
ModuleNotFoundError: No module named 'folium'이 보일 때
CELL 1의 설치 줄이 끝나기 전에 아래 줄을 실행했거나 런타임을 다시 시작했을 수 있습니다. CELL 1 전체를 다시 실행하고 설치 완료 후 import 결과가 출력되는지 확인합니다.
업로드했는데 지정된 CSV가 없다는 오류가 보일 때
다른 파일을 선택했거나 내려받은 파일명이 바뀌었을 수 있습니다. Colab 왼쪽 파일 목록에서 이름을 확인하고 week-10-public-facilities-practice.csv를 다시 업로드합니다. 자동으로 붙은 괄호나 숫자가 있으면 기기에서 원래 파일명으로 바꿉니다.
12-24분 · CELL 3-4: 숫자·결측·중복·좌표 범위를 순서대로 정리하기
정제 순서는 결과에 영향을 줍니다. 먼저 지도에 꼭 필요한 세 열을 숫자로 변환하고, 변환할 수 없거나 비어 있는 행을 제외합니다. 이어서 시설 식별자의 중복을 제거하고 마지막으로 좌표 범위를 확인합니다.
원본 크기로 보기 ↗
CELL 3 · 작업 사본에서 세 열을 숫자로 변환
clean_df = raw_df.copy()
numeric_columns = ["program_count", "latitude", "longitude"]
for column_name in numeric_columns:
clean_df[column_name] = pd.to_numeric(
clean_df[column_name],
errors="coerce",
)
print("숫자 변환 후 결측값:")
print(clean_df[numeric_columns].isna().sum())
pd.to_numeric()은 값을 숫자형으로 바꿉니다. errors="coerce"는 unknown처럼 숫자로 바꿀 수 없는 값을 오류로 중단시키지 않고 NaN 결측값으로 표시합니다. 이것은 문제가 해결되었다는 뜻이 아니라 다음 검사에서 발견할 수 있는 공통 표시로 바뀌었다는 뜻입니다.
raw_df.copy()로 정제 전 표를 유지합니다.
지도 계산에 필요한 수치 열 세 개만 지정합니다.
같은 변환을 세 열에 반복 적용합니다.
숫자가 아닌 값은 NaN으로 드러냅니다.
CELL 4 · 결측, 중복, 좌표 범위 처리
rows_after_conversion = len(clean_df)
clean_df = clean_df.dropna(
subset=["program_count", "latitude", "longitude"],
)
rows_after_missing = len(clean_df)
clean_df = clean_df.drop_duplicates(
subset=["place_id"],
keep="first",
)
rows_after_duplicates = len(clean_df)
valid_coordinate_mask = (
clean_df["latitude"].between(-90, 90)
& clean_df["longitude"].between(-180, 180)
)
clean_df = clean_df.loc[valid_coordinate_mask].copy()
clean_df["program_count"] = clean_df["program_count"].astype(int)
clean_df = clean_df.reset_index(drop=True)
print("변환 직후:", rows_after_conversion)
print("결측 처리 후:", rows_after_missing)
print("중복 처리 후:", rows_after_duplicates)
print("좌표 범위 처리 후:", len(clean_df))
print("범주 수:", clean_df["category"].nunique())
assert len(raw_df) == 29
assert len(clean_df) == 24
assert clean_df["category"].nunique() == 3
assert clean_df["latitude"].between(-90, 90).all()
assert clean_df["longitude"].between(-180, 180).all()
display(clean_df)
dropna(subset=["latitude", "longitude", "program_count"])는 지정한 세 열 가운데 하나라도 결측인 행을 제외합니다. drop_duplicates(subset=["place_id"])는 같은 시설 식별자가 반복된 경우 첫 행만 남깁니다. between()은 각 좌표가 양 끝값을 포함한 범위 안에 있는지 행마다 True 또는 False로 검사합니다.
| 출력 | 기대값 | 제외된 문제 |
|---|---|---|
| 변환 직후 | 29 | 아직 제외하지 않음 |
| 결측 처리 후 | 27 | 빈 위도 1행, 숫자 변환 실패 1행 |
| 중복 처리 후 | 26 | 중복 place_id 1행 |
| 좌표 범위 처리 후 | 24 | 범위 밖 위도 1행, 경도 1행 |
inplace=True를 사용하지 않는 이유
이번 수업에서는 각 코드가 새 DataFrame을 돌려주고 그 결과를 다시 clean_df에 저장하는 흐름을 사용합니다. 입력과 반환값을 눈으로 따라가기 쉽고, 함수가 무엇을 돌려주는지 확인할 수 있기 때문입니다.
KeyError: 'latitude'가 보일 때
열 이름의 대소문자, 공백 또는 파일이 다른지 확인합니다. CELL 2의 list(raw_df.columns) 출력에 latitude가 정확히 있는지 봅니다. 수업용 CSV를 다시 올리고 CELL 2부터 순서대로 실행합니다.
정제 후 24행이 나오지 않을 때
수업용 CSV 자체를 스프레드시트에서 수정했거나 셀 실행 순서가 섞였을 수 있습니다. 원본 파일을 다시 내려받고 런타임을 다시 시작한 뒤 CELL 1부터 CELL 4까지 한 번씩 실행합니다. raw_df가 29행인지 먼저 확인합니다.
24-32분 · CELL 5: 중심점·확대 수준·바탕지도로 지도 그릇 만들기
마커를 올리기 전에 빈 지도 객체를 만듭니다. 이번 예시에서는 정제된 위도와 경도의 평균을 중심점으로 사용합니다. 명백한 좌표 오류를 먼저 제외했기 때문에 평균 중심이 수업용 시설 범위 근처에 놓입니다.
map_center = [
clean_df["latitude"].mean(),
clean_df["longitude"].mean(),
]
facility_map = folium.Map(
location=map_center,
zoom_start=12,
tiles="CartoDB positron",
control_scale=True,
)
print("지도 중심:", map_center)
display(facility_map)
| 설정 | 이번 값 | 역할 |
|---|---|---|
location | 평균 위도·경도 목록 | 처음 지도를 열 때 화면 중심 |
zoom_start | 12 | 처음 보이는 확대 수준 |
tiles | CartoDB positron | 도로와 지명을 보여 주는 바탕지도 스타일 |
control_scale | True | 거리 감각을 위한 축척 표시 |
facility_map은 아직 마커가 없는 지도 객체입니다. 지도 중심은 데이터값을 표현하는 마커가 아니며, 평균 위치에 새로운 시설이 있다는 뜻도 아닙니다. 단지 처음 화면을 어디에 맞출지 정하는 설정입니다.
바탕지도가 회색이거나 빈 화면으로 보일 때
지도 타일은 인터넷을 통해 불러옵니다. 네트워크 연결과 브라우저의 외부 콘텐츠 차단 여부를 확인합니다. 코드 셀이 오류 없이 실행되었다면 잠시 기다린 뒤 출력을 다시 표시해 봅니다.
zoom_start를 크게 하면 데이터가 더 정확해지는가?
아닙니다. 화면을 더 확대할 뿐 원본 좌표의 정확도나 데이터 품질은 바뀌지 않습니다. 너무 크게 확대하면 전체 분포를 보기 어렵고, 너무 작게 축소하면 마커가 겹쳐 보일 수 있습니다.
32-43분 · CELL 6: 함수와 반복문으로 24개 원형 마커 만들기
이제 앞에서 배운 함수, 조건문과 반복문을 다시 사용합니다. 첫 번째 함수는 프로그램 수를 6~20픽셀 반지름으로 변환하고, 두 번째 함수는 시설 범주를 세 가지 색상으로 변환합니다. 아래 색상은 밝은 바탕지도에서 문자와 마커가 묻히지 않도록 대비를 확인한 해안 팔레트입니다.
minimum_value = clean_df["program_count"].min()
maximum_value = clean_df["program_count"].max()
minimum_radius = 6
maximum_radius = 20
def scale_radius(program_count):
if minimum_value == maximum_value:
return (minimum_radius + maximum_radius) / 2
value_ratio = (
(program_count - minimum_value)
/ (maximum_value - minimum_value)
)
return minimum_radius + value_ratio * (
maximum_radius - minimum_radius
)
def category_to_color(category):
if category == "도서관":
return "#25696f"
elif category == "박물관":
return "#b94736"
elif category == "문화센터":
return "#455b92"
else:
raise ValueError("알 수 없는 시설 범주입니다: " + str(category))
marker_count = 0
for row_index, row in clean_df.iterrows():
marker_color = category_to_color(row["category"])
marker_radius = scale_radius(row["program_count"])
tooltip_text = row["place_name"] + " · " + row["category"]
popup_text = (
row["place_name"]
+ " | 범주: "
+ row["category"]
+ " | 프로그램 수: "
+ str(row["program_count"])
)
folium.CircleMarker(
location=[row["latitude"], row["longitude"]],
radius=marker_radius,
color=marker_color,
weight=2,
fill=True,
fill_color=marker_color,
fill_opacity=0.75,
tooltip=tooltip_text,
popup=popup_text,
).add_to(facility_map)
marker_count = marker_count + 1
assert marker_count == len(clean_df)
print("정제 행 수:", len(clean_df))
print("생성 마커 수:", marker_count)
display(facility_map)
iterrows()는 DataFrame을 한 행씩 꺼내며 반복합니다. row_index에는 현재 행의 인덱스가, row에는 그 행의 값들이 들어 있습니다. 반복할 때마다 같은 함수와 같은 마커 생성 코드를 사용하지만 각 행의 좌표·수치·범주·이름이 달라 결과가 달라집니다.
| 코드 | 사용하는 열 | 보이는 결과 |
|---|---|---|
location=[latitude, longitude] | latitude, longitude | 마커의 지도 위치 |
radius=marker_radius | program_count | 6~20픽셀 사이 원 크기 |
color, fill_color | category | 시설 종류별 색 |
tooltip | place_name, category | 마우스를 올렸을 때 짧은 설명 |
popup | 이름, 범주, 실제 수치 | 클릭했을 때 자세한 문자 정보 |
마커의 툴팁과 팝업만 있으면 모든 학생이 정보를 읽을 수 있는가?
아닙니다. 원형 마커는 포인터로 가리키거나 누를 때 정보를 보여 주므로 키보드와 화면 낭독기만 사용하는 사람에게 같은 내용을 전달하지 못할 수 있습니다. 3교시 노트북은 설명 패널 안에 장소·범주·프로그램 수 24행을 담은 텍스트 표를 함께 만듭니다. 지도와 표가 같은 clean_df를 사용하면 시각적 탐색과 문자 기반 탐색이 동일한 자료를 설명합니다.
왜 Circle이 아니라 CircleMarker인가?
Folium 공식 문서에서 CircleMarker의 반지름은 화면의 픽셀 단위이고, Circle의 반지름은 지도 위의 미터 단위입니다. 이번 수치는 시설의 영향 반경이나 실제 거리와 관계없는 프로그램 개수이므로, 화면에서 기호 크기를 비교하는 CircleMarker가 더 적절합니다.
| 요소 | 반지름 단위 | 적합한 질문 | 이번 수업 |
|---|---|---|---|
folium.Circle() | 미터 | 시설에서 500m 범위는 어디인가? | 사용하지 않음 |
folium.CircleMarker() | 화면 픽셀 | 어느 시설의 프로그램 수가 더 많은가? | 필수 사용 |
모든 마커의 크기가 같아 보일 때
radius에 고정 숫자를 넣지 않았는지 확인합니다. scale_radius(row["program_count"])의 반환값을 marker_radius에 저장하고, 그 변수를 CircleMarker의 radius에 전달해야 합니다.
마커가 한 개만 보일 때
folium.CircleMarker(location=[latitude, longitude], radius=radius).add_to(facility_map)가 반복문 안쪽에 같은 들여쓰기로 들어 있는지 확인합니다. 모든 좌표가 같은 열이나 고정값을 사용하지 않았는지도 확인합니다. 가까운 위치의 마커는 겹칠 수 있으므로 확대와 마커 수 출력도 함께 봅니다.
ValueError: 알 수 없는 시설 범주가 보일 때
print(clean_df["category"].unique())로 실제 범주 이름을 확인합니다. 공백이나 다른 철자가 있다면 원본의 분류 기준을 확인해 정리합니다. 오류를 피하려고 모든 새로운 범주를 임의로 같은 색으로 처리하지 않습니다.
43-50분 · CELL 7: 정제 CSV와 인터랙티브 지도 HTML 저장하기
노트북 화면에 지도가 보이는 것과 제출할 파일이 저장되는 것은 다른 일입니다. 정제된 DataFrame은 CSV로, Folium 지도 객체는 HTML로 각각 저장합니다.
cleaned_filename = "week10_cleaned_public_facilities.csv"
map_filename = "week10_public_facilities_map.html"
clean_df.to_csv(
cleaned_filename,
index=False,
encoding="utf-8-sig",
)
facility_map.save(map_filename)
assert Path(cleaned_filename).is_file()
assert Path(map_filename).is_file()
assert Path(cleaned_filename).stat().st_size > 0
assert Path(map_filename).stat().st_size > 0
saved_df = pd.read_csv(cleaned_filename)
assert len(saved_df) == len(clean_df)
assert set(saved_df["place_id"]) == set(clean_df["place_id"])
print("정제 CSV:", cleaned_filename, len(saved_df), "rows")
print("지도 HTML:", map_filename)
files.download(cleaned_filename)
files.download(map_filename)
index=False는 pandas가 자동으로 만든 0부터 시작하는 인덱스를 새 CSV 열로 추가하지 않게 합니다. encoding="utf-8-sig"는 한글이 포함된 CSV를 일부 스프레드시트 프로그램에서 열 때 글자가 깨지는 문제를 줄이기 위한 설정입니다. 저장 직후 다시 읽어 행 수와 식별자가 유지되었는지 확인합니다.
HTML 파일만 열었는데 지도 배경이 늦게 보일 수 있다
저장된 HTML은 지도 구조와 데이터를 담지만 바탕지도 타일과 일부 스크립트 자원을 인터넷에서 불러옵니다. 파일이 존재해도 네트워크가 차단되면 배경이 보이지 않을 수 있습니다. 제출 전 인터넷이 연결된 브라우저에서 마커와 출처 표기가 보이는지 확인합니다.
3교시 위치 데이터 지도 미션의 고정 계약
| 검사 영역 | 통과 조건 | 학생이 확인할 출력 |
|---|---|---|
| 원본 보존 | raw_df 29행 유지 | 원본과 정제본의 행 수가 다름 |
| 정제 데이터 | 유효한 행 20개 이상, 범주 3개 이상 | 행 수·범주 수 출력 |
| 좌표 | 결측 없음, 위도·경도 범위 통과 | 좌표 검사 모두 True |
| 시각 인코딩 | 수치→크기, 범주→색상, 이름·값→문자 | 서로 다른 크기·색과 툴팁·팝업 |
| 마커 수 | 정제된 행 수와 같은 마커 수 | 두 출력 숫자가 같음 |
| 세 파일 | 실행 결과가 있는 노트북, 정제 CSV, 지도 HTML | 파일명과 크기 확인 |
3교시에는 전체 코드를 처음부터 작성하지 않습니다. 준비된 노트북의 정제 기준, 크기 함수, 색상 함수와 문자 정보 영역을 완성합니다. 자동 검사 PASS와 세 파일 제출을 마친 학생은 남은 시간과 관계없이 즉시 귀가합니다.
HTML을 제출하지 않고 노트북 화면을 캡처해도 되는가?
안 됩니다. 캡처 이미지는 확대·이동·툴팁·팝업이 작동하는지 확인할 수 없습니다. 이번 주의 최종 시각 결과는 facility_map.save()로 저장한 인터랙티브 HTML입니다.
50-60분 · 선택 확장: 지점 지도와 단계구분도 구분하기
Folium은 GeoJSON 지역 경계와 집계표를 연결해 folium.Choropleth() 단계구분도도 만들 수 있습니다. 그러나 지점 지도와 단계구분도는 같은 데이터를 모양만 바꾼 것이 아닙니다. 관찰 단위와 필요한 파일이 다릅니다.
| 구분 | 관찰 단위 | 필요한 위치 정보 | 적합한 질문 |
|---|---|---|---|
| CircleMarker 지점 지도 | 시설 한 곳 | 각 행의 위도·경도 | 시설은 어디에 있고 값은 얼마나 다른가? |
| Choropleth 단계구분도 | 구·시·도 같은 지역 하나 | 지역 경계 GeoJSON과 일치하는 식별자 | 지역별 집계값은 어떻게 분포하는가? |
시설 24행을 그대로 지역 색으로 칠할 수는 없습니다. 먼저 시설을 지역별로 묶어 집계하고, 집계표의 지역 이름 또는 코드를 GeoJSON 속성과 정확히 연결해야 합니다. 이 결합은 필수 귀가 조건이나 추가 점수에 포함하지 않으며, 기본 지도를 완성한 뒤 구조를 이해하려는 학생만 공식 예시를 읽습니다.
두 질문에 맞는 지도 고르기
- “각 공공도서관의 위치와 프로그램 수를 비교하고 싶다.”
- “서울의 각 자치구별 공공문화 프로그램 총수를 비교하고 싶다.”
첫 질문에는 지점 지도, 두 번째 질문에는 지역별 집계와 단계구분도가 적합합니다. 선택 이유를 관찰 단위와 필요한 위치 정보로 설명합니다.
핵심 정리
- CSV를 불러온
raw_df는 그대로 보존하고clean_df에서만 정리합니다. - 숫자 변환 실패는
errors="coerce"로 결측값으로 드러낸 뒤 근거에 따라 처리합니다. - 결측, 중복, 위도·경도 범위를 순서대로 처리해 29행을 유효한 24행으로 만듭니다.
folium.Map()은 중심점·확대 수준·바탕지도를 가진 지도 그릇을 만듭니다.- 함수는 수치와 범주를 반지름과 색으로 바꾸고, 반복문은 각 행을
CircleMarker하나로 바꿉니다. - 정제 행 수와 마커 수를 비교하고 정제 CSV와 지도 HTML을 실제 파일로 저장합니다.
공식 자료와 추가 읽기
- Folium 공식 문서: Getting startedMap 생성, 타일, 마커, 팝업·툴팁, HTML 저장의 기본 흐름
- Folium 공식 문서: Circle과 CircleMarker미터 단위와 화면 픽셀 단위 반지름의 차이
- Folium 공식 문서: GeoJSON과 Choropleth선택 확장에서 지역 경계와 집계 데이터를 연결하는 구조
- pandas 공식 문서: to_numeric숫자 변환과
errors="coerce"의 동작 - pandas 공식 문서: DataFrame.dropna특정 열의 결측값을 기준으로 행을 처리하는 방법
- pandas 공식 문서: DataFrame.drop_duplicates식별 열을 기준으로 중복 행을 처리하는 방법
- pandas 공식 문서: Series.between위도와 경도의 양 끝값을 포함한 범위 검사