使用分类数据(Categorical Data)节省时间和空间
1. 自定义pandas选项,设置
首先,大家可能不知道,pandas里面有一个方法pd.set_option(),利用它我们可以改变一些pandas中默认的核心设置,
从而适应我们自身的需要,开始前还是老样子,让我们先导入numpy和pandas包
Copy import numpy as np
import pandas as pd
f 'Using { pd . __name__} , Version { pd . __version__} '
现在让我们编写一个start方法来实现自定义pandas设置
Copy def start ():
options = {
'display' : {
'max_columns' : None ,
'max_colwidth' : 25 ,
'expand_frame_repr' : False ,
'max_rows' : 14 ,
'max_seq_items' : 50 ,
'precision' : 4 ,
'show_dimensions' : False
},
'mode' : {
'chained_assignment' : None
}
}
for category , option in options . items ():
for op , value in option . items ():
pd . set_option ( f ' { category } . { op } ' , value)
if __name__ == '__main__' :
start ()
del start
大家可以发现,我们在方法的最后调用了pandas的set_option方法,直接利用我们自定义的参数替代了原有的pandas参数,现在让我们测试一下:
Copy pd . get_option ( 'display.max_rows' )
可以发现max_rows 已经被替换成了我们设置的14,现在用一个真实的例子,我们利用一组公开的鲍鱼各项指标的数据来实验,数据源来自机器学习平台的公开数据
Copy 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
我们可以看到,数据截断为14行,保留了小数点后4位小数作为精度,和我们刚刚设置的precision=4是一样的
2. testing模块构建测试数据
通过pandas.util.testing提供的方法,我们可以很容易的通过几行代码就构建出一个简单的测试数据类型,比如我们现在构建一个DataTime类型的数据,时间间隔为月:
Copy 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 ()
瞎生成一组乱七八糟的数据:
Copy tm.makeDataFrame().head()
关于可以随机生成的数据类型, 一共大概有30多种,大家如果感兴趣可以多试试:
Copy [i for i in dir (tm) if i . startswith ( 'make' ) ]
Copy [ '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:
让我们从.str开始看:假设现在我们有一些原始的城市/州/ 邮编数据作为Dataframe的一个字段:
Copy addr = pd . Series ([
'Washington, D.C. 20003' ,
'Brooklyn, NY 11211-1755' ,
'Omaha, NE 68154' ,
'Pittsburgh, PA 15211'
])
Copy 0 WASHINGTON , D . C . 20003
1 BROOKLYN , NY 11211 - 1755
2 OMAHA , NE 68154
3 PITTSBURGH , PA 15211
dtype : object
Copy addr . str . count ( r ' \d ' )
Copy 0 5
1 9
2 5
3 5
dtype : int64
如果我们想把每一行分成城市,州,邮编分开,可以用正则;
Copy regex = ( r ' ( ?P<city>[A-Za-z ] + ) , '
r ' ( ?P<state>[A-Z] {2} ) '
r ' ( ?P<zip> \d {5} (?: - \d {4} ) ? ) ' )
addr . str . replace ( '.' , '' ). str . extract (regex)
第二个访问器.dt用于类似日期时间的数据。它其实属于Pandas的DatetimeIndex,如果在Series上调用,它首先转换为DatetimeIndex
Copy daterng = pd . Series (pd. date_range ( '2018' , periods = 9 , freq = 'Q' ))
daterng
Copy 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 ]
Copy daterng . dt . day_name ()
Copy 0 Saturday
1 Saturday
2 Sunday
3 Monday
4 Sunday
5 Sunday
6 Monday
7 Tuesday
8 Tuesday
dtype : object
Copy daterng [ daterng . dt . quarter > 2 ]
Copy 2 2018 - 09 - 30
3 2018 - 12 - 31
6 2019 - 09 - 30
7 2019 - 12 - 31
dtype : datetime64 [ ns ]
Copy daterng [ daterng . dt . is_year_end ]
Copy 3 2018 - 12 - 31
7 2019 - 12 - 31
dtype : datetime64 [ ns ]
最后有关.cat访问器我们会在第5个技巧中提到
4. 合并其他列拼接DatetimeIndex
现在先让我们构建一个包含时间类型数据的Dataframe:
Copy 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是分开的三列,我们如果想要把它们合并为完整的时间并作为df的索引,可以这么做:
Copy df . index = pd . to_datetime (df[datecols])
df . head ()
我们可以扔掉没用的列并把这个df压缩为Series:
Copy df = df . drop (datecols, axis = 1 ). squeeze ()
df . head ()
Copy 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
Copy pandas.core.series.Series
5. 使用分类数据(Categorical Data)节省时间和空间
刚刚我们在第3个技巧的时候提到了访问器,现在让我们来看最后一个.cat
pandas中Categorical这个数据类型非常强大,通过类型转换可以让我们节省变量在内存占用的空间,提高运算速度,不过有关具体的pandas加速实战,我会在
下一期说,现在让我们来看一个小栗子:
Copy colors = pd . Series ([
'periwinkle' ,
'mint green' ,
'burnt orange' ,
'periwinkle' ,
'burnt orange' ,
'rose' ,
'rose' ,
'mint green' ,
'rose' ,
'navy'
])
import sys
colors . apply (sys.getsizeof)
Copy 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字典,给每一种颜色指定
一个数字
Copy mapper = { v : k for k , v in enumerate (colors. unique ())}
mapper
Copy { 'periwinkle' : 0 , 'mint green' : 1 , 'burnt orange' : 2 , 'rose' : 3 , 'navy' : 4 }
接着我们把刚才的colors数组转化为int类型:
Copy as_int = colors . map (mapper)
as_int
Copy 0 0
1 1
2 2
3 0
4 2
5 3
6 3
7 1
8 3
9 4
dtype : int64
再让我们看一下占用的内存:
Copy as_int.apply(sys.getsizeof)
Copy 0 24
1 28
2 28
3 24
4 28
5 28
6 28
7 28
8 28
9 28
dtype : int64
现在可以观察到我们的内存占用的空间几乎是之前的一半,其实,刚刚我们做的正是模拟Categorical Data的转化原理。现在让我们直接调用一下:
Copy colors . memory_usage (index = False , deep = True )
Out : 650
Copy colors . astype ( 'category' ). memory_usage (index = False , deep = True )
Out : 495
大家可能感觉节省的空间并不是非常大对不对? 因为目前我们这个数据根本不是真实场景,我们仅仅把数据容量增加10倍,现在再让我们看看效果:
Copy manycolors = colors . repeat ( 10 )
len (manycolors) / manycolors . nunique ()
Out : 20.0
Copy f "Not using category : { manycolors . memory_usage (index = False , deep = True ) } "
'Not using category : 6500'
Copy f "Using category : { manycolors . astype ( 'category' ). memory_usage (index = False , deep = True ) } "
'Using category : 585'
这回内存的占用量差距明显就出来了,现在让我们用.cat来简化一下刚刚的工作:
Copy new_colors = colors . astype ( 'category' )
new_colors
Copy 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]
Copy new_colors . cat . categories
Copy Index(['burnt orange', 'mint green', 'navy', 'periwinkle', 'rose'], dtype='object')
现在让我们查看把颜色代表的数字:
Copy 0 3
1 1
2 0
3 3
4 0
5 4
6 4
7 1
8 4
9 2
dtype: int8
我们如果不满意顺序也可以从新排序:
Copy new_colors.cat.reorder_categories(mapper).cat.codes
Copy 0 0
1 1
2 2
3 0
4 2
5 3
6 3
7 1
8 3
9 4
dtype: int8
有关cat其他的方法,我们还是可以通过遍历dir来查看:
Copy [i for i in dir(new_colors.cat) if not i.startswith('_')]
Copy ['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来添加
Copy ccolors.iloc[5] = 'a new color'
Copy ---------------------------------------------------------------------------
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
Copy new_colors = new_colors.cat.add_categories(['a new color'])
Copy new_colors.iloc[5] = 'a new color'
6. 利用Mapping巧妙实现映射
假设现在我们有存贮国家的一组数据,和一组用来映射国家所对应的大洲的数据:
Copy 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')
}
我们可以通过下面的方法来实现简单的映射:
Copy from typing import Any
def membership_map(s: pd.Series, groups: dict,
fillvalue: Any=-1) -> pd.Series:
groups = {x: k for k, v in groups.items() for x in v}
return s.map(groups).fillna(fillvalue)
Copy membership_map(countries, groups, fillvalue='other')
很简单对不对,现在让我们看一下最关键的一行代码,groups = {x: k for k, v in groups.items() for x in v},这个是我之前提到过的字典推导式:
Copy 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,这里我们直接用刚才鲍鱼的数据集
Copy abalone.to_json('df.json.gz', orient='records',lines=True, compression='gzip')
abalone.to_json('df.json', orient='records', lines=True)
Copy import os.path
os.path.getsize('df.json') / os.path.getsize('df.json.gz')
8. 其他
Copy def create_id(*args):
return ''.join([i for i in map(str,args) if i!='Nan'])
df['id'] = np.vectorize(create_id)(df[1],df[2],df[3])
# Concatenate all values to one column
df['x'] = df.astype(str).values.sum(axis=1)
#A clean way to initialize data frame with a list of namedtuple
Point = namedtuple('Point', ['x', 'y'])
points = [Point(1, 2), Point(3, 4)]
pd.DataFrame(points, columns=Point._fields)
Out[13]:
x y
0 1 2
1 3 4
9. 总结