11장 추천 시스템#

자료 출처: Wikipedia, “Recommender system”.

11.1 개요#

추천 시스템(recommender system 또는 recommendation system)은 추천 플랫폼 또는 추천 엔진이라고도 한다. 추천 시스템은 어떤 사용자가 좋아할 것으로 생각되는 아이템을 추천해주는 시스템으로 이제는 우리 일상 생활 구석구석에서 그 예를 찾아볼 수 있다.

추천 시스템은 달리 표현하면 어떤 아이템에 대한 사용자의 선호도 또는 평점(rating)을 예측하는 시스템으로서 정보 필터링 시스템(information filtering system)의 한 분야이다. 영화, 음악, 뉴스, 서적, 연구논문, 검색어, 소셜태그, 그밖에 여러 제품 등 다양한 영역에서 활용된다. 또한 식당, 의복, 금융서비스, 생명보험, 파트너(온라인 데이트), 조크, 트위터 페이지, 전문가, 협업자(예: 공동 연구) 등에 대한 추천 시스템도 있다.

추천 시스템이 작동하는 방식은 크게 두 가지로 나눌 수 있다. 즉 협업 필터링(collaborative filtering)과 콘텐츠 기반 필터링(content-based filtering)이다. 콘텐츠 기반 필터링을 성격 기반 접근(personality-based approach)이라고도 한다.

협업 필터링은 어떤 사용자의 과거 행동(이전에 구매했거나 선택한 아이템 기록이나 해당 아이템에 부여한 숫자 평점), 그리고 다른 사용자들이 내린 비슷한 결정을 바탕으로 모형을 만든다. 그것을 통해 해당 사용자가 관심을 가질만한 아이템(또는 아이템에 대한 평점)을 예측한다. 이에 반해 콘텐츠 기반 필터링은 어떤 아이템이 갖고 있는 일련의 개별적 특성을 활용하여 그와 비슷한 속성을 가진 아이템을 추천하는 방식이다. 협업 필터링과 콘텐츠 기반 필터링을 결합시킨 것을 하이브리드 추천 시스템이라고 한다.

11.2 추천 시스템 종류 및 특징#

사례: 뮤직 추천 시스템#

1. Last.fm: 협업 필터링

  • 어떤 사용자가 정기적으로 듣는 곡들을 다른 사용자들의 음악 청취 행태와 비교하여 추천곡 “스테이션”을 만든다.

  • 해당 사용자의 라이브러리에 들어있지는 않지만 그와 관심이 비슷한 다른 사용자들이 자주 청취하는 곡들을 추천하는 방식이다.

2. Pandora: 콘텐츠 기반 필터링

  • 노래 또는 아티스트의 특성(Music Genome Project에서 제공하는 400여 개의 속성 중 일부)을 이용하여 유사한 특성을 지닌 음악을 재생하는 “스테이션”을 마련한다.

  • 이와 함께 사용자 피드백을 통해 스테이션을 개선해 나간다. 즉 사용자가 특정 노래에 “싫어요”를 누르면 관련 특성을 줄이고, 사용자가 어떤 노래에 “좋아요”를 누르면 관련 특성을 강화하는 방식이다.

두 접근법의 장단점

앞의 예에서 Last.fm 같은 협업 필터링의 경우, 정확한 추천을 위해서는 사용자에 대해 많은 양의 정보가 필요하다. 소위 콜드 스타트(cold start), 즉 아직 충분한 정보를 모으지 않은 사용자 또는 아이템에 대해서는 어떠한 추론도 할 수 없는 문제를 지닌다. 이에 반해 Pandora 같은 콘텐츠 기반 필터링의 경우에는 초기에 극히 제한적인 정보로도 추천이 가능하지만, 추천의 제공 범위가 극히 제한적이라는 한계를 갖는다.

협업 필터링#

협업 필터링 방법은 사용자들의 행태, 활동, 선호도에 대한 많은 양의 정보를 수집 및 분석하고, 다른 사용자들과의 유사성을 기반으로 사용들이 무엇을 좋아할지 예측한다. 협업 필터링은 과거에 의견이 일치한 사람들이 장래에도 일치할 것이며, 과거에 좋아한 것과 비슷한 종류의 아이템을 미래에도 좋아할 것이라는 가정에 기반한다. 현재 사용자/아이템과 유사한 평점 기록을 가진 다른 사용자/아이템을 찾아서 추천한다.

협업 필터링의 주요 이점은 콘텐츠에 의존하지 않으므로 아이템 자체에 대한 “이해” 없이도 영화와 같은 복잡한 아이템을 우수하게 추천할 수 있다는 점이다. 협업 필터링 추천 시스템에서는 사용자 유사성(similarity) 또는 아이템 유사성을 측정하는 다양한 알고리즘을 사용한다.

사례

협업 필터링 시스템에 대한 상업적, 비상업적인 사례들이 많다. 협업 필터링의 가장 유명한 사례 중 하나는 Amazon.com이 사용함으로써 대중화된 알고리즘인 아이템-아이템(item-to-item) 방식, 즉 아이템 \(x\)를 사는 사람이 \(y\)도 구매한다는 아이디어이다. Last.fm은 비슷한 사용자의 청취 습관을 비교하여 음악을 추천한다. Readgeek은 책에 대한 평점을 비교하여 추천한다. Facebook, MySpace, LinkedIn, 기타 소셜 네트워크들은 협업 필터링을 사용하여 새로운 친구, 그룹, 기타 소셜 친구를 추천한다. Twitter는 많은 신호와 인메모리(in-memory) 계산을 통해 사용자가 누구를 팔로우할지 추천한다.

한계

협업 필터링 접근법 다음 세 가지 문제로 어려움을 겪는다.

  • 콜드 스타트(cold start): 정확한 추천을 위해서는 사용자에 대해 많은 양의 축적된 데이터를 필요로 한다.

  • 확장성(scalability): 사용자가 수백만 명이고 제품 수도 아주 많을 경우 추천 사항을 계산할 때 대량의 계산 능력이 필요하다.

  • 희소성(sparsity): 주요 전자상거래 사이트에서 판매되는 아이템의 수는 엄청나게 많지만, 가장 활발한 사용자라도 전체 데이터베이스의 아주 일부에만 평점을 매긴다. 즉 가장 인기있는 아이템조차도 평점을 준 사람이 아주 적다.

콘텐츠 기반 필터링#

콘텐츠 기반 필터링은 아이템에 대한 설명(description)과 사용자 선호에 대한 프로파일을 기반으로 한다. 이 방법은 아이템에 대해서는 알려진 정보(이름, 위치, 설명 등)가 있지만, 사용자에 대해서는 정보가 없는 경우에 적합하다. 추천을 사용자별 분류 문제로 취급하고 아이템의 속성을 기반으로 사용자가 좋아하고 싫어하는 아이템의 분류 시스템을 학습시킨다. 아이템을 설명하는 데에는 키워드가 사용되며, 해당 사용자가 좋아하는 아이템이 어떤 유형인지 알기 위해 사용자 프로파일이 필요하다.

이 알고리즘은 사용자가 과거에 좋아했던 (또는 현재 살펴보고 있는) 것과 가급적 유사한 아이템을 추천한다. 특히 다양한 후보 아이템 중에서 사용자가 이전에 평가한 아이템들과 비교를 통해 가장 매칭이 잘 되는 아이템을 추천한다. 이 접근법은 정보 검색 및 정보 필터링 연구에 뿌리를 두고 있다.

아이템 특성 추출

기본적으로 이 방법은 시스템 내에서 아이템의 특성이 기록된 아이템 프로파일(즉, 여러가지 속성 및 특성 집합)을 사용한다. 아이템의 특성을 추출하기 위해 아이템 표현 알고리즘(item presentation algorithm)이 사용되는데, 널리 사용되는 알고리즘은 tf-idf(term frequency–inverse document frequency, 단어빈도-역문서빈도)이다. 이는 여러 문서로 이루어진 문서군이 있을 때 어떤 단어가 특정 문서 내에서 얼마나 중요한 것인지를 나타내는 통계적 수치이다.

아이템 특성 가중치 벡터

이 시스템은 아이템 특성의 가중치 벡터를 기반으로 사용자의 콘텐츠 기반 프로파일을 만든다. 가중치는 사용자에게 각 특성의 중요성을 의미하며 다양한 기술을 사용하여 계산할 수 있다. 간단히 아이템 평점 벡터의 평균값을 사용할 수도 있으며, 보다 정교한 방법은 베이즈 분류기(Bayesian Classifier), 클러스터 분석(cluster analysis), 결정 트리(decision tree), 인공신경망(artificial neural network)과 같은 머신러밍 기법을 사용하여 사용자가 해당 아이템을 좋아할 확률을 추정한다. 사용자로부터 직접적인 피드백(좋아요 또는 싫어요)을 받아 어떤 특성의 중요성에 대한 가중치를 높이거나 낮출 수 있다.

사용자의 선호를 학습하는 기능 필요

콘텐츠 기반 필터링의 주요 이슈는 추천 시스템이 어떤 콘텐츠에 대한 사용자의 행동에서 사용자의 선호를 학습하여 그것을 다른 콘텐츠 유형에 활용할 수 있는지 여부이다. 이미 사용자가 사용하고 있는 것과 동일한 유형의 콘텐츠를 추천하는 데 국한된다면, 추천 시스템의 가치가 크게 떨어질 것이다. 다른 서비스의 다른 콘텐츠 유형을 추천할 수 있어야 강력한 힘을 갖는다. 예를 들어, 사용자의 뉴스 검색을 기반으로 뉴스 기사만 추천하는 것이 아니라 음악, 비디오, 상품, 토론 등을 추천할 수 있다면 훨씬 더 유용할 것이다.

사례

Pandora Radio는 사용자가 초기 시드로 제공한 노래와 비슷한 특성을 가진 음악을 재생한다. 영화와 관련된 여러 콘텐츠 기반 추천 시스템이 있는데, Rotten Tomatoes, IMDb(Internet Movie Database), Jinni, Rovi Corporation, Jaman 등이 있다. 문서 관련 추천 시스템은 연구자들에게 문서 추천 사항을 제공하는 것을 목표로 한다. 공중보건(public health) 전문가들은 건강 교육 및 예방 전략을 개인화하기 위한 추천 시스템을 연구하고 있다.

하이브리드 추천 시스템#

최근의 연구에 따르면 협업 필터링과 콘텐츠 기반 필터링을 결합한 하이브리드 방식이 더 효과적일 수 있다는 사실이 입증되고 있다. 하이브리드 접근법은 여러 가지 방법으로 구현될 수 있다. 콘텐츠 기반 및 협업 기반 예측을 개별적으로 수행한 다음 결합하는 방식, 협업 기반 접근법에 콘텐츠 기반 기능을 추가하는 방식(혹은 그 반대), 여러 접근법을 하나의 모형으로 통일하는 방법 등이다.

몇몇 실증연구에 따르면 하이브리드 방식의 성과를 순수한 협업 또는 콘텐츠 기반 기법과 비교한 결과, 하이브리드 방식이 순수한 방식보다 더 정확한 추천을 제공하는 것으로 나타났다. 또한 하이브리드 방식을 사용하여 콜드 스타트나 희소성 문제처럼 추천 시스템에서 흔히 발생하는 문제를 일정 부분 극복할 수 있다.

넷플릭스(Netflix)는 하이브리드 추천 시스템의 좋은 예이다. 유사한 사용자들의 시청 및 검색 습관을 비교할 뿐만 아니라(협업 필터링), 사용자가 높은 평점을 준 영화와 특성을 공유하는 영화를 제공하는 방식(콘텐츠 기반 필터링)으로 추천 시스템이 작동한다.

추천 시스템이 갖춰야 할 요소#

일반적으로 추천 시스템은 가장 정확한 추천 알고리즘을 찾는 데 관심이 있지만, 그것 외에 다른 요소들도 중요하다.

  • 다양성(diversity) – 사용자는 한 가지 추천보다는 다양한 추천에 더 만족하는 경향이 있다.

  • 지속성(persistence) – 매번 새로운 아이템을 보여주는 것보다 원래의 추천 사항을 다시 보여주는 것이 더 효과적일 수 있다.

  • 프라이버시 – 추천 시스템은 사용자가 중요한 정보를 공개해야 하기 때문에 프라이버시 문제에 신경을 써야한다. 협업 필터링을 위해 사용자 프로파일을 구축할 경우 개인정보 보호 문제가 제기될 수 있다.

  • 사용자 인구통계적 특성 – 사용자의 인구통계적 특성이 사용자 만족도에 영향을 미칠 수 있다. 가령 한 연구에 따르면 노년층 사용자가 청년층 사용자보다 추천 항목에 더 관심을 가지는 경향이 있다.

  • 견고성(robustness) – 사용자들이 추천 시스템에 참여할 수 있는 경우, 이를 이용한 사기 행위가 발생할 수 있기 때문에 이를 통제해야 한다.

  • 세렌디피티(serendipity) – 평범하지 않은 뜻밖의 추천이 효과적일 수 있다.(세렌디피티는 뜻밖의 행운이란 뜻)

  • 신뢰(trust) – 추천 시스템은 사용자가 시스템을 신뢰하지 않는다면 가치가 없다.

  • 레이블링(labelling) – 동일한 추천 사항이더라도 ‘스폰서’로 표시된 경우, 또는 ‘유기농’이라고 표시된 경우, 또는 아무런 레이블이 없는 경우 클릭률이 서로 다르다.

모바일 추천 시스템#

추천 시스템에서 관심이 높아지고 있는 연구 분야 중 하나가 모바일 추천 시스템이다. 어디에서나 인터넷 접속이 가능한 스마트폰으로 인해 개인화되고 상황에 맞는 추천을 제공할 수 있게 되었다.

추천 시스템으로서는 일반적인 데이터에 비해 모바일 데이터가 더 복잡하기 때문에 훨씬 어려운 연구 영역이다. 모바일 데이터는 동질적이지 않고, 노이즈가 많으며, 공간적 및 시간적 자기상관성이 있고, 검증 및 일반성 문제가 있다.

모바일 추천 시스템의 한 예는 도시에서 주행하는 택시들에게 수익성 있는 주행 경로를 추천하는 시스템이다(우버와 리프트가 채택). 이 시스템의 입력 데이터는 위치(위도 및 경도), 타임 스탬프, 운행 상태 등이다. 이 데이터를 사용하여 택시 운행경로 상의 픽업 포인트를 추천하는데, 목표는 승객 점유 시간과 이익을 최적화 하는 것이 될 것이다. 이러한 유형의 시스템은 당연히 해당 택시가 어디에 위치하느냐에 따라 다르며, 핸드폰 같은 장치에서 작동해야 하므로 계산 능력 및 에너지 요구 사항이 낮아야 한다.

11.3 이커머스 추천 시스템 예제#

코드 출처: Hwang (2019), Hands-On Data Science for Marketing

이 예제를 통해 e-commerce 비즈니스 데이터로 협업 필터링 알고리즘을 사용하여 제품 추천 시스템을 구축하는 방법을 배워보자. scikit-learn을 사용하여 파이썬에서 협업 필터링 알고리즘을 구현하는 방법을 배움과 동시에 데이터 전처리 과정도 학습한다.

여기에서 다룰 데이터세트는 앞에서도 다뤘는데, 각종 기프트 상품을 취급하는 영국 한 온라인 몰의 전자상거래(electronic commerce) 데이터이다. 이 회사의 고객들은 세계 전역의 도매상들이며, 온라인으로만 상품을 취급한다. 2010년 12월 1일부터 2011년 12월 9일까지의 거래 내역이다.

데이터 로딩 및 전처리#

import pandas as pd

url = ('http://archive.ics.uci.edu/ml/machine-learning-databases/00352/' + 
       'Online%20Retail.xlsx')
OnlineRetail = pd.read_excel(url)
print(OnlineRetail.shape)
# 관측 개수가 50만개가 넘어서 데이터 로딩에 시간이 걸림(3~4분 정도)
(541909, 8)
df = OnlineRetail
df.head()
InvoiceNo StockCode Description Quantity InvoiceDate UnitPrice CustomerID Country
0 536365 85123A WHITE HANGING HEART T-LIGHT HOLDER 6 2010-12-01 08:26:00 2.55 17850.0 United Kingdom
1 536365 71053 WHITE METAL LANTERN 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom
2 536365 84406B CREAM CUPID HEARTS COAT HANGER 8 2010-12-01 08:26:00 2.75 17850.0 United Kingdom
3 536365 84029G KNITTED UNION FLAG HOT WATER BOTTLE 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom
4 536365 84029E RED WOOLLY HOTTIE WHITE HEART. 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom

변수 설명

  • InvoiceNo : 인보이스 번호. 각 거래에 고유하게 지정된 6자리 정수. 이 코드가 문자 ‘C’로 시작하면 취소를 나타냄. 이름(nominal) 변수

  • StockCode : 아이템 코드. 각 제품에 고유하게 지정된 5자리 정수. 이름(nominal) 변수

  • Description : 아이템 이름. 이름(nominal) 변수

  • Quantity : 거래 아이템 수량. 숫자(numeric) 변수

  • InvoiceDate : 인보이스 날짜 및 시간. 각 거래가 생성된 날짜 및 시간

  • UnitPrice : 단가. 단위당 아이템 가격(영국 파운드). 숫자(numeric) 변수

  • Customer ID : 고객 ID. 각 고객에게 고유하게 지정된 5자리 정수. 숫자(numeric) 변수

  • Country : 국가 이름. 각 고객이 거주하는 국가의 이름. 이름(nominal) 변수

df.loc[df['Quantity'] < 0]    # Quantity 변수가 음수인 경우 거래 취소를 나타냄
InvoiceNo StockCode Description Quantity InvoiceDate UnitPrice CustomerID Country
141 C536379 D Discount -1 2010-12-01 09:41:00 27.50 14527.0 United Kingdom
154 C536383 35004C SET OF 3 COLOURED FLYING DUCKS -1 2010-12-01 09:49:00 4.65 15311.0 United Kingdom
235 C536391 22556 PLASTERS IN TIN CIRCUS PARADE -12 2010-12-01 10:24:00 1.65 17548.0 United Kingdom
236 C536391 21984 PACK OF 12 PINK PAISLEY TISSUES -24 2010-12-01 10:24:00 0.29 17548.0 United Kingdom
237 C536391 21983 PACK OF 12 BLUE PAISLEY TISSUES -24 2010-12-01 10:24:00 0.29 17548.0 United Kingdom
... ... ... ... ... ... ... ... ...
540449 C581490 23144 ZINC T-LIGHT HOLDER STARS SMALL -11 2011-12-09 09:57:00 0.83 14397.0 United Kingdom
541541 C581499 M Manual -1 2011-12-09 10:28:00 224.69 15498.0 United Kingdom
541715 C581568 21258 VICTORIAN SEWING BOX LARGE -5 2011-12-09 11:57:00 10.95 15311.0 United Kingdom
541716 C581569 84978 HANGING HEART JAR T-LIGHT HOLDER -1 2011-12-09 11:58:00 1.25 17315.0 United Kingdom
541717 C581569 20979 36 PENCILS TUBE RED RETROSPOT -5 2011-12-09 11:58:00 1.25 17315.0 United Kingdom

10624 rows × 8 columns

기존의 df에서 Quantity 변수가 0보다 큰 관측만을 선택한다. 원래 541,909개의 관측에서 10,624개가 사라지고 531,285 rows × 8 columns가 된다.

df = df.loc[df['Quantity'] > 0]
df.shape
(531285, 8)
df['CustomerID'].describe()
count    397924.000000
mean      15294.315171
std        1713.169877
min       12346.000000
25%       13969.000000
50%       15159.000000
75%       16795.000000
max       18287.000000
Name: CustomerID, dtype: float64

CustomerID 변수에서 관측값 개수가 397,924개이고 나머지는 결측값(NaN)이라는 것을 알 수 있다. df 데이터프레임에서 CustomerID 변수에 isna() 함수를 적용하여 결측값(NaN)이면 1, 그렇지 않으면 0인 데이터프레임을 만든 다음, sum() 함수를 사용하여 전체를 합침으로써 결측값 개수가 133,361개임을 파악할 수 있다.

df['CustomerID'].isna().sum()
133361
df.loc[df['CustomerID'].isna()].head()
InvoiceNo StockCode Description Quantity InvoiceDate UnitPrice CustomerID Country
622 536414 22139 NaN 56 2010-12-01 11:52:00 0.00 NaN United Kingdom
1443 536544 21773 DECORATIVE ROSE BATHROOM BOTTLE 1 2010-12-01 14:32:00 2.51 NaN United Kingdom
1444 536544 21774 DECORATIVE CATS BATHROOM BOTTLE 2 2010-12-01 14:32:00 2.51 NaN United Kingdom
1445 536544 21786 POLKADOT RAIN HAT 4 2010-12-01 14:32:00 0.85 NaN United Kingdom
1446 536544 21787 RAIN PONCHO RETROSPOT 2 2010-12-01 14:32:00 1.66 NaN United Kingdom

pandas 패키지의 dropna() 함수는 주어진 데이터프레임에서 결측값이 들어있는 관측을 제거해준다. 이때 subset 인수를 사용하면 특정 변수를 기준으로 결측값 관측을 삭제할 수 있다. 이를 제거한 데이터프레임은 397,924 rows × 8 columns가 된다.

df = df.dropna(subset=['CustomerID'])
df.shape    # (397924, 8)
(397924, 8)

협업 필터링#

협업 필터링은 가령 어떤 사람이 과거에 아이템 A, B, C를 구매하고, 또 다른 사람은 아이템 A, B, D를 구매한 경우, 유사성을 공유하고 있기 때문에 첫 번째 사람은 아이템 D를 구매할 가능성이 있고, 두 번째 사람은 아이템 C를 구매할 가능성이 있다고 본다. 협업 필터링 알고리즘은 사용자 행동 기록과 사용자간의 유사성을 기반으로 제품을 추천한다. 협업 필터링 알고리즘을 구현하는 첫 번째 단계는 사용자-아이템 행렬(user-to-item matrix)을 구축하는 것이다. 사용자-아이템 행렬에서 행에는 개별 사용자들이, 그리고 열에는 개별 아이템들이 위치한다.

사용자-아이템 행렬 예

사용자-아이템 행렬 예

이 예에서 1부터 5까지 다섯 명의 사용자와 A부터 E까지 다섯 개의 아이템이 있다. 각 셀의 값은 해당 사용자가 해당 아이템을 구입했는지 여부를 나타낸다(1은 구입, 0은 미구입). 이 예에서 가령 사용자 1은 아이템 B와 D를 구매하고, 사용자 2는 아이템 A, B, C, E를 구매했다.

사용자간 유사성 측정

사용자-아이템 행렬을 구축한 다음에는 이를 이용해서 사용자간의 유사성을 계산해야 한다. 유사성을 측정하는 다양한 방법이 있는데, 여기서는 예시적으로 코사인 유사성(cosine similarity)을 사용하기로 한다. 코사인 유사성 계산식은 다음과 같다.

\[ {\rm cos}(U_1,U_2)=\frac{\sum_{i=1}^{n}P_{1i}P_{2i}} {\sqrt{ \sum_{i=1}^{n}P_{1i}^{2}\cdot\sum_{i=1}^{n}P_{2i}^{2} }} \tag{11.1} \]

여기서 \(U_1\)\(U_2\)는 사용자 1과 사용자 2를 나타내며, \(P_{1i}\)\(P_{2i}\)는 사용자 1과 사용자 2가 제품 \(i\)를 몇 개 구입했는지를 나타낸다. 여기에서는 개수 대신 구매는 1, 비구매는 0의 값을 갖는다.

[사례 1] 아래 표는 사용자 1과 2만 표시한 것으로 이들 두 사용자의 코사인 유사성을 식 11.1로 계산하면 다음과 같다.

사례 1

\[ {\rm cos}(U_1,U_2)=\frac{0+1+0+0+0}{\sqrt{(1+1)(1+1+1+1)}}=\frac{1}{\sqrt{8}}=0.354 \]

[사례 2] 아래 표는 사용자 2와 4만 표시한 것으로 이들 두 사용자의 코사인 유사성을 식 11.1로 계산하면 다음과 같다.

사례 2

\[ {\rm cos}(U_1,U_2)=\frac{1+0+1+0+1}{\sqrt{(1+1+1+1)(1+1+1)}}=\frac{3}{\sqrt{12}}=0.866 \]

코사인 유사성이 클수록 두 명의 사용자가 더욱 유사함을 의미한다. 앞의 예에서 사용자 2와 4의 아이템 구매 패턴이 사용자 1과 2의 구매 패턴보다 훨씬 유사하다는 것을 눈으로도 쉽게 확인할 수 있다. 그러나 아이템의 개수가 많아지면 눈으로 파악하기 어려우며, 식 11.1과 같은 공식이 필요할 것이다.

ChatGPT Q&A#

사용자 유사성을 측정할 때 코사인 유사성(cosine similarity) 대신 가령 상관계수를 사용해도 될까?


네, 사용자 유사성을 측정할 때 코사인 유사성(cosine similarity) 대신 상관계수(correlation coefficient)를 사용하는 것도 가능합니다. 코사인 유사성과 상관계수는 서로 다른 상황과 데이터 특성에 따라 유용할 수 있습니다.

코사인 유사성 (Cosine Similarity)

  • 코사인 유사성은 두 벡터 간의 각도를 측정하여 유사성을 평가합니다.

  • 값의 범위는 -1에서 1까지이며, 1은 완전히 같은 방향(유사함), 0은 독립적, -1은 완전히 반대 방향(불일치)을 의미합니다.

  • 크기에 덜 민감하고 방향성에 중점을 둡니다. 즉, 두 벡터의 길이(값의 크기)가 달라도 유사한 방향을 가지고 있다면 높은 유사성을 나타냅니다.

  • 특히 텍스트 데이터나 고차원 데이터에서 유용하게 사용됩니다.

상관계수 (Correlation Coefficient)

  • 상관계수는 두 변수 간의 선형적 관계의 강도와 방향을 측정합니다.

  • 값의 범위는 -1에서 1까지이며, 1은 완벽한 양의 선형 관계, 0은 선형 관계의 부재, -1은 완벽한 음의 선형 관계를 의미합니다.

  • 두 변수의 변동이 얼마나 잘 일치하는지를 나타냅니다.

  • 주로 통계학에서 사용되며, 특히 평가 점수나 선호도와 같이 연속적인 값에 대한 유사성을 측정할 때 유용합니다.

언제 어떤 것을 사용할까?

  • 코사인 유사성: 벡터의 크기보다 방향성이 더 중요할 때 사용합니다. 예를 들어, 사용자가 아이템에 대해 부여한 평점 패턴의 유사성을 측정할 때 적합할 수 있습니다.

  • 상관계수: 두 사용자 간의 평가 점수가 얼마나 선형적으로 관련되어 있는지를 알고 싶을 때 사용합니다. 예를 들어, 사용자가 아이템에 대해 부여한 평점의 경향이 얼마나 비슷한지를 파악할 때 유용합니다.

최종적으로 선택은 분석하려는 데이터의 특성과 목적에 따라 달라질 수 있습니다. 때로는 두 방법을 모두 시도해보고, 어떤 방법이 문제에 더 적합한 결과를 제공하는지 평가하는 것이 좋습니다.

사용자-아이템 행렬#

우리가 보유한 데이터는 고객이 구매한 개별 아이템을 나타낸다. 그러나 협업 필터링 알고리즘을 사용하여 제품 추천 시스템을 구축하려면 각 고객이 어떤 일련의 아이템을 구매했는지를 보여주는 데이터가 있어야 한다. 즉 주어진 데이터를 변환하여 각 행은 고객, 그리고 각 열은 서로 다른 제품을 나타내는 사용자-아이템 행렬로 바꿔야 한다.

이를 위해 데이터프레임 dfpivot_table 함수를 적용한다. 행(row)에 해당하는 indexCustomerID로 정한다. 열에 해당하는 columns를 제품 코드인 StockCode로 정한다. valuesQuantity 변수로 하고, aggfunc=sum을 사용하면 각 사용자가 각 아이템별로 구매한 모든 수량을 합칠 수 있다.

customer_item_matrix = df.pivot_table(
    index='CustomerID', columns='StockCode', values='Quantity', aggfunc='sum')
# 평균값은 aggfunc='sum' 대신 aggfunc='mean' 사용
customer_item_matrix.loc[12481:].head()    # CustomerID 12481 이후를 프린트함 
StockCode 10002 10080 10120 10125 10133 10135 11001 15030 15034 15036 ... 90214V 90214W 90214Y 90214Z BANK CHARGES C2 DOT M PADS POST
CustomerID
12481.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN 36.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN 32.0
12483.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN 16.0
12484.0 NaN NaN NaN NaN NaN NaN 16.0 NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN 21.0
12488.0 NaN NaN NaN NaN NaN 10.0 NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN 3.0
12489.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN 2.0

5 rows × 3665 columns

사용자-아이템 행렬을 0-1로 인코딩 한다. 1은 제품 구매를 의미하고, 0은 구매하지 않을 것을 의미한다. map(lambda) 함수를 사용하여 행렬의 원소 값이 0보다 크면 1, 그렇지 않으면 0으로 만든다.

customer_item_matrix = customer_item_matrix.map(lambda x: 1 if x > 0 else 0)
customer_item_matrix.loc[12481:].head()
StockCode 10002 10080 10120 10125 10133 10135 11001 15030 15034 15036 ... 90214V 90214W 90214Y 90214Z BANK CHARGES C2 DOT M PADS POST
CustomerID
12481.0 0 0 0 0 0 0 0 0 0 1 ... 0 0 0 0 0 0 0 0 0 1
12483.0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 1
12484.0 0 0 0 0 0 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 1
12488.0 0 0 0 0 0 1 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 1
12489.0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 1

5 rows × 3665 columns

협업 필터링 실행#

사이킷런(scikit-learn, sklearn)에서 cosine_similarity 함수를 불러온 다음, 이를 사용자-아이템 행렬에 적용하여 사용자간 코사인 유사성 행렬(user-to-user similarity matrix)을 만든다.

사용자간 유사성 행렬

from sklearn.metrics.pairwise import cosine_similarity

user_user_sim_matrix = pd.DataFrame(cosine_similarity(customer_item_matrix))
user_user_sim_matrix.head()
0 1 2 3 4 5 6 7 8 9 ... 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338
0 1.0 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 0.000000 0.000000 0.000000 ... 0.0 0.000000 0.000000 0.0 0.000000 0.000000 0.0 0.000000 0.000000 0.000000
1 0.0 1.000000 0.063022 0.046130 0.047795 0.038484 0.0 0.025876 0.136641 0.094742 ... 0.0 0.029709 0.052668 0.0 0.032844 0.062318 0.0 0.113776 0.109364 0.012828
2 0.0 0.063022 1.000000 0.024953 0.051709 0.027756 0.0 0.027995 0.118262 0.146427 ... 0.0 0.064282 0.113961 0.0 0.000000 0.000000 0.0 0.000000 0.170905 0.083269
3 0.0 0.046130 0.024953 1.000000 0.056773 0.137137 0.0 0.030737 0.032461 0.144692 ... 0.0 0.105868 0.000000 0.0 0.039014 0.000000 0.0 0.067574 0.137124 0.030475
4 0.0 0.047795 0.051709 0.056773 1.000000 0.031575 0.0 0.000000 0.000000 0.033315 ... 0.0 0.000000 0.000000 0.0 0.000000 0.000000 0.0 0.000000 0.044866 0.000000

5 rows × 4339 columns

위 결과는 CustomerID 대신 행렬 인덱스로 표시돼 있기 때문에 이를 원래 사용자-아이템 행렬의 CustomerID로 바꿔준다.

user_user_sim_matrix.columns = customer_item_matrix.index
user_user_sim_matrix['CustomerID'] = customer_item_matrix.index
user_user_sim_matrix = user_user_sim_matrix.set_index('CustomerID')
user_user_sim_matrix.head()
CustomerID 12346.0 12347.0 12348.0 12349.0 12350.0 12352.0 12353.0 12354.0 12355.0 12356.0 ... 18273.0 18274.0 18276.0 18277.0 18278.0 18280.0 18281.0 18282.0 18283.0 18287.0
CustomerID
12346.0 1.0 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 0.000000 0.000000 0.000000 ... 0.0 0.000000 0.000000 0.0 0.000000 0.000000 0.0 0.000000 0.000000 0.000000
12347.0 0.0 1.000000 0.063022 0.046130 0.047795 0.038484 0.0 0.025876 0.136641 0.094742 ... 0.0 0.029709 0.052668 0.0 0.032844 0.062318 0.0 0.113776 0.109364 0.012828
12348.0 0.0 0.063022 1.000000 0.024953 0.051709 0.027756 0.0 0.027995 0.118262 0.146427 ... 0.0 0.064282 0.113961 0.0 0.000000 0.000000 0.0 0.000000 0.170905 0.083269
12349.0 0.0 0.046130 0.024953 1.000000 0.056773 0.137137 0.0 0.030737 0.032461 0.144692 ... 0.0 0.105868 0.000000 0.0 0.039014 0.000000 0.0 0.067574 0.137124 0.030475
12350.0 0.0 0.047795 0.051709 0.056773 1.000000 0.031575 0.0 0.000000 0.000000 0.033315 ... 0.0 0.000000 0.000000 0.0 0.000000 0.000000 0.0 0.000000 0.044866 0.000000

5 rows × 4339 columns

추천하기

# CustomerID=12350에 대해 사용자간 유사성 지수를 내림차순으로 프린트함
user_user_sim_matrix.loc[12350.0].sort_values(ascending=False)
CustomerID
12350.0    1.000000
17935.0    0.183340
12414.0    0.181902
12652.0    0.175035
16692.0    0.171499
             ...   
14885.0    0.000000
14886.0    0.000000
14887.0    0.000000
14888.0    0.000000
18287.0    0.000000
Name: 12350.0, Length: 4339, dtype: float64

유사성이 가장 높은 12350(A) 및 17935(B) 고객에 대해 사용자-아이템 행렬의 원소가 0이 아닌 아이템을 반환시킨다. A가 구입한 아이템 중 B가 아직 구입하지 않은 아이템을 B에 대한 추천 리스트로 지정한다.

items_bought_by_A = set(customer_item_matrix.loc[12350].iloc[
    customer_item_matrix.loc[12350].to_numpy().nonzero()].index)
items_bought_by_B = set(customer_item_matrix.loc[17935].iloc[
    customer_item_matrix.loc[17935].to_numpy().nonzero()].index)
items_to_recommend_to_B = items_bought_by_A - items_bought_by_B
items_to_recommend_to_B
{20615,
 20652,
 21171,
 21832,
 21864,
 21908,
 21915,
 22348,
 22412,
 22620,
 '79066K',
 '79191C',
 '84086C'}

원래의 데이터프레임 df를 이용해 items_to_recommend_to_B에 있는 관측에 대해 StockCodeDescription을 반환시킨다.

df.loc[
    df['StockCode'].isin(items_to_recommend_to_B), ['StockCode', 'Description']
].drop_duplicates().set_index('StockCode')
Description
StockCode
21832 CHOCOLATE CALCULATOR
21915 RED HARMONICA IN BOX
22620 4 TRADITIONAL SPINNING TOPS
79066K RETRO MOD TRAY
21864 UNION JACK FLAG PASSPORT COVER
79191C RETRO PLASTIC ELEPHANT TRAY
21908 CHOCOLATE THIS WAY METAL SIGN
20615 BLUE POLKADOT PASSPORT COVER
20652 BLUE POLKADOT LUGGAGE TAG
22348 TEA BAG PLATE RED RETROSPOT
22412 METAL SIGN NEIGHBOURHOOD WITCH
21171 BATHROOM METAL SIGN
84086C PINK/PURPLE RETRO RADIO

아이템간 유사성 행렬

사용자-아이템 행렬을 전치(transpose)시켜 cosine_similarity 함수를 적용하면 아이템간 코사인 유사성을 계산할 수 있다. StockCode 대신 행렬 인덱스로 표시돼 있기 때문에 이를 원래 사용자-아이템 행렬의 StockCode로 바꿔준다.

item_item_sim_matrix = pd.DataFrame(cosine_similarity(customer_item_matrix.T))
item_item_sim_matrix.columns = customer_item_matrix.T.index
item_item_sim_matrix['StockCode'] = customer_item_matrix.T.index
item_item_sim_matrix = item_item_sim_matrix.set_index('StockCode')
item_item_sim_matrix.head()
StockCode 10002 10080 10120 10125 10133 10135 11001 15030 15034 15036 ... 90214V 90214W 90214Y 90214Z BANK CHARGES C2 DOT M PADS POST
StockCode
10002 1.000000 0.000000 0.094868 0.090351 0.062932 0.098907 0.095346 0.047673 0.075593 0.090815 ... 0.0 0.0 0.0 0.0 0.0 0.029361 0.0 0.066915 0.000000 0.078217
10080 0.000000 1.000000 0.000000 0.032774 0.045655 0.047836 0.000000 0.000000 0.082261 0.049413 ... 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.016182 0.000000 0.000000
10120 0.094868 0.000000 1.000000 0.057143 0.059702 0.041703 0.060302 0.060302 0.095618 0.028718 ... 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.070535 0.000000 0.010993
10125 0.090351 0.032774 0.057143 1.000000 0.042644 0.044682 0.043073 0.000000 0.051224 0.030770 ... 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.070535 0.000000 0.070669
10133 0.062932 0.045655 0.059702 0.042644 1.000000 0.280097 0.045002 0.060003 0.071358 0.057152 ... 0.0 0.0 0.0 0.0 0.0 0.036955 0.0 0.070185 0.049752 0.021877

5 rows × 3665 columns

추천하기

어떤 고객이 방금 가령 StockCode 23166 제품을 구매했을 때, 이와 가장 유사한 제품 10개를 추천하고자 한다.

top_10_similar_items = list(
    item_item_sim_matrix.loc[23166].sort_values(ascending=False).iloc[:10].index)

원래의 데이터프레임 df를 이용해 top_10_similar_items에 있는 관측에 대해 StockCodeDescription을 반환시킨다.

df.loc[df['StockCode'].isin(top_10_similar_items), ['StockCode', 'Description']
      ].drop_duplicates().set_index('StockCode').loc[top_10_similar_items]
Description
StockCode
23166 MEDIUM CERAMIC TOP STORAGE JAR
23165 LARGE CERAMIC TOP STORAGE JAR
23167 SMALL CERAMIC TOP STORAGE JAR
22993 SET OF 4 PANTRY JELLY MOULDS
23307 SET OF 60 PANTRY DESIGN CAKE CASES
22722 SET OF 6 SPICE TINS PANTRY DESIGN
22720 SET OF 3 CAKE TINS PANTRY DESIGN
22666 RECIPE BOX PANTRY YELLOW DESIGN
23243 SET OF TEA COFFEE SUGAR TINS PANTRY
22961 JAM MAKING SET PRINTED

11.4 영화 추천 시스템 예제#

코드 출처: (1) The Age of Recommender Systems (Kaggle post) (2) Hwang (2019), Hands-On Data Science for Marketing

여기에서는 영화 추천에 있어서 인구통계적 필터링과 콘텐츠 기반 필터링에 대한 내용과 파이썬 코딩을 학습한다. 먼저 인구통계적(demographic) 필터링은 인구통계적 특성(나이, 성별, 인종/민족, 거주지, 사회경제적 상태 등)에 따라 영화의 인기 및 장르를 고려하여 추천을 제공하는 것을 말한다.

콘텐츠 기반(content based) 필터링은 특정 아이템에 대해 콘텐츠에 있어서 그것과 유사한 아이템을 제안하는 방식으로서 영화 추천에 있어서는 영화의 장르, 감독, 줄거리, 배우 등 영화에 대한 메타데이터(metadata)를 사용하여 추천을 행하는 방식이다. 메타데이터란 어떤 정보나 데이터의 속성을 설명하는 데이터이다. 이를테면, 디지털 카메라는 사진(화상 데이터)을 찍어 기록할 때마다 카메라 자체의 정보와 촬영 당시의 시간, 노출, 플래시 사용 여부, 해상도, 사진 크기, 장소 등의 정보를 화상 데이터와 같이 저장하는데, 이를 사진에 대한 메타데이터라고 한다.

데이터#

IMDB의 TMDb(The Movie Database)에서 뽑은 두 가지의 데이터세트를 병합해서 사용한다.

tmdb_5000_credits.csv

  • movie_id: 각 영화의 고유 식별 ID

  • cast: 주연 및 조연 배우의 이름

  • crew: 감독, 편집자, 작곡가, 작가 등의 이름

tmdb_5000_movies.csv

  • budget: 영화 제작 예산

  • genre: 영화 장르, 액션, 코미디, 스릴러 등

  • homepage: 영화 홈페이지로 연결되는 링크

  • id: 첫 번째 데이터세트와 동일한 movie_id

  • keywords: 영화와 관련된 키워드 또는 태그

  • original_language: 영화 제작 언어

  • original_title: 번역 또는 수정 전의 영화 제목

  • overview: 영화에 대한 간단한 개요

  • popularity: 영화 인기를 나타내는 숫자

  • production_companies: 영화 제작소

  • production_countries: 영화 생산 국가

  • release_date: 영화 릴리스 날짜

  • revenue: 영화에서 생성된 전 세계 수익

  • runtime: 영화 실행 시간(분)

  • status: “Released” 또는 “Rumored”

  • tagline: 영화의 태그 라인

  • title: 영화 제목

  • vote_average: 영화가 받은 평균 평점

  • vote_count: 받은 투표 수

import pandas as pd
import numpy as np
url1 = ('https://raw.githubusercontent.com/sd12832/MoViZ/master/' + 
        'src/tmdb_5000_credits.csv')
df1 = pd.read_csv(url1)
print(df1.shape)
df1.head()
(4803, 4)
movie_id title cast crew
0 19995 Avatar [{"cast_id": 242, "character": "Jake Sully", "... [{"credit_id": "52fe48009251416c750aca23", "de...
1 285 Pirates of the Caribbean: At World's End [{"cast_id": 4, "character": "Captain Jack Spa... [{"credit_id": "52fe4232c3a36847f800b579", "de...
2 206647 Spectre [{"cast_id": 1, "character": "James Bond", "cr... [{"credit_id": "54805967c3a36829b5002c41", "de...
3 49026 The Dark Knight Rises [{"cast_id": 2, "character": "Bruce Wayne / Ba... [{"credit_id": "52fe4781c3a36847f81398c3", "de...
4 49529 John Carter [{"cast_id": 5, "character": "John Carter", "c... [{"credit_id": "52fe479ac3a36847f813eaa3", "de...
url2 = ('https://raw.githubusercontent.com/sd12832/MoViZ/master/' + 
        'src/tmdb_5000_movies.csv')
df2 = pd.read_csv(url2)
print(df2.shape)
df2.head(2)
(4803, 20)
budget genres homepage id keywords original_language original_title overview popularity production_companies production_countries release_date revenue runtime spoken_languages status tagline title vote_average vote_count
0 237000000 [{"id": 28, "name": "Action"}, {"id": 12, "nam... http://www.avatarmovie.com/ 19995 [{"id": 1463, "name": "culture clash"}, {"id":... en Avatar In the 22nd century, a paraplegic Marine is di... 150.437577 [{"name": "Ingenious Film Partners", "id": 289... [{"iso_3166_1": "US", "name": "United States o... 2009-12-10 2787965087 162.0 [{"iso_639_1": "en", "name": "English"}, {"iso... Released Enter the World of Pandora. Avatar 7.2 11800
1 300000000 [{"id": 12, "name": "Adventure"}, {"id": 14, "... http://disney.go.com/disneypictures/pirates/ 285 [{"id": 270, "name": "ocean"}, {"id": 726, "na... en Pirates of the Caribbean: At World's End Captain Barbossa, long believed to be dead, ha... 139.082615 [{"name": "Walt Disney Pictures", "id": 2}, {"... [{"iso_3166_1": "US", "name": "United States o... 2007-05-19 961000000 169.0 [{"iso_639_1": "en", "name": "English"}] Released At the end of the world, the adventure begins. Pirates of the Caribbean: At World's End 6.9 4500

데이터세트 병합(merge): df1movie_id 변수를 id로 이름을 바꾸어 df2 와 동일하게 만든 다음, merge 메서드를 사용해 df1df2id 변수를 매개로 병합시킨다.

df1.columns = ['id', 'tittle', 'cast', 'crew']
df = df2.merge(df1, on = 'id')
print(df.shape)
(4803, 23)

인구통계 필터링#

모든 영화에 대해 평점을 매겨 최고 등급의 영화를 추천하려고 한다. 이를 위해서는 영화에 대한 사람들의 평점을 평균할 필요가 있다. 그런데 가령 20명이 투표에 참가해서 평균 9.0점인 영화와 1만명이 참가해서 평균이 8.0점인 영화를 그대로 비교하는 것은 적절하지 않다. IMDB의 영화 평점 산정은 다음과 같은 가중평균 방식이다.

\[ {\rm Weighted \ Rating \ (WR)}=\frac{v}{v+m}\times R + \frac{m}{v+m}\times C \]

여기에서 \(v\)는 해당 영화에 대한 투표수, \(m\)은 차트에 표시되기 위해 필요한 최소 투표수, \(R\)은 해당 영화의 평균 평점, \(C\)는 전체 영화의 평균 평점이다. 앞에서 예로 든 20명이 투표에 참가해서 평균 9.0점인 영화의 경우 가중평균평점(\(\rm WR\))이 6.12이고, 1만명이 참가해서 평균이 8.0점인 영화는 \(\rm WR\)이 7.70이다.

전체 영화

각 영화의 평점(vote_average)에 대해 전체 평균값(\(C\))을 계산한 결과는 6.09이다. 영화 투표수(vote_count)의 90% 분위수(quantile)는 1,838.4로서 이를 \(m\)(차트에 표시되기 위해 필요한 최소 투표수)으로 삼는다.

C = df['vote_average'].mean()
m = df['vote_count'].quantile(0.9)
C, m
(6.092171559442016, 1838.4000000000015)

평점 투표수가 \(m\)(차트에 표시되기 위해 필요한 최소 투표수)보다 큰 영화만 추려내 q_movies로 지정한 결과, 관측이 원래 4,803개의 1/10인 481개로 줄어든다.

q_movies = df.loc[df['vote_count'] >= m]
q_movies.shape    # (481, 23)
(481, 23)

위에 제시된 가중평균점수 산정 방식을 함수(weighted_rating)로 만들어 투표수 상위 10%인 q_movies에 적용하여 점수를 계산한 다음, score 변수라는 이름으로 q_movies에 추가한다. q_moviesscore 변수 기준으로 내림차순 정렬한 다음, 최상위 10개를 프린트한다.

# IMDB 평점 계산 함수
def weighted_rating(x, m=m, C=C):
    v = x['vote_count']
    R = x['vote_average']
    return (v/(v+m) * R) + (m/(m+v) * C)

# q_movies 데이터프레임에 IMDB 평점 계산 함수 적용
score = q_movies.apply(weighted_rating, axis=1)
q_movies = q_movies.assign(score = score)
q_movies = q_movies.sort_values('score', ascending=False)
q_movies[['title', 'vote_count', 'vote_average', 'score', 'original_language']].head(10)
title vote_count vote_average score original_language
1881 The Shawshank Redemption 8205 8.5 8.059258 en
662 Fight Club 9413 8.3 7.939256 en
65 The Dark Knight 12002 8.2 7.920020 en
3232 Pulp Fiction 8428 8.3 7.904645 en
96 Inception 13752 8.1 7.863239 en
3337 The Godfather 5893 8.4 7.851236 en
95 Interstellar 10867 8.1 7.809479 en
809 Forrest Gump 7927 8.2 7.803188 en
329 The Lord of the Rings: The Return of the King 8064 8.1 7.727243 en
1990 The Empire Strikes Back 5879 8.2 7.697884 en

비영어권 영화

영화 제작 언어(original_language)가 영어(en)가 아닌 영화들만 추려내 f_movies로 지정한다. 관측이 298개이다.

f_movies = df.loc[df['original_language'] != 'en']
f_movies.shape    # (298, 23)
(298, 23)

투표수(vote_count)에 대한 요약통계량을 보면, 투표수 최대는 3,840, 최소는 0, 중위값은 90.5이다.

f_movies['vote_count'].describe()    
count     298.000000
mean      254.748322
std       486.358454
min         0.000000
25%        23.250000
50%        90.500000
75%       264.500000
max      3840.000000
Name: vote_count, dtype: float64

영화의 평점(vote_average)에 대해 평균값(\(C\))을 계산한 결과 6.49이다. 영화 투표수(vote_count)의 50% 분위수(quantile)에 해당하는 90.5를 차트에 표시되기 위해 필요한 최소 투표수(\(m\))로 삼기로 한다.

C = f_movies['vote_average'].mean()
m = f_movies['vote_count'].quantile(0.5)
C, m
(6.492617449664429, 90.5)

앞에서 만든 가중평균점수 함수(weighted_rating)를 외국 영화 투표수 상위 50%인 f_movies에 적용한 다음, 앞에서와 동일한 방식으로 score 변수 기준으로 최상위 10개를 반환한다.

score = f_movies.apply(weighted_rating, axis=1)
f_movies = f_movies.assign(score = score)
f_movies = f_movies.sort_values('score', ascending=False)
f_movies[['title', 'vote_count', 'vote_average', 'score', 'original_language']].head(10)
title vote_count vote_average score original_language
2294 Spirited Away 3840 8.3 7.585209 ja
4302 The Good, the Bad and the Ugly 2311 8.1 7.210428 it
1260 Amélie 3310 7.8 7.190166 fr
1987 Howl's Moving Castle 1991 8.2 7.188084 ja
2247 Princess Mononoke 1983 8.2 7.185965 ja
3866 City of God 1814 8.1 7.089379 pt
3940 Oldboy 1945 8.0 7.072963 ko
2465 Pan's Labyrinth 3041 7.6 7.031899 es
3622 Once Upon a Time in the West 1128 8.1 6.855666 it
4535 Seven Samurai 878 8.2 6.773468 ja

줄거리 기반 추천#

콘텐츠 기반 추천 시스템은 영화의 콘텐츠(개요, 캐스트, 제작진, 키워드, 태그 라인 등)를 이용하여 어떤 영화에 대해 다른 영화와의 유사성을 계산하여 해당 영화와 가장 유사한 영화를 추천하는 방식이다. 여기에서는 줄거리 기반메타데이터 기반 추천 시스템을 만들어 본다.

먼저 줄거리 기반 추천 시스템은 줄거리(plot descriptions)를 기반으로 모든 영화에 대해 쌍별 유사성 점수를 계산하고 그것을 기반으로 영화를 추천하는 방식이다.

이를 위해 먼저 각 영화의 overview 변수에 대해 TF-IDF(Term Frequency-Inverse Document Frequency: 단어빈도-역문서빈도) 행렬을 만든다. 여기에서 TF(단어빈도)는 해당 문서에서 해당 단어가 얼마나 자주 나오는지 상대적 빈도로서 해당 단어 개수를 총 단어 개수로 나눈 것이다. 또한 IDF(역문서빈도)는 전체 문서에서 해당 단어가 얼마나 드물게 나오는지를 측정하는 것으로서 전체 문서 개수를 해당 단어를 가진 문서 개수로 나눈 것이다. 각 단어가 문서에서 가지는 종합적인 중요도는 TF와 IDF를 곱하여 평가한다. 즉 해당 단어의 상대적 빈도가 높을수록, 그리고 많은 문서에 공통으로 나오지 않을수록 중요도가 높은 것으로 평가하는 셈이다.

ChatGPT Q&A#

TF-IDF 행렬을 구하는 방법에 대해 아주 간단한 예를 들어 설명해줘.


TF-IDF (Term Frequency-Inverse Document Frequency)는 문서 내에서 특정 단어의 중요도를 평가하는 방법입니다. 이를 통해 문서들로 구성된 코퍼스(문서 집합)에서 각 단어의 중요도를 나타내는 행렬을 만들 수 있습니다. 여기서 간단한 예를 들어 설명해보겠습니다.

예시

문서 집합 (코퍼스):

  • 문서 1: “the cat sat on the mat”

  • 문서 2: “the dog sat on the log”

단계별 설명:

  1. TF (Term Frequency): 각 문서에서 단어가 등장하는 빈도를 계산합니다.

    • 예를 들어, 문서 1에서 ‘the’의 빈도는 2, ‘cat’의 빈도는 1입니다.

  2. IDF (Inverse Document Frequency): 코퍼스 전체에서 단어가 얼마나 흔하게 등장하는지 반대로 계산합니다. 이는 흔한 단어의 중요도를 낮추고, 드문 단어의 중요도를 높입니다.

    • 예를 들어, ‘the’는 두 문서 모두에 등장하므로 덜 중요한 단어로 간주됩니다.

    • IDF는 일반적으로 log(총 문서 / 단어가 등장한 문서 수)로 계산됩니다.

  3. TF-IDF 계산: 각 단어의 TF와 IDF 값을 곱하여 계산합니다.

    • 예를 들어, 문서 1에서 ‘the’의 TF-IDF 값은 ‘the’의 TF 값과 IDF 값을 곱한 값이 됩니다.

결과 행렬

결과적으로, 각 문서에 대해 단어별 TF-IDF 값을 계산하고, 이를 행렬로 나타낼 수 있습니다. 행렬의 각 행은 문서를, 각 열은 코퍼스의 고유 단어를 나타냅니다.

이러한 TF-IDF 행렬은 텍스트 마이닝, 문서 분류, 정보 검색 등에서 널리 사용됩니다. 예를 들어, 문서 간의 유사성을 비교하거나 키워드 추출, 검색 엔진 최적화 등에 활용될 수 있습니다.

ChatGPT Q&A#

그래서 위 예(즉, 문서 1: “the cat sat on the mat”, 문서 2: “the dog sat on the log”)에서 TF-IDF 행렬은 뭐가 되는거야?


위의 예시인 “the cat sat on the mat”과 “the dog sat on the log” 두 문서에 대한 TF-IDF 행렬을 구하는 과정은 다음과 같습니다.

먼저, 각 단계를 수행해야 합니다:

  1. 단어 목록 작성: 두 문서에 등장하는 모든 고유 단어를 나열합니다.

    • 고유 단어: ‘the’, ‘cat’, ‘sat’, ‘on’, ‘mat’, ‘dog’, ‘log’

  2. TF 계산: 각 문서에서 각 단어의 빈도(Term Frequency)를 계산합니다.

    문서 1의 TF:

    • ‘the’: 2/6

    • ‘cat’: 1/6

    • ‘sat’: 1/6

    • ‘on’: 1/6

    • ‘mat’: 1/6

    • ‘dog’: 0

    • ‘log’: 0

    문서 2의 TF:

    • ‘the’: 2/6

    • ‘cat’: 0

    • ‘sat’: 1/6

    • ‘on’: 1/6

    • ‘mat’: 0

    • ‘dog’: 1/6

    • ‘log’: 1/6

  3. IDF 계산: 각 단어에 대한 역문서 빈도(Inverse Document Frequency)를 계산합니다.

    • ‘the’, ‘sat’, ‘on’: log(2/2) = 0 (모든 문서에 등장)

    • ‘cat’, ‘mat’, ‘dog’, ‘log’: log(2/1) = log(2) (한 문서에만 등장)

  4. TF-IDF 계산: 각 문서의 각 단어에 대해 TF와 IDF를 곱합니다.

    문서 1의 TF-IDF:

    • ‘the’: 2/6 * 0 = 0

    • ‘cat’: 1/6 * log(2) ≈ 0.1155

    • ‘sat’: 1/6 * 0 = 0

    • ‘on’: 1/6 * 0 = 0

    • ‘mat’: 1/6 * log(2) ≈ 0.1155

    • ‘dog’: 0

    • ‘log’: 0

    문서 2의 TF-IDF:

    • ‘the’: 2/6 * 0 = 0

    • ‘cat’: 0

    • ‘sat’: 1/6 * 0 = 0

    • ‘on’: 1/6 * 0 = 0

    • ‘mat’: 0

    • ‘dog’: 1/6 * log(2) ≈ 0.1155

    • ‘log’: 1/6 * log(2) ≈ 0.1155

결과적으로, TF-IDF 행렬은 다음과 같습니다 (값은 반올림하여 표시):

the

cat

sat

on

mat

dog

log

문서 1

0

0.1155

0

0

0.1155

0

0

문서 2

0

0

0

0

0

0.1155

0.1155

이 행렬에서 각 행은 문서를 나타내고, 각 열은 특정 단어의 TF-IDF 값을 나타냅니다. 이 값들은 문서 내에서 단어의 중요도를 반영합니다.

영화 콘텐츠 TF-IDF 행렬

여기에서 만들려고 하는 TF-IDF 행렬에서 각 행은 영화이고, 각 열은 overview 어휘(한 개 이상의 문서에 나타나는 모든 단어)이며, 각 셀은 TF-IDF를 나타낸다. 가령 영화 “올드보이”의 한 문장으로 된 줄거리를 살펴보자.

df['overview'].loc[3940] # 올드보이 줄거리
'With no clue how he came to be imprisoned, drugged and tortured for 15 years, a desperate businessman seeks revenge on his captors.'

이와 같은 영화의 overview가 있을 때, 여기에 들어있는 단어들을 이용하여 영화의 유사성을 측정하는 것이 줄거리 기반 추천이다.

sklearn에서 TfidfVectorizer를 불러들인다. 그것을 이용해서 불용어(stop words), 즉 자주 등장하지만 분석에서 큰 의미가 없는 단어들(“the”, “a” 등)을 제거한다.

from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(stop_words='english')

overview 변수가 결측값(NaN)인 경우 문자가 없는 것으로 간주한다(즉 empty string). 그런 다음 overview 변수에 대해 tfidf를 적용하여 TF-IDF 행렬을 만들고, 이를 tfidf_matrix로 지정한다.(4,803 rows × 20,978 columns)

df['overview'] = df['overview'].fillna('')
tfidf_matrix = tfidf.fit_transform(df['overview'])
tfidf_matrix.shape    # (4803, 20978)
(4803, 20978)

sklearncosine_similarity 함수를 TF-IDF 행렬에 적용하여 이를 기반으로 영화간 유사성 행렬(cosine_sim)을 만든다. 행 인덱스 및 열 이름을 영화 제목(title)으로 한다.

sim_matrix = pd.DataFrame(
    cosine_similarity(tfidf_matrix), index = df['title'], columns = df['title'])
sim_matrix.head()
title Avatar Pirates of the Caribbean: At World's End Spectre The Dark Knight Rises John Carter Spider-Man 3 Tangled Avengers: Age of Ultron Harry Potter and the Half-Blood Prince Batman v Superman: Dawn of Justice ... On The Downlow Sanctuary: Quite a Conundrum Bang Primer Cavite El Mariachi Newlyweds Signed, Sealed, Delivered Shanghai Calling My Date with Drew
title
Avatar 1.000000 0.000000 0.0 0.024995 0.000000 0.030353 0.000000 0.037581 0.000000 0.000000 ... 0.000000 0.0 0.029175 0.042176 0.000000 0.0 0.0 0.000000 0.000000 0.000000
Pirates of the Caribbean: At World's End 0.000000 1.000000 0.0 0.000000 0.033369 0.000000 0.000000 0.022676 0.000000 0.000000 ... 0.000000 0.0 0.006895 0.000000 0.000000 0.0 0.0 0.021605 0.000000 0.000000
Spectre 0.000000 0.000000 1.0 0.000000 0.000000 0.000000 0.000000 0.030949 0.024830 0.000000 ... 0.027695 0.0 0.000000 0.000000 0.017768 0.0 0.0 0.014882 0.000000 0.000000
The Dark Knight Rises 0.024995 0.000000 0.0 1.000000 0.010433 0.005145 0.012601 0.026954 0.020652 0.133740 ... 0.000000 0.0 0.000000 0.000000 0.000000 0.0 0.0 0.033864 0.042752 0.022692
John Carter 0.000000 0.033369 0.0 0.010433 1.000000 0.000000 0.009339 0.037407 0.000000 0.017148 ... 0.012730 0.0 0.000000 0.000000 0.000000 0.0 0.0 0.006126 0.000000 0.000000

5 rows × 4803 columns

예를 들어, 영화 “The Dark Knight Rises”에 대해 줄거리 기반 추천 10개 영화(코사인 유사성 상위 10개 영화)를 반환시켰더니 다음과 같은 결과가 나왔다.

top_10_similar_items = sim_matrix[
    'The Dark Knight Rises'].sort_values(ascending=False).iloc[1:11]
top_10_similar_items
title
The Dark Knight                            0.301512
Batman Forever                             0.298570
Batman Returns                             0.287851
Batman                                     0.264461
Batman: The Dark Knight Returns, Part 2    0.185450
Batman Begins                              0.167996
Slow Burn                                  0.166829
Batman v Superman: Dawn of Justice         0.133740
JFK                                        0.132197
Batman & Robin                             0.130455
Name: The Dark Knight Rises, dtype: float64

메타데이터 기반 추천#

여기에서는 3명의 톱배우, 감독, 장르, 영화 플롯 키워드 등의 메타데이터를 기반으로 추천을 하고자 한다. 이를 위해서는 cast, crew, keywords, genres 변수에서 이를 추출해야 한다. 우선 이들 변수가 어떤 형태인지 살펴보자.

df[['cast', 'crew', 'keywords', 'genres']].head()
cast crew keywords genres
0 [{"cast_id": 242, "character": "Jake Sully", "... [{"credit_id": "52fe48009251416c750aca23", "de... [{"id": 1463, "name": "culture clash"}, {"id":... [{"id": 28, "name": "Action"}, {"id": 12, "nam...
1 [{"cast_id": 4, "character": "Captain Jack Spa... [{"credit_id": "52fe4232c3a36847f800b579", "de... [{"id": 270, "name": "ocean"}, {"id": 726, "na... [{"id": 12, "name": "Adventure"}, {"id": 14, "...
2 [{"cast_id": 1, "character": "James Bond", "cr... [{"credit_id": "54805967c3a36829b5002c41", "de... [{"id": 470, "name": "spy"}, {"id": 818, "name... [{"id": 28, "name": "Action"}, {"id": 12, "nam...
3 [{"cast_id": 2, "character": "Bruce Wayne / Ba... [{"credit_id": "52fe4781c3a36847f81398c3", "de... [{"id": 849, "name": "dc comics"}, {"id": 853,... [{"id": 28, "name": "Action"}, {"id": 80, "nam...
4 [{"cast_id": 5, "character": "John Carter", "c... [{"credit_id": "52fe479ac3a36847f813eaa3", "de... [{"id": 818, "name": "based on novel"}, {"id":... [{"id": 28, "name": "Action"}, {"id": 12, "nam...

위에서 보듯이 cast, crew, keywords, genres의 설명 내역에 큰따옴표로 표시된 것을 파이썬에서 사용가능한 데이터로 전환해야 한다. 이를 위해 AST(Abstract Syntax Trees) 모듈을 불러들여 literal_eval 함수를 각 변수에 적용한다.

from ast import literal_eval

features = ['cast', 'crew', 'keywords', 'genres']
for i in features:
    df[i] = df[i].apply(literal_eval)
df[['cast', 'crew', 'keywords', 'genres']].head()
cast crew keywords genres
0 [{'cast_id': 242, 'character': 'Jake Sully', '... [{'credit_id': '52fe48009251416c750aca23', 'de... [{'id': 1463, 'name': 'culture clash'}, {'id':... [{'id': 28, 'name': 'Action'}, {'id': 12, 'nam...
1 [{'cast_id': 4, 'character': 'Captain Jack Spa... [{'credit_id': '52fe4232c3a36847f800b579', 'de... [{'id': 270, 'name': 'ocean'}, {'id': 726, 'na... [{'id': 12, 'name': 'Adventure'}, {'id': 14, '...
2 [{'cast_id': 1, 'character': 'James Bond', 'cr... [{'credit_id': '54805967c3a36829b5002c41', 'de... [{'id': 470, 'name': 'spy'}, {'id': 818, 'name... [{'id': 28, 'name': 'Action'}, {'id': 12, 'nam...
3 [{'cast_id': 2, 'character': 'Bruce Wayne / Ba... [{'credit_id': '52fe4781c3a36847f81398c3', 'de... [{'id': 849, 'name': 'dc comics'}, {'id': 853,... [{'id': 28, 'name': 'Action'}, {'id': 80, 'nam...
4 [{'cast_id': 5, 'character': 'John Carter', 'c... [{'credit_id': '52fe479ac3a36847f813eaa3', 'de... [{'id': 818, 'name': 'based on novel'}, {'id':... [{'id': 28, 'name': 'Action'}, {'id': 12, 'nam...
# 첫 번째 영화의 crew 리스트(각 crew에 대한 설명이 딕셔너리 형태로 나오는데, 처음 3명만 반환시킴)
a = df['crew'].iloc[0]
a[0:3]
[{'credit_id': '52fe48009251416c750aca23',
  'department': 'Editing',
  'gender': 0,
  'id': 1721,
  'job': 'Editor',
  'name': 'Stephen E. Rivkin'},
 {'credit_id': '539c47ecc3a36810e3001f87',
  'department': 'Art',
  'gender': 2,
  'id': 496,
  'job': 'Production Design',
  'name': 'Rick Carter'},
 {'credit_id': '54491c89c3a3680fb4001cf7',
  'department': 'Sound',
  'gender': 0,
  'id': 900,
  'job': 'Sound Designer',
  'name': 'Christopher Boyes'}]

crew 변수에서 감독 이름을 추출하기 위해 get_director라는 함수를 만든다. 모든 원소에 대해 jobDirector이면 name을 반환하고, 그렇지 않으면 NaN을 반환하도록 하는 함수이다.

def get_director(x):
    for i in x:
        if i['job'] == 'Director':
            return i['name']
    return np.nan
b = df['cast'].iloc[0]
b[0:3]
[{'cast_id': 242,
  'character': 'Jake Sully',
  'credit_id': '5602a8a7c3a3685532001c9a',
  'gender': 2,
  'id': 65731,
  'name': 'Sam Worthington',
  'order': 0},
 {'cast_id': 3,
  'character': 'Neytiri',
  'credit_id': '52fe48009251416c750ac9cb',
  'gender': 1,
  'id': 8691,
  'name': 'Zoe Saldana',
  'order': 1},
 {'cast_id': 25,
  'character': 'Dr. Grace Augustine',
  'credit_id': '52fe48009251416c750aca39',
  'gender': 1,
  'id': 10205,
  'name': 'Sigourney Weaver',
  'order': 2}]
df['keywords'].iloc[0]
[{'id': 1463, 'name': 'culture clash'},
 {'id': 2964, 'name': 'future'},
 {'id': 3386, 'name': 'space war'},
 {'id': 3388, 'name': 'space colony'},
 {'id': 3679, 'name': 'society'},
 {'id': 3801, 'name': 'space travel'},
 {'id': 9685, 'name': 'futuristic'},
 {'id': 9840, 'name': 'romance'},
 {'id': 9882, 'name': 'space'},
 {'id': 9951, 'name': 'alien'},
 {'id': 10148, 'name': 'tribe'},
 {'id': 10158, 'name': 'alien planet'},
 {'id': 10987, 'name': 'cgi'},
 {'id': 11399, 'name': 'marine'},
 {'id': 13065, 'name': 'soldier'},
 {'id': 14643, 'name': 'battle'},
 {'id': 14720, 'name': 'love affair'},
 {'id': 165431, 'name': 'anti war'},
 {'id': 193554, 'name': 'power relations'},
 {'id': 206690, 'name': 'mind and soul'},
 {'id': 209714, 'name': '3d'}]

cast, keywords 변수는 리스트로 돼있는데, 이 리스트의 각 원소에서 name에 해당하는 것을 3개까지 추출하기 위해 get_list라는 함수를 만든다. isinstance는 주어진 인스턴스가 특정 클래스 또는 데이터 타입인지 검사해주는 함수이다.

def get_list(x):
    if isinstance(x, list):
        names = [i['name'] for i in x]
        if len(names) > 3:
            names = names[:3]
        return names
    return []

get_directorget_list 함수를 적용하여 director, cast, keywords, genres 변수를 (다시) 만든다.

# 'crew' 변수에 get_director 함수를 적용하여 'director' 변수를 만듦
# Define director, cast, keywords and genres features that are in a suitable form.
df['director'] = df['crew'].apply(get_director)

# 'cast', 'keywords', 'genres' 변수에 get_list 함수를 적용하여 이름을 3개씩 뽑아냄
features = ['cast', 'keywords', 'genres']
for i in features:
    df[i] = df[i].apply(get_list)

df[['title', 'cast', 'director', 'keywords', 'genres']].head()
title cast director keywords genres
0 Avatar [Sam Worthington, Zoe Saldana, Sigourney Weaver] James Cameron [culture clash, future, space war] [Action, Adventure, Fantasy]
1 Pirates of the Caribbean: At World's End [Johnny Depp, Orlando Bloom, Keira Knightley] Gore Verbinski [ocean, drug abuse, exotic island] [Adventure, Fantasy, Action]
2 Spectre [Daniel Craig, Christoph Waltz, Léa Seydoux] Sam Mendes [spy, based on novel, secret agent] [Action, Adventure, Crime]
3 The Dark Knight Rises [Christian Bale, Michael Caine, Gary Oldman] Christopher Nolan [dc comics, crime fighter, terrorist] [Action, Crime, Drama]
4 John Carter [Taylor Kitsch, Lynn Collins, Samantha Morton] Andrew Stanton [based on novel, mars, medallion] [Action, Adventure, Science Fiction]

모든 문자를 소문자(lower case)로 바꾸고 빈칸을 없애는 clean_data 함수를 만든다.

def clean_data(x):
    if isinstance(x, list):
        return [str.lower(i.replace(" ", "")) for i in x]
    else:
        if isinstance(x, str):
            return str.lower(x.replace(" ", ""))
        else:
            return ''

cast, keywords, director, genres 변수에 clean_data 함수를 실행시킨다.

features = ['cast', 'keywords', 'director', 'genres']
for i in features:
    df[i] = df[i].apply(clean_data)

df[['title', 'cast', 'director', 'keywords', 'genres']].head()
title cast director keywords genres
0 Avatar [samworthington, zoesaldana, sigourneyweaver] jamescameron [cultureclash, future, spacewar] [action, adventure, fantasy]
1 Pirates of the Caribbean: At World's End [johnnydepp, orlandobloom, keiraknightley] goreverbinski [ocean, drugabuse, exoticisland] [adventure, fantasy, action]
2 Spectre [danielcraig, christophwaltz, léaseydoux] sammendes [spy, basedonnovel, secretagent] [action, adventure, crime]
3 The Dark Knight Rises [christianbale, michaelcaine, garyoldman] christophernolan [dccomics, crimefighter, terrorist] [action, crime, drama]
4 John Carter [taylorkitsch, lynncollins, samanthamorton] andrewstanton [basedonnovel, mars, medallion] [action, adventure, sciencefiction]

앞에서 만든 모든 문자열을 하나로 합치는 create_soup 함수를 만들어 df 데이터프레임에 적용시켜 soup이라는 변수를 추가한다. ' '.join은 리스트를 따옴표 안의 구분자를 포함시켜 문자열로 변환해주는 함수이다.

def create_soup(x):
    return ' '.join(x['keywords']) + ' ' + ' '.join(x['cast']) + ' ' +\
x['director'] + ' ' + ' '.join(x['genres'])
df['soup'] = df.apply(create_soup, axis=1)

df['soup'].iloc[0]
'cultureclash future spacewar samworthington zoesaldana sigourneyweaver jamescameron action adventure fantasy'

sklearn에서 CountVectorizer를 불러들여 count로 지정한다. 이때 불용어(stop words) 제거 옵션을 선택한다. 그런 다음, 바로 위에서 만든 soup 변수에 대해 count를 적용하여 각 단어의 빈도 행렬을 만들고 이를 count_matrix로 지정한다.

from sklearn.feature_extraction.text import CountVectorizer

count = CountVectorizer(stop_words='english')
count_matrix = count.fit_transform(df['soup'])

sklearncosine_similarity 함수를 count_matrix에 적용하여 영화간 유사성 행렬(cosine_sim)을 만든다. 행 인덱스 및 열 이름을 영화 제목(title)으로 한다.

최종적으로 영화 “The Dark Knight Rises”에 대해 메타데이터 기반 추천 10개 영화(코사인 유사성 상위 10개 영화)를 반환시킨 결과가 아래 나와 있다.

sim_matrix = pd.DataFrame(
    cosine_similarity(count_matrix), index = df['title'], columns = df['title'])
top_10_similar_items = sim_matrix[
    'The Dark Knight Rises'].sort_values(ascending=False).iloc[1:11]
top_10_similar_items
title
Batman Begins               0.700000
The Dark Knight             0.700000
Amidst the Devil's Wings    0.547723
The Prestige                0.400000
Romeo Is Bleeding           0.400000
Black November              0.358569
Takers                      0.335410
Faster                      0.335410
Kiss of Death               0.316228
The Sweeney                 0.316228
Name: The Dark Knight Rises, dtype: float64