Pandas之旅(四) : 多是社區內最實用的Pandas技巧

Pandas鮮爲人知的七大實用技巧

你們好,我今天勤快地回來了,這一期主要是和你們分享一些pandas的實用技巧,會在平常生活中大大提高效率,但願能夠幫助到你們,仍是老樣子,先給你們奉上這一期的章節目錄:python

  1. 自定義pandas選項,設置
  2. 實用pandas中testing模塊構建測試數據
  3. 巧用accessor訪問器
  4. 合併其餘列拼接DatetimeIndex
  5. 使用分類數據(Categorical Data)節省時間和空間
  6. 利用Mapping巧妙實現映射
  7. 壓縮pandas對象
  8. 源碼及GitHub地址

好啦,話很少說,讓咱們一個個看吧git

1. 自定義pandas選項,設置

首先,你們可能不知道,pandas裏面有一個方法pd.set_option(),利用它咱們能夠改變一些pandas中默認的核心設置,
從而適應咱們自身的須要,開始前仍是老樣子,讓咱們先導入numpy和pandas包github

import numpy as np
import pandas as pd
f'Using {pd.__name__}, Version {pd.__version__}'
'Using pandas, Version 0.23.0'

如今讓咱們編寫一個start方法來實現自定義pandas設置json

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參數,如今讓咱們測試一下:api

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是同樣的app

2. 實用pandas中testing模塊構建測試數據

經過pandas.util.testing提供的方法,咱們能夠很容易的經過幾行代碼就構建出一個簡單的測試數據類型,好比咱們如今構建一個DataTime類型的數據,
時間間隔爲月:dom

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上,你們若是想要下載能夠點擊下面的連接:

這一期就到這裏啦,但願你們可以繼續支持我,完結,撒花

相關文章
相關標籤/搜索