import pandas as pd
import scipy.sparse as sparse
import numpy as np
from scipy.sparse.linalg import spsolve
import openpyxl
import implicit
# conda install -c conda-forge openpyxl
website_url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/00352/Online%20Retail.xlsx'
retail_data = pd.read_excel(website_url, engine='openpyxl')
# openpyxl error일 시에 engine='openpyxl'
#송장번호, 아이템ID, 아이템 설명, 구매량,
#구매일자, 아이템 가격, 고객ID, 고객 국적 -columns
--------------------------------------------#image_1
clean_data = retail_data[retail_data['CustomerID'].notna()]
clean_data.info()
# customer_id 결측치 or NA 제거
--------------------------------------------#image_2
item_lookup = clean_data[['StockCode', 'Description']].drop_duplicates()
item_lookup['StockCode'] = item_lookup['StockCode'].astype(str)
item_lookup.head()
#아이템 ID에 따른 아이템 설명 테이블을 만듦
#추천한 아이템이 어떤 아이템인지, 어떤 아이템을 구매한 건지 파악 하기위함
----------------------------------------------#image_3
#ALS 알고리즘을 사용하기에 앞서 데이터 형태를 만들어 줘야함
# unique 고객ID -> 행
# unique ItemID -> 열
# 각 유저가 각 아이템을 구매한 총 횟수 -> 값
# 희소행렬 (Sparse Matrix)
clean_data['CustomerID'] = clean_data['CustomerID'].astype(int)
#matrix가 불필요한 메모리까지 차지하기에 sparse matrix로 만듦 -> 값이 없는 매트릭스도 존재하기 때문
clean_data = clean_data[['CustomerID', 'StockCode', 'Quantity']]
# 고객 ID, 아이템 ID, 구매량
group_clean = clean_data.groupby(['CustomerID', 'StockCode']).sum().reset_index()
# 고객과 아이템 별로 총 얼마나 구매했는지 -> .sum()
group_clean[group_clean['Quantity'] == 0] = 1
#구매 수량이 0인 데이터는 환불한 고객이기에 1로 변환
group_purchased = group_clean[group_clean['Quantity']> 0]
# 구매한 물건만 뽑기
group_purchased.head()
----------------------------------------------#image_4
customers = list(np.sort(group_purchased['CustomerID'].unique()))
products = list(group_purchased['StockCode'].unique())
quantity = list(group_purchased['Quantity'])
rows = group_purchased['CustomerID'].astype('category').cat.codes
cols = group_purchased['StockCode'].astype('category').cat.codes
#print(len(customers))
#print(len(products))
purchased_sparse = sparse.csr_matrix((quantity, (rows, cols)), shape=(len(customers), len(products)))
# unique 고객ID -> 행
# unique ItemID -> 열
# 각 유저가 각 아이템을 구매한 총 횟수 -> 값
# 희소행렬 (Sparse Matrix)을 만들어줌
----------------------------------------------#image_5
def make_train (matrix, percentage =0.2):
'''
-----------------------------------------------------
설명
유저-아이템 행렬 (matrix)에서
1. 0이상의 값을 가지면 1의 값을 갖도록 binary하게 테스트 데이터를 만들고
2. 훈련 데이터는 원본 행렬에서 percentage 비율만큼 0으로 바뀜
-----------------------------------------------------
반환
training_set: 훈련 데이터에서 percentage 비율만큼 0으로 바뀐 행렬
test_set: 원본 유저-아이템 행렬의 복사본
user_inds: 훈련 데이터에서 0으로 바뀐 유저의 index
'''
test_set = matrix.copy()
test_set[test_set !=0] = 1 # binary하게 만들기
training_set = matrix.copy()
nonzero_inds = training_set.nonzero()
nonzero_pairs = list(zip(nonzero_inds[0], nonzero_inds[1]))
random.seed(0)
num_samples = int(np.ceil(percentage * len(nonzero_pairs)))
samples = random.sample (nonzero_pairs, num_samples)
user_inds = [index[0] for index in samples]
item_inds = [index[1] for index in samples]
training_set[user_inds, item_inds] = 0
training_set.eliminate_zeros()
return training_set, test_set, list(set(user_inds))
product_train, product_test, product_users_altered = make_train(purchase_sparse, 0.2)
# import implicit
# Cython과 OpenMP를 이용해 병렬적으로 모형을 처리할 수 있기 때문에 훨씬 빠르게 ALS를 구현할 수 있습니다.
alpha = 15
model = implicit.als.AlternatingLeastSquares(factors=20, regularization = 0.1, iterations = 50)
conf = (product_train*alpha).astype('double') #->행렬 형태를 double로 지정
model.fit(conf.T)
user_vecs, item_vecs = model.item_factors, model.user_factors
#함수 구현
def get_items_purchased(customer_id, mf_train, customer_list, products_list, item_lookup):
cust_ind = np.where (customer_list == customer_id)[0][0]
purchased_ind = mf_train[cust_ind,:].nonzero()[1]
prod_codes = products_list[purchased_ind]
return item_lookup.loc[item_lookup.StockCode.isin(prod_codes)]
customers_arr = np.array(customers)
products_arr = np.array(products)
image_1
image_2
image_3
image_4
image_5
image_6
from sklearn.preprocessing import MinMaxScaler
def rec_items(customer_id, mf_train, user_vecs, item_vecs, customer_list, item_list, item_lookup, num_items = 10):
'''
유저의 추천 아이템 반환
-----------------------------------------------------
INPUT
1. customer_id - Input the customer's id number that you want to get recommendations for
2. mf_train: 훈련 데이터
3. user_vecs: 행렬 분해에 쓰인 유저 벡터
4. item_vecs: 행렬 분해에 쓰인 아이템 벡터
5. customer_list: 평점 행렬의 행에 해당하는 고객 ID
6. item_list: 평점 행렬의 열에 해당하는 아이템 ID
7. item_lookup: 아이템 ID와 설명을 담은 테이블
8. num_items: 추천할 아이템 개수
-----------------------------------------------------
반환
구매한 적이 없는 아이템 중 예측 평점이 높은 최고 n개의 추천 아이템
'''
cust_ind = np.where(customer_list == customer_id)[0][0]
pref_vec = mf_train[cust_ind,:].toarray() # 훈련 데이터의 실제 평점
pref_vec = pref_vec.reshape(-1) + 1 # 1을 더해서 환불한 것도 구매한 걸로 간주
pref_vec[pref_vec > 1] = 0 # 구매한 것들을 모두 0으로
rec_vector = user_vecs[cust_ind,:].dot(item_vecs.T) # 추천 시스템에 기반한 예측 평점
# Min-Max Scaling
min_max = MinMaxScaler()
rec_vector_scaled = min_max.fit_transform(rec_vector.reshape(-1,1))[:,0]
recommend_vector = pref_vec*rec_vector_scaled # 구매하지 않은 아이템에 대해서만 예측 평점이 남도록
product_idx = np.argsort(recommend_vector)[::-1][:num_items] # num_items만큼 내림차순으로 평점 정렬한 index
rec_list = []
for index in product_idx:
code = item_list[index] # 아이템 id
# id와 description 담기
rec_list.append([code, item_lookup['Description'].loc[item_lookup['StockCode'] == code].iloc[0]])
codes = [item[0] for item in rec_list]
descriptions = [item[1] for item in rec_list]
final_frame = pd.DataFrame({'StockCode': codes, 'Description': descriptions})
return final_frame[['StockCode', 'Description']]
image_7
참고문헌
https://assaeunji.github.io/machine%20learning/2020-11-29-implicitfeedback/