R로 구현한 맥주 추천 엔진

데이터가 공개되면 이를 이용한 여러 재미있는 분석작업을 할 수 있다. 얼마전에 R기반 맥주 추천 구현 포스팅을 보고 몇일전에 관련 데이터 소스를 구해서 이에 대한 추천엔진 구현 포스팅을 올려본다. 이 데이터의 출처는 Beer Advocate이며 데이터 소스는 web data library이다.

사실 금번 분석의 동기는 데이터마이닝 수업 텀 프로젝트임을 미리 밝혀둔다.

일단 추천엔진은 구현 방법에 따라 너무 많은 구현 방식이 있어 뭐가 좋은지 잘 모르겠으나 주말을 이용해서 한번 고민해봤다는데 의미를 둬본다.

추천엔진을 초반에 거리차이들의 p.value로 구해보려다 수식상 오히려 리뷰 갯수가 많아지면 p.value가 작게 나오는 경향이 있어 구현 목적상 맞질 않아 포기하고 다른 간단한 방법으로 구현해 보았다.

일단 맥주 리뷰 데이터는 dropbox계정에 올려두었으며 이를 아래와 같이 다운받아 사용하면 된다.

options(width = 100)
library(data.table)

download.file("http://dl.dropboxusercontent.com/u/8686172/beer_reviews.zip", 
    destfile = "beer_reviews.zip", mode = "wb", cacheOK = FALSE)
beers <- read.csv(unz("beer_reviews.zip", "beer_reviews.csv"), stringsAsFactors = FALSE)
beers <- data.table(beers)

review_df <- beers[, list(review_overall, review_aroma, review_appearance, review_palate, 
    review_taste), by = c("review_profilename", "beer_name", "beer_style")]
setkeyv(review_df, "review_profilename")

맥주 추천엔진 구현 방법은 아주 간단하다.

  1. 먼저 입력된 맥주쌍을 모두 리뷰한 사용자들의 리스트를 구한다.
  2. 각 리뷰어의 맥주쌍간의 리뷰에 대한 유클리드언 거리를 구한다.
  3. 모든 리뷰어들의 리뷰거리에 대해서 weighted mean을 한다. weight function은 정규분포를 기반으로 하는데 이 이유는 같은 리뷰인데 많은 수치 차이를 보이는 몇몇의 사용자들의 존재 때문이다.
  4. 유사도 수치를 기반으로 맥주를 나열한다.
beer_similarity <- function(beer1, beer2) {
    reviewers <- review_df[beer_name %chin% c(beer1, beer2), list(num_uniq = length(unique(.SD$beer_name))), 
        by = "review_profilename"][num_uniq == 2]
    if (nrow(reviewers) <= 1) 
        return(NA)
    reviews <- review_df[reviewers][beer_name %chin% c(beer1, beer2)]
    setkeyv(reviews, c("review_profilename", "beer_name"))
    beer1_reviews <- reviews[, list(review_overall, review_aroma, review_appearance, review_palate, review_taste)]
    beer_dist <- as.matrix(dist(scale(beer1_reviews), diag = TRUE, upper = TRUE))
    two_dist <- vector("numeric", length = (nrow(beer_dist) - 1)/2)
    idx <- 1
    for (i in seq(1, nrow(beer_dist) - 1, by = 2)) {
        two_dist[idx] <- beer_dist[i, i + 1]
        idx <- idx + 1
    }
    names(two_dist) <- unique(reviews$review_profilename)
    sum(dnorm(two_dist, mean = mean(two_dist), sd = sd(two_dist)))/sum(two_dist * dnorm(two_dist, mean = mean(two_dist), 
        sd = sd(two_dist)))
}

몇가지 맥주를 중심을 리뷰를 해봤다.

# http://nbviewer.ipython.org/20a18d52c539b87de2af 결과와 비교
beer1 <- "Coors Light"
beer2 <- c("Natural Light", "Michelob Ultra", "Bud Light", "Blue Moon Belgian White", "Fat Tire Amber Ale", 
    "Dale's Pale Ale", "Guinness Draught", "60 Minute IPA", "Sierra Nevada Pale Ale")
sims <- sapply(beer2, function(x) beer_similarity(beer1, x))
sims[order(sims, decreasing = T)]
##               Bud Light           Natural Light          Michelob Ultra        Guinness Draught 
##                  0.4481                  0.4426                  0.4201                  0.3001 
## Blue Moon Belgian White      Fat Tire Amber Ale         Dale's Pale Ale  Sierra Nevada Pale Ale 
##                  0.2868                  0.2690                  0.2574                  0.2542 
##           60 Minute IPA 
##                  0.2519


beer1 <- "Sierra Nevada Pale Ale"
beer2 <- c("Sierra Nevada Stout", "Samuel Adams Boston Lager", "Indica India Pale Ale", "Suntory The Premium Malt's", 
    "Budweiser", "Guinness Draught", "Pilsner Urquell", "Smithwick's", "Stella Artois", "Tsingtao", "Bud Light")
sims <- sapply(beer2, function(x) beer_similarity(beer1, x))
sims[order(sims, decreasing = T)]
##      Indica India Pale Ale        Sierra Nevada Stout  Samuel Adams Boston Lager 
##                     0.4013                     0.3937                     0.3936 
##            Pilsner Urquell           Guinness Draught                Smithwick's 
##                     0.3903                     0.3615                     0.3261 
##              Stella Artois Suntory The Premium Malt's                   Tsingtao 
##                     0.3130                     0.2969                     0.2950 
##                  Budweiser                  Bud Light 
##                     0.2768                     0.2524


# top 50 맥주만 리뷰
no_of_rev <- beers[, list(num_of_rev = .N), by = c("beer_name", "beer_style")]

no_of_rev <- no_of_rev[order(num_of_rev, decreasing = T)]

sims <- sapply(no_of_rev$beer_name[1:50], function(x) beer_similarity("Samuel Adams Boston Lager", x))

no_of_rev[1:50, `:=`(sims, sims)]
##                                  beer_name                     beer_style num_of_rev   sims
##     1:                       90 Minute IPA American Double / Imperial IPA       3290 0.3487
##     2: Old Rasputin Russian Imperial Stout         Russian Imperial Stout       3111 0.3382
##     3:       Sierra Nevada Celebration Ale                   American IPA       3000 0.3623
##     4:                     Two Hearted Ale                   American IPA       2728 0.3502
##     5:                Arrogant Bastard Ale            American Strong Ale       2704 0.3485
##    ---                                                                                     
## 58888:                        Brandy Brown             American Brown Ale          1     NA
## 58889:                   English Nut Brown              English Brown Ale          1     NA
## 58890:                 Very Hoppy Pale Ale        American Pale Ale (APA)          1     NA
## 58891:                     Highland Porter                American Porter          1     NA
## 58892:                    Baron Von Weizen                     Hefeweizen          1     NA
no_of_rev[order(sims, decreasing = T)][1:50]
##                                      beer_name                       beer_style num_of_rev   sims
##  1:                                   Pale Ale          American Pale Ale (APA)       1880 0.4100
##  2:                              Oatmeal Stout                    Oatmeal Stout       1744 0.4050
##  3:                             India Pale Ale     English India Pale Ale (IPA)       1824 0.4049
##  4:                               Dead Guy Ale            Maibock / Helles Bock       2234 0.4009
##  5:                  Samuel Adams Winter Lager                             Bock       1824 0.3998
##  6:                          Anchor Steam Beer   California Common / Steam Beer       1752 0.3995
##  7:                   Samuel Adams Octoberfest             Märzen / Oktoberfest       1893 0.3955
##  8:                     Sierra Nevada Pale Ale          American Pale Ale (APA)       2587 0.3936
##  9:                                 Prima Pils                  German Pilsener       1863 0.3822
## 10:                              60 Minute IPA                     American IPA       2475 0.3807
## 11:                           Guinness Draught                  Irish Dry Stout       2210 0.3724
## 12:                                 Punkin Ale                      Pumpkin Ale       1849 0.3704
## 13:             Young's Double Chocolate Stout               Milk / Sweet Stout       2257 0.3701
## 14:                                 Hop Wallop   American Double / Imperial IPA       1818 0.3679
## 15:            Sierra Nevada Torpedo Extra IPA                     American IPA       2029 0.3677
## 16:               Samuel Smith's Oatmeal Stout                    Oatmeal Stout       2025 0.3663
## 17:                               HopDevil Ale                     American IPA       2302 0.3639
## 18:              Sierra Nevada Celebration Ale                     American IPA       3000 0.3623
## 19:                            Chocolate Stout                   American Stout       2281 0.3621
## 20:                             120 Minute IPA   American Double / Imperial IPA       1745 0.3611
## 21: Sierra Nevada Bigfoot Barleywine Style Ale              American Barleywine       2492 0.3602
## 22:                 Stone IPA (India Pale Ale)                     American IPA       2575 0.3567
## 23:                         Orval Trappist Ale                 Belgian Pale Ale       1919 0.3564
## 24:                           Storm King Stout           Russian Imperial Stout       2452 0.3538
## 25:                             Trois Pistoles          Belgian Strong Dark Ale       1736 0.3518
## 26:                            Two Hearted Ale                     American IPA       2728 0.3502
## 27:             Brooklyn Black Chocolate Stout           Russian Imperial Stout       2447 0.3494
## 28:                     Racer 5 India Pale Ale                     American IPA       1971 0.3491
## 29:                              90 Minute IPA   American Double / Imperial IPA       3290 0.3487
## 30:                       Arrogant Bastard Ale              American Strong Ale       2704 0.3485
## 31:                Hennepin (Farmhouse Saison)           Saison / Farmhouse Ale       1866 0.3462
## 32:                         Double Bastard Ale              American Strong Ale       1851 0.3455
## 33:               Chimay Grande Réserve (Blue)          Belgian Strong Dark Ale       2009 0.3447
## 34:                        Schneider Aventinus                       Weizenbock       1899 0.3441
## 35:                                Hop Rod Rye                         Rye Beer       1913 0.3408
## 36:                       Tröegs Nugget Nectar         American Amber / Red Ale       1955 0.3397
## 37:        Old Rasputin Russian Imperial Stout           Russian Imperial Stout       3111 0.3382
## 38:                                      Duvel          Belgian Strong Pale Ale       2450 0.3367
## 39:                            La Fin Du Monde                           Tripel       2483 0.3348
## 40:                        Stone Ruination IPA   American Double / Imperial IPA       2704 0.3348
## 41:              Weihenstephaner Hefeweissbier                       Hefeweizen       1981 0.3342
## 42:              Ayinger Celebrator Doppelbock                       Doppelbock       2070 0.3333
## 43:                       St. Bernardus Abt 12                 Quadrupel (Quad)       2217 0.3309
## 44:                         Bell's Hopslam Ale   American Double / Imperial IPA       2443 0.3276
## 45:               Stone Imperial Russian Stout           Russian Imperial Stout       2329 0.3264
## 46:                   Founders Breakfast Stout American Double / Imperial Stout       2502 0.3222
## 47:                    Trappistes Rochefort 10                 Quadrupel (Quad)       2170 0.3193
## 48:    Founders KBS (Kentucky Breakfast Stout) American Double / Imperial Stout       1932 0.3120
## 49:                            Pliny The Elder   American Double / Imperial IPA       2527 0.3101
## 50:                  Samuel Adams Boston Lager                     Vienna Lager       2418     NA
##                                      beer_name                       beer_style num_of_rev   sims

위에서 Sierra Nevada Pale Ale 의 경우 작년 미국 출장시 처음 먹어보고 반해서 국내서 찾아본 맥주였는데 국내에 없어서 비슷한 맥주를 찾아보려 몇가지 국내에서 구할 수 있는 맥주 중심으로 계산을 해봤다. 그런데, 역시나 마트에서 5천원 남짓에 파는 Indica India Pale Ale이 1등으로 나오는걸 보면 추천엔진이 꽝은 아니라는 것을 알 수 있다. 사실 Pale Ale류의 맥주는 미국에서 Sierra Nevada Pale Ale로 처음 먹어봤으며 최근엔 집중적으로 Indica India Pale Ale을 맥주창고 같은곳에 있으면 돈 아끼지 않고 마시기 때문이다. 그런데 한가지 이 결과의 문제는 국내에서 파는 맥주중에서 Pale Ale류가 거의 없다는데 있다. 그래서 비슷한 종류인 인디카가 위에 올라왔는지도…. ㅜㅜ

sierra_nevada

추천엔진을 구현할 수 있는 방법은 유사도 측정하는 방법의 개수만큼보다 더 많은것 같은데, 정말 간단하고 쉽게 한번 만들어 봤는데 관심있는 주제라 그런지 재미가 있었다.

내가 맥주 수입업자라면 국내에서 가장 잘 팔리는 맥주와 유사한 새로운 맥주를 발굴하는데 이런 데이터를 활용할 수 있을거란 생각도 해본다.

CC BY-NC 4.0 R로 구현한 맥주 추천 엔진 by from __future__ import dream is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.