Pandas不为人知的七大实用技巧大家好,我今天勤快地回来了,这一期主要是和大家分享一些pandas的实用技巧,会在日常生活中大大提升效率,希望可以帮助到大家,还是老样子,先给大家奉上这一期的章节目录:自定义pandas选项,设置实用pandas中testing模块构建测试数据巧用accessor访问器合并其他列拼接DatetimeIndex使用分类数据(Categorical Data)节省时间和空间利用Mapping巧妙实现映射压缩pandas对象源码及GitHub地址好啦,话不多说,让我们一个个看吧1. 自定义pandas选项,设置首先,大家可能不知道,pandas里面有一个方法pd.set_option(),利用它我们可以改变一些pandas中默认的核心设置,从而适应我们自身的需要,开始前还是老样子,让我们先导入numpy和pandas包import numpy as npimport pandas as pdf’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 tmtm.N, tm.K = 15, 3 # 规定行和列import numpy as npnp.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.accessorsOut:{‘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. 200031 BROOKLYN, NY 11211-17552 OMAHA, NE 681543 PITTSBURGH, PA 15211dtype: objectaddr.str.count(r’\d’) # 查看邮编有几位0 51 92 53 5dtype: 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 extensionaddr.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上调用,它首先转换为DatetimeIndexdaterng = pd.Series(pd.date_range(‘2018’, periods=9, freq=‘Q’)) # 时间间隔为季度daterng0 2018-03-311 2018-06-302 2018-09-303 2018-12-314 2019-03-315 2019-06-306 2019-09-307 2019-12-318 2020-03-31dtype: datetime64[ns]daterng.dt.day_name()0 Saturday1 Saturday2 Sunday3 Monday4 Sunday5 Sunday6 Monday7 Tuesday8 Tuesdaydtype: objectdaterng[daterng.dt.quarter > 2] # 查看2019年第3季度和第4季度2 2018-09-303 2018-12-316 2019-09-307 2019-12-31dtype: datetime64[ns]daterng[daterng.dt.is_year_end] #查看年末的一天3 2018-12-317 2019-12-31dtype: datetime64[ns]最后有关.cat访问器我们会在第5个技巧中提到4. 合并其他列拼接DatetimeIndex现在先让我们构建一个包含时间类型数据的Dataframe:from itertools import productdatecols = [‘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.07672017-01-02 -1.27982017-01-03 0.40322017-02-01 1.23772017-02-02 -0.2060Name: data, dtype: float64type(df)pandas.core.series.Seriesdf.index.dtype_str’datetime64[ns]‘5. 使用分类数据(Categorical Data)节省时间和空间刚刚我们在第3个技巧的时候提到了访问器,现在让我们来看最后一个.catpandas中Categorical这个数据类型非常强大,通过类型转换可以让我们节省变量在内存占用的空间,提高运算速度,不过有关具体的pandas加速实战,我会在下一期说,现在让我们来看一个小栗子:colors = pd.Series([ ‘periwinkle’, ‘mint green’, ‘burnt orange’, ‘periwinkle’, ‘burnt orange’, ‘rose’, ‘rose’, ‘mint green’, ‘rose’, ’navy’])import syscolors.apply(sys.getsizeof)0 591 592 613 594 615 536 537 598 539 53dtype: 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_int0 01 12 23 04 25 36 37 18 39 4dtype: int64再让我们看一下占用的内存:as_int.apply(sys.getsizeof)0 241 282 283 244 285 286 287 288 289 28dtype: int64现在可以观察到我们的内存占用的空间几乎是之前的一半,其实,刚刚我们做的正是模拟Categorical Data的转化原理。现在让我们直接调用一下:colors.memory_usage(index=False, deep=True)Out:650colors.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.0f"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_colors0 periwinkle1 mint green2 burnt orange3 periwinkle4 burnt orange5 rose6 rose7 mint green8 rose9 navydtype: categoryCategories (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.codes0 31 12 03 34 05 46 47 18 49 2dtype: int8我们如果不满意顺序也可以从新排序:new_colors.cat.reorder_categories(mapper).cat.codes0 01 12 23 04 25 36 37 18 39 4dtype: 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 definednew_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 Anydef 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) #压缩为jsonimport os.pathos.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这一期就到这里啦,希望大家能够继续支持我,完结,撒花