현업에서 데이터가 주어졌을 때, 이 데이터를 가공하고 분석해서 목표 달성을 위한 의사 결정에 도움이 되는 다양한 정보들을 찾아낼 수가 있다.
본 문서는 이러한 정보를 찾기 위해서 처음부터 어떻게 하면 되는지 쇼핑몰 고객 주문 데이터를 예로 들어 설명한다.
쇼핑몰 고객 주문 데이터 분석의 목표는 다음과 같다.
먼저 쇼핑몰 고객 주문 데이터를 pandas의 read_csv() 함수등을 통해서 읽어서 retail이라는 DataFrame을 만든다.
DataFrame의 head(), info(), describe()를 사용한다.
retail.head()로 전체 구조를 살펴본다.
retail.columns
Index(['InvoiceNo', 'StockCode', 'Description', 'Quantity', 'InvoiceDate',
'UnitPrice', 'CustomerID', 'Country'],
dtype='object')
각 column의 의미를 파악한다.
retail.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 541909 entries, 0 to 541908
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 InvoiceNo 541909 non-null object
1 StockCode 541909 non-null object
2 Description 540455 non-null object
3 Quantity 541909 non-null int64
4 InvoiceDate 541909 non-null object
5 UnitPrice 541909 non-null float64
6 CustomerID 406829 non-null float64
7 Country 541909 non-null object
dtypes: float64(2), int64(1), object(5)
memory usage: 33.1+ MB
전체 541909개의 item이 있는데, 갯수를 잘 보면 Description열과 CustomerID열에 일부가 비어있는 것을 볼 수 있다.
Description은 몰라도 CustomerID는 때에 따라 key값으로도 쓰일 수 있기 때문에 값이 비어 있으면 안된다. CustomerID가 비어있는 데이터는 분석을 할 수 없으므로 해당 row는 제거하도록 한다.
retail.describe()
Quantity와 UnitPrice의 min값이 음수가 있다. 갯수와 가격에 음수는 말이 안되는 상황이므로 이것도 이후에 찾아서 해당 row를 제거해야 한다.
데이터 정제(cleansing)는 읽어들인 데이터에서 불필요한 부분을 찾아서 모두 제거하고 분석에 필요한 부분만 남기는 것을 말한다.
retail.isnull().sum()
InvoiceNo 0
StockCode 0
Description 1454
Quantity 0
InvoiceDate 0
UnitPrice 0
CustomerID 135080
Country 0
dtype: int64
CustomerID가 null인 행(row)이 135080개나 있는데 이 행을 모두 제거한다.
retail = retail[pd.notnull(retail['CustomerID'])]
len(retail)
406829
전체 541909개의 행에서 406829개의 행만 남은 것을 알 수 있다.
'Quantity'와 'UnitPrice'열에서 값이 음수인 데이터는 잘못된 것이므로 해당 row도 제거한다.
retail = retail[retail['Quantity'] > 0]
retail = retail[retail['UnitPrice'] > 0]
len(retail)
397884
올바른 데이터 타입 적용과 메모리 효율을 위해 데이터 타입을 변경한다.
retail['CustomerID'] = retail['CustomerID'].astype(np.int32)
retail['CheckoutPrice'] = retail['UnitPrice'] * retail['Quantity']
retail.head()
retail.to_csv('../Data/OnlineRetailClean.csv')
다양한 기준에 맞춰 매출액 상위권의 정보를 확인한다.
날짜를 다양한 기준(연,월,시,분...)에서 비교하려면 문자열 형식으로 된 날짜를 pandas의 datetime형식으로 바꾼다.
retail['InvoiceDate'] = pd.to_datetime(retail['InvoiceDate'], infer_datetime_format = True)
retail.info()
...
5 InvoiceDate 397884 non-null datetime64[ns]
...
InvoiceDate열의 데이터 형식이 문자열에서 datetime형식으로 바뀐것을 볼 수 있다.
전체 매출
total_revenue = retail['CheckoutPrice'].sum()
total_revenue
8911407.904
국가별 매출
rev_by_countries = retail.groupby('Country').sum()['CheckoutPrice'].sort_values()
rev_by_countries
Country
Saudi Arabia 1.459200e+02
Bahrain 5.484000e+02
Czech Republic 8.267400e+02
RSA 1.002310e+03
Brazil 1.143600e+03
European Community 1.300250e+03
Lithuania 1.661060e+03
Lebanon 1.693880e+03
United Arab Emirates 1.902280e+03
Unspecified 2.667070e+03
Malta 2.725590e+03
USA 3.580390e+03
Canada 3.666380e+03
Iceland 4.310000e+03
Greece 4.760520e+03
Israel 7.221690e+03
Poland 7.334650e+03
Austria 1.019868e+04
Cyprus 1.359038e+04
Italy 1.748324e+04
Denmark 1.895534e+04
Channel Islands 2.045044e+04
Singapore 2.127929e+04
Finland 2.254608e+04
Portugal 3.343989e+04
Norway 3.616544e+04
Japan 3.741637e+04
Sweden 3.837833e+04
Belgium 4.119634e+04
Switzerland 5.644395e+04
Spain 6.157711e+04
Australia 1.385213e+05
France 2.090240e+05
Germany 2.288671e+05
EIRE 2.655459e+05
Netherlands 2.854463e+05
United Kingdom 7.308392e+06
Name: CheckoutPrice, dtype: float64
영국(UK)가 가장 매출이 많음을 알 수 있다.
국가별 매출을 그래프로 표기
plot = rev_by_countries.plot(kind='bar', color=COLORS[-1], figsize=(20, 10))
plot.set_xlabel('Country', fontsize=11)
plot.set_ylabel('Revenue', fontsize=11)
plot.set_title('Revenue by Country', fontsize=13)
plot.set_xticklabels(labels=rev_by_countries.index, rotation=45) # x축 눈금을 rev_by_countries의 index(국가명)를 사용한다.
국가별 매출을 전체 매출액 대비 비율로 표시
rev_by_countries / total_revenue
Country
Saudi Arabia 0.000016
Bahrain 0.000062
Czech Republic 0.000093
RSA 0.000112
Brazil 0.000128
European Community 0.000146
Lithuania 0.000186
Lebanon 0.000190
United Arab Emirates 0.000213
Unspecified 0.000299
Malta 0.000306
USA 0.000402
Canada 0.000411
Iceland 0.000484
Greece 0.000534
Israel 0.000810
Poland 0.000823
Austria 0.001144
Cyprus 0.001525
Italy 0.001962
Denmark 0.002127
Channel Islands 0.002295
Singapore 0.002388
Finland 0.002530
Portugal 0.003752
Norway 0.004058
Japan 0.004199
Sweden 0.004307
Belgium 0.004623
Switzerland 0.006334
Spain 0.006910
Australia 0.015544
France 0.023456
Germany 0.025682
EIRE 0.029798
Netherlands 0.032032
United Kingdom 0.820116
Name: CheckoutPrice, dtype: float64
영국이 전체 매출의 82%를 차지하고 있어 사실상 영국에만 사업을 집중하고 있음을 알 수 있다.
그래프 출력을 함수로 표현
def plot_bar(df, xlabel, ylabel, title, color=COLORS[0], figsize=(20,10), rotation=45):
plot = df.plot(kind='bar', color=color, figsize=figsize)
plot.set_xlabel(xlabel, fontsize=11)
plot.set_ylabel(ylabel, fontsize=11)
plot.set_title(title, fontsize=13)
plot.set_xticklabels(labels=df.index, rotation=rotation)
plot_bar(rev_by_countries, 'Country', 'Revenue', 'Revenue by Country')
앞과 동일한 결과가 나온다.
def extract_month(date):
month = str(date.month)
if date.month < 10:
month = '0' + month
return str(date.year) + month
rev_by_month = retail.set_index('InvoiceDate').groupby(extract_month).sum()['CheckoutPrice']
plot_bar(rev_by_month, 'Month', 'Revenue', 'Revenue by Month')
매출이 거의 10, 11월에 몰려있다. 12월의 메출은 11월 매출의 반토막이다. 12월 매출이 적은 것은 데이터 수집할 때 마지막 12월의 매출 데이터 전체가 수집이 안되었기 때문이다.
월별 매출 분석을 약간 응용하면 된다.
rev_by_dow = retail.set_index('InvoiceDate').groupby(lambda date : date.dayofweek).sum()['CheckoutPrice']
DAY_OF_WEEK = np.array(['Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun']) ## 일반 리스트로는 처리 못하고 numpy의 ndarray로 바꿔 주어야 한다.
rev_by_dow.index = DAY_OF_WEEK[rev_by_dow.index] ## 요일이 0~6으로 표현되어 이를 문자열로 변경하는 작업
plot_bar(rev_by_dow, 'Week', 'Revenue', 'Revenue by Week')
목요일까지는 완만히 매출이 증가하다가 목요일 이후부터 매출이 감소하는 것을 알 수 있다.
rev_by_hour = retail.set_index('InvoiceDate').groupby(lambda date : date.hour).sum()['CheckoutPrice']
plot_bar(rev_by_hour, 'Hour', 'Revenue', 'Revenue by Hour')
7시를 시작으로 주문이 시작되어 12시까지 증가세, 15시까지 하락을, 15시 이후 부터 급락하는 것을 알 수 있다.
매출 분석으로부터 얻을 수 있는 결론
Top 3 판매 제품
top_selling = retail.groupby('StockCode').sum()['Quantity'].sort_values(ascending=False)[:3]
top_selling
StockCode
23843 80995
23166 77916
84077 54415
Name: Quantity, dtype: int32
Top 10 매출 제품
top_revenue = retail.groupby('StockCode').sum()['CheckoutPrice'].sort_values(ascending=False)[:10]
top_revenue
StockCode
23843 168469.60
22423 142592.95
85123A 100603.50
85099B 85220.78
23166 81416.73
POST 77803.96
47566 68844.33
84879 56580.34
M 53779.93
23084 51346.20
Name: CheckoutPrice, dtype: float64
monthly_top3 = retail.set_index('InvoiceDate').groupby(['StockCode', extract_month]).sum()[['Quantity', 'CheckoutPrice']].loc[top_selling.index]
plot_bar(monthly_top3['CheckoutPrice'], 'Product/Month', 'Revenue', 'Revenue of Top3 items')
구매 횟수별 우수 고객
retail.groupby('CustomerID').count()['Quantity'].sort_values(ascending=False)
지불 금액별 우수 고객
retail.groupby('CustomerID').sum()['CheckoutPrice'].sort_values(ascending=False)
먼저 'InvoiceDate' column을 연-월로 단순화 시킨 column('Month' column)을 추가한다.
def get_month_as_datetime(date):
return datetime(date.year, date.month, 1) # year, month, day
# invoice date field에서 연도와 월만 빼내어 'Month'열에 저장
retail['Month'] = retail['InvoiceDate'].apply(get_month_as_datetime)
'Month' column으로부터 사용자별 최초 구매월을 찾는다.
# 각 사용자당 구매월들 가운데서 최초 구매월을 찾아서 리턴한다.
month_group = retail.groupby('CustomerID')['Month']
retail['MonthStarted'] = month_group.transform(np.min)
retail['MonthPassed'] = (retail['Month'].dt.year - retail['MonthStarted'].dt.year) * 12 + \
(retail['Month'].dt.month - retail['MonthStarted'].dt.month)
def get_unique_no(x):
return len(np.unique(x))
# 최초 구매월부터 한달씩 지날때마다
cohort_group = retail.groupby(['MonthStarted', 'MonthPassed'])
# 해당 월에 구매한 구매자들의 수를 집계한다.
cohort_df = cohort_group['CustomerID'].apply(get_unique_no).reset_index()
'MonthStarted' 열을 pivot으로 하여 pivot 열에서 중복되는 항목들은 모두 열로 보낸다.
cohort_df = cohort_df.pivot(index='MonthStarted', columns='MonthPassed')
백분율로 다시 환산한다.
customer_cohort = cohort_df.div(cohort_df.iloc[:, 0], axis=0) * 100
customer_cohort = customer_cohort.round(decimals=2)
heatmap으로 출력
xticks = np.arange(0, 13)
yticks = ['2010/12', '2011/01', '2011/02', '2011/03', '2011/04', '2011/05', '2011/06', '2011/07', '2011/08', '2011/09', '2011/10', '2011/11', '2011/12']
plt.figure(figsize = (15, 8))
sns.heatmap(customer_cohort,
annot=True,
xticklabels=xticks,
yticklabels=yticks,
fmt='.1f')
order_by_hour = retail.set_index('InvoiceDate').groupby(lambda date : date.hour).count()['CustomerID']
plot_bar(order_by_hour, 'hour', '# orders', 'Order by hour')
12시(정오)에서 가장 주문이 많을 것을 알 수 있다.
30분 단위로 파악
def half_an_hour(date):
minute = ':00'
if date.minute > 30:
minute = ':30'
hour = str(date.hour)
if date.hour < 10 :
hour = '0' + hour
return hour + minute
order_by_hour_half = retail.set_index('InvoiceDate').groupby(half_an_hour).count()['CustomerID']
plot_bar(order_by_hour_half, 'half an hour', '# orders', 'Order by half an hour')
12시~12시30분 사이가 가장 주문이 많을 것을 알 수 있다.
order_count_by_hour = retail.set_index('InvoiceDate').groupby(['CustomerID', lambda date : date.hour]).count()['StockCode']
order_count_by_hour
CustomerID
12346 10 1
12347 8 22
10 24
12 47
13 18
..
18283 15 1
16 56
19 87
18287 9 3
10 67
Name: StockCode, Length: 11205, dtype: int64
각 사용자별로 시간대별 주문량이 계산되어 나온다.
12347 사용자의 경우만 살펴보면 위 출력에서는 짤렸지만 14시에 주문량이 60으로 가장 많을 것을 알 수 있다.
idx = order_count_by_hour.groupby('CustomerID').idxmax()
idx
CustomerID
12346 (12346, 10)
12347 (12347, 14)
12348 (12348, 19)
12349 (12349, 9)
12350 (12350, 16)
...
18280 (18280, 9)
18281 (18281, 10)
18282 (18282, 13)
18283 (18283, 14)
18287 (18287, 10)
Name: StockCode, Length: 4338, dtype: object
사용자별 시간대별 주문량에서 사용자별로 그룹을 만들면서 최대 주문이 있는 시간으로 중복 행(row)들을 연산한다. DataFrame의 idxmax는 가장 큰 값이 있는 행의 index를 리턴한다. 여기서의 index는 사용자와 시간의 조합이 된다.
예를 들어 12347 사용자(CustomerID)의 경우는 14시가 가장 주문이 많은 시간대이다.
5.3.2의 index를 5.3.1에 적용한다.
result = order_count_by_hour.loc[idx]
result.reset_index()
앞의 5.3.2의 결과에 시간대별로 그룹핑을 한다.
그러면 시간대별로 가장 주문이 많은 사용자들이 집계가 되는데 여기에는 'CustomerID', 'StockCode'가 있는 행의 index가 집계가 된다.
여기서 다시 'CustomerID'만 뽑아야 한다.
result.reset_index().groupby('level_1').groups
결과는 시간대별로 그룹원들의 index list가 반환된다.
예를 들어 10시의 groups 값을 보면,
idxs = result.reset_index().groupby('level_1').groups[10]
idxs
Int64Index([ 0, 11, 21, 27, 28, 41, 42, 45, 49, 51,
...
4290, 4292, 4294, 4296, 4297, 4319, 4322, 4330, 4334, 4337],
dtype='int64', length=510)
10시에 510개의 건이 있는데 각각의 행의 index번호가 0, 11, 21, ..., 4334, 4337이다.(이 행에 'CustomerID', 'StockCode'가 있다.)
여기에 'CustomerID'만 따로 뽑으려면,
result.reset_index().loc[idxs]['CustomerID'].reset_index().drop(['index'], axis=1)
이렇게 하면 오전 10시에 가장 많은 주문을 한 고객의 리스트가 나오게 된다.
해당 데이터셋을 따라서 직접 코딩해 보고 있는데요, 국가별 매출 분석에서
rev_by_countries = retail.groupby('Country').sum()['CheckoutPrice'].sort_values()
rev_by_countries
이 코드를 실행할 때 계속 *표시가 뜨고 Interrupt kernel도 안 먹혀요. 코드를 수정하면 해결될 거 같은데 수정해 주실 수 있나요?