- Github: yoonkt200
5.2 구매 데이터를 분석하여 상품 추천하기¶
이번 절에서는 구매 데이터 분석에 기반한 온라인 스토어 상품 추천 시뮬레이션 예제를 알아보겠다. 피처 엔지니어링, 그리고 행렬 완성 기반 점수 예측방법을 이용하여 상품 추천 시뮬레이션을 수행합니다. 분석에 사용할 'Uk Retail'데이터는 영국의 한 선물 판매 온라인 스토어에서 발생한 거래 데이터로, 주 고객은 선물 도매상입니다.
Step1 탐색적 분석:UK Retail 데이터 분석하기¶
예제에서 사용할 UK Retail 데이터셋은 다음과 같은 피처로 구성되어 있다.
- InvoiceNo : 거래 고유 번호
- StockCode : 상품 고유 번호
- Description : 상품명
- Quantity : 거래 수량
- InvoiceDate : 거래 일시
- UnitPrice : 상품 단가
- CustomerID : 구매자 고유 번호
- Country : 구매 국가
아래의 코드를 통해 데이터를 살펴본 결과, 약 54만 개 정도의 데이터가 존재하며 그 중 14만 개의 데이터는 구매자 정봐 결측값인 것을 알 수 있다.
- 데이터셋 살펴보기
# -*- coding:utf-8 -*-
%matplotlib inline
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
# 영국 온라인 스토어 도매 거래 데이터
df = pd.read_csv("./data/online_retail.csv", dtype = {'CustomerID': str, 'InvoiceID': str}, encoding="ISO-8859-1")
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'], format = "%m/%d/%Y %H:%M")
print(df.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 datetime64[ns] 5 UnitPrice 541909 non-null float64 6 CustomerID 406829 non-null object 7 Country 541909 non-null object dtypes: datetime64[ns](1), float64(1), int64(1), object(5) memory usage: 33.1+ MB None
df.head()
InvoiceNo | StockCode | Description | Quantity | InvoiceDate | UnitPrice | CustomerID | Country | |
---|---|---|---|---|---|---|---|---|
0 | 536365 | 85123A | WHITE HANGING HEART T-LIGHT HOLDER | 6 | 2010-12-01 08:26:00 | 2.55 | 17850 | United Kingdom |
1 | 536365 | 71053 | WHITE METAL LANTERN | 6 | 2010-12-01 08:26:00 | 3.39 | 17850 | United Kingdom |
2 | 536365 | 84406B | CREAM CUPID HEARTS COAT HANGER | 8 | 2010-12-01 08:26:00 | 2.75 | 17850 | United Kingdom |
3 | 536365 | 84029G | KNITTED UNION FLAG HOT WATER BOTTLE | 6 | 2010-12-01 08:26:00 | 3.39 | 17850 | United Kingdom |
4 | 536365 | 84029E | RED WOOLLY HOTTIE WHITE HEART. | 6 | 2010-12-01 08:26:00 | 3.39 | 17850 | United Kingdom |
본격적인 탐색적 데이터 분석에 앞서, 데이터에서 예외적인 상황을 필터링하여 이상치를 제거해야 한다. 가장 먼저 결측 데이터를 제거하겠다.다음 코드와 실행 결과는 유저 정보가 없는 13만5천여 개의 데이터, 상품 상세정보가 없는 1,500여 개의 데이터를 제거한 것이다.
- 결측 데이터 제거하기
df.isnull().sum()
InvoiceNo 0 StockCode 0 Description 1454 Quantity 0 InvoiceDate 0 UnitPrice 0 CustomerID 135080 Country 0 dtype: int64
df = df.dropna()
print(df.shape)
(406829, 8)
다음은 데이터가 일반적이지 않은 경우를 탐색하고 이를 제거하겠다. 이번에는 상품 수량 데이터가 이상한 경우를 탐색한다. 아래의 코드는 상품 수량이 0 이하인 경우 해당 값을 데이터 프레임에서 제거하는 과정이다. 이러한 경우는 아마도 환불이나 주문 취소를 의미하는 것 같지만 그 의미가 명확하지 않으니 제거한다. 코드의 실행 결과, 약 9,000여 개의 데이터가 제거되었다.
- 탐색 데이터의 조건 필터링: 상품 수량이 0이하인 경우
# 상품 수량이 음수인 경우를 제거한다.
print(df[df['Quantity'] <=0].shape[0])
df = df[df['Quantity'] > 0]
8905
이번에는 상품 가격이 0 이하인 경우를 탐색한다. 이 역시 일반적인 상황이라고 할 수 없는 상황이기 때문에 조건에 해당하는 데이터를 제거한다. 아래 코드의 실행 결과, 총 40개의 데이터가 제거되었다.
- 탐색 데이터의 조건 필터링: 상품가격이 0 이하인 경우
# 상품 가격이 0 이하인 경우를 제거한다.
print(df[df['UnitPrice'] <=0].shape[0])
df = df[df['UnitPrice']>0]
40
마지막으로 상품 코드가 이상한 경우를 탐색하고 제거한다. 데이터 내의 StockCode를 관찰해보면 대부분의 상품 코드가 번호로 이루어져 있는 것을 알 수 있다. 따라서 상품 코드가 번호가 아닌 경우는 예외적인 상황일 것이다. 아래 실핼 결과는 이러한 데이터를 살펴본 것이다.
- 탐색 데이터의 조건 필터링: 상품 코드가 일반적이지 않은 경우
# 상품 코드가 일반적이지 않은 경우를 탐색한다.
df['ContainDigit'] = df['StockCode'].apply(lambda x: any(c.isdigit() for c in x))
print(df[df['ContainDigit'] == False].shape[0])
1414
df[df['ContainDigit'] == False].head()
InvoiceNo | StockCode | Description | Quantity | InvoiceDate | UnitPrice | CustomerID | Country | ContainDigit | |
---|---|---|---|---|---|---|---|---|---|
45 | 536370 | POST | POSTAGE | 3 | 2010-12-01 08:45:00 | 18.00 | 12583 | France | False |
386 | 536403 | POST | POSTAGE | 1 | 2010-12-01 11:27:00 | 15.00 | 12791 | Netherlands | False |
1123 | 536527 | POST | POSTAGE | 1 | 2010-12-01 13:04:00 | 18.00 | 12662 | Germany | False |
2239 | 536569 | M | Manual | 1 | 2010-12-01 15:35:00 | 1.25 | 16274 | United Kingdom | False |
2250 | 536569 | M | Manual | 1 | 2010-12-01 15:35:00 | 18.95 | 16274 | United Kingdom | False |
그리고 아래의 코드를 실행하여 일반적이지 않은 상품 코드를 가진 데이터를 제거하자.
# 상품 코드가 일반적이지 않은 경우 제거
df = df[df['ContainDigit'] == True]
이제 본격적으로 탐색적 분석을 수행할 차례이다. 어떤 방향성을 가지고 탐색적 분석을 진행해야 할까? 분석 방향을 잘 설정하기 위해 지금부터 우리는 특정 시점에 도매상들에게 선물을 판매하는 온라인 스토어의 운영자 입장이 되어보자. 그리고 우리의 고민은 다음과 같다. '연말에 온라인 스토어에 방문하는 유저들에게 어떤 상품을 추천해줄 수 있을까?' 연말에 방문한 유저들에게 상품을 추천해준다는 것은 유저-상품 간의 구매 확률을 예측해보는 시뮬레이션이라고 할 수 있다. 아래와 같은 분석 과정이 필요하다.
- 연말 이전까지의 데이터를 유저-상품 간의 구매를 예측하는 모델의 학습 데이터셋으로 사용한다.
- 실제 연말에 구매한 유저-상품 간의 정보를 테스트 데이터셋으로 사용한다.
- 모델이 예측한 유저-상품 간의 구매 정보와 실제 구매 정보(테스트 데이터셋)을 비교하여 추천이 잘 되었는지 평가한다.
EDA (탐색적분석)이 필요하다. 필요한 탐색은 특정 시간을 기준으로 데이터를 나누고, 데이터에서 구매 패턴과 같은 특징을 발견하는 것이다. 따라서 가장 먼저 할 것은 일자별 주문의 탐색이다. 다음의은 가장 오래된 데이터와 가장 최신의 데이터를 출력한 것이며 이를 통해 데이터가 2010년 12월부터 2011년 12월까지 존재하는 것을 알 수 있다.
- 데이터의 기간 탐색하기
# 거래 데이터에서 가장 오래된 데이터와 가장 최신의 데이터를 탐색한다.
df['date'] = df['InvoiceDate'].dt.date
print(df['date'].min())
print(df['date'].max())
2010-12-01 2011-12-09
다음으로 일자별 거래랑을 탐색한다. 코드에서는 일자를 나타내는 date 피처를 그룹의 기준으로 하여, 일자별 Qauntity의 합계를 계산한다. 그래프를 살펴보면 대체적으로 연말에 가까워질수록 거래량이 증가하는 것을 알 수 있으며 10~11월 정도를 기점으로 증가폭이 조금씩 커지고 있다는 것을 알 수 있다.
- 일자별 거래 수량 탐색하기
# 일자별 총 거래 수량을 탐색한다
date_quantity_series = df.groupby('date')['Quantity'].sum()
date_quantity_series.plot()
<AxesSubplot:xlabel='date'>
다음은 일자별 거래 횟수를 탐색한다. 아래의 코드도 마찬가지로 date 피처를 그룹의 기준으로 하였고, nunique()함수를 InvoiceNo 피처에 적용하여 일자별로 발생한 거래 횟수를 계산한다. 코드의 실행 결과는 일자별 거래량을 시계열 그래프로 나타낸것이다. 거래 횟수는 연말에 가까워질수록 거래 수량보다 조금 더 가파르게 상승하고 있다.
- 일자별 거래 횟수 탐색하기
# 일자별 총 거래 횟수를 탐색
date_transaction_series = df.groupby('date')['InvoiceNo'].nunique()
date_transaction_series.plot()
<AxesSubplot:xlabel='date'>
마지막으로 일자별 거래 상품 개수를 탐색한다. 지금까지의 내용은 종합해보면 연말이 시작되는 약 10~11얼 정도부터 연중보다 더 많이 그리고 더 자주 구매가 일어난다는 것을 알 수 있다.
- 일자별 거래 상품 개수 탐색하기
# 일자별 거래된 상품의 unique한 개수, 즉 상품 거래 다양성을 탐색한다.
date_unique_item_series = df.groupby('date')['StockCode'].nunique()
date_unique_item_series.plot()
<AxesSubplot:xlabel='date'>
이번에는 전체 데이터에서 등장한 유저들의 구매 패턴을 탐색해 보자 이를 통해 총 4,334명의 유저가 데이터에 존재하는 것을 알 수 있다.
- 전체 유저의 수 탐색하기
# 총 유저의 수를 계산하여 출력한다.
print(len(df['CustomerID'].unique()))
4334
4,334명의 유저를 대상으로 각각의 거래 횟수를 탐색한다. 다음 코드는 CustomerID를 그룹으로 하여 unique한 InvoieNo를 계산한 것이고, 이 결과에 describe()함수를 적용하여 유저별 거래 횟수에 대한 요약 통계 정보를 출력했다. 출력 결과를 살펴보면 유저들은 평균적으로 약 4회 정도의 구매가 있었다는 것을 알 수 있고, 대부분의 유저는 1~5회 정도의 구매 횟수를 보인다는 것을 알 수 있다.
- 유저별 거래 횟수를 탐색한다.
# 유저별 거래 횟수 탐색
customer_unique_transaction_series = df.groupby('CustomerID')['InvoiceNo'].nunique()
customer_unique_transaction_series.describe()
count 4334.000000 mean 4.246654 std 7.642535 min 1.000000 25% 1.000000 50% 2.000000 75% 5.000000 max 206.000000 Name: InvoiceNo, dtype: float64
그리고 이를 상자 그림으로 살펴본 결과는 아래와 같다.
- 유저별 거래 횟수 시각화하기
# 상자 그림 시각화
plt.boxplot(customer_unique_transaction_series.values)
plt.show()
다음으로 유저별로 구매한 상품은 몇 종류나 되는지를 탐색해보자. 아래의 코드는 CustomerID 그룹에 unique한 StockCode를 계산하여 describe()함수를 적용한 것이다. 그리고 이를 통해 유저들은 평균적으로 약 60여 개 종류의 상품을 구매했다는 것을 알 수 있다. 하지만 데이터의 편차는 매우 높은 수치를 보이고 있다.
- 유저별 상품 구매 종류 탐색하기
# 유저별 상품 구매 종류 탐색
customer_unique_item_series = df.groupby('CustomerID')['StockCode'].nunique()
customer_unique_item_series.describe()
count 4334.000000 mean 61.432856 std 85.312937 min 1.000000 25% 16.000000 50% 35.000000 75% 77.000000 max 1786.000000 Name: StockCode, dtype: float64
마찬가지로 이 결과를 상자 그림으로 살펴보자. 거래 횟수보다는 조금 더 다양하게 데이터가 분포 되어있는 것을 알 수 있다.
- 유저별 상품 구매 종류 시각화 하기
# 상자 그림으로 시각화
plt.boxplot(customer_unique_item_series.values)
plt.show()
- `이번에는 유저가 아닌 상품을 기준으로 EDA를 실시해 보자'
- 아래의 내용들을 탐색적으로 분석하자
- 총 상품 갯수
- 가장 거래가 많은 상품 top 10 탐색
- 상품별 판매수량 분포 탐색
- 거래별 가격 탐색
- 아래의 내용들을 탐색적으로 분석하자
# 총 상품 개수 탐색
print(len(df['StockCode'].unique()))
3660
# 가장 거래가 많은 상품 top 10 탐색
df.groupby('StockCode')['InvoiceNo'].nunique().sort_values(ascending=False)[:10]
StockCode 85123A 1978 22423 1703 85099B 1600 47566 1379 84879 1375 20725 1289 22720 1146 23203 1080 20727 1052 22383 1043 Name: InvoiceNo, dtype: int64
# 상품별 판매수량 분포 탐색
print(df.groupby('StockCode')['Quantity'].sum().describe())
plt.plot(df.groupby('StockCode')['Quantity'].sum().values)
plt.show()
count 3660.000000 mean 1409.149727 std 3513.654056 min 1.000000 25% 65.000000 50% 395.000000 75% 1417.500000 max 80995.000000 Name: Quantity, dtype: float64
# 분포를 정렬하여 출력
plt.plot(df.groupby('StockCode')['Quantity'].sum().sort_values(ascending=False).values)
plt.show()
# 거래별 가격 탐색
df['amount'] = df['Quantity'] * df['UnitPrice']
df.groupby('InvoiceNo')['amount'].sum().describe()
count 18405.000000 mean 476.378845 std 1678.749892 min 0.380000 25% 157.900000 50% 302.360000 75% 465.700000 max 168469.600000 Name: amount, dtype: float64
# 거래별로 발생한 가격 분포 탐색
plt.plot(df.groupby('InvoiceNo')['amount'].sum().values)
plt.show()
# 분포를 정렬하여 출력한다.
plt.plot(df.groupby('InvoiceNo')['amount'].sum().sort_values(ascending=False).values)
plt.show()
이제 특정 시점을 기준으로 데이터를 분리하여 구매의 패턴을 분석해본다. 중심적으로 살펴볼 내용은 두 데이터에서 동일하게 등장하는 유저-상품 단위의 구매데이터, 즉 재구매 여부이다. 또한 신규 구매가 얼마나 일어났는지 역시 중요하게 살펴 볼 내용이다. 먼저 11월 1일을 연말의 기준으로 삼아 두 개의 데이터로 분리한다. 이 두 데이터는 추후에 예측 분석에 사용할 학습 데이터셋, 그리고 테스트용 데이터셋을 의미하며 각각 312,902개, 81,568개의 데이터로 분리 되어있다.
- 일자를 기준으로 데이터 분리하기
import datetime
# 2011년 11월을 기준으로 하여 기준 이전과 이후로 데이터를 분리한다.
df_year_round = df[df['date'] < datetime.date(2011, 11, 1)]
df_year_end = df[df['date'] > datetime.date(2011, 11, 1)]
print(df_year_round.shape)
print(df_year_end.shape)
(314902, 11) (79850, 11)
분리된 데이터에서 재구매, 신규 구매 등이 어떻게 일어났는지를 분석해 보자. 먼저 해야 하는 것은 11월 이전 데이터셋에서 유저별로 구매했던 상품의 리스트를 추출하는 것이다. 아래의 코드는 CustomerID를 그룹으로 하여 StockCode 피처에 apply(set) 함수를 적용한 것으로 이를 통해 유저별 StockCode의 집합(set)을 추출할 수 있다.
- 11월 이전 유저별로 구매했던 상품의 집합 추출하기
# 11월 이전 데이터에서 구매했던 상품의 set을 추출한다.
customer_item_round_set = df_year_round.groupby('CustomerID')['StockCode'].apply(set)
print(customer_item_round_set)
CustomerID 12346 {23166} 12347 {84625A, 22621, 23421, 23146, 22774, 22195, 21... 12348 {22951, 22437, 23076, 21725, 21977, 21726, 219... 12350 {22412, 20652, 79066K, 22348, 79191C, 84086C, ... 12352 {22550, 22801, 22722, 22779, 22630, 22780, 221... ... 18280 {82484, 22358, 22495, 22727, 22180, 22499, 226... 18281 {22028, 22716, 22037, 23209, 23008, 22467, 23007} 18282 {22089, 21108, 23295, 21109, 21270, 23187, 22424} 18283 {22756, 23247, 22661, 22910, 21874, 22962, 207... 18287 {22753, 22756, 22603, 72349B, 84920, 85040A, 2... Name: StockCode, Length: 3970, dtype: object
다음은 유저-상품 단위의 딕셔너리(사전)을 정의한다. 이 딕셔너리는 유저가 상품을 11월 이전에 구매했는지 혹은 11월 이후에 구매했는지를 기록하기 위한 것이다. 아래의 코드를 실행하면 유저가 11월 이전에 구매한 상품은 딕셔너리에 'old'라고 표기된다.
- 유저별 구매 사전 구축하기
# 11월 이전에 구매했는지 혹은 이후에 구매했는지를 유저별로 기록하기 위한 사전을 정의한다
customer_item_dict = {}
# 11월 이전에 구매한 상품은 'old'라고 표기한다.
for customer_id, stocks in customer_item_round_set.items():
customer_item_dict[customer_id] = {}
for stock_code in stocks:
customer_item_dict[customer_id][stock_code] = 'old'
print(str(customer_item_dict)[:100] + "...")
{'12346': {'23166': 'old'}, '12347': {'84625A': 'old', '22621': 'old', '23421': 'old', '23146': 'old...
이는 실행 결과를 보면 쉽게 이해할 수 있다. 12346번 유저는 23166 상품을 구매했었고, 12347번 유저는 20719.. 을 구매했다. 동일한 방식으로 11월 이후 데이터에서 유저별로 구매한 상품의 집합을 추출하자.
- 11월 이후 유저별로 구매했던 상품의 집합 추출하기
# 11월 이후 유저별로 구매했던 상품의 집합 추출하기
customer_item_end_set = df_year_end.groupby('CustomerID')['StockCode'].apply(set)
print(customer_item_end_set)
CustomerID 12347 {23508, 23497, 21064, 21265, 84625A, 23271, 23... 12349 {23497, 22722, 23113, 22441, 23460, 23283, 234... 12352 {22978, 22627, 21669, 22635, 22624, 23089, 226... 12356 {21843, 22423} 12357 {23247, 21507, 22315, 22969, 21874, 22716, 227... ... 18272 {22722, 23113, 22969, 22074, 22960, 22993, 229... 18273 {79302M} 18274 {22851, 21974, 21231, 84988, 21108, 23243, 845... 18282 {22818, 23175, 22423, 23174, 22699} 18283 {22661, 22574, 22910, 21986, 21874, 21930, 218... Name: StockCode, Length: 1880, dtype: object
11월 이후에 구매한 유저별 상품의 집합을 이용하여 앞서 정의했던 유저-상품 구매 상태 딕셔너리를 업데이트한다. 다음 코드는 기존에 구매하여 'old'라고 표기되어 있던 것은 'both'로 업데이트하고, 사전에 없던 유저-상품인 경우에는 'new'라고 표기하는 과정이다. 딕셔너리의 업데이트가 완료되면 11월 이전에만 구매한 유저-상품은 'old', 이후에만 구매한 상품은 'new', 모두 구매한 상품은 'both'로 표기된 딕셔너리 구축이 완료된다. 이제 이를 통해 유저별 재구매, 신규 구매 등의 패턴을 분석 할 수 있다.
- 유저별 구매 사전 업데이트 하기
# 11월 이전에만 구매한 상품은 'old', 이후에만 구매한 상품은 'new', 모두 구매한 상품은 'both'라고 표기한다.
for customer_id, stocks in customer_item_end_set.items():
# 11월 이전 구매기록이 있는 유저인지를 체크한다.
if customer_id in customer_item_dict:
for stock_code in stocks:
# 구매한 적 있는 상품인지를 체크한 뒤, 상태를 표기한다.
if stock_code in customer_item_dict[customer_id]:
customer_item_dict[customer_id][stock_code] = 'both'
else:
customer_item_dict[customer_id][stock_code] = 'new'
# 11월 이전 구매기록이 없는 유저라면 모두 'new'로 표기한다.
else:
customer_item_dict[customer_id] = {}
for stock_code in stocks:
customer_item_dict[customer_id][stock_code] = 'new'
print(str(customer_item_dict)[:100] + "...")
{'12346': {'23166': 'old'}, '12347': {'23508': 'new', '23497': 'new', '21064': 'new', '21265': 'new'...
구축 완료된 딕셔너리를 조금 더 편하게 분석하기 위해 데이터 프레임의 형태로 다시 정리한다. 다음 코드는 미리 비어있는 데이터 프레임을 생성해 놓고 딕셔너리를 반복문으로 들여다보며 비어있는 프레임에 데이터를 추가한다.
- 구매 사전을 데이터 프레임으로 정리하기
# 'old', 'new', 'both'를 유저별로 탐색하여 데이터 프레임을 생성한다.
columns = ['CustomerID', 'old', 'new', 'both']
df_order_info = pd.DataFrame(columns=columns)
# 데이터 프레임을 생성하는 과정
for customer_id in customer_item_dict:
old = 0
new = 0
both = 0
# 딕셔너리의 상품 상태(old, new, both)를 체크하여 데이터 프레임에 append 할 수 있는 형태로 처리한다.
for stock_code in customer_item_dict[customer_id]:
status = customer_item_dict[customer_id][stock_code]
if status == 'old':
old += 1
elif status == 'new':
new += 1
else:
both += 1
# df_order_info에 데이터를 append한다.
row = [customer_id, old, new, both]
series = pd.Series(row, index=columns)
df_order_info = df_order_info.append(series, ignore_index=True)
df_order_info.head()
CustomerID | old | new | both | |
---|---|---|---|---|
0 | 12346 | 1 | 0 | 0 |
1 | 12347 | 0 | 11 | 0 |
2 | 12348 | 21 | 0 | 0 |
3 | 12350 | 16 | 0 | 0 |
4 | 12352 | 0 | 14 | 0 |
완성된 데이터 프레임을 출력한 결과는 아래의 출력 결과와 같다. 데이트 프레임에서는 각 열에 유저별 'old'의 개수가 몇 개인지, 'new'의 개수는 몇 개인지,'both'의 개수는 몇 개인지를 계산하여 저장하고 있다.
이렇게 정리된 데이터 프레임을 활용하여 재구매와 신규 구매가 어떤 패턴으로 발생하였는지 탐색 해보자. 다음 코드는 3가지를 출력한 것으로 첫번째는 데이터프레임의 열 개수, 즉 전체 유저 수를 출력한 것이다. 그리고 두번째는 'old'가 1개이상이면서 동시에 'new'가 1개 이상인 유저가 몇 명인지를 출력한 것이다. 이를 통해 11월 이후에 기존에 구매한 적 없던 신규 상품을 구매한 유저가 약 3분의 1 가량 된다는 것을 알 수 있다. 마지막 세번째는 'both'가 1인상인 유저 수를 출력한 것으로 이는 재구매한 상품이 있는 유저 수를 의미한다. 즉 3분의 1 정도는 11월 이전에 구매했던 상품을 11월 이후에 다시 구매한다는 것을 의미한다.
- 재구매, 신규 구매 유저 분석하기
# 데이터 프레임에서 전체 유저 수를 출력한다.
print(df_order_info.shape[0])
# 데이터 프레임에서 old가 1 이상이면서, new가 1인상인 유저 수를 출력한다.
# 11월 이후에 기존에 구매한 적 없는 새로운 상품을 구매한 유저를 의미한다.
print(df_order_info[(df_order_info['old']>0 ) & (df_order_info['new']>0)].shape[0])
# 데이터 프레임에서 both가 1 이상인 유저 수를 출력한다
# 재구매한 상품이 있는 유저 수를 의미한다.
print(df_order_info[df_order_info['both'] > 0 ].shape[0])
3970 0 0
신규 구매한 상품이 있는 유저들은 얼마나 많은 종류의 신규 상품을 구매했는지를 탐색한다. 다음 코드는 이를 탐색한 것으로 평균적으로 10개 종류의 신규 상품을 구매하는 것으로 나타났다. 하지만 이는 편차가 매우 큰것으로 보인다. 따라서 신규 구매를 하는 유저들은 일반적으로 많은 종류의 상품을 구매하지는 않을 것으로 예상 할 수 있다.
- 신규 구매 상품 종류 탐색하기
# 만약 새로우 상품을 구매한다면 얼마나 많은 종류의 새로운 상품을 구매하는지 탐색한다.
print(df_order_info['new'].value_counts()[1:].describe())
count 150.000000 mean 10.153333 std 12.077900 min 1.000000 25% 1.000000 50% 4.000000 75% 12.750000 max 43.000000 Name: new, dtype: float64
Step 2 예측분석: SVD를 활용한 상품 구매 예측하기¶
지금까지 탐색한 내용을 토대로 상품 추천 시뮬레이션을 준비해보겠다. 앞서 설명한 대로 상품 추천 시뮬레이션이라는 것은 과거의 학급 데이터셋을 이용하여 미래의 유저-상품 구매를 예측하는 것이다. 이는 Chapter 03에서 학습한 '미래에 볼 영화의 평점 예측하기'와 동일한 방식으로 수행 할 수 있습니다. Chapter 03에서는 특정 시점 이전의 데이터 SVD 모델을 학습하고, 이를 통해 특정 시점 이후의 유저-아이템의 점수를 예측했다. 마찬가지로 이번 예제에서도 유저-상품의 점수를 예측하여 상품 추천에 활용해 보자. 우선 SVD 예측 모델 학습을 진행하기에 앞서 학습 데이터인 11월 이전의 데이터에서 추천 대상이 되는 유저와 상품은 얼마나 되는지를 탐색해보자.
- 추천 대상인 유저와 상품 출력하기
# 추천 대상 데이터에 포함되는 유저와 상품의 개수를 출력
print(len(df_year_round['CustomerID'].unique()))
print(len(df_year_round['StockCode'].unique()))
3970 3608
SVD 모델 학습함에 있어 Chapter 03에서의 내용과 한 가지 다른 점은 우리는 유저-아이템의 'Rating'에 해당하는 선호도 점수를 가지고 있지 않다는 점이다. 그렇다면 이 문제를 어떻게 해결할까? 바로 피처엔지니어링을 통해 이 점수를 만들어내야 한다. 적당한 유저-상품 간의 점수rating를 만들어 내기 위해 앞선 탐색적 데이터 분석에서 정리했던 다음 내용을 떠올려보자. '유저별 구매 횟수는 일반적으로 1~5사이에 분포되어있음' 따라서 우리는 이 정보를 이용하여 유저-상품 간의 구매 횟수가 Rating으로 사용하기에 적절한지를 탐색해 볼 것이다. 유저별 구매 횟수가 일반적으로 1~5사이라면 유저-상품 간의 구매 횟수를 계산하여 U-I-R 데이터로 활용할 데이터 프레임을 생성하는 과정이다.
- SVD 모델에 사용할 Raiting 탐색하기
# Rating 데이터를 생성하기 위한 탐색: 유저-상품간 구매 횟수를 탐색한다.
uir_df = df_year_round.groupby(['CustomerID', 'StockCode'])['InvoiceNo'].nunique().reset_index()
uir_df.head()
CustomerID | StockCode | InvoiceNo | |
---|---|---|---|
0 | 12346 | 23166 | 1 |
1 | 12347 | 16008 | 1 |
2 | 12347 | 17021 | 1 |
3 | 12347 | 20665 | 1 |
4 | 12347 | 20719 | 3 |
이렇게 Rating이라고 가정한 유저-상품 간의 구매 횟수가 어떻게 분포되어 있는지를 그래프로 탐색해 본 결과는 아래와 같다. 그래프를 살펴보면 대부분의 점수가 1~5 사이에 위치하기는 하지만 점수가 낮은 쪽으로 많이 쏠려있는 것을 확인할 수 있다. 아마도 이러한 분포를 가진 Rating으로 SVD 모델을 학습한다면 행렬을 제대로 완성하지 못 할 확률이 높다.
# Rating(InvoiceNo) 피처의 분포를 탐색한다.
uir_df['InvoiceNo'].hist(bins=20, grid=False)
<AxesSubplot:>
이러한 상황에 적용할 수 있는 피처 엔지니어링 기법으로 로그를 통한 피처 정규화Log Normalization 방법이 있다. 이는 Chapter 03 에서 학습하였던 피처의 정규화의 여러 가지 방법 중 하나이다. 이 방법의 목적은 위의 실행 결과에 나타난 그래프 처럼 데이터의 왜도(한쪽의 긴꼬리를 가진 형태의 비대칭적인 분포 정도)가 높은 경우에 '데이터 사이의 편차를 줄여 왜도를 감소시키는 것'에 있다. 이는 로그라는 개념의 수학적인 성질에 기반하는 것이다. 아래의 코드와 실행 결과는 로그를 통한 피처 정규화를 적용한 뒤, InvoiceNo 피처의 분포를 다시 탐색한 것이다. 여전히 왜도가 높긴 하지만 적용 이전에 비해서는 피처를 Rating으로 쓰기에 조금 더 적합해졌다는 것을 알 수 있다.
- Log Normalization 적용하기
# Rating(InvoiceNo) 피처를 log normalization 해준 뒤, 다시 분포를 탐색한다.
uir_df['InvoiceNo'].apply(lambda x: np.log10(x)+1).hist(bins=20, grid=False)
<AxesSubplot:>
로그를 통한 정규화 적용 후, 다시 피처 스케일링을 적용하여 1~5사이의 값으로 변환합니다. 아래의 코드는 변환 이후의 분포를 다시 그래프로 출력한 것이다. 코드에서는 최대-최소 스케일링방법을 적용합니다.
- 피처 스케일링 적용하기
# 1~5 사이의 점수로 변환한다.
uir_df['Rating'] = uir_df['InvoiceNo'].apply(lambda x: np.log10(x)+1)
uir_df['Rating'] = ((uir_df['Rating'] - uir_df['Rating'].min()) /
(uir_df['Rating'].max() - uir_df['Rating'].min()) * 4) + 1
uir_df['Rating'].hist(bins=20, grid=False)
<AxesSubplot:>
유저-상품 간의 Rating 점수를 정의하였으니 우리에게 필요했던 U-I-R 매트릭스 데이터가 완성되었다. 이제 이를 기반으로 유저-상품 간의 점수를 예측하는 SVD 모델을 학습할 수 있습니다. 그리고 이를 통해 상품 추천 시뮬레이션을 진행할 것이다. 우선 아래의 코드를 통해 데이터셋을 다시 한번 적합한 형태로 정리하자.
- SVD 모델 학습을 위한 데이터셋 생성
# SVD 모델 학습을 위한 데이터셋을 생성
uir_df = uir_df[['CustomerID', 'StockCode', 'Rating']]
uir_df.head()
CustomerID | StockCode | Rating | |
---|---|---|---|
0 | 12346 | 23166 | 1.000000 |
1 | 12347 | 16008 | 1.000000 |
2 | 12347 | 17021 | 1.000000 |
3 | 12347 | 20665 | 1.000000 |
4 | 12347 | 20719 | 2.048881 |
이렇게 생성된 데이터셋으로 SVD 모델을 학습한다. 모델의 대략적인 성능을 알아보기 위해 11월 이전 데이터로 생성한 학습 데이터인 uir_df를 또 다시 학습 데이터와 테스트 데이터셋으로 분리하여 모델을 학습하고 평가한다. 여기서 테스트 데이터는 11월 이후의 데이터를 의미하는 것이 아님을 주의하기
- SVD 모델 성능 테스트하기
import time
from surprise import SVD, Dataset, Reader, accuracy
from surprise.model_selection import train_test_split
# SVD 라이브러리를 사용하기 위한 학습 데이터를 생성합니다. 대략적인 성능을 알아보기 위해 학습 데이터와 테스트 데이터를 8:2로 분할
reader = Reader(rating_scale=(1, 5))
data = Dataset.load_from_df(uir_df[['CustomerID', 'StockCode', 'Rating']], reader)
train_data, test_data = train_test_split(data, test_size=0.2)
# SVD 모델을 학습
train_start = time.time()
model = SVD(n_factors=8,
lr_all=0.005,
reg_all=0.02,
n_epochs=200)
model.fit(train_data)
train_end = time.time()
print("training time of model: %.2f seconds" % (train_end - train_start))
predictions = model.test(test_data)
# 테스트 데이터의 RMSE를 출력하여 모델의 성능을 평가 한다
print("RMSE of test dataset in SVD model:")
accuracy.rmse(predictions)
training time of model: 32.21 seconds RMSE of test dataset in SVD model: RMSE: 0.3343
0.3343025985576829
모델의 대략적인 성능을 알아보았으니 이제 11월 데이터를 모두 학습 데이터로만 사용하여 모델을 학습한다. 아래 코드에서 생성된 모델로 다음 단계인 상품 추천 시뮬레이션을 진행하자.
# SVD 라이브러리를 사용하기 위한 학급 데이터를 생성한다. 11월 이전 전체를 full trainset으로 활용한다.
reader = Reader(rating_scale=(1, 5))
data = Dataset.load_from_df(uir_df[['CustomerID', 'StockCode', 'Rating']], reader)
train_data = data.build_full_trainset()
# SVD 모델을 학습한다.
train_start = time.time()
model = SVD(n_factors = 8,
lr_all= 0.005,
reg_all = 0.02,
n_epochs=200)
model.fit(train_data)
train_end = time.time()
# SVD 라이브러리를 사용하기 위한 학습 데이터를 생성합니다. 11월 이전 전체를 full trainset으로 활용합니다.
reader = Reader(rating_scale=(1, 5))
data = Dataset.load_from_df(uir_df[['CustomerID', 'StockCode', 'Rating']], reader)
train_data = data.build_full_trainset()
# SVD 모델을 학습합니다.
train_start = time.time()
model = SVD(n_factors=8,
lr_all=0.005,
reg_all=0.02,
n_epochs=200)
model.fit(train_data)
train_end = time.time()
print("training time of model: %.2f seconds" % (train_end - train_start))
training time of model: 35.98 seconds
Step3 예측 평가: 상품 추천시뮬레이션하기¶
이번 단계에서는 앞서 학습한 SVD 모델을 활용하여 상품 추천 시뮬레이션을 수행해보자. 추천의 대상이 되는 유저는 11월 이전 데이터에 등장한 모든 유저를 대상으로 한다. 반면, 유저들에게 추천의 대상이 되는 상품은 아래의 3가지로 분류할 수 있다. 이 3가지 분류의 기준은 Step1에서 탐색했던 신규 구매/재구매에 대한 탐색을 기반으로 한 것이다.
- 이전에 구매한적 없던 상품 추천: 신규 구매를 타겟으로 하는 추천
- 이전에 구매했던 상품 추천: 재구매를 타겟으로 하는 추천
- 모든 상품을 대상으로 상품 추천: 모든 유저-상품의 점수를 고려하여 추천 다음 코드는 11월 이전의 데이터에 등장한 모든 유저와 해당 유저들이 이전에 구매한적 없던 상품들을 대상으로 한 예측 점수를 딕셔너리 형태로 추출하는 과정이다. 우선, 이전 코드에서 생성된 결과인 train_data에 build_anti_testset()함수를 적용하여 U-I-R 데이터에서 Rating이 0인 유저-상품 쌍을 test-data 변수로 추출한다. 이는 구매기록이 없는 유저-상품 쌍을 의미한다. 그리고 SVD모델의 tset()함수로 추천 대상이 되는 유저-상품의 Rating을 예측하고 이를 딕셔너리 형태로 추출한다. 딕셔너리의 형태는 다음 실행 결과에서 볼 수 있듯이{유저:{상품:점수,상품:점수...}}의 형태로 이루어져 있다.
- 첫 번째 추천 대상의 유저-상품 점수 추출하기
# 이전에 구매하지 않았던 상품을 예측의 대상으로 선정한다.
test_data = train_data.build_anti_testset()
target_user_predictions = model.test(test_data)
# 구매 예측 결과를 딕셔너리 형태로 변환한다.
new_order_prediction_dict = {}
for customer_id, stock_code, _, predicted_rating, _ in target_user_predictions:
if customer_id in new_order_prediction_dict:
if stock_code in new_order_prediction_dict[customer_id]:
pass
else:
new_order_prediction_dict[customer_id][stock_code] = predicted_rating
else:
new_order_prediction_dict[customer_id] = {}
new_order_prediction_dict[customer_id][stock_code] = predicted_rating
print(str(new_order_prediction_dict)[:300] + "...")
{'12346': {'16008': 1, '17021': 1.2338720874235072, '20665': 1.0508415827950532, '20719': 1.3070243945733184, '20780': 1.0045195406735448, '20782': 1.1519040718711102, '20966': 1.0601354825150928, '21035': 1.1251388132819924, '21041': 1.0889751211241505, '21064': 1.0529349641234258, '21154': 1.10639...
마찬가지의 방법으로 두 번째 추천 대상인 데이터에 등장한 모든 유저-이전에 구매했던 상품 간의 예측 점수를 딕셔너리 형태로 추출하는 과정을 수행한다. 이번에는 build_anti_testset() 함수 대신 build_testset() 함수를 사용하여 이전에 구매했었던 상품을 대상으로 test_data를 추출한다. 그리고 딕셔너리를 추출하는 과정은 위와 동일하다. 다음 코드와 실행 결과를 통해 두 번째 딕셔너리의 결과를 확인해 보자.
- 두 번째 추천 대상의 유저-상품 점수 추출하기
# 이전에 구매했었던 상품을 예측의 대상으로 선정한다.
test_data = train_data.build_testset()
target_user_predictions = model.test(test_data)
# 구매 예측 결과를 딕셔너리 형태로 변환한다.
reorder_prediction_dict = {}
for customer_id, stock_code, _, predicted_rating, _ in target_user_predictions:
if customer_id in reorder_prediction_dict:
if stock_code in reorder_prediction_dict[customer_id]:
pass
else:
reorder_prediction_dict[customer_id][stock_code] = predicted_rating
else:
reorder_prediction_dict[customer_id] = {}
reorder_prediction_dict[customer_id][stock_code] = predicted_rating
print(str(reorder_prediction_dict)[:300] + "...")
{'12346': {'23166': 1.0412641527880722}, '12347': {'16008': 1.1621796438628693, '17021': 1.3857288201693798, '20665': 1.256534349008698, '20719': 1.6981231570265976, '20780': 1.14408164772837, '20782': 1.2832143694963711, '20966': 1.055047825143465, '21035': 1.2036849642820446, '21041': 1.0147886511...
세 번째 추천 대상은 위에서 생성한 두 개의 딕셔너리를 하나의 딕셔너리로 통합하는 과정으로 생성 할 수 있다. 다음 코드로 모든 유저와 모든 상품 간의 에측 점수를 딕셔너리 형태로 저장한다.
- 세 번째 추천 대상의 유저-상품 점수 추출하기
# 두 딕셔너리를 하나로 통합한다.
total_prediction_dict = {}
# new_order_prediction_dict 정보를 새로운 딕셔너리에 저장한다.
for customer_id in new_order_prediction_dict:
if customer_id not in total_prediction_dict:
total_prediction_dict[customer_id] = {}
for stock_code, predicted_rating in new_order_prediction_dict[customer_id].items():
if stock_code not in total_prediction_dict[customer_id]:
total_prediction_dict[customer_id][stock_code] = predicted_rating
# reorder_prediction_dict 정보를 새로운 딕셔너리에 저장한다.
for customer_id in reorder_prediction_dict:
if customer_id not in total_prediction_dict:
total_prediction_dict[customer_id] = {}
for stock_code, predicted_rating in reorder_prediction_dict[customer_id].items():
if stock_code not in total_prediction_dict[customer_id]:
total_prediction_dict[customer_id][stock_code] = predicted_rating
print(str(total_prediction_dict)[:300] + "...")
{'12346': {'16008': 1, '17021': 1.2338720874235072, '20665': 1.0508415827950532, '20719': 1.3070243945733184, '20780': 1.0045195406735448, '20782': 1.1519040718711102, '20966': 1.0601354825150928, '21035': 1.1251388132819924, '21041': 1.0889751211241505, '21064': 1.0529349641234258, '21154': 1.10639...
앞의 단계들을 거쳐 우리는 세 가지 상품 추천의 시뮬레이션 결과를 각각의 딕셔너리 형태로 갖게 되었다. 그렇다면 이제 시뮬레이션의 결과가 실제 구매와 얼마나 유사한지 평가해볼 차례이다. 다음 코드는 11월 이후의 데이터를 테스트 데이터로 활용하기 위해 각 유저들이 11월 이후에 실제로 구매한 상품의 리스트를 데이터 프레임의 형태로 정리한 것이다. 이를 위해 CustomerID를 그룹으로 하고 StockCode의 set을 추출한다. 그리고 이 결과에 reset_index() 함수를 적용하여 데이터 프레임 형태로 변환한다.
- 시뮬레이션을 테스트할 데이터 프레임 생성하기
# 11월 이후의 데이터를 테스트 데이터셋으로 사용하기 위한 데이터 프레임을 생성한다.
simulation_test_df = df_year_end.groupby('CustomerID')['StockCode'].apply(set).reset_index()
simulation_test_df.columns = ['CustomerID', 'RealOrdered']
simulation_test_df.head()
CustomerID | RealOrdered | |
---|---|---|
0 | 12347 | {23508, 23497, 21064, 21265, 84625A, 23271, 23... |
1 | 12349 | {23497, 22722, 23113, 22441, 23460, 23283, 234... |
2 | 12352 | {22978, 22627, 21669, 22635, 22624, 23089, 226... |
3 | 12356 | {21843, 22423} |
4 | 12357 | {23247, 21507, 22315, 22969, 21874, 22716, 227... |
시뮬레이션 테스트용 데이터 프레임에 3개 딕셔너리의 시뮬레이션 결과를 추가한다. 이를 위해 아래 코드와 같이 add_predicted_stock_set()이라는 함수를 정의한다. 이 함수는 유저의 id와 위에서 추출했던 딕셔너리 중 1개를 인자로 입력 받는다. 그리고 딕셔너리 안에서의 유저 정보를 참고하여 예측된 점수순으로 상품을 정렬하고 리스트 형태로 반환한다. 이 함수를 데이터 프레임의 apply에 적용하면 PredictedOrder라는 새로운 피처를 생성 할 수 있다.
- 시뮬레이션 결과 추가하기
# 이 데이터프레임에 상품 추천 시뮬레이션 결과를 추가하기 위한 함수를 정의한다.
def add_predicted_stock_set(customer_id, prediction_dict):
if customer_id in prediction_dict:
predicted_stock_dict = prediction_dict[customer_id]
# 예측된 상품의 Rating이 높은 순으로 정렬한다.
sorted_stocks = sorted(predicted_stock_dict, key=lambda x : predicted_stock_dict[x], reverse=True)
return sorted_stocks
else:
return None
# 상품 추천 시뮬레이션 결과를 추가하다.
simulation_test_df['PredictedOrder(New)'] = simulation_test_df['CustomerID']. \
apply(lambda x: add_predicted_stock_set(x, new_order_prediction_dict))
simulation_test_df['PredictedOrder(Reorder)'] = simulation_test_df['CustomerID']. \
apply(lambda x: add_predicted_stock_set(x, reorder_prediction_dict))
simulation_test_df['PredictedOrder(Total)'] = simulation_test_df['CustomerID']. \
apply(lambda x: add_predicted_stock_set(x, total_prediction_dict))
simulation_test_df.head()
CustomerID | RealOrdered | PredictedOrder(New) | PredictedOrder(Reorder) | PredictedOrder(Total) | |
---|---|---|---|---|---|
0 | 12347 | {23508, 23497, 21064, 21265, 84625A, 23271, 23... | [22178, 85099B, 82486, 20914, 22630, 22386, 22... | [22726, 22727, 22728, 21731, 22729, 22423, 219... | [22178, 85099B, 82486, 20914, 22630, 22386, 22... |
1 | 12349 | {23497, 22722, 23113, 22441, 23460, 23283, 234... | None | None | None |
2 | 12352 | {22978, 22627, 21669, 22635, 22624, 23089, 226... | [84086B, 22227, 90119, 85131B, 85123A, 90035A,... | [22779, 37448, 22780, 21232, 22654, 22423, 221... | [84086B, 22227, 90119, 85131B, 85123A, 90035A,... |
3 | 12356 | {21843, 22423} | [84086B, 85131B, 90035A, 85099B, 90042A, 22197... | [22423, 37450, 21843, 22649, 21094, 22699, 210... | [84086B, 85131B, 90035A, 85099B, 90042A, 22197... |
4 | 12357 | {23247, 21507, 22315, 22969, 21874, 22716, 227... | None | None | None |
코드를 실행한 결과, 총 3개의 상품 추천 시뮬레이션 결과가 추가 되었다. 이제 테스트셋을 완성하였으니 추천 시뮬레이션이 실제 구매와 얼마나 비슷하게 예측되었는지를 평가해볼 차례이다. 우리가 사용할 평가 방식은 다음과 같다.
- 유저별로 예측된 상품의 점수 순으로 상위 k개의 상품을 추천 대상으로 정의한다.
- 추천한 k개의 상품 중, 실제 구매로 얼마만큼 이어졌는지 평가한다.
이 방식은 우리가 Chapter 04에서 학습했던 분류 모델의 평가 방법 중 하나인 재현도(Reacll)와 동일한 개념이다. 다만 k개의 대상으로 제한한다는 것이 다른 점이다. 다음 코드의 calculate_recall() 함수는 이 과정을 코드로 정의한 것이다. real_order 파라미터는 실제 구매한 상품의 리스트이고, predicted_order 파라미터는 예측 점수순으로 정렬된 상품의 리스트이다. 그리고 k파라미터는 추천할 개수를 의미한다. 만약 추천할 대상 상품 리스트가 없다면 11월 이전 데이터셋에 존재하지 않는 유저이기 때문에 None을 반환하고, 추천할 상품 리스트가 존재한다면 리스트 중 상위 k개를 선정하여 실제 구매한 리스트에 몇 개나 존재하는지를 계산하여 반환한다.
- 상품 추천 평가 기준 정의하기
# 구매 예측의 상위 k개의 recall(재현율)을 평가 기준으로 정의합니다.
def calculate_recall(real_order, predicted_order, k):
# 만약 추천 대상 상품이 없다면, 11월 이후에 상품을 처음 구매하는 유저입니다.
if predicted_order is None:
return None
# SVD 모델에서 현재 유저의 Rating이 높은 상위 k개의 상품을 "구매 할 것으로 예측"합니다.
predicted = predicted_order[:k]
true_positive = 0
for stock_code in predicted:
if stock_code in real_order:
true_positive += 1
# 예측한 상품 중, 실제로 유저가 구매한 상품의 비율(recall)을 계산합니다.
recall = true_positive / len(predicted)
return recall
위에서 정의한 함수를 apply()로 적용하여 3개의 시뮬레이션 펴가 결과를 저장한다. 실행 결과 생성되는 피처는 점수순으로 k개의 상품을 추천해 주었을 때의 재현도(Recall) 점수를 계산한 것이다.
- 상품 추천 평가하기
# 시뮬레이션 대상 유저에게 상품을 추천해준 결과를 평가합니다.
simulation_test_df['top_k_recall(Reorder)'] = simulation_test_df. \
apply(lambda x: calculate_recall(x['RealOrdered'],
x['PredictedOrder(Reorder)'],
5), axis=1)
simulation_test_df['top_k_recall(New)'] = simulation_test_df. \
apply(lambda x: calculate_recall(x['RealOrdered'],
x['PredictedOrder(New)'],
5), axis=1)
simulation_test_df['top_k_recall(Total)'] = simulation_test_df. \
apply(lambda x: calculate_recall(x['RealOrdered'],
x['PredictedOrder(Total)'],
5), axis=1)
이제 이를 이용하여 추천 시뮬레이션의 성능을 평가한다.
- 평가 결과 출력하기
# 평가 결과를 유저 평균으로 살펴본다.
print(simulation_test_df['top_k_recall(Reorder)'].mean())
print(simulation_test_df['top_k_recall(New)'].mean())
print(simulation_test_df['top_k_recall(Total)'].mean())
0.31106369008535784 0.008273145108338806 0.07222586999343401
실행 결과는 세 가지 추천 시뮬레이션의 평균 재현도를 각각 계산하여 출력한 것이다. 이미 한 번 구매했던 상품을 대상으로 하여 추천해주었을 때 평균 재현도는 약 31%, 신규 구매를 대상으로 할때는 0.8%, 전체 상품을 대상으로 할때는 약 7% 정도로 나타났다. 이를 통해 우리는 재구매할만한 상품을 추천해주는 것이 새로운 상품을 추천해주는 것보다 더 좋은 결과를 낼 것이라고 예상할 수 있다. 아마도 이 온라인 스토어의 주 구매자는 도매상이기 때문에 새로운 상품을 구매하는 것보다는 기존의 상품을 다시 구매하는 성향이 강한 것이 아닐까 추측해 볼 수 있다. 다음으로 추천 시뮬레이션 각각의 세부 결과를 살펴보겠다. 아래의 코드는 이미 한 번 구매했던 상품을 대상으로 하여 추천해 주었을때의 재현도를 value_counts()함수로 상세하게 출력한 것이다.
- 재구매 상품 추천의 상세 결과
# 평가 결과를 점수 기준으로 살펴본다.
simulation_test_df['top_k_recall(Reorder)'].value_counts()
0.000000 453 0.200000 411 0.400000 266 0.600000 187 0.800000 114 1.000000 73 0.500000 7 0.250000 6 0.666667 4 0.333333 1 0.750000 1 Name: top_k_recall(Reorder), dtype: int64
실행 결과를 다음과 같이 해석할 수 있다.
- 재현도 0:453명은 5개를 추천해준다면 하나도 구매하지 않을 것으로 예상
- 재현도 0.2:411명은 5개를 추천해준다면 1개읫 ㅏㅇ품을 구매할 것으로 예상 만약 5개의 추천 상품을 제공한다면, 이 중 과반수 이상은 실제 구매로 이어질 것으로 예상되기 때문에 이 시뮬레이션 결과는 제법 성공적인 예측을 한 것이다. 이번에는 이전에 구매한적 없는 상품을 대상으로 추천한 결과를 살펴보자. 다음 실행 결과처럼 전체적으로 0에 가까운 재현도를 보이고 있다. 따라서 대부분의 유저는 추천된 상품을 구매하지 않을 것으로 보이며 이 시뮬레이션 결과는 좋지 않은 것으로 평가 할 수 있다.
- 신규 상품 추천의 상세 결과
# 평가 결과를 점수 기준으로 살펴보자
simulation_test_df['top_k_recall(New)'].value_counts()
0.0 1470 0.2 45 0.4 6 0.6 2 Name: top_k_recall(New), dtype: int64
전체 상품을 대상으로 추천한 결과를 살펴보자. 두 번째 시뮬레이션 결과보다는 조금 낫지만 이번 시뮬레이션 결과 역시 그다지 좋지 않은 것으로 보인다.
- 전체 상품 추천의 상세 결과
# 평가 결과를 점수 기준으로 살펴본다.
simulation_test_df['top_k_recall(Total)'].value_counts()
0.0 1198 0.2 195 0.4 70 0.6 36 0.8 13 1.0 11 Name: top_k_recall(Total), dtype: int64
3개의 시뮬레이션의 평가 결과, 그 중 재구매할만한 상품을 추천해 주는 것이 가장 좋은 시뮬레이션 인 것으로 평가되었다. 이제 '연말 선물로 구매할만한 상품 추천하기' 시뮬레이션을 아래와 같이 최종 정리해보자.
- 시뮬레이션 최종 결과 정리
# 추천 시뮬레이션 결과를 살펴본다.
k = 5
result_df = simulation_test_df[simulation_test_df['PredictedOrder(Reorder)'].notnull()]
result_df['PredictedOrder(Reorder)'] = result_df['PredictedOrder(Reorder)'].\
apply(lambda x: x[:k])
result_df = result_df[['CustomerID', 'RealOrdered', 'PredictedOrder(Reorder)', 'top_k_recall(Reorder)']]
result_df.columns = [['구매자ID', '실제주문', '5개추천결과', 'Top5추천_주문재현도']]
result_df.sample(5).head()
구매자ID | 실제주문 | 5개추천결과 | Top5추천_주문재현도 | |
---|---|---|---|---|
25 | 12428 | {22386, 23480, 23380, 22192, 23515, 21115, 223... | [85099B, 22386, 85123A, 21928, 23203] | 0.6 |
751 | 14700 | {22753, 22756, 23395, 21668, 22771, 21035, 206... | [22771, 21669, 21670, 21672, 21671] | 0.8 |
346 | 13394 | {23076, 22460, 23307, 22302, 22910, 84945, 220... | [21238, 22469, 84978, 21080, 22303] | 0.0 |
1127 | 15910 | {23274, 23270, 22739, 23571, 23435, 22572, 227... | [22197, 21034, 20914, 22386, 22993] | 0.0 |
118 | 12664 | {84997C, 84997D, 84997A} | [84997A, 84997D, 84997B, 84997C, 22467] | 0.6 |
- 출처: 이것이 데이터 분석이다 with 파이썬
'이것이 데이터분석 이다 with 파이썬' 카테고리의 다른 글
Chapter2.1_웹크롤링으로 기초 데이터 수집하기 (0) | 2021.05.18 |
---|---|
Chapter 5.1 중고나라 휴대폰 거래가격 예측 (0) | 2021.05.07 |
Chapter3.3_Movie (2) | 2021.05.03 |
Google Analytics 4 _ tistory 연계 방법 (0) | 2021.04.27 |
Chapter 3.1 프로야구 선수의 다음 해 연봉 예측하기 (0) | 2021.04.24 |