데이터시각화 특강 (11주차) 11월22일
카이로의 The Functional Art
• 최서연 • 12 min read
- 카이로의 The Functional Art
- 데이터 프레임 합치기
- Folium
Cairo, A. Functional Art, The: An Introduction to Information Graphics and Visualization, New Riders, 2012. San Francisco, US.
- 독자가 해석할 여지가 크지 않지만, 명확하다
-
프리젠테이션방식의 시각화는 화자가 다듬은 이야기를 전달하기에 좋은 시각화이다. 즉 잘 정리된 메시지를 전달하기에 좋다.
카이로 曰
- 문학적 유기체라는 작품
- 어떤 소설책을 시각화.
- 수형도 + 칼라
- 수형도의 의미: 단원, 문단, 문장, 단어 (수형도 계층적 구조를 시각화 하기에 뛰어남. ex: 리그레션트리!)
- 색깔: 여행, 음악, 파티 등 소설에서 자주 등장하는 소재 (색은 범주형 변수를 표현하기에 뛰어남)
- highlight 가진 data 표현하기에 적합한 수형도
-
익스플로래이션 방식은 독자가 스스로 그림에서 메시지를 찾아낸다.
- 독자마다 다른 메시지를 찾는다고 말할 수 있겠지!
-
소설을 읽어보지 않은 사람: 이 그래픽으로 소설책의 전체 주제를 미리 파악가능
-
소설을 이미 읽어본 사람: 분석 & 탐구를 할 수 있음. ex: 파티와 음악이 동시에 등장하는 경우가 많다.
-
카이로: 사실 프리젠테이션과 익스플로레이션은 절충가능함
- aes(x='GDP',y='불평등',text='년도',color='정부')
- 초록색정부: 소득이 증가 & 불평등이 훨씬 더 증가 (기울기가 크다.)
- 갈색정부: 매우 빠른 경제 성장 (다른 정부에 비해 긴 x 길이(?)를 가짐. 즉, 정권을 오래 잡음!)
- 포인트간의 간격이 조밀하다 = 변화가 더디다 // 포인트간의 간격이 넓다 = 변화가 빠르다.
-
언뜻보기에는 우리에게 익숙한 라인플랏인듯 보이지만 의외로 정보를 해석할만한 요소가 있다.
익스플로레이션형의 그래프는 그릴줄도 알아야 하지만 남이 그린 그래프를 해석할 수도 있어야함.
-
둘다 틀렸다.
-
가난한 나라의 출산율은 점점 감소
-
잘사는 나라의 출산율은 점점 증가
-
결국 세계의 인구는 안정화 될 것 (증가하지도 감소하지도 않는다)
-
아래의 그림이 그 증거이다. (but 리들리의 말을 잘 설명해내지 못하는 그림)
-
리들러의 메시지는 아래의 그림들이 더 잘 전달한다.
-
어떠한 현상을 살펴볼때 그것의 부분집합들이 역시 그러한지 살펴보는것은 기본임
-
중요한 선을 제외한 나머지는 회색처리(일러스트레이터 사용) 한 것이 시각적으로 우수하며, 인상적이었음
-
과학적인 논문작업에 들어갈 그림이라면 임의로 회색처리한 것이 다소 비판을 받을 수 있음.
- 중요하지 않다는 주관적인 관점이 아니라 객관적인 관점을 제시하면 비판을 덜 받지 않을까...!
- Left: aes(x='출산율', y='수입')
- Right: aes(x='출산율', y='전체 중학생중 여학생이 차지하는 비율')
-
해설: 당신이 부자가 될수록 당신은 더 적은 아이를 가질 것이다. 중학교에 진학하는 여성이 적을수록 아이를 많이 낳는다. (그림밑에 주석을 해석)
-
여학생들을 중학교에 보내지 않으면 출산율이 올라가나요???
- 잘못된 해석이 나오지 않게!
-
아래는 남미국가들의 국방력을 시각화한 그림
- 쓸모없는 그래픽
- 뭐 기억나는 것이 있나요?
(데이터를 효율적으로 전달하지 못한 사례)
-
아래가 더 우수한 그림이다. 더 정확한 비교를 할 수 있어요.
-
그리고 위의 그림보다 아래의 그림이 더 우수한 시각화이다.
- 일단 하이라이팅의 색 조화가 보기에 좋다
- 브라질이 국방력도 우수하고 예산도 많이 투자하는 것 같지만 인구가 흑막인것 같다.
-
흑막을 제거
-
최종적으로 제안하는 그래프
- 좌측하단: aes(x='인구', y='군인수', size='예산')
- 우측하단: 관심있는 그래프가 아님
-
1사분면의 의미: 인구도 높고 군인수도 많은 나라 (똑같은 정보라 의미가 없다. 마치 x축이 토익점수, y축이 텝스점수 같은느낌임)
- 모든 점들이 직선에 몰려있다면? $\to$ 왜 2차원으로 표현했지?
-
저같으면 aes(x='예산(인구효과제거)', y='군인수(인구효과제거)',size='인구')로 할것 같아요.
- 1사분면의 의미: 예산도 많이 쓰고 군인수도 많은나라 = 콜롬비아.
- 4사분면의 의미: 예산은 많이 쓰는데 군인수가 적은나라 = 브라질
-
산점도에서 데이터를 한눈에 파악하고 특징을 요약하기 위해서는 X,Y를 너무 비슷한 성질의 변수로 설정하지마라.
아래중 어떤것이 더 바람직한 그래프인가?
- aes(x='토익', y='텝스', color='합/불', shape='회사의종류')
- 이것? 하지만 토익 점수가 좋으면 텝스 점수도 좋지 않을까?
- aes(x='토익', y='GPA', color='합/불', shape='회사의종류')
- 토익이랑 GPA가 관련있다고 단정지어 말할 수 있을까.
- 명암으로 왜 크기비교를 하는것인가?
-
비교를 위해서는 바플랏이 더 우수하다.
-
카이로교수님의 강의자료에 등장하는 그림
-
회색이 before, 검은색이 after
- 크기비교는 바플랏으로 하는것이 아니다.
-
우리눈은 작은원이 큰원의 절반정도 차지한다고 느껴진다.
-
그렇지만 실제로는 아래와 같음
-
버블차트는 크기를 왜곡시켜서 메세지를 이해하기 어렵게 만든다.
-
하지만 아래의 버블차트는 우수하다.
-
선거지도는 수치비교에 별로 관심이 없다.
-
대신에 민주당표와 공화당표가 어떤 지역에 몰렸는지 파악하는 것이 중요
-
따라서 aes중 가장 중요한 x,y를 모두 지역에 투자함
-
시간경과에 따른 변화를 보여주고 싶으면 라인플랏, 비교를 하고 싶다면 바플랏, 관계를 알고싶다면 산점도.
import pandas as pd
df2016=pd.read_csv("https://raw.githubusercontent.com/PacktPublishing/Pandas-Cookbook/master/data/stocks_2016.csv")
df2017=pd.read_csv("https://raw.githubusercontent.com/PacktPublishing/Pandas-Cookbook/master/data/stocks_2017.csv")
df2018=pd.read_csv("https://raw.githubusercontent.com/PacktPublishing/Pandas-Cookbook/master/data/stocks_2018.csv")
- 차 종(symnol)과 주식의 Shares,Low,High열이 있음
pd.concat([df2016,df2017,df2018]).reset_index(drop=True)
Symbol | Shares | Low | High | |
---|---|---|---|---|
0 | AAPL | 80 | 95 | 110 |
1 | TSLA | 50 | 80 | 130 |
2 | WMT | 40 | 55 | 70 |
3 | AAPL | 50 | 120 | 140 |
4 | GE | 100 | 30 | 40 |
5 | IBM | 87 | 75 | 95 |
6 | SLB | 20 | 55 | 85 |
7 | TXN | 500 | 15 | 23 |
8 | TSLA | 100 | 100 | 300 |
9 | AAPL | 40 | 135 | 170 |
10 | AMZN | 8 | 900 | 1125 |
11 | TSLA | 50 | 220 | 400 |
-
R에서 rbind와 유사하다
-
rbind와의 차이점: 합치려는 데이터 프레임의 columns이 꼭 동일할 필요는 없다.
import rpy2
%load_ext rpy2.ipython
%%R
library(tidyverse)
df1=tibble(x=c(1,2,3),y=c(1,2,4))
df2=tibble(x=c(1,2,4),y=c(2,3,4))
rbind(df1,df2) # r은 열 이름이 꼭 같아야 축 이어붙이기가 가능하다.
R[write to console]: ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.1 ── R[write to console]: ✔ ggplot2 3.3.5 ✔ purrr 0.3.4 ✔ tibble 3.1.3 ✔ dplyr 1.0.7 ✔ tidyr 1.1.3 ✔ stringr 1.4.0 ✔ readr 1.4.0 ✔ forcats 0.5.1 R[write to console]: ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ── ✖ dplyr::filter() masks stats::filter() ✖ dplyr::lag() masks stats::lag()
# A tibble: 6 × 2 x y <dbl> <dbl> 1 1 1 2 2 2 3 3 4 4 1 2 5 2 3 6 4 4
pd.concat([df2016,df2017.iloc[:,1:]])
Symbol | Shares | Low | High | |
---|---|---|---|---|
0 | AAPL | 80 | 95 | 110 |
1 | TSLA | 50 | 80 | 130 |
2 | WMT | 40 | 55 | 70 |
0 | NaN | 50 | 120 | 140 |
1 | NaN | 100 | 30 | 40 |
2 | NaN | 87 | 75 | 95 |
3 | NaN | 20 | 55 | 85 |
4 | NaN | 500 | 15 | 23 |
5 | NaN | 100 | 100 | 300 |
-
R에서 cbind와 비슷한 느낌이다. (그런데 row의 숫자가 서로 달라도 괜찮음)
pd.concat([df2016,df2017,df2018],axis='columns')
Symbol | Shares | Low | High | Symbol | Shares | Low | High | Symbol | Shares | Low | High | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | AAPL | 80.0 | 95.0 | 110.0 | AAPL | 50 | 120 | 140 | AAPL | 40.0 | 135.0 | 170.0 |
1 | TSLA | 50.0 | 80.0 | 130.0 | GE | 100 | 30 | 40 | AMZN | 8.0 | 900.0 | 1125.0 |
2 | WMT | 40.0 | 55.0 | 70.0 | IBM | 87 | 75 | 95 | TSLA | 50.0 | 220.0 | 400.0 |
3 | NaN | NaN | NaN | NaN | SLB | 20 | 55 | 85 | NaN | NaN | NaN | NaN |
4 | NaN | NaN | NaN | NaN | TXN | 500 | 15 | 23 | NaN | NaN | NaN | NaN |
5 | NaN | NaN | NaN | NaN | TSLA | 100 | 100 | 300 | NaN | NaN | NaN | NaN |
pd.concat([df2016,df2017,df2018],keys=[2016,2017,2018])
Symbol | Shares | Low | High | ||
---|---|---|---|---|---|
2016 | 0 | AAPL | 80 | 95 | 110 |
1 | TSLA | 50 | 80 | 130 | |
2 | WMT | 40 | 55 | 70 | |
2017 | 0 | AAPL | 50 | 120 | 140 |
1 | GE | 100 | 30 | 40 | |
2 | IBM | 87 | 75 | 95 | |
3 | SLB | 20 | 55 | 85 | |
4 | TXN | 500 | 15 | 23 | |
5 | TSLA | 100 | 100 | 300 | |
2018 | 0 | AAPL | 40 | 135 | 170 |
1 | AMZN | 8 | 900 | 1125 | |
2 | TSLA | 50 | 220 | 400 |
# 같은 결과
pd.concat({2016:df2016,2017:df2017,2018:df2018})
pd.concat(dict(zip([2016,2017,2018],[df2016,df2017,df2018])))
# {2016:df2016,2017:df2017,2018:df2018} = dict(zip([2016,2017,2018],[df2016,df2017,df2018]))
# 이기 때문에 가능!
- 인덱스 명을 데이터 프레임 수보다 적게 입력했을때- 앞 데이터 프레임부터 주어진 인덱스 지정해주고 나머진 잘림!!
- 인덱스 명을 데이터 프레임 수보다 많게 입력했을때- 알아서 무시함
pd.concat([df2016,df2017,df2018],axis=1,keys=[2016,2017,2018])
2016 | 2017 | 2018 | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
Symbol | Shares | Low | High | Symbol | Shares | Low | High | Symbol | Shares | Low | High | |
0 | AAPL | 80.0 | 95.0 | 110.0 | AAPL | 50 | 120 | 140 | AAPL | 40.0 | 135.0 | 170.0 |
1 | TSLA | 50.0 | 80.0 | 130.0 | GE | 100 | 30 | 40 | AMZN | 8.0 | 900.0 | 1125.0 |
2 | WMT | 40.0 | 55.0 | 70.0 | IBM | 87 | 75 | 95 | TSLA | 50.0 | 220.0 | 400.0 |
3 | NaN | NaN | NaN | NaN | SLB | 20 | 55 | 85 | NaN | NaN | NaN | NaN |
4 | NaN | NaN | NaN | NaN | TXN | 500 | 15 | 23 | NaN | NaN | NaN | NaN |
5 | NaN | NaN | NaN | NaN | TSLA | 100 | 100 | 300 | NaN | NaN | NaN | NaN |
# 같은 결과
pd.concat({2016:df2016,2017:df2017,2018:df2018},axis=1)
pd.concat(dict(zip([2016,2017,2018],[df2016,df2017,df2018])),axis=1)
import numpy as np
import pandas as pd
import folium
-
global view
m=folium.Map(scrollWheelZoom=False) # 해당 옵션은 스크롤 했을때 확대/축소 안 주기
m
-
location과 scale을 조정 후(위도, 경도 찾아서 리스트로 넣어주기)
-
tiles
옵션을 주어서 지도의 외형을 변경(다음과 같은 옵션은 찾기 힘들 수도..)
- "OpenStreetMap" 기본 옵션
- "Stamen Terrain", "Stamen Toner", "Stamen Watercolor"
- "CartoDB positron", "CartoDB dark_matter"
m=folium.Map(scrollWheelZoom=False,
location=[28.37797213245762, -81.5704639045799],
zoom_start=14, # 높은 수를 입력할 수록 확대됨
tiles="CartoDB positron")
-
지도위에 무엇인가 오브젝트를 추가하는데, 가장 기본적인 것이 마커임
-
마커안에 내용을 넣을 수 있음. (경우에 따라서는 유용하다)
m=folium.Map(scrollWheelZoom=False,
location=[28.37797213245762, -81.5704639045799],
zoom_start=14, # 높은 수를 입력할 수록 확대됨
tiles="Stamen Toner")
folium.Marker(location=[28.37797213245762, -81.5704639045799],
tooltip='Click me',
popup='Walt Disney World',
icon=folium.Icon(color='red',icon='university',prefix='fa')).add_to(m)
folium.Marker(location=[28.37797213245762, -81.5704639045799],
tooltip='Click me',
popup='Walt Disney World',
icon=folium.Icon(color='black',icon='home',prefix='fa')).add_to(m)
folium.Marker(location=[28.375707419518612, -81.54966175680092],
#tooltip='Click me',
popup='Epcot',
icon=folium.Icon(color='red',icon='music',prefix='fa')
).add_to(m)
folium.Marker(location=[28.37797213245762, -81.54966175680092],
#tooltip='Click me',
popup="Disney's Hollywood Studios",
icon=folium.Icon(color='red',icon='thumbs-up',prefix='fa')
).add_to(m)
m
-
icon='university'
대신에 쓸만한 옵션들
- icon='street-view'
- icon='tree'
- icon='plane'
- icon='bell'
- ...
-
마커에 HTML넣기
-
마커에 DataFrame (을 HTML로 바꿔서) 넣기
df= pd.DataFrame(data=[[2019,35],[2020,35],[2021,33]],columns=['Year','Enrollment'])
df.to_html()
'<table border="1" class="dataframe">\n <thead>\n <tr style="text-align: right;">\n <th></th>\n <th>Year</th>\n <th>Enrollment</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <th>0</th>\n <td>2019</td>\n <td>35</td>\n </tr>\n <tr>\n <th>1</th>\n <td>2020</td>\n <td>35</td>\n </tr>\n <tr>\n <th>2</th>\n <td>2021</td>\n <td>33</td>\n </tr>\n </tbody>\n</table>'
m=folium.Map(scrollWheelZoom=False,
location=[28.37797213245762, -81.5704639045799],
zoom_start=12, # 높은 수를 입력할 수록 확대됨
tiles="Stamen Toner")
folium.Marker(location=[28.37797213245762, -81.5704639045799],
tooltip='Click me',
popup='<h2> Walt Disney World </h2><br>', # 글씨크기 굵게
icon=folium.Icon(color='red',icon='university',prefix='fa')).add_to(m)
folium.Marker(location=[28.37797213245762, -81.54966175680092],
tooltip='Click me',
popup="<h5> Disney's Hollywood Studios </h5><br>" +df.to_html(),
icon=folium.Icon(color='black',icon='home',prefix='fa')).add_to(m)
m
-
논리구조상 HTML 오브젝트를 아무거나 넣을 수 있다. $\to$ 그림도 넣을 수 있다!
-
마커에 DataFrame (을 HTML로 바꿔서) 넣기
import base64
import matplotlib.pyplot as plt
fig=plt.figure()
plt.plot([1,2,3,4],[2,3,4,3],'o--r')
fig.savefig('temp.png')
_encoded = base64.b64encode(open('temp.png','rb').read())
_myhtml = '<img src="data:image/png;base64,{}">'.format
_iframe = folium.IFrame(_myhtml(_encoded.decode('UTF-8')),width=400,height=300)
_popup = folium.Popup(_iframe)
m=folium.Map(scrollWheelZoom=False,
location=[28.37797213245762, -81.5704639045799],
zoom_start=13,
tiles="CartoDB positron"
)
folium.Marker(location=[28.37797213245762, -81.5704639045799],
#tooltip='Click me',
popup=_popup,
icon=folium.Icon(color='red',icon='music',prefix='fa')
).add_to(m)
folium.CircleMarker(location=[28.37797213245762, -81.5704639045799],
radius=40,
color=None, # 테두리 색
fill=True,
fill_color='red',
popup='Walt Disney World').add_to(m)
folium.Marker(location=[28.37797213245762, -81.54966175680092],
#tooltip='Click me',
popup="<h5> Disney's Hollywood Studios </h5><br>" +df.to_html()
).add_to(m)
m
-
다양한 마커를 지옴처럼 생각해도 괜찮아보이며,
-
동그라미 하나 그리는데 많은 코드가 요구되는 감이 있고,
-
Circle이 아니라 라인, 사각형, 폴리곤등도 지도에 추가할수 있으나 더 많은 노력이 필요하다.
-
Heatmap은 폴리움에서 데이터 시각화를 하기에 적합한 기본도구임
from folium import plugins
data=(np.random.normal(size=(100,3)) + np.array([[28,77,5]])).tolist() # (좌표,weight)를 의미함, 그리고 numpy는 list가 될 수 없기 때문에 tolist 옵션 사용
m=folium.Map([28,77],zoom_start=6,scrollWheelZoom=False)
plugins.HeatMap(data).add_to(m)
m
-
입자의 위치
단 $(x_0,y_0)=(35.837688889992634, 127.11212714586104)$ 라고 하고 $\epsilon_t$와 $\eta_t$는 서로 독립인 정규분포이다.
- 최종적인 차원: 프레임수 점의수 2
t0 = np.array([[28.37797213245762, -81.5704639045799]]*3000)
walk = np.random.normal(size=(200,3000,2)) * 0.0001 # 분산 0.0001 작게 움직이게
t0 # 시점0
array([[ 28.37797213, -81.5704639 ], [ 28.37797213, -81.5704639 ], [ 28.37797213, -81.5704639 ], ..., [ 28.37797213, -81.5704639 ], [ 28.37797213, -81.5704639 ], [ 28.37797213, -81.5704639 ]])
t0+walk[0,:,:]# 시점1
array([[ 28.37796644, -81.57053024], [ 28.37807714, -81.57053668], [ 28.37779609, -81.5705447 ], ..., [ 28.37793518, -81.57038177], [ 28.37791151, -81.57056842], [ 28.37780552, -81.57038262]])
t0+walk[0,:,:]+walk[1,:,:]# 시점2
array([[ 28.37809721, -81.57056055], [ 28.37802541, -81.5704506 ], [ 28.37790288, -81.5704469 ], ..., [ 28.37811346, -81.57020931], [ 28.37787899, -81.57042655], [ 28.37787352, -81.57026543]])
-
그런데 walk[0,:,:]+walk[1,:,:]
를 좀더 간단하게 쓸수 있다.
walk[0,:,:]+walk[1,:,:]
array([[ 1.25077074e-04, -9.66484300e-05], [ 5.32740051e-05, 1.33073873e-05], [-6.92531710e-05, 1.70091754e-05], ..., [ 1.41332011e-04, 2.54598508e-04], [-9.31401371e-05, 3.73504263e-05], [-9.86132444e-05, 1.98477784e-04]])
walk[:2,:,:].sum(axis=0)
array([[ 1.25077074e-04, -9.66484300e-05], [ 5.32740051e-05, 1.33073873e-05], [-6.92531710e-05, 1.70091754e-05], ..., [ 1.41332011e-04, 2.54598508e-04], [-9.31401371e-05, 3.73504263e-05], [-9.86132444e-05, 1.98477784e-04]])
-
따라서 시점2는 아래와 같이 표현가능
t0+walk[:2,:,:].sum(axis=0)
array([[ 28.37809721, -81.57056055], [ 28.37802541, -81.5704506 ], [ 28.37790288, -81.5704469 ], ..., [ 28.37811346, -81.57020931], [ 28.37787899, -81.57042655], [ 28.37787352, -81.57026543]])
-
그렇다면 시점3은?
t0+walk[:3,:,:].sum(axis=0)
array([[ 28.37817196, -81.57069823], [ 28.37802073, -81.57043654], [ 28.37782612, -81.57047201], ..., [ 28.37826973, -81.57007106], [ 28.37798494, -81.57036401], [ 28.37779857, -81.57019955]])
-
따라서 data 는
data = [(t0+walk[:i,:,:].sum(axis=0)).tolist() for i in range(200)]
np.array(data).shape
(200, 3000, 2)
-
이제 data[0], data[1], data[2], ... data[199]까지 대입하여 각각 그림을 그리고 시각화하면 된다.
m=folium.Map(scrollWheelZoom=False,
location=[28.37797213245762, -81.5704639045799],
zoom_start=16,
tiles="CartoDB positron"
)
plugins.HeatMapWithTime(data,radius=5).add_to(m)
m