본문 바로가기
데이터분석/Python

머신러닝 Example by Python - 구매 이력 테이터를 이용한 사용자 그룹 만들기

by 버섯도리 2022. 1. 14.
## 데이터의 기초 통계량 계산 및 시각화

import time
from scipy import stats


# 데이터 구조 정의
# 사용자 ID를 키로 갖고 상품 코드의 셋을 값으로 갖는 딕셔너리와
# 상품 코드를 키로 갖고 사용자 ID의 셋을 값으로 갖는 딕셔너리
user_product_dic = {}
product_user_dic = {}

# 상품 코드를 키로 갖고 상품명을 값으로 갖는 딕셔너리
# 군집화의 내용을 확인하는 단계에서 상품명을 사용합니다.
product_id_name_dic = {}

# 파일을 읽어 위에서 정의한 데이터 구조를 채웁니다.
# 파일은 여기에서...
# https://archive.ics.uci.edu/ml/machine-learning-databases/00352/
for line in open('D:/99_Study/FirstML\source/10_UserGroup/online_retail_utf.txt'):
    
    # 데이터를 한 행씩 읽어 필요한 항목을 저장합니다.
    line_items = line.strip().split('\t')
    user_code = line_items[6]
    product_id = line_items[1]
    product_name = line_items[2]
    
    # 사용자 ID가 없을 경우 무시합니다.
    if len(user_code) == 0:
        continue
    
    # 영국에서 구매한 사용자만 고려하므로 국가가 united kingdom이 아닌 경우엔 무시합니다.
    country = line_items[7]
    if country != 'United Kingdom':
        continue
    
    # 연도를 읽을 때 에러 처리. 파일 헤더를 무시합니다.
    
    try:
#        invoice_year = time.strptime(line_items[4], '%m/%d/%y %H:%M').tm_year
        invoice_year = time.strptime(line_items[4], '%Y-%m-%d %H:%M').tm_year
        
    except ValueError:
        continue
    
    # 2011년에 일어난 구매가 아닌 것은 무시합니다.
    if invoice_year != 2011:
        continue
    
    # 읽은 정보로 데이터 구조를 채웁니다.
    # 상품 가짓수를 고려하므로 상품 코드를 셋으로 가지도록 하겠습니다.
    user_product_dic.setdefault(user_code, set())
    user_product_dic[user_code].add(product_id)
    
    product_user_dic.setdefault(product_id, set())
    product_user_dic[product_id].add(user_code)
    
    product_id_name_dic[product_id] = product_name
    
# 데이터 구조를 다 채웠으므로 각 사용자가 구매한 상품 가짓수로 리스트를 만듭니다.
product_per_user_li = [len(x) for x in user_product_dic.values()]


# 이 장에서 사용할 최종 사용자 수와 상품 가짓수를 출력합니다.
print('# of users:', len(user_product_dic))
print('# of products:', len(product_user_dic))

# 각 사용자가 구매한 상품 가짓수로 기초 통계량을 출력합니다.
print(stats.describe(product_per_user_li))


## 사용자가 구매한 상품 가짓수

from collections import Counter
import matplotlib.pyplot as plt

# 한글 폰트 사용을 위해서 세팅
from matplotlib import font_manager, rc
font_path = "C:/Windows/Fonts/NGULIM.TTF"
font = font_manager.FontProperties(fname=font_path).get_name()
rc('font', family=font)

# 사용자가 구매한 고유 상품 가짓수를 플롯해봅니다.
plot_data_all = Counter(product_per_user_li)
plot_data_x = list(plot_data_all.keys())
plot_data_y = list(plot_data_all.values())
plt.xlabel('고유 상품 가짓수')
plt.ylabel('사용자 수')
plt.scatter(plot_data_x, plot_data_y, marker='+')

plt.show()


## 예외적인 구매 패턴을 보이는 사용자 제거하기

# 구매한 상품의 가짓수가 1인 사용자의 사용자 ID를 찾습니다.
min_product_user_li = [k for k,v in user_product_dic.items() if len(v)==1]

# 마찬가지로 구매한 상품의 가짓수가 600개 이상인 사용자의 사용자 ID를 찾습니다.
max_product_user_li = [k for k,v in user_product_dic.items() if len(v)>=600]

print('# of users purchased one product : %d' % (len(min_product_user_li)))
print('# of users purchased more than 600 product : %d' % (len(max_product_user_li)))

# 찾아낸 사용자를 군집화에 사용할 user_product_dic에서 제외합니다.
user_product_dic = {k:v for k,v in user_product_dic.items() if len(v)>1 and len(v)<=600}

print('# of left user : %d' % (len(user_product_dic)))

# 구매한 상품별 갯수를 딕셔너리로 저장

id_product_dic = {}

for product_set_li in user_product_dic.values():
    for x in product_set_li:
        if x in id_product_dic:
            product_id = id_product_dic[x]
        else:
            id_product_dic.setdefault(x, len(id_product_dic))

print("# of left items : %d" % (len(id_product_dic)))


## 원-핫 인코딩을 이용한 피처 생성

# 사용자 ID 참조를 위한 딕셔너리
id_user_dic = {}
# 군집화의 입력으로 사용할 리스트
user_product_vec_li = []
# 군집화에서 사용할 총 고유 상품 가짓수. 즉, 원-핫 인코딩으로 변환할 피처의 가짓수
all_product_count = len(id_product_dic)

for user_code, product_per_user_set in user_product_dic.items():
    # 고유 상품 가짓수를 길이로 하는 리스트 생성 (초기값=0)
    user_product_vec = [0] * all_product_count
    
    # id_user_dic의 길이를 이용하여 사용자 ID를 0부터 시작하는 user_id로 바꿉니다.
    id_user_dic[len(id_user_dic)] = user_code
    
    # 사용자가 구매한 상품 코드를 키로 하여 user_product_vec에서의
    # 해당 상품 코드의 상품 ID를 찾습니다. 그리고 값을 1로 셋팅합니다.
    for product_id in product_per_user_set:
        user_product_vec[id_product_dic[product_id]] = 1
    
    # 한 사용자의 처리가 끝났으므로 이 사용자의 user_product_vec을 배열에 추가합니다.
    # 이때 배열의 인덱스는 새로 정의한 user_id가 됩니다.
    user_product_vec_li.append(user_product_vec)

# print(id_user_dic[0])
# print(user_product_dic[id_user_dic[0]])
# print(user_product_vec_li[0])
# print(len(user_product_vec_li[0]))


### K-Means 군집화

## 사이킷런의 predict 함수를 이용하여 사용자가 속할 클러스터 예측

from sklearn.cluster import KMeans
import random


# 학습용과 평가용 데이터로 나누기 위해 사용자-상품 벡터를 사용합니다.
random.shuffle(user_product_vec_li)

# 학습용 데이터에 사용자 2500명을, 평가용 데이터에 나머지 사용자를 넣습니다.
# 학습용 데이터에 있는 사용자 정보만을 가지고 클러스터를 만든 후
# 평가용 데이터의 사용자가 어느 클러스터에 속하는지 알아봅니다.
train_data = user_product_vec_li[:2500]
test_data = user_product_vec_li[2500:]
print("# of train data:%d, # of test data:%d" % (len(train_data),len(test_data)))

# 학습 데이터를 군집화하여 4개의 클러스터를 생성한 후, 그 결과를 km_predict에 저장합니다.
km_predict = KMeans(n_clusters=4, init='k-means++', n_init=10, max_iter=20).fit(train_data)

# km_predict의 predict 함수를 이용하여 평가 데이터가 전 단계에서 만든 4개의 클러스터 중 어느 곳에
# 속하는지 살펴봅니다.
km_predict_result = km_predict.predict(test_data)
print(km_predict_result)


## 사이킷런을 이용하여 실루엣 계수 구하기

from sklearn.metrics import silhouette_score
import numpy as np

test_data = np.array(user_product_vec_li)

for k in range(2, 9):
    km = KMeans(n_clusters=k).fit(test_data)
    print("score for %d clusters:%.3f" % (k, silhouette_score(test_data, km.labels_)))


## 사이킷런을 이용하여 클러스터 수 K에 따라 달라지는 급내제곱합 구하기 (엘보 방법)

# 클러스터 수를 키로 하고 inertia를 값으로 하는 딕셔너리입니다.
ssw_dic = {}

# 클러스터 수 K를 1부터 8까지 바꾸어가며 급내제곱합의 평균값을 계산하고,
# K를 키로 지정하여 딕셔너리에 넣습니다.
for k in range(1, 8):
    km = KMeans(n_clusters=k).fit(test_data)
    ssw_dic[k] = km.inertia_

# 클러스터 수 K를 x축으로, inertia를 y축으로 하여 플롯을 그립니다.
plot_data_x = list(ssw_dic.keys())
plot_data_y = list(ssw_dic.values())
plt.xlabel("# of clusters")
plt.ylabel("withis ss")
plt.plot(plot_data_x, plot_data_y, linestyle="-", marker='o')
plt.show()


## 클러스터에 속한 사용자가 구매한 상품명에 나타나는 키워드의 빈도 구하기

def analyze_clusters_keywords(labels, product_id_name_dic, user_product_dic, id_user_dic):
    # 각 클러스터의 ID와 클러스터에 들어있는 사용자 수를 출력합니다.
    print(Counter(labels))
    cluster_item = {}
    
    for i in range(len(labels)):
        cluster_item.setdefault(labels[i], [])
        
        # 각 사용자의 임시 ID i에 대해 사용자 코드를 찾은 후
        # 그 사용자 코드와 연결된 구매상품의 ID를 참조한 후
        # 그 ID를 이용해 상품명을 찾아
        # 딕셔너리에 클러스터 ID를 키로, 상품명을 값으로 추가합니다.
        
        for x in user_product_dic[id_user_dic[i]]:
            cluster_item[labels[i]].extend([product_id_name_dic[x]])
    
    for cluster_id, product_name in cluster_item.items():
        # 각 클러스터 안의 상품명을 join 명령으로 합쳐 하나의 문자열로 만든 뒤
        # OF를 공백으로 replace하고
        # 스페이스 혹은 탭으로 split하여 키워드로 분해한 뒤
        # 연속되는 두 키워드를 합쳐서 하나의 키워드로 만듭니다.
        bigram = []
        product_name_keyword = (' ').join(product_name).replace(' OF ', ' ').split()
        
        for i in range(0, len(product_name_keyword) - 1):
            bigram.append(' '.join(product_name_keyword[i:i+2]))
        
        # 클러스터의 ID와 그 ID를 가지는 클러스터에 속한 사용자들이
        # 구매한 상품의 상품명 안에서 가장 자주 나타나는 단어 20개를 빈도순으로 출력합니다.
        print('cluster_id : ', cluster_id)
#        print(Counter(product_name_keyword).most_common(20))
        print(Counter(bigram).most_common(20))
                
km = KMeans(n_clusters=2, n_init=10, max_iter=20)
km.fit(test_data)
analyze_clusters_keywords(km.labels_, product_id_name_dic, user_product_dic, id_user_dic)


## 각 클러스터의 사용자가 구입한 고유 상품 가짓수의 기초 통계량 구하기

def analyze_clusters_product_count(labels, user_product_dic, id_user_dic):
    product_len_dic = {}
    
    for i in range(0, len(labels)):
        product_len_dic.setdefault(labels[i], [])
        
        # 클러스터의 ID를 키로 하는 딕셔너리에
        # 그 클러스터에 속한 사용자가 구매한 고유 상품의 가짓수를 저장합니다.
        product_len_dic[labels[i]].append(len(user_product_dic[id_user_dic[i]]))
        
    for k, v in product_len_dic.items():
        print('cluster : ', k)
        print(stats.describe(v))

analyze_clusters_product_count(km.labels_, user_product_dic, id_user_dic)


### 계층적 군집화

## scipy를 이용한 집괴적 군집화

from scipy.cluster.hierarchy import linkage
from scipy.cluster.hierarchy import dendrogram

# scipy의 집괴적 군집화 함수
# 이번에는 두 클러스터에 속한 모든 샘플 간의 거리 평균을
# 클러스터를 집괴하는 기준으로 합니다.
# 거리 함수로는 유클리디안 함수를 씁니다.
row_clusters = linkage(test_data, method='complete', metric='euclidean')

# 사용자 ID를 사용자 코드로 반환합니다.
tmp_label=[]
for i in range(len(id_user_dic)):
    tmp_label.append(id_user_dic[i])

# 플롯을 그립니다.
row_denr = dendrogram(row_clusters, labels=tmp_label)
# =============================================================================
#   File "C:\Users\pc1\anaconda3\lib\site-packages\scipy\cluster\hierarchy.py", line 3290, in dendrogram
#     raise ValueError("Dimensions of Z and labels must be consistent.")
# 
# ValueError: Dimensions of Z and labels must be consistent.
# 
# =====> 버그인 듯 싶어 hierarchy.py 직접 수정.... (주석 처리)
# =============================================================================
plt.tight_layout()
plt.ylabel('euclid')
plt.show()
# ---> 플롯이 따로 그려지는데... ㅡㅡ


## 샘플 데이터를 이용한 집괴적 군집화와 계통 트리 만들기 for 계통트리 설명

small_test_data = np.array(random.sample(user_product_vec_li, 10))
small_row_clusters = linkage(small_test_data, method="complete", metric="euclidean")

plt.figure(figsize=(25,10))
row_denr = dendrogram(small_row_clusters, labels=list(range(len(small_test_data))), leaf_font_size=20.)
plt.ylabel('euclid')
plt.show()


## 사이킷런을 이용한 집괴적 군집화

from sklearn.cluster import AgglomerativeClustering

ward = AgglomerativeClustering(n_clusters=2, affinity='euclidean', linkage='ward')
ward.fit(test_data)


## 집괴적 군집화로 생성된 클러스터 내부의 상품명 키워드 살펴보기

analyze_clusters_keywords(ward.labels_, product_id_name_dic, user_product_dic, id_user_dic)
 
 
 

 

 

 

 
출처 : 처음 배우는 머신러닝 : 기초부터 모델링, 실전 예제, 문제 해결까지