2019年腾讯广告算法大赛冠军思路、代码(PART 1)
方案地址:https://zhuanlan.zhihu.com/p/73062485
代码地址:https://github.com/guoday/Tencent2019_Preliminary_Rank1st
数据地址:https://algo.qq.com/application/home/home/review.html
官方题目描述:
通俗题目描述:
利用历史曝光信息,广告信息,用户信息来预测一个广告的日曝光量。
数据描述:
- 历史曝光日志数据文件
- 广告请求 id:唯一标识每次请求(每个请求对应一个用户某一时刻,可能多个广告位)。
- 广告请求时间:该字段为时间戳,即 1970 纪元后经过的浮点秒数。
- 广告位 id:加密后无业务含义,只区分不同广告位,每个广告位只能曝光特定素材尺寸的广告。
- 用户 id(即看广告的人):加密后无业务含义,只区分不同用户,可和后面的用户特征数据中 id 相关联。
- 曝光广告 id:加密后无业务含义,只区分不同广告,可以和广告特征文件中的广告 id 关联。
- 曝光广告素材尺寸:枚举型取值,不同广告位对素材的尺寸要求不同,同一个广告位可能适配多个不同尺寸的素材。
- 曝光广告出价 bid:这里只记录 cpc 出价,非 cpc 广告此处记录折算后的 cpc价格。
- 曝光广告 pctr:预估的 pctr,和 bid 相乘得到 basic_ecpm。
- 曝光广告 quality_ecpm:将广告质量和用户体验等因素折算成 ecpm 的分数,主要影响因素有 pctr/pcvr/窄定向等。
- 曝光广告 totalEcpm:广告排序的分数依据,由 basic_ecpm 和 quality_ecpm相加得到。
- 广告静态数据
- 广告 id:和曝光日志中的广告 id 相关联。
- 创建时间:广告创建时的时间戳。
- 广告账户 id:广告所在账户的唯一标识,账户结构分为四级:账户——推广计划——广告——素材。
- 商品 id:广告推广目标的唯一标识,若推广目标是落地页,则该字段为空。
- 商品类型:广告推广目标的类型,枚举型。
- 广告行业 id:广告所属的行业类别标识。
- 素材尺寸:不同广告位对素材的尺寸要求不同,同一个广告可能有多个不同尺寸的素材,用逗号分隔。
- 广告操作数据
- 广告 id(同上)。
- 创建/修改时间: 即广告创建或者修改设置的时间。
- 操作类型:1-修改,2-新建。
- 修改字段:1-广告状态,2-出价,3-人群定向,4-广告时段设置。
- 操作后的字段值:
- 广告状态取值:1- 正常,0-失效。
- 出价:整数(单位分)。
- 投放时段:字符串。包含 7 个 64 位无符号整型数字(逗号分隔),每个整数分别代表周一到周日的投放时段。该整数转为 2 进制后从低到高 48 位bit 代表全天各时段(半小时为一时间窗口)是否投放,1-投放,0-不投。举例说明17179865088= 1111111111111111111111000000000000,代表投放时段为 6:00-17:00,281474976710655=111111111111111111111111111111111111111111111111,代表全天投放。
- 人群定向:字符串。格式如下:
feature_name1:feature_value1,feature_value2|feature_name2:feature_value3,feature_value4|… 此处 feature_name 取值同用户属性文件中的各列属性名,feature_value 取值 id 同用户属性文件中的定义,不同feature 用“|”分隔,不同 feature 取值用逗号分隔。广告通过人群定向的设置来召回对应的用户请求,对应的人群规则:不同 feature_name 是求 交 集 , 同 一 featurename 下 不 同 的 value 求并集 , 未 定 义 的feature_name 则表示 该维度 不 限 。 举 例 如:定向 设 置 为age:51,62,73,84|gender:1|area:1,3,5 ; 则表示该广告能被“(年龄 id 为51 或 62 或 73 或 84) 且 (性别取值为 1) 且 (地域取值为 1 或 3或 5)”的用户召回(即在这些用户上有曝光机会)。
- 用户特征属性文件
- 用户 id:此处和上面曝光日志文件中的用户 id 关联。
- 年龄(Age):每个取值随机映射为[1-N]的唯一 id。
- 性别(Gender):男/女。
- 地域(area):每个省/市用唯一 id 标识,可能多标签,使用逗号分隔不同 id。
- 婚恋状态(Status):单身/已婚等状态,可能去多值,使用逗号分隔。
- 学历(Education):博士/硕士/本科/高中/初中/小学。
- 消费能力(ConsuptionAbility):高/低。
- 设备(device):IOS/Android, 不区分版本号。
- 工作状态(work):在校大学生/商旅人士/政府公职人员/科研教育者/ IT 互联网工作者/医护工作者, 可能取多值,逗号分隔。
- 连接类型(ConnectionType):无线/2G/3G/4G。
- 行为兴趣(behavior):每个兴趣点一个 id,可多值,逗号分隔。
评价指标:
1 准确性指标
Ft 为预估的广告曝光值,At 为真实的曝光值。
2 单调性指标
3 综合指标
最后要将单调性指标和准确性指标综合到一起来计算最后得分。
代码 预处理(数据清洗)Preprocess.py:
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号的数据。