數據科學導論:數據收集與整理

本筆記是數據科學導論系列的第一篇,另有閱讀體驗更佳的 Jupyter Notebook 版本,Jupyter Notebook 和 Markdown 文件開源於 GitHub。

數據收集

儘管互聯網上已經有很多數據集,但有時候我們需要的數據不是現成的,需要收集數據。收集這些數據通常有兩種方法:爬蟲和 API。

爬蟲就是寫程序把網頁上的內容抓取下來,理論上,任何你能在網上看到的數據都是可以用爬蟲抓取的,但要遵守法律、網站條款和隱私權,控制爬蟲的抓取速度,不要把別人伺服器搞垮了。

API 可以理解為網站給程序用的介面,API 給出的數據更友好,但每個網站的 API 格式都不同,需要查閱文檔。有些網站不提供 API 介面,不妨去 GitHub 搜一搜,通常能找到開源的非官方 API,這些 API 其實就是打包好的爬蟲,你只要調用命令就能獲得數據了。

爬蟲

爬蟲首先需要發送請求給伺服器,然後伺服器會發回網頁內容。這個過程有多個庫可以使用,例如 requests。

import requestsnr = requests.get(http://httpbin.org))ncontent = r.textn

發回網頁內容後,你就能得到 HTML 代碼,HTML 代碼構成的就是網頁的內容,它們通常長這樣:

<!DOCTYPE html>n<html>n <head>n <title>This is a title</title>n </head>n <body>n <h2> Test </h2>n <p>Hello world!</p>n </body>n</html>n

HTML 代碼的特點有:

  1. 標籤通常成對出現
  2. 標題 <h1></h1> ... <h6></h6>
  3. 段落 <p></p>
  4. 換行 <br>
  5. href 內容是鏈接 <a href="http://www.example.com/">An example link</a>

在 Chrome 或者 Safari 瀏覽器里,你只要右鍵網頁-檢查就能找到你需要的數據對應的 HTML 代碼。

你可以硬著頭皮用正則表達式篩選出你要的數據,更好的方法是用現成的分析 HTML 的工具,例如 BeautifulSoup、Selenium。

from bs4 import BeautifulSoupnn# 把 Requsests 得到的內容傳給 BeautifulSoup,得到 bs4 對象nsoup = BeautifulSoup(source)nn# 查找所有的 <a>...</a> 標籤naTag = soup.findAll(a)nn# 得到鏈接natag.get(href)nn# 得到鏈接並生成列表nlink_list = [l.get(href) for l in aTag]nfor l in link_list:n if l is not None and l[:4] == http:n external_links.append(l)n

爬取的數據可以用詞典保存,Python 還有個很重要的模塊叫 collections,為數據科學家提供了很多工具,例如加強型詞典 defaultdict 和頻率計算 Counter。

限於篇幅,爬虫部分就在這裡結束。如果你想深入了解,這裡有一些爬蟲實例:

使用 urllib2 和 BeautifulSoup 爬取數據科學家所需技能

使用 LXML 和 Selenium 爬取洛杉磯 Happy Hour (PyCon 2014 Tutorial)

API

網站為了防止 API 被濫用,通常會要求你註冊賬號,訪問 API 的時候要加上你的賬號密鑰。有些 API 能控制你的賬戶行為,例如用 Twitter API 可以發推,所以不要讓你的密鑰出現在你的代碼里,而是讓代碼訪問密鑰文件得到密鑰。

這裡有一些有意思的 API:

Twitter

爛番茄電影評分

JSON

有時候 API 發回的是 JSON 格式的數據,JSON 的全稱是 JavaScript Object Notation,格式和 Python 中的詞典很像,但不好直接處理,需要轉換成詞典。

import jsonndataDict = json.loads(data)n

第三方庫

有些 API 非常複雜,例如 Twitter,用第三方庫會省力很多,例如 tweepy。

數據整理

收集數據後,我們先要探索數據 (data discovery, data unboxing),以對數據有基本的認識。數據可能是「臟」的,或者對我們的工作是無用的,所以還需要整理數據 (data wrangling, data prep, data munging, data transformation),讓數據更好地為分析師服務。

Kandel et al. (2012) 採訪了35位分析師後發現,許多分析師都把大部分時間花在整理數據上,而整理數據的過程,讓分析師更了解數據並能提出好的猜想。

I spend more than half of my time integrating, cleansing and transforming data without doing any actual analysis. Most of time I』m lucky if I get to do any 『analysis』 at all…

… Most of the time once you transform the data ... the insights can be scarily obvious.

貝爾實驗室數學家、R 語言之父 John Tukey 在 1965 年就提出了類似的見解。Tukey 指出,統計學家要想靈活地分析數據,就必須讓數據對使用者更友好,這個過程如此重要,以致於是數學、統計模型、計算機不能比擬的。

at all stages of data analysis, the nature and detail of output, both actual and potential, need to be matched to the capabilities of the people who use it and want it … Nothing - not the careful logic of mathematics, not statistical models and theories, not the awesome arithmetic power of modern computers - nothing can substitute here for the flexibility of the informed human mind.

Hoaglin(2003) 有一篇論文討論了 John Tukey 的事迹和他對統計學的貢獻。

整理要點

這裡提供一個通用的整理要點,但是整理數據是個主觀過程。沒有一成不變的規則

結構 (Structure):數據的形狀

  • 數據是矩形結構(Rectangular Data)嗎? 矩形結構包含表格(用關係代數處理)和矩陣(用線性代數處理)。如果不是這兩者,例如 JSON、XML,需要轉換成矩形結構(Rectangular Data)。
  • 有沒有超出定義的數據?例如在日期里出現了 ¥120。
  • 數據內有嵌套嗎?例如在支出里出現了¥180(住宿)。
  • 數據是什麼類型?定類數據(nominal)、定序數據(ordinal)還是定量數據(quantitative)?
  • 相同類型的數據格式一樣嗎?例如日期里出現了 4th May 和 04-05-2017

粒度 (Granularity): 主鍵的精細程度

主鍵(primary key)指賦予每條數據獨特性的指標,例如 user_id、transaction_id、(City, State)。主鍵的值最多出現一次,主鍵決定了數據的粒度。根據主鍵,可以把不同的數據拼合起來。

可信度 (Faithfulness): 數據的真實程度

可信度只能在上下文(context)中檢驗,如果出現了偏離數據分布太多的異常值(outlier),有三者方法處理:

  1. 刪掉
  2. 改為最接近的非異常值(non-outlier)
  3. 保持數據原樣,並添加一欄註明是否為異常值,添加一欄註明修改後的結果。

完整性 (Scope): 數據的完整程度

是否有缺失的數據或者條目?可以利用數字排序推測,比如數據中房間號有101、103、104,那麼我們可以認為 102 缺失。

時間契合度 (Temporality): 數據記錄的時間的有效程度

對於待解決的問題,數據對應的時間有效嗎?

限於篇幅,數據整理要點就說到這,如果你想更深入的閱讀,可以試試以下鏈接:

The Quartz guide to bad data

中文翻譯:The Quartz 壞數據手冊

Research Directions in Data Wrangling, Heer et al. 2011

工具

數據科學家使用大量的工具來提高整理和分析數據的效率。

文本編輯器,例如 Atom、Sublime Text,文本編輯器輕量、小地圖(mini-map)便於定位、豐富的快捷鍵,可以方便地對數據進行簡單修改、查找替換。

Trifacta 是免費的可視化數據整理工具,誕生於斯坦福和伯克利,支持編程操作和智能預測。

UNIX 命令行,可以向操作系統內核直接發送操作指令,省去進入編程環境的步驟,內核恐慌第28期回顧了 command line/shell 的歷史,有興趣的可以聽一聽。 macOS 上可以安裝 iTerm2 和 zsh 進行命令行操作。命令行操作也可以在 Jupyter Notebook 中完成,在命令前加上 ! 即可。

Pandas 是 Wes McKinney 開發的專門用於數據操作的 Python 第三方庫,設計參考了 R 語言。

下面我們使用 movielens 的數據,演示 UNIX 命令行和 Pandas 的使用。寫代碼之前,有幾個建議:

  1. 有問題就 Google。
  2. 某種程度上,程序員就是複製粘貼 Stack Overflow 上的代碼然後跑通的人。
  3. 取有意義的變數名。
  4. 使用快捷鍵。Jupyter Notebook 中,按 Tab 自動補全命令;在命令前後加上 ? 可以彈出手冊界面,esc 退出;寫代碼時按 Shift + Tab 可以更快地顯示手冊,如下圖所示。

UNIX 命令行

man something 手冊(manual)的縮寫,可以查看任何 UNIX 命令的指引。

ls -lh movieLens/ # 查看 movieLens/ 文件夾下的文件目錄。nn# 輸出ntotal 6136n-rw-r--r--@ 1 Jiawei staff 8.2K Oct 17 2016 README.txtn-rw-r--r--@ 1 Jiawei staff 179K Oct 17 2016 links.csvn-rw-r--r--@ 1 Jiawei staff 448K Oct 17 2016 movies.csvn-rw-r--r--@ 1 Jiawei staff 2.3M Oct 17 2016 ratings.csvn-rw-r--r--@ 1 Jiawei staff 41K Oct 17 2016 tags.csvnncat movieLens/README.txt # 查看 README.txt 文件全文。nnwc movieLens/movies.csv movieLens/ratings.csv movieLens/tags.csv # wc 是 word count 的縮寫,查看 movieLens/movies.csv、 movieLens/ratings.csv 、movieLens/tags.csv 文件的行數、詞數、位元組數。nn# 輸出n9126 39127 458390 movieLens/movies.csvn100005 100005 2438266 movieLens/ratings.csvn1297 1887 41902 movieLens/tags.csvn110428 141019 2938558 totalnnhead movieLens/movies.csv movieLens/ratings.csv movieLens/tags.csv n# 查看 movieLens/movies.csv、 movieLens/ratings.csv 、movieLens/tags.csv 文件的前10行。nn# 輸出n==> movieLens/movies.csv <==nmovieId,title,genresn1,Toy Story (1995),Adventure|Animation|Children|Comedy|Fantasyn2,Jumanji (1995),Adventure|Children|Fantasyn3,Grumpier Old Men (1995),Comedy|Romancen4,Waiting to Exhale (1995),Comedy|Drama|Romancen5,Father of the Bride Part II (1995),Comedyn6,Heat (1995),Action|Crime|Thrillern7,Sabrina (1995),Comedy|Romancen8,Tom and Huck (1995),Adventure|Childrenn9,Sudden Death (1995),Actionnn==> movieLens/ratings.csv <==nuserId,movieId,rating,timestampn1,31,2.5,1260759144n1,1029,3.0,1260759179n1,1061,3.0,1260759182n1,1129,2.0,1260759185n1,1172,4.0,1260759205n1,1263,2.0,1260759151n1,1287,2.0,1260759187n1,1293,2.0,1260759148n1,1339,3.5,1260759125nn==> movieLens/tags.csv <==nuserId,movieId,tag,timestampn15,339,sandra boring bullock,1138537770n15,1955,dentist,1193435061n15,7478,Cambodia,1170560997n15,32892,Russian,1170626366n15,34162,forgettable,1141391765n15,35957,short,1141391873n15,37729,dull story,1141391806n15,45950,powerpoint,1169616291n15,100365,activist,1425876220n

Pandas

我們可以用 Pandas 做一些簡單的數據操作來整理數據,在後續的章節里我們會更深入地了解 Pandas。

import pandas as pd # 導入 pandas 模塊,縮寫為 pdnpd.set_option(display.max_rows, 8) # 設置最多顯示 8 行表格n

導入操作

這裡有必要介紹一下索引 (Index) 的概念,索引就像是門牌號,通過索引可以找到索引對應的數據。例如 Python 的列表,可以用 aList[n] 來表示 aList 位置 n 上的數據,這個 [n] 就是列表的索引。Pandas 有兩種數據結構,一維的 Series 和二維的 DataFrame,這兩個數據結構都有 Index 對象作為索引,用 Numpy 保存索引外的數據,Series 有 index,DataFrame 有兩個,分別是 index 和 columns,index 是縱向的,columns 是橫向的。

# 導入 csv 文件,並把第 0 列設為 indexnmovies = pd.read_csv(movieLens/movies.csv, index_col=0)n# 導入 csv 文件,不設置 indexnratings = pd.read_csv(movieLens/ratings.csv)ntags = pd.read_csv(movieLens/tags.csv)n

數據屬性

# 使用 describe() 描述數據,輸出結果視情況而定nmovies.describe()nmovies.dtypes # 查看數據類型nmovies.shape # 查看數據形狀nmovies.index # 查看 indexnmovies.columns # 查看 columnsn

選擇條目

標籤 (label) 就是 index 和 column 上不同索引的名字,這些索引可以用整數表示,也可以用 label 表示。

# 選擇前 5 行,兩者等價nmovies[:5] nmovies.iloc[:5, :]n# 依據 label 選擇對應條目,兩者等價nmovies.titlenmovies[title]n# 選擇前五行的 genres 條目,loc 接受 label,iloc 接受整數nmovies.loc[:5, genres]nmovies.iloc[:5, 1]n# 傳入多個列表選擇數據,注意傳入嵌套列表的列表nmovies[[title, genres]]n# 與上一條命令等價nshowList = [title, genres]nmovies[showList]n# 輸出一系列布爾值列表nmovies[genres] == Comedy n# 依據布爾值選擇 genres 為 Comedy 的條目nmovies[movies[genres] == Comedy] n# 選擇多個布爾值條件(類型是 Comedy 且標題中含 Gay 的電影)nmovies[(movies[genres] == Comedy) n & (movies[title].str.contains(Gay))]n

Split-apply-combine

Split-apply-combine 過程會頻繁出現在你的數據科學項目里,簡而言之,該過程包括3個步驟:

  1. 把數據分組
  2. 對每個組應用函數
  3. 整合結果

如下圖所示,我們把數據分成3組,然後對每個組求均值,最後整合在一塊就是一次 split-apply-combine。

我現在想知道:

  • 每部電影的平均得分是多少?
  • 平均得分最高多少?哪幾部電影得分最高?
  • 每位用戶的平均打分是多少?

這就需要我把所有評分以 movieId 區分,相同 movieId 的取平均值後,得到新的表格。用戶的平均打分的過程亦是如此,讀者可自己嘗試。

groupedRatingPerMovie = ratings[rating].groupby(ratings[movieId])n# 注意不是 ratings.groupby(movieId)ngroupedRatingPerMovie.describe()nratingPerMovie = groupedRatingPerMovie.mean() # 計算平均值nmaxRating = ratingPerMovie.max() # 得到電影平均分的最高值nmaxRating nn# 輸出n5.0n# 用布爾值選擇數據nmaxRatedMovieId = ratingPerMovie[ratingPerMovie == maxRating] nmaxRatedMovieTitle = movies[movies.index.isin(maxRatedMovieId.index)] n# 得到最高分的電影名稱nmaxRatedMovieTitle[title]nn# 輸出nmovieIdn53 Lamerica (1994)n183 Mute Witness (1994)n301 Picture Bride (Bijo photo) (1994)n309 Red Firecracker, Green Firecracker (Pao Da Shu...n ... n160590 Survive and Advance (2013)n161944 The Last Brickmaker in America (2001)n162542 Rustom (2016)n163949 The Beatles: Eight Days a Week - The Touring Y...nName: title, Length: 315, dtype: objectn# 得到最高分的電影有幾次評分?ncountPerMovie = groupedRatingPerMovie.count()nmaxRatedMovieCount = countPerMovie[countPerMovie.index.isin(maxRatedMovieId.index)]nmaxRatedMovieCountnn# 輸出nmovieIdn53 1n183 1n301 1n309 3n ..n160590 1n161944 1n162542 1n163949 1nName: rating, Length: 315, dtype: int64n

函數

有些操作比較複雜,可以用 apply() 傳入函數來操作數據。我想把 movies.csv 里的 genres 變成列表。

genreSplited = movies[genres].apply(lambda x: x.split(|))nn# 輸出nmovieIdn1 [Adventure, Animation, Children, Comedy, Fantasy]n2 [Adventure, Children, Fantasy]n3 [Comedy, Romance]n4 [Comedy, Drama, Romance]n ... n163056 [Action, Adventure, Fantasy, Sci-Fi]n163949 [Documentary]n164977 [Comedy]n164979 [Documentary]nName: genres, Length: 9125, dtype: objectn

這裡使用的 lambda x: x.split(|) 是個 lambda 表達式,lambda 表達式就是不取名字、不可重複使用的函數,它等價於:

def someName(x):n return x.split(|)n

以上就是 Pandas 的簡單操作,第一次上手必然不熟悉,就像小學你剛接觸乘法和乘法表一樣,多多練習、熟能生巧。很多操作只要知道即可,用到的時候可以 Google 嘛。推薦一些深入 Pandas 和其他數據科學工具的好書:

Python for Data Analysis

作者就是創造 Pandas 的 Wes McKinney,介紹得非常仔細,但出版時間是 2012 年,有點久遠。

Python Data Science Handbook

與 Python for Data Analysis 內容差不多,但出版時間是 2016 年 11 月,非常新且開源。

作業

CS109 Lecture Notes: DataScraping

DS100 Homework: Language in the 2016 Presidential Election

致謝

數據科學導論筆記基於加州大學伯克利校區 DS100 與哈佛大學 CS109 的課程主頁改寫,參考了課件、筆記、閱讀材料及作業,感謝製作這兩門課程的 Joe Blitzstein、Hanspeter Pfister、Verena Kaynig-Fittkau、Joseph E. Gonzalez、Joseph Hellerstein、Deborah Nolan 和 Bin Yu。本文基於 DS100 Week 2 - Data Wrangling,及 CS 109 的 Lecture 2 - Web Scraping, Regular Expressions, Data Reshaping, Data Cleanup, Pandas。

推薦閱讀:

怎樣將一個24的n次方複雜度的計算優化?
深刻理解上下文管理器

TAG:数据科学 | Python | 数据整理 |