方案地址:https://zhuanlan.zhihu.com/p/73062485
代码地址:https://github.com/guoday/Tencent2019_Preliminary_Rank1st
数据地址:https://algo.qq.com/application/home/home/review.html
利用历史曝光信息,广告信息,用户信息来预测一个广告的日曝光量。
import os
import pandas as pd
import numpy as np
import random
import gc
import time
from tqdm import tqdm
def parse_rawdata():
#曝光日志
df=pd.read_csv('data/testA/totalExposureLog.out', sep='\t',names=['id','request_timestamp','position','uid','aid','imp_ad_size','bid','pctr','quality_ecpm','totalEcpm']).sort_values(by='request_timestamp')
#使用pandas来读取曝光日志文件,分隔符为‘\t’,命名列名为['id','request_t...],同时根据列名request_timestamp对所有数据排序,默认为升序
df[['id','request_timestamp','position','uid','aid','imp_ad_size']]=df[['id','request_timestamp','position','uid','aid','imp_ad_size']].astype(int)
##类型转化,因为读入的有些字符可能是字符串格式的,需要统一转化为float格式
df[['bid','pctr','quality_ecpm','totalEcpm']]=df[['bid','pctr','quality_ecpm','totalEcpm']].astype(float)
##类型转化,因为读入的有些字符可能是字符串格式的,需要统一转化为int格式
df.to_pickle('data/testA/totalExposureLog.pkl')
##将dataframe格式数据转换pickle,方便下次存取
del df
gc.collect()
#这两行的作用是删除df变量在内存中的占用,同时用gc.collect()来清理内存
##############################################################################
#静态广告
df =pd.read_csv('data/testA/ad_static_feature.out', sep='\t', names=['aid','create_timestamp','advertiser','good_id','good_type','ad_type_id','ad_size']).sort_values(by='create_timestamp')
##同理,读取静态广告文件,分隔符‘\t’,按列名['aid','create_timestamp'...]命名,按列排序。
df=df.fillna(-1)
## 对df中缺失值填充-1
for f in ['aid','create_timestamp','advertiser','good_id','good_type','ad_type_id']:
items=[]
for item in df[f].values:
try:
items.append(int(item))
except:
items.append(-1)
#try,except语句,当try中出现错误时执行except语句,可以保证程序都会执行下去
df[f]=items
df[f]=df[f].astype(int)
## 对于可能不是空值,但是有异常的值,某些填入字符串的值,利用遍历来转换,对这些值置为-1
df['ad_size']=df['ad_size'].apply(lambda x:' '.join([str(int(float(y))) for y in str(x).split(',')]))
#因为ad_size列中可能有多个数值,不同广告大小,所以使用匿名函数,
#将size列中的数据转化为str类型,同时去掉逗号,用空格分隔
df.to_pickle('data/testA/ad_static_feature.pkl')
del df
gc.collect()
##同理,清除内存
##############################################################################
#用户信息
df =pd.read_csv('data/testA/user_data', sep='\t',
names=['uid','age','gender','area','status','education','concuptionAbility','os','work','connectionType','behavior'])
df=df.fillna(-1)
##读取用户文件,同时命名,对缺失值填充-1
df[['uid','age','gender','education','consuptionAbility','os','connectionType']]=df[['uid','age','gender','education','concuptionAbility','os','connectionType']].astype(int)
## 类型转化
for f in ['area','status','work','behavior']:
df[f]=df[f].apply(lambda x:' '.join(x.split(',')))
#因为['area','status','work','behavior']中可能会有多值存在,
#所以进行数据清洗,方便后续处理,将分割由,转换为空格
df.to_pickle('data/testA/user_data.pkl')
del df
gc.collect()
##清除内存
##############################################################################
#测试数据
df=pd.read_csv('data/testA/test_sample.dat', sep='\t', names=['id','aid','create_timestamp','ad_size','ad_type_id','good_type','good_id','advertiser','delivery_periods','crowd_direction','bid'])
df=df.fillna(-1)
## 读取测试数据,缺失值填充-1
df[['id','aid','create_timestamp','ad_size','ad_type_id','good_type','good_id','advertiser']]=df[['id','aid','create_timestamp','ad_size','ad_type_id','good_type','good_id','advertiser']].astype(int)
## 类型转化
df['bid']=df['bid'].astype(float)
df.to_pickle('data/testA/test_sample.pkl')
del df
gc.collect()
## 保存pickle格式,清除内存。
def construct_log():
#构造曝光日志,分别有验证集的log和测试集的log
train_df=pd.read_pickle('data/testA/totalExposureLog.pkl')
##读取之前存储的pickle格式文件
train_df['request_day']=train_df['request_timestamp']//(3600*24)
##将时间戳粗略转化为‘天’为单位的计量值
wday=[]
hour=[]
minute=[]
for x in tqdm(train_df['request_timestamp'].values,total=len(train_df)):
##tqdm是python里面进度条的封装函数,通过封装一些处理语句,可以让程序有反馈,方便程序员操作
localtime=time.localtime(x)
## time.localtime作用是格式化时间戳为本地的时间,通过打印可返回一个结构体
#time.struct_time(tm_year=2016, tm_mon=11, tm_mday=27, tm_hour=10, tm_min=26, tm_sec=5, tm_wday=6, tm_yday=332, tm_isdst=0)
wday.append(localtime[6])#对应tm_wday
hour.append(localtime[3])#对应tm_hour
minute.append(localtime[4])#对应tm_min
train_df['wday']=wday
train_df['hour']=hour
train_df['minute']=minute
train_df['period_id']=train_df['hour']*2+train_df['minute']//30
#将时间粒度以半小时为单位作为一个特征
dev_df=train_df[train_df['request_day']==17974]
#构造验证集
del dev_df['period_id']
del dev_df['minute']
del dev_df['hour']
#删除验证集中period_id,minute,hour列
log=train_df
#备份训练集,这种赋值方法是对象指向型的,也就是说改变任何变量里的数据,另一个变量都会随之改变。
tmp = pd.DataFrame(train_df.groupby(['aid','request_day']).size()).reset_index()
#按照'aid','request_day'分组来构造曝光量,同一个aid,同一天的出现次数作为曝光量,同时reset_index()增加新的索引列index,从0开始
tmp.columns=['aid','request_day','imp']
#重新命名dataframe的列名
log=log.merge(tmp,on=['aid','request_day'],how='left')
#merge函数,方式为左连接,及左边的dataframe(log)在['aid','request_day']列上全取,右边的根据与之合并。
#构造出最后的训练集,最后由train_df返回
#与之前备份的训练集数据合并,相当于为训练集中的所有数据增加了标签列
log[log['request_day']<17973].to_pickle('data/user_log_dev.pkl')
#将小于17973号数据存为pickle格式数据
log.to_pickle('data/user_log_test.pkl')
#训练集构造保存为pickle格式,方便下次读取
del log
del tmp
gc.collect()
#清除内存变量
del train_df['period_id']
del train_df['minute']
del train_df['hour']
#删除训练集中的某些列
return train_df,dev_df
#返回训练集和验证集
def extract_setting():
aids=[]
with open('data/testA/ad_operation.dat','r') as f:
#以只读方式‘r’打开广告操作文件
for line in f:
#对文件中的按行遍历
line=line.strip().split('\t')
#Python strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列,
#同时split('\t'),用‘\t’分割数据,形成列表
try:
if line[1]=='20190230000000':
line[1]='20190301000000'
#对出现2月30号的数据视为异常,将其强制转变为3月1号数据
if line[1]!='0':
request_day=time.mktime(time.strptime(line[1], '%Y%m%d%H%M%S'))//(3600*24)
#time.strptime函数根据指定的格式把一个时间字符串解析为时间元组。返回一个时间结构体
#mktime()用来将参数timeptr所指的tm结构数据转换成从公元1970年1月1日0时0分0秒算起至今的UTC时间所经过的秒数。
#从而同步与训练集的时间戳数据
else:
request_day=0
#对于line[1]==0的数据,也就是update==0的,request_day也为0,可以视为异常数据
except:
print(line[1])
#如果上述发生错误语句,则打印这行的遍历结果
##根据operation文件里的特性,我们发现,广告按其id已经分好组了,判断是否重复
#只需要和最后一个比较就可以。下面的操作可以理解为一种填充,因为只有一个广告在
#operation里才会有曝光量,所以下面的语句是为了填充,如果某个广告只在操作表中
#出现过一次,那么我们则将其扩充到所有日期,如果某个广告在操作表中出现2次及以上
#,如果请求时间相同则不处理,如果请求时间不同,则扩充两次请求时间内全部为第一次出现操作。
if len(aids)==0:
aids.append([int(line[0]),0,"NaN","NaN"])
#line[0]为aid,存入的list为['aid','request_day','crowd_direction','delivery_periods']
elif aids[-1][0]!=int(line[0]):
for i in range(max(17930,aids[-1][1]+1),17975):
#需要注意的是在这个循环里,(aids[-1][1]+1)是一个一开始就确定的数
#将只出现一次的广告扩充所有日期原操作
aids.append(aids[-1].copy())
aids[-1][1]=i
aids.append([int(line[0]),0,"NaN","NaN"])
elif request_day!=aids[-1][1]:
#将出现2次及以上的广告在request间隔内扩充
for i in range(max(17930,aids[-1][1]+1),int(request_day)):
aids.append(aids[-1].copy())
aids[-1][1]=i
aids.append(aids[-1].copy())
aids[-1][1]=int(request_day)
if line[3]=='3':
aids[-1][2]=line[4]
#对'crowd_direction'赋值操作
if line[3]=='4':
aids[-1][3]=line[4]
#对'delivery_periods'赋值操作
ad_df=pd.DataFrame(aids)
#将列表生成dataframe
ad_df.columns=['aid','request_day','crowd_direction','delivery_periods']
#对dataframe格式数据重新命名列名
return ad_df
#返回广告操作数据以dataframe格式
def construct_train_data(train_df):
#构造训练集
#算出广告当天平均出价和曝光量
tmp = pd.DataFrame(train_df.groupby(['aid','request_day'])['bid'].nunique()).reset_index()
#对训练数据按['aid','request_day']进行分组操作之后提取bid属性,
#nunique()用这个函数可以查看数据有多少个不同值,重新建立索引
tmp.columns=['aid','request_day','bid_unique']
#对生成的新dataframe重新命名
train_df=train_df.merge(tmp,on=['aid','request_day'],how='left')
#为每一个广告,和请求时间加上bid数量属性,表明同一个广告,在同一个请求时间下,存在多少次不同出价
tmp = pd.DataFrame(train_df.groupby(['aid','request_day']).size()).reset_index()
#对训练数据按照['aid','request_day']分组,并求每组有多少个,重新设置索引
tmp_1 = pd.DataFrame(train_df.groupby(['aid','request_day'])['bid'].mean()).reset_index()
##提取每个分组的出价平均值
tmp.columns=['aid','request_day','imp']
#构造曝光量,将曝光近似为同一天有多少个请求
del train_df['bid']
#删除训练集中的bid列
tmp_1.columns=['aid','request_day','bid']
#构造平均出价作为同一广告,同一刻请求的出价
train_df=train_df.drop_duplicates(['aid','request_day'])
# 去重aid和request_day一样的数据
train_df=train_df.merge(tmp,on=['aid','request_day'],how='left')
#tmp文件里有曝光量的属性,与之合并
train_df=train_df.merge(tmp_1,on=['aid','request_day'],how='left')
#tep1里有平均出价的属性,与训练集合并
del tmp
del tmp_1
gc.collect()
#清空内存
train_df=train_df.drop_duplicates(['aid','request_day'])
del train_df['request_timestamp']
del train_df['uid']
#删除训练集中的['request_timestamp']和['uid']属性,删除无关属性,可以在训练中提高效率
#以下操作过滤未出现在广告操作文件的广告
ad_df=extract_setting()#调用之前写的extract_setting()函数,返回的是广告操作数据
ad_df=ad_df.drop_duplicates(['aid','request_day'],keep='last')
#操作数据去重,按照aid','request_day,保留最后一项
ad_df['request_day']+=1
#对操作数据的所有请求时间加一天,我的理解是这么做可以和训练数据出现的请求时间上是同步的,因为操作之后是后一天才计算曝光量的
train_df=train_df.merge(ad_df,on=['aid','request_day'],how='left')
#和训练数据合并,此时训练数据属性包括改广告曝光量,出价,以及对应的操作
train_df['is']=train_df['crowd_direction'].apply(lambda x:type(x)==str)
#生成训练数据新列,表示是否有定向人群的属性,如果该字段是字符串,则标记为true
train_df=train_df[train_df['is']==True]
#提取有is列是true的数据,相当于筛选
train_df=train_df[train_df['crowd_direction']!="NaN"]
#除去定向人群是NAN格式的数据,表示空值
train_df=train_df[train_df['delivery_periods']!="NaN"]
#在之前基础上,除去投送时期是空的数据
#以下操作过滤出价和曝光过高的广告
train_df=train_df[train_df['imp']<=3000]
#除去曝光量大于3000的数据,因为经过分析,大于3000的只有很少,我们可以把他们作为异常值处理,这样可以保证模型准确性
train_df=train_df[train_df['bid']<=1000]
#除去出价大于1000的,原因同上
train_dev_df=train_df[train_df['request_day']<17973]
#将请求日期小于17973的数据作为训练验证数据集
print(train_df.shape,train_dev_df.shape)
#输出训练数据的规模大小,以及验证数据集的
print(train_df['imp'].mean(),train_df['bid'].mean())
#输出训练数据集中曝光量的平均值,以及出价的平均值
return train_df,train_dev_df
#返回训练数据集和训练验证数据集
def construct_dev_data(dev_df):
#构造验证集,主要用来确定网络结构或者控制模型复杂程度的参数
#过滤掉当天操作的广告,和未出现在操作日志的广告
aids=set()
#set() 函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可以计算交集、差集、并集等。
exit_aids=set()
with open('data/testA/ad_operation.dat','r') as f:
#打开广告操作文件
for line in f:
#按行遍历
line=line.strip().split('\t')
#除去每行开头结尾的空格,同时按字符'\t'分割
if line[1]=='20190230000000':
line[1]='20190301000000'
#对出现2月30号的数据视为异常,将其强制转变为3月1号数据
if line[1]!='0':
request_day=time.mktime(time.strptime(line[1], '%Y%m%d%H%M%S'))//(3600*24)
#time.strptime函数根据指定的格式把一个时间字符串解析为时间元组。返回一个时间结构体
#mktime()用来将参数timeptr所指的tm结构数据转换成从公元1970年1月1日0时0分0秒算起至今的UTC时间所经过的秒数。
#从而同步与训练集的时间戳数据
else:
request_day=0
#对于line[1]==0的数据,也就是update==0的,request_day也为0,可以视为异常数据
if request_day==17974:
aids.add(int(line[0]))
#最后一天的所有广告操作,其广告id加入aids中
exit_aids.add(int(line[0]))
#所有的广告操作中广告id集合
dev_df['is']=dev_df['aid'].apply(lambda x: x in aids)
#apply函数将dataframe中每个数据调用后面的匿名函数
dev_df=dev_df[dev_df['is']==False]
#将验证集中的广告id如果出现在最后一天则除去
dev_df['is']=dev_df['aid'].apply(lambda x: x in exit_aids)
dev_df=dev_df[dev_df['is']==True]
#除去验证集中没有广告操作的数据
#过滤当天出价不唯一的广告
tmp = pd.DataFrame(dev_df.groupby('aid')['bid'].nunique()).reset_index()
#按照广告id统计出价个数,并且重新设置索引
tmp.columns=['aid','bid_unique']
dev_df=dev_df.merge(tmp,on='aid',how='left')
#在验证集上增加新列,每个广告id下的出价个数
dev_df=dev_df[dev_df['bid_unique']==1]
#保留出价个数为1次的广告id
#统计广告当天的曝光量
tmp = pd.DataFrame(dev_df.groupby('aid').size()).reset_index()
tmp.columns=['aid','imp']
#统计验证集上广告当天的曝光量,并重新命名列名
dev_df=dev_df.merge(tmp,on='aid',how='left')
#为验证集增加新列,以左连接方式在aid属性上合并数据,增加曝光量属性
dev_df=dev_df.drop_duplicates('aid')
#过滤广告ID重复数据
#过滤未出现在广告操作文件的广告
ad_df=extract_setting()
#返回扩充过的广告操作数据
ad_df=ad_df.drop_duplicates(['aid'],keep='last')
#过滤aid重复数据,保留最后一次
dev_df=dev_df.merge(ad_df,on='aid',how='left')
#验证集和操作数据合并
dev_df=dev_df[dev_df['crowd_direction']!="NaN"]
#过滤验证集中没有人群定向的数据
dev_df=dev_df[dev_df['delivery_periods']!="NaN"].reset_index()
#过滤验证集中没有投放时段的数据
del dev_df['index']
del dev_df['request_timestamp']
del dev_df['is']
del dev_df['uid']
#删除验证集中对应的列
#构建虚假广告,测试单调性
items=[]
#创建一个空列表
for item in dev_df[['aid','bid','crowd_direction', 'delivery_periods','imp']].values:
#产生一个遍历,item是一个对应numpy的数组,每次循环对应一行dev_df中'aid','bid','crowd_direction', 'delivery_periods','imp'属性列的值。
item=list(item)
#将numpy格式的item转换为列表格式
items.append(item+[1])
#为为列表增加新的元素1,作用是标记其为真是数据,同时将整个列表加入到items中,作为一个数据
for i in range(10):
#i从0-9产生一个遍历
while True:
t=random.randint(0,2*item[1])
#在python中的random.randint(a,b)用于生成一个指定范围内的整数。这里将产生一个0到2*bid的之间的一个随机整数
if t!=item[1]:
#如果产生的数不等于bid值
items.append(item[:1]+[t]+item[2:]+[0])
#构造一个广告id等于之前id,出价为t,'crowd_direction', 'delivery_periods'都与之前一样的新数据,同时在数据最后加0元素作为标记,标记其为构造的虚假数据
break
#直到产生一个虚假数据为止,跳出一个循环
else:
continue
#继续内层循环
#每一个真是数据,产生了10个由其产生的虚假数据,这些数据仅仅是出价不同
dev_df=pd.DataFrame(items)
#将items转换为dataframe结构
dev_df.columns=['aid', 'bid', 'crowd_direction', 'delivery_periods','imp','gold']
#重新命名列
del items
#删除内存中的items
gc.collect()
#内存清理
print(dev_df.shape)
#输出验证集大小
print(dev_df['imp'].mean(),dev_df['bid'].mean())
#输出验证集的曝光量平均,和出价平均
return dev_df
#返回验证集
print("parsing raw data ....")
parse_rawdata()
#调用函数parse_rawdata() ,生成测试数据,用户属性数据,广告静态数据等等对应的dataframe格式
print("construct log ....")
train_df,dev_df=construct_log()
#构建训练数据和验证数据集
print("construct train data ....")
train_df,train_dev_df=construct_train_data(train_df)
#构建训练数据集和训练验证数据集
print("construct dev data ....")
dev_df=construct_dev_data(dev_df)
#构建验证数据集
print("load test data ....")
test_df=pd.read_pickle('data/testA/test_sample.pkl')
#构建测试数据集
print("combine advertise features ....")
ad_df =pd.read_pickle('data/testA/ad_static_feature.pkl')
train_df=train_df.merge(ad_df,on='aid',how='left')
train_dev_df=train_dev_df.merge(ad_df,on='aid',how='left')
dev_df=dev_df.merge(ad_df,on='aid',how='left')
#合并广告训练数据和静态文件数据集
print("save preprocess data ....")
train_dev_df.to_pickle('data/train_dev.pkl')
train_df.to_pickle('data/train.pkl')
dev_df.to_pickle('data/dev.pkl')
test_df.to_pickle('data/test.pkl')
#将上述数据集存成对应的pickle格式,方便下次读取
print(train_dev_df.shape,dev_df.shape)
print(train_df.shape,test_df.shape)
train_dev_df 是17973号以前(不含17973)的数据,train_df是包含17973所有的数据,dev是17974号的数据。