Pandas-03 (fine_dust s2-17~23) / 미세먼지 분석 다시(파일 오류로 끊음 영상 s2-23)
1. 사용 라이브러리 import
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
2. 파일 합치기 및 인덱스 정렬 (concat, RangeIndex)
- date_day_2016~2019.xlsx file을 가져와 df로 합치기 작업
#data_day_2016~2019.xlsx 까지 파일 가져오기
#'./data_01/data_day_2016.xlsx' 파일을 가져와 df2016이라는 이름을 부여합니다.
df2016 = pd.read_excel('./data_01/data_day_2016.xlsx', engine='openpyxl')
#'./data_01/data_day_2017.xlsx' 파일을 가져와 df2017이라는 이름을 부여합니다.
df2017 = pd.read_excel('./data_01/data_day_2017.xlsx', engine = 'openpyxl')
#'./data_01/data_day_2018.xlsx' 파일을 가져와 df2018이라는 이름을 부여합니다.
df2018 = pd.read_excel('./data_01/data_day_2018.xlsx', engine='openpyxl')
#'./data_01/data_day_2019.xlsx' 파일을 가져와 df2019라는 이름을 부여합니다.
df2019 = pd.read_excel('./data_01/data_day_2019.xlsx', engine='openpyxl')
#df2016, df2017, df2018, df2019를 합쳐 한 개의 DataFrame으로 만들어 df라는 이름을 지정합니다.
dfList = [df2016, df2017, df2018, df2019]
df = pd.concat(dfList, axis=0)
#or
import glob
dfList = []
for x in sorted(glob.glob('data_01/data_day_*.xlsx')):
temp = pd.read_excel(x)
dfList.append(temp)
df = pd.concat(dfList, axis=0)
df.shape
#df의 shape을 출력하여 전체 행, 열의 수를 확인합니다.
df.shape
#df의 row의 개수 및 각 column의 정보 및 메모리 사용량을 확인합니다.
df.info(memory_usage='deep')
#df의 마지막 3개 행을 출력해 봅니다.
df.tail(3)
#데이터가 총 60208인데 마지막 행의 번호가 올바르지 않은 것을 볼 수 있음.
#--> concat을 하더라도 index는 유지 되기 때문.
#df2016, df2017, df2018, df2019의 각 shape을 확인합니다.
df2016.shape, df2017.shape, df2018.shape, df2019.shape #[(14269, 8), (14235, 8), (12447, 8), (19257, 8)]
#or
[x.shape for x in dfList]
#첫 번째 파일 마지막부터 두 번째 파일의 시작 부분의 실제 index를 확인해 봅니다.
df.iloc[14268:14271, :]
이 과정을 통해 dfList를 합칠 때 index번호가 reset되지 않고 그대로 들어간 것을 알 수 있음
여러 개 df를 합친 경우 index번호를 RangeIndex로 새롭게 부여
- pd.concat([df1, df2, ...],ignore_index=True)
- df.index = pd.RangeIndex(len(df))
#해결 방법 1) 처음부터 index번호를 len으로 정리하면서 합치기
dfList = [df2016, df2017, df2018, df2019]
df2 = pd.concat(dfList, ignore_index=True)
df2.tail(3) # 정상 출력
#2) index를 만들어 사용하기
df.index = pd.RangeIndex(len(df))
df.tail(3)
3. 날짜 타입 변경
#df의 컬럼별 데이터 타입을 확인합니다.
df.dtypes
#df의 '측정일시'에 대해서 astype을 사용해 데이터 타입을 datetime으로 변경합니다.
#측정일시 int64 -> datetime 으로 변경
df['측정일시'].astype('datetime64[ns]') #되지 않음
위 구문처럼 astype('datetime64[ns]') 로도 타입이 변경되지 않는다면 pd.to_datetime을 사용해야함
날짜, 시간 타입 변경
- pd.to_datetime(Series, format='형식')
- format: %Y: 4글자 년도, %y : 2글자 년도, %m : 2글자 월, %d : 2글자 일 (format 필수 X)
#df의 '측정일시'에 대해서 format='%Y%m%d'을 지정하여 데이터 타입을 datetime으로 변경합니다.
pd.to_datetime(df['측정일시'], format='%Y%m%d')
df.insert(위치, 이름, 데이터)
- return 이 없는 함수 (inplace 동작)
#df의 '측정일시'에 대해서 format='%Y%m%d'을 지정하여 데이터 타입을 datetime으로 변경한 뒤
#'측정일시2' 컬럼을 '측정일시' 뒤에 삽입 // 측정일시 위치 - 0
df.insert(1, '측정일시2', pd.to_datetime(df['측정일시'], format='%Y%m%d'))
df.head(2)
#확인해보면 측정일시[0] 뒤에 측정일시2[1]가 추가된 것 확인 가능
datetime 타입은 accessor-dt 를 사용할 수 있음
- dt.year : year값 반환
- dt.month : month값 반환
- dt.day : day값 반환
#df의 '측정일시2' 컬럼의 dt accessor를 사용하여 year 정보만 확인합니다.
df['측정일시2'].dt.year
#df의 '측정일시2' 컬럼의 dt를 사용하여 month 정보만 확인합니다.
df['측정일시2'].dt.month
#df의 '측정일시2' 컬럼의 dt를 사용하여 day 정보만 확인합니다.
df['측정일시2'].dt.day
#요일수치형(월:0~일:6)
df['측정일시2'].dt.dayofweek
#df에 '년', '월', '일' 컬럼을 추가합니다.
df['년'] = df['측정일시2'].dt.year
df['월'] = df['측정일시2'].dt.month
df['일'] = df['측정일시2'].dt.day
- df.drop([컬럼1, 컬럼2, ...], axis=1) : 컬럼 삭제
- df.drop ([컬럼1, 컬럼2, ...], axis=0) : 행 삭제
삭제할 컬럼 또는 행이 1개인 경우 목록이 아닌 한 개 이름만 지정할 수 있음
#'측정일시2' 컬럼이 필요 없으므로 제거합니다.
#컬럼을 제거하기 위해서는 drop 함수에서 axis=1 을 사용해야 합니다.
#삭제된 DataFrame을 df2 이름을 지정합니다.
df2 = df.drop(columns=['측정일시2'])
#or
df2 = df.drop(df['측정일시2'], axis=1)
#df -> .csv 파일로 저장 (이름: fine_dust.csv/ 인덱스 저장 x)
df2.to_csv('fine_dust.csv', index=False)
#fine_dust.csv 읽어오기
df2 = pd.read_csv('fine_dust.csv')
4. 결측치 확인
heatmap 그래프를 이용하여 결측치 확인 (데이터가 많을 때 직관적인 판단을 할 수 있게 함)
#df2를 복사하여 df를 생성합니다.
df = df2.copy()
#df의 행별 결측치를 조사합니다.
df.isna().sum()
#'측정일시'를 index로 설정하고, index 기준으로 오름차순 정렬해서 df1으로 이름 붙이기
#(그래프에서 y축으로 사용)
df1 = df.set_index('측정일시').set_index()
#df1의 '이산화질소농도(ppm)':'일산화탄소농도(ppm)'의 결측치의 상태를 그래프로 확인
#seaborn.heatmap()를 사용하며, df.isna()를 데이터로 지정해 확인 가능 (밝은 색이 결측치)
import koreanize_matplotlib
plt.figure(figsize=(14, 10)) #canvas 사이즈
ax = sns.heatmap(df1.loc[:, '이산화질소농도(ppm)':'일산화탄소농도(ppm)'].isna(), cbar=False)
ax.set_xticklabels(ax.get_xticklabels(), fontsize=13, rotation=0)
plt.show()
#-------------------------------------------------------------------------------------------
#데이터 중에서 오존농도, 미세먼지, 초미세먼지에 대해서만 사용할 것이므로,
#indexing한 뒤 다시 결측치 정보를 확인
#df1에서 '측정소명', '년', '월', '일','오존농도(ppm)', '미세먼지(㎍/㎥)', '초미세먼지(㎍/㎥)'
#컬럼만 추출하여 df_dust 라는 이름을 정의
df_dust = df1[['측정소명', '년', '월', '일','오존농도(ppm)', '미세먼지(㎍/㎥)', '초미세먼지(㎍/㎥)']]
#df_dust의 컬럼별 결측치 개수
df_dust.isna().sum()
#df_dust의 '오존농도(ppm)':'초미세먼지(㎍/㎥)'에 대한 결측치 heatmap을 그려봅니다.
plt.figure(figsize=(7,8))
ax = sns.heatmap(df_dust.loc[:, '오존농도(ppm)':'초미세먼지(㎍/㎥)'].isna(), cbar=False)
ax.xticklabels(ax.get_xticklabels(), rotation=0, fontsize=12)
plt.show()
#df_dust의 측정일시 index를 다시 컬럼으로 사용되도록 함 (다음 작업인 결측치 삭제를 사용하기 위함)
df_dust = df_dust.reset_index()
5. 결측치 제거
df.dropna(axis=0, how='any/ all', thresh=None, subset=None, inplace=False)
결측치 제거에 사용되는 메서드
how = 'any' : 결측치가 1개 이상 포함된 행 삭제
how = 'all' : 모든 데이터가 결측치인 행 삭제
axis=1 : 컬럼에 대해 동작
thresh=n : n 이상의 데이터를 가진 행은 삭제하지 않음
subset=[컬럼명1, ...] : subset으로 지정된 컬럼만 사용하여 삭제 대상 검색
#df_dust 에서 ['오존농도(ppm)','미세먼지(㎍/㎥)', '초미세먼지(㎍/㎥)']에서
#모든 데이터가 결측치인 행을 제거하여 결과를 temp1으로 저장합니다
temp1 = df_dust.dropna(how='all', subset=['오존농도(ppm)','미세먼지(㎍/㎥)', '초미세먼지(㎍/㎥)'])
#df_dust와 temp1의 행의 수 (len)을 확인하여 제거된 행의 개수를 확인합니다.
print(len(df_dust) - len(temp1))
#df_dust 에서 ['오존농도(ppm)','미세먼지(㎍/㎥)', '초미세먼지(㎍/㎥)']에서
#하나라도 결측치가 있는 행을 제거하여 결과를 temp2으로 저장합니다
temp2 = df_dust.dropna(how='any', subset=['오존농도(ppm)','미세먼지(㎍/㎥)', '초미세먼지(㎍/㎥)'])
#df_dust와 temp2의 행의 수 (len)을 확인하여 제거된 행의 개수를 확인합니다.
print(len(df_dust) - len(temp2))
#df_dust 에서 ['오존농도(ppm)','미세먼지(㎍/㎥)', '초미세먼지(㎍/㎥)']에서 2개 이상의 데이터를
#가진 행은 제거하지 않은 결과를 temp3로 저장합니다.
# (= 3개의 정보 중 1개의 데이터만 가진 행을 제거함)
temp3 = df_dust.dropna(thresh=2, subset=['오존농도(ppm)','미세먼지(㎍/㎥)', '초미세먼지(㎍/㎥)'])
#df_dust와 temp3의 행의 수 (len)을 확인하여 제거된 행의 개수를 확인합니다.
print(len(df_dust) - len(temp3))
#temp1, temp2, temp3의 '오존농도(ppm)':'초미세먼지(㎍/㎥)'에 대한 결측치 heatmap을 그려봅니다.
# temp1, temp3는 부분적으로 결측치가 남아있고, temp2는 결측치가 없습니다.
plt.figure(figsize=(7,8))
ax = sns.heatmap(temp3.loc[:, '오존농도(ppm)':'초미세먼지(㎍/㎥)'].isna(), cbar=False)
plt.show()
df.groupby(by=[컬럼1, 컬럼2, ...]).함수()
- 그룹 기준으로 목록을 지정하면 MultiIndex로 만들어짐
- MultiIndex인 경우 indexing은 tuple을 사용함
- ['년', '월']을 그룹 기준으로 사용한 경우 (2017, 6) 처럼 지정함
#df_dust에서 '년','월'별 '미세먼지(㎍/㎥)'데이터의 평균을 df로 만들어 meandf 라는 이름을 지정
meandf = df_dust.groupby(['년', '월'])[['미세먼지(㎍/㎥)']].mean()
meandf
#meandf에서 2017년 6월까지의 데이터만 출력합니다.
meandf.loc[:(2017, 6), :]
#meandf에 '결측치제거후' 및 '차이' 컬럼 추가
#'결측치제거후'= temp2에서 '년', '월'별 '미세먼지(㎍/㎥)' 데이터의 평균 사용
# '차이'= '미세먼지(㎍/㎥)' - '결측치제거후'
meandf['결측치제거후'] = temp2.groupby(['년','월'])[['미세먼지(㎍/㎥)']].mean()
meandf['차이'] = meandf['미세먼지(㎍/㎥)'] - meandf['결측치제거후']
#meandf에서 2017년 6월까지의 데이터만 출력
meandf.loc[:(2017, 6), :] # 여러 개의 값에 차이가 있는 것 확인 가능
# 선생님 코멘트
# temp2의 경우 NaN 데이터를 포함한 행 삭제시 평균 값이 달라진 것을 확인 할 수 있습니다.
# 삭제된 행 중, 미세먼지에 대한 값을 가진 것이 있기 때문입니다.
# "결측치 제거시에는 주의가 필요합니다."
6. 결측치 대체 (평균값)
Series.mask(조건, 조건이 참일 때 사용할 값 or 목록)
조건이 True 인 것에 대해 다른 값 변경
s.isna(): NA값에 대해 True / mask 사용 시 isna 사용 유용
Series.where(조건, 조건이 거짓일 때 사용할 값 or 목록)
조건이 False인 것에 대해 다른 값 변경
s.notna(): NA값에 대해 False / where 사용 시 notna 사용 유용
#mask/ where 연습 구문
import numpy as np
temp = pd.DataFrame({'A':[np.nan, 2, np.nan, 4],
'B':[7, 8, 9, 10],
'C':[1, 2, 4, 8]})
# temp의 'A' 열에 대해서 결측치인 경우 'B'의 값으로 대체합니다.
temp['A'].mask(temp['A'].isna(), temp['B'])
# 반대 (결측치가 아닐 경우)
temp['A'].where(temp['A'].notna(), temp['B'])
# temp의 'A' 열에 대해서 결측치인 경우 'C'의 값으로 대체합니다.
temp['A'].mask(temp['A'].isna(), temp['C'])
df.groupby(그룹기준컬럼)[함수를 적용할 컬럼명].transform(함수)
- index가 유지되면서 그룹별 함수 적용
#groupby + agg 및 일반 통계함수 -> index 변동 有
df_dust.groupby('측정일시')['미세먼지(㎍/㎥)'].mean()
df_dust.groupby('측정일시')['미세먼지(㎍/㎥)'].agg('mean')
#df_dust의 '측정일시' 별 '미세먼지(㎍/㎥)'의 평균 구하기
# 이때, index 변경을 하지 않기 위해 transform을 사용하고, fine_dust라는 이름을 지정
fine_dust = df_dust.groupby('측정일시')['미세먼지(㎍/㎥)'].transform('mean')
fine_dust
#df_dust['미세먼지(㎍/㎥)'], fine_dust의 결측치 값의 개수를 구합니다.
df_dust['미세먼지(㎍/㎥)'].isna().sum(), fine_dust.isna().sum()
#(881, 0)
#df_dust의 '미세먼지(㎍/㎥)'의 결측치를 fine_dust의 값으로 채우기
# 채우기 한 결과를 다시 df_dust['미세먼지(㎍/㎥)']로 저장
s = df_dust['미세먼지(㎍/㎥)']
df_dust['미세먼지(㎍/㎥)'] = s.mask(s.isna(), fine_dust)
#df_dust의 컬럼별 결측치 개수를 확인
df_dust.isna().sum() #미세먼지에 대한 결측치 제거
#-------------------------------------------------------------------------------------------
#meandf에 '결측치대체' 및 '차이2'라는 컬럼 추가
#'결측치대체' = df_dust에서 '년', '월' 별 '미세먼지(㎍/㎥)' 데이터의 평균 사용
#'차이2' = meandf에서 '미세먼지(㎍/㎥)' - '결측치대체'를 사용
meandf['결측치대체']=df_dust.groupby(['년','월'])['미세먼지(㎍/㎥)'].mean()
#이미 결측치 제거한 값이라 transform 할 필요 x
meandf['차이2']=df_dust['미세먼지(㎍/㎥)'] - meandf['결측치대체']
#meandf 에서 2017.6월까지의 데이터만 출력
meandf.loc[:(2017, 6), :]
#df_dust의 '오존농도(ppm)', '초미세먼지(㎍/㎥)' 컬럼에도 측정일시의 평균 값으로 채우기
for t in ['오존농도(ppm)', '초미세먼지(㎍/㎥)']:
x = df_dust.groupby('측정일시')[t].transform('mean')
s = df_dust[t]
df_dust[t] = s.mask(s.isna(), x) #따로 처리해도 되고 loop돌려도 됨
#df_dust의 컬럼별 결측치 개수를 확인합니다.
df_dust.isna().sum()