推薦系統的文獻汗牛充棟,你們對此應該都不陌生。之因此選這個題目一是簡單,在一週多晚上十點之後的自由時間裏,只有選簡單的題目才能完成,即使如此,依然捉襟見肘;二是但願好好研究下數據,一步步推到推薦系統的設計,而不是像之前直奔算法,固然也是時間緣由,這裏對數據的探索也是遠遠不夠的。python
本文前面探索階段所用的數據集太大,致使多個分析運行一天也出不告終果,因此後面在推薦系統的建模中,又換成了較小的 MovieLens 1M 數據集。正則表達式
import pandas as pd import numpy as np import datetime
%matplotlib inline import matplotlib.pyplot as plt
在數據探索階段,剛開始是漫無目的的,跟着感受走,對數據哪一塊感興趣就統計下,排個序,畫個圖,這樣慢慢就對數據熟悉起來,熟悉以後再來考慮如何利用用戶歷史行爲數據來作推薦。算法
讀取數據app
讀取評分數據:dom
ratings = pd.read_csv('./ml-latest/ratings.csv', header=0)
ratings.head()
userId | movieId | rating | timestamp | |
---|---|---|---|---|
0 | 1 | 169 | 2.5 | 1204927694 |
1 | 1 | 2471 | 3.0 | 1204927438 |
2 | 1 | 48516 | 5.0 | 1204927435 |
3 | 2 | 2571 | 3.5 | 1436165433 |
4 | 2 | 109487 | 4.0 | 1436165496 |
ratings['timestamp']=ratings.timestamp.map(datetime.datetime.utcfromtimestamp) # 時間格式轉換
ratings.head()
userId | movieId | rating | timestamp | |
---|---|---|---|---|
0 | 1 | 169 | 2.5 | 2008-03-07 22:08:14 |
1 | 1 | 2471 | 3.0 | 2008-03-07 22:03:58 |
2 | 1 | 48516 | 5.0 | 2008-03-07 22:03:55 |
3 | 2 | 2571 | 3.5 | 2015-07-06 06:50:33 |
4 | 2 | 109487 | 4.0 | 2015-07-06 06:51:36 |
ratings.count()
userId 22884377 movieId 22884377 rating 22884377 timestamp 22884377 dtype: int64
讀取電影信息數據:ide
movies = pd.read_csv('./ml-latest/movies.csv', header=0)
movies.head()
movieId | title | genres | |
---|---|---|---|
0 | 1 | Toy Story (1995) | Adventure|Animation|Children|Comedy|Fantasy |
1 | 2 | Jumanji (1995) | Adventure|Children|Fantasy |
2 | 3 | Grumpier Old Men (1995) | Comedy|Romance |
3 | 4 | Waiting to Exhale (1995) | Comedy|Drama|Romance |
4 | 5 | Father of the Bride Part II (1995) | Comedy |
movies = movies.set_index("movieId") movies.head()
title | genres | |
---|---|---|
movieId | ||
1 | Toy Story (1995) | Adventure|Animation|Children|Comedy|Fantasy |
2 | Jumanji (1995) | Adventure|Children|Fantasy |
3 | Grumpier Old Men (1995) | Comedy|Romance |
4 | Waiting to Exhale (1995) | Comedy|Drama|Romance |
5 | Father of the Bride Part II (1995) | Comedy |
movies.count()
title 34208 genres 34208 dtype: int64
該數據集包含了對 34208 部電影的 22884377 個評分數據。測試
流行度調查大數據
moviefreq = ratings.movieId.value_counts() # 統計每部電影的評分人數,可看出電影的流行程度,默認是降序排列 moviefreq.count()
33670
sorted_byfreq = movies.loc[moviefreq.index] # 根據頻次大小依次取電影信息 sorted_byfreq['ranking']=range(moviefreq.count()) # 加上排名 sorted_byfreq['freq']=moviefreq # 加上頻次 sorted_byfreq.iloc[0:10] # 前十大流行電影
title | genres | ranking | freq | |
---|---|---|---|---|
356 | Forrest Gump (1994) | Comedy|Drama|Romance|War | 0 | 81296 |
296 | Pulp Fiction (1994) | Comedy|Crime|Drama|Thriller | 1 | 79091 |
318 | Shawshank Redemption, The (1994) | Crime|Drama | 2 | 77887 |
593 | Silence of the Lambs, The (1991) | Crime|Horror|Thriller | 3 | 76271 |
480 | Jurassic Park (1993) | Action|Adventure|Sci-Fi|Thriller | 4 | 69545 |
260 | Star Wars: Episode IV - A New Hope (1977) | Action|Adventure|Sci-Fi | 5 | 67092 |
2571 | Matrix, The (1999) | Action|Sci-Fi|Thriller | 6 | 64830 |
110 | Braveheart (1995) | Action|Drama|War | 7 | 61267 |
1 | Toy Story (1995) | Adventure|Animation|Children|Comedy|Fantasy | 8 | 60424 |
527 | Schindler's List (1993) | Drama|War | 9 | 59857 |
sorted_byfreq[sorted_byfreq.title.str.contains('Kill Bill')] # 查看某部電影的流行度
title | genres | ranking | freq | |
---|---|---|---|---|
6874 | Kill Bill: Vol. 1 (2003) | Action|Crime|Thriller | 110 | 26225 |
7438 | Kill Bill: Vol. 2 (2004) | Action|Drama|Thriller | 157 | 22301 |
再來看下每部電影的評分次數分佈。優化
moviefreq1 = moviefreq.copy() moviefreq1.index = range(moviefreq1.count()) # 對索引從新賦值,方便畫圖 fig, ax = plt.subplots(1, 1, figsize=(12, 4)) moviefreq1.plot(ax=ax, title='Rating times');
由上可見,對於 34208 部電影來講,評分個數最多的也只有 81296 個,而一共有 247753 個用戶,更不用提大量評分次數遠小於 10000 的電影,可見,推薦的空間是很是大的。動畫
評分時間區間
統計下評分時間的頻次分佈。
ts = ratings.timestamp.copy()
ts.head()
0 2008-03-07 22:08:14 1 2008-03-07 22:03:58 2 2008-03-07 22:03:55 3 2015-07-06 06:50:33 4 2015-07-06 06:51:36 Name: timestamp, dtype: datetime64[ns]
ts2 = pd.Series(np.ones(ts.count()).astype('Int32'), index=ts.values).sort_index()
ts3 = ts2.to_period("Y").groupby(level=0).count()
fig, ax = plt.subplots(1, 1, figsize=(12, 4)) ts3.plot(ax=ax, kind='bar', title='Rating times');
評分次數隨年份的分佈。
評分排名
怎麼根據評分來對電影排名?先試試平均分吧,這個貌似比較公平。
meanrating = ratings['rating'].groupby(ratings['movieId']).mean()
meanrating = meanrating.sort_values(ascending = False)
meanrating.head()
movieId 95517 5.0 148781 5.0 141483 5.0 136872 5.0 139134 5.0 Name: rating, dtype: float64
sorted_byrate = movies.loc[meanrating.index] # 根據頻次大小依次取電影信息 sorted_byrate['ranking']=range(meanrating.count()) # 加上排名 sorted_byrate['rating']=meanrating # 加上評分 sorted_byrate.iloc[0:10] # 前十大流行電影
title | genres | ranking | rating | |
---|---|---|---|---|
movieId | ||||
95517 | Barchester Chronicles, The (1982) | Drama | 0 | 5.0 |
148781 | Under the Electric Sky (2014) | Documentary | 1 | 5.0 |
141483 | Lost Rivers (2013) | Documentary | 2 | 5.0 |
136872 | Zapatlela (1993) | (no genres listed) | 3 | 5.0 |
139134 | Soodhu Kavvum (2013) | Comedy|Thriller | 4 | 5.0 |
135727 | Aarya (2004) | Comedy|Drama|Romance | 5 | 5.0 |
103143 | Donos de Portugal (2012) | Documentary | 6 | 5.0 |
141434 | My Friend Victoria (2014) | Drama | 7 | 5.0 |
150268 | Dilwale (2015) | Action|Children|Comedy|Crime|Drama|Romance | 8 | 5.0 |
148857 | Christmas, Again (2015) | (no genres listed) | 9 | 5.0 |
上面全 5 分的電影居然一部都沒看過!
sorted_byrate[sorted_byrate.title.str.contains('Kill Bill')] # 查看某部電影的評分
title | genres | ranking | freq | |
---|---|---|---|---|
movieId | ||||
6874 | Kill Bill: Vol. 1 (2003) | Action|Crime|Thriller | 3066 | 3.889743 |
7438 | Kill Bill: Vol. 2 (2004) | Action|Drama|Thriller | 3387 | 3.856621 |
把評分人次也加上。
sorted_byrate = movies.loc[meanrating.index] # 根據頻次大小依次取電影信息 sorted_byrate['ranking']=range(meanrating.count()) # 加上排名 sorted_byrate['rating']=meanrating # 加上評分 sorted_byrate['freq']=moviefreq.loc[meanrating.index] # 加上評分個數 sorted_byrate.iloc[0:10] # 前十大流行電影
title | genres | ranking | rating | freq | |
---|---|---|---|---|---|
movieId | |||||
95517 | Barchester Chronicles, The (1982) | Drama | 0 | 5.0 | 1 |
148781 | Under the Electric Sky (2014) | Documentary | 1 | 5.0 | 1 |
141483 | Lost Rivers (2013) | Documentary | 2 | 5.0 | 1 |
136872 | Zapatlela (1993) | (no genres listed) | 3 | 5.0 | 1 |
139134 | Soodhu Kavvum (2013) | Comedy|Thriller | 4 | 5.0 | 1 |
135727 | Aarya (2004) | Comedy|Drama|Romance | 5 | 5.0 | 1 |
103143 | Donos de Portugal (2012) | Documentary | 6 | 5.0 | 1 |
141434 | My Friend Victoria (2014) | Drama | 7 | 5.0 | 1 |
150268 | Dilwale (2015) | Action|Children|Comedy|Crime|Drama|Romance | 8 | 5.0 | 2 |
148857 | Christmas, Again (2015) | (no genres listed) | 9 | 5.0 | 1 |
sorted_byrate[sorted_byrate.title.str.contains('Kill Bill')] # 查看某部電影的評分
title | genres | ranking | rating | freq | |
---|---|---|---|---|---|
movieId | |||||
6874 | Kill Bill: Vol. 1 (2003) | Action|Crime|Thriller | 3066 | 3.889743 | 26225 |
7438 | Kill Bill: Vol. 2 (2004) | Action|Drama|Thriller | 3387 | 3.856621 | 22301 |
原來這些全 5 分的電影都只有 1 個評分!這就把排名排上去了!看來平均分不靠譜,得把評分人次也考慮進去!
先把評分少於 30 個的剔出去。
sorted_byrate2 = sorted_byrate[sorted_byrate.freq>30]
sorted_byrate2.head(10) # 前十大評分最高電影
title | genres | ranking | rating | freq | |
---|---|---|---|---|---|
movieId | |||||
318 | Shawshank Redemption, The (1994) | Crime|Drama | 627 | 4.441710 | 77887 |
858 | Godfather, The (1972) | Crime|Drama | 641 | 4.353639 | 49846 |
50 | Usual Suspects, The (1995) | Crime|Mystery|Thriller | 668 | 4.318987 | 53195 |
527 | Schindler's List (1993) | Drama|War | 675 | 4.290952 | 59857 |
140737 | The Lost Room (2006) | Action|Fantasy|Mystery | 680 | 4.280822 | 73 |
1221 | Godfather: Part II, The (1974) | Crime|Drama | 681 | 4.268878 | 32247 |
2019 | Seven Samurai (Shichinin no samurai) (1954) | Action|Adventure|Drama | 682 | 4.262134 | 12753 |
904 | Rear Window (1954) | Mystery|Thriller | 815 | 4.246988 | 19422 |
1193 | One Flew Over the Cuckoo's Nest (1975) | Drama | 816 | 4.242451 | 35832 |
2959 | Fight Club (1999) | Action|Crime|Drama|Thriller | 817 | 4.233925 | 48879 |
這樣看着就正常多了!高分電影有點好電影的樣子了。之後沒電影看了,就來這排行榜上挨着找,不信你都看過!
評分均值和評分次數的相關性
前十大評分最高電影和前十大評分次數最高的電影中是有重合的,如《Shawshank Redemption》和《Schindler's List》,由此,咱們能夠驗證下評分均值和評分次數的相關性。
fig, ax = plt.subplots(1, 1, figsize=(12, 4)) ax.scatter(sorted_byrate['freq'],sorted_byrate['rating']);
由上圖可見,評分少的不見得就平均分就低,從總體趨勢來看,評分次數多的,平均分也高。可見,流行電影確實受人歡迎。從另外一個角度看,存在很多廣受大衆歡迎的電影,但也存在很多看的人很少,但評分很高質量很好的電影,太流行的電影確定你們都看過了,關鍵是如何找到那些還比較小衆的電影,這些電影可能具有大衆歡迎的元素,但因宣傳作得很差沒被大衆發現,也多是這些電影就是小衆,在小圈子裏很是受歡迎,但到更大的人羣中就不行。如何把這些電影推薦給什麼時候的人,是個性化推薦要考慮的問題。
反做弊
找出那些沒有真實評分,只給假評分的。
先統計下每一個人評分次數的分佈。
userfreq = ratings.userId.value_counts() # 統計每一個人的評分次數,默認是降序排列 userfreq.count()
247753
確實是 247753 我的的評分,一個不差,跟 README.txt 說的同樣。
userfreq.head()
185430 9281 46750 7515 204165 7057 135877 6015 58040 5801 Name: userId, dtype: int64
timesfreq = userfreq.copy() timesfreq.index = range(timesfreq.count()) # 對索引從新賦值,方便畫圖 timesfreq.head()
0 9281 1 7515 2 7057 3 6015 4 5801 Name: userId, dtype: int64
fig, ax = plt.subplots(1, 1, figsize=(15, 4)) timesfreq.plot(ax=ax); ax.set_xlabel("People ID"); ax.set_ylabel("Rating times");
由圖可見,人們的評分次數是呈冪律分佈的,只有少數人的評分次數巨多,而後迅速過渡到絕大多數人的評分次數。
timesfreq[timesfreq>2000].count()
295
評分超過 2000 次的人有 295 個,這些人太愛看電影了。至於後面評分較少的,也多是加入評分較晚,或者看過不少電影,只是沒在這評分而已,因此這裏面確定也隱藏了很多電影達人。
下面分別看看評分 2000 次以上和 2000 次如下的評分次數分佈。
timesfreq1 = userfreq[userfreq>=2000].copy() timesfreq1.index = range(timesfreq1.count()) # 對索引從新賦值,方便畫圖 fig, ax = plt.subplots(1, 1, figsize=(15, 4)) timesfreq1.plot(ax=ax); ax.set_xlabel("People ID"); ax.set_ylabel("Rating times");
timesfreq2 = userfreq[userfreq<2000].copy() timesfreq2.index = range(timesfreq2.count()) # 對索引從新賦值,方便畫圖 fig, ax = plt.subplots(1, 1, figsize=(15, 4)) timesfreq2.plot(ax=ax); ax.set_xlabel("People ID"); ax.set_ylabel("Rating times");
不得不說,冪律分佈無處不在啊。
看下評分次數少於 10 次的用戶個數。
userfreq[userfreq<10].count()
32775
居然有三萬多人。
timesfreq3 = userfreq[userfreq<10].copy() timesfreq3.index = range(timesfreq3.count()) # 對索引從新賦值,方便畫圖 fig, ax = plt.subplots(1, 1, figsize=(15, 4)) timesfreq3.plot(ax=ax); ax.set_xlabel("People ID"); ax.set_ylabel("Rating times");
userfreq[userfreq==1].count()
4251
評分 1 次的就有四千多人。
onerating = ratings[ratings.userId.isin(userfreq[userfreq==1].index.values.tolist())] # 這裏的 isin 方法但是費了好大勁找到的 print onerating.count() print onerating.head()
userId 4251 movieId 4251 rating 4251 timestamp 4251 dtype: int64 userId movieId rating timestamp 10137 108 2302 4.5 1352678182 19688 215 318 3.0 1434516586 23937 263 1029 4.5 1207138536 30266 356 3254 4.0 1325107825 32553 376 7153 5.0 1427304194
fig, ax = plt.subplots(1, 1, figsize=(15, 4)) onerating.rating.value_counts().plot(ax=ax, kind='bar', title='Ratings');
沒發現什麼異常狀況,原本想着只有一次評分的是否是都是來爲某電影刷分的,如今否認這種想法。
電影時間的分佈
movies.title.head()
movieId 1 Toy Story (1995) 2 Jumanji (1995) 3 Grumpier Old Men (1995) 4 Waiting to Exhale (1995) 5 Father of the Bride Part II (1995) Name: title, dtype: object
movieyears = movies.title.str.extract('(\((\d{4})\))', expand=True).ix[:,1] # 使用正則表達式取出上映年份 movieyears.head()
movieId 1 1995 2 1995 3 1995 4 1995 5 1995 Name: 1, dtype: object
yearfreq = movieyears.value_counts() # 統計每部電影的上映年份,可看出電影的流行程度,默認是降序排列 yearfreq.count()
129
yearfreqsort = yearfreq.sort_index() yearfreqsort.head()
1874 1 1878 1 1887 1 1888 2 1890 3 Name: 1, dtype: int64
看下這些電影的年份分佈。
第一幅圖太密集了,就分兩幅圖顯示。
fig, ax = plt.subplots(3, 1, figsize=(15, 12)) yearfreqsort.plot(ax=ax[0], kind='bar', title='freq'); yearfreqsort.iloc[0:60].plot(ax=ax[1], kind='bar', title='freq'); yearfreqsort.iloc[60:].plot(ax=ax[2], kind='bar', title='freq');
看每一年的電影個數,能夠感覺到歷史的變遷。電影個數在上個世紀 90 年代以前一直增加緩慢,到了 90 年代中期開始飛速增加,直到今天。
沒想到 1900 年以前還有幾部電影。看看什麼名字。
movies.ix[movieyears[movieyears<'1900'].index] # 1900 前的電影
title | genres | |
---|---|---|
movieId | ||
82337 | Four Heads Are Better Than One (Un homme de tê... | Fantasy |
82362 | Pyramid of Triboulet, The (La pyramide de Trib... | Fantasy |
88674 | Edison Kinetoscopic Record of a Sneeze (1894) | Documentary |
94431 | Ella Lola, a la Trilby (1898) | (no genres listed) |
94657 | Turkish Dance, Ella Lola (1898) | (no genres listed) |
94951 | Dickson Experimental Sound Film (1894) | Musical |
95541 | Blacksmith Scene (1893) | (no genres listed) |
96009 | Kiss, The (1896) | Romance |
98981 | Arrival of a Train, The (1896) | Documentary |
113048 | Tables Turned on the Gardener (1895) | Comedy |
120869 | Employees Leaving the Lumière Factory (1895) | Documentary |
125978 | Santa Claus (1898) | Sci-Fi |
129849 | Old Man Drinking a Glass of Beer (1898) | (no genres listed) |
129851 | Dickson Greeting (1891) | (no genres listed) |
140539 | Pauvre Pierrot (1892) | Animation |
140547 | The Merry Skeleton (1898) | Comedy |
140549 | Serpentine Dance: Loïe Fuller (1897) | (no genres listed) |
142851 | Arab Cortege, Geneva (1896) | Documentary |
148040 | Man Walking Around a Corner (1887) | (no genres listed) |
148042 | Accordion Player (1888) | Documentary |
148044 | Monkeyshines, No. 1 (1890) | Comedy |
148046 | Monkeyshines, No. 2 (1890) | (no genres listed) |
148048 | Sallie Gardner at a Gallop (1878) | (no genres listed) |
148050 | Traffic Crossing Leeds Bridge (1888) | Documentary |
148052 | London's Trafalgar Square (1890) | (no genres listed) |
148054 | Passage de Venus (1874) | Documentary |
148064 | Newark Athlete (1891) | Documentary |
148462 | Men Boxing (1891) | Action|Documentary |
148703 | The Wave (1891) | Documentary |
148705 | A Hand Shake (1892) | (no genres listed) |
148877 | Fencing (1892) | (no genres listed) |
都沒看過,不過確實挺厲害的,那時咱們仍是大清朝啊。
下面按年代顯示電影個數。
movieyears1 = movieyears.str[:3] + "0s" yearfreq1 = movieyears1.value_counts() # 統計每部電影的上映年份,可看出電影的流行程度,默認是降序排列 yearfreqsort1 = yearfreq1.sort_index() fig, ax = plt.subplots(1, 1, figsize=(15, 4)) yearfreqsort1.plot(ax=ax, kind='bar', title='freq');
能夠看到電影不斷增多的趨勢,之後也會愈來愈多。
電影基因的分佈
genreslist = [] # 存儲爲全部電影標註的基因 genreseries = movies.genres.str.split(pat = "|") genrecount = genreseries.count() for i in range(genrecount): genreslist.extend(genreseries.iloc[i]) # 把 Series 中的全部元素展平組成一個 list len(genreslist)
66668
上面的代碼運行得比較久,好在數據量不大,看個電視的功夫就完了,但下面再用這個代碼就很差使了。
movies.count()
title 34208 genres 34208 dtype: int64
allmoviegenres = pd.Series(genreslist) genrestats = allmoviegenres.value_counts() fig, ax = plt.subplots(1, 1, figsize=(15, 4)) genrestats.plot(ax=ax, kind='bar', title='freq');
戲劇最多,喜劇其次,接着是驚悚、浪漫、動做、犯罪、恐怖、記錄、冒險、科幻、神祕、幻想、兒童、動畫、戰爭、音樂劇、西部、黑色、IMAX。
下面咱們看下全部評分的影片的基因分佈。
#mi = movies.ix[ratings.movieId] #genreslist = [] # 存儲爲全部電影標註的基因 #genreseries = mi.genres.str.split(pat = "|") #genrecount = genreseries.count() #for i in range(genrecount): # genreslist.extend(genreseries.iloc[i]) # 把 Series 中的全部元素展平組成一個 list #allmoviegenres = pd.Series(genreslist) #genrestats = allmoviegenres.value_counts() #fig, ax = plt.subplots(1, 1, figsize=(15, 4)) #genrestats.plot(ax=ax, kind='bar', title='freq');
上面這段代碼運行了一夜,早上起來一看,內存錯誤……,爲了省事兒,代碼寫得慘不忍睹……
它實現的功能是取出全部評分涉及電影的基因,並對基因作統計,主要是爲了看看觀衆觀看的電影基因的分佈,跟上面的電影基因統計還不同。觀衆的觀看記錄表明了用戶的興趣所在,無論最後給的是高分低分,總算是由於感興趣纔看的,因此這裏對這些作個統計。
上面的代碼沒有考慮內存,下面對代碼作個優化,對 ratings 裏的電影一一提取基因來統計,稍微作改動下就沒有內存問題了,但運算速度就沒辦法了,仍是要對 22884377 個評分一個一個提取電影基因。
s1 = pd.Series(np.zeros(20,dtype=np.int32),index=['Drama','Comedy','Thriller','Romance','Action', \ 'Crime','Horror','Documentary','Adventure','Sci-Fi', \ 'Mystery','Fantasy','Children','Animation','War', \ '(no genres listed)','Musical','Western','Film-Noir','IMAX']) rcount = ratings.count()[0] for i in range(rcount): if (0 == (i%1000000)): # 至關於進度條,否則 7 個小時暗箱運行也不知道進度 print i mid = ratings.movieId.iloc[i] grs = movies.ix[mid].genres.split("|") s1.ix[grs] += 1
0 1000000 2000000 3000000 4000000 5000000 6000000 7000000 8000000 9000000 10000000 11000000 12000000 13000000 14000000 15000000 16000000 17000000 18000000 19000000 20000000 21000000 22000000
上面這塊代碼運行了七個多小時!很少說了,趕忙把結果保存下來!
s1.to_csv('genres_distribution.csv')
s1
Drama 10137200 Comedy 8437502 Thriller 6123348 Romance 4342070 Action 6547286 Crime 3803018 Horror 1685352 Documentary 279609 Adventure 5117321 Sci-Fi 3694890 Mystery 1794175 Fantasy 2449668 Children 1923874 Animation 1362681 War 1206361 (no genres listed) 2454 Musical 974697 Western 472588 Film-Noir 238480 IMAX 676420 dtype: int32
s1_sort = s1.sort_values(ascending = False) # 排序
fig, ax = plt.subplots(1, 1, figsize=(15, 4)) s1_sort.plot(ax=ax, kind='bar', title='freq');
由上可見,觀衆的評分記錄的電影基因跟電影實際存在的基因排序是一致的,能夠用電影的拍攝是爲了知足觀衆需求來解釋,也能夠說是觀衆有什麼看什麼。
除了紀錄片電影的觀看人次相比偏少,有點靠後,供過於求,固然仍是建議你們多看記錄片,獲取知識是一個春風化雨的過程。
因爲上面使用大數據集的慘痛教訓,這裏換成了較小的 MovieLens 1M Dataset 數據集。
讀取數據
ratings1m_train = pd.read_csv('./ml-1m/ratings.dat', sep="::", names=['userId','movieId','rating','timestamp'],engine='python')
ratings1m_train.head()
userId | movieId | rating | timestamp | |
---|---|---|---|---|
0 | 1 | 1193 | 5 | 978300760 |
1 | 1 | 661 | 3 | 978302109 |
2 | 1 | 914 | 3 | 978301968 |
3 | 1 | 3408 | 4 | 978300275 |
4 | 1 | 2355 | 5 | 978824291 |
ratings1m_train = ratings1m_train.drop(['timestamp','rating'], axis=1) # TopN 推薦忽略具體評分
ratings1m_train.count()
userId 1000209 movieId 1000209 dtype: int64
分離訓練集和測試集
from sklearn import cross_validation from sklearn.cross_validation import train_test_split
ratings1m_train.head()
userId | movieId | |
---|---|---|
0 | 1 | 1193 |
1 | 1 | 661 |
2 | 1 | 914 |
3 | 1 | 3408 |
4 | 1 | 2355 |
totalcount = ratings1m_train.count()[0]
all_index = np.arange(totalcount) train_index = np.random.choice(totalcount, int(0.8*totalcount), replace=False) # 從 0 到 totalcount 之間抽 80%,無放回 test_index = np.setdiff1d(all_index, train_index) # 集合的差
train_data = ratings1m_train.iloc[train_index] test_data = ratings1m_train.iloc[test_index]
計算用戶類似度
users = ratings1m_train.userId.unique() users
array([ 1, 2, 3, ..., 6038, 6039, 6040], dtype=int64)
movies = ratings1m_train.movieId.unique() movies
array([1193, 661, 914, ..., 2845, 3607, 2909], dtype=int64)
該數據集包含了 6040 個用戶對 3706 部電影的 1000209 個評分。
userSimilarity = pd.DataFrame(0, columns=users, index=users) # 用戶類似度矩陣,初始化爲 0
預測準確度是度量一個推薦系統預測用戶行爲的能力。這是個很是重要的離線評測指標,計算該指標時要有個離線的數據集,該數據集包含用戶的歷史行爲記錄,而後分紅訓練集和測試集,最後經過在訓練集上創建的用戶行爲模型用於測試集,而後計算預測行爲和在測試集的實際行爲的重合度做爲預測準確度。
通常認爲 RMSE 比 MAE 更苛刻,經過平方項加大了對預測不許的評分的懲罰。
準確率是推薦列表中有多大比例是發生了行爲的。召回率是用戶實際發生的行爲有多大比例是來自推薦。
評分預測一直是推薦系統研究的熱點,對此,亞馬遜前科學家 Greg Linden 認爲,電影推薦的目的是找出用戶最有可能感興趣的電影,而不是預測用戶看了電影后會給多少分,所以 TopN 更符合應用需求,也許有一部電影用戶看了給分很高,但其它用戶看的可能性很小,所以預測用戶是否會看一部電影,比預測評分更重要。
本次做業是研究 TopN 推薦問題,忽略數據集中的具體評分。TopN 推薦的任務是預測用戶會不會對某部電影評分,而不是預測評多少分。
預測推薦
創建物品到用戶的倒排表,對於每一個物品都保存對該物品產生過行爲的用戶列表。
movie_users = pd.Series('',index=movies) # 這裏 Series 不能直接存空的 list,因此只有先存個空字符串,而後用 split 把它轉爲 list movie_users = movie_users.str.split() for i in train_data.index.values: # 掃描訓練數據集 movie_users.ix[train_data.movieId.ix[i]].append(train_data.userId.ix[i])
import math C = dict() N = dict() for i in movie_users.index.values: for u in movie_users.ix[i]: N.setdefault(u,1) N[u] += 1 for v in movie_users.ix[i]: if u == v: continue C.setdefault(u,{}) C[u].setdefault(v,0) C[u][v] += 1 for u, related_users in C.items(): for v, cuv in related_users.items(): userSimilarity.ix[u][v]=cuv / math.sqrt(N[u]*N[v])
from operator import itemgetter def recommend(uid, n_sim_user, n_rec_movie): K = n_sim_user N = n_rec_movie rank = dict() watched_movies = train_data[train_data.userId == uid].movieId.values simusers = userSimilarity.ix[uid].sort_values(ascending=False)[0:K] for v in simusers.index.values: for m in train_data[train_data.userId == v].movieId.values: if m in watched_movies: continue rank.setdefault(m,0) rank[m] += simusers.ix[v] # 返回 N 部電影 return sorted(rank.items(), key=itemgetter(1), reverse=True)[0:N]
最後計算離線推薦算法的準確率、召回率和覆蓋率。
令系統的用戶集合爲 U, R(u) 是根據用戶在訓練集上的行爲給用戶做出的推薦列表,而 T(u) 是用戶在測試集上的行爲列表。那麼推薦結果的準確率定義爲:
\[Precision=\frac{\sum_{u\in U}|R(u)\cap T(u)|}{\sum_{u\in U}|R(u)|}\]
推薦結果的召回率定義爲:
\[Recall=\frac{\sum_{u\in U}|R(u)\cap T(u)|}{\sum_{u\in U}|T(u)|}\]
推薦系統的覆蓋率爲:
\[Recall=\frac{\sum_{u\in U}|R(u)|}{|I|}\]
代碼以下:
def evaluate(n_sim_user, n_rec_movie): N = n_rec_movie hit = 0 rec_count = 0 test_count = 0 all_rec_movies = set() popular_sum = 0 movie_count = ratings1m_train.movieId.unique().shape[0] for uid in train_data.userId.values: test_movies = test_data[test_data.userId == uid].movieId rec_movies = recommend(uid, n_sim_user, n_rec_movie) for movie, w in rec_movies: if movie in test_movies.values: hit += 1 all_rec_movies.add(movie) rec_count += N test_count += test_movies.count() precision = hit / (1.0*rec_count) recall = hit / (1.0*test_count) coverage = len(all_rec_movies) / (1.0*movie_count) return (precision, recall, coverage)
print evaluate(20, 10)
上面代碼運行了一個晚上沒有出結果,只得放棄,計算量太大。