Pandas之旅(四) : 可能是社区内最实用的Pandas技巧

58次阅读

共计 9258 个字符,预计需要花费 24 分钟才能阅读完成。

Pandas 不为人知的七大实用技巧
大家好,我今天勤快地回来了,这一期主要是和大家分享一些 pandas 的实用技巧,会在日常生活中大大提升效率,希望可以帮助到大家, 还是老样子,先给大家奉上这一期的章节目录:

自定义 pandas 选项,设置
实用 pandas 中 testing 模块构建测试数据
巧用 accessor 访问器
合并其他列拼接 DatetimeIndex
使用分类数据(Categorical Data)节省时间和空间
利用 Mapping 巧妙实现映射
压缩 pandas 对象
源码及 GitHub 地址

好啦,话不多说,让我们一个个看吧
1. 自定义 pandas 选项,设置
首先,大家可能不知道,pandas 里面有一个方法 pd.set_option(),利用它我们可以改变一些 pandas 中默认的核心设置,从而适应我们自身的需要,开始前还是老样子,让我们先导入 numpy 和 pandas 包
import numpy as np
import pandas as pd
f’Using {pd.__name__}, Version {pd.__version__}’
‘Using pandas, Version 0.23.0’

现在让我们编写一个 start 方法来实现自定义 pandas 设置
def start():
options = {
‘display’: {
‘max_columns’: None,
‘max_colwidth’: 25,
‘expand_frame_repr’: False, # Don’t wrap to multiple pages
‘max_rows’: 14,
‘max_seq_items’: 50, # Max length of printed sequence
‘precision’: 4,
‘show_dimensions’: False
},
‘mode’: {
‘chained_assignment’: None # Controls SettingWithCopyWarning
}
}

for category, option in options.items():
for op, value in option.items():
pd.set_option(f'{category}.{op}’, value) # Python 3.6+

if __name__ == ‘__main__’:
start()
del start # Clean up namespace in the interpreter
大家可以发现,我们在方法的最后调用了 pandas 的 set_option 方法,直接利用我们自定义的参数替代了原有的 pandas 参数,现在让我们测试一下:
pd.get_option(‘display.max_rows’)
Out:14
可以发现 max_rows 已经被替换成了我们设置的 14,现在用一个真实的例子,我们利用一组公开的鲍鱼各项指标的数据来实验,数据源来自机器学习平台的公开数据
url = (‘https://archive.ics.uci.edu/ml/’
‘machine-learning-databases/abalone/abalone.data’)
cols = [‘sex’, ‘length’, ‘diam’, ‘height’, ‘weight’, ‘rings’]
abalone = pd.read_csv(url, usecols=[0, 1, 2, 3, 4, 8], names=cols)
abalone

sex
length
diam
height
weight
rings

0
M
0.455
0.365
0.095
0.5140
15

1
M
0.350
0.265
0.090
0.2255
7

2
F
0.530
0.420
0.135
0.6770
9

3
M
0.440
0.365
0.125
0.5160
10

4
I
0.330
0.255
0.080
0.2050
7

5
I
0.425
0.300
0.095
0.3515
8

6
F
0.530
0.415
0.150
0.7775
20







4170
M
0.550
0.430
0.130
0.8395
10

4171
M
0.560
0.430
0.155
0.8675
8

4172
F
0.565
0.450
0.165
0.8870
11

4173
M
0.590
0.440
0.135
0.9660
10

4174
M
0.600
0.475
0.205
1.1760
9

4175
F
0.625
0.485
0.150
1.0945
10

4176
M
0.710
0.555
0.195
1.9485
12

我们可以看到,数据截断为 14 行,保留了小数点后 4 位小数作为精度,和我们刚刚设置的 precision= 4 是一样的
2. 实用 pandas 中 testing 模块构建测试数据
通过 pandas.util.testing 提供的方法,我们可以很容易的通过几行代码就构建出一个简单的测试数据类型,比如我们现在构建一个 DataTime 类型的数据,时间间隔为月:
import pandas.util.testing as tm
tm.N, tm.K = 15, 3 # 规定行和列

import numpy as np
np.random.seed(444)

tm.makeTimeDataFrame(freq=’M’).head() # 设置时间间隔为月
# tm.makeTimeDataFrame(freq=’D’).head() 设置时间间隔为天

A
B
C

2000-01-31
0.3574
-0.8804
0.2669

2000-02-29
0.3775
0.1526
-0.4803

2000-03-31
1.3823
0.2503
0.3008

2000-04-30
1.1755
0.0785
-0.1791

2000-05-31
-0.9393
-0.9039
1.1837

瞎生成一组乱七八糟的数据:
tm.makeDataFrame().head()

A
B
C

nTLGGTiRHF
-0.6228
0.6459
0.1251

WPBRn9jtsR
-0.3187
-0.8091
1.1501

7B3wWfvuDA
-1.9872
-1.0795
0.2987

yJ0BTjehH1
0.8802
0.7403
-1.2154

0luaYUYvy1
-0.9320
1.2912
-0.2907

关于可以随机生成的数据类型, 一共大概有 30 多种,大家如果感兴趣可以多试试:
[i for i in dir(tm) if i.startswith(‘make’)]
[‘makeBoolIndex’,
‘makeCategoricalIndex’,
‘makeCustomDataframe’,
‘makeCustomIndex’,
‘makeDataFrame’,
‘makeDateIndex’,
‘makeFloatIndex’,
‘makeFloatSeries’,
‘makeIntIndex’,
‘makeIntervalIndex’,
‘makeMissingCustomDataframe’,
‘makeMissingDataframe’,
‘makeMixedDataFrame’,
‘makeMultiIndex’,
‘makeObjectSeries’,
‘makePanel’,
‘makePeriodFrame’,
‘makePeriodIndex’,
‘makePeriodPanel’,
‘makePeriodSeries’,
‘makeRangeIndex’,
‘makeStringIndex’,
‘makeStringSeries’,
‘makeTimeDataFrame’,
‘makeTimeSeries’,
‘makeTimedeltaIndex’,
‘makeUIntIndex’,
‘makeUnicodeIndex’]

这样我们如果有测试的需求,会很容易地构建相对应的假数据来测试。
3. 巧用 accessor 访问器
accessor(访问器)具体就是类似 getter 和 setter,当然,Python 里面不提倡存在 setter 和 getter 方法,但是这样可以便于大家理解,pandas Series 类型有 3 类 accessor:
pd.Series._accessors

Out:{‘cat’, ‘dt’, ‘str’}

.cat 用于分类数据,
.str 用于字符串(对象)数据,
.dt 用于类似日期时间的数据。

让我们从.str 开始看:假设现在我们有一些原始的城市 / 州 / 邮编数据作为 Dataframe 的一个字段:
addr = pd.Series([
‘Washington, D.C. 20003’,
‘Brooklyn, NY 11211-1755’,
‘Omaha, NE 68154’,
‘Pittsburgh, PA 15211′
])
addr.str.upper() # 因为字符串方法是矢量化的,这意味着它们在没有显式 for 循环的情况下对整个数组进行操作
0 WASHINGTON, D.C. 20003
1 BROOKLYN, NY 11211-1755
2 OMAHA, NE 68154
3 PITTSBURGH, PA 15211
dtype: object

addr.str.count(r’\d’) # 查看邮编有几位
0 5
1 9
2 5
3 5
dtype: int64

如果我们想把每一行分成城市,州,邮编分开,可以用正则;
regex = (r'(?P<city>[A-Za-z]+), ‘ # One or more letters
r'(?P<state>[A-Z]{2}) ‘ # 2 capital letters
r'(?P<zip>\d{5}(?:-\d{4})?)’) # Optional 4-digit extension

addr.str.replace(‘.’, ”).str.extract(regex)

city
state
zip

0
Washington
DC
20003

1
Brooklyn
NY
11211-1755

2
Omaha
NE
68154

3
Pittsburgh
PA
15211

第二个访问器.dt 用于类似日期时间的数据。它其实属于 Pandas 的 DatetimeIndex,如果在 Series 上调用,它首先转换为 DatetimeIndex
daterng = pd.Series(pd.date_range(‘2018′, periods=9, freq=’Q’)) # 时间间隔为季度
daterng
0 2018-03-31
1 2018-06-30
2 2018-09-30
3 2018-12-31
4 2019-03-31
5 2019-06-30
6 2019-09-30
7 2019-12-31
8 2020-03-31
dtype: datetime64[ns]

daterng.dt.day_name()
0 Saturday
1 Saturday
2 Sunday
3 Monday
4 Sunday
5 Sunday
6 Monday
7 Tuesday
8 Tuesday
dtype: object

daterng[daterng.dt.quarter > 2] # 查看 2019 年第 3 季度和第 4 季度
2 2018-09-30
3 2018-12-31
6 2019-09-30
7 2019-12-31
dtype: datetime64[ns]

daterng[daterng.dt.is_year_end] #查看年末的一天
3 2018-12-31
7 2019-12-31
dtype: datetime64[ns]

最后有关.cat 访问器我们会在第 5 个技巧中提到
4. 合并其他列拼接 DatetimeIndex
现在先让我们构建一个包含时间类型数据的 Dataframe:
from itertools import product
datecols = [‘year’, ‘month’, ‘day’]

df = pd.DataFrame(list(product([2017, 2016], [1, 2], [1, 2, 3])),
columns=datecols)
df[‘data’] = np.random.randn(len(df))
df

year
month
day
data

0
2017
1
1
-0.0767

1
2017
1
2
-1.2798

2
2017
1
3
0.4032

3
2017
2
1
1.2377

4
2017
2
2
-0.2060

5
2017
2
3
0.6187

6
2016
1
1
2.3786

7
2016
1
2
-0.4730

8
2016
1
3
-2.1505

9
2016
2
1
-0.6340

10
2016
2
2
0.7964

11
2016
2
3
0.0005

我们可以发现 year,month,day 是分开的三列,我们如果想要把它们合并为完整的时间并作为 df 的索引,可以这么做:
df.index = pd.to_datetime(df[datecols])
df.head()

year
month
day
data

2017-01-01
2017
1
1
-0.0767

2017-01-02
2017
1
2
-1.2798

2017-01-03
2017
1
3
0.4032

2017-02-01
2017
2
1
1.2377

2017-02-02
2017
2
2
-0.2060

我们可以扔掉没用的列并把这个 df 压缩为 Series:
df = df.drop(datecols, axis=1).squeeze()
df.head()
2017-01-01 -0.0767
2017-01-02 -1.2798
2017-01-03 0.4032
2017-02-01 1.2377
2017-02-02 -0.2060
Name: data, dtype: float64

type(df)
pandas.core.series.Series

df.index.dtype_str
‘datetime64[ns]’

5. 使用分类数据(Categorical Data)节省时间和空间
刚刚我们在第 3 个技巧的时候提到了访问器,现在让我们来看最后一个.cat
pandas 中 Categorical 这个数据类型非常强大,通过类型转换可以让我们节省变量在内存占用的空间,提高运算速度,不过有关具体的 pandas 加速实战,我会在下一期说,现在让我们来看一个小栗子:
colors = pd.Series([
‘periwinkle’,
‘mint green’,
‘burnt orange’,
‘periwinkle’,
‘burnt orange’,
‘rose’,
‘rose’,
‘mint green’,
‘rose’,
‘navy’
])

import sys
colors.apply(sys.getsizeof)

0 59
1 59
2 61
3 59
4 61
5 53
6 53
7 59
8 53
9 53
dtype: int64

我们首先创建了一个 Series,填充了各种颜色,接着查看了每个地址对应的颜色所占内存的大小
注意这里我们使用 sys.getsizeof() 来获取占内存大小,但是实际上空格也是占内存的,sys.getsizeof(”) 返回的是 49bytes
接下来我们想把每种颜色用占内存更少的数字来表示(机器学习种非常常见),这样可以减少占用的内存,首先让我们创建一个 mapper 字典,给每一种颜色指定一个数字
mapper = {v: k for k, v in enumerate(colors.unique())}
mapper
{‘periwinkle’: 0, ‘mint green’: 1, ‘burnt orange’: 2, ‘rose’: 3, ‘navy’: 4}

接着我们把刚才的 colors 数组转化为 int 类型:
# 也可以通过 pd.factorize(colors)[0] 实现
as_int = colors.map(mapper)
as_int
0 0
1 1
2 2
3 0
4 2
5 3
6 3
7 1
8 3
9 4
dtype: int64

再让我们看一下占用的内存:
as_int.apply(sys.getsizeof)
0 24
1 28
2 28
3 24
4 28
5 28
6 28
7 28
8 28
9 28
dtype: int64

现在可以观察到我们的内存占用的空间几乎是之前的一半,其实,刚刚我们做的正是模拟 Categorical Data 的转化原理。现在让我们直接调用一下:
colors.memory_usage(index=False, deep=True)

Out:650
colors.astype(‘category’).memory_usage(index=False, deep=True)

Out: 495
大家可能感觉节省的空间并不是非常大对不对?因为目前我们这个数据根本不是真实场景,我们仅仅把数据容量增加 10 倍,现在再让我们看看效果:
manycolors = colors.repeat(10)
len(manycolors) / manycolors.nunique() # Much greater than 2.0x

Out:20.0
f”Not using category : {manycolors.memory_usage(index=False, deep=True)}”
‘Not using category : 6500’

f”Using category : {manycolors.astype(‘category’).memory_usage(index=False, deep=True)}”
‘Using category : 585’

这回内存的占用量差距明显就出来了,现在让我们用.cat 来简化一下刚刚的工作:
new_colors = colors.astype(‘category’)
new_colors
0 periwinkle
1 mint green
2 burnt orange
3 periwinkle
4 burnt orange
5 rose
6 rose
7 mint green
8 rose
9 navy
dtype: category
Categories (5, object): [burnt orange, mint green, navy, periwinkle, rose]

new_colors.cat.categories # 可以使用.cat.categories 查看代表的颜色
Index([‘burnt orange’, ‘mint green’, ‘navy’, ‘periwinkle’, ‘rose’], dtype=’object’)

现在让我们查看把颜色代表的数字:
new_colors.cat.codes
0 3
1 1
2 0
3 3
4 0
5 4
6 4
7 1
8 4
9 2
dtype: int8

我们如果不满意顺序也可以从新排序:
new_colors.cat.reorder_categories(mapper).cat.codes
0 0
1 1
2 2
3 0
4 2
5 3
6 3
7 1
8 3
9 4
dtype: int8

有关 cat 其他的方法,我们还是可以通过遍历 dir 来查看:
[i for i in dir(new_colors.cat) if not i.startswith(‘_’)]
[‘add_categories’,
‘as_ordered’,
‘as_unordered’,
‘categories’,
‘codes’,
‘ordered’,
‘remove_categories’,
‘remove_unused_categories’,
‘rename_categories’,
‘reorder_categories’,
‘set_categories’]

Categorical 数据通常不太灵活,比如我们不能直接在 new_colors 上新增一个新的颜色,要首先通过.add_categories 来添加
ccolors.iloc[5] = ‘a new color’
—————————————————————————

NameError Traceback (most recent call last)

<ipython-input-36-1766a795336d> in <module>()
—-> 1 ccolors.iloc[5] = ‘a new color’

NameError: name ‘ccolors’ is not defined

new_colors = new_colors.cat.add_categories([‘a new color’])
new_colors.iloc[5] = ‘a new color’ # 不会报错
new_colors.values # 成功添加
6. 利用 Mapping 巧妙实现映射
假设现在我们有存贮国家的一组数据,和一组用来映射国家所对应的大洲的数据:
countries = pd.Series([
‘United States’,
‘Canada’,
‘Mexico’,
‘Belgium’,
‘United Kingdom’,
‘Thailand’
])

groups = {
‘North America’: (‘United States’, ‘Canada’, ‘Mexico’, ‘Greenland’),
‘Europe’: (‘France’, ‘Germany’, ‘United Kingdom’, ‘Belgium’)
}
我们可以通过下面的方法来实现简单的映射:
from typing import Any

def membership_map(s: pd.Series, groups: dict,
fillvalue: Any=-1) -> pd.Series:
# Reverse & expand the dictionary key-value pairs
groups = {x: k for k, v in groups.items() for x in v}
return s.map(groups).fillna(fillvalue)
membership_map(countries, groups, fillvalue=’other’)
很简单对不对,现在让我们看一下最关键的一行代码,groups = {x: k for k, v in groups.items() for x in v},这个是我之前提到过的字典推导式:
test = dict(enumerate((‘ab’, ‘cd’, ‘xyz’)))
{x: k for k, v in test.items() for x in v}
7. 压缩 pandas 对象
如果你的 pandas 版本大于 0.21.0,那么都可以直接把 pandas 用压缩形式写入,常见的类型有 gzip, bz2, zip,这里我们直接用刚才鲍鱼的数据集:
abalone.to_json(‘df.json.gz’, orient=’records’,lines=True, compression=’gzip’) # 压缩为 gz 类型
abalone.to_json(‘df.json’, orient=’records’, lines=True) #压缩为 json
import os.path
os.path.getsize(‘df.json’) / os.path.getsize(‘df.json.gz’) #压缩大小差了 10 倍,还是 gz 更厉害
8. 源码及 GitHub 地址
这一期为大家总结了很多 pandas 实用的小技巧,希望大家喜欢
我把这一期的 ipynb 文件和 py 文件放到了 Github 上,大家如果想要下载可以点击下面的链接:
Github 仓库地址:https://github.com/yaozeliang/pandas_share

这一期就到这里啦,希望大家能够继续支持我,完结,撒花

正文完
 0