Python特征工程篇

For Data Mining

Posted by Jiayue Cai on March 8, 2018

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出现次数少导致训练不充分,而交叉统计特征则能够更快速准确地捕获到用户的个性化兴趣。