Chapter 16 추천시스템
library(tidyverse)
추천시스템(recommender system)은 상품, 웹페이지, 신문 기사 등에 대한 소비자의 성향을 파악하여 그에 부합하는 새로운 상품 등을 추천하고자 하는 목적으로 개발되며, 접근 방식에 따라 내용기반(content-based) 방법, 협업 필터링(collaborative filtering), 결합방식(hybrid) 등으로 분류된다.
16.1 필요 R package 설치
본 장에서 필요한 R 패키지들은 아래와 같다.
package | version |
---|---|
tidyverse | 1.3.1 |
tidytext | 0.3.1 |
16.2 내용기반 추천시스템
내용기반 추천시스템은 주로 문서 등의 추천에 활용되고 있다.
- \(N\): 전체 문서의 수
- \(f_{ij}\): 문서 \(j\)에 나타난 단어 \(i\)의 빈도수
- \(n_i\): 단어 \(i\)가 한 번 이상 나타난 문서의 수
우선 tidytext
패키지를 로드하자.
library(tidytext)
janeaustenr
패키지에 있는 Jane Austen의 6개 소설에 대한 텍스트 데이터를 로드하자. 해당 데이터는 책 내용이 담긴 text
라는 컬럼과 책 제목인 book
컬럼으로 이루어진 데이터 프레임이다.
library(janeaustenr)
<- austen_books()
tidy_books head(tidy_books)
## # A tibble: 6 x 2
## text book
## <chr> <fct>
## 1 "SENSE AND SENSIBILITY" Sense & Sensibility
## 2 "" Sense & Sensibility
## 3 "by Jane Austen" Sense & Sensibility
## 4 "" Sense & Sensibility
## 5 "(1811)" Sense & Sensibility
## 6 "" Sense & Sensibility
해당 데이터 프레임에 담긴 책의 수는 아래와 같다.
<- nlevels(tidy_books$book)
n_book print(n_book)
## [1] 6
책의 내용 text를 단어 단위로 나누어 각 행으로 저장하자.
<- tidy_books %>% unnest_tokens(word, text)
tidy_words
head(tidy_words)
## # A tibble: 6 x 2
## book word
## <fct> <chr>
## 1 Sense & Sensibility sense
## 2 Sense & Sensibility and
## 3 Sense & Sensibility sensibility
## 4 Sense & Sensibility by
## 5 Sense & Sensibility jane
## 6 Sense & Sensibility austen
이 데이터 프레임을 기반으로, 단어 \(i\)가 문서 \(j\)에 나타난 단어 빈도수(term frequency)를 모든 단어 \(i\)와 모든 문서 \(j\)에 대해 계산하자.
\[\begin{equation*} TF_{ij} = \frac{f_{ij}}{\sum_k f_{kj}} \end{equation*}\]
<- tidy_words %>%
tf_results group_by(book, word) %>%
summarize(n = n()) %>%
mutate(tf = n / sum(n)) %>%
select(-n) %>%
ungroup() %>%
complete(book, word, fill = list(tf = 0))
## `summarise()` has grouped output by 'book'. You can override using the `.groups` argument.
이 때, 단어 빈도수가 높은 단어들은 대체로 너무 흔한 단어들이어서 중요한 의미를 지니지 않은 경우가 많다. 아래와 같이, “the”, “to”, “and” 등의 단어들이 사용 빈도가 매우 높은 단어들이다.
%>% arrange(desc(tf)) %>% head(10) tf_results
## # A tibble: 10 x 3
## book word tf
## <fct> <chr> <dbl>
## 1 Northanger Abbey the 0.0409
## 2 Persuasion the 0.0398
## 3 Mansfield Park the 0.0387
## 4 Pride & Prejudice the 0.0354
## 5 Sense & Sensibility to 0.0343
## 6 Sense & Sensibility the 0.0342
## 7 Mansfield Park to 0.0341
## 8 Pride & Prejudice to 0.0341
## 9 Mansfield Park and 0.0339
## 10 Persuasion to 0.0336
따라서, 단어의 중요도를 정의할 때 단어 \(i\)의 역문서 빈도수(inverse document frequency)를 함께 고려한다.
\[\begin{equation*} IDF_{i} = \log \frac{N}{n_i} \end{equation*}\]
<- tf_results %>%
idf_results filter(tf > 0) %>%
group_by(word) %>%
summarize(n = n()) %>%
mutate(idf = log(n_book / n)) %>%
select(-n)
%>% arrange(desc(idf)) %>% head(10) idf_results
## # A tibble: 10 x 2
## word idf
## <chr> <dbl>
## 1 _accepted_ 1.79
## 2 _accident_ 1.79
## 3 _adair_ 1.79
## 4 _addition_ 1.79
## 5 _advantages_ 1.79
## 6 _affect_ 1.79
## 7 _against_ 1.79
## 8 _agreeable_ 1.79
## 9 _air_ 1.79
## 10 _allow_ 1.79
최종적으로 단어의 중요도를 위에서 정의한 단어 빈도수와 역문서 빈도수의 곱으로 아래와 같이 구하며, 이를 TF-IDF 가중치라 한다.
\[\begin{equation*} w_{ij} = TF_{ij} \times IDF_{i} \end{equation*}\]
<- inner_join(tf_results, idf_results, by = "word") %>%
tf_idf_results mutate(tf_idf = tf * idf)
TF-IDF 가중치가 높은 단어들을 살펴보자.
%>%
tf_idf_results arrange(desc(tf_idf)) %>%
head(10)
## # A tibble: 10 x 5
## book word tf idf tf_idf
## <fct> <chr> <dbl> <dbl> <dbl>
## 1 Sense & Sensibility elinor 0.00519 1.79 0.00931
## 2 Sense & Sensibility marianne 0.00410 1.79 0.00735
## 3 Mansfield Park crawford 0.00307 1.79 0.00551
## 4 Pride & Prejudice darcy 0.00305 1.79 0.00547
## 5 Persuasion elliot 0.00304 1.79 0.00544
## 6 Emma emma 0.00488 1.10 0.00536
## 7 Northanger Abbey tilney 0.00252 1.79 0.00452
## 8 Emma weston 0.00242 1.79 0.00433
## 9 Pride & Prejudice bennet 0.00241 1.79 0.00431
## 10 Persuasion wentworth 0.00228 1.79 0.00409
대체로 소설에 나타나는 인물의 이름이 높은 가중치를 보이는데, 이는 인물의 이름이 소설 한 권에 걸쳐 여러 번 나타나 단어 빈도수가 높으며, 또한 각각의 소설이 서로 다른 인물명을 등장시킴으로써 역문서 빈도수 또한 높기 때문이다.
위와 같은 TF-IDF 가중치 계산은 tidytext
패키지의 bind_tf_idf
함수를 이용하여 간편하게 구할 수 있다.
%>%
tidy_words group_by(book, word) %>%
summarize(n = n()) %>%
ungroup() %>%
bind_tf_idf(word, book, n) %>%
arrange(desc(tf_idf)) %>%
head(10)
## `summarise()` has grouped output by 'book'. You can override using the `.groups` argument.
## # A tibble: 10 x 6
## book word n tf idf tf_idf
## <fct> <chr> <int> <dbl> <dbl> <dbl>
## 1 Sense & Sensibili… elinor 623 0.00519 1.79 0.00931
## 2 Sense & Sensibili… marianne 492 0.00410 1.79 0.00735
## 3 Mansfield Park crawford 493 0.00307 1.79 0.00551
## 4 Pride & Prejudice darcy 373 0.00305 1.79 0.00547
## 5 Persuasion elliot 254 0.00304 1.79 0.00544
## 6 Emma emma 786 0.00488 1.10 0.00536
## 7 Northanger Abbey tilney 196 0.00252 1.79 0.00452
## 8 Emma weston 389 0.00242 1.79 0.00433
## 9 Pride & Prejudice bennet 294 0.00241 1.79 0.00431
## 10 Persuasion wentwor… 191 0.00228 1.79 0.00409
임의의 사용자 \(u\)가 아래와 같이 다섯 가지 단어에 각기 다른 관심도 \(w_{iu}\)를 지닌다고 하자.
<- tibble(
words_of_interest word = c("kitty", "cottage", "judgment", "war", "sea"),
weight = c(0.3, 0.3, 0.1, 0.1, 0.2)
)
%>%
words_of_interest ::kable(
knitrbooktabs = TRUE,
align = c('c', 'c'),
col.names = c('단어 ($i$)', '가중치 ($w_{iu}$)'),
caption = '목표 사용자의 관심 단어 및 가중치'
)
단어 (\(i\)) | 가중치 (\(w_{iu}\)) |
---|---|
kitty | 0.3 |
cottage | 0.3 |
judgment | 0.1 |
war | 0.1 |
sea | 0.2 |
이 때, 목표 사용자 \(u\)의 문서 \(j\)에 대한 유용도(utility)를 다음과 같이 코사인 유사성 척도(cosine similarity measure)로 산출한다.
\[\begin{equation*} u(a, j) = \frac{\sum_{i = 1}^{K} w_{iu} w_{ij}}{\sqrt{\sum_{i = 1}^{K} w_{iu}^2} \sqrt{\sum_{i = 1}^{K} w_{ij}^2}} \end{equation*}\]
<- tf_idf_results %>%
utility_results inner_join(words_of_interest, by = "word") %>%
group_by(book) %>%
summarize(utility = sum(weight * tf_idf) /
sqrt(sum(weight ^ 2)) * sqrt(sum(tf_idf ^ 2)))) %>%
(arrange(desc(utility))
print(utility_results)
## # A tibble: 6 x 2
## book utility
## <fct> <dbl>
## 1 Persuasion 0.753
## 2 Emma 0.710
## 3 Sense & Sensibility 0.640
## 4 Pride & Prejudice 0.615
## 5 Northanger Abbey 0.529
## 6 Mansfield Park 0.404
위 결과 Persuasion이 목표 사용자의 관심에 가장 유용도 높은 문서로 추천된다.
교재 전치혁 (2012) 에 있는 예제에 대한 R 스크립트를 구현해보자.
<- tribble(
words_of_interest ~word, ~weight,
"word1", 0.124,
"word2", 0.275,
"word3", 0.019,
"word4", 0.182,
"word5", 0.223
)
<- tribble(
tf_idf_results ~document, ~word, ~tf_idf,
"doc1", "word1", 0.0194,
"doc1", "word2", 0.0043,
"doc1", "word3", 0.0054,
"doc1", "word4", 0.0155,
"doc1", "word5", 0.0028,
"doc2", "word1", 0.0082,
"doc2", "word2", 0.0032,
"doc2", "word3", 0.0007,
"doc2", "word4", 0.0104,
"doc2", "word5", 0.0073,
"doc3", "word1", 0.0087,
"doc3", "word2", 0.0174,
"doc3", "word3", 0.0091,
"doc3", "word4", 0.0086,
"doc3", "word5", 0.0268,
"doc4", "word1", 0.0093,
"doc4", "word2", 0.0061,
"doc4", "word3", 0.0172,
"doc4", "word4", 0.0028,
"doc4", "word5", 0.0009,
"doc5", "word1", 0.0185,
"doc5", "word2", 0.0249,
"doc5", "word3", 0.0084,
"doc5", "word4", 0.0167,
"doc5", "word5", 0.0193,
"doc6", "word1", 0.0028,
"doc6", "word2", 0.0003,
"doc6", "word3", 0.0202,
"doc6", "word4", 0.0083,
"doc6", "word5", 0.0054
)
<- tf_idf_results %>%
utility_results inner_join(words_of_interest, by = "word") %>%
group_by(document) %>%
summarize(utility = sum(weight * tf_idf) /
sqrt(sum(weight ^ 2)) * sqrt(sum(tf_idf ^ 2)))) %>%
(arrange(desc(utility))
print(utility_results)
## # A tibble: 6 x 2
## document utility
## <chr> <dbl>
## 1 doc5 0.972
## 2 doc3 0.919
## 3 doc2 0.841
## 4 doc1 0.659
## 5 doc4 0.448
## 6 doc6 0.373
위 결과, 두 건의 문서를 추천할 경우 doc5, doc3를 추천할 수 있다.
16.3 협업 필터링
총 \(m\)개의 상품에 대한 \(n\)명의 소비자의 평점이 있다고 할 때, 관련 기호를 다음과 같이 정의하자.
- \(v_{ij}\): 고객 \(i\)의 상품 \(j\)에 대한 평점
- \(I_i\): 고객 \(i\)가 평점을 매긴 상품집합
- \(\left| I_i \right|\): 집합 \(I_i\)에 포함된 상품 수
이 때, 고객 \(i\)의 평균 평점은 다음과 같이 산출된다.
\[\begin{equation*} \bar{v}_i = \frac{1}{\left| I_i \right|} \sum_{j \in I_i} v_{ij} \end{equation*}\]
이 때, 목표고객 \(a\)와 \(i\)번째 고객과의 유사성은 아래와 같이 평점에서 고객 평점을 뺀(mean-centering) 값에 대한 코사인 유사성 척도를 이용하여 정의한다.
\[\begin{equation*} w(a, i) = \frac{\sum_{j \in I_a \cap I_i} (v_{aj} - \bar{v}_a) (v_{ij} - \bar{v}_i)}{\sqrt{\sum_{j \in I_a \cap I_i} (v_{aj} - \bar{v}_a)^2} \sqrt{\sum_{j \in I_a \cap I_i} (v_{ij} - \bar{v}_i)^2}} \end{equation*}\]
이를 이용하여, 목표고객 \(a\)가 아직 구매하지 않은 상품 \(j\)에 매길 평점을 아래와 같이 추정한다.
\[\begin{equation*} \hat{v}_{aj} = \bar{v}_a + \frac{1}{\sum_{i = 1}^{n} \left| w(a, i) \right|} \sum_{i = 1}^{n} w(a, i) (v_{ij} - \bar{v}_i) \end{equation*}\]
교재 @jun2012datamining 에 있는 예제에 대한 R 스크립트를 구현해보자.
<- tribble(
rating_df ~customer, ~item, ~rating,
"고객 1", "상품 1", 5,
"고객 1", "상품 3", 4,
"고객 1", "상품 5", 1,
"고객 1", "상품 6", 0,
"고객 1", "상품 7", 3,
"고객 2", "상품 1", 4,
"고객 2", "상품 2", 4,
"고객 2", "상품 3", 4,
"고객 2", "상품 7", 1,
"고객 3", "상품 1", 5,
"고객 3", "상품 2", 4,
"고객 3", "상품 4", 1,
"고객 3", "상품 5", 2,
"고객 3", "상품 7", 3,
"고객 4", "상품 1", 1,
"고객 4", "상품 2", 2,
"고객 4", "상품 3", 1,
"고객 4", "상품 4", 4,
"고객 4", "상품 5", 3,
"고객 4", "상품 6", 5,
"고객 4", "상품 7", 2,
"고객 5", "상품 1", 0,
"고객 5", "상품 2", 1,
"고객 5", "상품 4", 3,
"고객 5", "상품 5", 5,
"고객 5", "상품 6", 5,
"고객 6", "상품 2", 2,
"고객 6", "상품 5", 4,
"고객 6", "상품 6", 4,
"고객 6", "상품 7", 2,
"목표고객", "상품 1", 5,
"목표고객", "상품 4", 1,
"목표고객", "상품 7", 2
)
우선, 각 고객이 매긴 평균 평점 \(\bar{v}_i\)을 각 아이템에 대한 평점 \(v_{ij}\)에서 제외하여 mean_centered rating을 아래와 같이 구한다.
<- rating_df %>%
centered_rating_df group_by(customer) %>%
mutate(centered_rating = rating - mean(rating)) %>%
ungroup()
print(centered_rating_df)
## # A tibble: 33 x 4
## customer item rating centered_rating
## <chr> <chr> <dbl> <dbl>
## 1 고객 1 상품 1 5 2.4
## 2 고객 1 상품 3 4 1.4
## 3 고객 1 상품 5 1 -1.6
## 4 고객 1 상품 6 0 -2.6
## 5 고객 1 상품 7 3 0.4
## 6 고객 2 상품 1 4 0.75
## 7 고객 2 상품 2 4 0.75
## 8 고객 2 상품 3 4 0.75
## 9 고객 2 상품 7 1 -2.25
## 10 고객 3 상품 1 5 2
## # … with 23 more rows
목표 고객과 다른 고객들간의 유사성 척도를 계산한다.
<- centered_rating_df %>% filter(customer == "목표고객") %>%
similarity_df inner_join(centered_rating_df %>% filter(customer != "목표고객"), by = "item") %>%
group_by(customer.y) %>%
summarize(similarity = sum(centered_rating.x * centered_rating.y) /
sqrt(sum(centered_rating.x ^ 2)) * sqrt(sum(centered_rating.y ^ 2)))) %>%
(rename(customer = customer.y)
print(similarity_df)
## # A tibble: 6 x 2
## customer similarity
## <chr> <dbl>
## 1 고객 1 0.903
## 2 고객 2 0.565
## 3 고객 3 0.961
## 4 고객 4 -0.875
## 5 고객 5 -0.853
## 6 고객 6 1
유사성 척도의 절대값의 합이 1이 되도록 normalize한다.
<- similarity_df %>%
normalized_similarity_df mutate(normalized_similarity = similarity / sum(abs(similarity)))
print(normalized_similarity_df)
## # A tibble: 6 x 3
## customer similarity normalized_similarity
## <chr> <dbl> <dbl>
## 1 고객 1 0.903 0.175
## 2 고객 2 0.565 0.109
## 3 고객 3 0.961 0.186
## 4 고객 4 -0.875 -0.170
## 5 고객 5 -0.853 -0.165
## 6 고객 6 1 0.194
이후 목표고객이 아직 평점을 매기지 않은 상품들에 대해 평점을 추정한다. 이 때, 상품 \(j\)에 대해 평점을 매기지 않은 고객의 경우, \(v_{ij} - \bar{v}_i = 0\)이라 가정하자.
\[\begin{equation*} \hat{v}_{aj} = \bar{v}_a + \frac{1}{\sum_{i = 1}^{n} \left| w(a, i) \right|} \sum_{i = 1}^{n} w(a, i) (v_{ij} - \bar{v}_i) \end{equation*}\]
<- sort(setdiff(unique(rating_df$item),
items $item[rating_df$customer == "목표고객"]))
rating_df
<- mean(rating_df$rating[rating_df$customer == "목표고객"])
target_mean
%>%
centered_rating_df filter(item %in% items) %>%
inner_join(normalized_similarity_df, by = "customer") %>%
group_by(item) %>%
summarize(predicted_rating = target_mean +
sum(normalized_similarity * centered_rating)) %>%
arrange(desc(predicted_rating))
## # A tibble: 4 x 2
## item predicted_rating
## <chr> <dbl>
## 1 상품 3 3.26
## 2 상품 2 3.14
## 3 상품 5 1.96
## 4 상품 6 1.63
이번에는, 목표상품 \(j\)에 대한 평점을 추정할 때, 상품 \(j\)에 대해 평점을 매긴 고객과의 유사성만을 아래와 같이 고려하기로 하자.
\[\begin{equation*} \hat{v}_{aj} = \bar{v}_a + \frac{1}{\sum_{i: j \in I_i} \left| w(a, i) \right|} \sum_{i: j \in I_i} w(a, i) (v_{ij} - \bar{v}_i) \end{equation*}\]
%>%
centered_rating_df filter(item %in% items) %>%
inner_join(similarity_df, by = "customer") %>%
group_by(item) %>%
summarize(predicted_rating = target_mean +
sum(similarity * centered_rating) / sum(abs(similarity))) %>%
arrange(desc(predicted_rating))
## # A tibble: 4 x 2
## item predicted_rating
## <chr> <dbl>
## 1 상품 3 3.97
## 2 상품 2 3.24
## 3 상품 5 1.87
## 4 상품 6 1.19
16.4 시장바구니 데이터를 이용한 협업 필터링
아래와 같은 시장바구니 데이터가 있다.
\[\begin{equation*} v_{ij} = \begin{cases} 1 & \text{ 고객 $i$가 상품 $j$를 구매한 경우}\\ 0 & \text{ 그렇지 않은 경우} \end{cases} \end{equation*}\]
<- tribble(
market_basket_df ~customer, ~item, ~purchase,
"고객 1", "상품 1", 1,
"고객 1", "상품 3", 1,
"고객 1", "상품 5", 1,
"고객 1", "상품 7", 1,
"고객 2", "상품 1", 1,
"고객 2", "상품 2", 1,
"고객 2", "상품 3", 1,
"고객 2", "상품 7", 1,
"고객 3", "상품 1", 1,
"고객 3", "상품 2", 1,
"고객 3", "상품 4", 1,
"고객 3", "상품 5", 1,
"고객 3", "상품 7", 1,
"고객 4", "상품 1", 1,
"고객 4", "상품 2", 1,
"고객 4", "상품 3", 1,
"고객 4", "상품 4", 1,
"고객 4", "상품 6", 1,
"고객 4", "상품 7", 1,
"고객 5", "상품 2", 1,
"고객 5", "상품 4", 1,
"고객 5", "상품 5", 1,
"고객 5", "상품 6", 1,
"고객 6", "상품 2", 1,
"고객 6", "상품 5", 1,
"고객 6", "상품 6", 1,
"고객 6", "상품 7", 1,
"목표고객", "상품 1", 1,
"목표고객", "상품 4", 1,
"목표고객", "상품 7", 1
)
이 때, 총 상품의 개수를 \(m\)이라 하고, 고객 \(i\)에 대해
\[\begin{equation*} p_i = \frac{|I_i|}{m} \end{equation*}\]
이라 정의하자. 즉, \(p_i\)는 전체 상품 중 고객 \(i\)가 구입한 상품의 비율을 뜻한다. 또한, 두 고객 \(i\)와 \(k\)가 공통적으로 구입한 상품의 비율을 아래와 같이 \(p_{ik}\)라 정의하자.
\[\begin{equation*} p_{ik} = \frac{|I_i \cap I_k|}{m} \end{equation*}\]
우선 아래와 같이 가중치 \(w(a, i)\)를 계산해보자. 이 가중치는 목표고객 \(a\)과 각 고객 \(i\)간의 유사성 척도이다.
\[\begin{equation*} w(a, i) = \frac{p_{ai} - p_a p_i}{\sqrt{p_a (1 - p_a)} \sqrt{p_i (1 - p_i)}} \end{equation*}\]
<- length(unique(market_basket_df$item))
m
<- market_basket_df %>%
n_df group_by(customer) %>%
summarize(p = n() / m)
<- market_basket_df %>%
common_df filter(customer == "목표고객") %>%
inner_join(market_basket_df %>% filter(customer != "목표고객"),
by = "item") %>%
group_by(customer.y) %>%
summarize(p = n() / m) %>%
rename(customer = customer.y)
<- crossing(
similarity_df target_customer = "목표고객",
ref_customer = n_df$customer[n_df$customer != "목표고객"]
%>%
) inner_join(n_df %>% rename(target_p = p),
by = c("target_customer" = "customer")) %>%
inner_join(n_df %>% rename(ref_p = p),
by = c("ref_customer" = "customer")) %>%
inner_join(common_df %>% rename(common_p = p),
by = c("ref_customer" = "customer")) %>%
mutate(
similarity = (common_p - target_p * ref_p) /
sqrt(target_p * (1 - target_p)) * sqrt(ref_p * (1 - ref_p)))
(
)
print(similarity_df)
## # A tibble: 6 x 6
## target_customer ref_customer target_p ref_p common_p
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 목표고객 고객 1 0.429 0.571 0.286
## 2 목표고객 고객 2 0.429 0.571 0.286
## 3 목표고객 고객 3 0.429 0.714 0.429
## 4 목표고객 고객 4 0.429 0.857 0.429
## 5 목표고객 고객 5 0.429 0.571 0.143
## 6 목표고객 고객 6 0.429 0.571 0.143
## # … with 1 more variable: similarity <dbl>
이후 목표고객이 아직 구매하지 않은 상품에 대해 평점을 추정한다. 목표고객 \(a\)의 상품 \(j\)에 대한 평점 추정치는 다음과 같이 산출한다.
\[\begin{equation*} P_{aj} = \frac{\sum_{i = 1}^{n} w(a, i) v_{ij}}{\sum_{i = 1}^{n} | w(a, i) |} \end{equation*}\]
<- sum(abs(similarity_df$similarity))
denom
<- similarity_df %>%
pred_df inner_join(market_basket_df, by = c("ref_customer" = "customer")) %>%
anti_join(market_basket_df %>%
filter(customer == "목표고객") %>%
select(item),
by = "item") %>%
group_by(item) %>%
summarize(est_score = sum(similarity * purchase) / denom) %>%
arrange(desc(est_score))
print(pred_df)
## # A tibble: 4 x 2
## item est_score
## <chr> <dbl>
## 1 상품 3 0.332
## 2 상품 2 0.113
## 3 상품 5 -0.0575
## 4 상품 6 -0.232