# 영국 선물샵 온라인 도매 거래 데이터 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") df.head()
df.info() df.isnull().sum()
- 데이터 전처리
#결측치 제거 df = df.dropna() print(df.shape) #상품 수량이 음수인 경우를 제거 print(df[df['Quantity']<=0].shape[0]) df = df[df['Quantity']>0] #상품 가격이 0 이하인 경우를 제거 print(df[df['UnitPrice']<=0].shape[0]) df = df[df['UnitPrice']>0] # 상품 코드가 일반적이지 않은 경우를 탐색 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() # 상품 코드가 일반적이지 않은 경우를 제거합니다. df = df[df['ContainDigit'] == True]
- 탐색적 데이터 분석
# 거래 데이터에서 가장 오래된 데이터와 가장 최신의 데이터를 탐색 df['date'] = df['InvoiceDate'].dt.date print(df['date'].min()) print(df['date'].max()) # 일자별 총 거래 수량을 탐색 date_quantity_series = df.groupby('date')['Quantity'].sum() date_quantity_series.plot()
# 일자별 총 거래 횟수를 탐색 date_transaction_series = df.groupby('date')['InvoiceNo'].nunique() date_transaction_series.plot()
# 일자별 거래된 상품의 unique한 갯수, 즉 상품 거래 다양성을 탐색 date_unique_item_series = df.groupby('date')['StockCode'].nunique() date_unique_item_series.plot()
# 총 유저의 수를 계산하여 출력 print(len(df['CustomerID'].unique())) # 유저별 거래 횟수를 탐색 customer_unique_transaction_series = df.groupby('CustomerID')['InvoiceNo'].nunique() customer_unique_transaction_series.describe() # 상자 그림 시각화 plt.boxplot(customer_unique_transaction_series.values) plt.show()
# 유저별 아이템 구매 종류 개수를 탐색 customer_unique_item_series = df.groupby('CustomerID')['StockCode'].nunique() customer_unique_item_series.describe() # 상자 그림 시각화 plt.boxplot(customer_unique_item_series.values) plt.show()
# 총 상품 갯수를 탐색 print(len(df['StockCode'].unique())) # 가장 거래가 많은 상품 top 10 탐색 df.groupby('StockCode')['InvoiceNo'].nunique().sort_values(ascending=False)[:10] # 상품별 판매수량 분포를 탐색 print(df.groupby('StockCode')['Quantity'].sum().describe()) plt.plot(df.groupby('StockCode')['Quantity'].sum().values) plt.show()
# 분포를 정렬하여 출력 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() # 거래별로 발생한 가격 분포를 탐색 plt.plot(df.groupby('InvoiceNo')['amount'].sum().values) plt.show()
# 분포를 정렬하여 출력 plt.plot(df.groupby('InvoiceNo')['amount'].sum().sort_values(ascending=False).values) plt.show()
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)
# 11월 이전 데이터에서 구매했던 상품의 set을 추출 customer_item_round_set = df_year_round.groupby('CustomerID')['StockCode'].apply(set) print(customer_item_round_set)
# 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] + "...")
# 11월 이후 데이터에서 구매하는 상품의 set을 추출 customer_item_end_set = df_year_end.groupby('CustomerID')['StockCode'].apply(set) print(customer_item_end_set)
# 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] + "...")
# '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()
# 데이터 프레임에서 전체 유저 수를 출력 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])
# new 피처의 value_counts를 출력하여, 새로운 상품을 얼마나 구매하는지 탐색 df_order_info['new'].value_counts() # 만약 새로운 상품을 구매한다면, 얼마나 많은 종류의 새로운 상품을 구매하는지 탐색 print(df_order_info['new'].value_counts()[1:].describe())
- SVD를 이용한 상품 구매 예측
# 추천 대상 데이터에 포함되는 유저와 상품의 갯수를 출력 print(len(df_year_round['CustomerID'].unique())) print(len(df_year_round['StockCode'].unique())) # Rating 데이터를 생성하기 위한 탐색 : 유저-상품간 구매 횟수를 탐색 uir_df = df_year_round.groupby(['CustomerID', 'StockCode'])['InvoiceNo'].nunique().reset_index() uir_df.head()
# Rating(InvoiceNo) 피처의 분포를 탐색 uir_df['InvoiceNo'].hist(bins=20, grid=False)
# Rating(InvoiceNo) 피처를 log normalization 해준 뒤, 다시 분포를 탐색 uir_df['InvoiceNo'].apply(lambda x: np.log10(x)+1).hist(bins=20, grid=False)
# 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)
# SVD 모델 학습을 위한 데이터셋을 생성 uir_df = uir_df[['CustomerID', 'StockCode', 'Rating']] uir_df.head()
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)
# 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))
""" 11월 이전 데이터에서 유저-상품에 대한 Rating을 기반으로 추천 상품을 선정 1. 이전에 구매하지 않았던 상품 추천 : anti_build_testset()을 사용 2. 이전에 구매했던 상품 다시 추천 : build_testset()을 사용 3. 모든 상품을 대상으로 하여 상품 추천 """ # 이전에 구매하지 않았던 상품을 예측의 대상으로 선정 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] + "...")
- 이전에 구매했던 제품과 이전에 구매하지 않았던 제품을 분할할 수 있습니다.
# 이전에 구매했었던 상품을 예측의 대상으로 선정 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] + "...")
# 두 딕셔너리를 하나로 통합 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] + "...")
# 11월 이후의 데이터를 테스트 데이터셋으로 사용하기 위한 데이터프레임을 생성 simulation_test_df = df_year_end.groupby('CustomerID')['StockCode'].apply(set).reset_index() simulation_test_df.columns = ['CustomerID', 'RealOrdered'] simulation_test_df.head()
# 이 데이터프레임에 상품 추천 시뮬레이션 결과를 추가하기 위한 함수를 정의 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()
- 모델 평가하기
# 구매 예측의 상위 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 # 시뮬레이션 대상 유저에게 상품을 추천해준 결과를 평가합니다. 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())
# 평가 결과를 점수 기준으로 살펴보기 simulation_test_df['top_k_recall(Reorder)'].value_counts() # 평가 결과를 점수 기준으로 살펴보기 simulation_test_df['top_k_recall(New)'].value_counts() # 평가 결과를 점수 기준으로 살펴봅니다. simulation_test_df['top_k_recall(Total)'].value_counts()
# SVD 모델의 추천기준에 부합하지 않는 유저를 추출 not_recommended_df = simulation_test_df[simulation_test_df['PredictedOrder(Reorder)'].isnull()] print(not_recommended_df.shape) not_recommended_df.head()
# 추천 시뮬레이션 결과를 살펴보기 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()
입력 층(데이터 변환 - 가중치) 층(데이터 변환 - 가중치) ....
- 예측을 하고 실제 값과 비교
- 실제 값과 예측한 결과를 비교하는 함수를 손실 함수(Loss Function)라고 합니다.
- 손실 함수가 만들어 낸 점수를 가지고 옵티마이저를 통과한 뒤, 가중치를 수정해서 다시 학습을 합니다.
- 옵티마이저가 BackPropagation(역전파) 알고리즘을 담당합니다.
1/(1+e^(-x))
: 입력값이 0 이하면 0.5 이하의 값 출력, 0 이상은 0.5 이상의 값 출력, 딥러닝에서는 역전파(Back Propagation) 알고리즘을 사용하는데, 하나의 층을 통과할 때 마다 가중치가 약해지는데, 이렇게 된다면 Gradient Vanishing(기울기 소멸)현상이 발생함