問題描述
本文主要研究對象是北京某家法律網站,這是家電子商務類大型法律資訊網站,致力為用戶提供豐富的法律信息與專業咨詢服務,也為律師與律所提供有效的互聯網整合營銷解決方案,訪問量劇增,數據信息量也大幅增長,面對大量信息用戶無法及時從中獲得自己需要的信息,信息使用效率越來越低;低效的信息供給是無法滿足用戶需求的,容易流失客戶,基於此背景尋求用戶行為分析及服務推薦系統開發。
問題目標分析
- (1) 網站用戶行為分析,挖掘各種行為特征(針對用戶群整體、分群),如訪問量、訪問頁面類別、點擊次數、停留時間等
- (2)根據用戶訪問需求特征(習慣),更加高效、人性化的服務推薦(推薦高關聯度的、熱門的、未瀏覽過的等)
本文代碼略長,主要在數據分析即用戶行為分析花費時間較多,且書中代碼不全,本文中代碼來自整理網上資源和自編。
分析思路、步驟及代碼
數據入庫
可用文的MariaDB也可用MySQL5.6,直接把數據導入,數據庫取數
1 import pandas as pd 2 from sqlalchemy import create_engine 3 import matplotlib.pyplot as plt 4 import re 5 import numpy as np 6 import time 7 8 9 engine=create_engine('mysql+pymysql://root:****@localhost:3306/test') # ****數據庫的密碼 10 sql=pd.read_sql('all_gzdata',engine,chunksize=10000) 11 12 counts1=[i['fullURLId'].value_counts() for i in sql] # counts1此時是個列表 13 counts1 14 counts1=pd.concat(counts1).groupby(level=0).sum() # 由於sql分塊的 上面counts1也是,concat縱向向鏈接 15 counts1=counts1.reset_index() 16 counts1.columns=['index','num'] 17 counts1['type']=counts1['index'].str.extract('(\d{3})',expand=True) 18 counts1.head() 19 counts1_=counts1[['type','num']].groupby('type').sum() 20 counts1_.sort_values(by='num',ascending=False,inplace=True) 21 counts1_['percentage']=(counts1_['num']/counts1_['num'].sum())*100 22 counts1_.head() 23 # 以上是區分URLId 查看用戶瀏覽習慣,那個瀏覽較多 24 # 保存生成的表可用 pd.to_csv()/pd.to_excel(),以下不贅述 25 26 a=counts1.set_index('type') 27 b=counts1.groupby('type').sum() 28 c=pd.merge(a,b,left_index=True,right_index=True)# 均以行索引為鏈接鍵 合並 29 c['percentage']=(c['num_x']/c['num_y'])*100 30 c.head() 31 c.reset_index(inplace=True) # inplace是否改變自身,True則改變原數據 32 d=c.sort_values(by=['type','percentage'],ascending=[True,False]).reset_index() 33 d.head() # 常見到groupby中level=0也終於現身了,就是原index(不精確) 34 #del d['level_0'] # 刪去某列 35 # 以上是查看 Id中大類別下小類別比重,突出重點
各種用戶行為分析
數據探索分析
- 網頁類型分析:(網頁ID,fullURLId)咨詢相關、知識相關、其他類型(大類及子類分布情況、占比)
- 點擊次數分析:(真實IP,realIP),點擊次數分析、點擊分布特征、網頁類型排名及占比
- 網頁排名(點擊):點擊網址排名、翻頁網頁統計
1 sql=pd.read_sql('all_gzdata',engine,chunksize=10000) 2 def count107(i): 3 j=i[['fullURL']][i['fullURLId'].str.contains('107')].copy() 4 j['type']=None 5 j['type'][j['fullURL'].str.contains('info/.+?/')]='知識首頁' 6 j['type'][j['fullURL'].str.contains('info/.+?/.+?')]='知識列表頁' 7 j['type'][j['fullURL'].str.contains('/\d+?_*\d+?\.html')]='知識內容頁' 8 return j['type'].value_counts() 9 counts2=[count107(i) for i in sql] 10 counts2=pd.concat(counts2).groupby(level=0).sum() 11 counts2.head() 12 counts2_=pd.DataFrame(counts2).reset_index() 13 counts2_['percentage']=(counts2_['type']/counts2_['type'].sum())*100 14 counts2_.columns=['107類型','num','百分比'] 15 # 以上是 統計107開頭(知識類,源於問題分析中網址分類:咨詢,知識,其他) 16 17 def countquestion(i): 18 j=i[['fullURLId']][i['fullURL'].str.contains('\?')].copy() 19 return j 20 sql=pd.read_sql('all_gzdata',engine,chunksize=10000) 21 counts3=[countquestion(i)['fullURLId'].value_counts() for i in sql] 22 counts3=pd.concat(counts3).groupby(level=0).sum() 23 counts3 24 counts3_=pd.DataFrame(counts3) 25 counts3_['percentage']=(counts3_['fullURLId']/counts3_['fullURLId'].sum())*100 26 counts3_.reset_index(inplace=True) 27 counts3_.columns=['fullURLId','num','percentage'] 28 counts3_.sort_values(by='percentage',ascending=False,inplace=True) 29 counts3_.reset_index(inplace=True) 30 c3=(counts3_['num'].sum()/counts1['num'].sum())*100 31 c3 32 # 以上是統計?網頁各類占比情況,探究?網頁特征及在總樣本占比 33 34 def page199(i): 35 j=i[['pageTitle']][i['fullURLId'].str.contains('199') & i['fullURL'].str.contains('\?')] 36 j['title']=1 37 j['title'][j['pageTitle'].str.contains('法律快車-律師助手')]='法律快車-律師助手' 38 j['title'][j['pageTitle'].str.contains('免費發布法律咨詢')]='免費發布法律咨詢' 39 j['title'][j['pageTitle'].str.contains('咨詢發布成功')]='咨詢發布成功' 40 j['title'][j['pageTitle'].str.contains('法律快搜')]='快搜' 41 j['title'][j['title']==1]='其他類型' 42 return j 43 sql=pd.read_sql('all_gzdata',engine,chunksize=10000) 44 counts4=[page199(i) for i in sql] 45 counts4_=pd.concat(counts4) 46 ct4_result=counts4_['title'].value_counts() 47 ct4_result=pd.DataFrame(ct4_result) 48 ct4_result.reset_index(inplace=True) 49 ct4_result['percentage']=(ct4_result['title']/ct4_result['title'].sum())*100 50 ct4_result.columns=['title','num','percentage'] 51 ct4_result.sort_values(by='num',ascending=False,inplace=True) 52 ct4_result 53 # 以上是統計199,其他類網址細分類別特征,占比 54 55 def wandering(i): 56 j=i[['fullURLId','fullURL','pageTitle']][(i['fullURL'].str.contains('.html'))==False] 57 return j 58 sql=pd.read_sql('all_gzdata',engine,chunksize=10000) 59 counts5=[wandering(i) for i in sql] 60 counts5_=pd.concat(counts5) 61 62 ct5Id=counts5_['fullURLId'].value_counts() 63 ct5Id 64 ct5Id_=pd.DataFrame(ct5Id) 65 ct5Id_.reset_index(inplace=True) 66 ct5Id_['index']=ct5Id_['index'].str.extract('(\d{3})',expand=True) 67 ct5Id_=pd.DataFrame(ct5Id_.groupby('index').sum()) 68 ct5Id_['percentage']=(ct5Id_['fullURLId']/ct5Id_['fullURLId'].sum())*100 69 ct5Id_.reset_index(inplace=True) 70 ct5Id_.columns=['fullURLId','num','percentage'] 71 ct5Id_.sort_values(by='num',ascending=False,inplace=True) 72 ct5Id_.reset_index(inplace=True) 73 #del ct5Id_['index'] # 可直接在reset_index(drop=True) 74 75 # 以上是分析 閑逛網站的用戶瀏覽特征,那些吸引了這些閑逛用戶 76 77 sql=pd.read_sql('all_gzdata',engine,chunksize=10000) 78 counts6=[i['realIP'].value_counts() for i in sql] 79 counts6_=pd.concat(counts6).groupby(level=0).sum() 80 counts6_=pd.DataFrame(counts6_) 81 counts6_[1]=1 82 ct6=counts6_.groupby('realIP').sum() 83 ct6.columns=['用戶數'] # 點擊次數情況,來表征用戶,即點擊n次的用戶數為m 84 ct6.index.name='點擊次數' # realIP 出現次數 85 ct6['用戶百分比']=ct6['用戶數']/ct6['用戶數'].sum()*100 86 ct6['記錄百分比']= ct6['用戶數']*ct6.index/counts6_['realIP'].sum()*100 # 點擊n次記錄數占總記錄數的比值 87 ct6.sort_index(inplace=True) 88 ct6_=ct6.iloc[:7,:].T 89 ct6['用戶數'].sum() #總點擊次數 90 ct6_.insert(0,'總計',[ct6['用戶數'].sum(),100,100]) 91 ct6_['7次以上']=ct6_.iloc[:,0]-ct6_.iloc[:,1:].sum(axis=1) 92 ct6_ 93 94 beyond7=ct6.index[7:] 95 bins=[7,100,1000,50000] #用於划分區間 96 beyond7_cut=pd.cut(beyond7,bins,right=True,labels=['8~100','101~1000','1000以上'])#right=True 即(7,100],(101,1000] 97 beyond7_cut_=pd.DataFrame(beyond7_cut.value_counts()) 98 beyond7_cut_.index.name='點擊次數' 99 beyond7_cut_.columns=['用戶數'] 100 beyond7_cut_.iloc[0,0]=ct6.loc[8:100,:]['用戶數'].sum() #依次為點擊在(7,100]用戶數,iloc索引號比第幾個少一 101 beyond7_cut_.iloc[1,0]=ct6.loc[101:1000,:]['用戶數'].sum() #注意,這里使用iloc會有問題,因這里行索引並非0開始的連續整數,而是名稱索引,非自然整數索引 102 beyond7_cut_.iloc[2,0]=ct6.loc[1001:,:]['用戶數'].sum() 103 beyond7_cut_.sort_values(by='用戶數',ascending=False,inplace=True) 104 beyond7_cut_.reset_index(drop=True,inplace=True) 105 beyond7_cut_ # 點擊8次以上情況統計分析,點擊分布情況 106 107 for_once=counts6_[counts6_['realIP']==1] # 分析點擊一次用戶行為特征 108 del for_once[1] # 去除多余列 109 for_once.columns=['點擊次數'] 110 for_once.index.name='realIP' # IP 找到,以下開始以此為基准 鏈接數據,merge 111 112 sql=pd.read_sql('all_gzdata',engine,chunksize=10000) 113 for_once_=[i[['fullURLId','fullURL','realIP']] for i in sql] 114 for_once_=pd.concat(for_once_) 115 for_once_merge=pd.merge(for_once,for_once_,right_on='realIP',left_index=True,how='left') 116 for_once_merge #瀏覽一次用戶行為信息,瀏覽的網頁,可分析網頁類型 117 for_once_url=pd.DataFrame(for_once_merge['fullURL'].value_counts()) 118 for_once_url.reset_index(inplace=True) 119 for_once_url.columns=['fullURL','num'] 120 for_once_url['percentage']=for_once_url['num']/for_once_url['num'].sum()*100 121 for_once_url #瀏覽一次用戶行為信息,瀏覽的網頁,可分析網頁類型:稅法、婚姻、問題 122 # 以上是分析用戶點擊情況,點擊頻率特征,點一次就走 跳失率。。 123 124 def url_click(i): 125 j=i[['fullURL','fullURLId','realIP']][i['fullURL'].str.contains('\.html')] # \帶不帶都一樣 126 return j 127 128 sql=pd.read_sql('select * from all_gzdata',engine,chunksize=10000) 129 counts8=[url_click(i) for i in sql] 130 counts8_=pd.concat(counts8) 131 ct8=counts8_['fullURL'].value_counts() 132 ct8=pd.DataFrame(ct8) 133 ct8.columns=['點擊次數'] 134 ct8.index.name='網址' 135 ct8.sort_values(by='點擊次數',ascending=False).iloc[:20,:] #網址的點擊率排行,用戶關注度 136 137 ct8_=ct8.reset_index() #500和501一樣結果,思路不一樣,推第一個500 138 click_beyond_500=ct8_[(ct8_['網址'].str.contains('/\d+?_*\d+?\.html')) & (ct8_['點擊次數']>50)] 139 click_beyond_501=ct8_[ct8_['網址'].str.contains('/\d+?_*\d+?\.html')][ct8_['點擊次數']>50] # 會報警 140 141 # 以上是網頁點擊情況分析,點擊最多是那些,關注度 142 143 #for i in sql: #逐塊變換並去重 144 # d = i.copy() 145 # d['fullURL'] = d['fullURL'].str.replace('_\d{0,2}.html', '.html') #將下划線后面部分去掉,規范為標准網址 146 # d = d.drop_duplicates() #刪除重復記錄 147 # d.to_sql('changed_gzdata', engine, index = False, if_exists = 'append') #保存 148 149 def scanning_url(i): 150 j=i.copy() 151 j['fullURL']=j['fullURL'].str.replace('_\d{0,2}.html','.html') 152 return j 153 sql=pd.read_sql('select * from all_gzdata',engine,chunksize=10000) 154 counts9=[scanning_url(i) for i in sql] 155 counts9_=pd.concat(counts9) 156 ct9=counts9_['fullURL'].value_counts() # 每個網頁被點擊總次數,多頁合為一頁 157 ct9_=counts9_[['realIP','fullURL']].groupby('fullURL').count() # 每個IP對所點擊網頁的點擊次數,多頁合為一頁 158 ct9__=ct9_['realIP'].value_counts() 159 ct9__20=ct9__[:20] 160 ct9__20.plot() 161 162 #另一種分析視角,翻頁行為統計分析 163 pattern=re.compile('http://(.*\d+?)_\w+_\w+\.html$|http://(.*\d+?)_\w+\.html$|http://(.*\w+?).html$',re.S) # re.S 字符串跨行匹配;# 獲取網址中以http://與.html中間的主體部分,即去掉翻頁的內容,即去掉尾部"_d" 164 # 三個分別對應 主站點, 165 c9=click_beyond_500.set_index('網址').sort_index().copy() #sort_index()保證了 同一主站翻頁網頁是按從第一頁開始排列的,即首頁、下一頁,為下方計算下一頁點擊占上一頁比率鋪墊 166 c9['websitemain']=np.nan # 統計主站點,即記錄主站點,翻頁的首頁點擊次數 167 for i in range(len(c9)): 168 items=re.findall(pattern,c9.index[i]) 169 if len(items)==0: 170 temp=np.nan 171 else: 172 for j in items[0]: 173 if j!='': 174 temp=j 175 c9.iloc[i,1]=temp 176 c9.head() 177 c9_0=c9['websitemain'].value_counts() 178 c9_0=pd.DataFrame(c9_0) 179 c9_1=c9_0[c9_0['websitemain']>=2].copy() # 濾掉不存在翻頁的網頁 180 c9_1.columns=['Times'] # 用於識別是一類網頁,主網址出現次數 181 c9_1.index.name='websitemain' 182 c9_1['num']=np.arange(1,len(c9_1)+1) 183 c9_2=pd.merge(c9,c9_1,left_on='websitemain',right_index=True,how='right') # 鏈接左列與右索引 且左列與右索引的保留二者的行,且已右邊為基礎(即右邊全保留,在此基礎上添加左邊的二者有共通行的) 184 # 當列與列做鏈接 索引會被忽略舍去,鏈接列與索引或索引與索引時索引會保留 185 c9_2.sort_index(inplace=True) 186 187 c9_2['per']=np.nan 188 def getper(x): 189 print(x) 190 for i in range(len(x)-1): 191 x.iloc[i+1,-1]=x.iloc[i+1,0]/x.iloc[i,0] # 翻頁與上一頁點擊率比值,保存在最后一列處;同類網頁下一頁與上一頁點擊率比 192 return x 193 result=pd.DataFrame() 194 for i in range(1,c9_2['num'].max()+1): # 多少種翻頁類,c9_2['num'].max()+1,從1開始 195 k=getper(c9_2[c9_2['num']==i]) # 同類翻頁網頁下一頁與上一頁點擊數比值 196 result=pd.concat([result,k]) 197 c9_2['Times'].value_counts() 198 199 flipPageResult=result[result['Times']<10] 200 flipPageResult.head() 201 # 以上是用戶翻頁行為分析,網站停留情況,文章分頁優化 202 203 def countmidurl(i): # 刪除數據規則之中間網頁(帶midques_) 204 j=i[['fullURL','fullURLId','realIP']].copy() 205 j['type']='非中間類型網頁' 206 j['type'][j['fullURL'].str.contains('midques_')]='中間類型網頁' 207 return j['type'].value_counts() 208 209 sql=pd.read_sql('select * from all_gzdata',engine,chunksize=10000) 210 counts10=[countmidurl(i) for i in sql] 211 counts10_=pd.concat(counts10).groupby(level=0).sum() 212 counts10_ 213 214 def count_no_html(i): # 刪除數據規則之目錄頁(帶html) 215 j=i[['fullURL','pageTitle','fullURLId']].copy() 216 j['type']='有html' 217 j['type'][j['fullURL'].str.contains('\.html')==False]="無html" 218 return j['type'].value_counts() 219 sql=pd.read_sql('select * from all_gzdata',engine,chunksize=10000) 220 counts11=[count_no_html(i) for i in sql] 221 counts11_=pd.concat(counts11).groupby(level=0).sum() 222 counts11_ 223 224 def count_law_consult(i): # 數據刪除規則之咨詢、律師助手登錄 225 j=i[['fullURL','pageTitle','fullURLId']].copy() 226 j['type']='其他' 227 j['pageTitle'].fillna('空',inplace=True) 228 j['type'][j['pageTitle'].str.contains('快車-律師助手')]='快車-律師助手' 229 j['type'][j['pageTitle'].str.contains('咨詢發布成功')]='咨詢發布成功' 230 j['type'][(j['pageTitle'].str.contains('免費發布法律咨詢')) | (j['pageTitle'].str.contains('法律快搜'))]='快搜免費發布法律咨詢' 231 return j['type'].value_counts() 232 sql=pd.read_sql('select * from all_gzdata',engine,chunksize=10000) 233 counts12=[count_law_consult(i) for i in sql] 234 counts12_=pd.concat(counts12).groupby(level=0).sum() 235 counts12_ 236 237 def count_law_ques(i): # 數據刪除規則之去掉與分析網站無關的網頁 238 j=i[['fullURL']].copy() 239 j['fullURL']=j['fullURL'].str.replace('\?.*','') 240 j['type']='主網址不包含關鍵字' 241 j['type'][j['fullURL'].str.contains('lawtime')]='主網址包含關鍵字' 242 return j 243 sql=pd.read_sql('select * from all_gzdata',engine,chunksize=10000) 244 counts13=[count_law_ques(i) for i in sql] 245 counts13_=pd.concat(counts13)['type'].value_counts() 246 counts13_ 247 248 def count_duplicate(i): # 數據刪除規則之去掉同一用戶同一時間同一網頁的重復數據(可能是記錄錯誤) 249 j=i[['fullURL','realIP','timestamp_format']].copy() 250 return j 251 sql=pd.read_sql('select * from all_gzdata',engine,chunksize=10000) 252 counts14=[count_duplicate(i) for i in sql] 253 counts14_=pd.concat(counts14) 254 print(len(counts14_[counts14_.duplicated()==True]),len(counts14_.drop_duplicates())) 255 256 ct14=counts14_.drop_duplicates() 257 258 # 以上是數據清洗的操作 259 # 開始對數據庫數據進行清洗操作,構建模型數據 260 sql=pd.read_sql('select * from all_gzdata',engine,chunksize=10000) 261 for i in sql: #只要.html結尾的 & 截取問號左邊的值 & 只要包含主網址(lawtime)的&網址中間沒有midques_的 262 d = i[['realIP', 'fullURL','pageTitle','userID','timestamp_format']].copy() # 只要網址列 263 d['fullURL'] = d['fullURL'].str.replace('\?.*','') # 網址中問號后面的部分 264 d = d[(d['fullURL'].str.contains('\.html')) & (d['fullURL'].str.contains('lawtime')) & (d['fullURL'].str.contains('midques_') == False)] # 只要含有.html的網址 265 # 保存到數據庫中 266 d.to_sql('cleaned_one', engine, index = False, if_exists = 'append') 267 268 sql=pd.read_sql('select * from cleaned_one',engine,chunksize=10000) 269 for i in sql: #刪除 快車-律師助手 & 免費發布法律咨詢 & 咨詢發布成功 & 法律快搜) 270 d = i[['realIP','fullURL','pageTitle','userID','timestamp_format']]# 只要網址列 271 d['pageTitle'].fillna(u'空',inplace=True) 272 d = d[(d['pageTitle'].str.contains(u'快車-律師助手') == False) & (d['pageTitle'].str.contains(u'咨詢發布成功') == False) & 273 (d['pageTitle'].str.contains(u'免費發布法律咨詢') == False) & (d['pageTitle'].str.contains(u'法律快搜') == False) 274 ].copy() 275 # 保存到數據庫中 276 d.to_sql('cleaned_two', engine, index = False, if_exists = 'append') 277 # 278 sql=pd.read_sql('select * from cleaned_two',engine,chunksize=10000) 279 def dropduplicate(i): 280 j = i[['realIP','fullURL','pageTitle','userID','timestamp_format']].copy() 281 return j 282 283 count15 = [dropduplicate(i) for i in sql] 284 count15 = pd.concat(count15) 285 print(len(count15)) # 2012895 286 count16 = count15.drop_duplicates(['fullURL','userID','timestamp_format']) # 一定要進行二次刪除重復,因為不同的塊中會有重復值 287 print(len(count16)) # 647300 288 count16.to_sql('cleaned_three', engine) 289 290 # 以上是數據清洗及保存到庫的操作,清洗完畢 291 # 查看各表的長度 292 sql = pd.read_sql('all_gzdata', engine, chunksize = 10000) 293 lens = 0 294 for i in sql: 295 temp = len(i) 296 lens = temp + lens 297 print(lens) # 837450 298 # 查看cleaned_one表中的記錄數 299 sql1 = pd.read_sql('cleaned_one', engine, chunksize = 10000) 300 lens1 = 0 301 for i in sql1: 302 temp = len(i) 303 lens1 = temp + lens1 304 print(lens1) # 1341930 305 # 查看cleaned_two表中的記錄數 306 sql2 = pd.read_sql('cleaned_two', engine, chunksize = 10000) 307 lens2 = 0 308 for i in sql2: 309 temp = len(i) 310 lens2 = temp + lens2 311 print(lens2)#2012895 312 # 查看cleaned_three表中的記錄數 313 sql3 = pd.read_sql('cleaned_three', engine, chunksize = 10000) 314 lens3 = 0 315 for i in sql3: 316 temp = len(i) 317 lens3 = temp + lens3 318 print(lens3) #1294600 319 320 # 以上是查看,處理的保存的各表長度,清洗掉的數據是不是太大 321 322 sql = pd.read_sql('cleaned_three', engine, chunksize = 10000) 323 l0 = 0 324 l1 = 0 325 l2 = 0 326 for i in sql: 327 d = i.copy() 328 # 獲取所有記錄的個數 329 l0 += len(d) 330 # 獲取類似於http://www.lawtime.cn***/2007020619634_2.html格式的記錄的個數 331 # 匹配1 易知,匹配1一定包含匹配2 332 x1 = d[d['fullURL'].str.contains('_\d{0,2}.html')] 333 l1 += len(x1) 334 # 匹配2 335 # 獲取類似於http://www.lawtime.cn***/29_1_p3.html格式的記錄的個數 336 x2 = d[d['fullURL'].str.contains('_\d{0,2}_\w{0,2}.html')] 337 l2 += len(x2) 338 # x1.to_sql('l1', engine, index=False, if_exists = 'append') # 保存 339 # x2.to_sql('l2', engine, index=False, if_exists = 'append') # 保存 340 341 print(l0,l1,l2) #1941900 166365 27780 342 343 344 # 去掉翻頁的網址 345 sql = pd.read_sql('cleaned_three', engine, chunksize = 10000) 346 l4 = 0 347 for i in sql: #初篩 348 d = i.copy() 349 # 注意!!!替換1和替換2的順序不能顛倒,否則刪除不完整 350 # 替換1 將類似於http://www.lawtime.cn***/29_1_p3.html下划線后面部分"_1_p3"去掉,規范為標准網址 351 d['fullURL'] = d['fullURL'].str.replace('_\d{0,2}_\w{0,2}.html','.html')#這部分網址有 9260 個 352 # 替換2 將類似於http://www.lawtime.cn***/2007020619634_2.html下划線后面部分"_2"去掉,規范為標准網址 353 d['fullURL'] = d['fullURL'].str.replace('_\d{0,2}.html','.html') #這部分網址有 55455-9260 = 46195 個 354 d = d.drop_duplicates(['fullURL','userID']) # 刪除重復記錄(刪除有相同網址和相同用戶ID的)【不完整】因為不同的數據塊中依然有重復數據 355 l4 += len(d) 356 d.to_sql('changed_1', engine, index=False, if_exists = 'append') # 保存 357 print(l4 )# 1643197 358 sql = pd.read_sql('changed_1', engine, chunksize = 10000) 359 def dropduplicate(i): #二次篩選 360 j = i[['realIP','fullURL','pageTitle','userID','timestamp_format']].copy() 361 return j 362 counts1 = [dropduplicate(i) for i in sql] 363 counts1 = pd.concat(counts1) 364 print(len(counts1))# 1095216 365 a = counts1.drop_duplicates(['fullURL','userID']) 366 print(len(a))# 528166 367 a.to_sql('changed_2', engine) # 保存 368 # 驗證是否清洗干凈,即changed_2已不存翻頁網址 369 sql = pd.read_sql('changed_2', engine, chunksize = 10000) 370 l0 = 0 371 l1 = 0 372 l2 = 0 373 for i in sql: 374 d = i.copy() 375 # 獲取所有記錄的個數 376 l0 += len(d) 377 # 獲取類似於http://www.lawtime.cn***/2007020619634_2.html格式的記錄的個數 378 # 匹配1 易知,匹配1一定包含匹配2 379 x1 = d[d['fullURL'].str.contains('_\d{0,2}.html')] 380 l1 += len(x1) 381 # 匹配2 382 # 獲取類似於http://www.lawtime.cn***/29_1_p3.html格式的記錄的個數 383 x2 = d[d['fullURL'].str.contains('_\d{0,2}_\w{0,2}.html')] 384 l2 += len(x2) 385 print(l0,l1,l2)# 528166 0 0表示已經刪除成功 386 387 # 手動添加咨詢類和知識類網址的類別,type={'咨詢類','知識類'} 388 sql = pd.read_sql('changed_2', engine, chunksize = 10000) 389 def countzhishi(i): 390 j = i[['fullURL']].copy() 391 j['type'] = 'else' 392 j['type'][j['fullURL'].str.contains('(info)|(faguizt)')] = 'zhishi' 393 j['type'][j['fullURL'].str.contains('(ask)|(askzt)')] = 'zixun' 394 return j 395 counts17 = [countzhishi(i) for i in sql] 396 counts17 = pd.concat(counts17) 397 counts17['type'].value_counts() 398 a = counts17['type'].value_counts() 399 b = pd.DataFrame(a) 400 b.columns = ['num'] 401 b.index.name = 'type' 402 b['per'] = b['num']/b['num'].sum()*100 403 b 404 # 接上;咨詢類較單一,知識類有豐富的二級、三級分類,以下作分析 405 c = counts17[counts17['type']=='zhishi'] 406 d = c[c['fullURL'].str.contains('info')] 407 print(len(d)) # 102140 408 d['iszsk'] = 'else' # 結果顯示是空 409 d['iszsk'][d['fullURL'].str.contains('info')] = 'infoelsezsk' # 102032 410 d['iszsk'][d['fullURL'].str.contains('zhishiku')] = 'zsk' # 108 411 d['iszsk'].value_counts() 412 # 由結果可知,除了‘info'和’zhishifku'沒有其他類型,且 【info類型(不包含zhishiku):infoelsezsk】和【包含zhishiku:zsk】類型無相交的部分。 413 # 因此分析知識類別下的二級類型時,需要分兩部分考慮,求每部分的類別,再求並集,即為所有二級類型 414 415 # 對於http://www.lawtime.cn/info/jiaotong/jtsgcl/2011070996791.html類型的網址進行這樣匹配,獲取二級類別名稱"jiaotong" 416 pattern = re.compile('/info/(.*?)/',re.S) 417 e = d[d['iszsk'] == 'infoelsezsk'] 418 for i in range(len(e)): #用上面已經處理的'iszsk'列分成兩種類別的網址,分別使用正則表達式進行匹配,較慢 419 e.iloc[i,2] = re.findall(pattern, e.iloc[i,0])[0] 420 print(e.head()) 421 # 對於http://www.lawtime.cn/zhishiku/laodong/info/***.html類型的網址進行這樣匹配,獲取二級類別名稱"laodong" 422 # 由於還有一類是http://www.lawtime.cn/zhishiku/laodong/***.html,所以使用'zhishiku/(.*?)/'進行匹配 423 pattern1 = re.compile('zhishiku/(.*?)/',re.S) 424 f = d[d['iszsk'] == 'zsk'] 425 for i in range(len(f)): 426 # print i 427 f.iloc[i,2] = re.findall(pattern1, f.iloc[i,0])[0] 428 print(f.head()) 429 430 e.columns = ['fullURL', 'type1', 'type2'] 431 print(e.head()) 432 f.columns = ['fullURL', 'type1', 'type2'] 433 print(f.head()) 434 # 將兩類處理過二級類別的記錄合並,求二級類別的交集 435 g = pd.concat([e,f]) 436 h = g['type2'].value_counts() 437 # 求兩類網址中的二級類別數,由結果可知,兩類網址的二級類別的集合的並集滿足所需條件 438 len(e['type2'].value_counts()) # 66 439 len(f['type2'].value_counts()) # 31 440 len(g['type2'].value_counts()) # 69 441 print(h.head()) 442 print(h.index) # 列出知識類別下的所有的二級類別 443 444 for i in range(len(h.index)): # 將二級類別存入到數據庫中 445 x=g[g['type2']==h.index[i]] 446 x.to_sql('h.index', engine, if_exists='append') 447 448 q = e.copy() 449 q['type3'] = np.nan 450 resultype3 = pd.DataFrame([],columns=q.columns) 451 for i in range(len(h.index)): # 正則匹配出三級類別 452 pattern2 = re.compile('/info/'+h.index[i]+'/(.*?)/',re.S) 453 current = q[q['type2'] == h.index[i]] 454 print(current.head()) 455 for j in range(len(current)): 456 findresult = re.findall(pattern2, current.iloc[j,0]) 457 if findresult == []: # 若匹配結果是空,則將空值進行賦值給三級類別 458 current.iloc[j,3] = np.nan 459 else: 460 current.iloc[j,3] = findresult[0] 461 resultype3 = pd.concat([resultype3,current])# 將處理后的數據拼接,即網址做索引,三列為一級、二級、三級類別 462 resultype3.set_index('fullURL',inplace=True) 463 resultype3.head(10) 464 # 統計婚姻類下面的三級類別的數目 465 j = resultype3[resultype3['type2'] == 'hunyin']['type3'].value_counts() 466 print(len(j)) # 145 467 j.head() 468 469 Type3nums = resultype3.pivot_table(index = ['type2','type3'], aggfunc = 'count') #類別3排序 470 # 方式2: Type3nums = resultype3.groupby([resultype3['type2'],resultype3['type3']]).count() 471 r = Type3nums.reset_index().sort_values(by=['type2','type1'],ascending=[True,False]) 472 r.set_index(['type2','type3'],inplace = True) 473 474 r.to_excel('2_2_3Type3nums.xlsx') 475 r 476 477 # 讀取數據庫數據 ,屬性規約,提取模型需要的數據(屬性);此處是只選擇用戶和用戶訪問的網頁 478 sql = pd.read_sql('changed_2', engine, chunksize = 10000) 479 l1 = 0 480 l2 = 0 481 for i in sql: 482 zixun = i[['userID','fullURL']][i['fullURL'].str.contains('(ask)|(askzt)')].copy() 483 l1 = len(zixun) + l1 484 hunyin = i[['userID','fullURL']][i['fullURL'].str.contains('hunyin')].copy() 485 l2 = len(hunyin) + l2 486 zixun.to_sql('zixunformodel', engine, index=False,if_exists = 'append') 487 hunyin.to_sql('hunyinformodel', engine, index=False,if_exists = 'append') 488 print(l1,l2) # 393185 16982 489 # 方法二: 490 m = counts17[counts17['type'] == 'zixun'] 491 n = counts17[counts17['fullURL'].str.contains('hunyin')] 492 p = m[m['fullURL'].str.contains('hunyin')] 493 p # 結果為空,可知,包含zixun的頁面中不包含hunyin,兩者沒有交集 494 #savetosql(m,'zixun') 495 #savetosql(n,'hunyin') 496 m.to_sql('zixun',engine) 497 n.to_sql('hunyin',engine)
服務推薦,協同過濾算法
協同過濾是利用集體智慧的一個典型方法。要理解什么是協同過濾 (Collaborative Filtering, 簡稱 CF),首先想一個簡單的問題,如果你現在想看個電影,但你不知道具體看哪部,你會怎么做?大部分的人會問問周圍的朋友,看看最近有什么好看的電影推薦,而我們一般更傾向於從口味比較類似的朋友那里得到推薦。這就是協同過濾的核心思想,協同思想就是基於類似個體喜好推導目標喜好。
協同過濾一般是在海量的用戶中發掘出一小部分和你品位比較類似的,在協同過濾中,這些用戶成為鄰居,然后根據他們喜歡的其他東西組織成一個排序的目錄作為推薦給你。當然其中有一個核心的問題:
- 如何確定一個用戶是不是和你有相似的品位?
- 如何將鄰居們的喜好組織成一個排序的目錄?
協同過濾相對於集體智慧而言,它從一定程度上保留了個體的特征,就是你的品位偏好,所以它更多可以作為個性化推薦的算法思想。可以想象,這種推薦策略在 Web 2.0 的長尾中是很重要的,將大眾流行的東西推薦給長尾中的人怎么可能得到好的效果,這也回到推薦系統的一個核心問題:了解你的用戶,然后才能給出更好的推薦 。協同過濾推薦算法主要的功能是預測和推薦。算法通過對用戶歷史行為數據的挖掘發現用戶的偏好,基於不同的偏好對用戶進行群組划分並推薦品味相似的商品,算法分為兩類,分別是基於用戶的協同過濾算法(user-based collaboratIve filtering),和基於物品的協同過濾算法(item-based collaborative filtering)。簡單的說就是:人以類聚,物以群分。
- 基於用戶的協同過濾:將物品A推薦給哪個用戶?(假設答案是用戶B);適用於用戶少、物品多的場景
- 基於物品的協同過濾:將哪個物品推薦給用戶B?(按前假設,物品A);適用於用戶多、物品少的場景(可減少計算量)
本文分析目標:長尾網頁豐富、個性化需求強烈。網頁數是小於用戶量的,適用於基於物品的協同過濾推薦系統對用戶個性化推薦。步驟如下:
- 計算物品之間相似度
- 根據物品的相似度和用戶的歷史行為給用戶生成推薦列表
相似度計算方式
夾角余弦、傑卡德相似系數、相關系數(Pearson Correlation)、歐氏距離
夾角余弦
相似系數
傑卡德(Jaccard)相似系數(針對0-1矩陣),傑卡德距離=1-傑卡德系數
,同時喜歡ab產品的用戶數與喜歡a或b用戶數比值
改進相似度,考慮新老用戶活躍度不同,活躍用戶(老用戶)對物品相似度貢獻會小於不活躍的用戶(新用戶)
,N為同時喜好ab產品的用戶,A(N)為該用戶的活躍度(如點擊量)
用戶-物品評分矩陣
P=R*SIM;P用戶對物品感興趣程度,R用戶對物品興趣(二元選擇,0-1陣),SIM所有物品之間相似度。
模型評價
離線測試評測指標
- 預測准確度:均方根誤差(RMSE)、平均絕對誤差(MAE)
- 分類准確度:准確率(P,precesion)=TP/(TP+FP)、召回率(R,recall)=TP/(TP+FN)、F1=2*PR/(P+R)
對本題來講,P表示用戶對一個被推薦產品感興趣的可能性,R表示一個用戶喜歡的產品被推薦的概率,F1指標綜合考慮了P、R。
推薦算法
數據集清洗完畢輸出模型所需格式包括:用戶ID,網址。該部分引用CSDN:江流靜一
1 ## 推薦,基於物品的協同過濾推薦、隨機推薦、按流行度推薦 2 3 data=pd.read_sql('hunyinformodel',engine) 4 data.head() 5 def jaccard(a,b): # 傑卡德相似系數,對0-1矩陣,故需要先將數據轉成0-1矩陣 6 return 1.0*(a*b).sum()/(a+b-a*b).sum() 7 class recommender(): 8 sim=None 9 def similarity(self,x,distance): 10 y=np.ones((len(x),len(x))) 11 for i in range(len(x)): 12 for j in range(len(x)): 13 y[i,j]=distance(x[i],x[j]) 14 return y 15 def fit(self,x,distance=jaccard): 16 self.sim=self.similarity(x,distance) 17 return self.sim 18 def recommend(self,a): 19 return np.dot(self.sim,a)*(1-a) 20 len(data['fullURL'].value_counts()) 21 len(data['realIP'].value_counts()) 22 # 網址數遠小於用戶數,即商品數小於使用的客戶數,采用物品的協同過濾推薦 23 24 start0=time.clock() 25 data.sort_values(by=['realIP','fullURL'],ascending=[True,True],inplace=True) 26 realIP=data['realIP'].value_counts().index 27 realIP=np.sort(realIP) 28 fullURL=data['fullURL'].value_counts().index 29 fullURl=np.sort(fullURL) 30 d=pd.DataFrame([],index=realIP,columns=fullURL) 31 for i in range(len(data)): 32 a=data.iloc[i,0] 33 b=data.iloc[i,1] 34 d.loc[a,b]=1 35 d.fillna(0,inplace=True) 36 end0=time.clock() 37 usetime0=end0-start0 38 print('轉化0-1矩陣耗時' + str(usetime0) +'s!') 39 #d.to_csv() 40 41 df=d.copy() 42 sampler=np.random.permutation(len(df)) 43 df = df.take(sample) 44 train=df.iloc[:int(len(df)*0.9),:] 45 test=df.iloc[int(len(df)*0.9):,:] 46 47 df=df.values() 48 df_train=df[:int(len(df)*0.9),:] # 9299 49 df_test=df[int(len(df)*0.9):,:] # 1034 50 df_train=df_tain.T 51 df_test=df_test.T 52 print(df_train.shape,df_test.shape) # (4339L, 9299L) (4339L, 1034L) 53 54 start1=time.clock() 55 r=recommender() 56 sim=r.fit(df_train) # 計算相似度矩陣 57 end1=time.clock() 58 a=pd.DataFrame(sim) 59 usetime1=end1-start1 60 print('建立矩陣耗時'+ str(usetime1)+'s!') 61 print(a.shape) 62 63 a.index=train.columns 64 #a.columns=train.columns 65 a.columns=train.index 66 a.head(20) 67 68 start2=time.clock() 69 result=r.recommend(df_test) 70 end2=time.clock() 71 72 result1=pd.DataFrame(result) 73 usetime2=end2-start2 74 75 print('測試集推薦函數耗時'+str(usetime2)+'s!') 76 result1.head() 77 result1.index=test.columns 78 result1.columns=test.index 79 result1 80 81 def xietong_result(k,recomMatrix): # k表推薦個數,recomMatrix推薦矩陣表DataFrame 82 recomMatrix.fillna(0.0,inplace=True) 83 n=range(1,k+1) 84 recommends=['xietong'+str(y) for y in n] 85 currentemp=pd.DataFrame([],index=recomMatrix.columns,columns=recommends) 86 for i in range(len(recomMatrix.columns)): 87 temp = recomMatrix.sort_values(by = recomMatrix.columns[i], ascending = False) 88 j = 0 89 while j < k: 90 currentemp.iloc[i,j] = temp.index[j] 91 if temp.iloc[j,i] == 0.0: 92 currentemp.iloc[i,j:k] = np.nan 93 break 94 j += 1 95 return currentemp 96 start3 = time.clock() 97 xietong_result = xietong_result(3, result1) 98 end3 = time.clock() 99 print('按照協同過濾推薦方法為用戶推薦3個未瀏覽過的網址耗時為' + str(end3 - start3)+'s!') #29.4996622053s! 100 xietong_result.head() 101 102 # test = df.iloc[int(len(df)*0.9):, :] # 所有測試數據df此時是矩陣,這樣不能用 103 randata = 1 - df_test # df_test是用戶瀏覽過的網頁的矩陣形式,randata則表示是用戶未瀏覽過的網頁的矩陣形式 104 randmatrix = pd.DataFrame(randata, index = test.columns,columns=test.index)#這是用戶未瀏覽過(待推薦)的網頁的表格形式 105 def rand_recommd(K, recomMatrix):# 106 import random # 注意:這個random是random模塊, 107 import numpy as np 108 109 recomMatrix.fillna(0.0,inplace=True) # 此處必須先填充空值 110 recommends = ['recommed'+str(y) for y in range(1,K+1)] 111 currentemp = pd.DataFrame([],index = recomMatrix.columns, columns = recommends) 112 113 for i in range(len(recomMatrix.columns)): #len(res.columns)1 114 curentcol = recomMatrix.columns[i] 115 temp = recomMatrix[curentcol][recomMatrix[curentcol]!=0] # 未曾瀏覽過 116 # = temp.index[random.randint(0,len(temp))] 117 if len(temp) == 0: 118 currentemp.iloc[i,:] = np.nan 119 elif len(temp) < K: 120 r = temp.index.take(np.random.permutation(len(temp))) #注意:這個random是numpy模塊的下屬模塊 121 currentemp.iloc[i,:len(r)] = r 122 else: 123 r = random.sample(temp.index, K) 124 currentemp.iloc[i,:] = r 125 return currentemp 126 127 start4 = time.clock() 128 random_result = rand_recommd(3, randmatrix) # 調用隨機推薦函數 129 end4 = time.clock() 130 print('隨機為用戶推薦3個未瀏覽過的網址耗時為' + str(end4 - start4)+'s!') # 2.1900423292s! 131 #保存的表名命名格式為“3_1_k此表功能名稱”,是本小節生成的第5張表格,功能為random_result:顯示隨機推薦的結果 132 #random_result.to_csv('random_result.csv') 133 random_result # 結果中出現了全空的行,這是冷啟動現象,瀏覽該網頁僅此IP一個,其他IP不曾瀏覽無相似系數 134 135 def popular_recommed(K, recomMatrix): 136 recomMatrix.fillna(0.0,inplace=True) 137 import numpy as np 138 recommends = ['recommed'+str(y) for y in range(1,K+1)] 139 currentemp = pd.DataFrame([],index = recomMatrix.columns, columns = recommends) 140 141 for i in range(len(recomMatrix.columns)): 142 curentcol = recomMatrix.columns[i] 143 temp = recomMatrix[curentcol][recomMatrix[curentcol]!=0] 144 if len(temp) == 0: 145 currentemp.iloc[i,:] = np.nan 146 elif len(temp) < K: 147 r = temp.index #注意:這個random是numpy模塊的下屬模塊 148 currentemp.iloc[i,:len(r)] = r 149 else: 150 r = temp.index[:K] 151 currentemp.iloc[i,:] = r 152 153 return currentemp 154 155 # 確定用戶未瀏覽的網頁(可推薦的)的數據表格 156 TEST = 1-df_test # df_test是用戶瀏覽過的網頁的矩陣形式,TEST則是未曾瀏覽過的 157 test2 = pd.DataFrame(TEST, index = test.columns, columns=test.index) 158 print(test2.head()) 159 print(test2.shape ) 160 161 # 確定網頁瀏覽熱度排名: 162 hotPopular = data['fullURL'].value_counts() 163 hotPopular = pd.DataFrame(hotPopular) 164 print(hotPopular.head()) 165 print(hotPopular.shape) 166 167 # 按照流行度對可推薦的所有網址排序 168 test3 = test2.reset_index() 169 list_custom = list(hotPopular.index) 170 test3['index'] = test3['index'].astype('category') 171 test3['index'].cat.reorder_categories(list_custom, inplace=True) 172 test3.sort_values('index',inplace = True) 173 test3.set_index ('index', inplace = True) 174 print(test3.head()) 175 print(test3.shape) 176 177 # 按照流行度為用戶推薦3個未瀏覽過的網址 178 recomMatrix = test3 # 179 start5 = time.clock() 180 popular_result = popular_recommed(3, recomMatrix) 181 end5 = time.clock() 182 print('按照流行度為用戶推薦3個未瀏覽過的網址耗時為' + str(end5 - start5)+'s!')#7.70043007471s! 183 184 #保存的表名命名格式為“3_1_k此表功能名稱”,是本小節生成的第6張表格,功能為popular_result:顯示按流行度推薦的結果 185 #popular_result.to_csv('3_1_6popular_result.csv') 186 187 popular_result
Ref:
《Python數據分析與挖掘實戰》第12章(上)——協同推薦
推薦系統:協同過濾collaborative filtering
《Python數據分析與挖掘實戰》第12章(中)——協同推薦
《Python數據分析與挖掘實戰》第12章(下)——協同推薦
《數據分析與挖掘實戰》:源代碼及數據需要可自取:https://github.com/Luove/Data