데이터가 공개되면 이를 이용한 여러 재미있는 분석작업을 할 수 있다. 얼마전에 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")
맥주 추천엔진 구현 방법은 아주 간단하다.
- 먼저 입력된 맥주쌍을 모두 리뷰한 사용자들의 리스트를 구한다.
- 각 리뷰어의 맥주쌍간의 리뷰에 대한 유클리드언 거리를 구한다.
- 모든 리뷰어들의 리뷰거리에 대해서 weighted mean을 한다. weight function은 정규분포를 기반으로 하는데 이 이유는 같은 리뷰인데 많은 수치 차이를 보이는 몇몇의 사용자들의 존재 때문이다.
- 유사도 수치를 기반으로 맥주를 나열한다.
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류가 거의 없다는데 있다. 그래서 비슷한 종류인 인디카가 위에 올라왔는지도…. ㅜㅜ
추천엔진을 구현할 수 있는 방법은 유사도 측정하는 방법의 개수만큼보다 더 많은것 같은데, 정말 간단하고 쉽게 한번 만들어 봤는데 관심있는 주제라 그런지 재미가 있었다.
내가 맥주 수입업자라면 국내에서 가장 잘 팔리는 맥주와 유사한 새로운 맥주를 발굴하는데 이런 데이터를 활용할 수 있을거란 생각도 해본다.
R로 구현한 맥주 추천 엔진 by from __future__ import dream is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.