一、資料科學的生命周期
原文: 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 年之間的資料,将其納入我們的分析中。我們将很快讨論資料操作,并且你可能會重新分析這個示例,來确定納入這一先驗是否會提供更明智的結果。