完整代码: https://github.com/cindycindyhi/kaggle-Titanic
特征工程系列:
为什么有的机器学习项目成功了有的却失败了呢?毕竟算法是有限的改进也是有限的,最主要的因素就是特征的选择了。如果我们有一些与类别非常相关同时又相互独立的特征,学习起来是很容易的,相反就不一定了。通常情况下,并不是直接把原始数据作为特征,而是从中构建一些特征。这是机器学习中的主要工作。在这一步骤中,通常直觉、创造性、魔法和技术一样重要。
当然,机器学习的一个终极目标就是将特征工程过程越来越多地自动化。现在经常采用的一种方式是先自动产生大量的候选特征,然后根据它们与分类类别的信息增益等方法来选取最好的特征。但是,运行包含大量特征的学习器来寻找有用的特征组合太耗时,也容易导致过拟合。还是需要人为的介入特征的选择中。
什么是派生属性呢?派生属性就是从原始数据中得到的一些属性,比如上一节从Age属性经过Factorize得到的Age_bin属性就是一个派生属性,当然这种派生只是非常简单的派生。为什么要对这些属性做各种各样的统计和处理呢,这其实是特征工程的一部分,先构建足够多可能会对结果有意义的属性,然后再从这些候选集中选择我们想要的特征。特征工程非常繁琐,但是对数据挖掘非常重要,一般来说,做一个数据挖掘项目,百分之八十的努力要用在特征工程上。除了基本的转换和interaction属性,我们也要创造性的从原始属性中发现新属性。比如电话号码,可以从中提取出来国家和区域特征。
一 研究业务逻辑提取特征
Titanic的数据集相对较为简单,但是对于一些字符串类型的属性,比如Name我们可以从中提取出来一些可以揭示其社会地位的称号。
名字的长度也会代表一个人的社会地位,社会地位高的人可能会更容易得到救生船。
1 df['Names'] = df['Name'].map(lambda x: len(re.split(' ',x)))
对于名字中间的爵位,可以看到称号有Mr Mrs Master等,经过统计可以看到有以下几种称号:
这些称号有法语还有英语,需要依据当时的文化环境将其归类,如何将其归类可以参考这篇文章trevorstephens.com/post/73461351896/titanic-getting-started-with-r-part-4-feature
1 df['Title'] = df['Name'].map(lambda x: re.compile(",(.*?)\.").findall(x)[0]) 2 df['Title'][df.Title=='Jonkheer'] = 'Master' 3 df['Title'][df.Title.isin(['Ms','Mlle'])] = 'Miss' 4 df['Title'][df.Title == 'Mme'] = 'Mrs' 5 df['Title'][df.Title.isin(['Capt', 'Don', 'Major', 'Col', 'Sir'])] = 'Sir' 6 df['Title'][df.Title.isin(['Dona', 'Lady', 'the Countess'])] = 'Lady' 7 df['Title_id'] = pd.factorize(df.Title)[0]+1
对于Ticket属性也需要处理,可以看到Ticket字段有的全是数字有的是字母和数字的集合,进一步对数据分析发现约25%的数据有前缀,前缀共有45种,如果把 . 和 / 去掉的话还剩29种,数字部分也有一定的规律:以1开头的一般是一等舱2开头的是二等舱3开头的是三等舱,4-9开头的大都是三等舱。以上这些数据告诉我们处理Ticket是有意义的,能够发现其内部蕴涵的信息。
1 def processTicket(): 2 global df 3 df['TicketPrefix'] = df['Ticket'].map(lambda x: getTicketPrefix(x.upper())) 4 df['TicketPrefix'] = df['TicketPrefix'].map(lambda x: re.sub\ 5 ('[\.?\/?]','',x)) 6 df['TicketPrefix'] = df['TicketPrefix'].map(lambda x:re.sub\ 7 ('STON','SOTON',x)) 8 df['TicketPrefix'] = pd.factorize(df['TicketPrefix'])[0] 9 df['TicketNumber'] = df['Ticket'].map(lambda x: getTicketNumber(x) ) 10 df['TicketNumberLength'] = df['TicketNumber'].map(lambda x: len(x)).\ 11 astype(int) 12 df['TicketNumberStart'] = df['TicketNumber'].map(lambda x: x[0:1]).\ 13 astype(int) 14 df['TicketNumber'] = df['TicketNumber'].astype(int) 15 def getTicketPrefix(ticket): 16 match = re.compile("([a-zA-Z\.\/]+)").search(ticket) 17 if match: 18 return match.group() 19 else: 20 return 'U' 21 def getTicketNumber(ticket): 22 match = re.compile("([0-9]+$)").search(ticket) 23 if match: 24 return match.group() 25 else: 26 return '0'
二 简单组合属性提取特征
一些属性可以从它本身的数据里提取一些信息,有些属性则需要和其他属性组合来产生信息。比如对淘宝上的一个商品来说,购买数/点击率可以反应商品的转化率,也是商品的一个非常重要的特征。
对于Titanic来说,我们用Age*Pclass组合产生一个属性,虽然没有一个名词来解释它,但是从结果数据上来看,我们增大了年纪大的人的权重也提高了高等舱的权重,从最后幸存的结果上看,这个组合还是有意义的。除了这两个属性之外,我们还可以对其他数值属性进行数学运算,以得到更大的候选特征集。
1 numerics = df.loc[:, ['Age_scaled', 'Fare_scaled', 'Pclass_scaled', 'Parch_scaled', 'SibSp_scaled', 2 'Names_scaled', 'CabinNumber_scaled', 'Age_bin_id_scaled', 'Fare_bin_id_scaled']] 3 print "\nFeatures used for automated feature generation:\n", numerics.head(10) 4 5 new_fields_count = 0 6 for i in range(0, numerics.columns.size-1): 7 for j in range(0, numerics.columns.size-1): 8 if i <= j: 9 name = str(numerics.columns.values[i]) + "*" + str(numerics.columns.values[j]) 10 df = pd.concat([df, pd.Series(numerics.iloc[:,i] * numerics.iloc[:,j], name=name)], axis=1) 11 new_fields_count += 1 12 if i < j: 13 name = str(numerics.columns.values[i]) + "+" + str(numerics.columns.values[j]) 14 df = pd.concat([df, pd.Series(numerics.iloc[:,i] + numerics.iloc[:,j], name=name)], axis=1) 15 new_fields_count += 1 16 if not i == j: 17 name = str(numerics.columns.values[i]) + "/" + str(numerics.columns.values[j]) 18 df = pd.concat([df, pd.Series(numerics.iloc[:,i] / numerics.iloc[:,j], name=name)], axis=1) 19 name = str(numerics.columns.values[i]) + "-" + str(numerics.columns.values[j]) 20 df = pd.concat([df, pd.Series(numerics.iloc[:,i] - numerics.iloc[:,j], name=name)], axis=1) 21 new_fields_count += 2 22 23 print "\n", new_fields_count, "new features generated"
这个过程自动产生大量的特征,这里用了9个特征产生了176个特征,可能这些特征有些过于多了,但是它只是一个候选集,我们可以通过一些处理筛选掉一些特征。当然有些模型也很适合大量特征的训练集,比如随机森林(有论文验证,随机森林是分类算法中表现最好的模型)。
产生的这些特征可能高度相关于原始特征,线性模型处理这类特征时会产生multicollinearity问题,可以用计算这些特征的皮尔逊相关系数,筛选相关性特征。如果用随机森林模型训练的话,可以不需要这个步骤。
三 用PCA进行维归约
通过上面三个部分的处理,我们得到了具有大量特征的维度很高的数据集,特征较多不能直接用来作为模型输入,一是因为这些特征间具有多重共线性,可能 会导致空间的不稳定;二是因为高维空间本身具有稀疏性,一维正态分布有68%的值落于正负标准差之间,而在十维空间上只有0.02%;三是由于过多的属性 会使挖掘需要很长时间。对于一些模型来说,比如使用L1(Lasso),当有大量属性时效果很好,因为它可以有效忽略掉噪声变量。而一些模型则容易过拟 合。
数据归约技术可以用来得到数据集的规约表示,它小得多,但仍接近于保持原始数据的完整性。也就是说,在归约后的数据集上进行数据挖掘将更加有效,仍然产生几乎相同的数据分析结果。
PCA(主成份分析)是一种维归约的方法,它搜索k个最能代表数据的n维正交向量,将原始数据投影到一个小的多的空间上,导致维归约。PCA通过创建一个替换的较小的变量集组合属性的基本要素。具体原理及python的实现过程可以参考这篇blog Implementing a Principal Component Analysis (PCA) in Python step by step
我们可以直接使用scikit-learn的PCA函数进行维规约
1 X = df.values[:, 1::]
2 y = df.values[:, 0]
3 variance_pct = .99
4 # Create PCA object
5 pca = PCA(n_components=variance_pct)
6 # Transform the initial features
7 X_transformed = pca.fit_transform(X,y)
8 # Create a data frame from the PCA'd data
9 pcaDataFrame = pd.DataFrame(X_transformed)
实验发现,PCA对于线性模型(不使用Lasso)非常有效,但是对于随机森林模型没有提高。