Pandas 那些年踩过的坑
Content
在进行数据处理和分析时,pandas就像一条高速公路,能够帮助我们快速的进行各种数据处理和分析操作。但是高速公路也可能有各种坑,一不小心就翻车。
在平时的工作中,也积累了pandas处理的各种坑,记录下来,跟大家分享一下。
import pandas as pd
import numpy as np
1. Pandas IO中的坑
先从pandas的读写操作写起。使用pandas读写CSV文件的最常见的操作,即使这个最简单的操作,就很有可能掉入坑里。
1.1 解决读的坑,让pandas读文件内存占用减小 80%
资源总是有限的, 僧多肉少是常见的
而我一次在公司的机器学习平台申请到5G内存,需要打开的csv文件只有900M,当你信心满满的使用 pandas.read_csv 去读取文件,意想不到的是内存爆了, 内存爆了,内存爆了!!!
于是乎,就去学习了一下pandas在内存中存数据的方式,并且找到了解决方式,并很好的填了这个坑。
一般来说,用pandas处理小于100M的数据,性能不是问题。当用pandas来处理几百兆甚至几个G的数据时,将会比较耗时,同时会导致程序因内存不足而运行失败。那么怎么就解决这个问题呢,我们先来讨论一下pandas的内存使用。
如下表所示,pandas共有6种大的数据类型,在底层pandas会按照数据类型将列分组形成数据块(blocks), 相同数据类型的列会合到一起存储。实际上,对于整型和浮点型数据,pandas将它们以 NumPy ndarray 的形式存储。
从表中可以看到,不同的存储方式所占用的内存不同。其中类型为category的数据在底层使用整型数值来表示该列的值,而不是用原值。当我们把一列转换成category类型时,pandas会用一种最省空间的int子类型去表示这一列中所有的唯一值。当一列只包含有限种值时,这种设计是很不错的。
了解到这里,我们是不是可以将占用内存多的数据类型转为占用内存低的数据类型,以到达减小内存的占用的目的。
memory usage | float | int | unit | category | bool | object |
---|---|---|---|---|---|---|
1 bytes | int8 | unit8 | ||||
2 bytes | float16 | int16 | unit16 | |||
4 bytes | float32 | int32 | unit32 | |||
8 bytes | float64 | int64 | unit64 | |||
variable | Slytherin | category | bool | object |
随便找了个数据,实际操作看一下:
data = pd.read_csv('game_logs.csv')
data.head()
/Users/kk_j/anaconda3/envs/python2_for_project/lib/python2.7/site-packages/IPython/core/interactiveshell.py:2717: DtypeWarning: Columns (12,13,14,15,19,20,81,83,85,87,93,94,95,96,97,98,99,100,105,106,108,109,111,112,114,115,117,118,120,121,123,124,126,127,129,130,132,133,135,136,138,139,141,142,144,145,147,148,150,151,153,154,156,157,160) have mixed types. Specify dtype option on import or set low_memory=False.
interactivity=interactivity, compiler=compiler, result=result)
date | number_of_game | day_of_week | v_name | v_league | v_game_number | h_name | h_league | h_game_number | v_score | ... | h_player_7_name | h_player_7_def_pos | h_player_8_id | h_player_8_name | h_player_8_def_pos | h_player_9_id | h_player_9_name | h_player_9_def_pos | additional_info | acquisition_info | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 18710504 | 0 | Thu | CL1 | na | 1 | FW1 | na | 1 | 0 | ... | Ed Mincher | 7.0 | mcdej101 | James McDermott | 8.0 | kellb105 | Bill Kelly | 9.0 | NaN | Y |
1 | 18710505 | 0 | Fri | BS1 | na | 1 | WS3 | na | 1 | 20 | ... | Asa Brainard | 1.0 | burrh101 | Henry Burroughs | 9.0 | berth101 | Henry Berthrong | 8.0 | HTBF | Y |
2 | 18710506 | 0 | Sat | CL1 | na | 2 | RC1 | na | 1 | 12 | ... | Pony Sager | 6.0 | birdg101 | George Bird | 7.0 | stirg101 | Gat Stires | 9.0 | NaN | Y |
3 | 18710508 | 0 | Mon | CL1 | na | 3 | CH1 | na | 1 | 12 | ... | Ed Duffy | 6.0 | pinke101 | Ed Pinkham | 5.0 | zettg101 | George Zettlein | 1.0 | NaN | Y |
4 | 18710509 | 0 | Tue | BS1 | na | 2 | TRO | na | 1 | 9 | ... | Steve Bellan | 5.0 | pikel101 | Lip Pike | 3.0 | cravb101 | Bill Craver | 6.0 | HTBF | Y |
5 rows × 161 columns
data.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 171907 entries, 0 to 171906
Columns: 161 entries, date to acquisition_info
dtypes: float64(77), int64(6), object(78)
memory usage: 738.1 MB
可以看到这个数据占用内存738.1M,而文件原来的大小仅仅128M,内存占用是原文件大小的 6 倍!!!
再来尝试一下在打开文件的时候指定列的类型,将数据类型为object的列变成category的数据类型。
object_cols = data.select_dtypes(include=['object']).columns.tolist()
dtype_list = ['category' for x in object_cols]
cols_dtype_dict = dict(zip(object_cols, dtype_list))
data1 = pd.read_csv('game_logs.csv', dtype=cols_dtype_dict, date_parser=['date'], infer_datetime_format=True)
data1.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 171907 entries, 0 to 171906
Columns: 161 entries, date to acquisition_info
dtypes: category(78), float64(77), int64(6)
memory usage: 157.2 MB
可以看到,***内存占用从 738.1M 降到了157.2M,有效降低 78.7%***, 而且那一堆Warning 也没了
很开心对不对,没有资源,咱自己创造资源
1.2 解决写的坑,让磁盘空间节约60%
经常听见有小伙伴说,XXXX服务器磁盘空间又满了,大家清理一下自己不用的数据,数据很重要,不能删怎么办。
还是那句话,没有资源,咱创造资源
data1.to_csv('game_logs.gz', compression='gzip', index=False)
去磁盘再去看看文件大小,是不是磁盘变大了。错了,是不是文件变小了。
在我的电脑里,这个文件从 128M 减小到18M。我去,磁盘占用减小了86%
那读取的时候怎么办呢,读取方式不变,还是 read_csv
1.3 解决写的坑,避免挖个坑
这个坑比较简单,但是一不小心就翻车。看个例子
df = pd.DataFrame(np.random.rand(2,2), columns=['a', 'b'])
df
a | b | |
---|---|---|
0 | 0.977292 | 0.343893 |
1 | 0.478050 | 0.781146 |
df.to_csv('test_df.csv')
df1 = pd.read_csv('test_df.csv')
df1
Unnamed: 0 | a | b | |
---|---|---|---|
0 | 0 | 0.977292 | 0.343893 |
1 | 1 | 0.478050 | 0.781146 |
通过以上例子,可以看到,一存一读间,却多了一列。
这种情况极易给后面的操作埋下一个大坑,而且还蒙在鼓里找不出原因。
怎么解决呢,只需要在存的时候,指定 index 参数为 False 即可。再来试一下:
df.to_csv('test_df.csv', index=False)
df = pd.read_csv('test_df.csv')
df
a | b | |
---|---|---|
0 | 0.977292 | 0.343893 |
1 | 0.478050 | 0.781146 |
1.4 python2:加上encoding, 读写好习惯
这个就不举例子讲了。但是讲一下原因。
在工作中经常处理带中文字符的csv文件,一个好的习惯是,在使用pandas的read_csv(其他的read操作一样)进行文件读取时,***加上参数 encoding=‘utf-8’***,并且在数据的操作中都始终使用utf-8的编码格式,会减少非常多的坑。另外在使用 .to_csv 存储带有中文字符的DataFram数据时,加上参数 ***encoding=‘utf-8-sig’***,这样存成的csv就可以用excel打开,而不乱码。
关于编码知识,可以看这里:https://blog.csdn.net/u010223750/article/details/56684096/
1.5 乱入:用pandas进行onehot的神坑
机器学习特征工程中,经常会用到one-hot编码。并且pandas中已经提供了这一函数pandas.get_dummies()。
但是使用这个函数进行one hot操作后得到的数据类型竟然是是uint8,如果进行数值计算时会溢出。
data_df = pd.DataFrame({'sex': ['male', 'female', 'female', 'female', 'female', 'male', 'female'],
'height': [182, 160, 176, 172, 174, 170, 155],
'weight': [65, 50, 55, 48, 48, 100, 80],
'is_air_hostesses': [1, 1, 1, 1, 1, 0, 0]})
data_df
height | is_air_hostesses | sex | weight | |
---|---|---|---|---|
0 | 182 | 1 | male | 65 |
1 | 160 | 1 | female | 50 |
2 | 176 | 1 | female | 55 |
3 | 172 | 1 | female | 48 |
4 | 174 | 1 | female | 48 |
5 | 170 | 0 | male | 100 |
6 | 155 | 0 | female | 80 |
sex_one_hot_df = pd.get_dummies(data_df['sex'])
sex_one_hot_df
female | male | |
---|---|---|
0 | 0 | 1 |
1 | 1 | 0 |
2 | 1 | 0 |
3 | 1 | 0 |
4 | 1 | 0 |
5 | 0 | 1 |
6 | 1 | 0 |
sex_one_hot_df.dtypes
female uint8
male uint8
dtype: object
-sex_one_hot_df
female | male | |
---|---|---|
0 | 0 | 255 |
1 | 255 | 0 |
2 | 255 | 0 |
3 | 255 | 0 |
4 | 255 | 0 |
5 | 0 | 255 |
6 | 255 | 0 |
这真的是一个神坑,如果特征比较多的话,根本发现不了。如果没有发现,后续如果做其他操的时候,就会出错。这个坑藏得深啊。
正确的做法是转换一下数据类型:
sex_one_hot_df = sex_one_hot_df.astype('float')
2. DataFrame 链式索引的坑
2.1 解决:SettingWithCopyWarning:
SettingWithCopyWarning 可能是人们在学习 Pandas 时遇到的最常见的障碍之一。
首先来看看,它出现的情况之一(其他情况大同小异):
sub_df = df.loc[df.a > 0.6]
sub_df
a | b | |
---|---|---|
0 | 0.688818 | 0.510446 |
4 | 0.945565 | 0.801788 |
sub_df['c'] = [1,2]
/Users/kk_j/anaconda3/envs/python2_for_project/lib/python2.7/site-packages/ipykernel_launcher.py:1: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
"""Entry point for launching an IPython kernel.
没有出任何意外,SettingWithCopyWarning 出现。首先要理解的是,SettingWithCopyWarning 是一个警告 Warning,而不是错误 Error,它告诉你,你的操作可能没有按预期运行,需要检查结果以确保没有出错。当你查看结果,发现结果没有错,就是在按预期进行,你极有可能忽略这个Warning, 而当下次它再次出现时,你不会再检查,然后错误就出现了。
直接说他出现的原因,那就是***链式索引产生的新的变量并没有在内存中创建副本,当接下来对新的变量进行修改时,有修改原数据的风险。***
怎么解决呢。很简单,只需要在链式索引后面加上一个.copy() 即可:
sub_df = df.loc[df.a > 0.6].copy()
sub_df['c'] = [1,2]
sub_df
a | b | c | |
---|---|---|---|
0 | 0.688818 | 0.510446 | 1 |
4 | 0.945565 | 0.801788 | 2 |
再试试,可以看到没有再出现问题。
但是我们也注意到,在Warning的提示里,提到:Try using .loc[row_indexer,col_indexer] = value instead。这也是一种解决办法,当你仅仅是想更改原始数据,你可以使用这个操作。
对这个问题的详细原理讲解,请参考: https://www.dataquest.io/blog/settingwithcopywarning/
2.2 DataFrame 里存None:这个坑是真的坑
真的不好写开场白,直接上例子:
v = {'value': 'a'}
d = [{'name': 'class', 'age': 10}, {'name': None, 'age': 11}, {'name': 'def', 'age': 9}]
df = pd.DataFrame(d)
new_1 = df[(df['age'] >= 10) | df['name'].str.contains(v['value'])]
# 颠倒里面条件的顺序
new_2 = df[df['name'].str.contains(v['value']) | (df['age'] >= 10)]
print('-'*40)
print(df)
print('-'*40)
print(new_1)
print('-'*40)
print(new_2)
----------------------------------------
age name
0 10 class
1 11 None
2 9 def
----------------------------------------
age name
0 10 class
1 11 None
----------------------------------------
age name
0 10 class
这。。。。。逻辑操作“或”俩边的条件对调下,结果也能不一样?一脸懵逼。
但是接下来,我进行了简单的探索。
df['age'] >= 10
0 True
1 True
2 False
Name: age, dtype: bool
df['name'].str.contains(v['value'])
0 True
1 None
2 False
Name: name, dtype: object
(df['age'] >= 10) | df['name'].str.contains(v['value'])
0 True
1 True
2 False
dtype: bool
df['name'].str.contains(v['value']) | (df['age'] >= 10)
0 True
1 False
2 False
dtype: bool
这。。。。。。还是一脸懵逼。
百度了一圈,还是没有找到答案。但是找到了解决了办法:
把 None 改为了 ‘’ 就可以了。
v = {'value': 'a'}
d = [{'name': 'class', 'age': 10}, {'name': '', 'age': 11}, {'name': 'def', 'age': 9}]
df = pd.DataFrame(d)
new_1 = df[(df['age'] >= 10) | df['name'].str.contains(v['value'])]
# 颠倒里面条件的顺序
new_2 = df[df['name'].str.contains(v['value']) | (df['age'] >= 10)]
print('-'*40)
print(df)
print('-'*40)
print(new_1)
print('-'*40)
print(new_2)
----------------------------------------
age name
0 10 class
1 11
2 9 def
----------------------------------------
age name
0 10 class
1 11
----------------------------------------
age name
0 10 class
1 11
2.3 这个坑不算坑
这里就举个例子,自己体会:
print type(df['age'])
df['age']
<class 'pandas.core.series.Series'>
0 10
1 11
2 9
Name: age, dtype: int64
print type(df[['age']])
df[['age']]
<class 'pandas.core.frame.DataFrame'>
age | |
---|---|
0 | 10 |
1 | 11 |
2 | 9 |
前面是Series后面是DataFrame,这不知道算不算一个坑
3. DataFrame 拼接里面的坑与技巧
pandas 里多个DataFrame的拼接,主要是append, merge,concat,join四个函数。想详细了解的话看一下官方文档。
这里简单说一下concat和merge.
3.1 concat:坑虽小,须谨慎
解释这个坑,也只有靠例子。直接上代码:
df1 = pd.DataFrame({ 'A': ['A0', 'A1', 'A2'],
'B': ['B0', 'B1', 'B2'],
'C': ['C0', 'C1', 'C2'],
'D': ['D0', 'D1', 'D2']})
df2 = pd.DataFrame({'A': ['A4', 'A5', 'A6'],
'B': ['B4', 'B5', 'B6'],
'C': ['C4', 'C5', 'C6'],
'D': ['D4', 'D5', 'D6']})
df3 = pd.DataFrame({'A': ['A8', 'A9', 'A10'],
'B': ['B8', 'B9', 'B10'],
'C': ['C8', 'C9', 'C10'],
'D': ['D8', 'D9', 'D10']})
frames = [df1, df2, df3]
result = pd.concat(frames)
result
A | B | C | D | |
---|---|---|---|---|
0 | A0 | B0 | C0 | D0 |
1 | A1 | B1 | C1 | D1 |
2 | A2 | B2 | C2 | D2 |
0 | A4 | B4 | C4 | D4 |
1 | A5 | B5 | C5 | D5 |
2 | A6 | B6 | C6 | D6 |
0 | A8 | B8 | C8 | D8 |
1 | A9 | B9 | C9 | D9 |
2 | A10 | B10 | C10 | D10 |
df4 = pd.DataFrame({'val':[0,1,2,3,4,5,6,7,8],'A': ['A0', 'A1', 'A2', 'A3','A4', 'A5', 'A6', 'A7','A8']})
result['val'] = df4['A']
result
A | B | C | D | val | |
---|---|---|---|---|---|
0 | A0 | B0 | C0 | D0 | A0 |
1 | A1 | B1 | C1 | D1 | A1 |
2 | A2 | B2 | C2 | D2 | A2 |
0 | A4 | B4 | C4 | D4 | A0 |
1 | A5 | B5 | C5 | D5 | A1 |
2 | A6 | B6 | C6 | D6 | A2 |
0 | A8 | B8 | C8 | D8 | A0 |
1 | A9 | B9 | C9 | D9 | A1 |
2 | A10 | B10 | C10 | D10 | A2 |
注意看最后一列 ‘val’ ,和我们预期(预期的是从 A0-A8 )的真的不一样。原来赋值操作是按照index赋值的,结果就是这么出乎我们的意料。
其实,concat的时候加上参数 ignore_index=True 就好了:
result = pd.concat(frames, ignore_index=True)
result
A | B | C | D | |
---|---|---|---|---|
0 | A0 | B0 | C0 | D0 |
1 | A1 | B1 | C1 | D1 |
2 | A2 | B2 | C2 | D2 |
3 | A4 | B4 | C4 | D4 |
4 | A5 | B5 | C5 | D5 |
5 | A6 | B6 | C6 | D6 |
6 | A8 | B8 | C8 | D8 |
7 | A9 | B9 | C9 | D9 |
8 | A10 | B10 | C10 | D10 |
result['val'] = df4['A']
result
A | B | C | D | val | |
---|---|---|---|---|---|
0 | A0 | B0 | C0 | D0 | A0 |
1 | A1 | B1 | C1 | D1 | A1 |
2 | A2 | B2 | C2 | D2 | A2 |
3 | A4 | B4 | C4 | D4 | A3 |
4 | A5 | B5 | C5 | D5 | A4 |
5 | A6 | B6 | C6 | D6 | A5 |
6 | A8 | B8 | C8 | D8 | A6 |
7 | A9 | B9 | C9 | D9 | A7 |
8 | A10 | B10 | C10 | D10 | A8 |
3.2 merge:小众的技巧
panda.merge 这个是pandas最常用的操作之一,具体用法可以看官方文档。这里有个小的tricks, 在做一些统计分析的时候很有用。还是具体看例子吧。
left = pd.DataFrame({'key': ['key1', 'key2', 'key3', 'key4'], 'val_l': [1, 2, 3, 4]})
left
key | val_l | |
---|---|---|
0 | key1 | 1 |
1 | key2 | 2 |
2 | key3 | 3 |
3 | key4 | 4 |
right = pd.DataFrame({'key': ['key3', 'key2', 'key1', 'key6'], 'val_r': [3, 2, 1, 6]})
right
key | val_r | |
---|---|---|
0 | key3 | 3 |
1 | key2 | 2 |
2 | key1 | 1 |
3 | key6 | 6 |
df_merge = pd.merge(left, right, on='key', how='left', indicator=True)
df_merge
key | val_l | val_r | _merge | |
---|---|---|---|---|
0 | key1 | 1 | 1.0 | both |
1 | key2 | 2 | 2.0 | both |
2 | key3 | 3 | 3.0 | both |
3 | key4 | 4 | NaN | left_only |
_merge 列不仅可以用来检查是否出现数值错误,还可以进行统计分析,比如:
df_merge['_merge'].value_counts()
both 3
left_only 1
right_only 0
Name: _merge, dtype: int64
4. 一些技巧
技巧总是讲不完,我这里随便再写点。
4.1 pandas 画图
这个举个例子就好了
from matplotlib import pyplot as plt
df_merge.val_l.plot(kind='bar')
plt.show()
data1.plot(kind='scatter', x='v_game_number', y='v_score', alpha=0.1)
plt.show()
plot这个命令底层调用的就是matplotlib。必须事先装好matplotlib,不然会报错。
这里的 2 个例子只是抛砖引玉,真正的功能非常强大,有兴趣的小伙伴可以学习一下
4.2 简单的相关性分析
写到这里,血累了。不想去找数据集,还是用前面自己构造的数据集演示一下这个小技巧:
data_df = pd.DataFrame({'sex': ['male', 'female', 'female', 'female', 'female', 'male', 'female'],
'height': [182, 160, 176, 172, 174, 170, 155],
'weight': [65, 50, 55, 48, 48, 100, 80],
'is_air_hostesses': [1, 1, 1, 1, 1, 0, 0]})
data_df
height | is_air_hostesses | sex | weight | |
---|---|---|---|---|
0 | 182 | 1 | male | 65 |
1 | 160 | 1 | female | 50 |
2 | 176 | 1 | female | 55 |
3 | 172 | 1 | female | 48 |
4 | 174 | 1 | female | 48 |
5 | 170 | 0 | male | 100 |
6 | 155 | 0 | female | 80 |
data_df[['sex', 'is_air_hostesses']].groupby(['sex'], as_index=False).mean().sort_values(by='is_air_hostesses', ascending=False)
sex | is_air_hostesses | |
---|---|---|
0 | female | 0.8 |
1 | male | 0.5 |
*** 可以看到女生做空乘的可能性更大一些 ***
data_df['height_band'] = pd.qcut(data_df['height'], 2)
data_df
height | is_air_hostesses | sex | weight | height_band | |
---|---|---|---|---|---|
0 | 182 | 1 | male | 65 | (172.0, 182.0] |
1 | 160 | 1 | female | 50 | (154.999, 172.0] |
2 | 176 | 1 | female | 55 | (172.0, 182.0] |
3 | 172 | 1 | female | 48 | (154.999, 172.0] |
4 | 174 | 1 | female | 48 | (172.0, 182.0] |
5 | 170 | 0 | male | 100 | (154.999, 172.0] |
6 | 155 | 0 | female | 80 | (154.999, 172.0] |
data_df[['height_band', 'is_air_hostesses']].groupby(['height_band'], as_index=False).mean().sort_values(by='is_air_hostesses', ascending=False)
height_band | is_air_hostesses | |
---|---|---|
1 | (172.0, 182.0] | 1.0 |
0 | (154.999, 172.0] | 0.5 |
这里可以看到身高大于172的是空乘的可能性更大一些
同样的也是为了抛砖引玉,不详细介绍了
5. 结束语
写pandas的这些坑,只是为了更好的提高工作效率,有兴趣的小伙伴可以学一学,相信会很有帮助。