머신러닝

PANDAS

haniru 2026. 1. 8. 23:49
# 데이터 정제와 결손값의 처리
# how -> any, all
# weather.dropna(axis=0, how='any', inplace=True)
pure_weather = weather.dropna(axis=0, how='any', inplace=False)
print(pure_weather)
print(pure_weather.shape)

missing_data = weather[weather['평균풍속'].isna()]
print(missing_data)
print(weather[weather.isnull().any(axis=1)])
print(weather.describe())
print(pure_weather.describe())​
d_df = pd.DataFrame(data = [[10,20,30,40], [50,60,70,80]], columns = ['A','B','C','D'])
print(d_df)
new_df = d_df.drop('B', axis=1, inplace=False)
print(new_df)
print(d_df.drop(0, axis=0))
print(d_df.drop(d_df.columns[0], axis=1))

 

1. 판다스의 특징과 자료구조

판다스는 파이썬에서 엑셀이나 R처럼 행과 열로 이루어진 데이터를 다루는 데 최적화된 라이브러리다.

  • 성능 및 용도: 데이터 조작이 빠르고 시계열 데이터나 라벨링된 행렬 데이터를 처리하기 좋다.
  • Series (시리즈): 1차원 배열 형태의 자료구조다. 코드 예시처럼 하나의 컬럼으로 구성되며 인덱스를 가진다.
  • DataFrame (데이터프레임): 여러 개의 시리즈가 모인 2차원 테이블 구조다. 엑셀의 시트와 유사하다.

2. 결측치(Missing Value) 처리

데이터셋에서 값이 비어있는 '결손값'은 분석 결과를 왜곡하는 가장 큰 문제 중 하나다. 이를 처리하는 주요 전략은 다음과 같다.

  • 제거(Drop): 결측치가 포함된 행이나 열을 삭제한다. 데이터 양이 충분할 때 사용한다.
  • 대체(Imputation):
    • 평균값(Mean): 수치형 데이터에서 일반적인 방식이다.
    • 최빈값(Mode): 범주형 데이터나 특정 크기(예: 발 사이즈)처럼 자주 나타나는 값을 사용한다. 히스토그램을 통해 분포를 확인하고 결정한다.
    • 중앙값(Median): 이상치(Outlier)의 영향이 클 때 사용한다.

 

판다스가 결측치를 어떻게 처리하는지 보여줌

Int32는 판다스의 Nullable 정수 타입으로 None(결측치)을 포함할 수 있다.

se = pd.Series([1, 2, None, 4], dtype="Int32") # 파이썬의 리스트
# se = pd.Series([1, 2, np.nan, 4])
print(se)

 

 

결측치 탐지

isna() 메서드는 데이터를 처음부터 끝까지 스캔하며 값이 비어있는지(NaN, None) 확인한다. 그 결과로 데이터와 동일한 크기의 **True/False 리스트(불리언 마스크)**를 반환한다.

  • isna()의 동작: 각 요소를 순회하며 결측치면 True, 정상 값이면 False를 할당한다.
  • 성능 최적화: 파이썬 for 문으로 하나씩 확인하는 것보다 판다스의 내장 함수를 쓰는 것이 훨씬 빠르다. 내부적으로 C로 구현된 벡터화 연산을 수행하기 때문이다.
  • any()의 활용: 데이터가 수백만 개일 때 일일이 출력할 수 없으므로, se.isna().any()를 통해 "단 하나라도 결측치가 있는가?"를 단번에 파악한다.
# 결손값
print(se.isna())
#
# for i in range(se.size):
#     if(se.isna()[i]):
#         print('NaN.')
#     else:
#         print('NaN 이 아니다.')

for val in se.isna():
    if val: # True라면 결측치임
        print('숫자가 아닙니다 (결측치)')
    else:
        print('숫자입니다')

# 전체 시리즈에 결측치가 하나라도 있는지 확인
if se.isna().any():
    print("데이터에 결측치가 포함되어 있음")

print(se.isna()[2])

 

판다스 시리즈의 인덱싱

1. np.nan과 데이터 타입의 형변환

np.nan은 부동소수점(float) 타입이다. 리스트에 정수만 있더라도 np.nan이 하나라도 포함되면, 판다스는 전체 데이터를 누락값 없이 표현하기 위해 **실수형(float64)**으로 자동 형변환한다.

  • data = [1, 2, np.nan, 4] → 정수와 실수가 섞임.
  • pd.Series(data) 실행 시 → 모든 숫자가 1.0, 2.0처럼 실수로 변환됨.
  • 단, 앞서 언급한 dtype="Int32" 등을 명시적으로 사용하면 정수 형태를 유지하며 결측치를 처리할 수 있음.

2. 인덱스의 두 가지 얼굴: 자동 부여 vs 명시적 지정

판다스의 시리즈는 **값(Values)**과 **색인(Index)**이 쌍을 이루는 구조다.

  • 자동 인덱스: 인덱스를 지정하지 않으면 0부터 시작하는 정수형 인덱스가 부여됨. 이는 리스트의 오프셋 주소와 같은 역할을 함.
  • 명시적 인덱스(Label): index=['a', 'b', 'c', 'd']와 같이 이름을 붙이면, 이제 숫자가 아닌 **'이름'**으로 데이터에 접근할 수 있음. 엑셀의 행 번호 대신 특정 이름을 붙인 것과 같음.

파이썬의 딕셔너리(Dictionary) 구조와 매우 유사

data = [1,2,np.nan,4] # 리스트이다. np.nan 이 있으니까 배열은 아님
indexed_se = pd.Series(data) # 인덱스 자동 부여
print(indexed_se)
print(indexed_se[0])

indexed_se = pd.Series(data, index=['a','b','c','d']) # 인덱스 지정
print(indexed_se)
print(indexed_se['a'])

 

딕셔너리와 판다스의 관계

1. 딕셔너리와 해시(Hash)

  • 중복 불가: 딕셔너리의 키는 고유해야 한다. 동일한 키로 값을 넣으면 기존 데이터는 사라지고 새로운 데이터로 덮어씌워진다.
  • 속도: 키를 해시 값으로 변환하여 저장하기 때문에, 데이터의 양이 방대해져도 특정 키를 찾는 속도가 매우 빠르다(O(1)). 판다스가 딕셔너리 구조를 선호하는 이유도 이 효율성 때문이다.

2. 시리즈(Series)와 특성(Feature)

  • 지금 작성한 income_se는 '수입'이라는 단일 특성을 가진 데이터다.
  • 시리즈는 1차원 배열이기 때문에 한 종류의 관측값만 담을 수 있다.
income = {'1월': 9500, '2월': 6200, '3월': 6050, '4월': 7000} # dictionary
income_se = pd.Series(income)
print(income_se)

 

개별적인 **벡터(Series)**들이 모여 하나의 **행렬(DataFrame)**을 이루는 것

1. 벡터에서 행렬로 (Series to DataFrame)

수학적으로 1차원 배열인 시리즈는 **벡터**이고, 이 벡터들이 공통된 인덱스를 기준으로 옆으로 결합하면 2차원 **행렬**인 데이터프레임이 된다.

  • Series: 데이터프레임의 세로줄 하나하나를 담당하는 부품이다.
  • DataFrame: 이 시리즈들이 딕셔너리의 'Key'를 컬럼명(Column Name)으로 삼아 조립된 완성품이다.
  • 컬럼 중심 사고: 데이터 분석에서는 보통 하나의 열(Column)을 하나의 **특성(Feature)**이나 변수로 취급하기 때문에 컬럼 단위의 조작이 매우 중요하다.

2. 인덱스(Index)의 자동 정렬

코드에서 month_se, income_se 등을 합칠 때 별도의 인덱스를 주지 않으면 판다스는 자동으로 0, 1, 2... 정수 인덱스를 부여한다.

만약 각 시리즈의 인덱스가 서로 달랐다면, 판다스는 이를 자동으로 정렬하여 합치며, 데이터가 없는 곳은 앞서 배운 NaN(결측치)으로 채운다.


3. 출력의 차이 (print vs Jupyter)

  • print(df): 텍스트 기반의 표준 출력이다. 데이터의 구조를 확인하는 용도로 쓰인다.
  • Jupyter Notebook (df): 웹 브라우저의 HTML/CSS 기능을 활용해 표(Table) 형태로 시각화해준다. 가독성이 훨씬 좋아 데이터 분석가들이 주피터 환경을 선호하는 이유이기도 하다.
month_se = pd.Series(['1월', '2월', '3월', '4월'])
income_se = pd.Series([9500, 6200, 6050, 7000])
expenses_se = pd.Series([5040, 2350, 2300, 4000])
df = pd.DataFrame({'월': month_se, '수익': income_se, '지출': expenses_se})
#df
print(df)

 

통계함수와 argmax

argmax는 단순히 가장 큰 값을 찾는 것이 아니라, **가장 큰 값이 위치한 '주소(Index)'**를 반환한다.

평균(mean)은 강력하지만 **이상치(Outlier)**에 매우 취약하다. 예를 들어, 100만 원씩 버는 사람 9명과 10억을 버는 사람 1명이 있을 때, 평균은 1억이 넘어간다. 이때 데이터의 실체를 더 정확하게 보여주는 것은 중앙값(median)이다.

# argmax: 가장 큰 값의 인덱스 값 찾는다
# 머신러닝에서 가장 큰 값 찾을 때 많이 씀
m_idx = np.argmax(income_se)
print('최대 수익이 발생한 월: ', income_se[m_idx])
print('월 최대 수익: ', income_se.max(), \
      ', 월 평균 수익: ', income_se.mean())

print(income_se.max())
print(income_se.min())
print(income_se.mean())
print(income_se.sum())
print(income_se.sum()/income_se.size)
print(income_se.median())
함수 설명 비고
max() 최댓값 가장 높은 수익
min() 최솟값 가장 낮은 수익
mean() 평균값 sum() / size와 동일
median() 중앙값 데이터를 크기순으로 나열했을 때 가운데 값
sum() 합계 전체 수익 총합
std() 표준편차 데이터가 평균에서 얼마나 떨어져 있는지(변동성)

 

 

개별 시리즈(Series)를 사용하여 수입과 지출의 합계를 잘 계산

month_se = pd.Series(['1월', '2월', '3월', '4월'])
income_se = pd.Series([9500, 6200, 6050, 7000])
expenses_se = pd.Series([5040, 2350, 2300, 4000])
df = pd.DataFrame({'월': month_se, '수익': income_se, '지출': expenses_se})

print("동윤이네 상점 수입 합: ", income_se.sum(),\
      "동윤이네 상점 지출 합: ", expenses_se.sum())

 

 

데이터를 파일 형태로 주고받을 때 사용하는 다양한 형식과 판다스(Pandas)를 이용한 데이터 로드 과정

1. 데이터 형식의 종류와 특징

데이터를 전송하거나 저장할 때 구분자(Delimiter)의 역할이 중요하다. 네트워크를 통해 들어오는 연속적인 데이터(Stream)를 의미 있는 단위로 끊어주기 위해 약속된 형식을 사용한다.

형식 구분자 특징
CSV 쉼표 (,) 가장 범용적임. 용량이 작지만 데이터 자체에 쉼표가 포함된 경우(예: 3,000) 파싱 오류 위험이 있음.
TSV 탭 (\t) 탭으로 구분하여 시각적으로 구분이 쉽고, 데이터 내의 쉼표 문제에서 비교적 자유로움.
JSON {key: value} 계층 구조를 표현하기 좋음. 웹 API에서 표준으로 사용되지만 CSV보다 용량이 큼.
XML <tag></tag> 마크업 언어 형태. 데이터의 구조를 명확히 정의할 수 있으나 형식이 복잡하고 무거움.

2. CSV 파일의 문제점과 처리 (쉼표 이슈)

**"3,000"**과 같은 돈 계산 데이터는 CSV에서 치명적인 문제를 일으킬 수 있다.

  • 현상: 쉼표를 단순 구분자로 인식하면 3과 000을 서로 다른 열(Column)로 분리해버리는 오류가 발생한다.
  • 해결책: 1. 데이터 생성 시 큰따옴표(" ")로 묶어 쉼표가 데이터의 일부임을 명시한다.3. 아예 구분자를 탭(TSV)이나 파이프(|) 기호로 바꾼다.
  • 2. 판다스에서 quotechar 옵션을 사용하거나, 전처리 과정에서 쉼표를 제거(replace)한 뒤 수치형으로 변환한다.

 

print(df)는 텍스트 위주로 보여주지만, 주피터 노트북에서 df를 직접 실행하면 HTML 테이블 형태로 출력되어 가독성이 훨씬 좋다.

file = './vehicle_prod.csv'
# index_col=0: 파일의 첫 번째 열(0번 열)을 데이터프레임의 인덱스로 지정함
df = pd.read_csv(file, index_col=0)
df.index
df.columns
속성 방향 설명
df.index 행 (Row) 세로 방향의 주소 (Index)
df.columns 열 (Column) 가로 방향의 특징 (Feature)
 
인덱스 접근하기

1. 인덱스와 컬럼 접근의 차이

판다스 객체에서 데이터를 가져오는 방식은 **'어떤 축(Axis)을 기준으로 접근하느냐'**에 따라 달라진다.

  • df.index[i]: 행들의 이름표(Index) 묶음에서 $i$번째 위치한 값을 가져온다. 이는 데이터 자체가 아니라 해당 데이터를 식별하는 **'주소(이름)'**를 확인하는 작업이다.
  • df['2007']: '2007'이라는 이름의 **컬럼(열)**을 통째로 가져온다. 결과물은 하나의 시리즈(Series) 형태다.

2. tolist()를 통한 데이터 추출 (Unwrapping)

판다스의 시리즈나 인덱스 객체는 단순한 리스트가 아니라 추가적인 기능(메타데이터, 인덱스 정보 등)이 포함된 클래스다. 이를 순수한 파이썬 **리스트(List)**로 바꾸는 과정이 tolist()다.

  • 변환의 이유: 판다스의 고유 기능을 떼어내고 값들만 모아서 다른 라이브러리에 전달하거나, 파이썬 기본 반복문에서 가볍게 쓰기 위해 사용한다.
  • 값을 벗겨내기: list_2[0]과 같이 인덱싱을 수행하면 리스트라는 컨테이너 내부에 들어있는 실제 원시 값(Raw Value)을 얻을 수 있다. 사용자가 언급한 '클래스를 벗겨서 값을 가져오는 역할'과 일맥상통한다.
print(df.index[0])
print("===\n", df.index[1])
print("===\n", df['2007'])
print("===\n", df['2008'])
list_1 = df.columns.tolist()
print("===\n", list_1)
print("===\n", list_1[0])
list_2 = df['2007'].tolist()
print("===\n", list_2)
print("===\n", list_2[0])

 

행의 합계 구하기

df['total'] = df.sum(axis=1)
print(df)

 

행의 평균 구하기

df['mean'] = df[['2007', '2008', '2009', '2010', '2011']].mean(axis=1)
print(df)

 

컬럼 삭제하기, 새로운 합계 구하기

판다스에서 데이터를 다룰 때 원본을 보존해야 하는 상황인지, 아니면 메모리 절약을 위해 즉시 수정해야 하는 상황인지에 따라 inplace 사용 여부를 결정하면 된다.

# df.drop('2007') # 어떤방향으로 할지 설정필요

df.drop('2007', axis=1) # 컬럼 지우기
print(df)

new_drop = df.drop('2007', axis=1)
print(new_drop)

df.drop('2007', inplace=True, axis=1) # 원본 수정
new_drop['total'] = new_drop[['2008', '2009', '2010', '2011']].sum(axis=1)

print(new_drop)

 

새로운 평균 구하기

new_drop['mean'] = new_drop[['2008', '2009', '2010', '2011']].mean(axis=1)
print(new_drop)

 

Total 행 추가하기

total_row는 Series 형태이므로, 이를 행으로 붙이려면 to_frame().T를 통해 가로로 눕힌(전치) 데이터프레임으로 변환해야 concat이 자연스럽게 이루어진다.

file = './vehicle_prod.csv'
df = pd.read_csv(file, index_col=0)

print(df)

# 방법 1
# total_row = df.select_dtypes(np.number).sum().rename()
# print(total_row)
#
# df.loc['Total'] = total_row

# 방법 2
total_row = df.select_dtypes(np.number).sum().rename('Total')
df = pd.concat([df, total_row.to_frame().T])

# 방법 3
# df.loc['Total'] = df.sum(numeric_only=True)

print(df)

print(df['2007'])
print(df[['2007', '2009']])

 

판다스의 drop 메서드 동작 방식

Axis(축) 설정에 따른 삭제 방향

drop 함수에서 axis는 삭제할 대상을 결정하는 가장 중요한 파라미터다.

설정 대상 동작 설명
axis=0 (기본값) 행(Row) 지정한 인덱스(레이블)를 가진 행을 삭제한다.
axis=1 열(Column) 지정한 컬럼 이름을 가진 열을 삭제한다.

 

① 컬럼 이름으로 삭제 (axis=1)

new_df = d_df.drop('B', axis=1, inplace=False)

 

② 인덱스 번호로 행 삭제 (axis=0)

print(d_df.drop(0, axis=0))

 

③ 컬럼 위치(인덱스)를 이용한 삭제

print(d_df.drop(d_df.columns[0], axis=1))

 

d_df = pd.DataFrame(data = [[10,20,30,40], [50,60,70,80]], columns = ['A','B','C','D'])
print(d_df)
new_df = d_df.drop('B', axis=1, inplace=False) # 열
print(new_df)
print(d_df.drop(0, axis=0)) # 행
print(d_df.drop(d_df.columns[0], axis=1)) # 1 열

 

total 컬럼 생성

# inplace로 데이터프레임 갱신하기
file = './vehicle_prod.csv'
df = pd.read_csv(file, index_col=0)
df['total'] = df.sum(axis=1)
print(df)

print(df['2007'])

 

2009년도 표 그리기

df['2009'].plot(kind='bar', color=('orange', 'r', 'b', 'm', 'c', 'k'))

 

df['2009'].plot(kind='pie', autopct='%1.1f%%', figsize=(7, 7))
import matplotlib.pyplot as plt
plt.ylabel('') # y축 라벨 제거

 

 

선형 그래프 그리기

file = './vehicle_prod.csv'
df = pd.read_csv(file, index_col=0)
df.plot.line()
# df.plt.show() # 주피터에서는 안써도 그래프가 그려지지만, 원래는 써야함

 

막대그래프 그리기

df.plot.bar()

 

전치

# transpose_df = df.transpose()
transpose_df = df.T
print(df)
print(transpose_df)

 

전치 그래프

transpose_df.plot.line()

 

head와 tail

 

  • head(n): 데이터프레임의 처음부터 n개의 행을 보여줌. 데이터가 제대로 불러와졌는지, 컬럼 이름과 값의 형식이 맞는지 확인할 때 유용.
  • tail(n): 데이터프레임의 마지막 n개의 행을 보여줌. 데이터의 전체 개수를 짐작하거나, 마지막 부분에 결측치나 합계 행이 포함되어 있는지 확인할 때 자주 씀.

 

# head와 tail
print(df.head()) # 기본 5개
print(df.head(3)) # 3개
print(df.tail()) # 기본 5개
print(df.tail(2))

 

loc과 iloc

loc은 레이블(Label) 기반, iloc은 정수 위치(Integer Position) 기반 접근자

① 행 슬라이싱: df[2:6]

  • 기본적으로 df[숫자:숫자] 형태는 행(Row)의 위치를 기준으로 슬라이싱

② loc의 포인터적 성격

  • df.loc: "이 레이블 주소로 가서 데이터를 찾아라"라는 의미의 접근자 객체

③ 연속 인덱싱 vs 다중 축 접근

  • df['2011'].iloc[[0, 4]]:
    1. df['2011']이 먼저 실행되어 해당 컬럼의 Series를 반환한다.
    2. 반환된 Series에서 .iloc[[0, 4]]를 통해 0번째와 4번째 행을 추출
print(df[2:6])
print(df.loc)
print(df.loc['Korea'])
print(df.loc['Mexico'])
print(df.loc[['US', 'Korea']])
# print(df['2011'][[0,4]])
print(df['2011'].iloc[[0, 4]])

print(df.loc['Korea', '2011'])
print(df.loc['Korea']['2011'])
# df.loc[행, 열]

 

iloc으로 데이터 접근하기

# loc, iloc 인덱서
df = pd.read_csv(file, index_col=0)
print(df)
print("====\n", df.head(3)['2009'])
print("====\n", df.iloc[4]) # df.loc['Korea']와 동일. df.iloc['Korea'] 는 오류. 결과는 series 타입으로 나온다
print("====\n", df.iloc[[2,4]]) # 주의: df.iloc[2,4]는 US 행의 2011 데이터. 현재 코드는 'US' 행과 'Korea'행을 가져옴
print("====\n", df.iloc[2,4]) # US 행의 2011 데이터 출력
print("====\n", df.iloc[2:4])
print("====\n", df.iloc[[2,3]])
print("====\n", df[2:4])

 

 

기온 데이터 불러오기

1. 인코딩과 바이트 체계

한글 데이터를 다룰 때 인코딩 설정은 필수적이다.

  • CP949 (EUC-KR 확장형): 윈도우 기본 인코딩으로 한글을 2바이트로 처리한다. 오래된 국내 공공기관 데이터나 엑셀에서 만든 CSV에서 주로 쓰인다.
  • UTF-8: 전 세계 표준으로, 영문 1바이트, 한글은 3바이트로 가변 처리한다. 용량은 조금 더 차지할 수 있으나 범용성이 가장 높아 네트워크 전송에 유리하다.
  • 파일 읽기 실패 시: UnicodeDecodeError가 발생하면 encoding='utf-8', 'cp949', 'euc-kr' 순서로 시도해보는 것이 일반적이다.

2. describe()를 통한 데이터 분석 기법

사용자가 언급한 통계적 특징은 데이터의 품질(Quality)을 판단하는 핵심 지표다.

지표 의미 분석 포인트
mean (평균) 전체 데이터의 산술 평균 중앙값(50%)과 비교하여 데이터 쏠림 확인
std (표준편차) 데이터의 흩어짐 정도 분산의 제곱근. 이 값이 클수록 데이터가 들쭉날쭉함
50% (중앙값) 데이터를 순서대로 세웠을 때 정중앙 값 평균보다 훨씬 작거나 크면 이상치 존재 가능성 높음
min / max 최소값과 최대값 상식에서 벗어난 값(예: 기온 100도)이 있는지 확인

데이터 특징 해석법

  1. 평균과 중앙값의 괴리: * 평균 > 중앙값: 매우 큰 값(이상치)이 평균을 끌어올리고 있음 (오른쪽으로 꼬리가 긴 분포).
    • 평균 < 중앙값: 매우 작은 값(이상치)이 평균을 깎아먹고 있음 (왼쪽으로 꼬리가 긴 분포).
  2. 표준편차(std)와 고른 데이터:
    • 표준편차가 0에 가까울수록 모든 데이터가 평균값과 비슷하다. 기상 데이터에서 기온의 표준편차가 작다면 날씨 변화가 거의 없는 지역임을 뜻한다.
# 영어 기본 1byte, 한글은 2byte 또는 3byte
weather_file = './weather.csv'
weather = pd.read_csv(weather_file, index_col=0, encoding='CP949')
print(weather.head())
print(weather.tail())
print('weather 데이터의 shape: ', weather.shape)
print(weather.describe()) # 분산이 작을수록 고르다, 평균과 중앙값과 다르면 이상치

 

 

결측값 찾기

print(weather.isna())
print("====\n", weather[weather.isna()])
print("====\n", weather[weather.isna().any(axis=1)]) # 행 방향

 

데이터 분석 (EDA)

print("평균 분석 \n", weather.mean())
print("표준편차 분석 \n", weather.std())
# 최대풍속 컬럼의 최댓값 출력
print("최대풍속의 최대값", weather['최대풍속'].max())

# 평균풍속 컬럼의 최댓값 출력
print("평균풍속의 최대값", weather['평균풍속'].max())

# 필요한 컬럼만 리스트로 묶어서 한 번에 최댓값 구하기
print(weather[['최대풍속', '평균풍속']].max())
# 최대풍속이 가장 높았던 날의 인덱스(날짜 등) 확인
max_wind_date = weather['최대풍속'].idxmax()
print(f"최대풍속 발생일: {max_wind_date}")

# 해당 날짜의 전체 데이터 확인
print(weather.loc[max_wind_date])

 

컬럼별 집계

print(weather.count())

 

결측치 있는 행만 추출

missing_data = weather[weather['평균풍속'].isna()] # logical index
print(missing_data)

 

결측치의 개수 파악하기

  • axis=0: 행(Row)을 기준으로 동작한다. 결측치가 발견되면 해당 날짜(행) 전체를 삭제한다.
  • how='any': 행에 포함된 여러 컬럼 중 단 하나라도 결측치가 있으면 삭제한다. (가장 엄격한 정제 방식)
  • how='all': 행의 모든 컬럼이 결측치인 경우에만 삭제한다. (데이터가 완전히 비어있는 행만 제거)
  • inplace=False: 원본 weather는 건드리지 않고, 정제가 완료된 새로운 데이터프레임을 반환하여 pure_weather에 저장한다.
missing_count_mean = weather['평균풍속'].isna().sum()
missing_count_max = weather['최대풍속'].isna().sum()
missing_count_all = weather.isna().sum().sum()
print(missing_count_mean)
print(missing_count_max)
print(missing_count_all)

 

결측치 처리하기

fillna(0)를 사용하여 결측치를 특정 값으로 채우기

기상 데이터에서 결측치를 0으로 채우는 것은 주의가 필요하다. 예를 들어, 풍속이나 강수량이 실제로는 있었는데 관측되지 않은 것일 수도 있는데, 이를 0으로 처리하면 평균값이 실제보다 낮게 왜곡될 위험이 있다.

# weather.fillna(0, inplace=True)
pure_weather = weather.fillna(0, inplace=False)
print(pure_weather.loc['2012-02-21'])
print(weather.loc['2012-02-21'])

 

더 나은 전처리 전략

방법 코드 특징
평균값 채우기 df.fillna(df.mean()) 전체적인 데이터 분포의 왜곡을 최소화함
직전 값으로 채우기 df.fillna(method='ffill') 어제 날씨와 오늘 날씨가 비슷할 것이라는 가정 (Forward fill)
선형 보간법 df.interpolate() 앞뒤 데이터의 중간값으로 자연스럽게 채움

 

결측치를 평균값으로 채우기

print(weather.describe())
print(weather.mean())
# 컬럼별 평균값으로 각각 채우기
pure_weather = weather.fillna(weather.mean(numeric_only=True))
print(pure_weather.describe())

 

결측치를 직전 값으로 채우기

 

  • 2012-02-11의 데이터가 원래 결측치였다면, pure_weather에서는 2012-02-10의 값이 그대로 들어가게 된다.
  • 만약 2012-02-11과 2012-02-12가 연속으로 결측치였다면, 두 날짜 모두 2012-02-10의 값으로 채워진다. 즉, 유효한 값이 나올 때까지 계속 전달(Forwarding)된다.
  • 첫 번째 행의 결측치: 만약 데이터프레임의 가장 첫 번째 행(index=0)이 결측치라면, 앞에 가져올 값이 없으므로 ffill을 적용해도 NaN으로 남는다.
  • 해결책: 이럴 때는 method='bfill'(Back fill, 뒤의 값을 앞으로 가져오기)을 추가로 사용하거나, 전체 평균값을 섞어서 사용해야 한다.

 

# 온도라는 것은 전날 값과 비슷할 수 있다
print(weather.loc['2012-02-11'])
print(weather.loc['2012-02-12'])
pure_weather = weather.fillna(method='ffill', inplace=False)
print(pure_weather.loc['2012-02-10'])
print(pure_weather.loc['2012-02-11'])
print(pure_weather.loc['2012-02-12'])
pure_weather.describe()

 

pd.DatetimeIndex의 유연성

작성한 코드에서 볼 수 있듯이 판다스는 다양한 날짜 형식을 똑똑하게 알아서 처리한다.

  • 다양한 구분자: /, -, . 등을 혼용해서 써도 판다스가 표준 형식(YYYY-MM-DD)으로 자동 변환한다.
  • 순서 인식: 01/03/2018처럼 월/일/연도 순서로 되어 있어도 문맥에 따라 날짜 객체로 파싱한다.
d_list = ["01/03/2018", "01-03-2018", "2018-01-05", "2018/01/06"] # ssv
dates = pd.DatetimeIndex(d_list)
print(dates)
print(dates.year)
print(dates.month)
print(dates.day)

dt_list = ["01,03,2028 11:12:13", "01-03-2018 11:22:13"]
times = pd.DatetimeIndex(dt_list)
print(times.hour)
print(times.minute)

 

 

날짜 데이터에서 일시를 년도로 바꾸기

weather = pd.read_csv(weather_file, encoding='CP949')
print("일시\n", weather['일시'])
print("before\n", weather)
weather['일시'] = pd.DatetimeIndex(weather['일시']).year # pd.DatetimeIndex 형태로 바꿔달라는 뜻
print("after\n", weather)
print(weather.head())

 

월마다 평균기온의 평균 구하기

 

  • [None for _ in range(12)] (리스트 컴프리헨션): 파이썬에서 가장 권장되는 방식이다. 내부적으로 최적화되어 있어 append()를 반복하는 일반 for 문보다 처리 속도가 빠르다.
  • [0] * 12 (리스트 곱셈): 단순한 숫자나 문자열로 초기화할 때 가장 간결하고 빠르다. 다만, 리스트 안에 가변 객체(리스트 등)를 넣을 때는 참조 문제(하나를 바꾸면 다 바뀌는 현상)가 발생할 수 있으니 주의해야 한다.

 

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

weather = pd.read_csv(weather_file, encoding='CP949')
weather['month'] = pd.DatetimeIndex(weather['일시']).month

# 초기화 함 ===
# monthly = [None for x in range(12)]
monthly = [None for _ in range(12)]
print("monthly1", monthly)

monthly = list()
for _ in range(12):
    monthly.append(None)
print("monthly2", monthly)

monthly_wind = [0 for x in range(12)]
print("monthly_wind1", monthly_wind)

monthly_wind = [0] * 12
print("monthly_wind2", monthly_wind)
# ===

for i in range(12): # 0~11
    monthly[i] = weather[weather['month'] == i + 1] # 해당 월 데이터를 리스트로 쫙 뽑음
    monthly_wind[i] = monthly[i]['평균기온'].mean() # 리스트로 뽑은 월 데이터 평균내기 (평균 리스트)

months = np.arange(1, 13)
plt.bar(months, monthly_wind, color='green')
plt.xlabel('Month')
plt.ylabel('Temperature')

 

월마다 평균기온의 평균 구하기

weather['month'] = pd.DatetimeIndex(weather['일시']).month
print(weather)

monthly_means = weather.groupby('month').mean(numeric_only=True) # goupby: 같은건 하나로 압축하자
print(monthly_means)
print(monthly_means['평균기온'])

months = np.arange(1, 13)
plt.bar(months, monthly_means['평균기온'], color='green')
print("평균기온", monthly_means['평균기온'])
plt.xlabel('Month')
plt.ylabel('Temperature')

 

월의 평균 (평균기온, 최대풍속, 평균풍속)과 최대 (평균기온, 최대풍속, 평균풍속) 구하기

 

  • y_means (연도별 평균): 해당 연도의 기후가 평년보다 따뜻했는지, 아니면 바람이 더 강했는지 등의 전반적인 경향을 보여준다. 기후 변화 분석의 가장 기초적인 지표다.
  • y_max (연도별 최댓값): 그해 가장 뜨거웠던 날의 기온이나 가장 강력했던 태풍의 풍속을 보여준다. 평균 데이터에서는 드러나지 않는 극한 기상(Extreme Weather) 사건을 파악할 때 사용한다.
weather_file = './weather.csv'
weather = pd.read_csv(weather_file, encoding='CP949')
weather['year'] = pd.DatetimeIndex(weather['일시']).year
print(weather)

y_means = weather.groupby('year').mean(numeric_only=True) # numeric_only 의 default 값은 False
print(y_means)

y_max = weather.groupby('year').max(numeric_only=True)
print(y_max)

 

불리언 인덱싱

① 비교 연산과 불리언 마스크(Boolean Mask)

monthly_means['평균풍속'] >= 4.0 코드가 실행되면, 판다스는 내부적으로 모든 행을 순회하며 값을 비교한다.

  • 결과값은 데이터 자체가 아니라, 각 행이 조건에 맞는지 여부를 나타내는 True/False로 구성된 Series다.
  • 이를 불리언 마스크 또는 필터라고 부른다.

② 로지컬 인덱싱(Logical Indexing)

monthly_means[selection] 처럼 대괄호([]) 안에 불리언 마스크를 넣으면, 판다스는 True에 해당하는 위치의 행만 추출하고 False인 행은 버린다.

 

selection = monthly_means['평균풍속'] >= 4.0
print(selection) # True, False만 리턴함
# []: logical indexing

print(monthly_means[selection]) # 평균풍속이 4.0보다 큰것만 나옴

 

 

연도별 평균 기온을 막대그래프로 시각화

groupby('year')를 실행하면, 그룹의 기준이 되었던 **'year' 컬럼이 데이터프레임의 인덱스(Index)**로 승격된다.

  • y_means를 출력해 보면 왼쪽 끝에 연도(2012, 2013...)가 붙어 있는데, 이것이 바로 인덱스다.
  • plt.bar(y_means.index, ...)라고 작성한 것은 **"X축의 눈금을 데이터프레임의 인덱스(연도)로 사용하겠다"**는 뜻이다.
  • y_means.index: 2012, 2013, 2014... 등 연도 값이 들어있는 배열 형태의 객체다.
  • y_means['평균기온']: 각 연도에 해당하는 Y축 값(기온)이다.
  • color='green': 막대 색상을 녹색으로 지정하여 시각적 구분을 줬다.
weather = pd.read_csv(weather_file, encoding='CP949')
print(weather)
weather['year'] = pd.DatetimeIndex(weather['일시']).year
y_means = weather.groupby('year').mean(numeric_only=True)

print(y_means.index)

plt.bar(y_means.index, y_means['평균기온'], color='green')
plt.xlabel('Year')
plt.ylabel('Temperature')

 

데이터 생성

pivot은 데이터프레임에서 특정 컬럼을 행 인덱스로, 다른 컬럼을 열 인덱스로 전환하여 표의 구조를 직관적으로 바꾸는 도구다.

  • index: 새로운 데이터프레임의 행이 될 컬럼명
  • columns: 새로운 데이터프레임의 열이 될 컬럼명
  • values: 표 안에 채워질 데이터 값
df = pd.DataFrame({'상품': ['시계','반지','반지','목걸이','팔찌'],
                   '재질': ['금','은','백금','금','은'],
                   '가격': [500000, 200000, 3500000, 300000, 600000
                          ]})
print(df)

 

0행 2열값 가져오

df.iloc[0,2]

 

상품을 행, 재질을 열로 만들기

new_df = df.pivot(index='상품', columns='재질', values='가격')
new_df.fillna(value=0, inplace=True)
print(new_df)

 

'브랜드'를 행 인덱스로, '자동차명'을 열 인덱스로 설정

import pandas as pd

data = {
    '자동차명': ['GV80', 'IONIQ 5', 'Model Y', 'IX3', 'S Class', 'GV60'],
    '브랜드': ['제네시스', '현대자동차', '테슬라', 'BMW', 'Mercedes Benz', '제네시스'],
    '종류': ['내연기관', '전기차', '전기차', '전기차', '내연기관', '전기차'],
    '가격': [8000, 5500, 5600, 8300, 14000, 7000]
}

df = pd.DataFrame(data)
print("--- 원본 데이터프레임 ---")
print(df)

# pivot 실행
# index: 새로운 행 이름, columns: 새로운 열 이름, values: 채울 값
pivot_df = df.pivot(index='브랜드', columns='자동차명', values='가격')

# 결측값(NaN)을 0.0으로 채우기
pivot_df = pivot_df.fillna(0.0)

print("\n--- 변환된 데이터프레임 ---")
print(pivot_df)

 

데이터프레임 생성

import pandas as pd

df_1 = pd.DataFrame( {'A' : ['a10', 'a11', 'a12'],
                      'B' : ['b10', 'b11', 'b12'],
                      'C' : ['c10', 'c11', 'c12']} ,
                      index = ['가', '나',  '다'] )

df_2 = pd.DataFrame( {'B' : ['b23', 'b24', 'b25'],
                      'C' : ['c23', 'c24', 'c25'],
                      'D' : ['d23', 'd24', 'd25']} ,
                      index = ['다', '라',  '마'] )
print(df_1)
print(df_2)

 

조인하기

① join='outer' (기본값)

df_3 = pd.concat([df_1, df_2], join='outer')

  • 논리: 합집합(Union). 두 데이터프레임이 가진 모든 컬럼(A, B, C, D)을 다 살린다.
  • 결과:
    • df_1에만 있는 A 컬럼, df_2에만 있는 D 컬럼이 모두 포함된다.
    • 데이터가 없는 칸은 NaN으로 채워진다. (예: df_1 영역의 D 컬럼 값은 모두 NaN)

② join='inner'

df_4 = pd.concat([df_1, df_2], join='inner')

  • 논리: 교집합(Intersection). 두 데이터프레임에 공통으로 존재하는 컬럼만 남긴다.
  • 결과:
    • 공통 컬럼인 B와 C만 남고, 한쪽에만 있는 A와 D는 삭제된다.
    • NaN이 발생하지 않아 데이터가 깔끔해지지만, 정보 손실(A, D 삭제)이 발생한다.
df_3 = pd.concat([df_1, df_2])
print(df_3)

df_3 = pd.concat([df_1, df_2], join='outer')
print(df_3)

df_4 = pd.concat([df_1, df_2], join='inner')
print(df_4)

 

머지하기

방식 (how) 설명 결과 특성
left 왼쪽(df_1) 기준 df_1의 모든 행을 유지함. 매칭되는 정보가 df_2에 없으면 NaN
right 오른쪽(df_2) 기준 df_2의 모든 행을 유지함. 매칭되는 정보가 df_1에 없으면 NaN
outer 합집합 (Union) 두 데이터프레임의 모든 행을 포함함. 매칭되지 않으면 양쪽 모두 NaN
inner 교집합 (Intersection) 양쪽 모두에 존재하는 키 값에 대해서만 행을 생성함 (가장 엄격함)
print(df_1.merge(df_2, how='left', on='B'))
print(df_1.merge(df_2, how='right', on='B'))
print(df_1.merge(df_2, how='outer', on='B'))
print(df_1.merge(df_2, how='inner', on='B'))

 

'머신러닝' 카테고리의 다른 글

머신러닝 - 다중회귀  (0) 2026.01.11
머신러닝 - 선형 회귀 기초  (0) 2026.01.09
SEABORN  (0) 2026.01.07
MATPLOTLIB  (0) 2026.01.07
넘파이(NumPy) 2  (0) 2026.01.07