Pandas 那些年踩过的坑

在进行数据处理和分析时,pandas就像一条高速公路,能够帮助我们快速的进行各种数据处理和分析操作。但是高速公路也可能有各种坑,一不小心就翻车。

在平时的工作中,也积累了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 usagefloatintunitcategoryboolobject
1 bytesint8unit8
2 bytesfloat16int16unit16
4 bytesfloat32int32unit32
8 bytesfloat64int64unit64
variableSlytherincategoryboolobject

随便找了个数据,实际操作看一下:

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)
datenumber_of_gameday_of_weekv_namev_leaguev_game_numberh_nameh_leagueh_game_numberv_score...h_player_7_nameh_player_7_def_posh_player_8_idh_player_8_nameh_player_8_def_posh_player_9_idh_player_9_nameh_player_9_def_posadditional_infoacquisition_info
0187105040ThuCL1na1FW1na10...Ed Mincher7.0mcdej101James McDermott8.0kellb105Bill Kelly9.0NaNY
1187105050FriBS1na1WS3na120...Asa Brainard1.0burrh101Henry Burroughs9.0berth101Henry Berthrong8.0HTBFY
2187105060SatCL1na2RC1na112...Pony Sager6.0birdg101George Bird7.0stirg101Gat Stires9.0NaNY
3187105080MonCL1na3CH1na112...Ed Duffy6.0pinke101Ed Pinkham5.0zettg101George Zettlein1.0NaNY
4187105090TueBS1na2TROna19...Steve Bellan5.0pikel101Lip Pike3.0cravb101Bill Craver6.0HTBFY

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
ab
00.9772920.343893
10.4780500.781146
df.to_csv('test_df.csv')
df1 = pd.read_csv('test_df.csv')
df1
Unnamed: 0ab
000.9772920.343893
110.4780500.781146

通过以上例子,可以看到,一存一读间,却多了一列。
这种情况极易给后面的操作埋下一个大坑,而且还蒙在鼓里找不出原因。

怎么解决呢,只需要在存的时候,指定 index 参数为 False 即可。再来试一下:

df.to_csv('test_df.csv', index=False)
df = pd.read_csv('test_df.csv')
df
ab
00.9772920.343893
10.4780500.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
heightis_air_hostessessexweight
01821male65
11601female50
21761female55
31721female48
41741female48
51700male100
61550female80
sex_one_hot_df = pd.get_dummies(data_df['sex'])
sex_one_hot_df
femalemale
001
110
210
310
410
501
610
sex_one_hot_df.dtypes
female    uint8
male      uint8
dtype: object
-sex_one_hot_df
femalemale
00255
12550
22550
32550
42550
50255
62550

这真的是一个神坑,如果特征比较多的话,根本发现不了。如果没有发现,后续如果做其他操的时候,就会出错。这个坑藏得深啊。

正确的做法是转换一下数据类型:

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
ab
00.6888180.510446
40.9455650.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
abc
00.6888180.5104461
40.9455650.8017882

再试试,可以看到没有再出现问题。

但是我们也注意到,在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
010
111
29

前面是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
ABCD
0A0B0C0D0
1A1B1C1D1
2A2B2C2D2
0A4B4C4D4
1A5B5C5D5
2A6B6C6D6
0A8B8C8D8
1A9B9C9D9
2A10B10C10D10
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
ABCDval
0A0B0C0D0A0
1A1B1C1D1A1
2A2B2C2D2A2
0A4B4C4D4A0
1A5B5C5D5A1
2A6B6C6D6A2
0A8B8C8D8A0
1A9B9C9D9A1
2A10B10C10D10A2

注意看最后一列 ‘val’ ,和我们预期(预期的是从 A0-A8 )的真的不一样。原来赋值操作是按照index赋值的,结果就是这么出乎我们的意料。

其实,concat的时候加上参数 ignore_index=True 就好了:

result = pd.concat(frames, ignore_index=True)
result
ABCD
0A0B0C0D0
1A1B1C1D1
2A2B2C2D2
3A4B4C4D4
4A5B5C5D5
5A6B6C6D6
6A8B8C8D8
7A9B9C9D9
8A10B10C10D10
result['val'] = df4['A']
result
ABCDval
0A0B0C0D0A0
1A1B1C1D1A1
2A2B2C2D2A2
3A4B4C4D4A3
4A5B5C5D5A4
5A6B6C6D6A5
6A8B8C8D8A6
7A9B9C9D9A7
8A10B10C10D10A8

3.2 merge:小众的技巧

panda.merge 这个是pandas最常用的操作之一,具体用法可以看官方文档。这里有个小的tricks, 在做一些统计分析的时候很有用。还是具体看例子吧。

left = pd.DataFrame({'key': ['key1', 'key2', 'key3', 'key4'], 'val_l': [1, 2, 3, 4]})
left
keyval_l
0key11
1key22
2key33
3key44
right = pd.DataFrame({'key': ['key3', 'key2', 'key1', 'key6'], 'val_r': [3, 2, 1, 6]})
right
keyval_r
0key33
1key22
2key11
3key66
df_merge = pd.merge(left, right, on='key', how='left', indicator=True)
df_merge
keyval_lval_r_merge
0key111.0both
1key222.0both
2key333.0both
3key44NaNleft_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
heightis_air_hostessessexweight
01821male65
11601female50
21761female55
31721female48
41741female48
51700male100
61550female80
data_df[['sex', 'is_air_hostesses']].groupby(['sex'], as_index=False).mean().sort_values(by='is_air_hostesses', ascending=False)
sexis_air_hostesses
0female0.8
1male0.5

可以看到女生做空乘的可能性更大一些

data_df['height_band'] = pd.qcut(data_df['height'], 2)
data_df
heightis_air_hostessessexweightheight_band
01821male65(172.0, 182.0]
11601female50(154.999, 172.0]
21761female55(172.0, 182.0]
31721female48(154.999, 172.0]
41741female48(172.0, 182.0]
51700male100(154.999, 172.0]
61550female80(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_bandis_air_hostesses
1(172.0, 182.0]1.0
0(154.999, 172.0]0.5

这里可以看到身高大于172的是空乘的可能性更大一些

同样的也是为了抛砖引玉,不详细介绍了

5. 结束语

写pandas的这些坑,只是为了更好的提高工作效率,有兴趣的小伙伴可以学一学,相信会很有帮助。

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值