(機器學習篇)大數據帶你精準預測消費者感興趣的商家活動
剛剛過去的雙十一剁手節還留有餘溫,借著這股尚未散去的熱情,我們來聊一聊如何利用大數據精準預測消費者最感興趣的商家活動。
在商業活動中,商家總是面臨這樣一個問題:策劃的活動是否能夠吸引到消費者參加?忙碌一場是否能獲得想要的收益? 消費者同樣面臨非常苦惱的問題:我不喜歡這類的活動信息,為什麼總是給我推送我不喜歡的東西?
信息不對稱導致的商家無法正確獲知消費者的具體喜好,而在信息爆炸時代,消費者說『no』權利的虛設, 需要一座從商家此岸通向消費者彼岸的橋樑—「精準營銷」
本篇將利用用戶歷史行為數據/人口特徵數據/用戶活動行為交互數據等,採用協同過濾演算法和機器學習分類演算法, 預測用戶感興趣的活動。
(數據來源:kaggle,使用到演算法:協同過濾+分類演算法
認識數據
目前,能夠拿到的數據包含:
訓練集(train.csv)包含用戶id,活動id,是否被邀請,時間戳,有興趣,無興趣;
測試集(test.csv)包含用戶id,活動id,是否被邀請,時間戳;
用戶數據(users.csv)包含用戶id,所在地區?,出生年,性別,註冊日期,所在地區
用戶_朋友數據(users_friends.csv)包含用戶id,每個用戶id所有的朋友id
活動數據(events.csv)包含活動id,用戶id,活動開始時間,活動舉辦城市,活動舉辦省份,郵政編碼,活動舉辦國家,經度,緯度,
(c_1~c_100:活動描述中出現頻次最高的詞的頻次,c_other:其他詞出現的頻次和)
活動參加者數據(event_attendees.csv)包含活動id,打算參加的用戶id,可能參的用戶id,不想參加的用戶id,被邀請的用戶id
基於已有數據信息可以大致從7個緯度展開:1) 用戶自身;2)用戶<==>用戶; 3)用戶<==>活動; 4)用戶<==>朋友; 5)朋友自身;6)活動自身;7)活動<==>活動
用戶自身的參與活動的熱情度
用戶與用戶之間的相似度
用戶有沒有參與或被邀請參加某活動
用戶的朋友對活動的熱情度或者參與度
用戶受朋友的影響程度
活動的受歡迎程度
活動與活動之間的相似度
特徵構建思路如下:
在本項目中會採用上述圖示的feature的構建思路,除了已知特徵(not_interested,invited)兩個特徵外,基於用戶的推薦度特徵,基於活動的推薦度特徵,用戶活躍度,朋友圈影響度,活動受歡迎度指標需要基於已有數據計算。下面將逐步計算各特徵數據
數據缺失
從缺失狀態看, 幸運的是train和test都不存在缺失數據;
不幸的是,我們需要的user人口數據特徵和events基本數據特徵在多個緯度存在缺失,在後續處理中要注意。
缺失狀態細節如下:
紅框框住的特徵有缺失,後續特徵清洗需要注意
數據清洗
從數據來看,我們需要做四大方面的工作: 1)文本數據====>數值型數據(如:gender) 2)時間數據====>數值型數據(如:jointedAt) 3)地理位置數據====>數值型數據(如:location) 4) 多欄位數據 ===>數值型數據
因為本項目數據集中存在文本,時間,地理位置等多中數據形式,需要定義轉換,如下所示:
## 定義清洗函數
#定義genderdef gender_to_data(genderstr):if genderstr==male:
return 1 elif genderstr==female: return 2 else: return 0#定義birthdef birth_to_int(birthyear): if birthyear==None: return 0else:
return int(birthyear)#定義timezonedef timezone_to_int(timezonestr): try: return int(timezonestr) except: return 0#定義年月def get_year_mounth(dateString):dttm = .join(dateString[:8].strip(").split(-))
return dttm #locale===>datalocale_id =defaultdict(int)for i,l in enumerate(locale.locale_alias.keys()): locale_id[l]=i+1def locale_to_data(localestr): return locale_id[localestr.lower()]#loacation===>datacountry_id = defaultdict(int)ctry_id = defaultdict(int)
for i,c in enumerate(pycountry.countries): country_id[c.name.lower()] = i+1 if c.name.lower() ==usa: ctry_id[US] =i if c.name.lower() == canada: ctry_id[CA] = i for cc in ctry_id.keys(): for s in pycountry.subdivisions.get(country_code = cc): country_id[s.name.lower()] = ctry_id[cc] +1def get_country_id(location):
if((isinstance(location,str)) and len(location.strip())>0 and location.rfind( )>-1): return country_id[location[location.rindex( )+2:].lower()] else: return 0def get_feature_hash(value): if len(value.strip())==0: return -1 else:return int(hashlib.sha224(value.encode(utf-8)).hexdigest()[0:4], 16)
def get_float_value(value): if len(value.strip())==0 or value==NA: return 0.0 else: return float(value)
數據範圍限定說明: 在本次項目中限定user和event的範圍為在train和test中出現的所有user和event匯總,沒有在train和test中出現的user和event不統計在內
具體步驟如下:
1.分別獲得獨立用戶和獨立活動的數量集合(set形式)
2.分別獲得每個獨立用戶有交互的活動列表和每個活動有交互的用戶列表(dict形式)
3.獲得用戶兩兩之間配對集合和活動兩兩之間配對集合
4.基於訓練數據集獲得用戶-活動-打分矩陣
5.基於打分矩陣和user數據計算基於用戶的相似度矩陣並獲得推薦度矩陣
6.計算用戶活躍度
7.計算朋友圈影響力
8.計算打分矩陣和event數據計算基於event的相似度矩陣並獲得推薦度矩陣
9.計算event受歡迎度
10.將特徵整合到訓練集和測試集
11.訓練+調參
具體代碼如下:
unique_users=set()#user集合
unique_events = set()#event集合events_per_user = defaultdict(set)#每個user對應的獨立活動集合users_per_event = defaultdict(set)#每個獨立活動對應的user集合for filename in ["/Users/wangli/Downloads/recommendation/train.csv", "/Users/wangli/Downloads/recommendation/test.csv"]: f = open(filename,r) f.readline()#表示要開始讀第一行 for line in f: cols = line.strip().split(,) #去除空格後按『,』分割成list unique_users.add(cols[0]) unique_events.add(cols[1]) events_per_user[cols[0]].add(cols[1])#某個用戶的events集合量 users_per_event[cols[1]].add(cols[0])#某個event的用戶集合量 f.close()print(訓練集和測試集合計獨立用戶的數量是:,len(unique_users))print(訓練集和測試集合計獨立用戶的數量是:,len(unique_events))
訓練集和測試集合計獨立用戶的數量是: 3391
訓練集和測試集合計獨立用戶的數量是: 13418#構建user—event得分矩陣
#字典形式存取稀疏矩陣,定義行和列的數量users_events_score = ss.dok_matrix((len(unique_users), len(unique_events)))users_index = dict()events_index = dict()#userid 對應到index,eventid 對應到indexfor i,u in enumerate(unique_users):# i 是第幾個,u是值 users_index[u]=ifor i,e in enumerate(unique_events): events_index[e]=if_train = open("/Users/wangli/Downloads/recommendation/train.csv", r)#開始讀取數據第一行f_train.readline()for line in f_train: cols = line.strip().split(,) i = users_index[cols[0]] #i是用戶對應的index j = events_index[cols[1]]#j是event對應的index # 第i個用戶對第j個活動的興趣度 用interest-not interest users_events_score[i,j] = int(cols[4])-int(cols[5]) f_train.close()#用戶對活動的打分數據處理完畢,是用戶數量*event數量的矩陣
相似度至少應該發生在兩個元素之間,在下面設定長度要>2
#這部分是用來後面計算用戶與用戶之間相似度和event之間相似度在限定的數據範圍內
unique_user_pairs = set()unique_event_pairs = set()for event in unique_events: users_id = users_per_event[event] if len(users_id)>2: #至少某個event中有兩個user行為 unique_user_pairs.update(itertools.combinations(users_id,2)) #users兩兩組合for user_id in unique_users: events = events_per_user[user_id] #取events_for_user字典中key=user 對應的value events 是list if len(events)>2:#某個用戶至少對兩個event有行為 unique_event_pairs.update(itertools.combinations(events,2))#events兩兩組合print(兩兩組合的唯一用戶數量:,len(unique_user_pairs))print(兩兩組合的唯一活動數量:,len(unique_event_pairs))
兩兩組合的唯一用戶數量: 235674
兩兩組合的唯一活動數量: 100472### 用到協同過濾==計算用戶與用戶之間的相似矩陣
nusers = len(users_index.keys()) #計算user總數量 3391f= open("/Users/wangli/Downloads/recommendation/users.csv", r)col_names = f.readline().strip().split(,)users_matrix = ss.dok_matrix((nusers,len(col_names)-1))# 3391*6的矩陣for line in f: cols = line.strip().split(,) if cols[0] in users_index: i = users_index[cols[0]]#取出對應的index users_matrix[i,0] = locale_to_data(cols[1]) users_matrix[i,1] = birth_to_int(cols[2]) users_matrix[i,2] = gender_to_data(cols[3]) users_matrix[i,3] = get_year_mounth(cols[4]) users_matrix[i,4] = get_country_id(cols[5]) users_matrix[i,5] = timezone_to_int(cols[6])f.close()#獲得用戶的3391*6的用戶矩陣#用戶矩陣歸一化處理users_matrix = normalize(users_matrix,norm=l1,axis=0,copy=False)
計算基於用戶相似度矩陣
用戶的相似度計算這裡採用pearson相關距離公式計算
#計算用戶相似度矩陣
sim = ssd.correlationusers_similarity_matrix = ss.dok_matrix((nusers,nusers))#3391*3391矩陣for i in range(0,nusers): users_similarity_matrix[i,i]=1.0 #對角線元素為1.0for u1,u2 in unique_user_pairs:#首先要保證在可計算範圍內的數 i = users_index[u1] #取出用戶對應的index j = users_index[u2] #取出用戶對應的index #計算correlation相似性 if (i,j) not in users_similarity_matrix: usim = sim(users_matrix.getrow(i).todense(),users_matrix.getrow(j).todense()) #todense獲得矩陣形式 users_similarity_matrix[i,j] = usim #對稱 users_similarity_matrix[j,i] = usim #對稱print(用戶相似矩陣有{}行*{}列.format(*users_similarity_matrix.shape))print(用戶相似矩陣中非零個數為:,users_similarity_matrix.count_nonzero())
經過上述的過程,完成基於用戶的相似度計算,user_matrix是按照字典形式存取的稀疏矩陣,形狀為(3391,3391),共有416609個非零值
用戶相似度計算完畢,下面計算用戶的活躍度
在本項目中,選用最直接的思路,即:如果用戶的朋友數量越多,越說明這個用戶可能屬於社交外向型用戶,社交的活躍度越高,越可能參加更多的活動。本項目中,採用平均活躍度作為衡量用戶活躍度的指標
#獲得每個用戶的平均活躍度
nusers = len(users_index.keys()) #用戶總個數num_friends = np.zeros((nusers)) #初始化用戶朋友個數,最多就是3391,可能根沒有在train和test中的用戶也有關聯,這裡只取train和test中的f = open("/Users/wangli/Downloads/recommendation/user_friends_1.csv", r)f.readline()ln = 0for line in f: if ln % 10000 == 0: print(下載行:,ln) cols = line.strip().split(,) user = cols[1] if user in users_index: i = users_index[user]#取user對應的index friends = cols[2].split( ) #獲得某個用戶具體的朋友 num_friends[i] = len(friends) #i用戶的朋友個數,這個數包含了不再train和test中的user ln +=1#歸一化sum_num_friends = num_friends.sum(axis=0)num_friends = num_friends/sum_num_friends
用戶活躍度計算完畢,下面計算基於活動的相似度矩陣
活動特徵中包括:活動信息基本特徵數據和活動描述關鍵詞頻次特徵數據;為更好的衡量數據的特徵,將活動特徵矩陣拆分成兩個:基於基本特徵的數據矩陣和基於關鍵詞頻次的特徵矩陣
基本特徵數據矩陣相似度採用皮爾遜相關距離計算;關鍵詞頻次相似度矩陣採用余玄距離計算
直觀理解:皮爾遜相關距離可以衡量兩個數據點之間距離的遠近,距離越小越相似;而余玄距離則是評估兩個數據方向夾角的程度,越小說明越相近,可以規避個體認知的差異,用餘弦相似性更加穩定。
#獲得活動特徵矩陣
f = open("/Users/wangli/Downloads/recommendation/events1.csv", r)f.readline()nevents = len(events_index.keys())#活動的數量events_prop_matrix = ss.dok_matrix((nevents,7)) #基本信息 13418*7events_cont_matrix = ss.dok_matrix((nevents,100)) #詞幹頻數 13418*100for line in f.readlines(): cols = line.strip().split(,)#拆分行 event_id = cols[1]#獲得event id if event_id in events_index: i = events_index[event_id] #獲得event對應的數字index events_prop_matrix[i,0] = get_year_mounth(cols[3])#start_time events_prop_matrix[i,1] = get_feature_hash(cols[4]) #city events_prop_matrix[i,2] = get_feature_hash(cols[5]) #state events_prop_matrix[i,3] = get_feature_hash(cols[6]) #zip events_prop_matrix[i,4] = get_feature_hash(cols[7]) #country events_prop_matrix[i,5] = get_float_value(cols[8]) # lat events_prop_matrix[i,6] = get_float_value(cols[9]) #lon for j in range(10,110): events_cont_matrix[i,j-10] = cols[j]f.close()#歸一化events_prop_matrix = normalize(events_prop_matrix,norm=l1,axis=0,copy=False)events_cont_matrix = normalize(events_cont_matrix,norm=l1,axis=0,copy=False)#獲得活動特徵矩陣f = open("/Users/wangli/Downloads/recommendation/events1.csv", r)f.readline()nevents = len(events_index.keys())#活動的數量events_prop_matrix = ss.dok_matrix((nevents,7)) #基本信息 13418*7events_cont_matrix = ss.dok_matrix((nevents,100)) #詞幹頻數 13418*100for line in f.readlines(): cols = line.strip().split(,)#拆分行 event_id = cols[1]#獲得event id if event_id in events_index: i = events_index[event_id] #獲得event對應的數字index events_prop_matrix[i,0] = get_year_mounth(cols[3])#start_time events_prop_matrix[i,1] = get_feature_hash(cols[4]) #city events_prop_matrix[i,2] = get_feature_hash(cols[5]) #state events_prop_matrix[i,3] = get_feature_hash(cols[6]) #zip events_prop_matrix[i,4] = get_feature_hash(cols[7]) #country events_prop_matrix[i,5] = get_float_value(cols[8]) # lat events_prop_matrix[i,6] = get_float_value(cols[9]) #lon for j in range(10,110): events_cont_matrix[i,j-10] = cols[j]f.close()#歸一化events_prop_matrix = normalize(events_prop_matrix,norm=l1,axis=0,copy=False)events_cont_matrix = normalize(events_cont_matrix,norm=l1,axis=0,copy=False)
基於活動的相似度計算完畢,下面計算活動的熱度
在本項目中活動熱度用意願參加活動的人數-不願意參加活動的人數衡量
nevents = len(events_index.keys())
events_popularity = ss.dok_matrix((nevents,1))f = open("/Users/wangli/Downloads/recommendation/event_attendees1.csv", r)f.readline()for line in f: cols = line.strip().split(,) event_id = cols[1] if event_id in events_index: i = events_index[event_id] events_popularity[i,0] = len(cols[2].split( )) - len(cols[5].split( ))#想要參加的人數-不想要參加的人數f.close()#歸一化處理events_popularity = normalize(events_popularity,norm=l1,axis=0,copy=False
特徵生成
在上面的過程中,完成了對用戶活躍度,基於用戶相似度,基於活動相似度,活動受歡迎度的計算,下面要將所有這些數據對應到訓練集和測試集對應的(user_id,event_id)中,最終生成訓練集和測試集數據
#計算基於用戶的推薦度
def users_recom(user_id,event_id): i = users_index[user_id] j = events_index[event_id] vs = users_events_score[:,j] sims = users_similarity_matrix[i,:] prob = sims * vs #第j個活動的推薦度 #user i 在 event j 上的總推薦度,值越大,越推薦 try: return prob[0,0] - users_events_score[i,j] #返回值 except IndexError: return 0 #計算基於活動的推薦度def events_recom(user_id,event_id): i = users_index[user_id] j = events_index[event_id] js = users_events_score[i,:] psim = events_prop_sim[:,j] csim = events_cont_sim[:,j] pprod = js * psim cprod = js * csim pscore =0 cscore =0 try: pscore = pprod[0,0]-users_events_score[i,j] except IndexError: pass try: cscore = cprod[0,0]-users_events_score[i,j] except IndexError: pass return pscore,cscore#計算user_id的活躍度def user_pop(user_id): if user_id in users_index: i = users_index[user_id] try: return num_friends[i] except IndexError: return 0#計算event_id的受歡迎度def event_pop(event_id): i = events_index[event_id] return events_popularity[i,0]
生成訓練集和測試集
data_train =pd.DataFrame(data=None,index=None,columns=None,copy=False)
data_test =pd.DataFrame(data=None,index=None,columns=None,copy=False)data_train[user] = train.userdata_train[event] = train.eventdata_train[invited] = train.invitedur=[]up=[]ep=[]ec=[]epp=[]for i in range(len(data_train)): tem_user = str(data_train.loc[i][0]) tem_event = str(data_train.loc[i][1]) user_reco = users_recom(tem_user,tem_event) user_pops = user_pop(tem_user) event_p_reco,event_c_reco = events_recom(tem_user,tem_event) event_pops = event_pop(tem_event) ur.append(user_reco) up.append(user_pops) ep.append(event_p_reco) ec.append(event_c_reco) epp.append(event_pops)data_train[user_recom]=urdata_train[user_pop] = updata_train[event_p_recom] = epdata_train[event_c_recom] = ecdata_train[event_pop] = epp
建模與預測
from sklearn.cross_validation import StratifiedKFoldfrom sklearn.linear_model import SGDClassifierX = np.matrix(pd.DataFrame(data_train,index=None, columns=[invited, event_pop,user_pop]))y = np.array(y2)nrows = len(train)kfold = StratifiedKFold(n_folds=10,random_state=0,shuffle=False)avgAccuracy = 0run =0for train,test in kfold: Xtrain, Xtest, ytrain, ytest = X[train], X[test], y[train], y[test] clf = LogisticRegression(penalty="l2") clf.fit(Xtrain, ytrain) accuracy = 0 ntest = len(ytest) for i in range(0, ntest): yt = clf.predict(Xtest[i, :]) if yt == ytest[i]: accuracy += 1 accuracy = float(accuracy / ntest) print ("accuracy (run %d): %f" % (run, accuracy)) avgAccuracy += accuracy run += 1print ("Average accuracy", (avgAccuracy / run))
from sklearn.learning_curve import learning_curveclf =LogisticRegression(penalty="l2")train_sizes,train_scores,test_scores = learning_curve(estimator=clf,X=X, y=y, train_sizes=np.linspace(0.05,1.0,20), cv=10, n_jobs=1)train_mean = np.mean(train_scores,axis=1)train_std = np.std(train_scores,axis=1)test_mean = np.mean(test_scores,axis=1)test_std = np.std(test_scores,axis=1)plt.plot(train_sizes,train_mean,color=blue,marker=o,markersize=5,label=traing accuracy)plt.fill_between(train_sizes,train_mean+train_std,train_mean-train_std,alpha=0.15,color=blue)plt.plot(train_sizes,test_mean,color=green,linestylex=--,marker=s,markersize=5,label=validation accuracy)plt.fill_between(train_sizes,test_mean+test_std,test_mean-test_std,alpha=0.15,color=green)plt.grid()plt.xlabel(Number of training samples)plt.ylabel(Accuracy)plt.legend(loc=lower right)plt.ylim([0.0,1.0])plt.show()
延伸與思考
本項目主要是借鑒該kaggle項目top1選手的思路進行,從過程上看雖然計算了推薦度,但是預測結果過擬合,僅用少有的幾個特徵可以達到大約73%的準確度,這說明,模型的複雜度提升並不一定保證更高的準確度,相比簡單的模型的范化能力會更好。更多,在本項目中仍有很多可以提升的地方,在後續的工作中進行優化。
推薦閱讀:
※微軟宣布在機器中英雙語翻譯領域取得突破性進展
※多版本中文停用詞詞表 + 多版本英文停用詞詞表 + python詞表合併程序
※集智:負基礎也能學會的機器學習(三)
※【ML專欄】【2018Q1】(4)眾包vs有監督學習