22.10.13
오늘도 EDA를 해보았다.
EDA가 뭔지 감이 조금 오는 것 같다.!.!.!.!
근데 수업을 듣다보니, 옛날에 옛날도 아니지 불과 몇주 전에 배웠던 내용들 중 기억 안나는 내용들도 있더라. enumerate라던지, heatmap에 사용할 mask를 만드는 법이라던지! 괜찮아괜찮아 복습 열심히 하자! 배웠었다는 것만 기억하고! 그때 그때 찾아보며 사용해도 되겠다!
정리 시작!
출처를 밝히기! 해당 내용은 멋쟁이사자처럼 AI School 박조은 강사님의 자료로 수업을 들으며 학습한 내용입니다. 일부 코드 및 내용의 인용을 포함하고 있습니다!
0306파일과 0307파일은 공공데이터포털에서 제공하는 국가 중점 데이터 중 하나인 의약품 처방정보 데이터셋을 EDA하는 실습이었다.
0306파일에서는 데이터셋의 크기가 너무 커 일부만 먼저 분석해보기 위해 샘플링해보는 과정을 진행했다.
0307파일에서는 여태껏 실습에서 많이 진행해봤듯이 EDA를 진행했다.
이제 어느정도 익숙해질만도 하다!
데이터셋 로드 -> 데이터 요약 정보 확인하기 -> 데이터 전처리(결측치 및 중복치 처리, 자료형 변환) -> 파생변수 만들기(분석할 특성을 정해 이용할 파생변수 만들기) -> 데이터 분석 (기술통계 값 및 시각화하여 확인하기)의 과정을 거쳤다. 분석 시, 상관계수를 구해보기도 하고, 특정 변수들에 대해 그룹화하여 시각화하기도 하였다. 변수가 가지는 의미를 생각해보며 특이한 값들을 추출하여 살펴보는 과정도 진행했다.(조건과 isin 활용)
EDA가 이렇게 하는거구나는 이제 조금은 느낌이 온다!
0306 파일
1. 데이터 로드하기
2. 데이터 샘플링(numpy 또는 pandas 활용)
3. 샘플링한 데이터만 csv로 저장
0307 파일
1. 샘플링한 데이터 로드
2. 데이터의 요약 값 살펴보기 (info, nunique, shape 등 전체 데이터프레임 살펴보기)
3. 데이터 전처리 (결측치 제거, 중복값 제거, 필요 없는 컬럼 제거, 데이터 타입 변경 등)
4. 파생변수 만들기(분석하기에 더 편리하도록 파생변수를 만든다. 원래 코드로 표시되어있던 데이터들을 문자열로 변경해주는 작업도 포함된다.)
5. 데이터 분석 및 시각화(기술통계값을 살펴보기도 하고, 그룹화하여 빈도수를 구해보기도 하며 데이터의 전체 및 세부적인 내용들을 분석한다. 특정 조건에 맞는 데이터들을 추출해 살펴보기도 한다. 상관계수를 구할 수도 있다. 다양한 시각화를 통해 더 쉽고 직관적으로 분석을 진행할 수 있다.)
와 저게 EDA구나! 이거 정리하면서 감이 좀 왔다. 와!
미니 프로젝트도 유사한 과정으로 진행하면 될 것 같기도 하다!
우리가 가진 데이터셋은 1000만개의 행이 있고, 가입자 일련번호만 보자면 100만개의 고유값이 있다. 데이터를 분석하거나 전처리할때, 일부에 대해 먼저 진행해보는 습관이 중요함을 잊지 말자! 이를 위해 가입자 일련번호 중 1만개만 랜덤으로 샘플링 해 데이터셋을 샘플링하였다.
Numpy를 활용해 데이터를 샘플링 하는 방법을 두가지 사용했다. 첫번째는 np.random.choice 였고, 다른 하나는 np.random.default_rng 를 사용하는 방법이었다.
np.random.seed(42)
sample_no = np.random.choice(raw['가입자 일련번호']).unique(), 10000)
sample_no
df_temp = raw[raw['가입자 일련번호'].isin(sample_no)]
np.random.choice
를 이용하여 가입자 일련번호의 고유값 중 10000개를 샘플링하는 코드이다. np.random.seed()를 활용해 랜덤 시드 값을 정해주고, 익숙한 함수인 random.choice를 활용하였다. 그리고 이를 이용해 dataframe을 샘플링하기 위해 isin과 boolean indexing을 활용했다.
다만, random.choice 기능을 공식적으로 권장하고 있지 않다. 다음의 방법인 random.default_rng 기능을 권장한다.
rng = np.random.default_rng(42)
sample_no = rng.choice(raw['가입자 일련번호'].unique(), 10000)
df_temp = raw[raw['가입자 일련번호'].isin(sample_no)]
이는 random generator라는 numpy의 기능이다. 링크 아직은 정확히 이해는 못하겠지만, 언젠가 이해할 날이 오겠지! 이해하기로는 randomState를 대체해주는 기능인 것 같다. default_rng를 통해 특정 randomState를 가졌다고 생각할 수 있는 random generator를 선언하여주게 되고, 그 generator를 활용해 random module의 기능들을 사용한다.
이때, choice() method의 replace 인자 기본값이 True 이기 때문에, 샘플링 된 값들 사이에 중복이 존재할 수 있다. 만약 추출할 사이즈를 shape 형태로 주면, 해당 shape를 가지는 numpy array가 되게 추출해준다. 신기해라. Numpy는 언제 배울까! 그때 또 머리가 깨지겠지!
Pandas를 활용해서도 간단하게 데이터를 샘플링 할 수 있다. 아주 간단하게, df.sample()
을 활용한다.
sample_no = raw['가입자 일련번호'].sample(10000, random_state=42)
df_temp = raw[raw['가입자 일련번호'].isin(sample_no)]
마찬가지로 repalce = True가 기본값(복원추출이라고 생각하면 되겠다!) 이기 때문에, 중복값이 존재한다.
프로젝트를 진행하면서 깨달은 사실이 있다.
여기서 pandas로 데이터를 샘플링 할때에는, numpy로 샘플링할때 unique()중에서 샘플링 한 것이 아니기 때문에, 그냥 전체 데이터 중 가입자 일련번호를 10000개 뽑은 것이다.
따라서 replace = False로 하더라도 중복값이 존재한다.
그리고 이렇게 샘플링한다면, 많이 등장하는 가입자 일련번호가 더 많이 추출 될 가능성이 높겠다.
데이터에 적절한 작업을 함으로써 활용하기 좋은 형태로 만드는 것.
결측치를 탐색하고 처리하는 것, 적절하지 못한 자료형을 바꾸는 것 등
to_datetime()
이거 지난번에 했었는데 사용법을 까먹었다.
pd.to_datetime() link
활용법이 아주 무궁무진하다. input으로 정수부터 문자열, Series, 심지어 DataFrame 까지 들어갈 수 있다.
format 인자에는 The strftime to parse time이 들어간다.
%Y는 4글자 연도, %y는 2글자 연도, %m과 %d는 월과 일이다.
object형식으로 되어있을때에는 format을 주지 않아도 된다.
파생변수 만들기(map)
map 을 이용하여 파생변수를 만드는건 지난번에도 자주 했던 방식. 오늘 배운 특이한 점을 살펴보자면, apply와는 달리 map에는 dictionary를 인자로 줄 수 있다는 것!
map(dict)와 같이 사용하면 series에 딕셔너리의 key -value 로 매핑을 진행해준다.
잘 활용하면 아주 편하고 간단하게 사용할 수 있겠다.
.replace(dict) 또한 동일한 기능을 수행해준다.
여태까지 배운 내용의 활용
파생변수를 만들 때, 코드 형식으로 되어있는 데이터를 문자열로 바꿔주는 실습을 오늘 많이 했다. 대부분 map을 이용해서 진행했다.
근데 map을 이용하기 위해서는 dictionary가 존재해야 하는데, 이 dict가 존재하지 않는 경우가 있었다.
웹상에 테이블 형태로 key와 value가 존재했다.
판다스의 pd.read_html()을 배웠으므로, 이를 이용할 수 있었다. 이를 통해 그 table을 읽어오고, table에서 원하는 내용을 뽑아 파생변수를 만드는 데에 사용할 수 있었다.
이 과정에서 pandas의 merge를 사용했다. 어떤 방법으로 잘 응용하여 사용할 수 있는지 충분한 고민과 실습이 필요하겠다.
0307 실습
df['제형코드'] = df['약품일반성분명코드'].str[-2:]
table = pd.read_html('https://www.health.kr/drug_info/basedrug/main_ingredient.html')[1]
df_table = table[['제형코드','제형명칭']]
df = pd.merge(df,df_table, on = '제형코드', how = 'left')
이렇게 진행할 수 있었다.
합친다는 생각이 들긴 했었는데, map을 사용하던 중이었으므로 map을 사용해야겠지 하고 시도했었다. 어떻게 합쳐야 될지 생각도 안들었고, 합치면 코드에 맞는 이름들만 잘 합쳐지나 라는 생각도 했다. 근데 SQL에서 JOIN 배운거 생각해보면 당연히 그렇게 합쳐진다!
뭐 어쨌든, 내가 시도한 코드는 이것이다.
df['제형코드'].apply(lambda x : table.loc[table['제형코드'] == x, '제형명칭'].iloc[0])
처음에는 table.loc[]한 결과를 return 하려 했었는데, 그것만 실행해보니 결과가 Series임을 알게 되었다. 따라서 [0]을 통해 value만 return하려 시도했고, apply 함수를 동작시켜보니 계속 keyerror : 0이 나왔다. 당연! index 값이 다 0이 아니니까!
그래서 iloc을 사용해 iloc[0]으로 했더니, 동작은 한다. 다만.... 1분 30초씩 걸린다.
멘토님께 질문 남겼더니 boolean indexing이 매 행마다 반복되어 아주 오래걸리는 거라고, 정제된 데이터를 변수에 담아 사용하거나 연산양을 줄이면 속도의 차이가 있을거라고 해주셨는데.. 흠.. 이해를 해보자!
저 코드가 동작하는 방식을 생각해보자. apply를 통해 df['제형코드'] Series의 요소 하나하나에 접근하며 lambda 함수를 사용한다.
요소 하나당 시행하는 동작이, table에서 boolean indexing을 진행해 조건에 맞는 데이터의 '제형명칭' 컬럼만 가져오고, iloc으로 값을 뽑아온다.
음.. 그러면.. 오래 걸리는건가..?
그렇겠지! boolean indexing이라는게 애초에 Seires의 모든 데이터에 대해서 True False로 값을 반환하는 연산을 진행해야 하는거니까. table의 모든 데이터(48개)에 대해 df['제형코드']의 모든 데이터(74만개) 만큼의 연산을 해야하니.. 오래 걸릴 수밖에 없겠다!
빠르게 만들 수 있는 방법이 있을까? 연산 양을 줄이는 방법? apply나 map을 사용하려면 무조건 74만번의 연산은 필요한 셈이니. 그걸 줄일 수는 없을 듯 하다. lambda 함수 내의 동작의 연산을 줄여보려 해도, 음 table의 index를 제형코드로 바꾸고 loc으로? 해보자.
table_index = table.set_index('제형코드')
df['제형코드'].apply(lambda x : table_index.loc[x, '제형명칭'])
와 뭐지! 뭐지!!!! 엄청 빨라졌다! 4초밖에 안걸린다! 대박! 와! 왜지!
와 왜 줄어들었지? 물론 merge가 0.4초 걸리는거 보다야 훨씬 느리지만. 왜 빨라진걸까? boolean indexing을 할 필요가 없어서! 48번씩 연산해야 할게 사라졌으니까! 대박이다. 이거 잘 기억해놔야겠다. 연산 양을 줄이거나 정제된 데이터를 변수에 담아서 사용한다!
그나저나 merge는 왜이렇게 빠른거람 엄청 빠르네! 그래도 기분 좋다. 이게 왜 된담!
기술통계를 보고, 특이한 값을 추출하여 살펴보는 실습을 해봤다.
예를 들면, 일련번호가 22인것이 max 였는데, 일련번호란 한 처방전 내에서 약들 사이에 매겨지는 번호를 의미하므로, 해당 일련번호가 있는 처방내역은 약이 22 종류나 처방된 것이다.
이를 실제 데이터로 추출하여 살펴보기 위해 다음과 같은 코드를 사용했다.
drug_22 = df.loc[df['일련번호']==22,'처방내역일련번호']
df[df['처방내역일련번호'].isin(drug_20)].sort_values('일련번호')
주로 데이터를 특정할 수 있는 컬럼의 값들을 변수에 담고, isin을 해당 컬럼에 사용하여 데이터를 추출해 살펴보는 방식으로 사용했다.
혹은 이렇게 간단하게 볼 수 있는 것도 있었다.
df[df['금액'] == df['금액'].max()]
금액이 가장 큰 데이터를 살펴보기 위해 이렇게 사용했다.
오늘 가장 히트였지! 기억이 거의 안나던데!
일단 mask를 만드는 것부터 기억이 안났다.
이렇게 만든다!
mask = np.triu(np.ones_like(corr))
np.ones_like 또는 np.zeros_like 함수는 인자로 넣어준 df의 shape와 동일한 형태를 가지는 요소가 모두 1 또는 0인 행렬을 만들어준다.
np.triu 또는 np.tril 함수는 행렬의 상삼각 혹은 하삼각 부분만 남겨준다.
이 두개를 조합하여 mask를 만들었다. 상삼각행렬로 만들었다!
그리고 heatmap의 mask 인자에 mask 변수를 주면? mask로 가린 부분은 표시가 되지 않는다. 이 외에도 vmax나 vmin은 cmap에서 수치가 가지는 최대와 최솟값을 의미하고, annot = True 와 fmt 지정으로 데이터 값을 표시할 수도 있었다.
seaborn의 그래프는 plt.figure(figsize = (12,10))과 같이 사이즈 변경할 수 있던거 기억하고!
seaborn의 style!
Matplotlib이나 pandas를 이용해 시각화를 해도 seaborn의 스타일을 사용하는 것을 추천할 정도로 잘 되어있다.
plotly의 속도를 빠르게!
전에도 배운 바 있듯이, plotly에서 연산까지 하려면 속도가 더욱 느려지기 때문에, 연산이 필요하다면 pandas를 이용해 연산을 마친 후 plotly로 시각화 하면 빠르게 할 수 있다.
countplot을 잊지 말아요...!
아주 당황했다. 분명히 범주형 변수인데.. 도수를 세서 표시해야 하는데.. seaborn에서 뭘로 시각화 해야하지...? barplot으로 하다가 포기했다. countplot이 있었다. 그 seaborn의 핵심 사진에 없는 플롯들. regplot, lmplot, countplot은 기억하자!
특히 countplot은 더욱! 범주형 변수의 도수를 시각화 할 수 있다.
sns.countplot(data = df, x= '월')
으로 시각화한다.