天天看點

資料科學的原理與技巧 一、資料科學的生命周期一、資料科學的生命周期

一、資料科學的生命周期

原文: DS-100/textbook/notebooks/ch01 譯者: 飛龍 協定: CC BY-NC-SA 4.0 自豪地采用 谷歌翻譯

在資料科學中,我們使用大量不同的資料集來對世界做出結論。在這個課程中,我們将通過計算和推理思維的雙重視角,來讨論資料科學的關鍵原理和技術。實際上,這涉及以下過程:

  • 提出一個問題
  • 擷取和清理資料
  • 進行探索性資料分析
  • 用預測和推理得出結論

在這個過程的最後一步之後,通常出現更多的問題,是以我們可以反複地執行這個過程,來發現我們的世界的新特征。這個正回報的循環對我們的工作至關重要,我們稱之為資料科學生命周期。

如果資料科學的生命周期與它說的一樣容易進行,那麼就不需要該主題的教科書了。幸運的是,生命周期中的每個步驟都包含衆多挑戰,這些挑戰揭示了強大和通常令人驚訝的見解,它們構成了使用資料在思考後進行決策的基礎。

和 Data8 一樣,我們将以一個例子開始。

譯者注:Data8 是 DS100 是先修課。我之前翻譯了它的課本, 《計算與推斷思維 中文版》

關于本書

在我們繼續之前,重要的是說出我們對讀者的假設。

在本書中,我們将當作你已經上完了 Data8 或者其他一些類似的東西。 特别是,我們假定你對以下主題有一定了解(同時給出 Data8 課本的頁面連結)。

另外,我們假設你已經上完了 CS61A 或者其他類似的東西,是以除了特殊情況外,不會解釋 Python 的文法。

譯者注:CS61A(SICP Python)是計算機科學的第一門課,中文版講義請見 《SICP Python 中文版》

DS100 的學生

回想一下,資料科學生命周期涉及以下大緻的步驟:

  • 問題表述:
    • 我們想知道什麼,或者我們想要解決什麼問題?
    • 我們的假設是什麼?
    • 我們的成功名額是什麼?
  • 資料采集和清洗:
    • 我們有什麼資料以及需要哪些資料?
    • 我們将如何收集更多資料?
    • 我們如何組織資料來分析?
  • 探索性資料分析:
    • 我們是否有了相關資料?
    • 資料有哪些偏差,異常或其他問題?
    • 我們如何轉換資料來實作有效的分析?
  • 預測和推斷:
    • 這些資料說了世界的什麼事情?
    • 它回答我們的問題,還是準确地解決問題?
    • 我們的結論有多健壯?

問題表述

我們想知道 DS100 中的學生姓名的資料,是否向我們提供了學生本身的其他資訊。 雖然這是一個模糊的問題,但這足以讓我們處理我們的資料,我們當然可以在問題變得更加精确的時候提出問題。

資料采集和清洗

在 DS100 中,我們将研究收集資料的各種方法。

我們首先看看我們的資料,這是我們從以前的 DS100 課程中下載下傳的學生姓名的名單。

如果你現在不了解代碼,請不要擔心;我們稍後會更深入地介紹這些庫。 相反,請關注我們展示的流程和圖表。

import pandas as pd

students = pd.read_csv('roster.csv')
students           
Name Role
Keeley
1 John
2 BRYAN
276 Ernesto
277 Athan
278 Michael

279 行 × 2 列

我們很快可以看到,資料中有一些奇怪的東西。 例如,其中一個學生的姓名全部是大寫字母。 另外,

Role

列的作用并不明顯。

在 DS100 中,我們将研究如何識别資料中的異常并執行修正。 大寫字母的差異将導緻我們的程式認為

'BRYAN'

'Bryan'

是不同的名稱,但他們對于我們的目标是相同的。 我們将所有名稱轉換為小寫來避免這種情況。

students['Name'] = students['Name'].str.lower()
students           
keeley
john
bryan
ernesto
athan
michael

現在我們的資料有了更容易處理的格式,我們繼續進行探索性資料分析。

探索性資料分析(EDA)

術語探索性資料分析(簡稱 EDA)是指發現我們的資料特征的過程,這些特征為未來的分析提供資訊。

這是上一頁的

students

表:

students           

我們留下了許多問題。 這個名單中有多少名學生?

Role

列是什麼意思? 我們進行 EDA 來更全面地了解我們的資料。

在 DS100 中,我們将研究探索性資料分析和實踐,來分析新資料集。

通常,我們通過重複提出簡單問題,他們有關我們想知道的資料,來探索資料。 我們将以這種方式建構我們的分析。

我們的資料集中有多少學生?

print("There are", len(students), "students on the roster.")
# There are 279 students on the roster.           

一個自然的後續問題是,這是否是完整的學生名單。 在這種情況下,我們碰巧知道這個清單包含班級中的所有學生。

Role

字段的含義是什麼?

了解字段的含義,通常可以通過檢視字段資料的唯一值來實作:

students['Role'].value_counts().to_frame()           
Student
Waitlist Student

我們可以在這裡看到,我們的資料不僅包含當時注冊了課程的學生,還包含等候名單上的學生。

Role

列告訴我們每個學生是否注冊。

那名稱呢? 我們如何總結這個字段?

在 DS100 中,我們将處理許多不同類型的資料(不僅僅是數字),而且我們将研究面向不同類型的資料的技術。

好的起點可能是檢查字元串的長度。

sns.distplot(students['Name'].str.len(), rug=True, axlabel="Number of Characters")
# <matplotlib.axes._subplots.AxesSubplot at 0x10e6fd0b8>           

這種可視化向我們展示了,大多數名稱的長度在 3 到 9 個字元之間。 這給了我們一個機會,來檢查我們的資料是否合理 - 如果有很多名稱長度為 1 個字元,我們就有充分的理由重新檢查我們的資料。

名稱裡面有什麼?

雖然這個資料集非常簡單,但我們很快就會看到,僅僅是名稱就可以揭示我們班級的相當多的資訊。

名稱裡面有什麼

到目前為止,我們已經對我們的資料提出了一個大緻的問題:“DS100 中的學生名稱是否告訴我們該課程的任何資訊?”

通過将所有名稱轉換為小寫字母,我們完成一些資料清理工作。 在我們的探索性資料分析過程中,我們發現,我們的名單包含班級和候補名單中的大約 270 個學生姓名,而大部分名稱長度在 4 到 8 個字元之間。

根據名稱,我們還能發現班級的什麼其他資訊? 我們可能會考慮資料集中的單個名稱:

students['Name'][5]
# 'jerry'           

從這個名稱中我們可以推斷出,這個學生可能是一個男生。我們也可以猜測學生的年齡。例如,如果我們知道,傑裡在 1998 年是一個非常受歡迎的嬰兒名稱,那麼我們可能會猜測這個學生大約二十歲。

這個想法給了我們兩個需要調查的新問題:

  • “DS100 中的學生名稱,是否告訴了我們課堂上的性别分布?”
  • “DS100 中的第一批學生,是否告訴了我們課堂上的年齡分布?”

為了調查這些問題,我們需要一個資料集,它将姓名與性别和年份相關聯。友善的是,美國社會保障部門線上提供這樣一個資料集:

https://www.ssa.gov/oact/babynames/index.html

。他們的資料集記錄了嬰兒出生時的名稱,是以通常稱為嬰兒名稱資料集。

我們将從下載下傳開始,然後将資料集加載到 Python 中。再次,不要擔心了解第一章中的代碼。了解整個過程更重要。

import urllib.request
import os.path

data_url = "https://www.ssa.gov/oact/babynames/names.zip"
local_filename = "babynames.zip"
if not os.path.exists(local_filename): # if the data exists don't download again
    with urllib.request.urlopen(data_url) as resp, open(local_filename, 'wb') as f:
        f.write(resp.read())

import zipfile
babynames = [] 
with zipfile.ZipFile(local_filename, "r") as zf:
    data_files = [f for f in zf.filelist if f.filename[-3:] == "txt"]
    def extract_year_from_filename(fn):
        return int(fn[3:7])
    for f in data_files:
        year = extract_year_from_filename(f.filename)
        with zf.open(f) as fp:
            df = pd.read_csv(fp, names=["Name", "Sex", "Count"])
            df["Year"] = year
            babynames.append(df)
babynames = pd.concat(babynames)
babynames           
Sex Count Year
Mary F 9217
Anna 3860
Emma 2587
2081 Verna M 5
2082 Winnie
2083 Winthrop

1891894 行 × 4 列

ls -alh babynames.csv
# -rw-r--r--  1 sam  staff    30M Jan 22 15:31 babynames.csv           

看起來,資料集包含名稱,嬰兒性别,具有該名稱的嬰兒數量以及這些嬰兒的出生年份。 為了确認,我們從檢查來自 SSN 的資料集描述:

https://www.ssa.gov/oact/babynames/background.html

所有名稱均來自 1879 年後美國出生人口的社保卡申請。請注意,很多 1937 年以前出生的人從未申請過社保卡,是以他們的名字不包含在我們的資料中。 對于其他申請人,我們的記錄可能不會顯示出生地點,并且他們的姓名也不會包含在我們的資料中。

所有資料均來自截至我們的 2017 年 3 月社保卡申請記錄的 100% 樣本。

這個資料的一個有用的可視化,是繪制每年出生的男性和女性嬰兒的數量:

pivot_year_name_count = pd.pivot_table(
    babynames, index='Year', columns='Sex',
    values='Count', aggfunc=np.sum)

pink_blue = ["#E188DB", "#334FFF"]
with sns.color_palette(sns.color_palette(pink_blue)):
    pivot_year_name_count.plot(marker=".")
    plt.title("Registered Names vs Year Stratified by Sex")
    plt.ylabel('Names Registered that Year')           

這個繪圖讓我們質疑,1880 年的美國是否有嬰兒。上面引用的一句話有助于解釋:

請注意,很多 1937 年以前出生的人從未申請過社保卡,是以他們的名字不包含在我們的資料中。 對于其他申請人,我們的記錄可能不會顯示出生地點,并且他們的姓名也不會包含在我們的資料中。

我們還可以在上圖中清楚地看到嬰兒潮的時期。

從名字推斷性别

我們使用這個資料集來估計我們班的男女生人數。 與我們班的名單一樣,我們先将名稱小寫:

babynames['Name'] = babynames['Name'].str.lower()
babynames           
mary
anna
emma
verna
winnie
winthrop

然後,我們計算對于每個名字,共有多少個男嬰和女嬰出生:

sex_counts = pd.pivot_table(babynames, index='Name', columns='Sex', values='Count',
                            aggfunc='sum', fill_value=0., margins=True)
sex_counts           
All
aaban 96
aabha 35
aabid 10
zyyon 6
zzyzx
170639571 173894326 344533897

96175 行 × 3 列

為了決定一個名字是男性還是女性,我們可以計算出這個名字給女性嬰兒的次數比例。

prop_female = sex_counts['F'] / sex_counts['All']
sex_counts['prop_female'] = prop_female
sex_counts           
prop_female
0.000000
0.495277

96175 行 × 4 列

然後,我們可以定義一個函數,查找給定名稱的女性比例。

def sex_from_name(name):
    if name in sex_counts.index:
        prop = sex_counts.loc[name, 'prop_female']
        return 'F' if prop > 0.5 else 'M'
    else:
        return None
sex_from_name('sam')
# 'M'           

嘗試在這個框中輸入一些名稱,來檢視這個函數是否輸出你期望的内容:

interact(sex_from_name, name='sam');           

我們在班級名單中,使用最可能的性别标記每個名稱。

students['sex'] = students['Name'].apply(sex_from_name)
students           
sex

279 行 × 3 列

現在,估計我們有多少男女學生就很容易了:

students['sex'].value_counts()
'''
M    144
F     92
Name: sex, dtype: int64
'''           

從名稱推斷年齡

我們可以采用類似的方法來估計班級的年齡分布,将每個姓名映射到資料集中的平均年齡。

def avg_year(group):
    return np.average(group['Year'], weights=group['Count'])

avg_years = (
    babynames
    .groupby('Name')
    .apply(avg_year)
    .rename('avg_year')
    .to_frame()
)
avg_years           
avg_year
zyyanna

96174 行 × 1 列

def year_from_name(name):
    return (avg_years.loc[name, 'avg_year']
            if name in avg_years.index
            else None)

# Generate input box for you to try some names out:
interact(year_from_name, name='fernando');

students['year'] = students['Name'].apply(year_from_name)
students           
year

279 行 × 4 列

之後,繪制年份的分布情況很容易:

sns.distplot(students['year'].dropna());           

為了計算平均年份:

students['year'].mean()
# 1983.846741800525           

這使得它看起來像是,學生平均是 35 歲。 這是一個大學大學課程,是以我們預計平均年齡在 20 歲左右。為什麼我們的估計會如此之遠?

作為資料科學家,我們經常遇到不符合我們預期的結果,并且必須做出判斷,我們的結果是由我們的資料,我們的流程還是不正确的假設造成的。 不可能定義适用于所有情況的規則。 相反,我們将為你提供工具來重新檢查資料分析的每一步,并告訴你如何使用它們。

在這種情況下,我們意想不到的結果,最可能是因為大多數名字都是舊的。 例如,在我們的資料記錄中,約翰這個名字在整個曆史中都相當流行,這意味着我們可能會猜測約翰出生于 1950 年左右。我們可以通過檢視資料來确認:

names = babynames.set_index('Name').sort_values('Year')
john = names.loc['john']
john[john['Sex'] == 'M'].plot('Year', 'Count');           

如果我們相信,我們班沒有人超過 40 歲或低于 10 歲(我們可以通過在課上觀察我們的教室發現),我們可以通過僅檢查 1978 年之間的資料,将其納入我們的分析中。我們将很快讨論資料操作,并且你可能會重新分析這個示例,來确定納入這一先驗是否會提供更明智的結果。