四、数据清理
原文:DS-100/textbook/notebooks/ch04
译者:飞龙
自豪地采用谷歌翻译
数据以多种格式出现,并且在分析的实用性方面差别很大。尽管我们希望,我们所有的数据都以表格的形式出现,并且每个数值的记录都一致和准确,但实际上,我们必须仔细检查数据,找出最终可能导致错误结论的潜在问题。
术语“数据清理”是指梳理数据,并决定如何解决不一致和缺失值的过程。我们将讨论数据集中发现的常见问题,以及解决这些问题的方法。
数据清理存在固有的局限性。例如,没有任何数据清理能够解决带偏差的采样过程。在着手进行有时很长的数据清理过程之前,我们必须保证,我们的数据是准确收集的,尽可能没有偏差。只有这样,我们才能调查数据本身,并使用数据清理来解决数据格式或输入过程中的问题。
我们将通过处理伯克利市警察数据集,介绍数据清理技术。
调查伯克利警察数据
我们将使用伯克利警察局的公开数据集,来演示数据清理技术。 我们已经下载了服务呼叫数据集和截停数据集。
我们可以使用带有-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 命令:
# 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
:
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 列
我们可以定义一个函数来显示数据的不同片段,然后与之交互:
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 的不良数据指南,来获取更完整的检查列表。
是否存在缺失值?
pandas
中这是一个简单的检查:
# 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
列中。
# 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
一次性应用所有转换。
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
列看起来包含一致的值。 我们可以检查每列中的唯一值,来查看是否有任何拼写错误:
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)
'''
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
)。 这表明人类输入了这些数据,而这一栏很难用于分析。 幸运的是,我们可以使用事件的经纬度而不是街道地址。
calls['BLKADDR'][[0, 5001]]
'''
0 2500 LE CONTE AVE
5001 ALLSTON WAY & FIFTH ST
Name: BLKADDR, dtype: object
'''
最后的接触
这个数据集似乎几乎可用于分析。 Block_Location
列似乎包含记录地址,纬度和经度的字符串。 我们将要分割经纬度以便使用。
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 列
然后,我们可以将星期序号与星期进行匹配:
# 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 |
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 列
我们将删除我们不再需要的列:
def drop_unneeded_cols(calls):
return calls.drop(columns=['CVDOW', 'InDbDate', 'Block_Location', 'City',
'State', 'EVENTDT', 'EVENTTM'])
最后,我们让calls
DF 穿过我们定义的所有函数的管道:
calls_final = (calls.pipe(combine_event_datetimes)
.pipe(split_lat_lon)
.pipe(match_weekday)
.pipe(drop_unneeded_cols))
df_interact(calls_final)
户籍数据集现在可用于进一步的数据分析。 在下一节中,我们将清理截停数据集。
# HIDDEN
# Save data to CSV for other chapters
# calls_final.to_csv('../ch5/data/calls.csv', index=False)
清理截停数据集
截停数据集记录警察截停的行人和车辆。 让我们准备进一步分析。
我们可以使用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
模块使得该文件可以简单地读取为字典。
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 字典,因此显示它将在笔记本中显示整个数据集。 这可能会导致浏览器崩溃,所以我们只显示上面的字典键。 为了查看数据而不会导致浏览器崩溃,我们可以将字典打印为一个字符串,并仅输出字符串的一些首字符。
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': {},
'''
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
。
# 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 列
# 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 列的说明。 由于这些列似乎包含我们在此次分析中不感兴趣的元数据,因此我们从表中删除它们。
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 月之后填写。
# 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 列
我们可以检查其他列的缺失值:
# 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
列的输入值不一致。 果然,我们在数据中发现了一些输入错误:
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
列:
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
列也不一致。 例如,一些处置以空格开始,一些以分号结束,另一些包含多个条目。 值的多样性表明,该字段包含人类输入的值,应谨慎对待。
# 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
列中允许的第一个字符。 这可能意味着,该列的格式会随时间而变化,或者允许官员输入处置,它不匹配数据描述中的格式。 无论如何,该列将很难处理。
我们可以采取一些简单的步骤来清理处置列,方法是删除前导和尾后空格,删除尾后分号并用逗号替换剩余的分号。
def clean_dispositions(stops):
cleaned = (stops['Dispositions']
.str.strip()
.str.rstrip(';')
.str.replace(';', ','))
return stops.assign(Dispositions=cleaned)
和以前一样,我们现在可以使stops
DF 由管道穿过我们定义的清理函数:
stops_final = (stops
.pipe(drop_unneeded_cols)
.pipe(clean_dispositions))
df_interact(stops_final)
# (29208 rows, 7 columns) total
总结
这两个数据集表明,数据清理往往既困难又乏味。 清理 100% 的数据通常需要很长时间,但不清理数据会导致错误的结论;我们必须衡量我们的选择,并在每次遇到新数据集时达到平衡。
数据清理过程中做出的决定,会影响所有未来的分析。 例如,我们选择不清理截停数据集的Location
列,因此我们应该谨慎对待该列。 在数据清理过程中做出的每一项决定,都应仔细记录以供日后参考,最好在笔记本上,以便代码和解释出现在一起。
# HIDDEN
# Save data to CSV for other chapters
# stops_final.to_csv('../ch5/data/stops.csv', index=False)
网友评论