-
chapter-5.2 구매 데이터를 분석하여 상품 추천하기이것이 데이터 분석이다 with 파이썬 2021. 10. 11. 17:47
5.2 구매 데이터를 분석하여 상품 추천하기¶
이번 절에서는 구매 데이터 분석에 기반한 온라인 스토어 상품 추천 시뮬레이션 예제를 알아보겠습니다. 예제에서는 피처 엔지니어링, 그리고 행렬 완성 기반 점수 예측 방법을 이용하여 상품 추천 시물레이션을 수행합니다. 분석에 사용할'UK Retail'데이터는 영국의 한 선물 판매 온라인 스토어에서 발생한 거래 데이터로, 주 고객은 선물 도매상입니다.
Step 1 탐색적 분석: UK Retail 데이터 분석하기¶
예제에서 사용할 UK Retail 데이터셋은 다음과 같은 피처로 구성되어 있습니다.
- invoiceNO : 거래 고유 번호
- StockCode : 상품 고유 번호
- Description : 상품명
- Quantiy : 거래 수량
- InvoiceDate : 거래 일시
- UnitPrice : 상품 단가
- CustomerID : 구매자 고유 번호
- Country : 구매 국가
아래의 코드를 통해 데이터를 살펴본 결과, 약 54만 개 정도의 데이터가 존재하며 그 중 14만 개의 데이터는 구매자 정보가 결측값인 것을 알 수 있습니다.
데이터셋 살펴보기¶
In [1]:# -*- 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()) df.head()
<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
Out[1]: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여 개의 데이터를 제거한 것입니다.
결측 데이터 제거하기¶
In [2]:df.isnull().sum()
Out[2]:InvoiceNo 0 StockCode 0 Description 1454 Quantity 0 InvoiceDate 0 UnitPrice 0 CustomerID 135080 Country 0 dtype: int64
In [3]:df = df.dropna() print(df.shape)
(406829, 8)
다음은 데이터가 일반적이지 않은 경우를 탐색하고 이를 제거하겠습니다. 이번에는 상품 수량 데이터가 이상한 경우를 탐색합니다. 아래의 코드는 상품 수량이 0 이하인 경우 해당 값을 데이터 프레임에서 제거하는 과정입니다. 이러한 경우는 아마도 환불이나 주문 취소를 의미하는 것 같지만 그 의미가 명확하지 않으니 제거합니다. 코드의 실행 결과, 약 9,000여 개의 데이터가 제거되었습니다.
탐색 데이터의 조건 필터링: 상품 수량이 0 이하인 경우¶
In [4]:# 상품 수량이 음수인 경우를 제거합니다. print(df[df['Quantity']<=0].shape[0]) df = df[df['Quantity']>0]
8905
이번에는 상품 가격이 0 이하인 경우를 탐색합니다. 이 역시 일반적인 상황이라고 할 수 없는 상황이기 때문에 조건에 해당하는 데이터를 제거합니다. 아래 코드의 실행 결과, 총 40개의 데이터가 제거되었습니다.
탐색 데이터의 조건 필터링: 상품가격이 0 이하인 경우¶
In [5]:# 상품 가격이 0 이하인 경우를 제거합니다. print(df[df['UnitPrice']<=0].shape[0]) df = df[df['UnitPrice']>0]
40
마지막으로 상품 코드가 이상한 경우를 탐색하고 제거합니다. 데이터 내의 StockCode를 관찰해 보면 대부분의 상품 코드가 번호로 이루어져 있는 것을 알 수 있습니다. 따라서 상품 코드가 번호가 아닌 경우는 예외적인 상황일 것입니다. 아래 실행 결과는 이러한 데이터를 살펴본 것입니다.
탐색 데이터의 조건 필터링: 상품 코드가 일반적이지 않은 경우¶
In [6]:# 상품 코드가 일반적이지 않은 경우를 탐색합니다. df['ContainDigit'] = df['StockCode'].apply(lambda x: any(c.isdigit() for c in x)) print(df[df['ContainDigit'] == False].shape[0]) df[df['ContainDigit'] == False].head()
1414
Out[6]: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 그리고 아래의 코드를 실행하여 일반적이지 않은 상품 코드를 가진 데이터를 제거합니다.
In [7]:# 상품 코드가 일반적이지 않은 경우를 제거합니다. df = df[df['ContainDigit'] == True]
이제 본격적으로 탐색적 분석을 수행할 차례입니다. 이번 예제에서는 어떤 방향성을 가지고 탐색적 분석을 진행해야 할까요? 분석 방향을 잘 설정하기 위해 지금부터 우리는 특정 시점에 도매상들에게 선물을 판매하는 온라인 스토어의 운영자 입장이 되어보겠습니다. 그리고 우리의 고민은 다음과 같습니다.
- '연말에 온라인 스토어에 방문하는 유저들에게 어떤 상품을 추천해줄 수 있을까?'
연말에 방문한 유저들에게 상품을 추천해준다는 것은 유저-상품 간의 구매 확률을 예측해보는 시뮬레이션이라고 할 수 있습니다. 이러한 예측 시뮬레이션은 아래와 같은 분석 과정이 필요합니다.
- (1) 연말 이전까지의 데이터를 유저-상품 간의 구매를 예측하는 모델의 학습 데이터셋으로 사용합니다.
- (2) 실제 연말에 구매한 유저-상품 간의 정보를 테스트 데이터셋으로 사용합니다.
- (3) 모델이 예측한 유저-상품 간의 구매 정보와 실제 구매 정보(테스트 데이터셋)을 비교하여 추천이 잘 되었는지 평가합니다.
이제 우리는 위와 같은 예측 분석을 수행하기 위한 탐색적 데이터 분석(EDA)이 필요합니다. 우리에게 필요한 탐색은 특정 시간을 기준으로 데이터를 나누고, 데이터에서 구매 패턴과 같은 특징을 발견하는 것입니다. 따라서 가장 먼저 할 것은 일자별 주문의 탐색입니다.
다음의 실행 결과는 가장 오래된 데이터와 가장 최신의 데이터를 출력한 것이며 이를 통해 데이터가 2010년 12월부터 2011년 12월까지 존재하는 것을 알 수 있습니다.데이터의 기간 탐색하기¶
In [8]:# 거래 데이터에서 가장 오래된 데이터와 가장 최신의 데이터를 탐색합니다. df['date'] = df['InvoiceDate'].dt.date print(df['date'].min()) print(df['date'].max())
2010-12-01 2011-12-09
다음으로 일자별 거래량을 탐색합니다. 아래의 실행 결과는 일자별 거래량을 시계열 그래프로 출력한 것입니다. 코드에서는 일자를 나타내는 date 피처를 그룹의 기준으로 하여, 일자별 Quantity의 합계를 계산하였습니다. 그래프를 살펴보면 대체적으로 연말에 가까워질수록 거래량이 증가하는 것을 알 수 있으며 10~11월 정도를 기점으로 증가폭이 조금씩 커지고 있다는 것을 알 수 있습니다.
일자별 거래 수량 탐색하기¶
In [9]:# 일자별 총 거래 수량을 탐색합니다. date_quantity_series = df.groupby('date')['Quantity'].sum() date_quantity_series.plot()
Out[9]:<AxesSubplot:xlabel='date'>
다음 일자별 거래 횟수를 탐색합니다. 아래의 코드도 마찬가지로 date 피처를 그룹의 기준으로 하였고, nunique() 함수를 InvoiceNO 피처에 적용하여 일자별로 발생한 거래 횟수를 계산합니다. 코드의 실행 결과는 일자별 거래량을 시계열 그래프로 나타낸 것입니다. 거래 횟수는 연말에 가까워질수록 거래 수량보다 조금 더 가파르게 상승하고 있습니다.
일자별 거래 횟수 탐색하기¶
In [10]:# 일자별 총 거래 횟수를 탐색합니다. date_transaction_series = df.groupby('date')['InvoiceNo'].nunique() date_transaction_series.plot()
Out[10]:<AxesSubplot:xlabel='date'>
마지막으로 일자별 거래 상품 개수를 탐색합니다. 아래 코드의 실행 결과, 지금까지의 그래프 중 가장 가파른 상승세를 나타내고 잇습니다. 지금까지의 내용을 종합해보면 연말이 시작되는 약 10~11월 정도부터 연중보다 더 많이 그리고 더 자주 구매가 일어난다는 것을 알 수 있습니다.
일자별 거래 상품 개수 탐색하기¶
In [11]:# 일자별 거래되 상품의 unique한 개수, 즉 상품 거래 다양성을 탐색합니다. date_unique_item_series = df.groupby('date')['StockCode'].nunique() date_unique_item_series.plot()
Out[11]:<AxesSubplot:xlabel='date'>
이번에는 전체 데이터에서 등장한 유저들의 구매 패턴을 탐색해봅시다. 아래의 실행 결과는 전체 데이터에 등장한 유저의 수를 출력하는 코드를 실행한 것입니다. 이를 통해 총 4,334명의 유저가 데이터에 존재하는 것을 알 수 있습니다.
전체 유저의 수 탐색하기¶
In [12]:# 총 유저의 수를 계산하여 출력합니다. print(len(df['CustomerID'].unique()))
4334
4,334명의 유저를 대상으로 각각의 거래 횟수를 탐색합니다. 다음 코드는 CustomerID를 그룹으로 하여 unique한 InvoiceNo를 계산한 것이고, 이결과에 describe() 함수를 적용하여 유저별 거래 횟수에 대한 요약 통꼐 정보를 출력하였습니다. 출력 결과를 살펴보면 유저들은 평균적으로 약 4회 정도의 구매가 있었다는 것을 알 수 있고, 대부분의 유저는 1~5회 정도의 구매 횟수를 보인다는 것을 알 수 있습니다.
유저별 거래 횟수 탐색하기¶
In [13]:# 유저별 거래 횟수를 탐색합니다. customer_unique_transaction_series = df.groupby('CustomerID')['InvoiceNo'].nunique() customer_unique_transaction_series.describe()
Out[13]: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
그리고 이를 상자 그림으로 살펴본 결과는 아래와 같습니다.
유저별 거래 횟수 시각화하기¶
In [14]:# 상자 그림 시각화로 살펴봅니다. plt.boxplot(customer_unique_transaction_series.values) plt.show()
다음으로 유저별로 구매한 상품은 몇 종류나 되는지를 탐색해봅시다. 아래의 코드는 CustomerID그룹에 unique한 StockCode를 계산하여 describe() 함수를 적용한 것입니다. 그리고 이를 통해 유저들은 평균적으로 약 60여 개 종류의 상품을 구매했다는 것을 알 수 있습니다. 하지만 데이터의 편차는 매우 높은 수치를 보이고 있습니다.
유저별 상품 구매 종류 탐색하기¶
In [15]:# 유저별 아이템 구매 종류 개수를 탐색합니다. customer_unique_item_series = df.groupby('CustomerID')['StockCode'].nunique() customer_unique_item_series.describe()
Out[15]: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
마찬가지로 이 결과를 상자 그림으로 살펴보았습니다. 거래 횟수보다는 조금 더 다양하게 데이터가 분포되어 있는 것을 알 수 있습니다.
유저별 상품 구매 종류 시각화하기¶
In [16]:# 상자 그림 시각화로 살펴봅니다. plt.boxplot(customer_unique_item_series.values) plt.show()
이제 특정 시점을 기준으로 데이터를 분리하여 구매의 패턴을 분석해봅시다. 중점적으로 살펴볼 내용은 두 데이터에서 동일하게 등장하는 유저-상품 단위의 구매 데이터, 즉 재구매 여부입니다. 또한 신규 구매가 얼마나 일어났는지 역시 중요하게 살펴볼 내용입니다.
먼저 11월 1일을 연말의 기준으로 삼아 두 개의 데이터로 분리합니다. 이 두 데이터는 추후에 예측분석에 사용할 학습 데이터셋, 그리고 테스트용 데이터셋을 의미하며 각각 314,902개, 81,568개의 데이터로 분리되었습니다.일자를 기준으로 데이터 분리하기¶
In [17]: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, 10) (81568, 10)
분리된 데이터에서 재구매, 신규 구매 등이 어떻게 일어났는지를 분석해봅시다. 먼저 해야 하는 것은 11월 이전 데이터셋에서 유저별로 구매했던 상품의 리스트를 추출하는 것입니다. 아래의 코드는 CustomerID를 그룹으로 하여 StockCode 피처에 apply(set) 함수를 적용한 것으로 이를 통해 유저별 StockCode의 집합(set)을 추출할 수 있습니다.
11월 이전 유저별로 구매했던 상품의 집합 추출하기¶
In [18]:# 11월 이전 데이터에서 구매했던 상품의 set을 추출합니다. customer_item_round_set = df_year_round.groupby('CustomerID')['StockCode'].apply(set) print(customer_item_round_set)
CustomerID 12346 {23166} 12347 {47567B, 23297, 22729, 22772, 23175, 22697, 84... 12348 {21213, 23078, 21985, 22952, 21967, 21725, 849... 12350 {79066K, 21832, 21866, 22557, 21908, 22412, 21... 12352 {21380, 22784, 23198, 22550, 23298, 21700, 217... ... 18280 {22499, 22358, 22495, 22727, 22725, 22180, 226... 18281 {22716, 22028, 23007, 22037, 23209, 23008, 22467} 18282 {21109, 21108, 22424, 22089, 23295, 21270, 23187} 18283 {23353, 22930, 85099F, 21156, 21213, 23247, 21... 18287 {22644, 22421, 21823, 21819, 20963, 23445, 230... Name: StockCode, Length: 3970, dtype: object
다음 유저-상품 단위의 딕셔너리(사전)를 정의합니다. 이 딕셔너리는 유저가 상품을 11월 이전에 구매했는지 혹은 11월 이후에 구매했는지를 기록하기 위한 것입니다. 아래의 코드를 실행하면 유저가 11월 이전에 구매한 상품은 딕셔너리에'old'라고 표기됩니다.
유저별 구매 사전 구축하기¶
In [19]:# 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': {'47567B': 'old', '23297': 'old', '22729': 'old', '22772': 'old...
이는 실행 결과를 보면 쉽게 이해할 수 있습니다. 12346번 유저는 23166 상품을 구매했었고, 12347번 유저는 22697, 22371, 85167B.... 상품을 구매했었다는 사실을 나타냅니다.
위에서와 동일한 방식으로 11월 이후 데이터에서 유저별로 구매한 상품의 집합을 추출합니다.
11월 이후 유저별로 구매했던 상품의 집합 추출하기¶
In [20]:# 11월 이후 데이터에서 구매하는 상품의 집합을 추출합니다. customer_item_end_set = df_year_end.groupby('CustomerID')['StockCode'].apply(set) print(customer_item_end_set)
CustomerID 12347 {21265, 23271, 23552, 23497, 20719, 23084, 846... 12349 {23198, 22059, 22554, 23296, 23497, 22195, 231... 12352 {22624, 23367, 23559, 21669, 23088, 22635, 226... 12356 {21843, 22423} 12357 {21116, 22714, 23247, 21844, 22508, 22027, 220... ... 18272 {23198, 22993, 22966, 23236, 22969, 22075, 226... 18273 {79302M} 18274 {21974, 23245, 22720, 21108, 84988, 22851, 232... 18282 {23174, 22699, 22818, 23175, 22423} 18283 {85099F, 21156, 21213, 21930, 22549, 23318, 20... Name: StockCode, Length: 1904, dtype: object
11월 이후에 구매한 유저별 상품의 집합을 이용하여 앞서 정의했던 유저-상품 구매 상태 딕셔너리를 업데이트합니다. 다음 코드는 기존에 구매하여'old'라고 표기되어 있던 것은'both'로 업데이트하고, 사전에 없던 유저-상품이 경우에는'new'라고 표기하는 과정입니다. 딕셔너리의 업데이트가 완료되면 11월 이전에만 구매한 유저-상품은'old', 이후에만 구매한 상품은'new', 모두 구매한 상품은'both'로 표기된 딕셔너리 구축이 완료됩니다. 이제 이를 통해 유저별 재구매, 신규 구매 등의 패턴을 분석할 수 있습니다.
유저별 구매 사전 업데이트하기¶
In [21]:# 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': {'47567B': 'old', '23297': 'old', '22729': 'old', '22772': 'old...
구축 완료된 딕셔너리를 조금 더 편하게 분석하기 위해 데이터 프레임의 형태로 다시 정리합니다. 다음 코드는 미리 비어있는 데이터 프레임을 생성해놓고 딕셔너리를 반복문으로 들여다보며 비어있는 프레임에 데이터를 추가합니다.
구매 사전을 데이터 프레임으로 정리하기¶
In [22]:# '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()
Out[22]:CustomerID old new both 0 12346 1 0 0 1 12347 92 3 8 2 12348 21 0 0 3 12350 16 0 0 4 12352 43 12 2 이렇게 정리된 데이터 프레임을 활용하여 재구매와 신규 구매가 어떤 패턴으로 발생하였는지 탐색해봅시다. 다음 코드는 3가지를 출력한 것으로 첫 번째는 데이터 프레임의 열 개수, 즉 전체 유저수를 출력한 것입니다. 그리고 두 번째 'old'가 1개 이상이면서 동시에'new'가 1개 이상인 유저가 몇 명인지를 출력한 것입니다. 이를 통해 11월 이후에 기존에 구매한 적 없던 신규 상품을 구매한 유저가 약 3분의 1 가량 된다는 것을 알 수 있습니다. 마지막 세 번째는'both'가 1 이상인 유저 수를 출력한 것으로 이는 재구매한 상품이 있는 유저 수를 의미합니다. 즉 3분의 1정도는 11월 이전에 구매했던 상품을 11월 이후에 다시 구매한다는 것을 의미합니다.
재구매, 신규 구매 유저 분석하기¶
In [23]:# 데이터 프레임에서 전체 유저 수를 출력합니다. 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])
4334 1446 1426
신규 구매한 상품이 있는 유저들은 얼마나 많은 종류의 신규 상품을 구매했는지를 탐색합니다. 다음 코드는 이를 탐색한 것으로 평균적으로 13개 종류의 신규 상품을 구매하는 것으로 나타났습니다. 하지만 이는 편차가 매우 큰 것으로 보입니다. 따라서 신규 구매를 하는 유저들은 일반적으로 많은 종류의 상품을 구매하지는 않을 것으로 예상할 수 있습니다.
신규 구매 상품 종류 탐색하기¶
In [24]:# 만약 새로운 상품을 구매한다면 얼마나 많은 종류의 새로운 상품을 구매하는지 탐색합니다. print(df_order_info['new'].value_counts()[1:].describe())
count 132.000000 mean 13.734848 std 19.130672 min 1.000000 25% 1.000000 50% 5.000000 75% 16.000000 max 81.000000 Name: new, dtype: float64
표로 정리하는 데이터 분석¶
- 데이터 탐색 주제 : 인사이트
- 일자별 거래 데이터 : 거래 수량,횟수,상품의 개수 모두 10~11월을 기점으로 상승함.
- 유저별 거래 횟수 : 유저별 구매 횟수는 일반적으로 1~5 사이에 분포되어 있음.
- 유저별 구매 상품 종류 : 유저별 구매 상품 종류는 다양한 편이며, 유저당 약 수십 여 개의 상품을 구매한다고 할 수 있음.
- 유저별 재구매, 신규 구매 : 11월을 기준으로 나누면 전체 유저 중, 3분의 1은 재구매를 하였고 3분의 1은 신규 구매를 하였음.
- 신규 구매 상품 종류 : 신규 구매 유저의 경우, 기존 구매 종류에 비해 많은 종류의 상품을 구매하지는 않았음.
Step 2 예측 분석: SVD를 활용한 상품 구매 예측하기¶
지금까지 탐색한 내용을 토대로 상품 추천 시뮬레이션을 준비해보겠습니다. 앞서 설명한 대로 상품추천 시뮬레이션이라는 것은 과거의 학습 데이터셋을 이용하여 미래의 유저-상품 구매를 예측하는 것입니다. 이는 Chapter 03에서 학습한'미래에 볼 영화의 평점 예측하기'와 동일한 방식으로 수행할 수 있습니다. Chapter 03에서는 특정 시점 이전의 데이터로 SVD 모델을 학습하고, 이를 통해 특정 시점 이후의 유저-아이템의 점수를 예측하였습니다. 마찬가지로 이번 예제에서도 유저-상품의 점수를 예측하여 상품 추천에 활요해봅시다.
우선 SVD 예측 모델 학습을 진행하기에 앞서 학습 데이터인 11월 이전의 데이터에서 추천 대상이 되는 유저와 상품은 얼마나 되는지를 탐색해봅시다.추천 대상인 유저와 상품 출력하기¶
In [25]:# 추천 대상 데이터에 포함되는 유저와 상품의 개수를 출력합니다. print(len(df_year_round['CustomerID'].unique())) print(len(df_year_round['StockCode'].unique()))
3970 3608
SVD 모델을 학습함에 있어 Chapter 03에서의 내용과 한 가지 다른 점은 우리의 유저-아이템의 'Rating'에 해당하는 선호도 점수를 가지고 있지 않다는 점입니다. 그렇다면 이 문제를 어떻게 해결할수 있을까요?
바로 피처 엔지니어링을 통해 이 점수를 만들어내야 합니다. 적당한 유저-상품 간의 점수를 만들어내기 위해 앞선 탐색적 데이터 분석에서 정리했던 다음 내용을 떠올려봅시다.- '유저별 구매 횟수는 일반적으로 1~5 사이에 분포되어 있음'
따라서 우리는 이 정보를 이용하여 유저-상품 간의 구매 횟수가 Rating으로 사용하기에 적절한지를 탐색해볼 것입니다. 유저별 구매 횟수가 일반적으로 1~5 사이라면 유저-상품 간의 구매 횟수 역시 크게 다르지 않을 것이기 때문입니다. 다음 코드는 유저-상품 간의 구매 횟수를 계산하여 U-I-R 데이터로 활용할 데이터 프레임을 생성하는 과정입니다.
SVD 모델에 사용할 Rating 탐색하기¶
In [26]:# Ranting 데이터를 생성하기 위한 탐색: 유저-상품간 구매 횟수를 탐색합니다. uir_df = df_year_round.groupby(['CustomerID', 'StockCode'])['InvoiceNo'].nunique().reset_index() uir_df.head()
Out[26]: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사이에 위치하기는 하지만 점수가 낮은 쪽으로 많이 쏠려있는 것을 확인할 수 있습니다. 아마도 이러한 분포를 가진 Ranting으로 SVD 모델을 학습한다면 행렬을 제대로 완성하지 못 할 확률이 높습니다.
In [27]:# Rating(InvoiceNo) 피처의 분포를 탐색합니다. uir_df['InvoiceNo'].hist(bins=20, grid=False)
Out[27]:<AxesSubplot:>
이러한 상황에 적용할 수 있는 피처 엔지니어링 기법으로 로그를 통한 피처 정규화방법이 있습니다. 이는 Chapter 03에서 학습하였던 피처의 정규화의 여러 가지 방법 중 하나입니다. 이 방법의 목적은 위의 실행 결과에 나타난 그래프처럼 데이터의 왜도(Skewness: 한쪽으로 긴 꼬리를 가진 형태의 비대칭적인 분포 정도)가 높은 경우에 '데이터 사이의 편차를 줄여 왜도를 감소시키는 것'에 있습니다. 이는 로그라는 개념의 수학적인 성질에 기반하는 것입니다.
아래의 코드와 실행 결과는 로그를 통한 피처 정규화를 적용한 뒤, InvoiceNo 피처의 분포를 다시 탐색한 것입니다. 여전히 왜도가 높긴 하지만 적용 이전에 비해서는 피처를 Rating으로 쓰기에 조금 더 적합해졌다는 것을 알 수 있습니다.Log Normalization 적용하기¶
In [28]:# Ration(InvoiceNo) 피처를 log normalization 해준 뒤, 다시 분포를 탐색합니다. uir_df['InvoiceNo'].apply(lambda x: np.log10(x)+1).hist(bins=20, grid=False)
Out[28]:<AxesSubplot:>
로그를 통한 정규화 적용 후, 다시 피처 스케일링을 적용하여 1~5 사이의 값으로 변환합니다. 아래의 코드는 변환 이후의 분포를 다시 그래프로 출력한 것입니다. 코드에서 최대-최소 스케일링 방법을 적용하였습니다.
피처 스케일링 적용하기¶
In [29]:# 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)
Out[29]:<AxesSubplot:>
유저-상품 간의 Rating 점수를 정의하였으니 우리에게 필요했던 U-I-R 매트릭스 데이터가 완성되었습니다. 이제 이를 기반으로 유저-상품 간의 점수를 예측하는 SVD 모델을 학습할 수 있습니다. 그리고 이를 통해 상품 추천 시뮬레이션을 진행할 것입니다. 우선 아래의 코드를 통해 데이터셋을 다시 한 번 적합한 형태로 정리합시다.
SVD 모델 학습을 위한 데이터셋 생성하기¶
In [30]:# SVD 모델 학습을 위한 데이터셋을 생성합니다. uir_df = uir_df[['CustomerID', 'StockCode', 'Rating']] uir_df.head()
Out[30]: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 모델 성능 테스트하기¶
In [31]: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: 34.73 seconds RMSE of test dataset in SVD model: RMSE: 0.3398
Out[31]:0.3398000123591349
모델의 대략적인 성능을 알아보았으니 이제 11월 데이터를 모두 학습 데이터로만 사용하여 모델을 학습합니다. 아래 코드에서 생성된 모델로 다음 단계인 상품 추천 시뮬레이션을 진행합니다.
전체 학습 데이터로 SVD 모델 학습하기¶
In [32]:# 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()
Step 3 예측 평가: 상품 추천 시뮬레이션하기¶
이번 단계에서는 앞서 학습한 SVD 모델을 활용하여 상품 추천 시뮬레이션을 수행해봅니다. 추천의 대상이 되는 유저는 11월 이전 데이터에 등장한 모든 유저를 대상으로 합니다. 반면, 유저들에게 추천의 대상이 되는 상품은 아래의 3가지로 분류할 수 있습니다. 이 3가지 분류의 기준은 Step1 에서 탐색했던 신규구매/재구매에 대한 탐색을 기반으로 한것입니다.
- (1)이전에 구매한 적 없던 상품 추천: 신규 구매를 타겟으로 하는 추천
- (2)이전에 구매했던 상품 추천: 재구매를 타겟으로 하는 추천
- (3)모든 상품을 대상으로 상품 추천: 모든 유저-상품의 점수를 고려하여 추천
다음 코드는 11월 이전의 데이터에 등장한 모든 유저와 해당 유저들이 이전에 구매한 적 없던 상품들을 대상으로 한 예측 점수를 딕셔너리 형태로 추출하는 과정입니다. 우선, 이전 코드에서 생선된 결과인 train_data에 build_anti_testset() 함수를 적용하여 U-I-R 데이터에 Rating이 0인 유저-상품 쌍을 test_data 변수로 추출합니다. 이는 구매기록이 없는 유저-상품 쌍을 의미합니다. 그리고 SVD 모델의 test() 함수로 추천 대상이 되는 유저-상품의 Rating을 예측하고 이를 딕셔너리 형태로 추출합니다. 딕셔너리의 형태는 다음 실행 결과에서 볼 수 있듯이 {유저:{상품:점수,상품:점수..}}의 형태로 이루어져 있습니다.
첫 번째 추천 대상의 유저-상품 점수 추출하기¶
In [33]:# 이전에 구매하지 않았던 상품을 예측의 대상으로 선정합니다. 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, '20665': 1, '20719': 1.224974137148373, '20780': 1, '20782': 1.0979684237291023, '20966': 1.0514523752849179, '21035': 1.0788344216614358, '21041': 1.0852500972091303, '21064': 1, '21154': 1.0286417556706084, '21171': 1, '21265': 1, '21578': 1, '21636': 1.010463755...
마찬가지의 방법으로 두 번째 추천 대상인 데이터에 등장한 모든 유저-이전에 구매했던 상품 간의 예측 점수를 딕셔너리 형태로 추출하는 과정을 수행합니다. 이번에는 build_anti_testset() 함수 대신 build_testset() 함수를 사용하여 이전에 구매했었던 상품을 대상으로 test_data를 추출합니다. 그리고 딕셔너리를 추출하는 과정은 위와 동일합니다. 다음 코드와 실행 결과를 통해 두 번째 딕셔너리의 결과를 확인해봅시다.
두 번째 추천 대상의 유저-상품 점수 추출하기¶
In [34]:# 이전에 구매했었던 상품을 예측의 대상으로 선정합니다. 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.1292958917473865}, '12347': {'16008': 1.0454151889211316, '17021': 1.2356270004733243, '20665': 1.2179116286350222, '20719': 2.0732206768015047, '20780': 1.1666002951368866, '20782': 1.2821191828953722, '20966': 1.2250661029436085, '21035': 1.3725063852392658, '21041': 1.615804...
세 번째 추천 대상은 위에서 생성한 두 개의 딕셔너리를 하나의 딕셔너리로 통합하는 과정으로 생성할 수 있습니다. 다음 코드로 모든 유저와 모든 상품 간의 예측 점수를 딕셔너리 형태로 저장합니다.
세 번째 추천 대상의 유저-상품 점수 추출하기¶
In [38]:# 두 딕셔너리를 하나로 통합합니다. 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, '20665': 1, '20719': 1.224974137148373, '20780': 1, '20782': 1.0979684237291023, '20966': 1.0514523752849179, '21035': 1.0788344216614358, '21041': 1.0852500972091303, '21064': 1, '21154': 1.0286417556706084, '21171': 1, '21265': 1, '21578': 1, '21636': 1.010463755...
앞의 단계들을 거쳐 우리는 세 가지 상품 추천의 시뮬레이션 결과를 각각의 딕셔너리 형태로 갖게 되었습니다. 그렇다면 이제 시뮬레이션의 결과가 실제 구매와 얼마나 유사한지 평가해볼 차레입니다. 다음 코드는 11월 이후의 데이터를 테스트 데이터로 활용하기 위해 각 유저들이 11월 이후에 실제로 구매한 상품의 리스트를 데이터 프레임의 형태로 정리한 것입니다. 이를 위해 CustomerID를 그룹으로 하고 StockCode의 set을 추출합니다. 그리고 이 결과에 reset_index() 함수를 적용하여 데이터 프레임 형태로 변환합니다.
시뮬레이션을 테스트할 데이터 프레임 생성하기¶
In [39]:# 11월 이후의 데이터를 테스트 데이터셋으로 사용하기 위한 데이터 프레임을 생성합니다. simulation_test_df = df_year_end.groupby('CustomerID')['StockCode'].apply(set).reset_index() simulation_test_df.columns = ['CustomerID', 'RealOrdered'] simulation_test_df.head()
Out[39]:CustomerID RealOrdered 0 12347 {21265, 23271, 23552, 23497, 20719, 23084, 846... 1 12349 {23198, 22059, 22554, 23296, 23497, 22195, 231... 2 12352 {22624, 23367, 23559, 21669, 23088, 22635, 226... 3 12356 {21843, 22423} 4 12357 {21116, 22714, 23247, 21844, 22508, 22027, 220... 시뮬레이션 테스트용 데이터 프레임에 3개 딕셔너리의 시뮬레이션 결과를 추가합니다. 이를 위해 아래 코드와 같이 add_predicted_stock_set() 이라는 함수를 정의합니다. 이 함수는 유저의 id와 위에서 추출했던 딕셔너리 중 1개를 인자로 입력 받습니다. 그리고 딕셔너리 안에서의 유저 정보를 참고하여 예측된 점수순으로 상품을 정렬하고 리스트 형태로 반환합니다. 이 함수를 데이터 프레임의 apply에 적용하면 PredictedOrder라는 새로운 피처를 생성할 수 있습니다.
시뮬레이션 결과 추가하기¶
In [45]:# 이 데이터 프레임에 상품 추천 시뮬레이션 결과를 추가하기 위한 함수를 정의합니다. 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()
Out[45]:CustomerID RealOrdered PredictedOrder(New) PredictedOrder(Reorder) PredictedOrder(Total) 0 12347 {21265, 23271, 23552, 23497, 20719, 23084, 846... [22197, 84879, 22326, 22993, 22467, 22328, 474... [22726, 22727, 20719, 22728, 22729, 22371, 217... [22726, 22197, 84879, 22326, 22993, 22727, 224... 1 12349 {23198, 22059, 22554, 23296, 23497, 22195, 231... None None None 2 12352 {22624, 23367, 23559, 21669, 23088, 22635, 226... [84086B, 85131B, 22988, 21094, 48188, 22971, 4... [22413, 22423, 22779, 22617, 21755, 22701, 227... [84086B, 85131B, 22988, 21094, 48188, 22971, 4... 3 12356 {21843, 22423} [84086B, 90042A, 22197, 90119, 85131B, 90035A,... [22423, 37450, 22649, 21094, 22699, 22131, 210... [84086B, 90042A, 22197, 90119, 85131B, 90035A,... 4 12357 {21116, 22714, 23247, 21844, 22508, 22027, 220... None None None 코드를 실행한 결과, 총 3개의 상품 추천 시뮬레이션 결과가 추가되었습니다.
이제 테스트 데이터셋을 완성하였으니 추천 시뮬레이션이 실제 구매와 얼마나 비슷하게 예측되었는지를 평가해볼 차례입니다. 우리가 사용할 평가 방식은 다음과 같습니다.
- (1)유저별로 예측된 상품의 점수 순으로 상위 k개의 상품을 추천 대상으로 정의합니다.
- (2)추천한 k개의 상품 중, 실제 구매로 얼마만큼 이어졌는지 평가합니다.
이 방식은 우리가 Chapter04 에서 학습했던 분류 모델의 평가 방법 중 하나인 재현도(Recall)와 동일한 개념입니다. 다만 k개의 대상으로 제한한다는 것이 다른 점이지요.
다음 코드의 calculate_recall() 함수는 이 과정을 코드로 정의한 것입니다. real_order 파라미터는 실제 구매한 상품의 리스트이고, predicted_order 파라미터는 예측 점수순으로 정렬된 상품의 리스트입니다. 그리고 k 파라미터는 추천할 개수를 의미합니다. 만약 추천할 대상 상품 리스트가 없다면 11월 이전 데이터셋에 존재하지 않는 유저이기 때문에 NOne을 반환하고, 추천할 상품 리스트가 존재한다면 리스트 중 상위k개를 선정하여 실제 구매한 리스트에 몇 개나 존재하는지를 계산하여 반환합니다.
상품 추천 평가 기준 정의하기¶
In [51]:# 구매 예측의 상위 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) 점수를 계산한 것입니다.
상품 추천 평가하기¶
In [52]:# 시뮬레이션 대상 유저에게 상품을 추천해준 결과를 평가합니다. 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)
이제 이를 이용하여 추천 시뮬레이션의 성능을 평가합니다.
평가 결과 출력하기¶
In [54]:# 평가 결과를 유저 평균으로 살펴봅니다. 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.3152922077922058 0.008311688311688308 0.07363636363636389
위의 실행 결과는 세 가지 추천 시뮬레이션의 평균 재현도를 각각 계산하여 출력한 것입니다. 이미 한 번 구매했던 상품을 대상으로 하여 추천해주엇을 때 평균 재현도는 약 31%, 신규 구매를 대상으로 할 때는 0.9%, 전체 상품을 대상으로 할 때는 약 7% 정도로 나타났습니다.
이를 통해 우리는 재구매할만한 상품을 추천해주는 것이 새로운 상품을 추천해주는 것보다 더 좋은 결과를 낼 것이라고 예상할 수 있습니다. 아마도 이 온라인 스토어의 주 구매자는 도매상이기 때문에 새로운 상품을 구매하는 것보다는 기존의 상품을 다시 구매하는 성향이 강한 것이 아닐까 추측해 볼 수 있습니다
다음으로 추천 시뮬레이션 각각의 세부 결과를 살펴보겠습니다. 아래의 코드는 이미 한 번 구매했던 상품을 대상으로 하여 추천해 주었을때의 재현도를 value_counts() 함수로 상세하게 출력한 것입니다.재구매 상품 추천의 상세 결과¶
In [55]:# 평가 결과를 점수 기준으로 살펴봅니다. simulation_test_df['top_k_recall(Reorder)'].value_counts()
Out[55]:0.000000 457 0.200000 392 0.400000 287 0.600000 200 0.800000 107 1.000000 78 0.500000 7 0.250000 6 0.666667 4 0.750000 1 0.333333 1 Name: top_k_recall(Reorder), dtype: int64
실행 결과를 다음과 같이 해석할 수 있습니다.
- 재현도 0:473명은 5개를 추천해준다면 하나도 구매하지 않을 것으로 예상된다.
- 재현도0.2:379명은 5개를 추천해준다면 1개의 상품을 구매할 것으로 예상된다.
만약 5개의 추천 상품을 제공한다면, 이 중 과반수 이상은 실제 구매로 이어질 것으로 예상되기 때문에 이 시뮬레이션 결과는 제법 성공적인 예측을 한 것입니다.
이번에는 이전에 구매한 적 없는 상품을 대상으로 추천한 결과를 살펴보겠습니다. 다음 실행 결과를 살펴보겠습니다. 다음 실행 결과처럼 전체적으로 0에 가까운 재현도를 보이고 있습니다. 따라서 대부분의 유저는 추천된 상품을 구매하지 않을 것으로 보이며 이 시뮬레이션 결과는 좋지 않은 것으로 평가할 수 있습니다.신규 상품 추천의 상세 결과¶
In [56]:# 평가 결과를 점수 기준으로 살펴봅니다. simulation_test_df['top_k_recall(New)'].value_counts()
Out[56]:0.0 1489 0.2 40 0.4 9 0.6 2 Name: top_k_recall(New), dtype: int64
전체 상품을 대상으로 추천한 결과를 살펴봅시다. 두 번째 시뮬레이션 결과보다는 조금 낫지만 이번 시뮬레이션 결과 역시 그다지 좋지 않은 것으로 보입니다.
전체 상품 추천의 상세 결과¶
In [57]:# 평가 결과를 점수 기준으로 살펴봅니다. simulation_test_df['top_k_recall(Total)'].value_counts()
Out[57]:0.0 1203 0.2 202 0.4 72 0.6 39 0.8 16 1.0 8 Name: top_k_recall(Total), dtype: int64
3개의 시뮬레이션의 평가 결과, 그 중 재구매할만한 상품을 추천해 주는 것이 가장 좋은 시뮬레이션인 것으로 평가되었습니다. 이제 '연말 선물로 구매할만한 상품 추천하기'시뮬레이션을 아래와 같이 최종 정리해봅시다.
시뮬레이션 최종 결과 정리¶
In [59]:# 추천 시뮬레이션 결과를 살펴봅니다. 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()
Out[59]:구매자ID 실제주문 5개추천결과 Top5추천_주문재현도 923 15172 {10135, 35923, 20970, 23209, 20682, 22863, 235... [22727, 23203, 20972, 23233, 22569] 0.0 765 14704 {21116, 22208, 21615, 23109, 21900, 21162, 222... [21034, 85123A, 20914, 22193, 82482] 0.2 1742 17758 {22694, 23332, 23503, 22747, 21744, 22112, 233... [22998, 85099B, 47566, 23209, 21034] 0.2 1762 17813 {21428, 23296, 85066, 23360, 82483, 22113, 229... [82486, 82484, 47566, 22151, 21094] 0.0 1725 17716 {23203, 22083, 23344, 23209, 22086, 23320, 229... [85123A, 21670, 21669, 21673, 21672] 0.0 출처 : "이것이 데이터 분석이다 with 파이썬"
'이것이 데이터 분석이다 with 파이썬' 카테고리의 다른 글
chapter-3.3 미래에 볼 영화의 평점 예측하기 (0) 2021.10.13 chapter_4.1 타이타닉 생존자 가려내기 (0) 2021.10.06 chapter-3.2 비트코인 시세 예측하기 (0) 2021.09.30 chapter-3.1 프로야구 선수의 다음 해 연봉 예측하기 (0) 2021.09.30 chapter-1 데이터에서 인사이트 발견하기 (0) 2021.09.26