Last updated on 2020-11-1…
特征工程是对原始数据进行一系列处理,将其提炼为模型可理解的输入数据格式,为模型提供信息增益以提升模型精度,特征工程是一个表示和展现信息的过程。 在实际工作中,特征工程旨在去除原始数据中的杂质和冗余,设计更高效的特征以刻画求解的问题与模型预测目标之间的关系。 特征是模型训练和预估的基础,决定了模型精度的上限。
以下为数次比赛的个人代码总结,文字总结版见我的另一篇博客《特征工程(文字总结版)》。
feature.py
import pandas as pd
import numpy as np
import os
print('Loading data ...')
train_x = pd.read_csv('./data/train.csv')
test_x = pd.read_csv('./data/test.csv')
print('Load data OK !')
print('Generating feature ...')
train_x.loc[train_x['label']==-1,'label']= 0
res = test_x['id']
del train_x['id']
del test_x['id']
train_x = train_x.fillna('-1')
test_x = test_x.fillna('-1')
train_y = train_x.pop('label')
print('Generate feature OK !')
#--------------------保存特征-------------------------#
np.save("train_x.npy",train_x)
print('train_x prepared !')
np.save("train_y.npy",train_y)
print('train_y prepared !')
np.save("test_x.npy",test_x)
print('test_x prepared !')
np.save("res.npy",res)
print('res prepared !')
常用语句
#常用头文件
# -*- coding: utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
import numpy as np #数学
import pandas as pd #表格
import matplotlib.pyplot as plt #图形
%matplotlib inline
pd.set_option('display.max_rows', None) #行全展示
pd.set_option('display.max_columns', None) #列全展示
#中文编码问题
#-*- encoding: utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding("gbk")
#格式读取
uname = ['user_id','gender','age','occupation','zip']
users = pd.read_table(filename1, sep='::', header = None, names=uname, engine='python') #sep为分隔符
#流式读取(命令行:cat xx.txt | python xx.py)
for line in sys.stdin: #等价于for line in open("xx.txt"):
try:
item = line.strip('\n').split('\t') #.decode('gbk')
except:
print("This line load error")
continue
#读取json(理解为字典格式的封装)
import json
for line in sys.stdin:
line = ln.decode('gbk').strip().strip('\n')
#line = line.replace("\\","")
json_data = json.loads(line) #loads()json数据:->python数据
#对于单值数据
cat = json_data['animal']['cat']
#对于多值数据({"xx1":"1","xx2":"2"})
Husky = json_data['animal']['dog'].get('Husky', {})
Husky = json.dumps(Husky, ensure_ascii=False, encoding="gbk") #dumps():python数据->json数据
Husky = json.loads(Husky)
#输出
for key in dict_:
print("\t".join([str(key), str(dict_[key]), str(ctr)]))
print("{}\t{}\t{:.8f}".format(key, dict_[key], ctr))
#批量命名变量
for i in range(1,11):
name='feature_'+ i
locals()[name] = ...
#划分数据集
from sklearn.model_selection import train_test_split
x_train,x_test,y_train,y_test = train_test_split(x,y,test_size=0.3,random_state=0,stratify=y) #stratify=y表示按原数据y中各类比例,分配给train和test
特征工程(简单)
数据选取
df = pd.read_csv("data/train.csv")
df.describe() #各项统计
df.info() #看总数及数值类型
list(df.columns) #获取列名(以list形式返回)
df15 = df[df.iyear >= 2015] #选取满足的行
df15 = df15[['eventid','region']] #选取需要的列
data = pd.concat([train,predict]) #上下拼接
data = pd.merge(data,ad_feature,on='aid',how='left') #键值左连接
max(df.iyear) #返回最大值
min(df.iyear) #返回最小值
df['region'].value_counts() #返回地区-地区出现次数,默认降序,升序为ascending=True
df['region'].value_counts(normalize=True) #返回地区-频率,默认降序
df2 = df.groupby(by=['region'])['eventid'].count() #分地区记录发生的事件数
count = pd.DataFrame({"region":df2.index,"counts":df2}) #第一列地区,第二列该地区发生的事件数
df2 = df.groupby(by=['weapontype'])['nkill'].sum() #返回武器-该武器杀人总数
缺失值处理
展示缺失比例:特证名-缺失数量(Missing Values)-缺失比例(% of Total Values)
def missing_values_table(df):
# Total missing values
mis_val = df.isnull().sum()
# Percentage of missing values
mis_val_percent = 100 * df.isnull().sum() / len(df)
# Make a table with the results
mis_val_table = pd.concat([mis_val, mis_val_percent], axis=1)
# Rename the columns
mis_val_table_ren_columns = mis_val_table.rename(
columns = {0 : 'Missing Values', 1 : '% of Total Values'})
# Sort the table by percentage of missing descending
mis_val_table_ren_columns = mis_val_table_ren_columns[mis_val_table_ren_columns.iloc[:,1] != 0].sort_values('% of Total Values', ascending=False).round(1)
# Print some summary information
print ("Your selected dataframe has " + str(df.shape[1]) + " columns.\n"
"There are " + str(mis_val_table_ren_columns.shape[0]) +
" columns that have missing values.")
# Return the dataframe with missing information
return mis_val_table_ren_columns
对于竞赛而言最好不要直接删除,最好另作特殊编码
,或者想办法最大程度保留缺失值所带来的信息
。
统计
样本的缺失值数量,作为新的特征。- 将缺失数量做一个
排序
,如果发现3份数据(train、test、unlabeled)都呈阶梯状,于是就可以根据缺失数量将数据划分为若干部分,作为新的特征。 - 使用
随机森林
中的临近矩阵对缺失值进行插值
,但要求数据的因变量没有缺失值。
df = df.drop(['PassengerId','Name','Ticket','Cabin'], axis=1) #对于大量缺失数据的列可直接删除
df = df.dropna() #删除含有NaN数据的行
df = df.fillna('-1') #全部直接人工赋值
df['nkill'].fillna(0, inplace = True) #单列直接人工赋值
df['Embarked'] = df['Embarked'].fillna('S') #离散值填充众数(或-1)
median_age = train['Age'].median() #连续值填充中位数(或均值)
df['Age'] = df['Age'].fillna(median_age)
数据格式转换
其实就相当于简易的编码,需根据分类器的特性来,比如说树模型在求解分裂点的时候只考虑排序分位点。 GBDT更适合连续特征:在树分裂的时候,选择连续特征等于选择了一个特征;而选择离散特征时却等于选择了一个特征的某一维。 对于离散特征,要特征统计后分桶,将特征维度压缩到合理范围。(其实这出于效率,会降低精度,但一定程度上抑制了过拟合)
df.loc[ (df.Sex == 'male'), 'Sex' ] = 0 #令男为0
df.loc[ (df.Sex == 'female'), 'Sex' ] = 1 #令女为1
df['Sex'] = df['Sex'].map( {'male': 0, 'female': 1} ).astype(int) #这种写法更好
特征合并
#1、称谓
df['Title'] = df['Name'].str.extract('([A-Za-z]+)\.', expand=False)
df['Title'] = df['Title'].fillna('NoTitle')
df['Title'] = df['Title'].replace(['Lady', 'Countess','Capt', 'Col','Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare')
df['Title'] = df['Title'].replace(['Mlle','Ms'], 'Miss')
df['Title'] = df['Title'].replace('Mme', 'Mrs')
#2、亲戚数量与子女数量
df['Companions'] = df['Parch'] + df['SibSp']
to_be_dropped.extend(['Parch', 'SibSp'])
指标筛选(From数学系)
指标筛选分为显著性分析和因子分析两步。
- 显著性分析:通过T检验方法分析正负样本,找出能够明显区分样本的指标。
- 因子分析:在上面的基础上对筛选出来的指标计算主成分特征值,从中找出特征值大的指标作为最终评价指标。
特征工程(进阶)
原始特征(取topK)
统计特征
#1、长度
vector_feature=['interest1','interest2','interest5','kw1','kw2','topic1','topic2']
for feat in vector_feature:
data['len_'+feat] = data[feat].apply(lambda x:0 if ((not x.strip()) or str(x)=='-1') else len(x.split(' ')))
#2、平均值
data['meanlen_interest'] = (data['len_interest1']+data['len_interest2']+data['len_interest5'])/3.0
#3、最大值
data['max_ct'] = data['ct'].apply(lambda x: 0 if ((not str(x).strip()) or str(x)=='-1') else max(int(i) for i in str(x).split(' ')))
#4、频数很少的种类,划为其他
def del_little_feature(data,feature):
data1 = data[feature].value_counts().reset_index().rename(columns = {'index':feature,feature:'count'})
data2 = data1[data1['count']<5]
del_kind = data2[feature].values.tolist()
for i in range(len(del_kind)):
data.loc[data[feature]==del_kind[i],feature]=-2
return data
data = del_little_feature(data, 'LBS')
print('LBS is prepared!')
#5、类别变量的nunique特征(如广告主id有多少个不同的广告id)
advert_id_nuq = ['model', 'make', 'os', 'city', 'province', 'user_tags', 'f_channel', 'app_id', 'carrier', 'nnt', 'devtype', 'app_cate_id', 'inner_slot_id']
for fea in advert_id_nuq:
gp1 = data.groupby('advert_id')[fea].nunique().reset_index().rename(columns={fea: "advert_id_%s_nuq_num" % fea})
gp2 = data.groupby(fea)['advert_id'].nunique().reset_index().rename(columns={'advert_id': "%s_advert_id_nuq_num" % fea})
data = pd.merge(data, gp1, how='left', on=['advert_id'])
data = pd.merge(data, gp2, how='left', on=[fea])
排序特征
将原始数值特征进行升序排序,将得到的rank作为新的特征。比如特征是15,10,2,100 ,排序后的新特征就是3,2,1,4 排序特征后通常用SVM来做,训练前需要对特征做归一化
import pandas as pd
feature_type = pd.read_csv('../data/features_type.csv')
numeric_feature = list(feature_type[feature_type.type=='numeric'].feature)
#rank特征的命名:在原始特征前加'r',如'x1'的rank特征为'rx1'
#三份数据集分别排序,使用的时候需要归一化。
#更合理的做法是merge到一起排序,这个我们也试过,效果差不多,因为数据分布相对比较一致。
test = pd.read_csv('../data/test_x.csv')[['uid']+numeric_feature]
test_rank = pd.DataFrame(test.uid,columns=['uid'])
for feature in numeric_feature:
test_rank['r'+feature] = test[feature].rank(method='max')
test_rank.to_csv('../data/test_x_rank.csv',index=None)
离散特征(区间划分)
等值划分
(按照值域均分)
df.loc[ df['Age'] <= 16, 'Age'] = 1
df.loc[ (df['Age'] > 16) & (df['Age'] <= 32), 'Age'] = 2
df.loc[ (df['Age'] > 32) & (df['Age'] <= 48), 'Age'] = 3
df.loc[ (df['Age'] > 48) & (df['Age'] <= 64), 'Age'] = 4
df.loc[ df['Age'] > 64, 'Age'] = 5
df['Age'] = df['Age'].astype(int)
等量划分
(按照样本数均分)
train = pd.read_csv("../data/train_x_rank.csv")
train_x = train.drop(['uid'],axis=1)
train_x[train_x<1500] = 1
train_x[(train_x>=1500)&(train_x<3000)] = 2
train_x[(train_x>=3000)&(train_x<4500)] = 3
...
train_x[train_x>=13500] = 10
#离散特征的命名:在原始特征前加'd',如'x1'的离散特征为'dx1'
rename_dict = {s:'d'+s[1:] for s in train_x.columns.tolist()}
train_x = train_x.rename(columns=rename_dict)
train_x['uid'] = train.uid
train_x.to_csv('../data/train_x_discretization.csv',index=None)
计数特征
计算每个样本离散特征1-10的数量,生成10个新的特征
- 以 uid 为 1 的样本为例,离散化后它的特征是5,3,1,3,3,3,2,4,3,2,5,3,2,3,2…2,2,2,2,2,2,2
- 可以进一步统计离散特征中 1~10 出现的次数ni(i=1,2,…,10),即可得到一个 10 维计数特征
- 基于这 10 维特征训练了 xgboost 分类器,线上得分是 0.58 左右,说明这 10 维特征具有不错的判别性
train_x = pd.read_csv('../data/train_x_discretization.csv')
train_x['n1'] = (train_x==1).sum(axis=1)
train_x['n2'] = (train_x==2).sum(axis=1)
train_x['n3'] = (train_x==3).sum(axis=1)
...
train_x['n10'] = (train_x==10).sum(axis=1)
train_x[['uid','n1','n2','n3','n4','n5','n6','n7','n8','n9','n10']].to_csv('../data/train_x_nd.csv',index=None)
类别特征(连续编码为哑变量)
from sklearn.preprocessing import OneHotEncoder,LabelEncoder
one_hot_feature=['LBS','age','carrier','consumptionAbility','education','gender','house','os','ct','marriageStatus']
for feature in one_hot_feature: #LabelEncoder将各种标签分配一个可数的连续编号
try:
data[feature] = LabelEncoder().fit_transform(data[feature].apply(int))
except:
data[feature] = LabelEncoder().fit_transform(data[feature])
enc = OneHotEncoder() #one-hot表示(这一步须结合分类器特性)
for feature in one_hot_feature:
enc.fit(data[feature].values.reshape(-1, 1))
train_x=enc.transform(train[feature].values.reshape(-1, 1))
test_x = enc.transform(test[feature].values.reshape(-1, 1))
多值特征(top编码)
取值范围不多时候:
value = []
ct_ = ['0','1','2','3','4']
ct_all = list(data['ct'].values)
for i in range(len(data)):
ct = ct_all[i]
va = []
for j in range(5):
if ct_[j] in ct:
va.append(1)
else:va.append(0)
value.append(va)
df = pd.DataFrame(value,columns=['ct0','ct1','ct2','ct3','ct4'])
print('Done')
取值范围很多的时候:
from sklearn.preprocessing import LabelEncoder
vecc = vec['interest1'].value_counts()[:2000] #代表出现数量前2000名的兴趣子片段,调大可提高精度
vecc_Fragment = vecc.index.tolist()
vecc_laber_encoder = LabelEncoder().fit_transform(vecc_Fragment)
vecci = dict(zip(vecc_Fragment,vecc_laber_encoder))
转化率特征(命中率|CTR)
num_ad = train['aid'].value_counts().sort_index()
num_ad_clicked = data_clicked['aid'].value_counts().sort_index()
ratio_ad_clicked = num_ad_clicked / num_ad
ratio_ad_clicked = pd.DataFrame({
'aid': ratio_ad_clicked.index,
'ratio_ad_clicked' : ratio_ad_clicked.values
})
data = pd.merge(data, ratio_ad_clicked, on=['aid'], how='left')
交叉特征(特征组合)
人工:
- 将特征进行两两交叉x*y、 x^2+y^2、 1/x+1/y等等,在生成特征的同时计算与标签列的皮尔逊相关系数保留topK特征。
data['aid_age']=((data['aid']*100)+(data['age']))
data['aid_gender']=((data['aid']*100)+(data['gender']))
data['aid_LBS']=((data['aid']*1000)+(data['LBS']).astype(int))
first_feature = ['app_cate_id', 'f_channel', 'app_id']
second_feature = ["make", "model", "osv1", "osv2", "osv3", "adid", "advert_name", "campaign_id", "creative_id", "carrier", "nnt", "devtype", "os"]
cross_feature = []
for feat_1 in first_feature:
for feat_2 in second_feature:
col_name = feat_1 + "_" + feat_2
cross_feature.append(col_name)
data[col_name] = data[feat_1].astype(str).values + '_' + data[feat_2].astype(str).values
一般来说人工交叉特征效率很低,现有的FM、FFM、DNN模型则在一定程度上做到了自动特征交叉。
- FM: 引入了交叉特征,增加了模型的非线性
- FFM: 把n个特征归属到f个field里,得到nf个隐向量的二次项
- DNN: 能够学习出高阶非线性特征;容易扩充其他类别的特征,比如在特征拥有图片,文字类特征的时候
维度压缩(利用统计)
假设将uid进行onehot编码,因为很多uid都只出现了一次,在一轮训练中,每个uid只会被训练一次,显然特征对应的权重的置信度是很低的。
所以对于很细粒度的特征,又要利用一些手段来进行维度的压缩。
比如将 uid
转化为 uid的出现次数
+ uid的转化率
归一化
对于那些目标变量为输入特征的光滑函数的模型
,如线性回归
、逻辑回归
等,其对输入特征的大小很敏感,有必要
对输入进行归一化
。
而对于那些基于树的模型
,如随机森林
、梯度提升树
等,其对输入特征的大小不敏感,故不需要归一化
。
#将新加入的特征归一化
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(train[['ratio_ad_clicked', 'num_ad_push2user']].values)
train_x = scaler.transform(train[['ratio_ad_clicked', 'num_ad_push2user']].values)
特征选择
可以想象经过上述构造特征的过程能够上到数百个特征,但是我们又不可能对所有特征进行训练,因为里面可能包含很多冗余特征,同时我们需要在少特征的情况下达到多特征的效果(奥卡姆剃刀原理)。
最常用的方法是相关系数法以及模型输出特征重要性的方法。由于数据量问题,并没用采取比较复杂的方法。
特征选择是特征工程里的一个重要问题,其目标是寻找最优特征子集。(我的下一篇博客有更详细介绍)。
引申阅读
stats特征和embedding特征从特征本质上而言其实是相通的,都是作为该特征filed特定的hashkey的信息输入,并且他们的原始信息都来源样本里的用户行为反馈,只不过二者更新方式(即获取特征信息的方式)不同。 embedding特征的更新是通过梯度回传进行软更新(回传的梯度依赖于样本里的用户行为构造的label),stats特征则是直接根据样本里用户行为进行硬更新。
软更新的embedding特征是基于整个模型参数每次进行小幅度更新,对于复现次数足够多的hashkey能够学习到更高的精度和更全面的信息。硬更新的stats特征对于复现次数少的hashkey能够做到更快速收敛。
对于高度稀疏复现次数少的交叉特征,embbeding特征因为hashkey出现次数少导致训练不充分,而交叉统计特征则能够更快速准确地捕获到用户的个性化兴趣。