- Github: yoonkt200
Chapter 01 데이터에서 인사이트 발견하기¶
이 장의 핵심 개념¶
- 탐색적 데이터 분석을 알아본다.
- 판다스, 넘파이, Matplotlib 등을 활용한 분석에 익숙해 진다.
- 적절한 시각화 방법으로 인사이트를 발견한다.
1.1 탐색적 데이터 분석의 과정¶
탐색적 데이터 분석은 데이터의 정보를 파악하며 문제 해결에 필요한 힌트를 분석하는 과정이다. 이제, 탐색적 데이터 분석의 과정을 몇 가지 예제와 함께 알아보겠다.
데이터와의 첫 만남¶
우리가 실제로 접하는 대부분의 데이터는 아직 분석에 활용된 적이 없는 혹은 정제되지 않은 데이터 이다. 이러한 데이터를 raw data라고 부른다.(이후 '데이터'로 통칭한다.) 탐색적 데이터 분석은 데이터를 열어보는 상황에서부터 시작되며 이때부터 다음과 같은 과정을 수행한다.
- 데이터의 출처와 주제에 대해 이해한다.
우선 데이터가 어디에서 생성된 것이고, 어떻게 수집된 것인지를 이해하는 것이 먼저다. 데이터의 이름, 구성 요소, 그리고 데이터의 출처와 주제등에 대해 가장 먼저 조사한다.
- 데이터의 크기를 알아본다.
데이터의 대략적인 양이나 개수를 알아보는 단계이다. 데이터의 크기에 따라서 특별한 종류의 샘플링이 필요하고, 어떤 방식으로 데이터를 처리할지도 달라 질 수 있기 때문이다.
샘플링(Sampling)이란 어떤 자료로부터 일부의 값을 추출하는 행위를 의미한다. 분석하고자 하는 데이터가 너무 크거나 전체 데이터를 사용할 수 없는 경우에는 샘플링된 데이터를 분석하는 것이 좋은 대안이 될 수 있다.
데이터의 구성 요소(피처)를 살펴본다.
마지막으로 데이터의 피처를 살펴본다. 피처란 데이터를 구성하는 요소를 의미한다. 만약 어떤 학급의 신체검사 결과를 기록한 데이터가 있다고 할 때, 키와 몸무게 그리고 시력 같은 측정 요소를 피처라고 부른다. 데이터를 탐색하는 단계에서 피처를 살펴보는 것은 아주 중요한 과정이다.
데이터의 속성 탐색하기¶
데이터를 탐색하는 단계에서 데이터의 외형적인 힌트를 얻었다면 이번 단계에서는 데이터의 실제적인 내용을 탐색한다. 실제적인 내용 탐색의 과정에서는 크게 피처의 속성 탐색, 그리고 피처 간의 상관 관계 탐색 이렇게 두 가지를 시행한다.
피처의 속성 탐색¶
신체검사 데이터에서 어떤 속성을 탐색할 수 있을까요? 우선 가장 쉽게 떠올릴 수 있는 것은 학급의 평균 키 계산이다. 평균을 구하는 것뿐만 아니라, 학창 시절 배웠던 표준편차, 중앙값, 데이터의 사분위 수 등의 통계값을 구할 수도 있다. 피처의
측정 가능한 정량적 속성을 정의하는 것이다. 피처의 속성 탐색 과정에서 가장 중요한 것은 데이터에 질문을 던지는 것이다. 피처의 속성 탐색은 '우리 반의 평균 키는 몇이나 될까?'와 같은 질문에서부터 출발하기 때문이다. 이러한 질문을 던지는 것이 좋은 데이터 분석가가 되는 첫걸음이라고 할 수 있다.
피처 간의 상관 관계 탐색¶
피처 간의 상관 관계 탐색이란 여러 개의 피처가 서로에게 미치는 영향력을 알아보는 것이다. 만약 학급의 신체검사 데이터를 살펴본 뒤, '우리 학급은 비만이 아닐까?'라는 질문을 했다고 가정해보자. 그렇다면 우리는 '몸무게'라는 피처를 살펴볼 것이다. 그리고 이를 통해 가정을 확인할 것이다. 하지만 우리가 한 가지 더 고려해야 할 점은 몸무게는 키와도 관계가 있다는 것이다. 이런 상황에서 필요한 것이 피처 간의 상관 관계 탐색이다. 데이터 분석에서는 이를 통계적인 방법으로 알아볼수 있으며, 이는 피처 간의 공분산, 혹은 상관 계수와 같은 개념을 포함합니다. 이 개념들에 대해서는 이후 예제에서 조금 더 알아본다.
탐색한 데이터의 시각화¶
마지막 단계는 탐색한 데이터의 시각화 단계이다. 지금까지의 과정을 통해 어느 정도 데이터를 파악했다면 이를 효과적으로 시각화할 차례이다.데이터 시각화는 수치적 자료만 가지고는 파악하기 힘든 패턴이나 인사이트를 발견하는 데 유용하기 때문이다. 키와 몸무게의 상관 관계는 쉽게 발견하기 어렵지만 이를 그래프로 시각화하면 직관적으로 파악할 수 있다.
1.2 멕시코풍 프랜차이즈 chipotle의 주문 데이터 분석하기¶
첫 번째 예제의 목표는 토이 데이터(분석에 용이한 형태로 만들어진 연습용 데이터 세트)를 활용한 데이터 분석에 익숙해지는 것이다. 우리가 분석해볼 첫 번째 토이 데이터는 멕시코풍 프랜차이즈 'chipotle'라는 음식점의 주문 데이터이다.
- chipotle 데이터셋의 기초 정보 출력하기
# -*- coding:utf-8 -*-
import pandas as pd
file_path = './data/chipotle.tsv'
# read_csv() 함수로 데이터를 데이터 프레임 형태로 불러옵니다.
chipo = pd.read_csv(file_path, sep = '\t')
print(chipo.shape)
print("------------------------------------")
print(chipo.info())
(4622, 5) ------------------------------------ <class 'pandas.core.frame.DataFrame'> RangeIndex: 4622 entries, 0 to 4621 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 order_id 4622 non-null int64 1 quantity 4622 non-null int64 2 item_name 4622 non-null object 3 choice_description 3376 non-null object 4 item_price 4622 non-null object dtypes: int64(2), object(3) memory usage: 180.7+ KB None
그리고 shape()와 info() 함수를 호출하면 위와 같은 실행 결과를 볼 수 있다. shape()는 데이터의 행과 열의 크기를 반환하고, info()는 행의 구성 정보와 열의 구성 정보를 나타낸다. 실행 결과를 보면 데이터가 4,622개의 행과 5개의 피처로 구성되어 있음을 알 수 있다. 또한 order_id와 quantity는 int64(숫자),나머지 3개의 피처는 object라는 타입으로 이루어져 있다. 그런데 한 가지 특이한 정보는 choice_description 피처가 3,376개의 non--null object로 구성되어 있다는 것입니다. 이 정보는 무엇을 의미할까? 데이터 프레임에서 object 타입은 일반적으로 문자열을 의미 한다. 그리고 null이라는 용어는 데이터가 비어 있는 것을 의미한다. 이를 결측값 이라고 한다. 즉, '3376 non-null object'라는 것은 3,376개의 비어 있지 않은 문자열 데이터가 있다는 정보이다. 하지만 데이터의 개수를 4,622개이므로 choice_description 피처는 1,246개의 결측값이 존재한다.
다음 코드를 보자. head(10) 함수는 첫 10개의 데이터를 테이블 형태로 출력합니다.또한 columns(행의 목록)와 index(열의 목록)를 호출하면 데이터의 행과 열에 대한 정보를 함께 출력 할 수 있다.
- chipotle 데이터셋의 행과 열, 데이터 확인하기
# chipo 라는 데이터 프레임에서 순서대로 10개의 데이터를 보여준다.
chipo.head(10)
print(chipo.columns)
print("------------------------------------")
print(chipo.index)
Index(['order_id', 'quantity', 'item_name', 'choice_description', 'item_price'], dtype='object') ------------------------------------ RangeIndex(start=0, stop=4622, step=1)
이때, chipotle 데이터셋 피처의 의미는 각각 다음과 같습니다.
order_id:주문 번호
quantity: 아이템의 주문 수량
item_name: 주문한 아이템의 이름
choice_description: 주문한 아이템의 상세 선택 옵션
item_price: 주문 아이템의 가격 정보
이제 본격적으로 이 피처들의 속성을 파악해 본다.
quantity와 item_price의 수치적 특징¶
우선 quantity와 item_price의 수치적 특징을 살펴본다. 이 두 피처는 연속형 피처이다. 연속형 피처는 키와 몸무게 처럼 어떠한 값도 가질 수 있는 연속적인 숫자 형태를 의미한다. 다음 코드의 출력 결과는 describe() 함수가 나타낸 피처의 기초 통계량이다. 하지만 현재 유일하게 존재하는 수치형 피처는 quantity뿐이기 때문에, 오직 quantity에 대한 정보만을 출력할 수 있다.
- describe()함수로 기초 통계량 출력하기
# order_id는 숫자의 의미를 가지지 않기 때문에 str으로 변환한다.
chipo['order_id'] = chipo['order_id'].astype(str)
print(chipo.describe()) # chipo 데이터 프레임에서 수치형 피처들의 기초 통계량을 확인한다.
quantity count 4622.000000 mean 1.075725 std 0.410186 min 1.000000 25% 1.000000 50% 1.000000 75% 1.000000 max 15.000000
출력 내용을 분석해보면 아이템의 평군 주문 수량(mean)은 약 1.07이라는 것을 알 수 있다. 이는 대부분이 한 아이템에 대해서 1개 정도만 주문했다는 것이고, '한 사람이 같은 메뉴를 여러 개 구매하는 경우는 많지 않다.'는 인사이트를 얻을 수 있다. 그렇다면 item_price의 수치적 특징은 어떻게 알아볼 수 있을까? 현재 item_price 피처는 object타입이기 때문에 describe()함수로 기초 통계량을 확인할 수 없다. 이를 위해서 추가적인 데이터 전처리 작업이 필요한데, 이는 "step3" 데이터 전처리 에서 알아 본다.
order_id와 item_name의 개수¶
다음으로 order_id와 item_name의 개수를 탐색한다. 이 두 피처는 범주형 피처이기 때문에 unique()함수를 사용한다. 이를 통해 피처 내에 몇 개의 범주가 있는지 확인 할 수 있다.
- unique() 함수로 범주형 피처의 개수 출력하기
print(len(chipo['order_id'].unique()))
print(len(chipo['item_name'].unique()))
1834 50
- 가장 많이 주문한 아이템 Top 10 출력하기
# 가장 많이 주문한 아이템 Top 10을 출력한다.
item_count = chipo['item_name'].value_counts()[:10]
for idx, (val, cnt) in enumerate(item_count.iteritems(), 1):
print("Top", idx, ":", val, cnt)
Top 1 : Chicken Bowl 726 Top 2 : Chicken Burrito 553 Top 3 : Chips and Guacamole 479 Top 4 : Steak Burrito 368 Top 5 : Canned Soft Drink 301 Top 6 : Steak Bowl 211 Top 7 : Chips 211 Top 8 : Bottled Water 162 Top 9 : Chicken Soft Tacos 115 Top 10 : Chicken Salad Bowl 110
아이템별 주문 개수와 총량¶
이번에는 groupby() 함수를 이용하여 아이템별 주문 개수와 총량을 구해보겠습니다. 판다스의 groupby() 함수는 데이터 프레임에서 특정 피처를 기준으로 그룹을 생성하며 이를 통해 그룹별 연산을 적용할 수 있다. 예를 들어 '학급'이라는 그룹을 만들었을 때, '학급별 평균 키','학급별 평균 몸무게'등을 구하는 것처럼 말이다.
- 아이템별 주문 개수와 총량 구하기
# 아이템별 주문 개수를 출력하자
order_count = chipo.groupby('item_name')['order_id'].count()
order_count[:10] # 아이템별 주문 개수를 출력한다.
item_name 6 Pack Soft Drink 54 Barbacoa Bowl 66 Barbacoa Burrito 91 Barbacoa Crispy Tacos 11 Barbacoa Salad Bowl 10 Barbacoa Soft Tacos 25 Bottled Water 162 Bowl 2 Burrito 6 Canned Soda 104 Name: order_id, dtype: int64
# 아이템별 주문 총량을 계산한다.
item_quantity = chipo.groupby('item_name')['quantity'].sum()
item_quantity[:10] #아이템별 주문 총량을 출력한다.
item_name 6 Pack Soft Drink 55 Barbacoa Bowl 66 Barbacoa Burrito 91 Barbacoa Crispy Tacos 12 Barbacoa Salad Bowl 10 Barbacoa Soft Tacos 25 Bottled Water 211 Bowl 4 Burrito 6 Canned Soda 126 Name: quantity, dtype: int64
위의 코드 chipo.groubpy("item_name")['quantity'].count()는 chipo Dataf에서 item_name을 그룹으로 quantity 피처의 count()를 계산한 것이다.이는 아이템별 주문 개수를 의미한다. 마찬가지 방법으로 chipo.groubpy(item_name)['quantity'].sum()은 아이템별 주문 총량을 나타낸다.
시각화¶
아이템별 주문의 총량을 막대 그래프로 시각화해볼 수도 있습니다. 코드에서는 tolist()와 넘파이의 arrange() 함수를 이용해 x_pos를 선언하고, 0부터 50까지의 숫자를 그래프의 x축 이름으로 사용합니다. 그 이유는 50개 아이템의 이름을 x축에 표현하기에는 그래프의 공간이 너무 협소하기 때문이다. y값(order_cnt)에는 주문 총량에 해당하는 값인 item_quantity.values.tolist()를 넣어 준다.
- 시각화로 분석 결과 살펴보기
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
item_name_list = item_quantity.index.tolist()
x_pos = np.arange(len(item_name_list))
order_cnt = item_quantity.values.tolist()
plt.bar(x_pos, order_cnt, align='center')
plt.ylabel('ordered_item_count')
plt.title('Distribution of all ordered item')
plt.show()
Step3 데이터 전처리: 전처리 함수 사용하기¶
앞선 내용의 item_price의 수치적 특징을 탐색하는 과정에서 우리는 item_price 피처의 요약 통계를 구할 수 없었습니다. 이는 item_price 피처가 문자열 타입이었기 때문이다. 이번 단계에서는 이러한 데이터를 전처리 하는 방법에 대해 알아보겠다.
- item_price 피처 살펴보기
print(chipo.info())
print('-------------')
print['item_price'].head()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 4622 entries, 0 to 4621 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 order_id 4622 non-null object 1 quantity 4622 non-null int64 2 item_name 4622 non-null object 3 choice_description 3376 non-null object 4 item_price 4622 non-null object dtypes: int64(1), object(4) memory usage: 180.7+ KB None -------------
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-9-318a91eae0c5> in <module> 1 print(chipo.info()) 2 print('-------------') ----> 3 print['item_price'].head() TypeError: 'builtin_function_or_method' object is not subscriptable
데이터 전처리에 앞서 item_price 피처를 살펴본 결과, 가격을 나타내느 숫자 앞에 "$"기호가 붙어 있는것을 발견할 수 있다. 따라서 이 피처를 수치 데이터로 사용하기 위해서는 "$"기호를 제거해주는 전처리 작업이 필요하다. "$"기호를 제거하는 전처리 방법은 chipo['item_price']에 apply()함수를 적용함으로써 가능하다. 그리고 apply()함수에는 lambda라는 함수 명령어를 추가한다.
- apply()와 lambda를 이용해 데이터 전처리하기
# column 단위 데이터에 apply() 함수로 전처리를 적용한다.
chipo['item_price'] = chipo['item_price'].apply(lambda x : float(x[1:]))
chipo.describe()
quantity | item_price | |
---|---|---|
count | 4622.000000 | 4622.000000 |
mean | 1.075725 | 7.464336 |
std | 0.410186 | 4.245557 |
min | 1.000000 | 1.090000 |
25% | 1.000000 | 3.390000 |
50% | 1.000000 | 8.750000 |
75% | 1.000000 | 9.250000 |
max | 15.000000 | 44.250000 |
apply() 함수는 시리즈 단위의 연산을 처리하는 기능을 수행하며, sum()이나 mean()과 같이 연상이 정의된 함수를 파라미터로 받는다. 따라서 피처 단위의 합계나 평균을 구할 수도 있고, 우리가 정의할 새로운 함수 문자열 데이터에서 첫 번째 문자열을 제거한 뒤 나머지 문자열을 수치형으로 바꿔주는 함수를 파라미터로 입력할 수도 있다. 위의 코드 lambda x:float(x[1:])는 이를 수행하는 함수를 lambda로 정의하여 입력 한 것이다.
- 주문당 평균 계산금액 출력하기
# 주문당 평균 계산금액을 출력한다.
chipo.groupby('order_id')['item_price'].sum().mean()
18.811428571428568
한 주문에 10달러 이상 지불한 주문 번호(id) 출력하기¶
다음으로 한 주문에 10달러 이상을 지불한 주문을 찾아보자. 우선 order_id피처(하나의 주문)를 기준으로 그룹을 만들어 quantity, item_price 피처의 합계를 계산한다. 그리고 그 결과에서 10 이상인 값을 필터링하자. 이에 대한 최종 결과인 result의 index.values를 출력하면 한 주문에 10달러 이상 지불한 id를 출력할 수 있다.
- 한 주문에 10달러 이상 지불한 주문 번호(id) 출력하기
# 한 주문에 10달러 이상 지불한 id를 출력한다.
chipo_orderid_group = chipo.groupby('order_id').sum()
results = chipo_orderid_group[chipo_orderid_group.item_price >= 10]
print(results[:10])
print(results.index.values)
quantity item_price order_id 1 4 11.56 10 2 13.20 100 2 10.08 1000 2 20.50 1001 2 10.08 1002 2 10.68 1003 2 13.00 1004 2 21.96 1005 3 12.15 1006 8 71.40 ['1' '10' '100' ... '997' '998' '999']
각 아이템의 가격 구하기¶
이번에는 각 아이템의 가격을 계산합니다. chipotle 데이터셋에서는 주어진 데이터만으로 각 아이템의 가격을 대략적으로 유추한다. 유추 과정은 다음과 같다.
chipo[chipo.quantity ==1]으로 동일 아이템을 1개만 구매한 주문을 선별한다.
item_name을 기준으로 groupby 연산을 수행한 뒤, min()함수로 각 그룹별 최처가를 계산한다.
item_price를 기준으로 정렬하는 sort_values() 함수를 적용한다. sort_values()는 series 데이터를 정렬해주는 함수이다.
- 각 아이템의 가격 구하기
# 각 아이템의 가격을 계산한다.
chipo_one_item = chipo[chipo.quantity ==1]
price_per_item = chipo_one_item.groupby('item_name').min()
price_per_item.sort_values(by = "item_price", ascending = False)[:10]
order_id | quantity | choice_description | item_price | |
---|---|---|---|---|
item_name | ||||
Steak Salad Bowl | 1032 | 1 | [Fresh Tomato Salsa, Lettuce] | 9.39 |
Barbacoa Salad Bowl | 1283 | 1 | [Fresh Tomato Salsa, Guacamole] | 9.39 |
Carnitas Salad Bowl | 1035 | 1 | [Fresh Tomato Salsa, [Rice, Black Beans, Chees... | 9.39 |
Carnitas Soft Tacos | 1011 | 1 | [Fresh Tomato Salsa (Mild), [Black Beans, Rice... | 8.99 |
Carnitas Crispy Tacos | 1774 | 1 | [Fresh Tomato Salsa, [Fajita Vegetables, Rice,... | 8.99 |
Steak Soft Tacos | 1054 | 1 | [Fresh Tomato Salsa (Mild), [Cheese, Sour Cream]] | 8.99 |
Carnitas Salad | 1500 | 1 | [[Fresh Tomato Salsa (Mild), Roasted Chili Cor... | 8.99 |
Carnitas Bowl | 1007 | 1 | [Fresh Tomato (Mild), [Guacamole, Lettuce, Ric... | 8.99 |
Barbacoa Soft Tacos | 1103 | 1 | [Fresh Tomato Salsa, [Black Beans, Cheese, Let... | 8.99 |
Barbacoa Crispy Tacos | 110 | 1 | [Fresh Tomato Salsa, Guacamole] | 8.99 |
그리고 각 아이템의 대략적인 가격을 아래와 같이 2개의 그래프로 시각화하여 나타낼 수 있다. 각가의 그래프는 아이템의 가격 분포(위쪽 결과)와 가격 히스토그램(아래쪽 결과)을 나타낸다. 이를 통해 2~4달러, 혹은 6~8달러 정도에 아이템의 가격이 형성되어 있다.
# 아이템 가격 분포 그래프를 출력한다.
item_name_list = price_per_item.index.tolist()
x_pos = np.arange(len(item_name_list))
item_price = price_per_item['item_price'].tolist()
plt.bar(x_pos, item_price, align='center')
plt.ylabel('item price($)')
plt.title('Distribution of item price')
plt.show()
# 아이템 가격 히스토그램을 출력한다.
plt.hist(item_price)
plt.ylabel('counts')
plt.title('Histogram of item price')
plt.show()
가장 비싼 주문에서 아이템이 총 며개 팔렸는지 구하기¶
orer_id에 그룹별 합계 연산을 적용한다. 그리고 item_price를 기준으로 sort_values를 반환하면 가장 비싼 주문순으로 연산 결과를 얻을 수 있다. 다음 결과에서는 가장 비싼 주문에서 23개의 아이템을 주문한 것을 알 수 있다.
- 가장 비싼 주문에서 아이템이 총 몇개 팔렸는지 구하기
# 가장 비싼 주문에서 아이템이 총 몇개 팔렸는지 구하기
chipo.groupby('order_id').sum().sort_values(by='item_price', ascending=False)[:5]
quantity | item_price | |
---|---|---|
order_id | ||
926 | 23 | 205.25 |
1443 | 35 | 160.74 |
1483 | 14 | 139.00 |
691 | 11 | 118.25 |
1786 | 20 | 114.30 |
'Veggie Salad Bowl'이 몇 번 주문되었는지 구하기¶
이번에는 특정 아이템인 'Veggie Salad Bowl'이 변 번이나 주문되었는지 알아보겠다. 이를 위해 chipo[chipo['item_name'] == 'Veggie Salad Bowl']으로 필터링 한뒤, drop_duplicates()라는 함수를 사용한다. 이는 한 주문 내에서 item_name이 중복 집계된 경우를 제거해주기 위함이다.그리고 최종 결과인 chipo_salad의 길이를 출력하면 Veggie Salad Bowl이 데이터 내에서 몇 번이나 주문되었는지 구할 수 있다. 출력 결과, Veggie Salad Bowl은 총 18번 주문된 것을 알 수 있다.
- 'Veggie Salad Bowl'이 몇 번 주문되었는지 구하기
# 'Veggie Salad Bowl'이 몇 번 주문되었는지를 계산한다.
chipo_salad = chipo[chipo['item_name'] == "Veggie Salad Bowl"]
# 한 주문 내에서 중복 집계된 item_name을 제거한다.
chipo_salad = chipo_salad.drop_duplicates(['item_name', 'order_id'])
print(len(chipo_salad))
chipo_salad.head(5)
18
order_id | quantity | item_name | choice_description | item_price | |
---|---|---|---|---|---|
186 | 83 | 1 | Veggie Salad Bowl | [Fresh Tomato Salsa, [Fajita Vegetables, Rice,... | 11.25 |
295 | 128 | 1 | Veggie Salad Bowl | [Fresh Tomato Salsa, [Fajita Vegetables, Lettu... | 11.25 |
455 | 195 | 1 | Veggie Salad Bowl | [Fresh Tomato Salsa, [Fajita Vegetables, Rice,... | 11.25 |
496 | 207 | 1 | Veggie Salad Bowl | [Fresh Tomato Salsa, [Rice, Lettuce, Guacamole... | 11.25 |
960 | 394 | 1 | Veggie Salad Bowl | [Fresh Tomato Salsa, [Fajita Vegetables, Lettu... | 8.75 |
'Chicken Bowl'을 2개이상 주문한 주문 횟수 구하기¶
비슷한 방법으로 'Chicken Bowl'을 2개 이상 주문한 주문 횟수를 구해보자. 먼저 'Chicken Bowl'을 주문한 데이터만을 필터링 한 뒤, 데이터프레임에서 quantity가 2 이상인 것을 선택하면 이 결과가 'Chicken Bowl'을 2개 이상 주문한 주문 횟수를 의미한다. 2개 이상으로 필터링을 적용한 출력 결과는 다음과 같다.
- "Chicken Bowl"을 2개 이상 주문한 주문 횟수 구하기
# "Chicken Bowl"을 2개 이상 주문한 주문 횟수를 구한다.
chipo_chicken = chipo[chipo['item_name'] == "Chicken Bowl"]
chipo_chicken_result = chipo_chicken[chipo_chicken['quantity'] >= 2]
print(chipo_chicken_result.shape[0])
33
1.3 국가별 음주 데이터 분석하기¶
두 번째 예제에서는 국가별 음주 데이터를 활용하여 탐색적 분석을 진행한다. 이를 통해 데이터 분석 라이브러리의 사용법과 탐색적 데이터 분석의 과정에 조금 더 익숙해지는 것을 목표로 한다.
Step 1 탐색: 데이터의 기초 정보 살펴보기¶
이번 데이터셋을 이루고 있는 피처는 다음과 같다.
- country: 국가 정보
- beer_servings: beer 소비량
- spirit_servings: spirit 소비량
- wine_servings: wine 소비량
- total_litres_of_pure_alchohol:총 알코올 소비량
- continent: 국가의 대륙 정보
이 중에서 먼저 info() 함수로 데이터의 기초적인 정보를 살펴본다. 총 193개의 데이터가 있으며, country와 contitnet 피처는 23개의 결측값이 존재한다.
- drinks 데이터셋의 기초 정보 출력하기
# -*- coding: utf-8 -8-
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
file_path = './data/drinks.csv'
drinks = pd.read_csv(file_path) # read_csv() 함수로 데이터를 데이터 프레임 형태로 불러온다.
print(drinks.info())
drinks.head(10)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 193 entries, 0 to 192 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 country 193 non-null object 1 beer_servings 193 non-null int64 2 spirit_servings 193 non-null int64 3 wine_servings 193 non-null int64 4 total_litres_of_pure_alcohol 193 non-null float64 5 continent 170 non-null object dtypes: float64(1), int64(3), object(2) memory usage: 9.2+ KB None
country | beer_servings | spirit_servings | wine_servings | total_litres_of_pure_alcohol | continent | |
---|---|---|---|---|---|---|
0 | Afghanistan | 0 | 0 | 0 | 0.0 | AS |
1 | Albania | 89 | 132 | 54 | 4.9 | EU |
2 | Algeria | 25 | 0 | 14 | 0.7 | AF |
3 | Andorra | 245 | 138 | 312 | 12.4 | EU |
4 | Angola | 217 | 57 | 45 | 5.9 | AF |
5 | Antigua & Barbuda | 102 | 128 | 45 | 4.9 | NaN |
6 | Argentina | 193 | 25 | 221 | 8.3 | SA |
7 | Armenia | 21 | 179 | 11 | 3.8 | EU |
8 | Australia | 261 | 72 | 212 | 10.4 | OC |
9 | Austria | 279 | 75 | 191 | 9.7 | EU |
나머지 피처의 수치적 정보를 살펴보기 위해 describe() 함수를 실행해보면 결과는 아래와 같다.
drinks.describe()
beer_servings | spirit_servings | wine_servings | total_litres_of_pure_alcohol | |
---|---|---|---|---|
count | 193.000000 | 193.000000 | 193.000000 | 193.000000 |
mean | 106.160622 | 80.994819 | 49.450777 | 4.717098 |
std | 101.143103 | 88.284312 | 79.697598 | 3.773298 |
min | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
25% | 20.000000 | 4.000000 | 1.000000 | 1.300000 |
50% | 76.000000 | 56.000000 | 8.000000 | 4.200000 |
75% | 188.000000 | 128.000000 | 59.000000 | 7.200000 |
max | 376.000000 | 438.000000 | 370.000000 | 14.400000 |
Step 2 인사이트의 발견: 탐색과 시각화하기¶
다음으로 피처 간의 상관 관계를 살펴본다. 피처 간의 상관 관계를 통계적으로 탐색하는 방법은 크게 두 가지 이다. 첫 번째 방법은 피처가 2개일 때의 상관 계수를 계산하는 단순 상관 분석 방법이며, 두 번째 방법은 대상 피처가 여러 개일 때 상호 간의 연관성을 분석하는 다중 상관 분석이다. 이 중, 우리가 적용해 볼 방법은 첫 번째 단순 상관 분석 이다. 상관 관계를 살펴볼 대상은 beer_servings, wine_servings, sprit_servings, total_litres_of_pure_alcohol 이렇게 4개의 피처이다. 이 피처는 술의 종류만 다를 뿐, 결국에는 비슷한 의미를 가지는 피처들이기 때문이다. 먼저 맥주와 와인 소비량의 상관 관계를 알아보자.
아래 코드처럼 데이터 프레임에서 두 피처를 선책한 뒤, corr()함수를 적용한다. 이를 통해 피처 간의 상관 계수를 matrix의 형태로 출력한 수 있다. 출력 결과, beer_servings와 wine_servings 두 피처 간의 상관 계수는 0.52 정도로 나타났다.
- 두 피처 간의 상관 계수 구하기
# beer_servings, wine_servings 두 피처 간의 상관 계수 구하기
# pearson은 상관 계수를 구하는 계산 방법 중 하나의 의미하며, 가장 널리 쓰이는 방법이다.
corr = drinks[['beer_servings', 'wine_servings']].corr(method = 'pearson')
print(corr)
beer_servings wine_servings beer_servings 1.000000 0.527172 wine_servings 0.527172 1.000000
이제 모든 피처들을 각각 1대 1로 비교하여 살펴보자. 아래 실행 결과는 4개 피처에 corr() 함수를 적용한 상관 계수 행렬을 출력한 것이다.
- 여러 피처의 상관 관계 분석하기
# 피처 간의 상관 계수 행렬을 구한다.
cols = ['beer_servings', 'spirit_servings', 'wine_servings', 'total_litres_of_pure_alcohol']
corr = drinks[cols].corr(method = 'pearson')
print(corr)
beer_servings spirit_servings wine_servings \ beer_servings 1.000000 0.458819 0.527172 spirit_servings 0.458819 1.000000 0.194797 wine_servings 0.527172 0.194797 1.000000 total_litres_of_pure_alcohol 0.835839 0.654968 0.667598 total_litres_of_pure_alcohol beer_servings 0.835839 spirit_servings 0.654968 wine_servings 0.667598 total_litres_of_pure_alcohol 1.000000
이를 조금 더 쉽게 실행 하기 위해 'heatmap','pairplot'이라는 기법을 사용해보자. 파이썬의 seaborn이라는 시각화 라이브러리를 활용하여 이 2개 기법을 사용할 수 있다.
코드는 매우 간단하다. heatmap의 경우 corr.values를, pairplot의 경우 데이터 프레임을 파라미터로 넣어준다. 그 외에도 그래프의 디자인과 관련된 몇 가지 파라미터를 설정할 수 있다.
import seaborn as sns
import matplotlib.pyplot as plt
# corr 행렬 히트맵을 시각화한다.
cols_view = ['beer', 'sprit', 'wine', 'alcohol'] # 그래프 출력을 위한 cols 이름을 축약한다.
sns.set(font_scale = 1.5)
hm = sns.heatmap(corr.values,
cbar=True,
annot=True,
square=True,
fmt='.2f',
annot_kws={'size':15},
yticklabels=cols_view,
xticklabels=cols_view)
plt.tight_layout()
plt.show()
# 시각화 라이브러리를 이용한 피처 간의 산점도 그래프를 출력한다.
sns.set(style = 'whitegrid', context='notebook')
sns.pairplot(drinks[['beer_servings', 'spirit_servings',
'wine_servings', 'total_litres_of_pure_alcohol']], height = 2.5)
plt.show()
다음 두 그래프로 결과를 살펴보면 total_litres_of_pure_alcohol피처가 대체적으로 모든 피처와 상관 관계가 있는 것으로 보이며, 특히 beer_servings와의 상관성이 매우 높은 것으로 나타난다. 코드의 첫 번째 그래프가 heatmap그래프, 그리고 두 번째 그래프가 pairplot 그래프이다.
Step3 탐색적 분석: 스무고개로 개념적 탐색 분석하기¶
본격적인 탐색적 분석에 들어가기에 앞서 continent 피처에 존재하는 결측 데이터를 처리해보자. 대륙에 대한 정보가 없는 국가를 'Others',줄여서 'OT'라는 대륙으로 새롭게 정의하겠다. 이를 처리하는 코드는 다음과 같다. fillna()함수를 사용하여 drinks 데이터 프레임의 continent 피처의 결측값을 OT로 채워준다.
- 결측 데이터 전처리하기
# 결측 데이터를 처리한다: 기타 대륙으로 통합 -> 'OT'
drinks['contitnet'] = drinks['continent'].fillna('OT')
drinks.head()
country | beer_servings | spirit_servings | wine_servings | total_litres_of_pure_alcohol | continent | contitnet | |
---|---|---|---|---|---|---|---|
0 | Afghanistan | 0 | 0 | 0 | 0.0 | AS | AS |
1 | Albania | 89 | 132 | 54 | 4.9 | EU | EU |
2 | Algeria | 25 | 0 | 14 | 0.7 | AF | AF |
3 | Andorra | 245 | 138 | 312 | 12.4 | EU | EU |
4 | Angola | 217 | 57 | 45 | 5.9 | AF | AF |
이번에는 전체 대륙 중에서 OT가 차지하는 비율이 얼마나 되는지를 파이차트로 확인하자. 아래 코드는 plt.pie()함수를 이용한 파이차트 출력 방법이다.
# 파이차트로 시각화하기
labels = drinks['continent'].value_counts().index.tolist()
fracs1 = drinks['continent'].value_counts().values.tolist()
explode = (0, 0, 0 ,0.25, 0 ,0 )
plt.pie(fracs1, explode=explode, labels=labels, autopct = '%.0f%%', shadow=True)
plt.title('null data to \'OT\'')
plt.show()
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-29-f5370eaae711> in <module> 5 explode = (0, 0, 0 ,0.25, 0 ,0 ) 6 ----> 7 plt.pie(fracs1, explode=explode, labels=labels, autopct = '%.0f%%', shadow=True) 8 plt.title('null data to \'OT\'') 9 plt.show() ~/.local/lib/python3.6/site-packages/matplotlib/pyplot.py in pie(x, explode, labels, colors, autopct, pctdistance, shadow, labeldistance, startangle, radius, counterclock, wedgeprops, textprops, center, frame, rotatelabels, normalize, data) 2832 wedgeprops=wedgeprops, textprops=textprops, center=center, 2833 frame=frame, rotatelabels=rotatelabels, normalize=normalize, -> 2834 **({"data": data} if data is not None else {})) 2835 2836 ~/.local/lib/python3.6/site-packages/matplotlib/__init__.py in inner(ax, data, *args, **kwargs) 1445 def inner(ax, *args, data=None, **kwargs): 1446 if data is None: -> 1447 return func(ax, *map(sanitize_sequence, args), **kwargs) 1448 1449 bound = new_sig.bind(ax, *args, **kwargs) ~/.local/lib/python3.6/site-packages/matplotlib/axes/_axes.py in pie(self, x, explode, labels, colors, autopct, pctdistance, shadow, labeldistance, startangle, radius, counterclock, wedgeprops, textprops, center, frame, rotatelabels, normalize) 3024 raise ValueError("'label' must be of length 'x'") 3025 if len(x) != len(explode): -> 3026 raise ValueError("'explode' must be of length 'x'") 3027 if colors is None: 3028 get_next_color = self._get_patches_for_fill.get_next_color ValueError: 'explode' must be of length 'x'
대륙별 spirit_servings의 통계적 정보는 어느 정도일까?¶
다음 코드는 agg() 함수를 사용하여 대륙 단위로 분석을 수행한다. agg()함수는 apply() 함수와 거의 동일한 기능을 하지만, apply()에 들어가는 함수 파라미터를 병렬로 설정하여 그룹에 대한 여러가지 연산결과를 동시에 얻을 수 있는 함수이다. 대륙별 'spitit_servings'의 통계적 정보를 구하기 위해서는 agg에['mean', 'min','max',sum'] 파라미터를 입력하는 것만으로도 간단히 탐색이 가능하다.
- agg()함수를 이용해 대륙별로 분석하기
# 대륙별 spirit_servings의 평균, 최소, 최대, 합계를 계산한다.
result = drinks.groupby('continent').spirit_servings.agg(['mean', 'min', 'max', 'sum'])
result.head()
mean | min | max | sum | |
---|---|---|---|---|
continent | ||||
AF | 16.339623 | 0 | 152 | 866 |
AS | 60.840909 | 0 | 326 | 2677 |
EU | 132.555556 | 0 | 373 | 5965 |
OC | 58.437500 | 0 | 254 | 935 |
SA | 114.750000 | 25 | 302 | 1377 |
전체 평균보다 많은 알코올 섭취하는 대륙은 어디일까?¶
전체 평균보다 많은 알코올을 섭취하는 대륙을 탐색할 때는 apply()나 agg() 함수 없이도 mean()함수만을 이용한 탐색을 수행할 수 있다.
# 전체 평균보다 많은 알코올을 섭취하는 대륙을 구한다.
total_mean = drinks.total_litres_of_pure_alcohol.mean()
continent_mean =drinks.groupby('continent')['total_litres_of_pure_alcohol'].mean()
continent_over_mean = continent_mean[continent_mean >= total_mean]
print(continent_over_mean)
continent EU 8.617778 SA 6.308333 Name: total_litres_of_pure_alcohol, dtype: float64
평균 beer_servings가 가장 높은 대륙은 어디일까?¶
그리고 mean() 함수만을 이용한 탐색에 idxmax()함수를 적용하면 평균 'beer_servings'가 가장 높은 대륙이 어디인지 찾을 수 있다.idxmax()는 시리즈 객체에서 값이 가장 큰 index를 반환하는 기능을 수행 한다.
# 평균 beer_servings가 가장 높은 대륙을 구한다.
beer_continent = drinks.groupby('continent').beer_servings.mean().idxmax()
print(beer_continent)
EU
위의 두가지 질문에 대한 탐색을 통해 우리는 AF, AS, EU, OC, OT 대륙 중 EU,OT,AS만이 평균보다 많이 알코올을 섭취하는 대륙인 것을 알 수 있었고, 또한 맥주를 가장 좋아하는 대륙은 유럽이라는 결과를 확인할 수 있었다.
시각화¶
아래 코드를 니 결과를 그래프로 시각하 한 것이다. 부분별로 실행하며 막대 그래프의 사용법을 익혀보자.
- 분석 결과를 시각화하기
# 대륙별 spirit_servings의 평균, 최소, 최대, 합계를 시각화합니다.
n_groups = len(result.index)
means = result['mean'].tolist()
mins = result['min'].tolist()
maxs = result['max'].tolist()
sums = result['sum'].tolist()
index = np.arange(n_groups)
bar_width = 0.1
rects1 = plt.bar(index, means, bar_width,
color='r',
label='Mean')
rects2 = plt.bar(index + bar_width, mins, bar_width,
color='g',
label='Min')
rects3 = plt.bar(index + bar_width * 2, maxs, bar_width,
color='b',
label='Max')
rects3 = plt.bar(index + bar_width * 3, sums, bar_width,
color='y',
label='Sum')
plt.xticks(index, result.index.tolist())
plt.legend()
plt.show()
# 대륙별 total_litres_of_pure_clcohol을 시각화 한다.
continents = continent_mean.index.tolist()
continents.append('mean')
x_pos = np.arange(len(continents))
alcohol = continent_mean.tolist()
alcohol.append(total_mean)
bar_list = plt.bar(x_pos, alcohol,align='center', alpha=0.5)
bar_list[len(continents)-1].set_color('r')
plt.plot([0.,6], [total_mean, total_mean], "k--")
plt.xticks(x_pos, continents)
plt.ylabel('total_litres_of_pure_alcohol')
plt.title('total_litres_of_pure_alcohol by Continent')
plt.show()
# 대륙별 beer_servings를 시각화 하기
beer_group = drinks.groupby('continent')['beer_servings'].sum()
continents = beer_group.index.tolist()
y_pos = np.arange(len(continents))
alcohol = beer_group.tolist()
bar_list = plt.bar(y_pos, alcohol, align='center', alpha=0.5)
bar_list[continents.index("EU")].set_color('r')
plt.xticks(y_pos, continents)
plt.ylabel('beer_servings')
plt.title('beer_servings by Continent')
plt.show()
Step4 통계적 분석: 분석 대상 간의 통계적 차이 검정하기¶
지금까지는 두 피처 간의 상관성을 계산하거나 혹은 그룹 단위로 나누어 수치 정보를 살펴보는 방식의 데이터 분석을 알아보았다. 이러한 분석은 데이터에서 인사이트를 발견하는 데 있어서 매우 유용한 방법이다. 하지만 이는 분석가의 주관에 따라 도출된 내용이기 때문에 분석 자체의 타당성을 증명하기에는 한계가 있다. 따라서 분석 결과에 타당성을 부여하기 위해 통계적으로 차이를 검정하는 과정이 필요하다. 그 중 가장 기본적인 방법인 t-test를 통해 분석 간에 통계적 차이를 검정하는 방법을 알아보자.
t-test란 두 집단 간 평균 차이에 대한 검정 방법으로, 모집단의 평균 등과 같이 실제 정보를 모를 때 현재의 데이터만으로 두 집단의 차이에 대해 검정 할 수 있는 방법이다. 단, t-test는 검정 대상인 두 집단의 데이터 개수가 비슷하면서 두 데이터가 정규 분포를 보이는 경우에 신뢰도가 높은 검정 방식이다.
파이썬에서는 scipy라는 라이브러리를 활용하여 두 집단 간의 t-test를 검정할 수 있다. 아래 코드는 t-test를 적용하는 아주 간단한 예제이다.ttest_ind(0 함수에 두 집단의 시리즈 데이터를 넣은 것으로 검정의 결과를 확인할 수 있는데, 이 함수의 파라미터인 equal_var는 t-test의 두가지 방법 중에 하나를 선택하는 것이다. 첫 번째는 두 집단의 분산이 같은 경우, 그리고 두 번째는 두집단의 분산이 같지 않은 경우를 가정한 것이다.
- 아프리카와 유럽 간의 맥주 소비량 차이 검정하기
# 아프리카와 유럽 간의 맥주 소비량 차이를 검정한다.
africa = drinks.loc[drinks['continent'] =='AF']
europe = drinks.loc[drinks['continent'] =='EU']
from scipy import stats
tTestResult = stats.ttest_ind(africa['beer_servings'], europe['beer_servings'])
tTestResultDiffVar = stats.ttest_ind(africa['beer_servings'],
europe['beer_servings'], equal_var=False)
print("The t-statistic and p-value assuming equal variances is %.3f and %.3f."
% tTestResult)
print("The t-statistic and p-value not assuming equal variances is %.3f and %.3f"
% tTestResultDiffVar)
The t-statistic and p-value assuming equal variances is -7.268 and 0.000. The t-statistic and p-value not assuming equal variances is -7.144 and 0.000
실행 결과에 등장하는 t-statistic은 t-test의 검정 통계량을 의미하는 것으로, 함께 출력되는 p-value와 연관 지어 해석해야 한다. p-value는 가설이 얼마나 믿을만한 것인지를 나타내는 지료로, 데이터를 새로 샘플링 했을 떼 귀무 가설이 맞다는 전제 하에 현재 나온 통계값 이상이 나올 확률이라고 정의할 수 있다. 만약 p-value가 너무 낮으면 귀무 가설이 일어날 확률이 너무 낮기 때문에 귀무 가설을 기각하게 된다. 보통 그 기준은 0.05나 0.01을 기준으로 하며, 이를 p-value(유의확률)이라고 한다. 귀무 가설이란 처음부터 버릴 것을 예상하는 가설이며, 가설이 맞지 않다는 것을 증명하기 위해 수립하는 가설이다. 그리고 반대되는 것을 대립 가설이라고 부르며, 귀무 가설이 거짓인 경우에 대안적으로 참이 되는 가설의 의미 한다. 위의 실행 결과에서는 등분산을 가정한 케이스와 가정하지 않은 케이스 모두 p-value가 0.000 이하로 나타났다. t-test의 귀무 가설은 '두 집단의 평균이 같다'이고 이 귀무 가설은 기각되었다. 따라서 '아프리카와 유럽 대륙 간의 맥주 소비량 차이'는 통계적으로 유의미하다는 결론을 내릴 수 있다. 이는 대립 가설인 '두 집단의 평균이 다르다'를 채택한 것이다. 하지만 두 집단의 데이터는 크기도 다르고, 각각의 크기 또한 크지 않기 때문에 실제로 통계적인 의미를 가지는 검정이라고 보기엔 어렵습니다. 만약 통계적 분석을 하기에 좋은 조건인 경우라면 이러한 방법을 사용할 수 있다는 것만 알아둡시다. 드디어 이번 절의 처음에 나왔던 주제인 '대한민국은 얼마나 독하게 술을 마시는 나라일까?'에 대한 탐색 코드를 살펴볼 차례이다. 이를 판단하는 기준으로, alcohol_rate 피처를 생성한다. 이 피처는 total_litres_of_pure_alcohol 피처를 모든 술의 총 소비량 으로 나눈 것이다. 그리고 alcohol_rate는 sort_values()함수를 사용하여 국가를 기준으로 정렬한다.
- 대한민국은 얼마나 술을 독하게 마시는 나라일까? 에 대한 탐색 코드 살펴보자
# total_servings 피처를 생성한다.
drinks['total_servings'] = drinks['beer_servings'] + drinks['wine_servings'] + drinks['spirit_servings']
# 술 소비량 대비 알코올 비율 피처를 생성한다.
drinks['alcohol_rate'] = drinks['total_litres_of_pure_alcohol'] / drinks['total_servings']
drinks['alcohol_rate'] = drinks['alcohol_rate'].fillna(0)
# 술 순위 정보를 생성한다.
country_with_rank = drinks[['country', 'alcohol_rate']]
country_with_rank = country_with_rank.sort_values(by=['alcohol_rate'], ascending=0)
country_with_rank.head(5)
country | alcohol_rate | |
---|---|---|
63 | Gambia | 0.266667 |
153 | Sierra Leone | 0.223333 |
124 | Nigeria | 0.185714 |
179 | Uganda | 0.153704 |
142 | Rwanda | 0.151111 |
이 결과를 토대로 시각화를 수행한 결과는 다음과 같다. x_pos = np.arange(len(country_list))를 통해 그래프의 x축에 해당하는 범위를 생성하고, rank = country_with_rank.alcohol_rate.tolist()를 통해 alcohol_rate순으로 정렬된 데이터에서 alcohol_rate 값을 생성하였다.
- 국가별 순위 정보를 시각화 하기
# 국가별 순위 정보를 그래프로 시각화 한다.
country_list = country_with_rank.country.tolist()
x_pos = np.arange(len(country_list))
rank = country_with_rank.alcohol_rate.tolist()
bar_list = plt.bar(x_pos, rank)
bar_list[country_list.index("South Korea")].set_color('r')
plt.ylabel('alcohol rate')
plt.title('liquor drink rank by contry')
plt.axis([0, 200, 0, 0.3])
korea_rank = country_list.index("South Korea")
korea_alc_rate = country_with_rank[country_with_rank['country'] == 'South Korea']['alcohol_rate'].values[0]
plt.annotate('South Korea : '+ str(korea_rank +1 ),
xy=(korea_rank, korea_alc_rate),
xytext=(korea_rank + 10, korea_alc_rate + 0.05),
arrowprops = dict(facecolor='red', shrink=0.05))
plt.show()
- 출처 : 이것이 데이터 분석이다 with 파이썬
'이것이 데이터분석 이다 with 파이썬' 카테고리의 다른 글
Chapter3.3_Movie (2) | 2021.05.03 |
---|---|
Google Analytics 4 _ tistory 연계 방법 (0) | 2021.04.27 |
Chapter 3.1 프로야구 선수의 다음 해 연봉 예측하기 (0) | 2021.04.24 |
Chapter 3.2 비트코인 시세 예측하기 again 2018 (1) | 2021.04.21 |
Chapter 4 . titanic 생존자 가려내기 (0) | 2021.04.19 |