前言
赛题链接为:Event Recommendation Engine Challenge
赛题介绍
根据用户信息与活动(event)信息,预测用户将对哪些活动感兴趣。
数据集
共有六个文件:train.csv,test.csv, users.csv,user_friends.csv,events.csv和 event_attendees.csv。
train.csv 包含六列:
user:用户id
event:活动id
invite:是否被邀请
timestamp:时间戳
interested:
not_interested
train.csv 包含四列(与train的属性相同,但没有interested和not_interested)。
users.csv 包含七列
user_id:用户的ID
locale:用户区域
birthyear:用户出生的年份
gender:性别
joinedAt:首次使用APP的时间
location:用户位置
timezone:UTC偏移量
user_friends.csv包含有关此用户的社交数据,包含两列:user和friends。
user:用户的id,
friends:用户朋友ID(以空格分隔)。
events.csv 包含有关活动的数据,有110列。前九列是 event_id,user_id,start_time,city,state,zip,country, lat和lng
event_id:活动id
user_id:创建活动的用户的id
start_time:开始时间
city、state、zip、country:活动场地详细信息
lat和lng:经纬度
count_1, count_2,…, count_100 表示:统计了活动名称或描述中出现的100个最常见的词干,统计它们出现的频率(会把时态语态都去掉, 对词做了词频的排序(表示前N个最常出现的词,每个词在活动中出现的频次。)
count_other: count_other 是其余词的统计。
event_attendees.csv包含有关哪些用户参加了各种事件的信息,并包含以下列: event_id,yes,maybe,invite和no。
event_id:活动id
yes:会参加的用户
maybe:可能参加的用户
invite:邀请的用户
no:不会参加的用户
所以,总的来说包括三类数据:
- 用户信息
- 用户社交信息
- 活动本身信息
评估指标
Mean average precision 平均精度均值,但是这里的准确度是截断的。( 比如推荐了300个活动就看前200个活动,后面不考虑。)
提交示例
User,Events
1776192,2877501688 1024025121 4078218285 2972428928 3025444328 1823369186 2514143386
3044012,2529072432 1532377761 1390707377 1502284248 3072478280 1918771225
4236494,152418051 4203627753 2790605371 799782433 823015621 2352676247 110357109
1.探索性数据分析(Exploratory Data Analysis,EDA)
首先导入需要的包
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
sns.set()
使用pandas读入数据,查看前几行的原数据:
train = pd.read_csv('train.csv')
train.head()
train.isnull().any() #查看缺失值情况
user False
event False
invited False
timestamp False
interested False
not_interested False
dtype: bool
没有缺失值。
我们用下列的方法,得到数值型数据的一些分布:
train.describe()
user event invited interested not_interested
count 1.539800e+04 1.539800e+04 15398.000000 15398.000000 15398.000000
mean 2.199685e+09 2.060937e+09 0.042473 0.268282 0.033381
std 1.268887e+09 1.190660e+09 0.201672 0.443079 0.179635
min 3.044012e+06 1.040700e+05 0.000000 0.000000 0.000000
25% 1.071319e+09 1.057229e+09 0.000000 0.000000 0.000000
50% 2.259555e+09 1.996503e+09 0.000000 0.000000 0.000000
75% 3.292836e+09 3.060446e+09 0.000000 1.000000 0.000000
max 4.293103e+09 4.294677e+09 1.000000 1.000000 1.000000
2.特征工程(Feature engineering )
首先,简单使用用户的历史数据就已经够做一个UserCF。
其次,使用event信息也也可完成ItemCF。
上述方式都可以完成推荐,但这样用户的社交信息没有充分得到利用。
基于用户的协同过滤算法(User Based Collaborative Filtering),简称UserCF
- 找到和目标用户兴趣相似的用户集合
- 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户
考虑到本问题是监督学习中的二分类问题:
输出变量 y(二分类,感兴趣/不感兴趣)
关键是输入变量X怎样构建的问题。
为了把更多维度的信息纳入考量,这里把协同过滤(UserCF、ItemCF)的推荐度看做一个特征,放到X中,添加feature。
train.csv,users.csv:可以构建用户基本信息
user_friends.csv:用户社交信息(朋友多少,朋友参加活动活跃度)
events.csv
event_attendees.csv
初步思路简图如下图所示
导入包
import itertools
import pickle #针对内存不足的情况
import pycounty #处理国家相似度
import locale
import scipy.spatial.distance as ssd #用来求各种距离
from collections import defaultdict #容器
from sklearn.preprocessing import normalize #数据预处理
处理user 和 event数据
# 处理user 和 event数据
class ProgramEntities:
"""
user和event 的文件数据量级大,包含样本多,包括了历史上所有的user和event
我们只关心train和test中出现的user和event,重点处理这部分关联数据
"""
def __init__(self):
# 统计训练集中有多少独立的用户的events
uniqueUsers = set()
uniqueEvents = set()
eventsForUser = defaultdict(set)
usersForEvent = defaultdict(set)
for filename in ["train.csv", "test.csv"]:
data = pd.read_csv(filename)
for i in range(len(data)):
uniqueUsers.add(data['user'][i])
uniqueEvents.add(data['event'][i])
eventsForUser[data['user'][i]].add(data['event'][i])
usersForEvent[data['event'][i]].add(data['user'][i])
#print(len(uniqueUsers))
#print(len(uniqueEvents))
self.userEventScores = ss.dok_matrix((len(uniqueUsers), len(uniqueEvents)))
self.userIndex = dict()
self.eventIndex = dict()
for i, u in enumerate(uniqueUsers): #用户编号
self.userIndex[u] = i
for i, e in enumerate(uniqueEvents): #电影编号
self.eventIndex[e] = i
data = pd.read_csv("train.csv")
for index in range(len(data)):
i = self.userIndex[data['user'][index]]
j = self.eventIndex[data['event'][index]]
self.userEventScores[i, j] = int(data['interested'][index]) - int(data['not_interested'][index])
# 为了防止不必要的计算,我们找出来所有用户 关联的event 并保存
sio.mmwrite("PE_userEventScores", self.userEventScores)
# 所谓的关联用户,指的是至少在同一个event上有行为的用户pair
# 关联的event指的是至少同一个user有行为的event pair
self.uniqueUserPairs = set()
self.uniqueEventPairs = set()
for event in uniqueEvents:
users = usersForEvent[event]
if len(users) > 2:
self.uniqueUserPairs.update(itertools.combinations(users, 2))
for user in uniqueUsers:
events = eventsForUser[user]
if len(events) > 2:
self.uniqueEventPairs.update(itertools.combinations(events, 2))
pickle.dump(self.userIndex, open("PE_userIndex.pkl", 'wb')) #保存到本地
pickle.dump(self.eventIndex, open("PE_eventIndex.pkl", 'wb'))
上述代码统计了所有的train 和 test中所有的user和event,同时构建了user对event感兴趣程度的矩阵(数值为1 0 -1,类似于对活动的打分)
用户总数:3391
users总数:13418
uniqueUsers:集合,保存train.csv和test.csv中的所有user ID
uniqueEvents:集合,保存train.csv和test.csv中的所有event ID
userIndex:字典,每个用户有个Index
eventIndex:字典,每个event有个Index
eventsForUser:字典,key为每个用户,value为该用户对应的event集合
usersForEvent:字典,key为每个event,value为该event对应的user集合
userEventScores:稀疏矩阵,只保存了非0值
处理用户矩阵
df_users = pd.read_csv('users.csv')
df_users.head()
信息总览
df_users.info()
user_id 38209 non-null int64
locale 38209 non-null object
birthyear 38209 non-null object
gender 38100 non-null object
joinedAt 38152 non-null object
location 32745 non-null object
timezone 37773 non-null float64
dtypes: float64(1), int64(1), object(5)
memory usage: 2.0+ MB
None
用户矩阵预处理
缺失值统计
df_users.isnull().sum()
user_id 0
locale 0
birthyear 0
gender 109
joinedAt 57
location 5464
timezone 436
1) gender属性的处理
缺失值的处理:
gender(性别)属性,缺失值适中,并且该属性为类目属性,把NaN作为一个新类别,加到类别特征中。
df_users['gender'] = df_users['gender'].fillna('NaN')
sns.countplot(df_users['gender'])
# 类别转换为数字
df_users['gender'] = df_users['gender'].fillna('NaN')
df_users['gender'], df_users_gender_categories = df_users['gender'].factorize()
sns.countplot(df_users['gender'])
print(df_users_gender_categories)
Index(['male', 'female', 'NaN'], dtype='object')
male 0
female 1
NaN 2
2) joinedAt列处理
可以提取“年份”、“月份”,缺失值用‘0’填充
def getJoinedYearMonth(dateString):
try:
dttm = datetime.datetime.strptime(dateString, "%Y-%m-%dT%H:%M:%S.%fZ")
return "".join( [int(dttm.year), int(dttm.month)] )
except:
return 0
df_users['joinedAt'] = df_users['joinedAt'].map(getJoinedYearMonth)
3) locale列处理
locale 表示 国家-地区,将其转化为数值型
df_users['locale'], df_users_locale_categories = df_users['locale'].factorize()
4) birthyear
该列处理比较简单,直接转换成数值
def getBirthYearInt(birthYear):
try:
return 0 if birthYear == "None" else int(birthYear)
except:
return 0
df_users['birthyear'] = df_users['birthyear'].map(getBirthYearInt)
5) timezone列处理
存在值就转换为int型,不存在用0填充
def getTimezoneInt(timezone):
try:
return int(timezone)
except:
return 0
df_users['timezone'] = df_users['timezone'].map(getTimezoneInt)
6) location列处理
df_users['location'] = df_users['location'].fillna('NaN')
df_users['location'], df_users_gender_categories = df_users['location'].factorize()
df_users.info()
user_id 38209 non-null int64
locale 38209 non-null int64
birthyear 38209 non-null int64
gender 38209 non-null int64
joinedAt 38209 non-null int64
location 38209 non-null int64
timezone 38209 non-null int64
保存处理好的文件
df_users.to_csv('users_trans.csv')
构建用户相似度矩阵
class Users:
"""
构建user/user相似度矩阵
注意这里的相似度是注册信息的相似度
"""
def __init__(self, programEntities, sim=ssd.correlation):#spatial.distance.correlation(u, v) #计算向量u和v之间的相关系数
nusers = len(programEntities.userIndex.keys())#3391
#print(nusers)
#计算用户相似度矩阵
self.userSimMatrix = ss.dok_matrix( (nusers, nusers) )#(3391,3391)
for i in range(0, nusers):
self.userSimMatrix[i, i] = 1.0
for u1, u2 in programEntities.uniqueUserPairs:
i = programEntities.userIndex[u1] #获取用户u1的索引
j = programEntities.userIndex[u2]
if (i, j) not in self.userSimMatrix:
#print(self.userMatrix.getrow(i).todense()) 如[[0.00028123,0.00029847,0.00043592,0.00035208,0,0.00032346]]
#print(self.userMatrix.getrow(j).todense()) 如[[0.00028123,0.00029742,0.00043592,0.00035208,0,-0.00032346]]
usim = sim(self.userMatrix.getrow(i).todense(),self.userMatrix.getrow(j).todense()) #计算两个用户向量之间的相似性,为对称矩阵
self.userSimMatrix[i, j] = usim
self.userSimMatrix[j, i] = usim
sio.mmwrite('US_userSimMatrix', self.userSimMatrix)
用户社交关系挖掘
考虑到:
- 如果user有更多的朋友,可能他性格外向,更容易参加各种活动
- 如果user朋友会参加某个活动,可能他也会跟随去参加一下
class UserFriends:
"""
挖掘用户社交信息
1)user朋友的数目
2)user朋友对活动的热衷程度
"""
def __init__(self, programEntities):
nusers = len(programEntities.userIndex.keys()) # 3391 用户数目
self.numFriends = np.zeros((nusers)) # array([0., 0., 0., ..., 0., 0., 0.]),保存每一个用户的朋友数
self.userFriends = ss.dok_matrix((nusers, nusers)) # 记录下每个用户的朋友点击事件的次数
fin = gzip.open('user_friends.csv')
fin.readline()
ln = 0
# 判断第一列的user是否在userIndex中,只有user在userIndex中才是我们关心的user
# 获取该用户的Index,和朋友数目
# 对于该用户的每一个朋友,如果朋友也在userIndex中,获取其朋友的userIndex,然后去userEventScores中获取该朋友对每个events的反应
# score即为该朋友对所有events的感兴趣度(平均分)
# userFriends矩阵记录了用户和朋友之间的score
for line in fin:
cols = line.decode().strip().split(',')
user = cols[0]
if user in programEntities.userIndex:
friends = cols[1].split(' ') # 获得该用户的朋友列表
i = programEntities.userIndex[user]
self.numFriends[i] = len(friends)
for friend in friends:
if friend in programEntities.userIndex:
j = programEntities.userIndex[friend]
eventsForUser = programEntities.userEventScores.getrow(j).todense() # 获取朋友对每个events的感兴趣程度:0, 1, or -1
# print(eventsForUser.sum(), np.shape(eventsForUser)[1] )
# socre即是用户朋友在13418个events上的平均分
score = eventsForUser.sum() / np.shape(eventsForUser)[1] # eventsForUser = 13418,
# print(score)
self.userFriends[i, j] += score
self.userFriends[j, i] += score
ln += 1
fin.close()
# 归一化数组
sumNumFriends = self.numFriends.sum(axis=0) # 每个用户的朋友数相加
#print(sumNumFriends)
self.numFriends = self.numFriends / sumNumFriends # 每个user的朋友数目比例
sio.mmwrite('UF_numFriends', np.matrix(self.numFriends)) # 将用户-朋友数矩阵保存
self.userFriends = normalize(self.userFriends, norm='l1', axis=0, copy=False)
sio.mmwrite('UF_userFriends', self.userFriends) # 将用户-朋友活动感兴趣度(打分)矩阵保存
UF_numFriends 是每个用户朋友的数目
UF_userFriends 是每个用户所有朋友对活动感兴趣程度的平均值 = 所有朋友对活动感兴趣度累加和(score和) / 朋友的数目
event相似度矩阵
event预处理
event = pd.read_csv("events.csv")
1)start_time
def getJoinedYearMonth(dateString):
try:
dttm = datetime.datetime.strptime(dateString, "%Y-%m-%dT%H:%M:%S.%fZ")
return "".join( [int(dttm.year), int(dttm.month)] )
except:
return 0
event['start_time'] = event['start_time'].map(getJoinedYearMonth)
2)lat和 lon列处理
# 显示高密度活动区域的散点图
event.plot(kind = "scatter", x = "lat", y = "lng", alpha = 0.4)
plt.legend()
plt.show()
event相似度矩阵
# 构造event和event相似度数据
class Events:
"""
构建event-event相似度,注意这里
是由event本身的内容(event信息)计算出的event-event相似度
"""
def __init__(self, programEntities, psim=ssd.correlation, csim=ssd.cosine):
nevents = len(programEntities.eventIndex) # 事件的数目
print(nevents) # 13418
self.eventPropMatrix = ss.dok_matrix((nevents, 7)) # 存储event-前7列特征
self.eventContMatrix = ss.dok_matrix((nevents, 100)) # 存储event关键词信息
self.eventPropSim = ss.dok_matrix((nevents, nevents))
self.eventContSim = ss.dok_matrix((nevents, nevents))
for e1, e2 in programEntities.uniqueEventPairs:
i = programEntities.eventIndex[e1]
j = programEntities.eventIndex[e2]
# 计算前7列数据的相识度
if not ((i, j) in self.eventPropSim):
epsim = psim(self.eventPropMatrix.getrow(i).todense(), self.eventPropMatrix.getrow(j).todense())
self.eventPropSim[i, j] = epsim
self.eventPropSim[j, i] = epsim
# 计算后面数据的相似度(有关关键词数据)
if not ((i, j) in self.eventContSim):
ecsim = csim(self.eventContMatrix.getrow(i).todense(), self.eventContMatrix.getrow(j).todense())
self.eventContSim[i, j] = ecsim
self.eventContSim[j, i] = ecsim
sio.mmwrite('EV_eventPropSim', self.eventPropSim)
sio.mmwrite('EV_eventContSim', self.eventContSim)
event热度数据
event_attendees.csv包含有关哪些用户参加了各种事件的信息,并包含以下列: event_id,yes,maybe,invite和no。
event_id:活动id
yes:会参加的用户
maybe:可能参加的用户
invite:邀请的用户
no:不会参加的用户
class EventAttendees:
"""
统计某个活动,参加和不参加的人数,计算活动热度
"""
def __init__(self, programEntities):
nevents = len(programEntities.eventIndex)#13418 事件的总数
self.eventPopularity = ss.dok_matrix( (nevents, 1) )
f = gzip.open('event_attendees.csv.gz')
f.readline()#skip header
for line in f:
cols = line.decode().strip().split(',')
eventId = cols[0]
if eventId in programEntities.eventIndex:
i = programEntities.eventIndex[eventId]
self.eventPopularity[i, 0] = len(cols[1].split(' ')) - len(cols[4].split(' '))#yes人数-no人数,即出席人数减未出席人数
f.close()
self.eventPopularity = normalize( self.eventPopularity, norm='l1', axis=0, copy=False)
sio.mmwrite('EA_eventPopularity', self.eventPopularity)
特征构建
class DataRewriter:
def __init__(self):
# 读入数据做初始化
self.userIndex = pickle.load(open('PE_userIndex.pkl', 'rb')) #训练集测试集中的user
self.eventIndex = pickle.load(open('PE_eventIndex.pkl', 'rb')) #训练集测试集中的event
self.userEventScores = sio.mmread('PE_userEventScores').todense() #用户对event 的评分矩阵
self.userSimMatrix = sio.mmread('US_userSimMatrix').todense() #用户(注册信息)相似度矩阵
self.eventPropSim = sio.mmread('EV_eventPropSim').todense() #event相似度矩阵
self.eventContSim = sio.mmread('EV_eventContSim').todense() #event 关键词相似度矩阵
self.numFriends = sio.mmread('UF_numFriends') #用户朋友数量
self.userFriends = sio.mmread('UF_userFriends').todense() #用户朋友对event热衷度
self.eventPopularity = sio.mmread('EA_eventPopularity').todense() #活动热度
def userReco(self, userId, eventId):
"""
基于用户的协同过滤——UserCF协同过滤,得到event的推荐度
"""
i = self.userIndex[userId]
j = self.eventIndex[eventId]
vs = self.userEventScores[:, j]
sims = self.userSimMatrix[i, :]
prod = sims * vs
try:
return prod[0, 0] - self.userEventScores[i, j]
except IndexError:
return 0
def eventReco(self, userId, eventId):
"""
根据基于event的协同过滤————itemCF,得到Event的推荐度
"""
i = self.userIndex[userId]
j = self.eventIndex[eventId]
js = self.userEventScores[i, :]
psim = self.eventPropSim[:, j]
csim = self.eventContSim[:, j]
pprod = js * psim
cprod = js * csim
pscore = 0
cscore = 0
try:
pscore = pprod[0, 0] - self.userEventScores[i, j]
except IndexError:
pass
try:
cscore = cprod[0, 0] - self.userEventScores[i, j]
except IndexError:
pass
return pscore, cscore
def userPop(self, userId):
"""
基于用户的朋友个数来推断用户的社交程度
主要的考量是如果用户的朋友非常多,可能会更倾向于参加各种社交活动
"""
if userId in self.userIndex:
i = self.userIndex[userId]
try:
return self.numFriends[0, i]
except IndexError:
return 0
else:
return 0
def friendInfluence(self, userId):
"""
朋友对用户的影响
主要考虑用户的所有朋友中,有多少是非常喜欢参加各种社交活动(event)的
用户的朋友圈如果都是积极参加各种event,可能会对当前用户有一定的影响
"""
nusers = np.shape(self.userFriends)[1]
i = self.userIndex[userId]
return (self.userFriends[i, :].sum(axis=0) / nusers)[0, 0]
def eventPop(self, eventId):
"""
活动本身的热度
主要通过参与的参数来界定的
"""
i = self.eventIndex[eventId]
return self.eventPopularity[i, 0]
def rewriteData(self, start=1, train=True, header=True):
"""
把前面user-based协同过滤和item-based协同过滤以及各种热度和影响度作为特征组合在一起
生成新的train,用于分类器分类使用
"""
fn = 'train.csv' if train else 'test.csv'
fin = open(fn)
fout = open('data_' + fn, 'w')
# write output header
if header:
ocolnames = ['invited', 'user_reco', 'evt_p_reco', 'evt_c_reco', 'user_pop', 'frnd_infl', 'evt_pop']
#新的特征:是否受邀请、UserCF推荐度、itemCF推荐度、用户受欢迎度、朋友对event热衷度、活动热度
if train:
ocolnames.append('interested')
ocolnames.append('not_interested')
fout.write(','.join(ocolnames) + '\n')
ln = 0
for line in fin:
ln += 1
if ln < start:
continue
cols = line.strip().split(',')
# user,event,invited,timestamp,interested,not_interested
userId = cols[0]
eventId = cols[1]
invited = cols[2]
if ln % 500 == 0:
print("%s : %d (userId, eventId) = (%s, %s)" % (fn, ln, userId, eventId))
user_reco = self.userReco(userId, eventId)
evt_p_reco, evt_c_reco = self.eventReco(userId, eventId)
user_pop = self.userPop(userId)
frnd_infl = self.friendInfluence(userId)
evt_pop = self.eventPop(eventId)
ocols = [invited, user_reco, evt_p_reco, evt_c_reco, user_pop, frnd_infl, evt_pop]
if train:
ocols.append(cols[4]) # interested
ocols.append(cols[5]) # not_interested
fout.write(','.join(map(lambda x: str(x), ocols)) + '\n')
fin.close()
fout.close()
def rewriteTrainingSet(self):
self.rewriteData(True)
def rewriteTestSet(self):
self.rewriteData(False)
dr = DataRewriter()
print('生成训练数据...\n')
dr.rewriteData(train=True, start=2, header=True)
print('生成预测数据...\n')
dr.rewriteData(train=False, start=2, header=True)
print('done')
模型构建与预测
def train():
"""
在我们得到的特征上训练分类器,target为1(感兴趣),或者是0(不感兴趣)
"""
trainDf = pd.read_csv('data_train.csv')
X = np.matrix( pd.DataFrame(trainDf, index=None, columns=['invited', 'user_reco', 'evt_p_reco',
'evt_c_reco','user_pop', 'frnd_infl', 'evt_pop']) )
y = np.array(trainDf.interested)
clf = SGDClassifier(loss='log', penalty='l2')
clf.fit(X, y)
return clf
def validate():
"""
10折的交叉验证,并输出交叉验证的平均准确率
"""
trainDf = pd.read_csv('data_train.csv')
X = np.matrix(pd.DataFrame(trainDf, index=None, columns=['invited', 'user_reco', 'evt_p_reco',
'evt_c_reco','user_pop', 'frnd_infl', 'evt_pop']) )
y = np.array(trainDf.interested)
nrows = len(trainDf)
kfold = KFold(n_splits=10,shuffle=False)
avgAccuracy = 0
run = 0
for train, test in kfold.split(X, y):
Xtrain, Xtest, ytrain, ytest = X[train], X[test], y[train], y[test]
clf = SGDClassifier(loss='log', 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 = accuracy / ntest
print('accuracy(run %d) : %f' % (run, accuracy) )
def test(clf):
"""
读取test数据,用分类器完成预测
"""
origTestDf = pd.read_csv("test.csv")
users = origTestDf.user
events = origTestDf.event
testDf = pd.read_csv("data_test.csv")
fout = open("result.csv", 'w')
fout.write(",".join(["user", "event", "outcome", "dist"]) + "\n")
nrows = len(testDf)
Xp = np.matrix(testDf)
yp = np.zeros((nrows, 2))
for i in range(0, nrows):
xp = Xp[i, :]
yp[i, 0] = clf.predict(xp)
yp[i, 1] = clf.decision_function(xp)
fout.write(",".join( map( lambda x: str(x), [users[i], events[i], yp[i, 0], yp[i, 1]] ) ) + "\n")
fout.close()
clf = train()
validate()
test(clf)
print('done')
参考链接
链接:
mAP(mean average precision)平均精度均值
collections.defaultdict()的使用