분명 첫 혼공학습단을 신청했던 즐거움에 공지가 나오기도 전에 메일 못받았다고 문의 넣는 나였는데...
성질머리 급해서 메일부터 넣었던... 족장님 이거 열정이 가득한거로 참작부탁드려요 그만큼 열심히 했잖아 나
어느덧 이렇게 마무리가 된게 참 시원하고 홀가분하기도 하지만, 그만큼 조금 아쉽기도 하다. 시간이 조금 더 허락했다면 더 열심히 할 수 있을것이라 생각했,,, 는 난 항상 최선을 다했다. 이정도 노력이면 충분한 것 같다.
매일 새벽 나는 이렇게 공부했다... 대학원준비와 학부공부, 출근, 후에 혼공단 공부까지...
이제 학부 졸업을 앞두고 있는데다, 대학원을 진학하기 위해 준비중인 학생이라 하루하루를 매우 바삐 살고 있었다. 그런데 여기다가 혼공단까지 하게 되니 처음에는 정말 죽을 맛이었다. 시간도 없고, 할건 많고, 끝내긴 해야하고 결국 잠을 조금씩 헌납해가면서 해야할 일을 마무리 지었던 것 같다.(와중에 출근도 했다)
나는 사실 MBTI가 ENTP인데, entp의 대표적인 특징이 흥미가 생기면 뒤도 안재고 실행에 옮긴다는 것이다. 다만, P의 성향이 짙어 미룰때 까지 미루는데 참... 혼공단 재밌어보인다고 신청했다가 미래의 나는 매일매일 공부하면서 과거의 날을 욕했던 기억이 있다. (그래도 하고나면 재밌고 뿌듯했습니다..)
심지어 ENTP의 두번째 특징은 한번 시작하면 끝을 본다는 특징도 있다. 미루는데 어떻게 끝을 보냐고? 혹 시작이 반이라는 이야기를 들어보았는가? 바로 그말이다. ENTP는 시작이 반이 아니라 시작이 99.9%이다.. 시작만 했다면 완성까지는 그냥한다. 항상 시작이 문제인것이지....
요즘 민간인 사찰도 하나봅니다. 누가 내얘기 적어놨어
하지만 그래도 1주차 부터 6주차까지 결과적으로는 너무 뿌듯했고, 만족스러웠다. 우수혼공단도 해보고 6주내내 족장님에게 블로그 정리 잘한다고 칭찬도 듣고, 나름대로 만족할만큼 공부도 했고. 무책임하게 일을 벌렸으나 그래도 항상 결과만 좋으면 된게 아니었을까? 적어도 그렇게 생각한다. 달면 삼키고 쓰면 뱉는다고, 원래 결과가 안좋으면 과정이 좋으면 그만. 과정이 별로라면 결과만 좋으면 그만이다. 아무튼 그렇다.
6주과정 야무지게 끝내서 홀가분하다. 이글을 보는 여러분들도 한번쯤은 혼공책 한권사서 끝까지 해보는 것을 추천한다. 나름 공부하는 재미도 알게 되고, 열정과 끈기를 시험할 수 있는 좋은 기회일 테니까. 그리고 공부하는데 따라오는 보상은 덤이다. 두서없이 지난시간을 돌아보았지만 그래도 즐거웠다. 기회가 된다면 다음 기수에도 만나길 고대한다. 그럼 이만!
막상 고민한게 무색하게 어찌저찌 잘 끝내고 여기까지 와서 되게 신기한 마음반, 뿌듯함 반이다.
6주차에는 회고록까지 있으니 이쯤 이야기하고 회고록에서 이야기 해보겠다.
마지막으로 배울 내용은 복잡한 데이터를 효과적으로 시각화 할 수 있는 객체지향 API에 대해서 이야기 해 볼 예정이다.
1. 객체지향 API로 그래프 꾸미기
(1) 객체지향 vs pyplot(절차지향)
우리는 저번시간까지, 데이터를 정제하고 이를 맷플롯립을 통해 표현하는 방법들을 공부했다. 그러면서, 우리는 그래프를 동시에 2가지를 그리는 경우에 대해 이야기 했었는데, subplot을 만들어 그리는 방식을 조금 더 집중적으로 배운다고 생각하면 된다. 기본적으로 하나의 그래프를 그릴 때는, 라이브러리에 있는 함수를 사용하여 간단하게 그릴 수 있다. 그러나 우리는 하나가 아닌 여러개의 그래프를 그려놓고 한번에 이를 파악해야하는 순간도 있기 때문에, 이를 쉽게 그릴 수 있도록 객체를 만들어 그리는 방식을 보통 많이 활용한다. 우선 객체지향과 pyplot 방식이 어떤 차이점이 있는지 코드를 통해 살펴보자.
import matplotlib.pyplot as plt
plt.rcParams['figure.dpi'] = 100
#pyplot 방식
plt.plot([1, 4, 9, 16])
plt.title('simple line graph')
plt.show()
#객체지향
fig, ax = plt.subplots()
ax.plot([1, 4, 9, 16])
ax.set_title('simple line graph')
fig.show()
다음 두 코드는 동일한 그래프를 결과로 반환한다. 그러나, 객체지향 방식은 pyplot 방식과 다르게, 각 그래프마다 설정을 달리 적용할 수 있도록 객체를 만들어 각 그래프에 대한 설정을 개별로 저장할 수 있다는 장점이 있다. 이러한 장점은 하나의 그래프를 그릴때는 체감이 되지않지만, 한번에 여러데이터를 한 페이지에 출력하는 것을 가정한다면 엄청 편리할 것이다.
(2) 한글 출력하기
가만보니 궁금한점이 있다. 제목이나 축을 표시할때 이때까지 영어를 사용했는데, 한글을 사용하면 안되는걸까? 사실은 사용해도 상관은 없다. 그러나 한글을 사용하고 싶다면, 한글을 지원하는 폰트를 사용해야한다. 기본적으로 맷플롯립에서 사용하는 글씨체(폰트)는 한글을 지원하지않아 한글이 깨져서 출력되는 현상이 있다. 책에서는 개발환경이 코랩이기 때문에 코랩에서 폰트를 적용하는 방법을 중점으로 이야기한다. 하지만 필자는 로컬에서 주피터노트북을 사용하기 때문에 이에 대한 내용을 중점으로 이야기 하겠다.
주피터 노트북과 같은 로컬환경에서 폰트를 적용하기전에, 폰트를 설치해야한다. 개발환경이 로컬이라면, 쉽게 사용하는 컴퓨터에 폰트를 설치해주기만 하면 준비 끝이다. 그러나 코랩의 경우 다음과 같이 폰트가 있는지 확인해보고, 없다면 설치 할 수 있다.
import sys
if 'google.colab' in sys.modules:
!echo 'debconf debconf/frontend select Noninteractive' | \
debconf-set-selctions
#나눔폰트 설치 및 적용
!sudo apt-get -qq -y install fonts-nanum
import matplotlib.font_manager as fm
font_files = fm.findSystemFonts(fontpaths=['/usr/share/fonts/truetype/nanum'])
for fpath in font_files:
fm.fontManager.addfont(fpath)
위의 코드를 실행시키면 성공적으로 폰트가 코랩환경에서도 설치가 된다. 설치 이후에는 주의할 점이 있는데, 코랩이나 주피터노트북을 중간에 사용하는 도중에 폰트를 설치하게되면, 개발환경은 이를 바로 적용할 수가 없어 사용하게 되면 오류가 생긴다. 그렇기 때문에 설치를 중간에 했다면, 커널을 다시 재부팅하면 된다. 이후 모듈을 재호출하여 사용하면 정상적으로 작동된다.
이제 폰트를 설치했으니, 이를 본격적으로 적용해보도록하자. 기본적으로 맷플롯립에서 사용하는 폰트는 'sans-serif' 다. 현재 사용중인 폰트를 확인 하고싶다면 rcParams 객체에 'font.family' 속성에 저장되어있는 것을 출력하면 된다.
import matplotlib.pyplot as plt
plt.rcParams['figure.dpi'] = 100 #폰트 적용을 위해 재호출 후 기존설정 재설정하기
plt.rcParams['font.family']
파일을 잘 다운받은 것이 확인된다. 이중 우리는 전체를 출력하기에는 너무 힘드므로, 발행건수가 제일 많은 상위 출판사 30개를 가지고만 그려보도록 하겠다. 하여 상위 30개의 데이터를 추출해보도록 하자. 이는 value_count() 메서드와 불리언 슬라이싱을 사용하면 쉽게 추출해낼 수 있다.
top30_pubs = ns_book7['출판사'].value_counts()[:30]
# 해당 열의 각데이터 종류별로 개수를 계산하여 내림차순으로 정렬하여 반환한다.
# 불리언슬라이싱은 마지막 인덱스 하나 전을 범위로 하기 때문에 이를 주의하자.
top30_pubs
top30_pubs_idx = ns_book7['출판사'].isin(top30_pubs.index)
#상위 30개 출판사가 아니라면 False, 맞다면 True를 반환하여 불리언배열을 만든다.
top30_pubs_idx
약 5만개의 책을 출판했다니, 은근 많다.(그냥 궁금해서 해보았다.) 5만개를 모두 산점도를 찍자니 조금 무리일 듯하니, 1000개 정도만 찍어보도록 하자. 그런데 우리 입맛대로 데이터를 추출하면 분석에 영향을 줄 수도 있으니, 무작위 확률로 추출하여 그래프를 그려보도록하자. 이는 sample() 메서드를 활용하면, 무작위로 표본을 추출해준다.
ns_book8 = ns_book7[top30_pubs_idx].sample(1000, random_state=42)
#random_state를 지정하면 다시 실행해도 동일한 표본을 반환한다.
#만약, 지정하지 않는다면 실행시 마다 무작위로 표본을 다시 추출한다.
ns_book8.head()
잘그려진것이 보인다. 그러나 그래프를 보고 분석을 들어가기에는 조금 문제가 있다. 바로, 어느 년도에 출판사마다 도서를 많이 대출했는지는 알 수가 없다는 것이다. 해당 그래프로는 대출을 했는지 안했는지 여부는 알 수 있으나, 이를 조금 더 디테일하게 얼마만큼을 했는지는 알기가 어렵다. 하여 마커의 크기 등 여러가지 설정을 변경하여 표현해보도록하자. 기본적으로 마커의 크기는 s 매개변수를 제공한다. 선그래프와 산점도는 마커 크기를 rcParams['line.markersize']를 통해 지정할 수 있으며 기본값이 6이다. 그리고, 이를 바탕으로 s 매개변수는 rcParams['line.markersize'] 값의 제곱을 사용한다.
fig, ax = plt.subplots(figsize = (10, 8))
ax.scatter(ns_book8['발행년도'], ns_book8['출판사'], s=ns_book8['대출건수'])
#s매개변수를 직접지정하면 마커크기가 동일해지지만, 입력데이터와 동일한 길이를 가진 배열로 지정하면
#데이터마다 다른 마커의 크기를 지정할 수있다.
ax.set_title('출판사별 발행 도서')
fig.show()
이제 효과적을 분석이 가능할 것 같다. 그러나 완벽을 추구하는 것이 좋지 않을까? 컬러맵이라는 걸 추가로 확용하면, 조금 더 데이터 건수에 따라 어떤 색인지를 표현해 줄 수 있다. 기본적으로 scatter 함수는 viridis 컬러맵을 사용한다. 우리는 주로 많이 사용하는 또다른 컬러맵인 jet 컬러맵을 활용하여 그려보자. 이때 cmap 매개변수를 통해 컬러맵을 지정할 수 있다. 그리고 컬러막대는 colorbar() 메서드를 객체에 전달하면 이를 그려준다.
이때까지 산점도, 막대그래프, 선그래프 등등 그래프를 그리는방법을 통달한 것 같다. 그러나 가장 중요한 것을 배우지 않았다. 이를 느꼈다면 분석에 조금 재능이 있을지도 모른다. 바로 굳이 여러그래프를 하나가 아닌 여러개로 봐야하냐는 것이다. 왜냐하면, 선그래프같은 경우에는 추이를 분석할 때 활용할 수 있는데, 단독이 아니라 여러 항목과 비교하며 추이를 비교할 때가 많다. 그렇다면 하나에 합치는 것이 조금 좋지 않을까? 하여 이를 조금 효율적으로 하기 위해 지금부터 이를 배워 보도록하자.
(1) 하나에 피겨에 여러 개 선 그래프 그리기
하나의 선그래프 위에 여러개의 선을 그리고 싶다면, 단순하게 plot() 함수를 여러번 호출하는 것 만으로도 충분히 할 수 있다.(너무 쉬워서 김빠지는가? 어렵게 나오면 그게 이상한거다.) 우리는 아까 활용했던 상위 30개의 출판사 인덱스를 그대로 활용하여 발행년도별 대출건수에 대한 그래프를 그려보도록 하자. 즉, 출판사 별로 발행년도 대비 대출건수의 추이를 한번에 파악해보겠다는 소리이다.
top30_pubs = ns_book7['출판사'].value_counts()[:30]
top30_pubs_idx = ns_book7['출판사'].isin(top30_pubs.index)
ns_book9 = ns_book7[top30_pubs_idx][['출판사', '발행년도', '대출건수']]
ns_book9 = ns_book9.groupby(by=['출판사', '발행년도']).sum()
#출판사별 연도별 대출건수를 표현하기 위하여 같은 연도의 대출건수는 하나로 합친다.
ns_book9 = ns_book9.reset_index()
# 인덱스를 초기화한다.
ns_book9[ns_book9['출판사'] == '황금가지'].head()
#황금가지라는 이름의 출판사에 해당하는 책데이터를 확인한다.
이제 두 출판사 간에 대출건수를 비교할 수 있다. 여기서 기능을 조금 살펴보면. 맷플롯립은 기본적으로 10가지의 선 색상을 활용한다. 그래서 색을 지정하지 않아도 자동으로 지정해준다. 그러나, 서로다른 10개 이상의 데이터를 표현할때는 중복된 색상을 사용하게 되므로, 구분이 쉽지않은 단점이 있다. 하여 이를 조금 해결하고자 맷플롯립에서는 legend() 메서드를 지원하여 범례를 추가 시킬 수 있다.
선 그래프를 많이 그려보았으니 이번에는 선그래프의 변형인 스택그래프를 그래보자. 스택그래프는 차곡차곡 쌓는 그래프라고 생각하면된다. 기존의 그래프는 각 그래프가 겹쳐지도록 그리지만 스택그래프는 이들이 서로 겹치지 않게 그린다고 생각하면 쉽다. 이를 그리려면 stackplot() 함수를 사용하면 된다.
스택그래프를 그리기 위해서는 y축의 데이터를 2차원 배열로 전달해야한다. 즉, x축에는 발행년도를, y축에는 출판사와 발행년도가 각각 행과 열로 된 2차원 배열로 주어야한다. 하여 이를 위해 데이터를 수정해보자. 이는 판다스의 pivot_table() 함수를 사용하면 된다.
이제 축에 대한 데이터를 모두 완성했으니 스택영역 그래프를 그려보도록 하자. 스택영역그래프 또한 범례를 지원한다. 또한 범례 위치를 조정할 수 있는데, 이는 loc 매개변수를 통해 지정해주면 된다. 한번 그려보자. 이때 fillna를 사용하여 누락된 값을 0으로 채워 이상하게 그려지는 경우를 방지하자.
잘 그려진 것을 확인할 수있는데, 다만 막대그래프가 조금 겹쳐져있어서 보기가 조금 불편하다. 나란히 두고싶은데, 어떻게 하면 될까? 바로 막대 위치를 조금 이동하며 그려주면 된다. 기본적으로 막대그래프의 너비를 조정하는 width 매개변수는 기본값이 0.8로 되어있다. 하여 두개를 그리고 있으니, 절반인 0.4로 각각 지정하고 위치를 0.2씩 좌우로 이동하면 겹치지 않게 나란히 그릴 수있다.
나란히 그려져서 이제 보기가 예쁘다. 하지만 데이터가 여러개라면 두께를 한없이 나눠야 하므로 많은 종류는 표현하려면 막대가 거의 선에 가까워져서 오히려 문제가 될 것 같다. 하지만 괜찮다. 이런 경우에는 스택그래프로 그려버리면 된다. 웬걸, 그리려하는데 막대그래프는 스택그래프를 그려주는 함수가 따로 없다. 그렇다는 소리는 알아서 그려야한다. 그래도 여기까지 공부했으니 한번 그려보자.
스택그래프의 원리만 알면 사실 쉽게 그릴 수 있다. y값이 끝나는 지점에 차곡차곡 막대를 쌓는다고 생각하면 되는데, 즉 하나의 그래프가 끝나는 y좌표에 연달아 그리기만 하면 된다는 소리이다. 하여 이를 위해 리스트 내포를 통해 y좌표를 배열로 만들어 그려보도록하자. 일단 간단한 예제를 통해 연습해 보겠다.
차이가 보이는가? 바로 열을 기준으로 다음행으로 넘어갈때마다 이전행의 데이터 값이 누적되어 더해지는 것을 알 수 있다. 이를 활용하여 이제 스택그래프를 그려보도록 하자.
ns_book12 = ns_book10.loc[top10_pubs].cumsum()
fig, ax = plt.subplots( figsize = (8,6))
for i in reversed(range(len(ns_book12))):
bar = ns_book12.iloc[i]
label = ns_book12.index[i]
ax.bar(year_cols, bar, label=label)
# 주의사항, 막대그래프를 위에 겹쳐서 그리는 방식이므로 가장 막대가 높은 그래프부터 그린다.
ax.set_title = ('연도별 대출건수')
ax.legend(loc='upper left')
ax.set_xlim(1985, 2025)
fig.show()
이제 선, 막대는 마스터했다. 이제 원그래프만이 남았다. 원그래프는 다른말로 파이차트라고도 하는데, 전체 데이터 대비 각데이터의 비율을 가지고 부채꼴로 표현하는 그래프이다. 원그래프는 그래서 pie() 함수를 통해 그릴 수 있다. 해당 함수를 사용하면 자동적으로 비율은 계산하여준다. 여기서 원그래프를 그릴때 주의할 점은 기본적으로 원그래프는 데이터의 비율차이가 명확하지 않은경우 이를 비교하기가 매우 애매한 점이 있다. 하여, 원그래프는 각 비율을 명시적으로 표현하여 잘못된 정보를 제 3자에게 주지 않도록 조심해야한다.
data = top30_pubs[:10]
labels = top30_pubs.index[:10]
fig, ax = plt.subplots(figsize= (8,6))
ax.pie(data, labels=labels, startangle=90, \
autopct='%.1f%%', explode=[0.1]+[0]*9)
# 시작지점을 90도(12시)로 하여 반시계방향으로 그래프를 그린다. 이때 비율은 소수점 1째짜리까지 출력
# 그리고 explode 매개변수를 사용하여 문학동네의 영역을 떼어내어 그린다.
ax.set_title('출판사 도서비율')
fig.show()
5주차에는 지난시간에 전처리된 데이터의 분포를 한번에 알아볼 수 있도록 시각적으로 표현하는
기본적인 시각화에 대해서 알아보았는데, 이를 조금 더 심화하게 알아보도록 하겠다.
1. 맷플롯립 기본요소 알아보기
(1) Figure 객체
우리는 그림을 그리기 위해서는 그림을 그릴 '종이'가 항상 필요하다. 맷플롯립에서는 그래프들을 출력하기 위한 캔버스와 같은 종이의 역할이 바로 Figure 객체라고 생각하면 된다. 하여 맷플롯립에서는 도화지를 Figure, 그래프를 Axes, 축을 Axis라고 구별하여 말한다. 이 객체는 그래프를 그릴때 어떤 사이즈로 그릴것인지를 주로 조정하는곳에 사용한다. 이번에 그래프를 그릴 다음 코드를 이용하여 파일을 다운로드 하여 그려보자.
저번시간에 그렸던 산점도 그래프를 우선적으로 그려보자, 저번 시간 복습 겸 떠올리자면 matplotlib.pyplot 모듈을 호출하여 scatter() 함수로 그리면된다. 이때, 우리는 도서권수를 x축으로 하고, 대출건수를 y축으로 하여 그래프를 그려보자. 그리고, alpha 값을 0.1로 하여 데이터의 빈도를 투명도로 표시하여 그려보자.
import matplotlib.pyplot as plt
plt.scatter(ns_book7['도서권수'], ns_book7['대출건수'], alpha=0.1)
# scatter 함수의 매개변수는 x축, y축 순서로 넘겨주면 된다.
plt.show()
그래프를 잘 그리긴 했으나, 그래프의 크기를 자유자재로 조절하였으면 좋겠다. 보는 사람이 조금더 편하게 볼수있도록 세심한 배려를 해주기 위해서는 이러한 기능이 필요한데, 이때 사용하는 것이 figure 객체라고 보면 된다. 이를 활용하여 다시 그려보자.
plt.figure(figsize=(9,6))
# 캔버스의 크기를 지정하는 경우에 가로와 세로의 길이를 튜플로 하여 figsize 매개변수에 전달하면된다.
# 이때, 그래프의 크기를 지정하는 것이 아닌 캔버스의 크기를 지정하는 것이다.
# 캔버스의 크기가 정해지면, 기본적으로 matplotlib의 함수는 타이트레이아웃으로, 캔버스를 최대한 채워서 그래프를 출력한다.
plt.scatter(ns_book7['도서권수'], ns_book7['대출건수'], alpha=0.1)
plt.show()
# figure 객체는 show() 메서드를 호출하여 그래프를 그리면 자동으로 초기화 된다.
이전보다 크게 출력되어 조금 더 가시성이 좋아진 것이 보인다. 하지만, 뭔가 이상한데 실제로 출력된 그래프의 크기가 내가 지정한 크기와는 사뭇 조금 다르다. 왜 그런 것일까?
이유는 조금 단순하긴 하지만, 우리가 사용하는 모니터가 다 다르기 때문이다. 게임을 하거나 화면을 조정할때 '해상도'라는 말을 한번쯤 들었을 것이다. 아무리 그래프의 크기를 지정하더라도, 모니터의 해상도와 DPI라는 값으로 인해 출력되는 크기가 다르게 나올수 밖에 없다는 것이다. 즉, 원하는 크기를 그대로 적용하고 싶다면 이를 고려하여야 한다는 것이다. 이는 figure 객체에서 설정이 가능하다.
- DPI(dot per inch)
: 화면을 출력할때, 1인치의 크기의 화면을 몇개의 픽셀로 표현하는지를 말하는 단위이다. 엄연히 이야기하면 '점'이라고 이야기 해야한다. 왜냐하면 기준을 픽셀로 하면 PPI라고 따로 이야기하지만, 종종 같은 의미로 사용하기도 하니 여기에선 넘어가자.
이제 이에 대한 이유를 알아내었으니, 이를 적용하여 그래프를 그려보자.
plt.figure(figsize=(900/72, 600/72))
# 맷플롯립의 DPI 기본값은 72 이므로, 픽셀을 기준으로 그림을 그리기 위해서는 픽셀값을 인치로 환산해야한다.
# 픽셀에서 인치로 환산하는 식은 (픽셀값)/(DPI값) 을 적용하면 된다.
plt.scatter(ns_book7['도서권수'], ns_book7['대출건수'], alpha=0.1)
plt.show()
이전에 맷플롯립은 타이트레이아웃을 기본으로 한다고 했었다. 기억나는가? 바로 이것이 두번째 이유이다. 타이트 레이아웃은 말 그대로 주어진 캔버스(figure) 사이즈를 공백없이 최대한 활용하여 출력한다. 그래서 일부 크기가 강제로 조절되는 것이다. 하여 이를 변경하기 위해서는 bbox_inches 매개변수를 None으로 정의해주면 해결이 된다. 하지만, 이렇게 크기에 중요시 하다보면, 우리는 코딩과정에서 코드와 함께보아야하는데 조금 불편할 수 있으니 되도록이면 크기 출력은 가시성을 떨어뜨리지 않는 선에서만 조절하면 된다.
%config InlineBackend.print_figure_kwargs ={'bbox_inches' : None}
plt.figure(figsize=(900/72, 600/72))
# 그래프의 크기를 figsize를 변경하지 않아도, 조정할 수 있는데 바로 dpi를 변경하면된다.
# plt.figure(dpi=144)
# 다만, 차이점은 그래프의 크기가 전부 커지는 형식이므로 마커등의 크기도 함께 커진다.
plt.scatter(ns_book7['도서권수'], ns_book7['대출건수'], alpha=0.1)
plt.show()
Figure 객체는 캔버스라고 하면, rcParams 객체는 그림을 그리는 방법을 담은 설명서라고 보면된다. 그런데, 설명서라고 하지만 직접 그리는 방식을 수정하고 변경할 수 있는 특징이 있다. 마커의 모양을 변경하거나, 해상도 변경 등 다양한 기능들이 있다.
#dpi 기본값 변경
plt.rcParams['figure.dpi'] = 100
#마커 모양변경하기
plt.rcParams['scatter.marker'] # 현재 적용된 마커의 정보를 반환하여 준다.
plt.rcParams['scatter.marker'] = '*' # 원하는 마커의 모양을 문자로 지정하면 변경하여 적용한다.
plt.scatter(ns_book7['도서권수'], ns_book7['대출건수'], alpha=0.1)
plt.show()
이외에도 '+' 등과같은 다양한 마커가 있는데, 종류가 궁금하면 맷플롯립 마커 옵션을 직접 찾아보면 나온다.
(3) 서브플롯
처음에 우리는 matplotlib은 그래프와 캔버스 그리고 축을 구분하여 부른다고 했다. 왜냐하면 우리가 그림을 그릴때도 한 종이안에 여러가지의 그림을 그릴 수 있지 않은가? 맷플롯립 또한 여러 그래프를 하나의 figure안에 그릴 수 있다. 이를 우리는 서브플롯이라고 이야기한다. 여러개의 그래프를 그리기 위해서는 subplot() 메서드로 그리고 싶은 그래프의 수를 지정해주면 된다.
서브플롯 또한 크기 조절이 가능한 figsize 매개변수가 존재한다. 그리고 서브플롯에는 그래프를 구분하기 쉽도록 제목을 달아주는 set_title() 메서드를 지원하므로 원하는 제목을 매개변수로 넘겨주면 된다.
fig, axs = plt.subplots(2, figsize=(6,8))
# 두개의 그래프(서브플롯)을 세로로 하나씩 그린다.
# 만약 가로로 그리고 싶다면, 행렬을 만들듯이 서브플롯을 정의하면된다.
# plt.subplots(1, 2, figsize=(6,8)) << 1행 2열(가로로 2개 출력)
# figsize 매개변수로 서브플롯의 크기를 지정할 수 있다.
axs[0].scatter(ns_book7['도서권수'], ns_book7['대출건수'], alpha=0.1)
axs[0].set_title('scatter plot') # 그래프의 제목을 만들어 준다.
# 첫번째 그래프 그리기
# 기본적으로 subplot은 Figure에 두개의 그래프 정보를 객체로 만들어서 그린다.
axs[1].hist(ns_book7['대출건수'], bins=100)
axs[1].set_yscale('log')
axs[1].set_title('histogram')
# 히스토그램을 두번째 서브플롯에 그린다.
# 만약 축의 이름을 지정해주고 싶다면 set_xlable, set_ylabel 를 활용하면된다
fig.show()
산점도랑 히스토그램을 그리는 것으로 어느정도 빈도에 대한 데이터의 특성은 파악했다. 이제는 데이터가 시간흐름에 따라 어떤 양상을 띄는지에 대한 '추이'가 조금 궁금하다. 추이를 파악하는데에는 선그래프나 막대그래프가 적절한데, 이를 한번 지금부터 그려보도록 하자.
(1) 전처리
일단 선그래프를 그리기 전에, 데이터 전처리는 필수이다. 하여 우선 데이터를 그리고자하는 그래프에 맞게 가공하자.
count_by_year = ns_book7['발행년도'].value_counts()
# 해당 열의 데이터 값별로 빈도수를 세서 반환한다.
count_by_year = count_by_year.sort_index()
# 연도별로 보는 것이 가독성에 좋으니, 이를 위해 인덱스값으로 정렬한다.
# 기본적으로 문자는 가나다 순, 숫자는 오름차순으로 정리한다.
count_by_year
막대그래프를 그리기 위해서는 주제별로 도서를 정리하여야하는데, 도서에는 주제별로 구분하는 십진분류코드가 있다. 이는 우리 데이터에 '주제분류번호' 열에 있으니 이를 활용하여 분류를 해보자. 근데 가만보니, 해당 열에는 NaN라는 결측값도 있어 분류하는 과정에서 제외작업도 거쳐야한다. 이는 지난시간에 배웠던 넘파이에 np.nan 메서드를 활용하면 된다.
import numpy as np
def kdc_1st_char(no): #함수를 정의하여 주제분류번호의 열값이 NaN인 경우 -1, 아니면 첫글자를 반환한다.
if no is np.nan:
return '-1'
else:
return no[0]
count_by_subject = ns_book7['주제분류번호'].apply(kdc_1st_char).value_counts()
count_by_subject
선그래프를 그리려면, matplotlib에 plot() 메서드를 사용하면 된다. 5-1에 배웠던 내용을 적용하여, 해상도와 축이름, 그래프 제목 등을 명시하여 가독성을 조금 높여보자.
plt.plot(count_by_year.index, count_by_year.values)
# count_by_year 객체로 전달하면 굳이 x축과 y축을 따로 지정하여 보내지 않아도 알아서 구분한다.
# 만약 선종류 및 색상, 마커등을 변경하고 싶으면 marker, linestyle, color 매개변수를 지정하면된다.
# 마커,선모양,색상을 한번에 정의하고 싶다면 '.:r'처럼 한번에 문자열로 넘겨주어도 된다.
# ex) plt.plot(count_by_year, '*-g') //마커를 별로하고, 선을 실선, 색상을 초록으로 한다.
plt.title('Books by year')
plt.xlabel('year')
plt.ylabel('number of books')
plt.show()
출력결과를 보니 자동으로 지정된 x축의 개수가 너무 간격이 커 분석하기에는 힘든것 같다. 하여 조금 더 구간을 세분화 해보자. 이는 xticks() 함수를 활용하면 된다. 만약 y축의 간격을 조정하고 싶다면 yticks() 함수를 사용한다.
plt.plot(count_by_year, '*-g')
plt.title('Books by year')
plt.xlabel('year')
plt.ylabel('number of books')
plt.xticks(range(1947, 2030, 10))
# x축의 간격을 1947년부터 10년 간격으로 2030까지 표시한다.
for idx, val in count_by_year[::5].items():
plt.annotate(val, (idx, val))
# 만약 가시성을 위해 표시된 텍스트의 위치를 조정하려면 xytext 매개변수에 좌표를 튜플로 지정해주면 된다.
# 조정하였으나 별 차이가 없다면 textcoords 매개변수를 offsets points로 지정하여 포인트단위로 위치를 지정하게 바꾼다.
# 마커를 찍을 위치를 반복문으로 만든다.
# 시작부터 5개 간격으로 데이터를 하나씩 찍는다.
plt.show()
선그래프는 plot()을 활용하여 그렸다면, 막대그래프는 bar() 함수를 사용하면 쉽게 그릴수 있다. 그전에 앞서 선그래프를 그릴때 처럼 막대위에 도서권수를 찍되, 잘 보이도록 나타내자.
plt.bar(count_by_subject.index, count_by_subject.values)
# 만약 막대크기(너비)를 지정하고 싶다면 width 매개변수를 지정하면된다.
# 또한 색상을 수정하길 원한다면 color 매개변수에 원하는 색상을 지정하면된다.
plt.title('Books by subject')
plt.xlabel('subject')
plt.ylabel('number of books')
for idx, val in count_by_subject.items():
plt.annotate(val, (idx, val), xytext=(0, 2), textcoords='offset points')
# 텍스트의 위치를 조정하고 싶다면 ha 매개변수에 위치를 지정하면 된다.
# 텍스트의 크기와 색상은 fontsize, color 매개변수를 지정하면된다.
plt.show()
만약 세로로 그리고 싶은 것이 아니라 가로로 그리고 싶다면 bar() 메서드 대신, barh()메서드를 활용한다. 이때 주의할 점은 x축과 y축의 위치가 바뀌므로 이를 주의하여야 한다. 뿐만 아니라, 막대의 너비가 높이로 바뀌므로 width가 아니라 height 매개변수가 된다. 그리고 annotate() 함수 또한 x축과 y축의 좌표가 반전된다. 하여 이를 고려하여 막대그래프를 가로로 그려보자.
plt.barh(count_by_subject.index, count_by_subject.values, height=0.7, color='blue')
# 매개변수 순서가 (x, y) 에서 (y, x)로 바뀌니 주의하자.
plt.title('Books by subject')
plt.xlabel('number of books')
plt.ylabel('subject')
for idx, val in count_by_subject.items():
plt.annotate(val, (val, idx), xytext=(2, 0), textcoords='offset points', \
fontsize = 8, va='center', color='green')
plt.show()
그러면서 점점 분량도 많아지고 난이도도 어려워지는 상황에서 나름 체계가 공부에 도움이 되는 것 같다.
(족장님 보고있나, 족장님이 공지로 어렵게 던진다는 교재 진도 저는 해냈습니다.)
3주차에는 효율적인 데이터 분석을 위해, 데이터를 정제하는 방법에 대해 집중적으로 다루었다. 이번에는 데이터가 가진 특징을 파악해보고자 한다.
1. 통계로 요약하기
'통계' 한번쯤은 들어보았을것이다. 다들 통계라 하면 수학이라고 생각해서, 통계라는 말을 들으면 벌써부터 착잡할 것이다. 하지만 그리 어렵지는 않다.
(물론 깊게 공부한다면 어려워 죽을지도 모른다, 필자는 전공이 수학교육이라 통계학 배울때 힘겨웠던 기억이 있다.)
어떠한 정보를 요약하여 파악하기 위해서는 통계가 필수적이다. 왜냐하면 우리는 일일히 분석을 하면서 데이터를 하나하나 읽어볼 시간도 없을 뿐더러, 그것은 매우 체력적으로 힘든 일이기 때문이다. 그래서 우리에게는 3줄요약처럼 간단하게파악할 수 있는 정보가 필요한데, 그 정보가 바로 통계라는 것이다.
통계를 딥하게 들어가면 분포니 뭐니 하는 이야기들이 있지만, 여기서는 우리가 자주 들어보았던 평균, 분산, 표준편차 들의 이야기를 다루어 볼 것이다. 교재에서는 각 통계량을 구해보면서 이야기하지만, 코딩을 하기전에 통계량의 의미부터 살펴본 뒤 가면 더 좋은 이야기가 될 것이므로, 통계량들의 특징부터 알아보자.
( tmi. 필자의 전공은 '수학교육'이므로 이에 대한 수학적 이론을 쉽고 쌈뽕하게 해보겠다.)
(1) 기술통계량의 정의 (+ 추가숙제)
어떤 정보들을 한번에 파악하려면, 요약본이 필요하다. 이 요약본이 바로 '기술통계'라고 생각하면 된다. 데이터가 가진 여러가지 특성을 정량적인 수치로 보여준다. 이때, 각 수학 정의나 계산식에 따라 구해진 값을 '통계량' 이라고 하는데 요약본에 주로 활용되는 통계량을 '기술통계량' 이라 생각하면 쉽다. 주로 이러한 기술통계량은 '평균', '최빈값', ' 중앙값' 등의 대푯값들과 산포도인 분산, 표준편차 외에도 최댓값, 최솟값, 분위수 등을많이 사용한다.
- 대푯값
: 말 그대로 어떤 데이터나 집단 등의 특성을 잘보여주는, 즉 데이터를 대표하는 값이라고 생각하면 된다. 대푯값으로는 평균, 최빈값, 중앙값 등이 있다.
1) (산술)평균 "Mean" : 우리가 익숙하게 아는 평균은 사실 모든 데이터 값을 더한 뒤, 데이터의 개수로 나눈 값을 말한다. 이 값은 각 데이터의 값들이 평균값과 차이가 크지 않을 때, 즉, 편차가 작아질수록 데이터의 특징을 잘 나타내는 특징이 있다.
$$ m=\frac{1}{n}\sum_{k=1}^{n}x_k $$ 2) 최빈값 "Mode" : 데이터의 값들 중에서 가장 많은 빈도의 값. 쉽게 말하면, 데이터들 중에서 가장 많이 등장하는 숫자를 말한다. 평균의 경우 계산값이므로 데이터가 수치여야 하지만, 최빈값은 데이터의 빈도를 따지므로 숫자, 문자 모두 가능하다.
3) 중앙값 "Median" : 데이터의 값을 크기순으로 나열 하였을때, 정중앙에 오는 값을 말한다. 중앙값을 구하기 위해서는 데이터는 대소비교가 가능한 경우에만 가능하다.
- 분위수
분위수란 크기순으로 나열한 후 일정한 간격으로 데이터를 쪼개어 구간을 만들 때, 경계에 있는 값을 말한다. 주로 사분위수를 많이 사용하는데, 사분위수는 데이터를 크기순으로 나열한 뒤 4등분하여 각 25%, 50%, 75% 경계에 있는 값을 말한다.
- 최댓값, 최솟값
최댓값(maximum), 최솟값(minimum)은 말그대로 데이터들 중에서 가장 큰 값과 작은 값을 말한다.
- 산포도
: 데이터(변량)이 어떤 기준으로 부터 얼마나 흩어져지에 대한 척도이다. 대푯값들을 구하더라도, 실제 데이터의 값들이 대푯값과 차이가 많이 난다면 의미가 없다. 그렇기 때문에 대푯값들이 얼마나 실제 데이터값과 얼마나 차이가 나는지에 대한 정보를 같이 제시해주어야하는데, 이 값이 바로 산포도이다. 대표적으로, 평균과 실제 데이터간의 차이를 설명하는 산포도로 분산과 표준편차가 있다.
1) 분산 "Variance"
: 단어의 뜻대로, 흩어진 정도를 말한다. 이때, 분산은 "평균으로부터 얼마나 떨어져있는가?"를 이야기한다. 즉, 대푯값 중 평균이 얼마나 실제 데이터를 잘 대표하는 지를 알려주는 값이다. 분산은 수학에서 "편차의 제곱의 평균"으로 정의하며, 이때 편차는 실제 데이터값에서 평균를 뺀 값을 말한다.
이때, 분산의 값이 커지면 커질수록 평균으로부터 데이터들이 많이 떨어져있다. 즉, 평균과 실제데이터들 간의 차이가 크다.
2) 표준편차 "Standard Deviation"
: 분산에 제곱근을 씌워 구한 값이다. 표준편차는 분산과 제곱근의 차이밖에 없어 차이가 없어 보일 수 있지만, 분산은 단지 평균으로부터 얼마나 각 실제 데이터들이 떨어져있는 가를 설명한다면, 표준편차는 전체적인 데이터가 어떻게 흩어져있는가에 대한 분포를 알려준다고 생각하면 쉽다. 즉, 분산은 데이터 하나하나에 집중한다면, 표준편차는 전체적으로 데이터가 어떻게 흩어진지를 알려준다.
지금까지 기술통계량에 대한 이야기를 공부했으니, 본격적으로 이를 직접 파이썬을 활용하여 구해보고자 한다. 각 수학적 정의에 따라 코딩하여 구해도 되지만, 요즘은 이를 한번에 구해주는 메소드들이 잘 지원이 되고 있다. 대표적인 예시로 pandas 라이브러리에 describe() 메서드가 있다.
import gdown
import pandas as pd
gdown.download('https://bit.ly/3736JW1', 'ns_book6.csv', quiet=False)
ns_book6 = pd.read_csv('ns_book6.csv', low_memory=False)
ns_book6.head()
ns_book6.describe() # 해당 데이터프레임에서 수치형데이터를 가진 열들의 기술통계량을 출력한다.
describe() 메서드에는 기본적으로 수치형데이터를 가진 열별로 평균, 분산, 표준편차, 사분위수, 최댓값, 최솟값을 구하여 출력한다.
기술통계량을 보니, 도서권수의 최솟값이 0이라고 보인다. 이말인 즉슨, 도서가 구비되어있지 않는데 대출은 진행된다는 소리인데 무엇인가 이상하다. 이런 경우에는 해당 데이터는 제외하거나 처리를 해야하는데 어떻게하는게 좋을까?
기본적으로, 이에 해당하는 상황이라면 분석에서 무시해도 괜찮을정도의 개수인지를 파악해야하는게 좋다. 만약 도서권수가 0인 데이터가 전체에 1%도 채 안된다면, 분석을 할때 삭제해도 큰 무리가 없을 것이다. 그러나 반대로 상당수의 책들이 0권이라면, 이를 섣불리 삭제하여 분석을 진행하면 결과에 문제가 생길 것이다. 하여 이를 확인하고 문제가 없다면, 제외시키자.
만약, 각각 기술통계량을 따로 구하고싶다면 다음 메서드들을 활용하여 개별로 구할수도 있다.
통계량
메서드
평균
.mean()
중앙값
.median()
최솟값
.min()
최댓값
.max()
사분위수
.quantile()
분산
.var()
표준편차
.std()
최빈값
.mode()
2. 분포 요약하기
각 데이터를 수치로 한번에 파악하는 기술통계량에 대하여 살펴보았다. 그런데, 수치로만 보니 여전히 이해하기에는 조금 시간이 걸린다. 시각적인 그림으로 볼 수 있다면, 더욱 편하게 데이터를 파악할 수 있을텐데 좋은 방법이 없을까? 이는 파이썬에서 matplotlib 라이브러리가 대표적으로 사용된다.
(1) 산점도
산점도는 각 데이터를 좌표평면상에 점으로 나타내는 방법이다. 학창시절에 수학시간에 배웠던 x축과 y축으로 이루어진 좌표평면이 기억나는가? 바로 그 평면에 x축과 y축의 이름을 지정해주고 그것을 좌표처럼 점으로 찍어 보여주는 것이라고 보면 된다.
import matplotlib.pyplot as plt
# 만약 해당 라이브러리가 설치되어있지 않다면 pip 명령어로 라이브러리를 설치하면 된다.
# pip install matplotlib
# 작성일 기준, scatter()메서드는 matplotlib.pyplot을 호출해야 사용가능하다.
# 뿐만 아니라, hist(), boxplot() 메소드 전부 사용하기 위한 모듈 이름이 바뀌었다.
plt.scatter(ns_book7['번호'], ns_book7['대출건수'])
# 산점도를 그려주는 scatter() 메서드에 x축의 정보와 y축의 정보를 넘겨주면 자동으로 그려준다.
# 같은 좌표에 찍히는 데이터들의 밀집도를 보고싶다면, alpha 매개변수에 값을 지정하면 밀집된 정도에 따라 진하게 표시한다.
plt.show()
# 산점도를 그림으로 그려 반환한다.
쉽게 설명하면 우리가 익히 아는 막대그래프라고 생각하면 된다. 조금 차이가 있다면, 히스토그램은 수치형데이터에서 일정한 구간을 나누어서 각 구간에 해당하는 데이터의 개수를 막대그래프로 표현해준다. 이 히스토그램을 표로 표현하는 도수분포표도 있다.
plt.hist(ns_book7['대출건수'], bins=100)
# 대출건수를 x축으로 하여 건수의 범위를 100개로 분할하여 히스토그램으로 그린다.
plt.yscale('log')
# 특정 구간의 데이터수가 너무 압도적으로 많아 다른 구간의 그래프가 보이지 않을때 사용한다.
# 즉, 값을 log를 취하여 숫자를 조금 작게 만든다.
plt.show()
상자수염이라고 불리는 box plot은 분위수에 따라 데이터를 상자모양으로 그려준다. 상자수염 그래프를 읽는 방법을 나중에 설명하기로 하고, 우선 이를 그려보자.
plt.boxplot(ns_book7[['대출건수', '도서권수']], whis=(0, 100))
# 데이터를 상자수염으로 그리기위한 기준열을 배열로 전달하여 그린다.
# 수염의 길이를 적절히 조절하려면 whis 매개변수에 길이를 지정하면 된다.
plt.yscale('log')
plt.show()
아님 작심삼일의 마법이 나에게도 적용되는 건지는 잘 모르겠지만 뭔가 점점 게을러지는 기분이 들기 시작한다.
그래도 혼공단만큼은 6주 내내 열심히 공부해서 잘 마무리 했으면 좋겠다.
지난 시간까지 우리는 데이터를 수집하는 과정에 대해 이야기했다. 수집하고자하는 데이터가 API를 지원한다면 이를 통해 JSON, XML 등의 파일형식으로 내려받아 활용하면 되고, 만약 이를 지원하지 않는다면 웹사이트의 HTML 코드에서 원하는 정보를 찾아 긁어오는 웹스크래핑을 배웠다.
그런데, 불러온 데이터는 바로 데이터 분석에 활용하기에는 너무 양이 방대하고 불필요한 정보도 많다. 하여 이번에는 데이터를 필요한 정보만을 뽑아내는 정제에 대하여 학습을 해보자. 우리가 사용한 데이터는 1차시에 활용했던 '도서대출 정보'이다. 만약 기억이 나지않는다면 돌아가서 확인하자.
1. 불필요한 데이터 삭제하기
우리는 도서관 이용자들이 과연 어떤 책을 많이 읽는지 한번 알아보고 싶은 생각이 들었다고 하자. 그래서 이를 알아보기 위해 API를 통해 해당 데이터를 요청하여 데이터 프레임으로 만든 것까지는 가정하겠다. 필자는 1주차에 내려받은 파일을 'data'라는 디렉토리에 저장해두었으므로, 바로 경로를 입력하여 불러오도록 하겠다.
import pandas as pd
filePath = "data/남산도서관 장서 대출목록 (2021년 4월).csv"
ns_df = pd.read_csv(filePath, encoding='EUC-KR', low_memory=False)
ns_df.head()
# 만약, head()는 기본적으로 첫행부터 5번째 행까지만을 출력한다. 따라서,
# 데이터프레임의 전문을 보고싶다면 show() 메소드를 활용하면 된다.
"쓰레기가 들어가면, 쓰레기가 나온다." 데이터 분석을 조금이라도 공부했던 사람이라면 다들 아는 말들 일 것이다. 이 것이 무슨 의미인가를 예를 들어 말하면, 방금전 데이터 분석을 위해 데이터프레임을 만들었으나 불필요한 정보들이 너무나도 많다. 이 상태로 만약 분석을 한다고 하자. 그렇다면 불필요한 정보들의 영향으로 되게 혼란스럽고 무의미한 결과가 나오지 않을까? 바로 그 상황을 이야기하는 것이다. 다시 말하면, 유의미하고 영향력있는 분석 결과가 나오기 위해선 우리가 분석하기 위한 데이터가 의미가 있는지를 먼저 신경써야한다는 말이다.
한마디로 정리하자면, 데이터 분석을 하는 것도 중요하지만 데이터를 잘 정제하는 것 또한 중요하다는 말이다.
(1) 데이터 슬라이싱
우리는 첫번째로 지난시간에 배운 loc 메서드를 사용하여 필요한 열과 행만을 골라서 볼 수 있다. 기본적으로 loc 메서드는 매개변수로 행과 열에 대한 정보를 받으면 해당되는 정보만을 다시 반환하여 준다. 또는 원하는 행만을 보고싶다면 [] 연산자를 활용하여도 좋다. 이때, loc 메서드는 열에 대한 정보는 각 열의 Index 값을 넘겨주어야하므로, 우리가 불러온 데이터의 열에 Index 이름을 확인하고 싶다면 'colums' 메서드를 활용하여 미리 알아보는 것이 좋다.
print(ns_df.colums)
# ns_df의 열에 대한 인덱스 값을 반환하여 출력한다.
# 만약, 특정열의 인데스만 보고다면 예를들어, ns_df.colums[0] 처럼 0번째 인덱스만을 반환받을 수 있다.
ns_book = ns_df.loc[:, '번호':'등록일자']
#loc 함수는 기본적으로 숫자로 던져주면 행을 나타내고, 열 인덱스를 넘겨주면 열을 반환한다.
ns_book.head()
loc 메서드를 사용하는 법을 배웠는데, 사용하다보니 필요한 정보만 추출하기 위해서는 모든 정보를 알아야할 귀찮음이 있다. 그냥 내가 원하는 정보외에는 일괄적으로 지울 수는 없을까? 이때는 불리언 인덱싱을 사용할 수 있다.
데이터 타입중에 "Boolean"을 알것이다. True, False 두가지의 값인데, 이를 활용하여 각 행과 열에 대해서, True 면 반환하고 False라면 반환하지 않는 것을 '불리언 인덱싱'이라고 한다. 즉, 모든 정보를 알지 않아도 필요한 정보가 아니라면 다 False 값을 넘겨주면 알아서 제외시킨다는 것이다. 이는 참거짓을 판단하기 위해 !=, >, <, and 와 같은 비교 연산자를 함께 사용한다. 즉, 열 데이터처럼 고유이름이 있는 경우에 사용하면 좋다.
selected_colums = ns_df.colums != 'Unnamed: 13'
#각 열 인덱스의 이름이 'Unnamed: 13'이 아니라면 True, 맞다면 False를 반환하여 배열로 만들어 반환한다.
ns_book = ns_df.loc[:, selected_colums]
#ns_df의 모든 행과 불리언 배열에서 True에 해당하는 열만 반환한다.
ns_book.head()
#ns_book 출력
필요없는 열과 행을 제외는 해봤지만, 아예 그냥 삭제를 하고싶은 생각이 있다. 그리고 데이터들 중에서 'NaN' 이라는 값이 무엇인지는 몰라도 심히 보기 좋지않다. 이는 어떻게 제거를 해야할까?
그전에 'NaN'이라는 값은 'Not A Number' 의 약자로 말 그대로 숫자가 아니다는 뜻이다. 판다스에서는 '비어있는 값' 즉, 결측값의 의미를 가지고 있다고 생각하면 된다. 즉 분석에서 가장 문제가 되는 녀석인데, 이를 잘 처리하는 것이 데이터 분석에서의 첫번째 미션이다.
결측값을 가장 쉽게 해결하는 방법은, 무식하지만 그냥 결측값이 있는 열이나 행을 통채로 삭제하면 된다. drop() 메서드에서 행과 열의 인덱스를 넘겨주면 해당되는 행과 열을 제거해준다.
하지만, 좀 더 쉽게 지우고 싶다면 dropna 메서드가 더 효과적이다. 이 함수는 각 행과 열에 NaN 값이 있는지 알아서 검사하고 있다면, 지정하는 방식에 따라 행과 열을 지워버린다. 이때 행과 열은 axis 매개변수를 통해 지정해주면 된다. 만약, 행을 지우고 싶다면 '0' 열을 지우고 싶다면 '1'로 지정해주면 된다.
ns_book = ns_df.drop('Unname: 13', axis= 1)
# Unname: 13에 해당하는 열 전체를 삭제한다. 만약 여러 열을 삭제하고 싶다면 인덱스들을 리스트로 만들어 넘기면 된다.
# inplace=True 매개변수를 사용하면, 새롭게 데이터프레임을 만들지 않고 해당 데이터프레임에서 바로 삭제한다.
ns_book = ns_df.dropna(axis=1)
# 각 열에서 NaN(결측값)이 있는 열을 삭제한다. 이때, 각 열에 결측값이 하나라도 있으면 삭제가 된다.
# 만약, 각 열에 모든값이 결측값일 때만 삭제를 하고싶다면 how 매개변수를 all 지정해서 넘겨주면 된다.
2. 중복된 데이터 제거하기
필요없는 데이터는 얼추 정리하는 방법을 익혔다. 그래서 이제는 어느정도 분석을 시작해도 괜찮겠지? 라는 생각도 잠시, 이번엔 중복된 데이터가 눈에 보였다. 인기도서의 경우 도서관에선 여러권을 구비한다는 사실이 머릿속에서 스쳐갔다. 이럴땐 어떻게 중복된 데이터를 처리해야할까?
(1) duplicated
판다스에서 지원하는 중복된 정보를 찾아내는 메서드이다. 기본적으로 모든 열의 정보가 일치하면 해당 열을 True로 반환해준다. 대부분 모든 열에 대해서 검사를 할 때도 있지만, 일부 열의 정보만 중복된 상황을 검사해야할 때도 있다. 이때는 메서드의 subset 이라는 매개변수에 검사하고자하는 기준 열의 인덱스 값을 리스트로 넘겨주면, 부분적으로 검사하여 중복된 행을 True로 반환한다.
ns_book.duplicated()
#해당 데이터 프레임 각 행의 데이터 값을 비교하여 모든 열의 정보가 일치하는 행이 있다면
#True, 그렇지 않으면 False로 반환한다.
#만약 일부분의 열로만 비교하고 싶다면 subset 매개변수에 기준 열의 인덱스를 넘겨주면 된다.
#중복된 행을 불리언 배열로 반환받고 싶은경우, keep의 매개변수를 False로 지정하면 된다.
sum(ns_book.duplicated())
#중복된 열의 개수가 총 몇개인지 확인한다.
#Boolean 타입은 True는 정수 1의 값을 가지고 False는 0을 가진다.
(2) groupby
어떤 도서가 여러가지가 있는지 확인을 했다. 그래서 같은 도서는 대출건수를 한번에 합쳐서 확인하고 싶다. 이는 groupby() 메서드를 사용하면 된다. 해당 메서드는 합칠 행의 기준이 되는 열 정보를 리스트나 문자열로 받으면, 해당 정보가 같은 열들 끼리 묶어서 반환한다. 이때, 매개변수로 넘겨받은 열 인덱스 중에 하나라도 'NaN' 가 있으면 해당 행은 삭제 처리하는 것이 기본값이므로, 이를 삭제하지 않기를 원한다면 dropna 매개변수를 False로 지정해주면 된다. 그리고 마지막으로 대출건수끼리 합칠 예정이므로 sum() 함수를 사용하면 된다.
count_df = ns_book[['도서명', '저자', 'ISBM', '권', '대출건수']]
# 필요한 열만 []연산자를 사용하여 추출
group_df = count_df.groupby(by=['도서명', '저자', 'ISBM', '권'], dropna= False)
loan_count = group_df.sum() # groupby()로 분류된 항목끼리의 대출건수를 하나로 합친다.
# 보통 위의 두 과정을 합쳐서 loan_df = count_df.groupby(by=['도서명', '저자', 'ISBM', '권'], dropna= False).sum()
# 로 표현하기도 한다.
loan_df.head()
3. 잘못된 데이터 수정하기
이때까지, 결측값을 삭제하여 분석을 시도하였는데 결측값이 몇개 없는 열을 날리기에는 아까운 느낌이 많이 든다. 그리고 데이터 값이 오타로 표현된 부분도 보기 좋게 수정을 하고, 혹은 결측값이 다양한 형태로 있는데 이를 그냥 하나로 표현해주고 싶은 생각이 든다. 굳이 삭제할 필요가 없거나, 오히려 삭제하면 데이터 분석에 문제가 될 것 같은 경우에는 수정이 더 나은 선택지가 아닐까?
(1) 결측값 파악하기
일단 수정을 하려면, 결측값이 얼마나 있는지 파악하는 것이 가장 첫번째 임무이다. 해서 우선적으로 다음과 같이 살펴보자.
import pandas as pd
filePath = "data/남산도서관 장서 대출목록 (2021년 4월).csv"
ns_book= pd.read_csv(filePath, encoding='EUC-KR', low_memory=False)
ns_book.head()
ns_book.info()
# 해당 데이터 프레임의 정보(Colum, 누락된 값이 없는 행 수, 열데이터 타입 등)를 요약하여 알려준다.
# 데이터프레임의 메모리 사용량을 알고싶다면 memory_usage= 'deep'로 지정해주면 된다.
ns_book.isna().sum() # 결측값을 각 열마다 몇개가 존재하는지에 대한 정보를 반환한다.
(2) 결측값 수정하기
우선적으로 데이터의 결측값이 얼마나 있는지 파악을 완료하였으니 이제는 수정하는 방법을 알아보자. 첫번째로 결측값을 찾아 불리언 배열로 만든 뒤에, 해당 값은 전부 빈 문자열로 만들어보자.
set_inbn_na_rows = ns_book['세트 ISBN'].isna()
# 세트 ISBN 열에 각 행마다 결측값이 있는지 검사하고 이를 불리언 배열로 반환한다.
ns_book.loc[set_inbn_na_rows, '세트 ISBN'] = ''
# 결측값이 있는 행을 빈 문자열로 바꾼다.
ns_book['세트 ISBN'].isna().sum()
# 결측값이 해당 열에 몇개가 있는지를 반환한다. 이때 NaN의 개수를 센다.
fillna() 메서드를 활용하면 더 간단하게 수정도 가능하다.
ns_book.fillna('없음').isna().sum()
# 데이터프레임 전체를 검사하여 결측값이 있다면 '없음'이라는 문자열로 대체한다.
ns_book['부가기호'].fillna('없음').isna().sum()
# '부가기호'열만 검사하여 결측값을 '없음'이라는 문자열로 대체한다.
ns_book.fillna({'부가기호' : '없음'}).isna().sum()
# 만약, 기준 열만 수정한 뒤 데이터 프레임 전체의 정보를 반환 받고 싶다면,
# key값을 해당 열의 이름으로 하고 대체하고자 하는 값을 value 하는 딕셔너리 형태로 전달하면 된다.
두번째로는 repalce() 메서드를 사용해도 된다.
import numpy as np
# 판다스 자체에서는 NaN 값에 대한 기준이 명확하게 없으므로, 넘파이 패키지에 nan 메서드를 활용한다.
ns_book.replace(np.nan, '없음')
# 데이터 프레임에서 결측값을 모두 '없음'이라는 문자열로 대체한다.
ns_book.replace([np.nan, '2021'], ['없음', '21'])
# 결측값은 '없음'으로 '2021'이라는 문자열은 '21'라는 문자열로 대체한다.
# replace([기존 값], [수정 값])
# 딕셔너리로 ns_book.replace({ np.nan: '없음', '2021':'21'}) 해도 동일한 결과를 반환한다.
ns_book.replace({'부가기호':np.nan}, '없음')
# 바꾸고자하는 열의 특정 값을 딕셔너리로 만들고, 바꾸고자 하는 값을 주면 열마다 다른 값으로도 수정할 수 있다.
4. 기본숙제_p.182
2. 1번문제의 데이터 프레임에서 'col1' 열의 합을 계산하는 명령으로 올바르지 않은 것을 고르시오.
① df['col1'].sum()
② df[['col1']].sum()
③ df.loc[: , df.columns == 'col1'].sum()
④ df.loc[:, [False, False, True]].sum()
나머지 항은 col1 열의 값을 각 값을 합하여 반환하지만, ④은 col3의 값을 계산한다.
쉬는시간이 아이스크림 녹듯이 없어졌다. 그래도 나름대로 얻는 성취는 가득하니 즐겁게 하게 되는 느낌이랄까.
1주차에서는 데이터 분석에 대한 이야기를 하고, 간단하게 파이썬 라이브러리와 사용을 해보았다.
2주차는 본격적으로 분석에 필요한 데이터를 수집한다. 데이터 수집 방법으로 가장 널리 알려진 'API'라는 것을 통해 데이터를 수집해보자.
1. API
API(Apllication Programming Interface)는 간단히 말하면, 프로그램 간 소통 방법을 정의한 것이다. 기본적으로 사람과 다르게 컴퓨터는 정해진 규칙에 따라 정보를 주고 받는다. 예를 들면, 기상청의 API를 사용하면 기상청에서 제공하는 날씨 정보를 얻을 수 있다. 한마디로 사용자가 컴퓨터를 통해 정보를 처리하기 위해서는 원하는 정보 처리에 따른 규칙이 필요한데, 그것이 바로 API라고 생각하면 된다.
데이터 분석에 필요한 정보를 수집할 때도 API를 사용한다. 왜냐하면 기본적으로 데이터를 수집하기 위해서는 그것이 저장된 데이터베이스에 접근해야하는데, 데이터베이스에는 민감한 개인정보 등 관리가 엄격히 이루어지는 정보들이 있다. 그래서 대부분의 기업 등에서는 데이터베이스의 접근권한이 엄격하고, 함부로 접근할 수 없도록 한다. 또한, 물리적으로 네트워크가 연결되있지 않은 경우 데이터를 수집하기 어려울 수 있다. 그래서 이러한 문제를 해결하고자 API를 통해 데이터를 주고받는다.
2. 웹페이지 통신규약 : HTTP, HTML
API 방식은 여러가지가 있는데, 그중에서 웹 기반 API를 많이 사용한다. 데이터 수집도 웹 기반 API를 사용하는데, 데이터를 수집하는 방식을 알기 위해서는 우선 웹페이지의 통신 방식을 이해해야 한다. 그 중 HTTP를 알아보자.
인터넷에서 웹페이지를 전송하는 기본 통신 방법. 웹사이트는 사용자가 웹 브라우저로 웹서버에 데이터를 요청하면 사이트를 운영하는 기업에서는 서버를 통해 해당 웹데이터를 HTML의 형태로 전송한다. 이때, 브라우저가 서버에 데이터를 요청하는 방법이 HTTP이다.
- HTML(Hyper Text Markup Language)
웹서버가 보내는 웹데이터의 한 종류. 웹페이지를 위한 표준언어이다. 쉽게 말하면 우리가 흔하게 보는 네이버 등의 모든 사이트는 서버를 통하여 웹사이트를 띄우기 위한 데이터를 보내는데, 이때 전송하는 데이터의 형식이 HTML이라는 것이다.
HTML는 다른말로 마크업 언어라고 부르기도 하는데, 간략하게 문법 구조를 설명하면 '태그(Tag)'라는 것을 사용한다.
3. CSV, JSON, XML
웹페이지를 주로 띄울때 보내는 데이터는 HTML 형식이라는 것은 이해가 된다. 그렇다면, 우리가 분석할 때 필요한 정보가 담긴 데이터도 HTML의 형식으로 주고 받을까?
데이터 분석에 필요한 데이터를 주고받을때는 주로 CSV, JSON, XML 이라는 형태로 제공한다. 왜냐하면 HTML의 경우에는 데이터의 구조가 상대적으로 복잡하기 때문이다. 데이터 분석을 하기 위해서는 분석가는 데이터를 수집한 뒤에 필요한 정보만을 추출해야하는데, 추출 하는 과정에서 데이터의 구조가 복잡하다면 데이터 추출과정에서 오류가 발생할 수 있기도 하다. 또한, 구조자체가 복잡하면 분석가가 이를 이해하는 시간도 오래 걸리는 이유도 있다.
그렇기 때문에 주로 우리는 CSV, JSON, XML이라는 형태로 서버에서 데이터를 제공한다. 왜냐하면 구조자체가 상대적으로 단순하고, 데이터의 호환성이 좋다.
'컴마로 분리된 값들' 단어의 뜻 그대로 CSV 파일 형식은 컴마, 세미콜론, 공백문자 등으로 데이터를 구분해놓은 방법이다. 즉, 텍스트 파일이기 때문에 데이터를 읽고 쓰기 용이한 형태이다. 그러나, 행별로 각 항목(열)의 개수가 맞지않으면 사용할 수 없다는 단점이 존재한다.
- JSON(JavaScript Object Notation)
자바스크립트 언어를 위해 만들어진 데이터 형식. 그러나 현재는 범용성이 높은 데이터로 사용된다. 즉, 자바스크립트 외에도 파이썬등 다양한 환경에서 읽을 수 있다. JSON 파일의 데이터는 key 값과 value값의 한쌍으로 된 paired data 형식이다. 파이썬에서 딕셔너리와 구조가 매우 흡사하다. 그렇기 때문에 파이썬에서는 JSON을 딕셔너리로 변환하여 처리가 가능하다.
- XML(eXtensible Markup Language)
HTML 언어의 경우 웹페이지를 표현하는데는 강점을 가지나 구조적이지 못한 단점으로, 데이터를 주고받는 API 에는 효율이 떨어진다. 그렇기 때문에 이를 보완하고자 만들어진 마크업 언어이다. XML 역시 마크업 언어이므로, 태그를 사용한다.
XML데이터 형식은 루트와 엘리먼트로 구분이 되고, 각 루트의 데이터를 엘리멘트로 하여 시작태그와 종료태그 사이에 넣어 구분한다.
JSON과 XML이 어떤 형식을 가졌는지 예제를 통해 살펴보자.
예를 들어 도서명이 "혼자 공부하는 데이터 분석" 이고, 저자가 "박해선", 발행년도가 2022년도라고 하면 이에 대한 데이터는
URL은 2022년 1월 1일부터 3월 31일까지 서울, 경기 지역 20대 남성이 대출한 기술과학(KDC 6) 분야 도서 데이터를 요청. 그 후 한 번에 최대 10건을 반환하며, 1페이지 결과를 제공한다.
인증키 뒤에 붙어있는 문자열을 '쿼리 스트링' 이라고 하는데, 즉 어떤 데이터를 요청할지에 대한 정보를 이를 통해 서버에 전달한다.
이제 호출 URL의 구조를 이해했으니 직접 데이터를 호출해보자.
파이썬에서 API를 사용하기 위해 지원하는 라이브러리가 있는데, 대표적으로 requests 패키지가 있다. 이를 사용하여 호출해보자.
import requests
url = "http://data4library.kr/api/loanItemSrch?authKey=[발급키 입력]&age=20&startDt=2021-04-01&endDt=2021-04-30&format=json"
r = requests.get(url)
data = r.json()
data
우리는 API를 활용하여 데이터를 수집하는 방법을 배웠다. 하지만 만약, 수집하고자 하는데이터가 API가 없다면 어떻게 수집을 해야할까? 그런 경우에는 우리는 "웹 스크래핑" 이라는 방식을 통해 데이터를 수집한다. 말 그대로 웹사이트에서 표시한 정보를 긁어온다는 뜻이다.
대표적으로 뷰티풀수프 'BeautifulSoup" 라이브러리가 웹크롤링을 하는 것에 사용된다. 하지만 이를 사용하기 위해서는 어느정도 우리는 웹페이지에 대한 분석지식을 가지고 있어야한다. 즉, HTML에 대한 이해도가 필요하다는 것이다. 왜냐하면 이를 사용하기 위해서는 우리가 원하는 정보가 어디있는지 찾아야하며, 해당하는 정보가 어느 태그에 있는지 알아야한다.