咱们制做特征和标签,将推荐问题转成监督学习问题。
咱们先回顾一下现有数据,有哪些特征能够直接利用:html
上面这些直接能够用的特征, 待作完特征工程以后, 直接就能够根据article_id或者是user_id把这些特征加入进去。 可是咱们须要先基于召回的结果,构造一些特征,而后制做标签,造成一个监督学习的数据集。python
构造监督数据集的思路, 根据召回结果, 咱们会获得一个{user_id: [可能点击的文章列表]}形式的字典。 那么咱们就能够对于每一个用户, 每篇可能点击的文章构造一个监督测试集, 好比对于用户user1, 假设获得的他的召回列表{user1: [item1, item2, item3]}, 咱们就能够获得三行数据(user1, item1), (user1, item2), (user1, item3)的形式, 这就是监督测试集时候的前两列特征。算法
构造特征的思路是这样, 咱们知道每一个用户的点击文章是与其历史点击的文章信息是有很大关联的, 好比同一个主题, 类似等等。 因此特征构造这块很重要的一系列特征是要结合用户的历史点击文章信息。咱们已经获得了每一个用户及点击候选文章的两列的一个数据集, 而咱们的目的是要预测最后一次点击的文章, 比较天然的一个思路就是和其最后几回点击的文章产生关系, 这样既考虑了其历史点击文章信息, 又得离最后一次点击较近,由于新闻很大的一个特色就是注重时效性。 每每用户的最后一次点击会和其最后几回点击有很大的关联。 因此咱们就能够对于每一个候选文章, 作出与最后几回点击相关的特征以下:数组
还须要考虑一下
5. 若是使用了youtube召回的话, 咱们还能够制做用户与候选item的类似特征app
固然, 上面只是提供了一种基于用户历史行为作特征工程的思路, 你们也能够思惟风暴一下,尝试一些其余的特征。 下面咱们就实现上面的这些特征的制做, 下面的逻辑是这样:dom
好了,废话很少说机器学习
import numpy as np import pandas as pd import pickle from tqdm import tqdm import gc, os import logging import time import lightgbm as lgb from gensim.models import Word2Vec from sklearn.preprocessing import MinMaxScaler import warnings warnings.filterwarnings('ignore')
数据存放位置以及结果输出位置函数
data_dir = './data' save_dir = './results'
划分训练和验证集的缘由是为了在线下验证模型参数的好坏,为了彻底模拟测试集,咱们这里就在训练集中抽取部分用户的全部信息来做为验证集。提早作训练验证集划分的好处就是能够分解制做排序特征时的压力,一次性作整个数据集的排序特征可能时间会比较长。学习
# all_click_df指的是训练集 # sample_user_nums 采样做为验证集的用户数量 def trn_val_split(all_click_df, sample_user_nums): all_click = all_click_df all_user_ids = all_click.user_id.unique() # replace=True表示能够重复抽样,反之不能够 sample_user_ids = np.random.choice(all_user_ids, size=sample_user_nums, replace=False) click_val = all_click[all_click['user_id'].isin(sample_user_ids)] click_trn = all_click[~all_click['user_id'].isin(sample_user_ids)] # 将验证集中的最后一次点击给抽取出来做为答案 click_val = click_val.sort_values(['user_id', 'click_timestamp']) val_ans = click_val.groupby('user_id').tail(1) click_val = click_val.groupby('user_id').apply(lambda x: x[:-1]).reset_index(drop=True) # 去除val_ans中某些用户只有一个点击数据的状况,若是该用户只有一个点击数据,又被分到ans中, # 那么训练集中就没有这个用户的点击数据,出现用户冷启动问题,给本身模型验证带来麻烦 val_ans = val_ans[val_ans.user_id.isin(click_val.user_id.unique())] # 保证答案中出现的用户再验证集中还有 click_val = click_val[click_val.user_id.isin(val_ans.user_id.unique())] return click_trn, click_val, val_ans
# 获取当前数据的历史点击和最后一次点击 def get_hist_and_last_click(all_click): all_click = all_click.sort_values(by=['user_id', 'click_timestamp']) click_last_df = all_click.groupby('user_id').tail(1) # 若是用户只有一个点击,hist为空了,会致使训练的时候这个用户不可见,此时默认泄露一下 def hist_func(user_df): if len(user_df) == 1: return user_df else: return user_df[:-1] click_hist_df = all_click.groupby('user_id').apply(hist_func).reset_index(drop=True) return click_hist_df, click_last_df
def get_trn_val_tst_data(data_path, offline=True): if offline: click_trn_data = pd.read_csv(data_path+'train_click_log.csv') # 训练集用户点击日志 click_trn_data = reduce_mem(click_trn_data) click_trn, click_val, val_ans = trn_val_split(click_trn_data , sample_user_nums) else: click_trn = pd.read_csv(data_path+'train_click_log.csv') click_trn = reduce_mem(click_trn) click_val = None val_ans = None click_tst = pd.read_csv(data_path+'testA_click_log.csv') return click_trn, click_val, click_tst, val_ans
# 返回多路召回列表或者单路召回 def get_recall_list(save_path, single_recall_model=None, multi_recall=False): if multi_recall: return pickle.load(open(save_path + 'final_recall_items_dict.pkl', 'rb')) if single_recall_model == 'i2i_itemcf': return pickle.load(open(save_path + 'itemcf_recall_dict.pkl', 'rb')) elif single_recall_model == 'i2i_emb_itemcf': return pickle.load(open(save_path + 'itemcf_emb_dict.pkl', 'rb')) elif single_recall_model == 'user_cf': return pickle.load(open(save_path + 'youtubednn_usercf_dict.pkl', 'rb')) elif single_recall_model == 'youtubednn': return pickle.load(open(save_path + 'youtube_u2i_dict.pkl', 'rb'))
Word2Vec主要思想是:一个词的上下文能够很好的表达出词的语义。经过无监督学习产生词向量的方式。word2vec中有两个很是经典的模型:skip-gram和cbow。测试
在使用gensim训练word2vec的时候,有几个比较重要的参数
注意
Word2Vec??
查看下面是个简单的测试样例:
from gensim.models import Word2Vec doc = [['30760', '157507'], ['289197', '63746'], ['36162', '168401'], ['50644', '36162']] w2v = Word2Vec(docs, size=12, sg=1, window=2, seed=2020, workers=2, min_count=1, iter=1) # 查看'30760'表示的词向量 w2v['30760']
skip-gram和cbow的详细原理能够参考下面的博客:
def trian_item_word2vec(click_df, embed_size=64, save_name='item_w2v_emb.pkl', split_char=' '): click_df = click_df.sort_values('click_timestamp') # 只有转换成字符串才能够进行训练 click_df['click_article_id'] = click_df['click_article_id'].astype(str) # 转换成句子的形式 docs = click_df.groupby(['user_id'])['click_article_id'].apply(lambda x: list(x)).reset_index() docs = docs['click_article_id'].values.tolist() # 为了方便查看训练的进度,这里设定一个log信息 logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.INFO) # 这里的参数对训练获得的向量影响也很大,默认负采样为5 w2v = Word2Vec(docs, size=16, sg=1, window=5, seed=2020, workers=24, min_count=1, iter=1) # 保存成字典的形式 item_w2v_emb_dict = {k: w2v[k] for k in click_df['click_article_id']} pickle.dump(item_w2v_emb_dict, open(save_path + 'item_w2v_emb.pkl', 'wb')) return item_w2v_emb_dict
# 能够经过字典查询对应的item的Embedding def get_embedding(save_path, all_click_df): if os.path.exists(save_path + 'item_content_emb.pkl'): item_content_emb_dict = pickle.load(open(save_path + 'item_content_emb.pkl', 'rb')) else: print('item_content_emb.pkl 文件不存在...') # w2v Embedding是须要提早训练好的 if os.path.exists(save_path + 'item_w2v_emb.pkl'): item_w2v_emb_dict = pickle.load(open(save_path + 'item_w2v_emb.pkl', 'rb')) else: item_w2v_emb_dict = trian_item_word2vec(all_click_df) if os.path.exists(save_path + 'item_youtube_emb.pkl'): item_youtube_emb_dict = pickle.load(open(save_path + 'item_youtube_emb.pkl', 'rb')) else: print('item_youtube_emb.pkl 文件不存在...') if os.path.exists(save_path + 'user_youtube_emb.pkl'): user_youtube_emb_dict = pickle.load(open(save_path + 'user_youtube_emb.pkl', 'rb')) else: print('user_youtube_emb.pkl 文件不存在...') return item_content_emb_dict, item_w2v_emb_dict, item_youtube_emb_dict, user_youtube_emb_dict
def get_article_info_df(): article_info_df = pd.read_csv(data_path + 'articles.csv') article_info_df = reduce_mem(article_info_df) return article_info_df
# 这里offline的online的区别就是验证集是否为空 click_trn, click_val, click_tst, val_ans = get_trn_val_tst_data(data_path, offline=False) click_trn_hist, click_trn_last = get_hist_and_last_click(click_trn) if click_val is not None: click_val_hist, click_val_last = click_val, val_ans else: click_val_hist, click_val_last = None, None click_tst_hist = click_tst
经过召回咱们将数据转换成三元组的形式(user1, item1, label)的形式,观察发现正负样本差距极度不平衡,咱们能够先对负样本进行下采样,下采样的目的一方面缓解了正负样本比例的问题,另外一方面也减少了咱们作排序特征的压力,咱们在作负采样的时候又有哪些东西是须要注意的呢?
其实负采样也能够留在后面作完特征在进行,这里因为作排序特征太慢了,因此把负采样的环节提到前面了。
# 将召回列表转换成df的形式 def recall_dict_2_df(recall_list_dict): df_row_list = [] # [user, item, score] for user, recall_list in tqdm(recall_list_dict.items()): for item, score in recall_list: df_row_list.append([user, item, score]) col_names = ['user_id', 'sim_item', 'score'] recall_list_df = pd.DataFrame(df_row_list, columns=col_names) return recall_list_df
# 负采样函数,这里能够控制负采样时的比例, 这里给了一个默认的值 def neg_sample_recall_data(recall_items_df, sample_rate=0.001): pos_data = recall_items_df[recall_items_df['label'] == 1] neg_data = recall_items_df[recall_items_df['label'] == 0] print('pos_data_num:', len(pos_data), 'neg_data_num:', len(neg_data), 'pos/neg:', len(pos_data)/len(neg_data)) # 分组采样函数 def neg_sample_func(group_df): neg_num = len(group_df) sample_num = max(int(neg_num * sample_rate), 1) # 保证最少有一个 sample_num = min(sample_num, 5) # 保证最多不超过5个,这里能够根据实际状况进行选择 return group_df.sample(n=sample_num, replace=True) # 对用户进行负采样,保证全部用户都在采样后的数据中 neg_data_user_sample = neg_data.groupby('user_id', group_keys=False).apply(neg_sample_func) # 对文章进行负采样,保证全部文章都在采样后的数据中 neg_data_item_sample = neg_data.groupby('sim_item', group_keys=False).apply(neg_sample_func) # 将上述两种状况下的采样数据合并 neg_data_new = neg_data_user_sample.append(neg_data_item_sample) # 因为上述两个操做是分开的,可能将两个相同的数据给重复选择了,因此须要对合并后的数据进行去重 neg_data_new = neg_data_new.sort_values(['user_id', 'score']).drop_duplicates(['user_id', 'sim_item'], keep='last') # 将正样本数据合并 data_new = pd.concat([pos_data, neg_data_new], ignore_index=True) return data_new
# 召回数据打标签 def get_rank_label_df(recall_list_df, label_df, is_test=False): # 测试集是没有标签了,为了后面代码同一一些,这里直接给一个负数替代 if is_test: recall_list_df['label'] = -1 return recall_list_df label_df = label_df.rename(columns={'click_article_id': 'sim_item'}) recall_list_df_ = recall_list_df.merge(label_df[['user_id', 'sim_item', 'click_timestamp']], \ how='left', on=['user_id', 'sim_item']) recall_list_df_['label'] = recall_list_df_['click_timestamp'].apply(lambda x: 0.0 if np.isnan(x) else 1.0) del recall_list_df_['click_timestamp'] return recall_list_df_
def get_user_recall_item_label_df(click_trn_hist, click_val_hist, click_tst_hist,click_trn_last, click_val_last, recall_list_df): # 获取训练数据的召回列表 trn_user_items_df = recall_list_df[recall_list_df['user_id'].isin(click_trn_hist['user_id'].unique())] # 训练数据打标签 trn_user_item_label_df = get_rank_label_df(trn_user_items_df, click_trn_last, is_test=False) # 训练数据负采样 trn_user_item_label_df = neg_sample_recall_data(trn_user_item_label_df) if click_val is not None: val_user_items_df = recall_list_df[recall_list_df['user_id'].isin(click_val_hist['user_id'].unique())] val_user_item_label_df = get_rank_label_df(val_user_items_df, click_val_last, is_test=False) val_user_item_label_df = neg_sample_recall_data(val_user_item_label_df) else: val_user_item_label_df = None # 测试数据不须要进行负采样,直接对全部的召回商品进行打-1标签 tst_user_items_df = recall_list_df[recall_list_df['user_id'].isin(click_tst_hist['user_id'].unique())] tst_user_item_label_df = get_rank_label_df(tst_user_items_df, None, is_test=True) return trn_user_item_label_df, val_user_item_label_df, tst_user_item_label_df # 读取召回列表 recall_list_dict = get_recall_list(save_path, single_recall_model='i2i_itemcf') # 这里只选择了单路召回的结果,也能够选择多路召回结果 # 将召回数据转换成df recall_list_df = recall_dict_2_df(recall_list_dict)
# 给训练验证数据打标签,并负采样(这一部分时间比较久) trn_user_item_label_df, val_user_item_label_df, tst_user_item_label_df = get_user_recall_item_label_df(click_trn_hist, click_val_hist, click_tst_hist, click_trn_last, click_val_last, recall_list_df)
# 将最终的召回的df数据转换成字典的形式作排序特征 def make_tuple_func(group_df): row_data = [] for name, row_df in group_df.iterrows(): row_data.append((row_df['sim_item'], row_df['score'], row_df['label'])) return row_data
trn_user_item_label_tuples = trn_user_item_label_df.groupby('user_id').apply(make_tuple_func).reset_index() trn_user_item_label_tuples_dict = dict(zip(trn_user_item_label_tuples['user_id'], trn_user_item_label_tuples[0])) if val_user_item_label_df is not None: val_user_item_label_tuples = val_user_item_label_df.groupby('user_id').apply(make_tuple_func).reset_index() val_user_item_label_tuples_dict = dict(zip(val_user_item_label_tuples['user_id'], val_user_item_label_tuples[0])) else: val_user_item_label_tuples_dict = None tst_user_item_label_tuples = tst_user_item_label_df.groupby('user_id').apply(make_tuple_func).reset_index() tst_user_item_label_tuples_dict = dict(zip(tst_user_item_label_tuples['user_id'], tst_user_item_label_tuples[0]))
对于每一个用户召回的每一个商品, 作特征。 具体步骤以下:
对于每一个用户, 获取最后点击的N个商品的item_id,
# 下面基于data作历史相关的特征 def create_feature(users_id, recall_list, click_hist_df, articles_info, articles_emb, user_emb=None, N=1): """ 基于用户的历史行为作相关特征 :param users_id: 用户id :param recall_list: 对于每一个用户召回的候选文章列表 :param click_hist_df: 用户的历史点击信息 :param articles_info: 文章信息 :param articles_emb: 文章的embedding向量, 这个能够用item_content_emb, item_w2v_emb, item_youtube_emb :param user_emb: 用户的embedding向量, 这个是user_youtube_emb, 若是没有也能够不用, 但要注意若是要用的话, articles_emb就要用item_youtube_emb的形式, 这样维度才同样 :param N: 最近的N次点击 因为testA日志里面不少用户只存在一次历史点击, 因此为了避免产生空值,默认是1 """ # 创建一个二维列表保存结果, 后面要转成DataFrame all_user_feas = [] i = 0 for user_id in tqdm(users_id): # 该用户的最后N次点击 hist_user_items = click_hist_df[click_hist_df['user_id']==user_id]['click_article_id'][-N:] # 遍历该用户的召回列表 for rank, (article_id, score, label) in enumerate(recall_list[user_id]): # 该文章创建时间, 字数 a_create_time = articles_info[articles_info['article_id']==article_id]['created_at_ts'].values[0] a_words_count = articles_info[articles_info['article_id']==article_id]['words_count'].values[0] single_user_fea = [user_id, article_id] # 计算与最后点击的商品的类似度的和, 最大值和最小值, 均值 sim_fea = [] time_fea = [] word_fea = [] # 遍历用户的最后N次点击文章 for hist_item in hist_user_items: b_create_time = articles_info[articles_info['article_id']==hist_item]['created_at_ts'].values[0] b_words_count = articles_info[articles_info['article_id']==hist_item]['words_count'].values[0] sim_fea.append(np.dot(articles_emb[hist_item], articles_emb[article_id])) time_fea.append(abs(a_create_time-b_create_time)) word_fea.append(abs(a_words_count-b_words_count)) single_user_fea.extend(sim_fea) # 类似性特征 single_user_fea.extend(time_fea) # 时间差特征 single_user_fea.extend(word_fea) # 字数差特征 single_user_fea.extend([max(sim_fea), min(sim_fea), sum(sim_fea), sum(sim_fea) / len(sim_fea)]) # 类似性的统计特征 if user_emb: # 若是用户向量有的话, 这里计算该召回文章与用户的类似性特征 single_user_fea.append(np.dot(user_emb[user_id], articles_emb[article_id])) single_user_fea.extend([score, rank, label]) # 加入到总的表中 all_user_feas.append(single_user_fea) # 定义列名 id_cols = ['user_id', 'click_article_id'] sim_cols = ['sim' + str(i) for i in range(N)] time_cols = ['time_diff' + str(i) for i in range(N)] word_cols = ['word_diff' + str(i) for i in range(N)] sat_cols = ['sim_max', 'sim_min', 'sim_sum', 'sim_mean'] user_item_sim_cols = ['user_item_sim'] if user_emb else [] user_score_rank_label = ['score', 'rank', 'label'] cols = id_cols + sim_cols + time_cols + word_cols + sat_cols + user_item_sim_cols + user_score_rank_label # 转成DataFrame df = pd.DataFrame( all_user_feas, columns=cols) return df
article_info_df = get_article_info_df() all_click = click_trn.append(click_tst) item_content_emb_dict, item_w2v_emb_dict, item_youtube_emb_dict, user_youtube_emb_dict = get_embedding(save_path, all_click)
# 获取训练验证及测试数据中召回列文章相关特征 trn_user_item_feats_df = create_feature(trn_user_item_label_tuples_dict.keys(), trn_user_item_label_tuples_dict, \ click_trn_hist, article_info_df, item_content_emb_dict) if val_user_item_label_tuples_dict is not None: val_user_item_feats_df = create_feature(val_user_item_label_tuples_dict.keys(), val_user_item_label_tuples_dict, \ click_val_hist, article_info_df, item_content_emb_dict) else: val_user_item_feats_df = None tst_user_item_feats_df = create_feature(tst_user_item_label_tuples_dict.keys(), tst_user_item_label_tuples_dict, \ click_tst_hist, article_info_df, item_content_emb_dict)
# 保存一份省的每次都要从新跑,每次跑的时间都比较长 trn_user_item_feats_df.to_csv(save_path + 'trn_user_item_feats_df.csv', index=False) if val_user_item_feats_df is not None: val_user_item_feats_df.to_csv(save_path + 'val_user_item_feats_df.csv', index=False) tst_user_item_feats_df.to_csv(save_path + 'tst_user_item_feats_df.csv', index=False)
这一块,正式进行特征工程,既要拼接上已有的特征, 也会作更多的特征出来,咱们来梳理一下已有的特征和可构造特征:
对于用户和商品还能够构造的特征:
# 读取文章特征 articles = pd.read_csv(data_path+'articles.csv') articles = reduce_mem(articles) # 日志数据,就是前面的全部数据 if click_val is not None: all_data = click_trn.append(click_val) all_data = click_trn.append(click_tst) all_data = reduce_mem(all_data) # 拼上文章信息 all_data = all_data.merge(articles, left_on='click_article_id', right_on='article_id')
分析一下点击时间和点击文章的次数,区分用户活跃度
若是某个用户点击文章之间的时间间隔比较小, 同时点击的文章次数不少的话, 那么咱们认为这种用户通常就是活跃用户, 固然衡量用户活跃度的方式可能多种多样, 这里咱们只提供其中一种,咱们写一个函数, 获得能够衡量用户活跃度的特征,逻辑以下:
首先根据用户user_id分组, 对于每一个用户,计算点击文章的次数, 两两点击文章时间间隔的均值
把点击次数取倒数和时间间隔的均值统一归一化,而后二者相加合并,该值越小, 说明用户越活跃
注意, 上面两两点击文章的时间间隔均值, 会出现若是用户只点击了一次的状况,这时候时间间隔均值那里会出现空值, 对于这种状况最后特征那里给个大数进行区分
这个的衡量标准就是先把点击的次数取到数而后归一化, 而后点击的时间差归一化, 而后二者相加进行合并, 该值越小, 说明被点击的次数越多, 且间隔时间短。
def active_level(all_data, cols): """ 制做区分用户活跃度的特征 :param all_data: 数据集 :param cols: 用到的特征列 """ data = all_data[cols] data.sort_values(['user_id', 'click_timestamp'], inplace=True) user_act = pd.DataFrame(data.groupby('user_id', as_index=False)[['click_article_id', 'click_timestamp']].\ agg({'click_article_id':np.size, 'click_timestamp': {list}}).values, columns=['user_id', 'click_size', 'click_timestamp']) # 计算时间间隔的均值 def time_diff_mean(l): if len(l) == 1: return 1 else: return np.mean([j-i for i, j in list(zip(l[:-1], l[1:]))]) user_act['time_diff_mean'] = user_act['click_timestamp'].apply(lambda x: time_diff_mean(x)) # 点击次数取倒数 user_act['click_size'] = 1 / user_act['click_size'] # 二者归一化 user_act['click_size'] = (user_act['click_size'] - user_act['click_size'].min()) / (user_act['click_size'].max() - user_act['click_size'].min()) user_act['time_diff_mean'] = (user_act['time_diff_mean'] - user_act['time_diff_mean'].min()) / (user_act['time_diff_mean'].max() - user_act['time_diff_mean'].min()) user_act['active_level'] = user_act['click_size'] + user_act['time_diff_mean'] user_act['user_id'] = user_act['user_id'].astype('int') del user_act['click_timestamp'] return user_act
user_act_fea = active_level(all_data, ['user_id', 'click_article_id', 'click_timestamp'])
和上面一样的思路, 若是一篇文章在很短的时间间隔以内被点击了不少次, 说明文章比较热门,实现的逻辑和上面的基本一致, 只不过这里是按照点击的文章进行分组:
def hot_level(all_data, cols): """ 制做衡量文章热度的特征 :param all_data: 数据集 :param cols: 用到的特征列 """ data = all_data[cols] data.sort_values(['click_article_id', 'click_timestamp'], inplace=True) article_hot = pd.DataFrame(data.groupby('click_article_id', as_index=False)[['user_id', 'click_timestamp']].\ agg({'user_id':np.size, 'click_timestamp': {list}}).values, columns=['click_article_id', 'user_num', 'click_timestamp']) # 计算被点击时间间隔的均值 def time_diff_mean(l): if len(l) == 1: return 1 else: return np.mean([j-i for i, j in list(zip(l[:-1], l[1:]))]) article_hot['time_diff_mean'] = article_hot['click_timestamp'].apply(lambda x: time_diff_mean(x)) # 点击次数取倒数 article_hot['user_num'] = 1 / article_hot['user_num'] # 二者归一化 article_hot['user_num'] = (article_hot['user_num'] - article_hot['user_num'].min()) / (article_hot['user_num'].max() - article_hot['user_num'].min()) article_hot['time_diff_mean'] = (article_hot['time_diff_mean'] - article_hot['time_diff_mean'].min()) / (article_hot['time_diff_mean'].max() - article_hot['time_diff_mean'].min()) article_hot['hot_level'] = article_hot['user_num'] + article_hot['time_diff_mean'] article_hot['click_article_id'] = article_hot['click_article_id'].astype('int') del article_hot['click_timestamp'] return article_hot
article_hot_fea = hot_level(all_data, ['user_id', 'click_article_id', 'click_timestamp'])
这个基于原来的日志表作一个相似于article的那种DataFrame, 存放用户特有的信息, 主要包括点击习惯, 爱好特征之类的
这些就是对用户进行分组, 而后统计便可
def device_fea(all_data, cols): """ 制做用户的设备特征 :param all_data: 数据集 :param cols: 用到的特征列 """ user_device_info = all_data[cols] # 用众数来表示每一个用户的设备信息 user_device_info = user_device_info.groupby('user_id').agg(lambda x: x.value_counts().index[0]).reset_index() return user_device_info # 设备特征(这里时间会比较长) device_cols = ['user_id', 'click_environment', 'click_deviceGroup', 'click_os', 'click_country', 'click_region', 'click_referrer_type'] user_device_info = device_fea(all_data, device_cols)
def user_time_hob_fea(all_data, cols): """ 制做用户的时间习惯特征 :param all_data: 数据集 :param cols: 用到的特征列 """ user_time_hob_info = all_data[cols] # 先把时间戳进行归一化 mm = MinMaxScaler() user_time_hob_info['click_timestamp'] = mm.fit_transform(user_time_hob_info[['click_timestamp']]) user_time_hob_info['created_at_ts'] = mm.fit_transform(user_time_hob_info[['created_at_ts']]) user_time_hob_info = user_time_hob_info.groupby('user_id').agg('mean').reset_index() user_time_hob_info.rename(columns={'click_timestamp': 'user_time_hob1', 'created_at_ts': 'user_time_hob2'}, inplace=True) return user_time_hob_info user_time_hob_cols = ['user_id', 'click_timestamp', 'created_at_ts'] user_time_hob_info = user_time_hob_fea(all_data, user_time_hob_cols)
这里先把用户点击的文章属于的主题转成一个列表, 后面再总的汇总的时候单独制做一个特征, 就是文章的主题若是属于这里面, 就是1, 不然就是0。
def user_cat_hob_fea(all_data, cols): """ 用户的主题爱好 :param all_data: 数据集 :param cols: 用到的特征列 """ user_category_hob_info = all_data[cols] user_category_hob_info = user_category_hob_info.groupby('user_id').agg({list}).reset_index() user_cat_hob_info = pd.DataFrame() user_cat_hob_info['user_id'] = user_category_hob_info['user_id'] user_cat_hob_info['cate_list'] = user_category_hob_info['category_id'] return user_cat_hob_info user_category_hob_cols = ['user_id', 'category_id'] user_cat_hob_info = user_cat_hob_fea(all_data, user_category_hob_cols)
user_wcou_info = all_data.groupby('user_id')['words_count'].agg('mean').reset_index() user_wcou_info.rename(columns={'words_count': 'words_hbo'}, inplace=True)
# 全部表进行合并 user_info = pd.merge(user_act_fea, user_device_info, on='user_id') user_info = user_info.merge(user_time_hob_info, on='user_id') user_info = user_info.merge(user_cat_hob_info, on='user_id') user_info = user_info.merge(user_wcou_info, on='user_id')
# 这样用户特征之后就能够直接读取了 user_info.to_csv(save_path + 'user_info.csv', index=False)
若是前面关于用户的特征工程已经给作完了,后面能够直接读取
# 把用户信息直接读入进来 user_info = pd.read_csv(save_path + 'user_info.csv')
if os.path.exists(save_path + 'trn_user_item_feats_df.csv'): trn_user_item_feats_df = pd.read_csv(save_path + 'trn_user_item_feats_df.csv') if os.path.exists(save_path + 'tst_user_item_feats_df.csv'): tst_user_item_feats_df = pd.read_csv(save_path + 'tst_user_item_feats_df.csv') if os.path.exists(save_path + 'val_user_item_feats_df.csv'): val_user_item_feats_df = pd.read_csv(save_path + 'val_user_item_feats_df.csv') else: val_user_item_feats_df = None
# 拼上用户特征 # 下面是线下验证的 trn_user_item_feats_df = trn_user_item_feats_df.merge(user_info, on='user_id', how='left') if val_user_item_feats_df is not None: val_user_item_feats_df = val_user_item_feats_df.merge(user_info, on='user_id', how='left') else: val_user_item_feats_df = None tst_user_item_feats_df = tst_user_item_feats_df.merge(user_info, on='user_id',how='left')
articles = pd.read_csv(data_path+'articles.csv') articles = reduce_mem(articles)
# 拼上文章特征 trn_user_item_feats_df = trn_user_item_feats_df.merge(articles, left_on='click_article_id', right_on='article_id') if val_user_item_feats_df is not None: val_user_item_feats_df = val_user_item_feats_df.merge(articles, left_on='click_article_id', right_on='article_id') else: val_user_item_feats_df = None tst_user_item_feats_df = tst_user_item_feats_df.merge(articles, left_on='click_article_id', right_on='article_id')
trn_user_item_feats_df['is_cat_hab'] = trn_user_item_feats_df.apply(lambda x: 1 if x.category_id in set(x.cate_list) else 0, axis=1) if val_user_item_feats_df is not None: val_user_item_feats_df['is_cat_hab'] = val_user_item_feats_df.apply(lambda x: 1 if x.category_id in set(x.cate_list) else 0, axis=1) else: val_user_item_feats_df = None tst_user_item_feats_df['is_cat_hab'] = tst_user_item_feats_df.apply(lambda x: 1 if x.category_id in set(x.cate_list) else 0, axis=1)
# 线下验证 del trn_user_item_feats_df['cate_list'] if val_user_item_feats_df is not None: del val_user_item_feats_df['cate_list'] else: val_user_item_feats_df = None del tst_user_item_feats_df['cate_list'] del trn_user_item_feats_df['article_id'] if val_user_item_feats_df is not None: del val_user_item_feats_df['article_id'] else: val_user_item_feats_df = None del tst_user_item_feats_df['article_id']
特征工程和数据清洗转换是比赛中相当重要的一块, 由于数据和特征决定了机器学习的上限,而算法和模型只是逼近这个上限而已,因此特征工程的好坏每每决定着最后的结果,特征工程能够一步加强数据的表达能力,经过构造新特征,咱们能够挖掘出数据的更多信息,使得数据的表达能力进一步放大。 在本节内容中,咱们主要是先经过制做特征和标签把预测问题转成了监督学习问题,而后围绕着用户画像和文章画像进行一系列特征的制做, 此外,为了保证正负样本的数据均衡,咱们还学习了负采样就技术等。