이번 주 학습 내용

지난주 크롤링에서 정리하지 못했던 조각들을 짧게 마무리하고, 이번 주 대부분은 데이터 분석 쪽으로 넘어갔다. NumPy와 Pandas로 데이터를 다루는 기본기를 익히고, Matplotlib과 Seaborn으로 그려보고, 피처 엔지니어링을 훑은 뒤 실제 공공데이터(한국복지패널조사)로 EDA까지 해봤다. "가져오는" 작업만 하다가 처음으로 "들여다보는" 작업을 해본 한 주였다. 마지막엔 다음 단계로 넘어가기 전에 인공지능과 머신러닝의 큰 그림을 잡는 개념 정리도 살짝 했다.

01. 크롤링 마무리 — 로컬 HTML 파싱과 DB 연동

실제 사이트 대신 로컬에 저장한 샘플 HTML 파일로 find/select 문법을 다시 한번 정리했다. findChildren()으로 특정 태그의 자식을 전부 가져오거나, CSS 자식 선택자(section#section1 > p)로 바로 아래 자식만 골라내는 연습이었다.

  • 크롤링한 데이터를 DB에 저장하는 연습문제를 풀면서 mysql.connector 말고 pymysql로도 똑같이 연결하고 저장할 수 있다는 걸 확인했다. 문법은 거의 같았다
  • 네이버 도서 API를 urllib으로 다시 호출해보면서, pubdate 값이 비어 있는 경우까지 고려해서 datetime.strptime으로 변환하기 전에 빈 문자열인지부터 체크하는 방어 코드를 추가했다
pub_str = book_info.get('pubdate', '') if pub_str: pub_date = datetime.strptime(pub_str, '%Y%m%d').date() else: pub_date = None

02. NumPy — 배열과 벡터 연산

배열 생성부터 shape/dtype, 인덱싱과 슬라이싱, 불리언 마스킹, 브로드캐스팅, 집계 함수까지 훑었다. 인상적이었던 건 반복문 대신 벡터화 연산을 썼을 때 속도 차이를 직접 시간으로 재본 것이었다.

  • 한 컬럼의 조건으로 다른 컬럼들을 골라내는 sales[sales[:, 2] <= 100, :2] 같은 복합 마스킹 패턴을 연습했다
  • 다차원 배열에서 np.where(조건)은 축마다 인덱스 배열을 따로 반환하는데, zip(*np.where(...))으로 풀면 "몇 번째, 몇 번째, 몇 번째" 조합으로 바로 읽을 수 있었다
  • 100만 개 원소를 반복문으로 처리했을 때(0.136초)와 벡터화 연산으로 처리했을 때(0.0042초)를 비교하니 30배 넘게 차이가 났다. 1억 개 합산에서는 순수 반복 대비 90배 가까이 차이 났다
  • dtype='int32'로 지정하면 int64 대비 메모리를 절반으로 줄일 수 있다는 것도 nbytes로 확인했고, np.memmap으로 큰 파일을 전부 메모리에 올리지 않고 일부만 읽는 방법도 살짝 다뤘다
for bran_i, fru_i, mon_i in zip(*np.where(sales_data > 150)): print(f'{mon_i+1}월 {bran_i}지점, 과일: {sales_info[fru_i]}, 판매: {sales_data[bran_i, fru_i, mon_i]}')

03. Pandas — 데이터 정제와 집계

Series와 DataFrame 기본기(loc/iloc, 조건 필터링, 결측치 처리, 중복 제거)를 다진 뒤, merge/concat/pivot_table/melt로 데이터 형태를 바꾸는 연습으로 넘어갔다.

  • groupby(...).unstack()으로 그룹 결과를 넓은 표로 펼치고, 다시 .stack()으로 원래 모양으로 되돌리는 왕복 연습이 형태 변환 감각을 잡는 데 도움이 됐다
  • resample('ME')로 날짜 데이터를 월 단위로 리샘플링하고, rolling(window=3).mean()으로 이동평균을 구하는 것까지 이어졌다
  • 날짜/최저가 레코드만 남기기처럼 sort_values + drop_duplicates 조합, loc을 이용한 조건부 값 갱신도 여러 시나리오로 반복했다
grouped = df.groupby(['Department', 'Location'])['Salary'].mean().unstack() # Location을 열로 펼쳐서 부서 × 지역 급여 표로 stacked = grouped.stack() # 다시 원래 형태로

04. Matplotlib — 그래프 그리기 기초

기본 line plot부터 plt.subplots()로 여러 그래프를 한 화면에 배치하는 것, 막대·선 그래프 중첩, 박스플롯·히스토그램·산점도까지 훑었다.

  • fig, ax = plt.subplots() 형태의 객체지향 스타일로 그리면 ax.set_xlabel()처럼 각 축을 따로 다룰 수 있어서 여러 그래프를 관리하기 편했다
  • imshow() + colorbar()로 2차원 히트맵을 그리고, LinearSegmentedColormap.from_list()로 원하는 색 조합의 컬러맵을 직접 만들어봤다
  • 막대그래프 위에 plt.text()로 값 라벨을 직접 찍는 것, plt.savefig()로 그래프를 이미지 파일로 저장하는 것까지 익혔다
from matplotlib.colors import LinearSegmentedColormap colors = ['#0000FF', '#00FF00', '#FF0000'] custom_cmap = LinearSegmentedColormap.from_list('custom', colors) plt.imshow(data, cmap=custom_cmap)

05. Seaborn — 통계 시각화

Matplotlib보다 통계 그래프에 특화된 라이브러리로, sns.load_dataset()으로 예제 데이터셋을 바로 불러와 다루는 것부터 시작했다.

  • scatterplot/lineplot/barplothue 옵션 하나로 그룹별 색 구분이 되고, barplotestimator로 집계 방식까지 알아서 처리해준다는 게 편했다
  • histplot(kde=True)로 히스토그램에 밀도곡선을 같이 그리는 것, pairplot(hue='species')로 여러 변수 간 관계를 한 번에 보는 것도 익혔다
  • titanic 데이터셋으로 barplot(hue=..., errorbar=None)처럼 신뢰구간 표시를 끄는 옵션도 써봤다

한글이 포함된 제목을 넣으려니 그래프 글자가 깨져서, plt.rcParams['font.family'] = 'Malgun Gothic'plt.rcParams['axes.unicode_minus'] = False를 미리 설정해두는 것도 배웠다.

plt.rcParams['font.family'] = 'Malgun Gothic' plt.rcParams['axes.unicode_minus'] = False sns.barplot(x=titanic['class'], y=titanic['survived'], hue=titanic.sex, errorbar=None)

06. 피처 엔지니어링

작은 예시 데이터로 파생 변수 만들기, 결측치 처리, 이상치 탐지, 표준화·정규화를 하나씩 연습했다.

  • groupby().mean()은 그룹 개수만큼 요약되지만, groupby().transform('mean')은 원본 행 개수 그대로 그룹 평균을 돌려줘서 원본 컬럼 옆에 바로 붙일 수 있다는 차이를 확인했다
  • 결측치는 평균 대치, 비결측 값 중 무작위 추출로 대치, IterativeImputer를 이용한 다중대치법까지 세 단계로 비교해봤다. IQR로 이상치를 직접 계산해보고 scipy.stats.iqr 결과와 맞춰봤다
  • 표준화·정규화 수식을 직접 구현해보고 StandardScaler/MinMaxScaler와 비교하는 것도 해봤다. 마지막엔 주사위를 10만 번 굴린 표본평균 분포를 정규분포 곡선과 겹쳐 그려서 중심극한정리를 눈으로 확인했다
avg_income = df.groupby('region')['income'].transform('mean') df['region_avg_income'] = avg_income

07. EDA — 한국복지패널조사로 성별 소득 격차 살펴보기

여기서부터 실제 공공데이터를 가지고 처음부터 끝까지 분석해봤다. 한국복지패널조사 원자료(.sav 파일, 14,915행 × 834열)를 pd.read_spss()로 불러온 뒤, 성별·출생연도·직종코드·월평균 임금 네 개 변수만 뽑아 이름을 알아보기 쉽게 바꾸는 것부터 시작했다.

  • 성별이 1/2 코드값으로 들어있어서 np.where로 'M'/'F' 문자열로 바꾸고, 컬럼별 결측치 비율을 구해서 성별에 따라 결측 패턴이 다른지 비교했다
  • 결측치를 제거한 뒤 성별 평균소득을 비교하니 남성이 여성보다 1.85배 높았고, scipy.stats.ttest_ind로 이 차이가 통계적으로 유의한지까지 검정해봤다(t=30.45, p값은 거의 0에 가까움)
  • 출생연도로 나이를 계산하고 청년/중장년/노년 3단계로 나눈 뒤, groupby(['age', 'sex']).mean().unstack()으로 남녀 소득을 나란히 펼쳐 소득비 추이를 그려봤다. 소득이 가장 높은 연령대는 50세였다
  • 직종코드 154개를 코드북 엑셀에서 따로 읽어와 직업명으로 매핑한 뒤 merge로 붙여서, 평균소득 상위·하위 직업들을 가로 막대그래프로 정리했다
data['sex'] = np.where(data['sex'] == 1.0, 'M', 'F') age_mean = data.dropna(subset=['income']).groupby(['age', 'sex'])['income'].mean().unstack() age_mean['ratio_M_over_F'] = age_mean['M'] / age_mean['F']

08. EDA 연습 — 주거 형태와 부채 비율

같은 복지패널조사 데이터에서 이번엔 주거 관련 변수(거주 위치, 면적, 주택가격, 자금 마련 경로, 부채 비율)로 연습문제를 풀었다.

  • 위치 코드는 지하층/반지하층/지상/옥탑으로, 자금 조달 코드는 자기돈/무상 도움/지인에게 빌림/금융기관/사채로 각각 라벨을 바꿨다
  • df.corr()sns.heatmap(annot=True)으로 면적·가격·부채 비율 사이의 상관관계를 한눈에 보고, 위치별 평균 가격과 가격 분포를 박스플롯으로, 자금 조달 방식별 부채 비율도 박스플롯으로 비교했다
  • 위치별 평균 가격을 봤을 때 '지하층'이 다른 위치보다 유독 낮게 나왔는데, 표본 수를 같이 확인하지 않고 그냥 넘어갔다. 표본이 적어서 생긴 왜곡일 가능성이 있어서 다음엔 value_counts()도 같이 봐야겠다고 느꼈다

09. 인공지능(AI) 개요

코드를 직접 다루기 전에, 인공지능이라는 큰 틀부터 짚고 넘어가는 시간을 가졌다. 인공지능은 추론, 학습, 문제 해결, 자연어 이해처럼 인간의 지능과 비슷한 작업을 기계가 수행하도록 만드는 학문이다.

  • 1956년 다트머스 컨퍼런스에서 처음 공식적인 개념으로 등장했고, 초창기엔 규칙을 사람이 직접 기호로 입력하는 방식이 중심이었다가 이후 데이터를 기반으로 스스로 패턴을 찾는 방식으로 무게중심이 옮겨갔다는 흐름이 흥미로웠다
  • 정의도 시대에 따라 조금씩 바뀌어왔다. 초기엔 "사람처럼 지능이 필요한 일을 하게 만드는 과학"처럼 인간을 기준 삼은 정의였다면, 요즘은 "환경을 인지하고 목표를 달성할 가능성을 극대화하는 시스템"처럼 목표 달성 관점으로 설명한다는 차이가 있었다
  • AI는 수준에 따라 세 단계로 나뉜다. 특정 작업 하나에 특화된 약인공지능(지금 쓰는 대부분의 AI 서비스가 여기 해당), 사람과 동등하게 스스로 사고하는 강인공지능, 인간을 능가하는 초인공지능인데 뒤의 둘은 아직 실현되지 않은 개념이다
  • 머신러닝은 이 인공지능이라는 큰 범주 안에 속한 한 가지 방법론이다. 모든 규칙을 사람이 일일이 정해주는 대신, 데이터를 통해 컴퓨터가 스스로 패턴을 찾고 그걸 바탕으로 예측이나 판단을 내리게 만드는 기술이라고 정리했다

KPT 회고

Keep

크롤링으로 데이터를 모으는 법을 배운 다음 주에 곧바로 그 데이터를 분석하는 법으로 넘어간 흐름이 자연스러웠다. NumPy 벡터화가 반복문보다 얼마나 빠른지 숫자로 직접 재본 것도 "왜 NumPy를 쓰는지"를 몸으로 이해하는 데 도움이 됐다.

실제 공공데이터로 결측치 확인부터 통계 검정, 시각화까지 한 번에 이어서 해본 것도 좋았다. 배운 조각들을 따로 연습할 때보다 훨씬 기억에 남았다.

Problem

주거 형태 EDA에서 지하층 평균 가격이 낮게 나온 걸 보고도 표본 수를 확인하지 않고 그대로 해석해버렸다. 평균만 보고 성급하게 결론 내릴 뻔했다.

범주를 나눌 때 np.where를 서너 겹 중첩해서 썼는데, 범주가 늘어날수록 코드가 한눈에 안 들어왔다.

Try

그룹별 평균이나 비율을 볼 때는 value_counts()로 표본 수도 같이 확인하는 습관을 들인다. 여러 단계로 범주를 나눠야 할 때는 np.where 중첩 대신 np.select나 딕셔너리 매핑을 써서 코드를 더 읽기 쉽게 만들어본다.

새롭게 발견한 것

표준화 계산 기준의 차이. 표준편차를 직접 수식으로 계산한 값과 StandardScaler로 변환한 값이 미묘하게 달랐다. pandas의 std()는 표본표준편차(n-1로 나눔)를, StandardScaler는 모표준편차(n으로 나눔)를 기준으로 삼기 때문이었다. 같은 "표준화"라는 이름이어도 라이브러리마다 계산 기준이 다를 수 있다는 걸 처음 알게 됐다.

np.where의 다차원 반환값. 1차원 배열에서 np.where(조건)은 인덱스 배열 하나를 주지만, 다차원 배열에서는 축마다 인덱스 배열을 따로따로 튜플로 묶어 돌려준다. zip(*np.where(...))으로 풀어야 "몇 번째 축, 몇 번째 축" 조합으로 읽을 수 있다는 걸 실습하며 알게 됐다.

sklearn의 실험적 기능. IterativeImputer는 그냥 import하면 안 되고, from sklearn.experimental import enable_iterative_imputer를 먼저 실행해야 활성화된다. sklearn에서 아직 정식 기능이 아닌 것들은 이렇게 명시적으로 켜야 한다는 걸 처음 봤다.

트러블슈팅

문제 : to_datetime에서 날짜 형식을 못 찾겠다는 경고

월 이름과 연도를 이어붙인 문자열을 pd.to_datetime()으로 변환하려는데, 형식을 지정하지 않아서 UserWarning: Could not infer format 경고가 떴다. 결과 자체는 나왔지만 매번 한 건씩 개별적으로 파싱한다는 경고였다.

-- 수정 전 pd.to_datetime(df['Month'] + ' 2026') # UserWarning: Could not infer format, so each element will be # parsed individually, falling back to dateutil

형식을 미리 정해두지 않으면 pandas가 각 값을 하나하나 추측해서 변환하느라 느려지고, 값에 따라 다르게 해석될 위험도 있었다. format 인자를 명시하면 경고 없이 훨씬 안정적으로 변환됐다.

-- 수정 후 pd.to_datetime(df['Month'] + ' 2026', format='%B %Y')

날짜 변환은 형식을 명시하는 게 기본이라는 걸 경고를 보고서야 챙기게 됐다.

문제 : 파일 경로 문자열에서 SyntaxWarning

엑셀 코드북 파일을 읽어오려고 경로를 문자열로 적었는데, 폴더 이름에 괄호가 들어 있어서 SyntaxWarning: invalid escape sequence '\(' 경고가 났다.

-- 수정 전 pd.read_excel('data\(2025년 20차 한국복지패널조사) 조사설계서(beta1)\...') # SyntaxWarning: invalid escape sequence '\('

일반 문자열에서 \는 이스케이프 문자로 해석되는데, 윈도우 경로에 쓰인 \ 뒤에 괄호가 오면서 의도치 않은 조합으로 읽힌 것이었다. 경로 앞에 r을 붙여 raw string으로 만들면 \를 그대로의 문자로 취급해서 경고가 사라졌다.

-- 수정 후 pd.read_excel(r'data\(2025년 20차 한국복지패널조사) 조사설계서(beta1)\...')

동작 자체엔 문제가 없었지만, 윈도우 경로를 문자열로 다룰 땐 raw string을 기본으로 써야겠다고 생각했다.

다음 주 목표

  • 범주를 여러 단계로 나눌 때 np.where 중첩 대신 np.select나 매핑 방식으로 정리해보기
  • 그룹 통계를 볼 때 value_counts()로 표본 수도 함께 확인하는 습관 들이기
  • 이번 주 배운 EDA와 시각화를 팀 프로젝트 데이터에도 적용해보기
데이터를 가져오는 법을 배운 다음 주, 이번엔 그 데이터를 들여다보는 법을 배웠다.
숫자 뒤에 있는 이야기를 읽어내는 게 생각보다 오래 걸리는 일이라는 걸 느낀 한 주였다.