構建你的資料科學作品集:機器學習專案

3 贊 回覆發表於2016-10-28

這是這個系列釋出的第三篇關於如何構建資料科學作品集Data Science Portfolio的文章。如果你喜歡這個系列並且想繼續關注,你可以在訂閱頁面的底部找到連結

資料科學公司在決定僱傭時越來越關注你在資料科學方面的作品集Portfolio。這其中的一個原因是,這樣的作品集是判斷某人的實際技能的最好的方法。好訊息是構建這樣的作品集完全要看你自己。只要你在這方面付出了努力,你一定可以取得讓這些公司欽佩的作品集。

構建高質量的作品集的第一步就是知道需要什麼技能。公司想要在資料科學方面擁有的、他們希望你能夠運用的主要技能有:

  • 溝通能力
  • 協作能力
  • 技術能力
  • 資料推理能力
  • 動機和主動性

任何好的作品集都由多個專案表現出來,其中每個都能夠表現出以上一到兩點。這是本系列的第三篇,本系列我們主要講包括如何打造面面俱到的資料科學作品集。在這一篇中,我們主要涵蓋了如何構建組成你的作品集的第二個專案,以及如何建立一個端對端的機器學習專案。在最後,我們將擁有一個展示你的資料推理能力和技術能力的專案。如果你想看一下的話,這裡有一個完整的例子。

一個端到端的專案

作為一個資料科學家,有時候你會拿到一個資料集並被問如何用它來講故事。在這個時候,溝通就是非常重要的,你需要用它來完成這個事情。像我們在前一篇文章中用過的,類似 Jupyter notebook 這樣的工具,將對你非常有幫助。在這裡你能找到一些可以用的報告或者總結文件。

不管怎樣,有時候你會被要求建立一個具有操作價值的專案。具有操作價值的專案將直接影響到公司的日常業務,它會使用不止一次,經常是許多人使用。這個任務可能像這樣 “建立一個演算法來預測週轉率”或者“建立一個模型來自動對我們的文章打標籤”。在這種情況下,技術能力比講故事更重要。你必須能夠得到一個資料集,並且理解它,然後建立指令碼處理該資料。這個指令碼要執行的很快,佔用系統資源很小。通常它可能要執行很多次,指令碼的可使用性也很重要,並不僅僅是一個演示版。可使用性是指整合進操作流程,並且甚至是是面向使用者的。

端對端專案的主要組成部分:

  • 理解背景
  • 瀏覽資料並找出細微差別
  • 建立結構化專案,那樣比較容易整合進操作流程
  • 執行速度快、佔用系統資源小的高效能程式碼
  • 寫好安裝和使用文件以便其他人用

為了有效的建立這種型別的專案,我們可能需要處理多個檔案。強烈推薦使用 Atom 這樣的文字編輯器或者 PyCharm 這樣的 IDE。這些工具允許你在檔案間跳轉,編輯不同型別的檔案,例如 markdown 檔案,Python 檔案,和 csv 檔案等等。結構化你的專案還利於版本控制,並上傳一個類似 Github 這樣的協作開發工具上也很有用。

Github 上的這個專案

在這一節中我們將使用 Pandasscikit-learn 這樣的庫,我們還將大量用到 Pandas DataFrames,它使得 python 讀取和處理表格資料更加方便。

找到好的資料集

為一個端到端的作品集專案的找到好的資料集很難。在記憶體和效能的限制下,資料集需要儘量的大。它還需要是實際有用的。例如,這個資料集,它包含有美國院校的錄取標準、畢業率以及畢業以後的收入,是個很好的可以講故事的資料集。但是,不管你如何看待這個資料,很顯然它不適合建立端到端的專案。比如,你能告訴人們他們去了這些大學以後的未來收入,但是這個快速檢索卻並不足夠呈現出你的技術能力。你還能找出院校的招生標準和更高的收入相關,但是這更像是常理而不是你的技術結論。

這裡還有記憶體和效能約束的問題,比如你有幾千兆的資料,而且當你需要找到一些差異時,就需要對資料集一遍遍執行演算法。

一個好的可操作的資料集可以讓你構建一系列指令碼來轉換資料、動態地回答問題。一個很好的例子是股票價格資料集,當股市關閉時,就會給演算法提供新的資料。這可以讓你預測明天的股價,甚至預測收益。這不是講故事,它帶來的是真金白銀。

一些找到資料集的好地方:

  • /r/datasets – 有上百的有趣資料的 subreddit(Reddit 是國外一個社交新聞站點,subreddit 指該論壇下的各不同版塊)。
  • Google Public Datasets – 通過 Google BigQuery 使用的公開資料集。
  • Awesome datasets – 一個資料集列表,放在 Github 上。

當你檢視這些資料集時,想一下人們想要在這些資料集中得到什麼答案,哪怕這些問題只想過一次(“房價是如何與標準普爾 500 指數關聯的?”),或者更進一步(“你能預測股市嗎?”)。這裡的關鍵是更進一步地找出問題,並且用相同的程式碼在不同輸入(不同的資料)上執行多次。

對於本文的目標,我們來看一下 房利美Fannie Mae貸款資料。房利美是一家在美國的政府贊助的企業抵押貸款公司,它從其他銀行購買按揭貸款,然後捆綁這些貸款為貸款證券來轉賣它們。這使得貸款機構可以提供更多的抵押貸款,在市場上創造更多的流動性。這在理論上會帶來更多的住房和更好的貸款期限。從借款人的角度來說,它們大體上差不多,話雖這樣說。

房利美髮布了兩種型別的資料 – 它獲得的貸款的資料,和貸款償還情況的資料。在理想的情況下,有人向貸款人借錢,然後還款直到還清。不管怎樣,有些人多次不還,從而喪失了抵押品贖回權。抵押品贖回權是指沒錢還了被銀行把房子給收走了。房利美會追蹤誰沒還錢,並且哪個貸款需要收回抵押的房屋(取消贖回權)。每個季度會發布此資料,釋出的是滯後一年的資料。當前可用是 2015 年第一季度資料。

“貸款資料”是由房利美髮布的貸款發放的資料,它包含借款人的資訊、信用評分,和他們的家庭貸款資訊。“執行資料”,貸款發放後的每一個季度公佈,包含借貸人的還款資訊和是否喪失抵押品贖回權的狀態,一個“貸款資料”的“執行資料”可能有十幾行。可以這樣理解,“貸款資料”告訴你房利美所控制的貸款,“執行資料”包含該貸款一系列的狀態更新。其中一個狀態更新可以告訴我們一筆貸款在某個季度被取消贖回權了。

一個沒有及時還貸的房子就這樣的被賣了

選擇一個角度

這裡有幾個我們可以去分析房利美資料集的方向。我們可以:

  • 預測房屋的銷售價格。
  • 預測借款人還款歷史。
  • 在獲得貸款時為每一筆貸款打分。

最重要的事情是堅持單一的角度。同時關注太多的事情很難做出效果。選擇一個有著足夠細節的角度也很重要。下面的角度就沒有太多細節:

  • 找出哪些銀行將貸款出售給房利美的多數被取消贖回權。
  • 計算貸款人的信用評分趨勢。
  • 找到哪些型別的家庭沒有償還貸款的能力。
  • 找到貸款金額和抵押品價格之間的關係。

上面的想法非常有趣,如果我們關注於講故事,那是一個不錯的角度,但是不是很適合一個操作性專案。

在房利美資料集中,我們將僅使用申請貸款時有的那些資訊來預測貸款是否將來會被取消贖回權。實際上, 我們將為每一筆貸款建立“分數”來告訴房利美買還是不買。這將給我們打下良好的基礎,並將組成這個漂亮的作品集的一部分。

理解資料

我們來簡單看一下原始資料檔案。下面是 2012 年 1 季度前幾行的貸款資料:

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 年 1 季度的前幾行執行資料:

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 年的。
  • 這些檔案是文字格式的,採用管道符號|進行分割。
  • 這些檔案是沒有表頭的,但我們有個檔案列明瞭各列的名稱。
  • 所有一起,檔案包含 2200 萬個貸款的資料。
  • 由於執行資料的檔案包含過去幾年獲得的貸款的資訊,在早些年獲得的貸款將有更多的執行資料(即在 2014 獲得的貸款沒有多少歷史執行資料)。

這些小小的資訊將會為我們節省很多時間,因為這樣我們就知道如何構造我們的專案和利用這些資料了。

構造專案

在我們開始下載和探索資料之前,先想一想將如何構造專案是很重要的。當建立端到端專案時,我們的主要目標是:

  • 建立一個可行解決方案
  • 有一個快速執行且佔用最小資源的解決方案
  • 容易可擴充套件
  • 寫容易理解的程式碼
  • 寫儘量少的程式碼

為了實現這些目標,需要對我們的專案進行良好的構造。一個結構良好的專案遵循幾個原則:

  • 分離資料檔案和程式碼檔案
  • 從原始資料中分離生成的資料。
  • 有一個 README.md 檔案幫助人們安裝和使用該專案。
  • 有一個 requirements.txt 檔案列明專案執行所需的所有包。
  • 有一個單獨的 settings.py 檔案列明其它檔案中使用的所有的設定
    • 例如,如果從多個 Python 指令碼讀取同一個檔案,讓它們全部 import 設定並從一個集中的地方獲得檔名是有用的。
  • 有一個 .gitignore 檔案,防止大的或密碼檔案被提交。
  • 分解任務中每一步可以單獨執行的步驟到單獨的檔案中。
    • 例如,我們將有一個檔案用於讀取資料,一個用於建立特徵,一個用於做出預測。
  • 儲存中間結果,例如,一個指令碼可以輸出下一個指令碼可讀取的檔案。
    • 這使我們無需重新計算就可以在資料處理流程中進行更改。

我們的檔案結構大體如下:

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

建立初始檔案

首先,我們需要建立一個 loan-prediction 資料夾,在此資料夾下面,再建立一個 data 資料夾和一個 processed 資料夾。data 資料夾存放原始資料,processed 資料夾存放所有的中間計算結果。

其次,建立 .gitignore 檔案,.gitignore 檔案將保證某些檔案被 git 忽略而不會被推送至 GitHub。關於這個檔案的一個好的例子是由 OSX 在每一個資料夾都會建立的 .DS_Store 檔案,.gitignore 檔案一個很好的範本在這裡。我們還想忽略資料檔案,因為它們實在是太大了,同時房利美的條文禁止我們重新分發該資料檔案,所以我們應該在我們的檔案後面新增以下 2 行:

data
processed

這裡是該專案的一個關於 .gitignore 檔案的例子。

再次,我們需要建立 README.md 檔案,它將幫助人們理解該專案。字尾 .md 表示這個檔案採用 markdown 格式。Markdown 使你能夠寫純文字檔案,同時還可以新增你想要的神奇的格式。這裡是關於 markdown 的導引。如果你上傳一個叫 README.md 的檔案至 Github,Github 會自動處理該 markdown,同時展示給瀏覽該專案的人。例子在這裡

至此,我們僅需在 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.txt 檔案的一個例子。

建立 requirements.txt 檔案之後,你應該安裝這些包了。我們將會使用 python3。如果你沒有安裝 python,你應該考慮使用 Anaconda,它是一個 python 安裝程式,同時安裝了上面列出的所有包。

最後,我們可以建立一個空白的 settings.py 檔案,因為我們的專案還沒有任何設定。

獲取資料

一旦我們有了專案的基本架構,我們就可以去獲得原始資料。

房利美對獲取資料有一些限制,所以你需要去註冊一個賬戶。在建立完賬戶之後,你可以找到在這裡的下載頁面,你可以按照你所需要的下載或多或少的貸款資料檔案。檔案格式是 zip,在解壓後當然是非常大的。

為了達到我們這個文章的目的,我們將要下載從 2012 年 1 季度到 2015 年 1 季度的所有資料。接著我們需要解壓所有的檔案。解壓過後,刪掉原來的 .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

在下載完資料後,你可以在 shell 命令列中使用 headtail 命令去檢視檔案中的行資料,你看到任何的不需要的資料列了嗎?在做這件事的同時查閱列名稱的 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,我們在處理資料檔案的目錄會獲得 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,目的是:
    • 獲取到所有 data 目錄中的檔名。
    • 遍歷每個檔案。
      • 如果檔案不是正確的格式 (不是以我們需要的格式作為開頭),我們將忽略它。
      • 通過使用 Pandas 的 read_csv 函式及正確的設定把檔案讀入一個 DataFrame
        • 設定分隔符為,以便所有的欄位能被正確讀出。
        • 資料沒有標題行,因此設定 headerNone 來進行標示。
        • HEADERS 字典中設定正確的標題名稱 – 這將會是我們的 DataFrame 中的資料列名稱。
        • 僅選擇我們加在 SELECT 中的 DataFrame 的列。
  • 把所有的 DataFrame 共同連線在一起。
  • 把已經連線好的 DataFrame 寫回一個檔案。
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)

我們可以通過呼叫上面的函式,通過傳遞的引數 AcquisitionPerformance 兩次以將所有的貸款和執行檔案連線在一起。下面的程式碼將:

  • 僅在命令列中執行 python assemble.py 時執行。
  • 將所有的資料連線在一起,並且產生 2 個檔案:
    • processed/Acquisition.txt
    • processed/Performance.txt
if __name__ == "__main__":
    concatenate("Acquisition")
    concatenate("Performance")

我們現在擁有了一個漂亮的,劃分過的 assemble.py 檔案,它很容易執行,也容易建立。通過像這樣把問題分解為一塊一塊的,我們構建工程就會變的容易許多。不用一個可以做所有工作的凌亂指令碼,我們定義的資料將會在多個指令碼間傳遞,同時使指令碼間完全的彼此隔離。當你正在一個大的專案中工作時,這樣做是一個好的想法,因為這樣可以更加容易修改其中的某一部分而不會引起其他專案中不關聯部分產生預料之外的結果。

一旦我們完成 assemble.py 指令碼檔案,我們可以執行 python assemble.py 命令。你可以在這裡檢視完整的 assemble.py 檔案。

這將會在 processed 目錄下產生 2 個檔案:

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 就可以了。如果這個引數的值是 None,那麼這些資產肯定沒有收回。為了避免我們的樣例中只有少量的執行資料,我們會為每個貸款計算出執行資料檔案中的行數。這樣我們就能夠從我們的訓練資料中篩選出貸款資料,排除了一些執行資料。

下面是我認為貸款資料和執行資料的關係:

在上面的表格中,貸款資料中的每一行資料都關聯到執行資料中的多行資料。在執行資料中,在取消贖回權的時候 foreclosure_date 就會出現在該季度,而之前它是空的。一些貸款還沒有被取消贖回權,所以與執行資料中的貸款資料有關的行在 foreclosure_date 列都是空格。

我們需要計算 foreclosure_status 的值,它的值是布林型別,可以表示一個特殊的貸款資料 id 是否被取消贖回權過,還有一個引數 performance_count ,它記錄了執行資料中每個貸款 id 出現的行數。 

計算這些行數有多種不同的方法:

  • 我們能夠讀取所有的執行資料,然後我們用 Pandas 的 groupby 方法在 DataFrame 中計算出與每個貸款 id 有關的行的行數,然後就可以檢視貸款 idforeclosure_date 值是否為 None
    • 這種方法的優點是從語法上來說容易執行。
    • 它的缺點需要讀取所有的 129236094 行資料,這樣就會佔用大量記憶體,並且執行起來極慢。
  • 我們可以讀取所有的執行資料,然後在貸款 DataFrame 上使用 apply 去計算每個貸款 id 出現的次數。
    • 這種方法的優點是容易理解。
    • 缺點是需要讀取所有的 129236094 行資料。這樣會佔用大量記憶體,並且執行起來極慢。
  • 我們可以在迭代訪問執行資料中的每一行資料,而且會建立一個單獨的計數字典。
    • 這種方法的優點是資料不需要被載入到記憶體中,所以執行起來會很快且不需要佔用記憶體。
    • 缺點是這樣的話理解和執行上可能有點耗費時間,我們需要對每一行資料進行語法分析。

載入所有的資料會非常耗費記憶體,所以我們採用第三種方法。我們要做的就是迭代執行資料中的每一行資料,然後為每一個貸款 id 在字典中保留一個計數。在這個字典中,我們會計算出貸款 id 在執行資料中出現的次數,而且看看 foreclosure_date 是否是 None 。我們可以檢視 foreclosure_statusperformance_count 的值 。

我們會新建一個 annotate.py 檔案,檔案中的程式碼可以計算這些值。我們會使用下面的程式碼:

  • 匯入需要的庫
  • 定義一個函式 count_performance_rows
    • 開啟 processed/Performance.txt 檔案。這不是在記憶體中讀取檔案而是開啟了一個檔案識別符號,這個識別符號可以用來以行為單位讀取檔案。 
    • 迭代檔案的每一行資料。
    • 使用分隔符|分開每行的不同資料。
    • 檢查 loan_id 是否在計數字典中。
      • 如果不存在,把它加進去。
    • loan_idperformance_count 引數自增 1 次,因為我們這次迭代也包含其中。
    • 如果 date 不是 None ,我們就會知道貸款被取消贖回權了,然後為foreclosure_status` 設定合適的值。
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

獲取值

只要我們建立了計數字典,我們就可以使用一個函式通過一個 loan_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 方法會返回一個預設值,所以在字典中不存在鍵的時候我們就可以得到一個可知的預設值。

轉換資料

我們已經在 annotate.py 中新增了一些功能,現在我們來看一看資料檔案。我們需要將貸款到的資料轉換到訓練資料集來進行機器學習演算法的訓練。這涉及到以下幾件事情:

  • 轉換所有列為數字。
  • 填充缺失值。
  • 為每一行分配 performance_countforeclosure_status
  • 移除出現執行資料很少的行(performance_count 計數低)。

我們有幾個列是文字型別的,看起來對於機器學習演算法來說並不是很有用。然而,它們實際上是分類變數,其中有很多不同的類別程式碼,例如 RS 等等. 我們可以把這些類別標籤轉換為數值:

通過這種方法轉換的列我們可以應用到機器學習演算法中。

還有一些包含日期的列 (first_payment_dateorigination_date)。我們可以將這些日期放到兩個列中:

在下面的程式碼中,我們將轉換貸款資料。我們將定義一個函式如下:

  • acquisition 中建立 foreclosure_status 列,並從 counts 字典中得到值。
  • acquisition 中建立 performance_count 列,並從 counts 字典中得到值。
  • 將下面的列從字串轉換為整數:
    • channel
    • seller
    • first_time_homebuyer
    • loan_purpose
    • property_type
    • occupancy_status
    • property_state
    • product_type
  • first_payment_dateorigination_date 分別轉換為兩列:
    • 通過斜槓分離列。
    • 將第一部分分離成 month 列。
    • 將第二部分分離成 year 列。
    • 刪除該列。
    • 最後,我們得到 first_payment_monthfirst_payment_yearrigination_monthorigination_year
  • 所有缺失值填充為 -1
def annotate(acquisition, counts):
    acquisition["foreclosure_status"] = acquisition["id"].apply(lambda x: get_performance_summary_value(x, "foreclosure_status", counts))
    acquisition["performance_count"] = acquisition["id"].apply(lambda x: get_performance_summary_value(x, "performance_count", counts))
    for column in [
        "channel",
        "seller",
        "first_time_homebuyer",
        "loan_purpose",
        "property_type",
        "occupancy_status",
        "property_state",
        "product_type"
    ]:
        acquisition[column] = acquisition[column].astype('category').cat.codes

    for start in ["first_payment", "origination"]:
        column = "{}_date".format(start)
        acquisition["{}_year".format(start)] = pd.to_numeric(acquisition[column].str.split('/').str.get(1))
        acquisition["{}_month".format(start)] = pd.to_numeric(acquisition[column].str.split('/').str.get(0))
        del acquisition[column]

    acquisition = acquisition.fillna(-1)
    acquisition = acquisition[acquisition["performance_count"] > settings.MINIMUM_TRACKING_QUARTERS]
    return acquisition

聚合到一起

我們差不多準備就緒了,我們只需要再在 annotate.py 新增一點點程式碼。在下面程式碼中,我們將:

  • 定義一個函式來讀取貸款的資料。
  • 定義一個函式來寫入處理過的資料到 processed/train.csv
  • 如果該檔案在命令列以 python annotate.py 的方式執行:
    • 讀取貸款資料。
    • 計算執行資料的計數,並將其賦予 counts
    • 轉換 acquisition DataFrame。
    • acquisition DataFrame 寫入到 train.csv
def read():
    acquisition = pd.read_csv(os.path.join(settings.PROCESSED_DIR, "Acquisition.txt"), sep="|")
    return acquisition

def write(acquisition):
    acquisition.to_csv(os.path.join(settings.PROCESSED_DIR, "train.csv"), index=False)

if __name__ == "__main__":
    acquisition = read()
    counts = count_performance_rows()
    acquisition = annotate(acquisition, counts)
    write(acquisition)

修改完成以後,確保執行 python annotate.py 來生成 train.csv 檔案。 你可以在這裡找到完整的 annotate.py 檔案。

現在資料夾看起來應該像這樣:

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

找到誤差標準

我們已經完成了訓練資料表的生成,現在我們需要最後一步,生成預測。我們需要找到誤差的標準,以及該如何評估我們的資料。在這種情況下,因為有很多的貸款沒有被取消贖回權,所以根本不可能做到精確的計算。

我們需要讀取訓練資料,並且計算 foreclosure_status 列的計數,我們將得到如下資訊:

import pandas as pd
import settings

train = pd.read_csv(os.path.join(settings.PROCESSED_DIR, "train.csv"))
train["foreclosure_status"].value_counts()
False    4635982
True        1585
Name: foreclosure_status, dtype: int64

因為只有很少的貸款被取消贖回權,只需要檢查正確預測的標籤的百分比就意味著我們可以建立一個機器學習模型,來為每一行預測 False,並能取得很高的精確度。相反,我們想要使用一個度量來考慮分類的不平衡,確保我們可以準確預測。我們要避免太多的誤報率(預測貸款被取消贖回權,但是實際沒有),也要避免太多的漏報率(預測貸款沒有別取消贖回權,但是實際被取消了)。對於這兩個來說,漏報率對於房利美來說成本更高,因為他們買的貸款可能是他們無法收回投資的貸款。

所以我們將定義一個漏報率,就是模型預測沒有取消贖回權但是實際上取消了,這個數除以總的取消贖回權數。這是“缺失的”實際取消贖回權百分比的模型。下面看這個圖表:

通過上面的圖表,有 1 個貸款預測不會取消贖回權,但是實際上取消了。如果我們將這個數除以實際取消贖回權的總數 2,我們將得到漏報率 50%。 我們將使用這個誤差標準,因此我們可以評估一下模型的行為。

設定機器學習分類器

我們使用交叉驗證預測。通過交叉驗證法,我們將資料分為3組。按照下面的方法來做:

  • 用組 1 和組 2 訓練模型,然後用該模型來預測組 3
  • 用組 1 和組 3 訓練模型,然後用該模型來預測組 2
  • 用組 2 和組 3 訓練模型,然後用該模型來預測組 1

將它們分割到不同的組,這意味著我們永遠不會用相同的資料來為其預測訓練模型。這樣就避免了過擬合。如果過擬合,我們將錯誤地拉低了漏報率,這使得我們難以改進演算法或者應用到現實生活中。

Scikit-learn 有一個叫做 crossvalpredict ,它可以幫助我們理解交叉演算法.

我們還需要一種演算法來幫我們預測。我們還需要一個分類器來做二元分類。目標變數 foreclosure_status 只有兩個值, TrueFalse

這裡我們用 邏輯迴歸演算法,因為它能很好的進行二元分類,並且執行很快,佔用記憶體很小。我們來說一下它是如何工作的:不使用像隨機森林一樣多樹結構,也不像支援向量機一樣做複雜的轉換,邏輯迴歸演算法涉及更少的步驟和更少的矩陣。

我們可以使用 scikit-learn 實現的邏輯迴歸分類器演算法。我們唯一需要注意的是每個類的權重。如果我們使用等權重的類,演算法將會預測每行都為 false,因為它總是試圖最小化誤差。不管怎樣,我們重視有多少貸款要被取消贖回權而不是有多少不能被取消。因此,我們給邏輯迴歸類的 class_weight 關鍵字傳遞 balanced 引數,讓演算法可以為不同 counts 的每個類考慮不同的取消贖回權的權重。這將使我們的演算法不會為每一行都預測 false,而是兩個類的誤差水平一致。

做出預測

既然完成了前期準備,我們可以開始準備做出預測了。我將建立一個名為 predict.py 的新檔案,它會使用我們在最後一步建立的 train.csv 檔案。下面的程式碼:

  • 匯入所需的庫
  • 建立一個名為 cross_validate 的函式:
    • 使用正確的關鍵詞引數建立邏輯迴歸分類器
    • 建立用於訓練模型的資料列的列表,移除 idforeclosure_status
    • 交叉驗證 train DataFrame
    • 返回預測結果
import os
import settings
import pandas as pd
from sklearn import cross_validation
from sklearn.linear_model import LogisticRegression
from sklearn import metrics

def cross_validate(train):
    clf = LogisticRegression(random_state=1, class_weight="balanced")

    predictors = train.columns.tolist()
    predictors = [p for p in predictors if p not in settings.NON_PREDICTORS]

    predictions = cross_validation.cross_val_predict(clf, train[predictors], train[settings.TARGET], cv=settings.CV_FOLDS)
    return predictions

預測誤差

現在,我們僅僅需要寫一些函式來計算誤差。下面的程式碼:

  • 建立函式 compute_error
    • 使用 scikit-learn 計算一個簡單的精確分數(與實際 foreclosure_status 值匹配的預測百分比)
  • 建立函式 compute_false_negatives
    • 為了方便,將目標和預測結果合併到一個 DataFrame
    • 查詢漏報率
      • 找到原本應被預測模型取消贖回權,但實際沒有取消的貸款數目
      • 除以沒被取消贖回權的貸款總數目
def compute_error(target, predictions):
    return metrics.accuracy_score(target, predictions)

def compute_false_negatives(target, predictions):
    df = pd.DataFrame({"target": target, "predictions": predictions})
    return df[(df["target"] == 1) & (df["predictions"] == 0)].shape[0] / (df[(df["target"] == 1)].shape[0] + 1)

def compute_false_positives(target, predictions):
    df = pd.DataFrame({"target": target, "predictions": predictions})
    return df[(df["target"] == 0) & (df["predictions"] == 1)].shape[0] / (df[(df["target"] == 0)].shape[0] + 1)

聚合到一起

現在,我們可以把函式都放在 predict.py。下面的程式碼:

  • 讀取資料集
  • 計算交叉驗證預測
  • 計算上面的 3 個誤差
  • 列印誤差
def read():
    train = pd.read_csv(os.path.join(settings.PROCESSED_DIR, "train.csv"))
    return train

if __name__ == "__main__":
    train = read()
    predictions = cross_validate(train)
    error = compute_error(train[settings.TARGET], predictions)
    fn = compute_false_negatives(train[settings.TARGET], predictions)
    fp = compute_false_positives(train[settings.TARGET], predictions)
    print("Accuracy Score: {}".format(error))
    print("False Negatives: {}".format(fn))
    print("False Positives: {}".format(fp))

一旦你新增完程式碼,你可以執行 python predict.py 來產生預測結果。執行結果向我們展示漏報率為 .26,這意味著我們沒能預測 26% 的取消貸款。這是一個好的開始,但仍有很多改善的地方!

你可以在這裡找到完整的 predict.py 檔案。

你的檔案樹現在看起來像下面這樣:

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

撰寫 README

既然我們完成了端到端的專案,那麼我們可以撰寫 README.md 檔案了,這樣其他人便可以知道我們做的事,以及如何複製它。一個專案典型的 README.md 應該包括這些部分:

  • 一個高水準的專案概覽,並介紹專案目的
  • 任何必需的資料和材料的下載地址
  • 安裝命令
    • 如何安裝要求依賴
  • 使用命令
    • 如何執行專案
    • 每一步之後會看到的結果
  • 如何為這個專案作貢獻
    • 擴充套件專案的下一步計劃

這裡 是這個專案的一個 README.md 樣例。

下一步

恭喜你完成了端到端的機器學習專案!你可以在這裡找到一個完整的示例專案。一旦你完成了專案,把它上傳到 Github 是一個不錯的主意,這樣其他人也可以看到你的資料夾的部分專案。

這裡仍有一些留待探索資料的角度。總的來說,我們可以把它們分割為 3 類: 擴充套件這個專案並使它更加精確,發現其他可以預測的列,並探索資料。這是其中一些想法:

  • annotate.py 中生成更多的特性
  • 切換 predict.py 中的演算法
  • 嘗試使用比我們發表在這裡的更多的房利美資料
  • 新增對未來資料進行預測的方法。如果我們新增更多資料,我們所寫的程式碼仍然可以起作用,這樣我們可以新增更多過去和未來的資料。
  • 嘗試看看是否你能預測一個銀行原本是否應該發放貸款(相對地,房利美是否應該獲得貸款)
    • 移除 train 中銀行在發放貸款時間的不知道的任何列
      • 當房利美購買貸款時,一些列是已知的,但之前是不知道的
    • 做出預測
  • 探索是否你可以預測除了 foreclosure_status 的其他列
    • 你可以預測在銷售時資產值是多少?
  • 探索探索執行資料更新之間的細微差別
    • 你能否預測借款人會逾期還款多少次?
    • 你能否標出的典型貸款週期?
  • 將資料按州或郵政編碼標出
    • 你看到一些有趣的模式了嗎?

如果你建立了任何有趣的東西,請在評論中讓我們知道!

如果你喜歡這篇文章,或許你也會喜歡閱讀“構建你的資料科學作品集”系列的其他文章:


via: https://www.dataquest.io/blog/data-science-portfolio-machine-learning/

作者:Vik Paruchuri 譯者:kokialoves, zky001, vim-kakali, cposture, ideas4u 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章