打造数据科学作品集:机器学习项目(上)


打造数据科学作品集:机器学习项目(上)_java

程派微信号:codingpy

本文是「打造数据科学作品集」的第三篇,如果你喜欢并希望及时获取本系列的最新文章,可以订阅我们。

全文大约 25000 字符,读完大约需要 37 分钟。分为上下两部分发布。

作者:Vik Paruchuri,译者:唐晓霆,校对:EarlGrey,出品

数据科学公司在招聘时越来越看重个人作品集,原因在于作品集是衡量实际能力最好的方式之一。好消息是,你完全掌控着自己的作品集。如果付出一些努力,你就可以打造出令用人单位印象深刻的高质量作品集。

想要打造高质量作品集,第一步需要搞清楚应该在作品中展现什么能力。公司希望数据科学家具备的能力(也就是他们希望作品集能够展示的能力)包括:

  • 沟通能力

  • 与他人协作能力

  • 技术能力

  • 数据推断能力

  • 主观能动性

一个好的作品集一般由多个项目构成,每一个项目展示以上 1-2 个能力点。本文是讲述如何建立一个丰满的数据科学作品集的第三篇。本文将介绍如何打造作品集中的第二个项目,以及如何创建一个完整的机器学习项目。最后,你会拥有一个可以展示合理解释数据能力和技术能力的项目。如果你想一窥项目全貌的话,这里是完整的项目文件。

一个完整的项目

作为一个数据科学家,有时候你会被叫去分析一个数据集,然后设法用数据讲故事。这时,良好的沟通和清晰的思路是非常重要的。像我们在之前用到的 Jupyter notebook 这样的工具,就能很好地帮助你做到这点。客户的预期是总结你发现的演示报告或文档。

然而,有时候你也会被叫去做有运营价值的项目。一个有运营价值的项目会直接影响公司的日常运营,而且会被大家频繁使用。类似这样的任务可能会是“设计一个可以预测用户变动率的算法”, 或者是“创建一个自动给文章打标签的模型”。在这类情况下,讲故事的能力就没有技术能力重要了。你需要能够分析数据集,理解它,然后编写可以处理这些数据的脚本。这些脚本还要跑的快,耗费最少的资源,如内存,这些都是很常见的要求。通常这些脚本需要频繁运行,所以最终的交付品就变成了这些脚本自身,而不是报告。这些成果经常集成到运营流程中,甚至可能会直接面对用户。

创建一个完整项目,要求你:

  • 理解整个项目环境

  • 探索数据并找到其中的细微差别

  • 建立一个结构良好的项目,使其容易集成至运营流程中

  • 写出既运行快又占用最少系统资源的高性能代码

  • 为代码的安装和使用写出良好的文档,方便他人使用

为了高效地创建这样的项目,我们需要和许多文件打交道。我们非常推荐使用像Atom 的文档编辑器,或者像 PyCharm 这样的IDE。这些工具允许你在不同文件间跳转,并且可以编辑不同类型的文件,比如 markdown 文件,Python 文件和 csv 文件。给你的代码建立良好的结构,方便进行版本管理,并上传到像Github这样的代码协作工具。打造数据科学作品集:机器学习项目(上)_java_02

在本文中,我们会使用 Pandas 和 scikit-learn 等库。我们会大量用到 Pandas 的DataFrame,这使得在 Python 中读取和处理表格数据变得非常简单。

寻找优质数据集

寻找优质数据集进行完整的项目分析很困难。数据集需要足够大,大到出现内存和性能的限制。还需要具备运营价值。举个例子,这个数据集中包含了美国大学的招生条件、毕业率和毕业生未来收入的数据。这就是一个可以用来讲故事的优质数据集。然而,如果你仔细想想,就会发现这里面没有足够的细节来建立一个完整的项目。

举例说,你可以告诉别人如果他们去某些(好)大学,他们未来的潜在收入就会更高,但是这只需要一个很快的查找比较就可以完成,没有足够的空间去展示你的技术能力。你也可以发现如果大学有更高的入学条件,它们的毕业生就更有可能获得高薪,但这些就更偏向于讲故事,而非运营价值了。

当你有 GB 以上的数据量时,或者当你想要预测一些数据细节,内存和性能限制就会逐渐凸现出来,因为得对数据集运行算法运算。

一个优质数据集允许你编写一系列脚本对数据做变形,从而回答一些动态问题。股票价格就是一个很好的数据集。你可以根据这些数据预测第二天的股价走势,并且在闭市的时候把新数据提供给算法。这可以帮助你执行交易,甚至是获取利润。这就不是在讲故事了 — 而是直接产生价值。

下面是一些能够找到优质数据集的地方:

浏览这些数据集时,想一想如果有这些数据集,人们可能会问什么问题,然后再想想这些问题是否是一次性的(“S&P 500 和房价的相关性是怎样的?”),或是持续性的(“你能预测股票价格吗?”)。这里的关键在于找到那些持续性的问题,这些问题需要多次运行,并输入不同的数据才能回答。

本文中,我们选择房利美(Fannie Mae)的贷款数据。房利美是一个由美国政府资助的从贷方手里购买房贷的企业。购买房贷之后,它会把这些房贷打包为一些由房贷支撑的证券(MBS)里,再卖出去。这样就帮助了贷方贷出更多的房贷,并给市场创造了更大的流动性。这从理论上说就会产生更多的房屋业主,进而产生更好的房贷政策。然而从借方的角度来看,情况并没有什么不同。

房利美公开了两种数据 — 收购到的房贷数据,和房贷表现情况数据。在最理想的情况下,一个人从贷方贷了款,然后一直还钱,直到贷款还清。然而,借方有几次没有还款,就可能会导致失去抵押品赎回权。这时,银行就会获得房屋的所有权,因为没还清房贷。房利美记录了哪些房贷没有还,哪些房贷需要取消抵押品赎回权。这个数据每个季度发布一次,而且会滞后一年。撰写本文时,最近的数据集是 2015 年第一季度。

房利美购买房贷时会发布收购信息,其中含有许多关于借方的信息,包括信用评分、房贷和房屋的信息。之后,每个季度发布房贷表现数据,涵盖了借方的支付信息,和抵押权的状态。房贷表现信息里可能有很多行。你可以这么想这个事,收购信息表示房利美现在控制了房贷,表现信息则包括了一系列房贷的状态更新。有的状态可能会说这笔贷款在某个季度借方抵押权被取消了。打造数据科学作品集:机器学习项目(上)_java_03

一个借方失去了抵押品赎回权(止赎)的房子正在被卖

选择分析角度

对于房利美数据集,我们可以有多个分析角度。我们可以:

  • 尝试预测一个止赎了的房屋的售价

  • 预测一个借方的还款历史

  • 计算出一个被收购时房贷的评分

重要的事是要坚持一个角度。一次专注于太多事情会很难做成一个优秀的项目。选择一个有足够细节的角度这点也很重要。以下是一些没有多少细节的角度:

  • 哪家银行卖给房利美最多止赎的房贷

  • 借方信用评分的趋势

  • 哪些房屋类型最经常止赎

  • 房贷金额和止赎售价的关系

上述的这些角度都很有趣,如果我们关注讲故事的话是很棒的话题,但对于一个运营性的项目来说就没那么好了。

有了房利美数据集,我们将尝试仅仅使用收购房贷时的数据,预测房贷是否会被止赎。实际上,我们会为每一份房贷“打分”,这个分数表示房利美是否应该购买这份房贷。这将是一个良好的基础,也是一个很棒的作品。

理解数据

我们首先快速查看原始数据文件。下面是 2012 年第一季度收购数据的前几行:

100000853384|R|OTHER|4.625|280000|360|02/2012|04/2012|31|31|1|23|801|N|C|SF|1|I|CA|945||FRM| 100003735682|R|SUNTRUST MORTGAGE INC.|3.99|466000|360|01/2012|03/2012|80|80|2|30|794|N|P|SF|1|P|MD|208||FRM|788 100006367485|C|PHH MORTGAGE CORPORATION|4|229000|360|02/2012|04/2012|67|67|2|36|802|N|R|SF|1|P|CA|959||FRM|794

下面是 2012 年第一季度的表现数据的前几行:

100000853384|03/01/2012|OTHER|4.625||0|360|359|03/2042|41860|0|N|||||||||||||||| 100000853384|04/01/2012||4.625||1|359|358|03/2042|41860|0|N|||||||||||||||| 100000853384|05/01/2012||4.625||2|358|357|03/2042|41860|0|N||||||||||||||||

在编写代码之前,花点时间去理解数据是很有用的。尤其对于运营型项目而言,因为我们没有互动式地去探索数据,很难发现某些细节,除非一开始就找到它们。这种情况下,第一步就是去房利美的网站上读一读有关数据集的材料:

读完这些材料之后,我们知道了一些有用的关键信息:

  • 从 2000 年到现在,每个季度都有一个收购文件和表现文件。数据滞后一年,所以最近的数据是 2015 年的

  • 这些文件是文本形式,用 | 作为分隔符

  • 这些文件没有头文档,但是我们有所有列名称的列表

  • 全部加起来,这些文件共包含 2.2 千万个房贷的数据

  • 因为表现文件涵盖了之前的房贷信息,所以早些时候的房贷会有更多的表现数据(举个例子,2014 年收购的房贷不会有太多表现信息)

在设计项目结构和处理数据时,这些信息能帮助我们节省一大笔时间。

设计项目结构

在开始下载和探索数据之前,设计好项目结构是非常重要的。在打造一个完整的项目时,我们的主要目标是:

  • 输出一个可行的解决方案

  • 解决方案运行快且消耗最少资源

  • 让他人可以很容易地扩展项目

  • 让他人可以容易地理解代码

  • 写的代码越少越好

为了达到这些目标,我们要设计好项目的结构。一个结构良好的项目遵从以下规范:

  • 数据文件和源代码分开

  • 原始数据和生成数据分开

  • 有一个 README.md 文件,介绍如何安装并使用这个项目

  • 有一个 requirements.txt 文件,包含项目所需的所有模块

  • 有一个 settings.py 文件,包含所有其他文件所需的设置

    • 例如,如果有很多Python脚本都读取同一个文件,就不如让它们都导入settings并从这一个地方来得到文件

  • 有一个 .gitignore 文件,来防止一些特别大的或者私密的文件被提交到 Git

  • 把任务分成几步,并分别放在可以单独执行的文件里

    • 例如, 用一个文件读取数据,一个文件建立特征,一个文件执行预测

  • 储存中间值。例如,一个脚本可能会输出一个文件,这个文件又会被另外一个脚本读取

    • 这使得我们可以在数据处理的流程中做一些改动,而又不需要重新计算

该项目的文件结构如下:

loan-prediction ├── data ├── processed ├── .gitignore ├── README.md ├── requirements.txt ├── settings.py

创建初始文件

首先,创建 loan-prediction 文件夹。在这个文件夹里,创建 data 文件夹和 processed 文件夹。第一个用来储存原始数据,第二个用来储存所有中间值。

接着,创建 .gitignore 文件。.gitignore 文件会确保一些文件会被 git 忽略,并不会被推送到 Github 上。OS X 在每个文件夹里创建的 .DS_Store 文件就是这类需要忽略的文件。要入门 .gitignore 文件,可以参考这里。还要忽略一些体积太大的文件,而且房利美的条款并不允许二次发布这些文件,所以我们应该在 .gitignore 文件最后加上这两行:

data processed

这里是本项目的示例 .gitignore 文件。

接着,创建 README.md ,这有助于人们理解项目。.md 代表这个文件是 markdown 格式。Markdown 能让你直接用纯文本写作,但是如果想的话,也可以添加一些好看的排版格式。这里是一个 markdown 指南。如果你往 Github 上传了一个叫 README.md 的文件,Github 会自动处理该文件,把它作为主页展示给浏览者。这里有一个例子。

目前,只需要在README.md里面放一段简短的描述:

Loan Prediction -----------------------

Predict whether or not loans acquired by Fannie Mae will go into foreclosure.
 Fannie Mae acquires loans from other lenders as a way of inducing them to lend more.  
 Fannie Mae releases data on the loans it has acquired and their performance afterwards [
here](http://www.fanniemae.com/portal/funding-the-market/data/loan-performance-data.html).

现在,创建 requirements.txt 文件。这可以帮助其他人安装我们的项目。目前还不知道具体需要哪些库,但下面这些是一个好的起点:

pandas matplotlib scikit-learn numpy ipython scipy

以上是用 Python 作数据分析最常用的几个库,在这个项目中应该会用到它们。这里是本项目的示例 requirements 文件。

创建 requirements.txt 之后,你应该安装这些模块。在本文中,我们使用 Python 3 。如果你还没有安装 Python,建议使用 Anaconda,这是一个可以安装上述所有模块的 Python 安装器。

最后,创建一个空白的 settings.py 文件,因为项目还没有任何设置。

获得数据

创建好整个项目的框架之后,就可以获取原始数据了。

房利美对数据下载有一些限制,所以你得先注册一个账号。下载页面在这里。注册完账户后,就可以随意下载贷款数据了。文件是 zip 格式,解压之后也挺大的。

本文中,我们会把 2012 年第一季度到 2015 年第一季度之间的所有数据都下载下来。然后解压文件,解压之后,删除原始的 .zip 文件。最后,loan-prediction 文件夹的结构应该类似这样:

loan-prediction ├── data │   ├── Acquisition_2012Q1.txt │   ├── Acquisition_2012Q2.txt │   ├── Performance_2012Q1.txt │   ├── Performance_2012Q2.txt │   └── ... ├── processed ├── .gitignore ├── README.md ├── requirements.txt ├── settings.py

下载完数据之后,可以用 head 和 tail 等 shell 命令去观察文件的前几行和后几行。有没有不需要的列?查看数据时可以参考一下介绍列名称的 PDF 文件

读取数据

有两个问题,使得直接处理数据比较困难:

  • 收购和表现数据集被分散在了许多文件里

  • 所有文件都缺少头文档

在开始处理这些数据之前,需要把所有的收购数据集中到一个文件,所有的表现数据集中到一个文件。每个文件只需要包含我们关心的列,和正常的头文档。这里有一个小问题,即表现数据特别大,所以可能的话我们得删减一些列。

第一步是在 settings.py 里面增添一些变量,包含到原始数据和中间数据的路径。我们也会加上一些之后会有用的设置:

DATA_DIR = "data" PROCESSED_DIR = "processed" MINIMUM_TRACKING_QUARTERS = 4 TARGET = "foreclosure_status" NON_PREDICTORS = [TARGET, "id"] CV_FOLDS = 3

把路径放在 settings.py 里面,会使得它们统一在一个地方,使得今后改动变得简单。当许多文件都用了同一些变量的时候,把它们放在一起会比分别在每个文件里做改动要简单得多。这里是该项目的示例 settings.py 文件。

第二步是创建一个叫做 assemble.py 的文件,这个文件会把分散的数据组合成 2 个文件。运行 python assemble.py 后,会在 processed 文件夹里面得到 2 个数据文件。

然后再 assemble.py 中写代码。首先,给每个文件定义头文档,所以我们需要查看解释列名称的 PDF 文档,然后为收购数据和表现数据文件分别创建一个列表,表示其中的行。

HEADERS = {
   "Acquisition": [
       "id",
       "channel",
       "seller",
       "interest_rate",
       "balance",
       "loan_term",
       "origination_date",
       "first_payment_date",
       "ltv",
       "cltv",
       "borrower_count",
       "dti",  
       "borrower_credit_score",
       "first_time_homebuyer",
       "loan_purpose",
       "property_type",      
       "unit_count",        
       "occupancy_status",        
       "property_state",        
       "zip",        
       "insurance_percentage",        
       "product_type",        
       "co_borrower_credit_score"     ],    
   "Performance": [        
       "id",      
       "reporting_period",        
       "servicer_name",        
       "interest_rate",        
       "balance",        
       "loan_age",        
       "months_to_maturity",        
       "maturity_date",        
       "msa",        
       "delinquency_status",        
       "modification_flag",        
       "zero_balance_code",        
       "zero_balance_date",        
       "last_paid_installment_date",        
       "foreclosure_date",        
       "disposition_date",        
       "foreclosure_costs",        
       "property_repair_costs",      
       "recovery_costs",        
       "misc_costs",        
       "tax_costs",        
       "sale_proceeds",        
       "credit_enhancement_proceeds",        
       "repurchase_proceeds",        
       "other_foreclosure_proceeds",        
       "non_interest_bearing_balance",        
       "principal_forgiveness_balance"     ] }

下一步是定义需要保留哪些列。因为我们关心的房贷只是关于它有没有被止赎,所以可以从表现数据里面丢弃很多列(不影响是否止赎的数据)。但是我们需要保留所有收购数据,因为我们想要尽可能多的房贷信息(毕竟我们要在收购房贷时预测是否会被止赎)。丢弃一些列可以省下一些磁盘空间和内存,同时也会加速代码的运行速度。

SELECT = {
   "Acquisition": HEADERS["Acquisition"],
   "Performance": [
       "id",
       "foreclosure_date"     ] }

接下来,写一个函数来拼接所有的数据集。下面的代码会:

  • 导入一些需要的库,包括settings

  • 定义函数 concatenate,它可以:

    • 如果文件的格式不对(并不是以预期的前缀开始),就忽略它

    • 用 Pandas 的read_csv函数,把文件读取到一个 DataFrame 里

    • 把所有的 DataFrame 拼接在一起

    • 把拼接好的 DataFrame 输出成一个文件

    • 把分隔符设置为 | ,正确读取数据

    • 数据现在没有头文档,所以把 header 设置成 None

    • 把 HEADERS 字典里的值设置为列的名称,这些会成为 DataFrame 里面的列名称

    • 只把加在 SELECT 里面的列从 DataFrame 里面选出来

    • 拿到 data 目录里面所有文件的名字

    • 遍历每个文件

import os
import settings
import pandas as pd

def concatenate(prefix="Acquisition"):     files = os.listdir(settings.DATA_DIR)     full = []
   for f in files:
       if not f.startswith(prefix):
           continue         data = pd.read_csv(os.path.join(settings.DATA_DIR, f), sep="|", header=None, names=HEADERS[prefix], index_col=False)         data = data[SELECT[prefix]]         full.append(data)     full = pd.concat(full, axis=0)     full.to_csv(os.path.join(settings.PROCESSED_DIR, "{}.txt".format(prefix)), sep="|", header=SELECT[prefix], index=False)

可以用参数 Acquisition 和 Performance 分别调用上面的函数,把所有的收购和表现文件拼接在一起。下面的代码会:

  • 只当脚本是在命令行用 python assemble.py 执行时运行

  • 拼接所有文件,并输出成两个文件:

    • processed/Acquisition.txt

    • processed/Performance.txt

if __name__ == "__main__":     concatenate("Acquisition")     concatenate("Performance")

我们现在有了一个模块化的 assemble.py 文件,既容易运行,又易扩展。像这样把大问题划分成小问题,我们将项目变得更简单。我们把不同文件分离开,定义它们之间的数据,而不是用一个脚本做所有的事情。当你在做一个大项目的时候,这样做通常很好,因为更改一些文件后不会产生不可预期的结果。

完成 assemble.py 脚本后,运行 python assemble.py 。你可以在这里找到完整的脚本。

这会在 processed 目录里面输出两个文件:

loan-prediction ├── data │   ├── Acquisition_2012Q1.txt │   ├── Acquisition_2012Q2.txt │   ├── Performance_2012Q1.txt │   ├── Performance_2012Q2.txt │   └── ... ├── processed │   ├── Acquisition.txt │   ├── Performance.txt ├── .gitignore ├── assemble.py ├── README.md ├── requirements.txt ├── settings.py

表现数据计算

下一步就是从 processed/Performance.txt 数据中计算一些值。我们想做的就是预测一间房产以后会不会被止赎。为了弄明白这一点,我们只需要看看表现数据里面的房贷是否有一个 foreclosure_date 。如果 foreclosure_date 是 None,那么这间房产就没有被止赎。我们也需要规避那些在表现数据里没有多少历史数据的房贷,要做到这一点,通过计算它们在表现数据里面累计有多少行就可以。

可以用下面的方法来思考收购数据和表现数据的关系:打造数据科学作品集:机器学习项目(上)_java_04

我们发现,收购数据里每一行都对应了表现数据中的多行。在表现数据中,当止赎发生的时候,当季度的 foreclosure_date 就会出现日期,在这之前都应该是空白的。一些贷款从未被止赎,所以与之相关的表现数据里的 foreclosure_date都是空白的。

我们需要计算 foreclorsure_status ,这是一个布尔值,代表一个贷款 id是否有被止赎过。我们也要计算 performance_count ,也就是每个 id 在表现数据里有多少行。

有几种方法可以计算 performance_count

  • 读取所有的表现数据,然后用 Pandas 的 groupby 方法求每个贷款 id相关联的行数,同时 id 对应的 foreclosure_date 有没有不是 None 过。

    • 这样做的好处是实现的语法很简单

    • 这样做的坏处是读取 129236094 行数据会花很多内存,而且极其慢

  • 我们可以读取所有的表现数据,然后在收购数据 DataFrame 上使用 apply,从而求得每个 id 的计数

    • 好处是概念上很简单

    • 坏处仍然是读取 129236094 行数据会花很多内存,而且极其慢

  • 我们可以遍历表现数据里的每一行,然后保存一个单独的包含计数的字典

    • 好处是不需要把所有数据一起读取进内存,所以这样做会很快,也会优化内存

    • 坏处是得花长一点时间来理清概念和实现,而且需要手工地解析每一行

把所有数据一并加载会花很多内存,所以我们采用第三种方法。我们所要的就是遍历表现数据里面的每一行,并且保存一个包含每个 id 的计数字典。在字典里面,我们记录下表现数据里面每个 id 出现了多少次,并且 foreclosure_date是否为非 None 过。这样就能求出 foreclosure_status 和 performance_count 。

新建一个文件 annotate.py ,并加入用来计算的代码。在下面的代码中,我们会:

  • 导入需要的库

  • 定义一个叫做 count_performance_rows 的函数

    • 根据分隔符 | 分割字符串

    • 检查 loan_id 是否在 counts 字典里

    • 给 load_id 对应的 performance_count 加1

    • 如果 date 不是 None,那么我们就知道这笔贷款止赎了,所以设置相应的 foreclosure_status

    • 如果不在,把它加入 counts

    • 打开 precessed/Performance.txt 。这不会把文件读取进内存,而仅仅是打开一个文件句柄,一行一行地读取文件内容

    • 遍历文件里的每一行

import os
import settings
import pandas as pd

def count_performance_rows():     counts = {}
   with open(os.path.join(settings.PROCESSED_DIR, "Performance.txt"), 'r') as f:
       for i, line in enumerate(f):
           if i == 0:                # Skip header row                 continue             loan_id, date = line.split("|")             loan_id = int(loan_id)
           if loan_id not in counts:                 counts[loan_id] = {
                   "foreclosure_status": False,
                   "performance_count": 0                 }             counts[loan_id]["performance_count"] += 1             if len(date.strip()) > 0:                 counts[loan_id]["foreclosure_status"] = True     return counts

得到计算结果

创建建了 counts 字典后,我们可以用一个函数抽取出和传入的 load_id 和 key 相应的值了:

def get_performance_summary_value(loan_id, key, counts):     value = counts.get(loan_id, {
       "foreclosure_status": False,
       "performance_count": 0     })
   return value[key]

上面这个函数会从 counts 字典里返回相应的值,并且可以让我们为收购数据里每一行添加 foreclosure_status 和 performance_count 值。字典的 get 方法在没有找到 key 的情况下就会返回一个默认值,所以就算没有找到也能返回合理的默认值。

欢迎转发至朋友圈。如无特殊注明,本公号所发文章均为原创或编译,如需转载,请联系「编程派」获得授权。