阿里云-零基础入门推荐系统 【特征工程】

news2024/11/23 22:12:16

文章目录

  • 赛题介绍
  • 评价方式理解
  • 赛题理解
  • 制作特征和标签, 转成监督学习问题
    • 导包
    • df节省内存函数
    • 训练和验证集的划分
    • 获取历史点击和最后一次点击
    • 读取训练、验证及测试集
    • 读取召回列表
    • 读取各种Embedding
    • 读取文章信息
    • 读取数据
    • 对训练数据做负采样
    • 将召回数据转换成字典
    • 制作与用户历史行为相关特征
    • 用户相关特征
    • 用户的设备习惯
    • 用户的时间习惯
    • 用户的主题爱好
    • 用户的字数偏好特征
    • 用户的信息特征合并保存
    • 用户特征直接读入
    • 文章的特征直接读入
    • 召回文章的主题是否在用户的爱好里面
    • 保存特征
  • 学习过程


赛题介绍

该赛题是以新闻APP中的新闻推荐为背景, 目的是要求我们根据用户历史浏览点击新闻文章的数据信息预测用户未来的点击行为, 即用户的最后一次点击的新闻文章

评价方式理解

最后提交的格式是针对每个用户, 我们都会给出五篇文章的推荐结果,按照点击概率从前往后排序。 而真实的每个用户最后一次点击的文章只会有一篇的真实答案, 所以我们就看我们推荐的这五篇里面是否有命中真实答案的。比如对于user1来说, 我们的提交会是:

user1, article1, article2, article3, article4, article5.

评价指标的公式如下:
在这里插入图片描述

假如article1就是真实的用户点击文章,也就是article1命中, 则s(user1,1)=1, s(user1,2-4)都是0, 如果article2是用户点击的文章, 则s(user,2)=1/2,s(user,1,3,4,5)都是0。也就是score(user)=命中第几条的倒数。如果都没中, 则score(user1)=0。 这个是合理的, 因为我们希望的就是命中的结果尽量靠前, 而此时分数正好比较高。

赛题理解

根据赛题简介,我们首先要明确我们此次比赛的目标: 根据用户历史浏览点击新闻的数据信息预测用户最后一次点击的新闻文章。从这个目标上看, 会发现此次比赛和我们之前遇到的普通的结构化比赛不太一样, 主要有两点:

  • 首先是目标上, 要预测最后一次点击的新闻文章,也就是我们给用户推荐的是新闻文章, 并不是像之前那种预测一个数或者预测数据哪一类那样的问题
  • 数据上, 通过给出的数据我们会发现, 这种数据也不是我们之前遇到的那种特征+标签的数据,而是基于了真实的业务场景, 拿到的用户的点击日志

所以拿到这个题目,我们的思考方向就是结合我们的目标,把该预测问题转成一个监督学习的问题(特征+标签),然后我们才能进行ML,DL等建模预测。

在这里插入图片描述

制作特征和标签, 转成监督学习问题

我们先捋一下基于原始的给定数据, 有哪些特征可以直接利用:

文章的自身特征, category_id表示这文章的类型, created_at_ts表示文章建立的时间, 这个关系着文章的时效性, words_count是文章的字数, 一般字数太长我们不太喜欢点击, 也不排除有人就喜欢读长文。
文章的内容embedding特征, 这个召回的时候用过, 这里可以选择使用, 也可以选择不用, 也可以尝试其他类型的embedding特征, 比如W2V等
用户的设备特征信息

构造监督数据集的思路, 根据召回结果, 我们会得到一个{user_id: [可能点击的文章列表]}形式的字典。 那么我们就可以对于每个用户, 每篇可能点击的文章构造一个监督测试集, 比如对于用户user1, 假设得到的他的召回列表{user1: [item1, item2, item3]}, 我们就可以得到三行数据(user1, item1), (user1, item2), (user1, item3)的形式, 这就是监督测试集时候的前两列特征。

构造特征的思路是这样, 我们知道每个用户的点击文章是与其历史点击的文章信息是有很大关联的, 比如同一个主题, 相似等等。 所以特征构造这块很重要的一系列特征是要结合用户的历史点击文章信息。我们已经得到了每个用户及点击候选文章的两列的一个数据集, 而我们的目的是要预测最后一次点击的文章, 比较自然的一个思路就是和其最后几次点击的文章产生关系, 这样既考虑了其历史点击文章信息, 又得离最后一次点击较近,因为新闻很大的一个特点就是注重时效性。 往往用户的最后一次点击会和其最后几次点击有很大的关联。 所以我们就可以对于每个候选文章, 做出与最后几次点击相关的特征如下:

  • 候选item与最后几次点击的相似性特征(embedding内积) — 这个直接关联用户历史行为
  • 候选item与最后几次点击的相似性特征的统计特征 — 统计特征可以减少一些波动和异常
  • 候选item与最后几次点击文章的字数差的特征 — 可以通过字数看用户偏好
  • 候选item与最后几次点击的文章建立的时间差特征 — 时间差特征可以看出该用户对于文章的实时性的偏好

当然, 上面只是提供了一种基于用户历史行为做特征工程的思路, 大家也可以思维风暴一下,尝试一些其他的特征。 下面我们就实现上面的这些特征的制作, 下面的逻辑是这样:

我们首先获得用户的最后一次点击操作和用户的历史点击, 这个基于我们的日志数据集做
基于用户的历史行为制作特征, 这个会用到用户的历史点击表, 最后的召回列表, 文章的信息表和embedding向量
制作标签, 形成最后的监督学习数据集

导包

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')

print("结束导包结束")

df节省内存函数

# 节省内存的一个函数
# 减少内存
def reduce_mem(df):
    starttime = time.time()
    numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
    start_mem = df.memory_usage().sum() / 1024**2
    for col in df.columns:
        col_type = df[col].dtypes
        if col_type in numerics:
            c_min = df[col].min()
            c_max = df[col].max()
            if pd.isnull(c_min) or pd.isnull(c_max):
                continue
            if str(col_type)[:3] == 'int':
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                    df[col] = df[col].astype(np.int32)
                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                    df[col] = df[col].astype(np.int64)
            else:
                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                    df[col] = df[col].astype(np.float16)
                elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                    df[col] = df[col].astype(np.float32)
                else:
                    df[col] = df[col].astype(np.float64)
    end_mem = df.memory_usage().sum() / 1024**2
    print('-- Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction),time spend:{:2.2f} min'.format(end_mem,
                                                                                                           100*(start_mem-end_mem)/start_mem,
                                                                                                           (time.time()-starttime)/60))
    return df



#data_path = './data_raw/'
#save_path = './temp_results/'
data_path = '/data/temp/用户行为预测数据集/' # '/home/admin/jupyter/data/' # 天池平台路径
save_path = 'results/0227/' # '/home/admin/jupyter/temp_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_a

读取召回列表

# 返回多路召回列表或者单路召回
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'))

在这里插入图片描述

读取各种Embedding

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, vector_size=16, sg=1, window=5, seed=2020, workers=24, min_count=1, epochs=1)  # 备注:size 改成 vector_size, iter 改成 epochs
    
    # 保存成字典的形式
    item_w2v_emb_dict = {k: w2v.wv[k] for k in click_df['click_article_id']}   # w2v 改成 w2v.wv
    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)
    
    return item_content_emb_dict, item_w2v_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

读取数据

print("开始读取数据")
# 读取数据
# 这里offline的online的区别就是验证集是否为空
click_trn, click_val, click_tst, val_ans = get_trn_val_tst_data(data_path, offline=False)
print("结束读取数据")


print("开始获取历史点击和最后一次点击")
click_trn_hist, click_trn_last = get_hist_and_last_click(click_trn)
print("结束获取历史点击和最后一次点击")

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

对训练数据做负采样

# 将召回列表转换成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




print("开始读取召回列表")
# 读取召回列表
recall_list_dict = get_recall_list(save_path, single_recall_model='i2i_itemcf') # 这里只选择了单路召回的结果,也可以选择多路召回结果
print("结束读取召回列表")


print("开始将召回数据转换成df")
# 将召回数据转换成df
recall_list_df = recall_dict_2_df(recall_list_dict)
print("结束将召回数据转换成df")


print("给训练验证数据打标签,并负采样(这一部分时间比较久)")
# 给训练验证数据打标签,并负采样(这一部分时间比较久)
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)
print("给训练验证数据打标签,并负采样(这一部分时间比较久)")


#trn_user_item_label_df.label

将召回数据转换成字典

# 将最终的召回的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

print("开始将召回数据转换成字典")
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]))
print("结束将召回数据转换成字典")

制作与用户历史行为相关特征

# 下面基于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



print("开始取文章信息")
article_info_df = get_article_info_df()
print("结束取文章信息")
all_click = click_trn.append(click_tst)


print("开始可以通过字典查询对应的item的Embedding")
item_content_emb_dict, item_w2v_emb_dict = get_embedding(save_path, all_click)
print("结束可以通过字典查询对应的item的Embedding")


print("开始制作与用户历史行为相关特征")
# 获取训练验证及测试数据中召回列文章相关特征
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)
print("结束制作与用户历史行为相关特征")

用户相关特征

click_tst.head()


print("开始读取文章特征,df节省内存函数")
# 读取文章特征
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)
print("结束读取文章特征,df节省内存函数")



print("开始拼上文章信息")
# 拼上文章信息
all_data = all_data.merge(articles, left_on='click_article_id', right_on='article_id')
#all_data.shape
print("all_data.shape:", all_data.shape)
print("结束拼上文章信息")






# 分析一下点击时间和点击文章的次数,区分用户活跃度
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

print("开始分析一下点击时间和点击文章的次数,区分用户活跃度")
user_act_fea = active_level(all_data, ['user_id', 'click_article_id', 'click_timestamp'])
#user_act_fea.head()
print("user_act_fea.head(1):", user_act_fea.head(1))
print("结束分析一下点击时间和点击文章的次数,区分用户活跃度")





# 分析一下点击时间和被点击文章的次数, 衡量文章热度特征
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



print("开始分析一下点击时间和被点击文章的次数, 衡量文章热度特征")
article_hot_fea = hot_level(all_data, ['user_id', 'click_article_id', 'click_timestamp']) 
# article_hot_fea.head()
print("article_hot_fea.head(1):", article_hot_fea.head(1))
print("结束分析一下点击时间和被点击文章的次数, 衡量文章热度特征")

用户的设备习惯

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


print("开始用户的设备习惯")
# 设备特征(这里时间会比较长)
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)
print("user_device_info.head(1):", user_device_info.head(1))
print("结束用户的设备习惯")

用户的时间习惯

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


print("开始用户的时间习惯")
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)
print("user_time_hob_info.head(1):", user_time_hob_info.head(1))
print("结束用户的时间习惯")

用户的主题爱好

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


print("开始用户的主题爱好")
user_category_hob_cols = ['user_id', 'category_id']
user_cat_hob_info = user_cat_hob_fea(all_data, user_category_hob_cols)
print("user_cat_hob_info.head(1):", user_cat_hob_info.head(1))
print("结束用户的主题爱好")

用户的字数偏好特征

print("开始用户的字数偏好特征")
# 用户的字数偏好特征
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)
print("user_wcou_info.head(1):", user_wcou_info.head(1))
print("结束用户的字数偏好特征")

用户的信息特征合并保存

print("开始用户的信息特征合并保存")
# 用户的信息特征合并保存
# 所有表进行合并
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)   
print("user_info.head(1):", user_info.head(1))
print("结束用户的信息特征合并保存")

用户特征直接读入

print("开始用户特征直接读入")
## 用户特征直接读入
# 把用户信息直接读入进来
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')



trn_user_item_feats_df.columns
print("trn_user_item_feats_df.columns:", trn_user_item_feats_df.columns)
print("结束用户特征直接读入")

文章的特征直接读入

print("开始文章的特征直接读入")
## 文章的特征直接读入
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')
print("tst_user_item_feats_df.head(1):", tst_user_item_feats_df.head(1))
print("结束文章的特征直接读入")

召回文章的主题是否在用户的爱好里面

print("开始召回文章的主题是否在用户的爱好里面")
## 召回文章的主题是否在用户的爱好里面
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)
print("tst_user_item_feats_df.head(1):", tst_user_item_feats_df.head(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']
print("结束召回文章的主题是否在用户的爱好里面")

保存特征

print("开始保存特征")
## 保存特征
# 训练验证特征
trn_user_item_feats_df.to_csv(save_path + 'trn_user_item_feats_df.csv', index=False)
print("trn_user_item_feats_df.head(1):", trn_user_item_feats_df.head(1))
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)
print("tst_user_item_feats_df.head(1):", tst_user_item_feats_df.head(1))
print("结束保存特征")

学习过程

20年当时自身功底是比较零基础(会写些基础的Python[三个科学计算包]数据分析),一开始看这块其实挺懵的,不会就去问百度或其他人,当时遇见困难挺害怕的,但22后面开始力扣题【目前已刷好几轮,博客没写力扣文章之前,力扣排名靠前已刷有5遍左右,排名靠后刷3次左右,代码功底也在一步一步提升】不断地刷、遇见代码不懂的代码,也开始去打印print去理解,到后面问其他人的问题越来越少,个人自主学习、自主解决能力也得到了进一步增强。

比赛源自:阿里云天池大赛 - 零基础入门推荐系统 - 新闻推荐

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1522713.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

spring boot集成redis实现共享存储session

spring boot集成redis实现共享存储session redis实现共享存储session 首先下载redis,我下载的版本是5.0.14,目前官网貌似找不到5.x版本&#xff0c;可以自行去网上寻找。我这里的springboot版本是2.6.4引入redis依赖 <!-- https://mvnrepository.com/artifact/org.spring…

麒麟系统Redis7.2哨兵集群部署

redis哨兵集群部署 1、原理 Redis 哨兵模式是指在 Redis 集群中,有一组专门的进程(即哨兵进程)负责监控主节点和从节点的状态,并在发现故障时自动进行故障转移,以保证 Redis 集群的高可用性。 Redis 提供了哨兵的命令,哨兵命令是一个独立的进程,哨兵进程会周期性地向主…

YOLOv9改进策略:注意力机制 | SimAM(无参Attention),效果秒杀CBAM、SE

&#x1f4a1;&#x1f4a1;&#x1f4a1;本文改进内容&#xff1a;SimAM是一种轻量级的自注意力机制&#xff0c;其网络结构与Transformer类似&#xff0c;但是在计算注意力权重时使用的是线性层而不是点积 yolov9-c-CoordAtt summary: 972 layers, 51024476 parameters, 510…

“一键秒变!PNG到JPG,图片批量转换新体验“

在这个数字时代&#xff0c;图片已成为我们生活与工作中不可或缺的一部分。无论是社交媒体上的个人分享&#xff0c;还是商务场合中的项目展示&#xff0c;一张好的图片往往能起到事半功倍的效果。然而&#xff0c;面对堆积如山的PNG图片&#xff0c;你是否曾感到力不从心&…

Linux-centos如何搭建yum源仓库

1.本地搭建&#xff08;无需连接外网&#xff09; 1.1检查网络配置&#xff0c;及网络连接 打开虚拟机&#xff0c;点击【编辑——虚拟网络编辑器】 点击【仅主机模式】查看子网段是否和局内IP匹配 进入局内&#xff0c;查看网络IP是否在你上述设置的网段内&#xff0c;如果不…

【PyTorch】成功解决ModuleNotFoundError: No module named ‘torch’

【PyTorch】成功解决ModuleNotFoundError: No module named ‘torch’ &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希…

SQLiteC/C++接口详细介绍之sqlite3类(十四)

返回目录&#xff1a;SQLite—免费开源数据库系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;十三&#xff09; 下一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;十五&#xff09; 43.sqlite3_preupdate_hook sqlite3_preup…

【工具】一键生成动态歌词字幕

那眼神如此熟悉 让人着迷无力抗拒 一次又一次相遇 在眼前却遥不可及 命运总爱淘气 将一切都藏匿 曾有你的回忆 无痕迹 若不是心心相吸 又怎么会一步一步靠近 &#x1f3b5; 董真《思如雪》 下载LRC歌词 https://www.musicenc.com/article/50287.htmlhttp…

Android 系统的启动过程

Android 系统的启动流程&#xff1a; RomBoot&#xff08;只读存储器引导程序&#xff09;&#xff1a;这是设备上电时运行的初始软件。RomBoot执行基本的硬件初始化&#xff0c;确保硬件处于可以运行后续启动阶段的状态。这一阶段非常重要&#xff0c;因为它为整个启动过程奠定…

VMwareWorkstation16与Ubuntu 22.04.6 LTS下载与安装

一、准备工作 VMware Workstation Pro 16官网下载&#xff1a; https://customerconnect.vmware.com/cn/downloads/info/slug/desktop_end_user_computing/vmware_workstation_pro/16_0。下载需要账号登录。 二、安装 双击exe文件稍等一会会弹出安装程序&#xff0c;如图 这…

【Miniconda】基于conda避免运行多个PyTorch项目时发生版本冲突

【Miniconda】基于conda避免运行多个PyTorch项目时发生版本冲突 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望得到…

高效备考2025年AMC8竞赛:吃透2000-2024年600道真题(免费送题)

我们继续来随机看五道AMC8的真题和解析&#xff0c;根据实践经验&#xff0c;对于想了解或者加AMC8美国数学竞赛的考生来说&#xff0c;吃透AMC8历年真题是备考更加科学、有效的方法之一。 即使不参加AMC8竞赛&#xff0c;吃透了历年真题600道和背后的知识体系&#xff0c;那么…

爬虫学习 Scrapy中间件代理UA随机selenium使用

目录 中间件UA、代理处理---process_requestUA随机 代理处理seleniumscrapy 中间件 控制台操作 (百度只起个名 scrapy startproject mid scrapy genspider baidu baidu.com setting.py内 ROBOTSTXT_OBEY FalseLOG_LEVEL "WARNING"运行 scrapy crawl baidu middle…

数据可视化-ECharts Html项目实战(1)

在之前的文章中&#xff0c;我们学习了如何安装Visual Studio Code并下载插件&#xff0c;想了解的朋友可以查看这篇文章。同时&#xff0c;希望我的文章能帮助到你&#xff0c;如果觉得我的文章写的不错&#xff0c;请留下你宝贵的点赞&#xff0c;谢谢。 安装 Visual Studio…

【IC设计】Verilog线性序列机点灯案例(三)(小梅哥课程)

声明&#xff1a;案例和代码来自小梅哥课程&#xff0c;本人仅对知识点做做笔记&#xff0c;如有学习需要请支持官方正版。 文章目录 该系列目录设计目标设计思路RTL及Testbench代码RTL代码Testbench代码 仿真结果上板视频 该系列目录 Verilog线性序列机点灯案例(一)&#xff…

LangChain: 调研报告

概述 LangChain是一个用于开发由语言模型驱动的应用程序的框架。它允许创建能够连接到上下文源&#xff08;如提示指令、少量示例、内容基础等&#xff09;的应用程序&#xff0c;并且能够进行推理&#xff08;基于提供的上下文如何回答问题、采取何种行动等&#xff09;。提供…

Ps:历史记录画笔工具

历史记录画笔工具 History Brush Tool可通过选择历史记录面板中的一个特定状态作为绘画源&#xff0c;然后使用画笔在图像的特定区域绘制&#xff0c;将这些区域恢复到选定的历史状态。 快捷键&#xff1a;Y 如果对图像进行了广泛的编辑&#xff0c;但希望将图像的一小部分恢复…

Explain 关键字

优质博文&#xff1a;IT-BLOG-CN explain关键字可以模拟优化器执行 SQL 查询语句&#xff0c;从而知道 MySQL 是如何处理 SQL 语句的。分析查询语句或表结构的性能瓶颈。执行语句&#xff1a;explain SQL语句。表头信息如下&#xff1a; 一、ID 参数 select 查询的序列号&…

算法---滑动窗口练习-8(最小覆盖子串)

最小覆盖子串 1. 题目解析2. 讲解算法原理3. 编写代码 1. 题目解析 题目地址&#xff1a;最小覆盖子串 2. 讲解算法原理 滑动窗口哈希表 算法的主要思想是使用滑动窗口的方法。它使用两个哈希表 hash1 和 hash2 来记录字符串 s 和 t 中各个字符的出现次数。其中 hash2 用于记…

MongoDB的安装方法图文详细

官网&#xff1a;www.mongodb.com 选择 Products > Community Edition 就能进入社区版 在这里下载 windows 版对应的安装包 注意&#xff1a;6.0.1 版本的 MongoDB 配置环境变量有问题&#xff0c;并且我不知道怎么解决&#xff0c;如果想要避免出问题&#xff0c;建议使…