# 四、數據清理
> 原文:[DS-100/textbook/notebooks/ch04](https://nbviewer.jupyter.org/github/DS-100/textbook/tree/master/notebooks/ch04/)
>
> 校驗:[飛龍](https://github.com/wizardforcel)
>
> 自豪地采用[谷歌翻譯](https://translate.google.cn/)
數據以多種格式出現,并且在分析的實用性方面差別很大。盡管我們希望,我們所有的數據都以表格的形式出現,并且每個數值的記錄都一致和準確,但實際上,我們必須仔細檢查數據,找出最終可能導致錯誤結論的潛在問題。
術語“數據清理”是指梳理數據,并決定如何解決不一致和缺失值的過程。我們將討論數據集中發現的常見問題,以及解決這些問題的方法。
數據清理存在固有的局限性。例如,沒有任何數據清理能夠解決帶偏差的采樣過程。在著手進行有時很長的數據清理過程之前,我們必須保證,我們的數據是準確收集的,盡可能沒有偏差。只有這樣,我們才能調查數據本身,并使用數據清理來解決數據格式或輸入過程中的問題。
我們將通過處理伯克利市警察數據集,介紹數據清理技術。
## 調查伯克利警察數據
我們將使用伯克利警察局的公開數據集,來演示數據清理技術。 我們已經下載了服務呼叫數據集和截停數據集。
我們可以使用帶有`-lh`標志的`ls` shell 命令,來查看這些文件的更多詳細信息:
```
!ls -lh data/
total 13936
-rw-r--r--@ 1 sam staff 979K Aug 29 14:41 Berkeley_PD_-_Calls_for_Service.csv
-rw-r--r--@ 1 sam staff 81B Aug 29 14:28 cvdow.csv
-rw-r--r--@ 1 sam staff 5.8M Aug 29 14:41 stops.json
```
上面的命令顯示了數據文件及其文件大小。 這是特別有用的,因為我們現在知道這些文件足夠小,可以加載到內存中。 作為一個經驗法則,將文件加載到內存中,內存大約占計算機總內存容量的四分之一,通常是安全的。 例如,如果一臺電腦有 4GB 的 RAM ,我們應該可以在`pandas`中加載 1GB 的 CSV 文件。 為了處理更大的數據集,我們需要額外的計算工具,我們將在本書后面介紹。
注意在`ls`之前使用感嘆號。 這告訴 Jupyter 下一行代碼是 shell 命令,而不是 Python 表達式。 我們可以使用`!`在 Jupyter 中運行任何可用的 shell 命令:
```py
# The `wc` shell command shows us how many lines each file has.
# We can see that the `stops.json` file has the most lines (29852).
!wc -l data/*
16497 data/Berkeley_PD_-_Calls_for_Service.csv
8 data/cvdow.csv
29852 data/stops.json
46357 total
```
### 理解數據生成
在數據清理或處理之前,我們將陳述你應該向所有數據集詢問的重要問題。 這些問題與數據的生成方式有關,因此數據清理通常無法解決這里出現的問題。
數據包含什么內容? 服務呼叫數據的網站指出,該數據集描述了“過去 180 天內的犯罪事件(而非犯罪報告)”。 進一步閱讀表明“并非所有警務服務的呼叫都包含在內(例如動物咬傷)”。
截停數據的網站指出,該數據集包含自 2015 年 1 月 26 日起的所有“車輛截停(包括自行車)和行人截停(最多五人)”的數據。
數據是普查嗎? 這取決于我們感興趣的人群。 例如,如果我們感興趣的是,過去 180 天內的犯罪事件的服務呼叫,那么呼叫數據集就是一個普查。 但是,如果我們感興趣的是,過去 10 年內的服務呼叫,數據集顯然不是普查。 由于數據收集開始于 2015 年 1 月 26 日,我們可以對截停數據集做出類似的猜測。
如果數據構成一個樣本,它是概率樣本嗎? 如果我們正在調查一個時間段,數據沒有它的條目,那么數據不會形成概率樣本,因為在數據收集過程中沒有涉及隨機性 - 我們有一定時間段的所有數據,但其他時間段沒有數據。
這些數據對我們的結論有何限制? 雖然我們會在數據處理的每一步都提出這個問題,但我們已經可以看到,我們的數據帶有重要的限制。 最重要的限制是,我們不能對我們的數據集未涵蓋的時間段進行無偏估計。
### 清理呼叫數據集
現在我們來清理呼叫數據集。`head` shell 命令打印文件的前五行。
```
!head data/Berkeley_PD_-_Calls_for_Service.csv
CASENO,OFFENSE,EVENTDT,EVENTTM,CVLEGEND,CVDOW,InDbDate,Block_Location,BLKADDR,City,State
17091420,BURGLARY AUTO,07/23/2017 12:00:00 AM,06:00,BURGLARY - VEHICLE,0,08/29/2017 08:28:05 AM,"2500 LE CONTE AVE
Berkeley, CA
(37.876965, -122.260544)",2500 LE CONTE AVE,Berkeley,CA
17020462,THEFT FROM PERSON,04/13/2017 12:00:00 AM,08:45,LARCENY,4,08/29/2017 08:28:00 AM,"2200 SHATTUCK AVE
Berkeley, CA
(37.869363, -122.268028)",2200 SHATTUCK AVE,Berkeley,CA
17050275,BURGLARY AUTO,08/24/2017 12:00:00 AM,18:30,BURGLARY - VEHICLE,4,08/29/2017 08:28:06 AM,"200 UNIVERSITY AVE
Berkeley, CA
(37.865491, -122.310065)",200 UNIVERSITY AVE,Berkeley,CA
```
它似乎是逗號分隔值(CSV)文件,盡管很難判斷整個文件是否格式正確。 我們可以使用`pd.read_csv`將文件讀取為`DataFrame`。 如果`pd.read_csv`產生錯誤,我們將不得不更進一步并手動解決格式問題。 幸運的是,`pd.read_csv`成功返回一個`DataFrame`:
```py
calls = pd.read_csv('data/Berkeley_PD_-_Calls_for_Service.csv')
calls
```
| | CASENO | OFFENSE | EVENTDT | EVENTTM | ... | Block_Location | BLKADDR | City | State |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 17091420 | BURGLARY AUTO | 07/23/2017 12:00:00 AM | 06:00 | ... | 2500 LE CONTE AVE\nBerkeley, CA\n(37.876965, -... | 2500 LE CONTE AVE | Berkeley | CA |
| 1 | 17020462 | THEFT FROM PERSON | 04/13/2017 12:00:00 AM | 08:45 | ... | 2200 SHATTUCK AVE\nBerkeley, CA\n(37.869363, -... | 2200 SHATTUCK AVE | Berkeley | CA |
| 2 | 17050275 | BURGLARY AUTO | 08/24/2017 12:00:00 AM | 18:30 | ... | 200 UNIVERSITY AVE\nBerkeley, CA\n(37.865491, ... | 200 UNIVERSITY AVE | Berkeley | CA |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5505 | 17018126 | DISTURBANCE | 04/01/2017 12:00:00 AM | 12:22 | ... | 1600 FAIRVIEW ST\nBerkeley, CA\n(37.850001, -1... | 1600 FAIRVIEW ST | Berkeley | CA |
| 5506 | 17090665 | THEFT MISD. (UNDER $950) | 04/01/2017 12:00:00 AM | 12:00 | ... | 2000 DELAWARE ST\nBerkeley, CA\n(37.874489, -1... | 2000 DELAWARE ST | Berkeley | CA |
| 5507 | 17049700 | SEXUAL ASSAULT MISD. | 08/22/2017 12:00:00 AM | 20:02 | ... | 2400 TELEGRAPH AVE\nBerkeley, CA\n(37.866761, ... | 2400 TELEGRAPH AVE | Berkeley | CA |
5508 行 × 11 列
我們可以定義一個函數來顯示數據的不同片段,然后與之交互:
```py
def df_interact(df):
'''
Outputs sliders that show rows and columns of df
'''
def peek(row=0, col=0):
return df.iloc[row:row + 5, col:col + 6]
interact(peek, row=(0, len(df), 5), col=(0, len(df.columns) - 6))
print('({} rows, {} columns) total'.format(df.shape[0], df.shape[1]))
df_interact(calls)
# (5508 rows, 11 columns) total
```
根據上面的輸出結果,生成的`DataFrame`看起來很合理,因為列的名稱正確,每列中的數據看起來都是一致的。 每列包含哪些數據? 我們可以查看數據集網站:
| 列 | 描述 | 類型 |
| --- | --- | --- |
| CASENO | 案件編號 | 數字 |
| OFFENSE | 案件類型 | 純文本 |
| EVENTDT | 事件的發生日期 | 日期時間 |
| EVENTTM | 事件的發生時間 | 純文本 |
| CVLEGEND | 事件描述 | 純文本 |
| CVDOW | 時間的發生星期 | 數字 |
| InDbDate | 數據集的上傳日期 | 日期時間 |
| Block_Location | 事件的街區級別的地址 | 地點 |
| BLKADDR | | 純文本 |
| City | | 純文本 |
| State | | 純文本 |
數據表面上看起來很容易處理。 但是,在開始數據分析之前,我們必須回答以下問題:
數據集中是否存在缺失值? 這個問題很重要,因為缺失值可能代表許多不同的事情。 例如,遺漏的地址可能意味著刪除了地點來保護隱私,或者某些受訪者選擇不回答調查問題,或錄制設備損壞。
是否有已填寫的缺失值(例如 999 歲,未知年齡,或上午 12:00 為未知日期)? 如果我們忽略它們,它們顯然將影響分析。
數據的哪些部分是由人類輸入的? 我們將很快看到,人類輸入的數據充滿了不一致和錯誤拼寫。
雖然要通過更多檢查,但這三種檢查方法在很多情況下都足夠了。 查看 [Quartz 的不良數據指南](https://github.com/Quartz/bad-data-guide),來獲取更完整的檢查列表。
### 是否存在缺失值?
`pandas`中這是一個簡單的檢查:
```py
# True if row contains at least one null value
null_rows = calls.isnull().any(axis=1)
calls[null_rows]
```
| | CASENO | OFFENSE | EVENTDT | EVENTTM | ... | Block_Location | BLKADDR | City | State |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 116 | 17014831 | BURGLARY AUTO | 03/16/2017 12:00:00 AM | 22:00 | ... | Berkeley, CA\n(37.869058, -122.270455) | NaN | Berkeley | CA |
| 478 | 17042511 | BURGLARY AUTO | 07/20/2017 12:00:00 AM | 16:00 | ... | Berkeley, CA\n(37.869058, -122.270455) | NaN | Berkeley | CA |
| 486 | 17022572 | VEHICLE STOLEN | 04/22/2017 12:00:00 AM | 21:00 | ... | Berkeley, CA\n(37.869058, -122.270455) | NaN | Berkeley | CA |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 4945 | 17091287 | VANDALISM | 07/01/2017 12:00:00 AM | 08:00 | ... | Berkeley, CA\n(37.869058, -122.270455) | NaN | Berkeley | CA |
| 4947 | 17038382 | BURGLARY RESIDENTIAL | 06/30/2017 12:00:00 AM | 15:00 | ... | Berkeley, CA\n(37.869058, -122.270455) | NaN | Berkeley | CA |
| 5167 | 17091632 | VANDALISM | 08/15/2017 12:00:00 AM | 23:30 | ... | Berkeley, CA\n(37.869058, -122.270455) | NaN | Berkeley | CA |
27 行 × 11 列
看起來`BLKADDR`中有 27 個呼叫沒有地址記錄。 不幸的是,對于地點的記錄方式,數據描述并不十分清楚。 我們知道,所有這些呼叫都是由于伯克利的事件,因此我們可以認為,這些呼叫的地址最初是在伯克利的某個地方。
### 有沒有已填充的缺失值?
從上面的缺失值檢查中,我們可以看到,如果位置缺失,`Block_Location`列會記錄`Berkeley, CA`。
另外,通過查看呼叫表,我們發現`EVENTDT`列日期正確,但所有時間都記錄了上午 12 點。 相反,時間在`EVENTTM`列中。
```py
# Show the first 7 rows of the table again for reference
calls.head(7)
```
| | CASENO | OFFENSE | EVENTDT | EVENTTM | ... | Block_Location | BLKADDR | City | State |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 17091420 | BURGLARY AUTO | 07/23/2017 12:00:00 AM | 06:00 | ... | 2500 LE CONTE AVE\nBerkeley, CA\n(37.876965, -... | 2500 LE CONTE AVE | Berkeley | CA |
| 1 | 17020462 | THEFT FROM PERSON | 04/13/2017 12:00:00 AM | 08:45 | ... | 2200 SHATTUCK AVE\nBerkeley, CA\n(37.869363, -... | 2200 SHATTUCK AVE | Berkeley | CA |
| 2 | 17050275 | BURGLARY AUTO | 08/24/2017 12:00:00 AM | 18:30 | ... | 200 UNIVERSITY AVE\nBerkeley, CA\n(37.865491, ... | 200 UNIVERSITY AVE | Berkeley | CA |
| 3 | 17019145 | GUN/WEAPON | 04/06/2017 12:00:00 AM | 17:30 | ... | 1900 SEVENTH ST\nBerkeley, CA\n(37.869318, -12... | 1900 SEVENTH ST | Berkeley | CA |
| 4 | 17044993 | VEHICLE STOLEN | 08/01/2017 12:00:00 AM | 18:00 | ... | 100 PARKSIDE DR\nBerkeley, CA\n(37.854247, -12... | 100 PARKSIDE DR | Berkeley | CA |
| 5 | 17037319 | BURGLARY RESIDENTIAL | 06/28/2017 12:00:00 AM | 12:00 | ... | 1500 PRINCE ST\nBerkeley, CA\n(37.851503, -122... | 1500 PRINCE ST | Berkeley | CA |
| 6 | 17030791 | BURGLARY RESIDENTIAL | 05/30/2017 12:00:00 AM | 08:45 | ... | 300 MENLO PL\nBerkeley, CA\n | 300 MENLO PL | Berkeley | CA |
7 行 × 11 列
作為數據清理步驟,我們希望合并`EVENTDT`和`EVENTTM`列,在一個字段中記錄日期和時間。 如果我們定義一個函數,接受`DF`并返回新的`DF`,我們可以稍后使用`pd.pipe`一次性應用所有轉換。
```py
def combine_event_datetimes(calls):
combined = pd.to_datetime(
# Combine date and time strings
calls['EVENTDT'].str[:10] + ' ' + calls['EVENTTM'],
infer_datetime_format=True,
)
return calls.assign(EVENTDTTM=combined)
# To peek at the result without mutating the calls DF:
calls.pipe(combine_event_datetimes).head(2)
```
| | CASENO | OFFENSE | EVENTDT | EVENTTM | ... | BLKADDR | City | State | EVENTDTTM |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 17091420 | BURGLARY AUTO | 07/23/2017 12:00:00 AM | 06:00 | ... | 2500 LE CONTE AVE | Berkeley | CA | 2017-07-23 06:00:00 |
| 1 | 17020462 | THEFT FROM PERSON | 04/13/2017 12:00:00 AM | 08:45 | ... | 2200 SHATTUCK AVE | Berkeley | CA | 2017-04-13 08:45:00 |
2 行 × 12 列
### 數據的哪些部分是由人類輸入的?
看起來,大多數數據列是機器記錄的,包括日期,時間,星期和事件位置。
另外,`OFFENSE`和`CVLEGEND`列看起來包含一致的值。 我們可以檢查每列中的唯一值,來查看是否有任何拼寫錯誤:
```py
calls['OFFENSE'].unique()
'''
array(['BURGLARY AUTO', 'THEFT FROM PERSON', 'GUN/WEAPON',
'VEHICLE STOLEN', 'BURGLARY RESIDENTIAL', 'VANDALISM',
'DISTURBANCE', 'THEFT MISD. (UNDER $950)', 'THEFT FROM AUTO',
'DOMESTIC VIOLENCE', 'THEFT FELONY (OVER $950)', 'ALCOHOL OFFENSE',
'MISSING JUVENILE', 'ROBBERY', 'IDENTITY THEFT',
'ASSAULT/BATTERY MISD.', '2ND RESPONSE', 'BRANDISHING',
'MISSING ADULT', 'NARCOTICS', 'FRAUD/FORGERY',
'ASSAULT/BATTERY FEL.', 'BURGLARY COMMERCIAL', 'MUNICIPAL CODE',
'ARSON', 'SEXUAL ASSAULT FEL.', 'VEHICLE RECOVERED',
'SEXUAL ASSAULT MISD.', 'KIDNAPPING', 'VICE', 'HOMICIDE'], dtype=object)
'''
```
```py
calls['CVLEGEND'].unique()
'''
array(['BURGLARY - VEHICLE', 'LARCENY', 'WEAPONS OFFENSE',
'MOTOR VEHICLE THEFT', 'BURGLARY - RESIDENTIAL', 'VANDALISM',
'DISORDERLY CONDUCT', 'LARCENY - FROM VEHICLE', 'FAMILY OFFENSE',
'LIQUOR LAW VIOLATION', 'MISSING PERSON', 'ROBBERY', 'FRAUD',
'ASSAULT', 'NOISE VIOLATION', 'DRUG VIOLATION',
'BURGLARY - COMMERCIAL', 'ALL OTHER OFFENSES', 'ARSON', 'SEX CRIME',
'RECOVERED VEHICLE', 'KIDNAPPING', 'HOMICIDE'], dtype=object)
'''
```
由于這些列中的每個值似乎都拼寫正確,因此我們不必對這些列執行任何更正。
我們還檢查了`BLKADDR`列的不一致性,發現有時記錄了地址(例如`2500LE CONTE AVE`),但有時記錄十字路口(例如`ALLSTON WAY & FIFTH ST`)。 這表明人類輸入了這些數據,而這一欄很難用于分析。 幸運的是,我們可以使用事件的經緯度而不是街道地址。
```py
calls['BLKADDR'][[0, 5001]]
'''
0 2500 LE CONTE AVE
5001 ALLSTON WAY & FIFTH ST
Name: BLKADDR, dtype: object
'''
```
### 最后的接觸
這個數據集似乎幾乎可用于分析。 `Block_Location`列似乎包含記錄地址,緯度和經度的字符串。 我們將要分割經緯度以便使用。
```py
def split_lat_lon(calls):
return calls.join(
calls['Block_Location']
# Get coords from string
.str.split('\n').str[2]
# Remove parens from coords
.str[1:-1]
# Split latitude and longitude
.str.split(', ', expand=True)
.rename(columns={0: 'Latitude', 1: 'Longitude'})
)
calls.pipe(split_lat_lon).head(2)
```
| | CASENO | OFFENSE | EVENTDT | EVENTTM | ... | City | State | Latitude | Longitude |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 17091420 | BURGLARY AUTO | 07/23/2017 12:00:00 AM | 06:00 | ... | Berkeley | CA | 37.876965 | -122.260544 |
| 1 | 17020462 | THEFT FROM PERSON | 04/13/2017 12:00:00 AM | 08:45 | ... | Berkeley | CA | 37.869363 | -122.268028 |
2 行 × 13 列
然后,我們可以將星期序號與星期進行匹配:
```py
# This DF contains the day for each 數字 in CVDOW
day_of_week = pd.read_csv('data/cvdow.csv')
day_of_week
```
| | CVDOW | Day |
| --- | --- | --- |
| 0 | 0 | Sunday |
| 1 | 1 | Monday |
| 2 | 2 | Tuesday |
| 3 | 3 | Wednesday |
| 4 | 4 | Thursday |
| 5 | 5 | Friday |
| 6 | 6 | Saturday |
```py
def match_weekday(calls):
return calls.merge(day_of_week, on='CVDOW')
calls.pipe(match_weekday).head(2)
```
| | CASENO | OFFENSE | EVENTDT | EVENTTM | ... | BLKADDR | City | State | Day |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 17091420 | BURGLARY AUTO | 07/23/2017 12:00:00 AM | 06:00 | ... | 2500 LE CONTE AVE | Berkeley | CA | Sunday |
| 1 | 17038302 | BURGLARY AUTO | 07/02/2017 12:00:00 AM | 22:00 | ... | BOWDITCH STREET & CHANNING WAY | Berkeley | CA | Sunday |
2 行 × 12 列
我們將刪除我們不再需要的列:
```py
def drop_unneeded_cols(calls):
return calls.drop(columns=['CVDOW', 'InDbDate', 'Block_Location', 'City',
'State', 'EVENTDT', 'EVENTTM'])
```
最后,我們讓`calls` DF 穿過我們定義的所有函數的管道:
```py
calls_final = (calls.pipe(combine_event_datetimes)
.pipe(split_lat_lon)
.pipe(match_weekday)
.pipe(drop_unneeded_cols))
df_interact(calls_final)
```
戶籍數據集現在可用于進一步的數據分析。 在下一節中,我們將清理截停數據集。
```py
# HIDDEN
# Save data to CSV for other chapters
# calls_final.to_csv('../ch5/data/calls.csv', index=False)
```
## 清理截停數據集
[截停數據集](https://data.cityofberkeley.info/Public-Safety/Berkeley-PD-Stop-Data/6e9j-pj9p)記錄警察截停的行人和車輛。 讓我們準備進一步分析。
我們可以使用`head`命令來顯示文件的前幾行。
```
!head data/stops.json
{
"meta" : {
"view" : {
"id" : "6e9j-pj9p",
"name" : "Berkeley PD - Stop Data",
"attribution" : "Berkeley Police Department",
"averageRating" : 0,
"category" : "Public Safety",
"createdAt" : 1444171604,
"description" : "This data was extracted from the Department’s Public Safety Server and covers the data beginning January 26, 2015. On January 26, 2015 the department began collecting data pursuant to General Order B-4 (issued December 31, 2014). Under that order, officers were required to provide certain data after making all vehicle detentions (including bicycles) and pedestrian detentions (up to five persons). This data set lists stops by police in the categories of traffic, suspicious vehicle, pedestrian and bicycle stops. Incident number, date and time, location and disposition codes are also listed in this data.\r\n\r\nAddress data has been changed from a specific address, where applicable, and listed as the block where the incident occurred. Disposition codes were entered by officers who made the stop. These codes included the person(s) race, gender, age (range), reason for the stop, enforcement action taken, and whether or not a search was conducted.\r\n\r\nThe officers of the Berkeley Police Department are prohibited from biased based policing, which is defined as any police-initiated action that relies on the race, ethnicity, or national origin rather than the behavior of an individual or information that leads the police to a particular individual who has been identified as being engaged in criminal activity.",
```
`stops.json`文件顯然不是 CSV 文件。 在這種情況下,該文件包含 JSON(JavaScript 對象表示法)格式的數據,這是一種常用的數據格式,其中數據記錄為字典格式。 Python 的[`json`模塊](https://docs.python.org/3/library/json.html)使得該文件可以簡單地讀取為字典。
```py
import json
# Note that this could cause our computer to run out of memory if the file
# is large. In this case, we've verified that the file is small enough to
# read in beforehand.
with open('data/stops.json') as f:
stops_dict = json.load(f)
stops_dict.keys()
# dict_keys(['meta', 'data'])
```
請注意,`stops_dict`是一個 Python 字典,因此顯示它將在筆記本中顯示整個數據集。 這可能會導致瀏覽器崩潰,所以我們只顯示上面的字典鍵。 為了查看數據而不會導致瀏覽器崩潰,我們可以將字典打印為一個字符串,并僅輸出字符串的一些首字符。
```py
from pprint import pformat
def print_dict(dictionary, num_chars=1000):
print(pformat(dictionary)[:num_chars])
print_dict(stops_dict['meta'])
'''
{'view': {'attribution': 'Berkeley Police Department',
'averageRating': 0,
'category': 'Public Safety',
'columns': [{'dataTypeName': 'meta_data',
'fieldName': ':sid',
'flags': ['hidden'],
'format': {},
'id': -1,
'name': 'sid',
'position': 0,
'renderTypeName': 'meta_data'},
{'dataTypeName': 'meta_data',
'fieldName': ':id',
'flags': ['hidden'],
'format': {},
'id': -1,
'name': 'id',
'position': 0,
'renderTypeName': 'meta_data'},
{'dataTypeName': 'meta_data',
'fieldName': ':position',
'flags': ['hidden'],
'format': {},
'''
```
```py
print_dict(stops_dict['data'], num_chars=300)
'''
[[1,
'29A1B912-A0A9-4431-ADC9-FB375809C32E',
1,
1444146408,
'932858',
1444146408,
'932858',
None,
'2015-00004825',
'2015-01-26T00:10:00',
'SAN PABLO AVE / MARIN AVE',
'T',
'M',
None,
None],
[2,
'1644D161-1113-4C4F-BB2E-BF780E7AE73E',
2,
1444146408,
'932858',
14
'''
```
我們可以推斷,字典中的`'meta'`鍵包含數據及其列的描述,`'data'`包含數據行的列表。 我們可以使用這些信息來初始化`DataFrame`。
```py
# Load the data from JSON and assign column titles
stops = pd.DataFrame(
stops_dict['data'],
columns=[c['name'] for c in stops_dict['meta']['view']['columns']])
stops
```
| | sid | id | position | created_at | ... | Incident Type | Dispositions | Location - Latitude | Location - Longitude |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 1 | 29A1B912-A0A9-4431-ADC9-FB375809C32E | 1 | 1444146408 | ... | T | M | None | None |
| 1 | 2 | 1644D161-1113-4C4F-BB2E-BF780E7AE73E | 2 | 1444146408 | ... | T | M | None | None |
| 2 | 3 | 5338ABAB-1C96-488D-B55F-6A47AC505872 | 3 | 1444146408 | ... | T | M | None | None |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 29205 | 31079 | C2B606ED-7872-4B0B-BC9B-4EF45149F34B | 31079 | 1496269085 | ... | T | BM2TWN; | None | None |
| 29206 | 31080 | 8FADF18D-7FE9-441D-8709-7BFEABDACA7A | 31080 | 1496269085 | ... | T | HM4TCS; | 37.8698757000001 | -122.286550846 |
| 29207 | 31081 | F60BD2A4-8C47-4BE7-B1C6-4934BE9DF838 | 31081 | 1496269085 | ... | 1194 | AR; | 37.867207539 | -122.256529377 |
29208 行 × 15 列
```py
# Prints column names
stops.columns
'''
Index(['sid', 'id', 'position', 'created_at', 'created_meta', 'updated_at',
'updated_meta', 'meta', 'Incident Number', 'Call Date/Time', 'Location',
'Incident Type', 'Dispositions', 'Location - Latitude',
'Location - Longitude'],
dtype='object')
'''
```
該網站包含以下列的文檔:
| 列 | 描述 | 類型 |
| --- | --- | --- |
| Incident 數字 | 計算機輔助調度(CAD)程序創建的事件數量 | 純文本 |
| Call Date/Time | 事件/截停的日期和時間 | 日期時間 |
| Location | 事件/截停的一般位置 | 純文本 |
| Incident Type | 這是在 CAD 程序中創建的發生事件的類型。 代碼表示交通截停(`T`),可疑車輛截停(`1196`),行人截停(`1194`)和自行車截停(`1194B`)。 | 純文本 |
| Dispositions | 按如下順序組織:第一個字符為種族,如下所示:`A`(亞洲),`B`(黑人),`H`(西班牙裔),`O`(其他),`W`(白人);第二個字符為性別,如下所示:`F`(女性),`M`(男性);第三個字符為年齡范圍,如下:`1`(小于 18),`2`(18-29),`3`(30-39),`4`(大于 40);第四個字符為原因,如下:`I`(調查),`T`(交通),`R`(合理懷疑),`K`(說教/假釋),`W`(通緝);第五個字符為執行,如下:`A`(逮捕),`C`(引用),`O`(其他),`W`(警告);第六個字符為車輛搜索,如下:`S`(搜索),`N`(無搜索)。也可能出現其他處置,它們是:`P` - 主要案件報告,`M` - 僅 MDT,`AR` - 僅逮捕報告(未提交案件報告),`IN` - 事故報告,`FC` - 穿卡區,`CO` - 碰撞調查報告,`MH` - 緊急情況精神評估,`TOW` - 扣押車輛,0 或 00000 - 官員截停了超過五人。 | 純文本 |
| Location - Latitude | 呼叫的一般緯度。 此數據僅在 2017 年 1 月之后上傳。 | 數字 |
| Location - Longitude | 呼叫的一般經度。 此數據僅在 2017 年 1 月之后上傳。 | 數字 |
請注意,網站不包含截停表的前 8 列的說明。 由于這些列似乎包含我們在此次分析中不感興趣的元數據,因此我們從表中刪除它們。
```py
columns_to_drop = ['sid', 'id', 'position', 'created_at', 'created_meta',
'updated_at', 'updated_meta', 'meta']
# This function takes in a DF and returns a DF so we can use it for .pipe
def drop_unneeded_cols(stops):
return stops.drop(columns=columns_to_drop)
stops.pipe(drop_unneeded_cols)
```
| | Incident Number | Call Date/Time | Location | Incident Type | Dispositions | Location - Latitude | Location - Longitude |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 2015-00004825 | 2015-01-26T00:10:00 | SAN PABLO AVE / MARIN AVE | T | M | None | None |
| 1 | 2015-00004829 | 2015-01-26T00:50:00 | SAN PABLO AVE / CHANNING WAY | T | M | None | None |
| 2 | 2015-00004831 | 2015-01-26T01:03:00 | UNIVERSITY AVE / NINTH ST | T | M | None | None |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 29205 | 2017-00024245 | 2017-04-30T22:59:26 | UNIVERSITY AVE/6TH ST | T | BM2TWN; | None | None |
| 29206 | 2017-00024250 | 2017-04-30T23:19:27 | UNIVERSITY AVE / WEST ST | T | HM4TCS; | 37.8698757000001 | -122.286550846 |
| 29207 | 2017-00024254 | 2017-04-30T23:38:34 | CHANNING WAY / BOWDITCH ST | 1194 | AR; | 37.867207539 | -122.256529377 |
29208 行 × 7 列
與呼叫數據集一樣,我們將回答截停數據集的以下三個問題:
+ 數據集中是否存在缺失值?
+ 是否有已填寫的缺失值(例如 999 歲,未知年齡或上午 12:00 為未知日期)?
+ 數據的哪些部分是由人類輸入的?
### 是否存在缺失值?
我們可以清楚地看到,有很多缺失的緯度和經度。 數據描述指出,這兩列僅在 2017 年 1 月之后填寫。
```py
# True if row contains at least one null value
null_rows = stops.isnull().any(axis=1)
stops[null_rows]
```
| | Incident Number | Call Date/Time | Location | Incident Type | Dispositions | Location - Latitude | Location - Longitude |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 2015-00004825 | 2015-01-26T00:10:00 | SAN PABLO AVE / MARIN AVE | T | M | None | None |
| 1 | 2015-00004829 | 2015-01-26T00:50:00 | SAN PABLO AVE / CHANNING WAY | T | M | None | None |
| 2 | 2015-00004831 | 2015-01-26T01:03:00 | UNIVERSITY AVE / NINTH ST | T | M | None | None |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 29078 | 2017-00023764 | 2017-04-29T01:59:36 | 2180 M L KING JR WAY | 1194 | BM4IWN; | None | None |
| 29180 | 2017-00024132 | 2017-04-30T12:54:23 | 6TH/UNI | 1194 | M; | None | None |
| 29205 | 2017-00024245 | 2017-04-30T22:59:26 | UNIVERSITY AVE/6TH ST | T | BM2TWN; | None | None |
25067 行 × 7 列
我們可以檢查其他列的缺失值:
```py
# True if row contains at least one null value without checking
# the latitude and longitude columns
null_rows = stops.iloc[:, :-2].isnull().any(axis=1)
df_interact(stops[null_rows])
# (63 rows, 7 columns) total
```
通過瀏覽上面的表格,我們可以看到所有其他缺失值在`Dispositions`列中。 不幸的是,我們從數據描述中并不知道,為什么這些值可能會缺失。 由于原始表格中,與 25,000 行相比,只有 63 個缺失值,因此我們可以繼續進行分析,同時注意這些缺失值可能會影響結果。
### 有沒有已填寫的缺失值?
看起來,沒有為我們填充之前的缺失值。 與呼叫數據集不同,它的日期和時間位于不同列中,截停數據集中的`Call Date/Time`列包含了日期和時間。
### 數據的哪些部分是由人類輸入的?
與呼叫數據集一樣,該數據集中的大部分列看起來都是由機器記錄的,或者是人類選擇的類別(例如事件類型)。
但是,`Location `列的輸入值不一致。 果然,我們在數據中發現了一些輸入錯誤:
```py
stops['Location'].value_counts()
'''
2200 BLOCK SHATTUCK AVE 229
37.8693028530001~-122.272234021 213
UNIVERSITY AVE / SAN PABLO AVE 202
...
VALLEY ST / DWIGHT WAY 1
COLLEGE AVE / SIXTY-THIRD ST 1
GRIZZLY PEAK BLVD / MARIN AVE 1
Name: Location, Length: 6393, dtype: int64
'''
```
真是一團糟! 有時看起來輸入了地址,有時是十字路口,其他時候是經緯度。 不幸的是,我們沒有非常完整的經緯度數據來代替這一列。 如果我們想將位置用于未來的分析,我們可能必須手動清理此列。
我們也可以檢查`Dispositions`列:
```py
dispositions = stops['Dispositions'].value_counts()
# Outputs a slider to pan through the unique Dispositions in
# order of how often they appear
interact(lambda row=0: dispositions.iloc[row:row+7],
row=(0, len(dispositions), 7))
# <function __main__.<lambda>>
```
`Dispositions`列也不一致。 例如,一些處置以空格開始,一些以分號結束,另一些包含多個條目。 值的多樣性表明,該字段包含人類輸入的值,應謹慎對待。
```py
# Strange values...
dispositions.iloc[[0, 20, 30, 266, 1027]]
'''
M 1683
M; 238
M 176
HF4TWN; 14
OM4KWS 1
Name: Dispositions, dtype: int64
'''
```
另外,最常見的處置是`M`,它不是`Dispositions`列中允許的第一個字符。 這可能意味著,該列的格式會隨時間而變化,或者允許官員輸入處置,它不匹配數據描述中的格式。 無論如何,該列將很難處理。
我們可以采取一些簡單的步驟來清理處置列,方法是刪除前導和尾后空格,刪除尾后分號并用逗號替換剩余的分號。
```py
def clean_dispositions(stops):
cleaned = (stops['Dispositions']
.str.strip()
.str.rstrip(';')
.str.replace(';', ','))
return stops.assign(Dispositions=cleaned)
```
和以前一樣,我們現在可以使`stops` DF 由管道穿過我們定義的清理函數:
```py
stops_final = (stops
.pipe(drop_unneeded_cols)
.pipe(clean_dispositions))
df_interact(stops_final)
# (29208 rows, 7 columns) total
```
### 總結
這兩個數據集表明,數據清理往往既困難又乏味。 清理 100% 的數據通常需要很長時間,但不清理數據會導致錯誤的結論;我們必須衡量我們的選擇,并在每次遇到新數據集時達到平衡。
數據清理過程中做出的決定,會影響所有未來的分析。 例如,我們選擇不清理截停數據集的`Location`列,因此我們應該謹慎對待該列。 在數據清理過程中做出的每一項決定,都應仔細記錄以供日后參考,最好在筆記本上,以便代碼和解釋出現在一起。
```py
# HIDDEN
# Save data to CSV for other chapters
# stops_final.to_csv('../ch5/data/stops.csv', index=False)
```
- 一、數據科學的生命周期
- 二、數據生成
- 三、處理表格數據
- 四、數據清理
- 五、探索性數據分析
- 六、數據可視化
- Web 技術
- 超文本傳輸協議
- 處理文本
- python 字符串方法
- 正則表達式
- regex 和 python
- 關系數據庫和 SQL
- 關系模型
- SQL
- SQL 連接
- 建模與估計
- 模型
- 損失函數
- 絕對損失和 Huber 損失
- 梯度下降與數值優化
- 使用程序最小化損失
- 梯度下降
- 凸性
- 隨機梯度下降法
- 概率與泛化
- 隨機變量
- 期望和方差
- 風險
- 線性模型
- 預測小費金額
- 用梯度下降擬合線性模型
- 多元線性回歸
- 最小二乘-幾何透視
- 線性回歸案例研究
- 特征工程
- 沃爾瑪數據集
- 預測冰淇淋評級
- 偏方差權衡
- 風險和損失最小化
- 模型偏差和方差
- 交叉驗證
- 正規化
- 正則化直覺
- L2 正則化:嶺回歸
- L1 正則化:LASSO 回歸
- 分類
- 概率回歸
- Logistic 模型
- Logistic 模型的損失函數
- 使用邏輯回歸
- 經驗概率分布的近似
- 擬合 Logistic 模型
- 評估 Logistic 模型
- 多類分類
- 統計推斷
- 假設檢驗和置信區間
- 置換檢驗
- 線性回歸的自舉(真系數的推斷)
- 學生化自舉
- P-HACKING
- 向量空間回顧
- 參考表
- Pandas
- Seaborn
- Matplotlib
- Scikit Learn