作者:東哥
pandas有些功能很逆天,但卻鮮為人知,本篇給大家盤點一下。
一、ACCESSOR
pandas有一種功能非常強大的方法,它就是accessor,可以將它理解為一種屬性介面,通過它可以獲得額外的方法。其實這樣說還是很籠統,下面我們通過程式碼和例項來理解一下。
>>> pd.Series._accessors
{'cat', 'str', 'dt'}
對於Series資料結構使用_accessors方法,可以得到了3個物件:cat,str,dt。
-
.cat
:用於分類資料(Categorical data) -
.str
:用於字元資料(String Object data) -
.dt
:用於時間資料(datetime-like data)
下面我們依次看一下這三個物件是如何使用的。
str物件的使用
Series資料型別:str字串
定義一個Series序列
>>> addr = pd.Series([
... 'Washington, D.C. 20003',
... 'Brooklyn, NY 11211-1755',
... 'Omaha, NE 68154',
... 'Pittsburgh, PA 15211'
... ])
>>> addr.str.upper()
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
關於以上str物件的2個方法說明:
-
Series.str.upper
:將Series中所有字串變為大寫 -
Series.str.count
:對Series中所有字串的個數進行計數
其實不難發現,該用法的使用與Python中字串的操作很相似。沒錯,在pandas中你一樣可以這樣簡單的操作,而不同的是你操作的是一整列的字串資料。仍然基於以上資料集,再看它的另一個操作:
>>> regex = (r'(?P<city>[A-Za-z ]+), ' # 一個或更多字母
... r'(?P<state>[A-Z]{2}) ' # 兩個大寫字母
... r'(?P<zip>\d{5}(?:-\d{4})?)') # 可選的4個延伸數字
...
>>> 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
關於以上str物件的2個方法說明:
-
Series.str.replace
:將Series中指定字串替換 -
Series.str.extract
:通過正規表示式提取字串中的資料資訊
這個用法就有點複雜了,因為很明顯看到,這是一個鏈式的用法。通過replace
將 " . "
替換為""
,即為空,緊接著又使用了3個正規表示式(分別對應city,state,zip)通過extract
對資料進行了提取,並由原來的Series資料結構變為了DataFrame資料結構。
當然,除了以上用法外,常用的屬性和方法還有.rstrip
,.contains
,split
等,我們通過下面程式碼檢視一下str
屬性的完整列表:
>>> [i for i in dir(pd.Series.str) if not i.startswith('_')]
['capitalize',
'cat',
'center',
'contains',
'count',
'decode',
'encode',
'endswith',
'extract',
'extractall',
'find',
'findall',
'get',
'get_dummies',
'index',
'isalnum',
'isalpha',
'isdecimal',
'isdigit',
'islower',
'isnumeric',
'isspace',
'istitle',
'isupper',
'join',
'len',
'ljust',
'lower',
'lstrip',
'match',
'normalize',
'pad',
'partition',
'repeat',
'replace',
'rfind',
'rindex',
'rjust',
'rpartition',
'rsplit',
'rstrip',
'slice',
'slice_replace',
'split',
'startswith',
'strip',
'swapcase',
'title',
'translate',
'upper',
'wrap',
'zfill']
屬性有很多,對於具體的用法,如果感興趣可以自己進行摸索練習。
dt物件的使用
Series資料型別:datetime
因為資料需要datetime型別,所以下面使用pandas的date_range()生成了一組日期datetime演示如何進行dt物件操作。
>>> daterng = pd.Series(pd.date_range('2017', periods=9, freq='Q'))
>>> daterng
0 2017-03-31
1 2017-06-30
2 2017-09-30
3 2017-12-31
4 2018-03-31
5 2018-06-30
6 2018-09-30
7 2018-12-31
8 2019-03-31
dtype: datetime64[ns]
>>> daterng.dt.day_name()
0 Friday
1 Friday
2 Saturday
3 Sunday
4 Saturday
5 Saturday
6 Sunday
7 Monday
8 Sunday
dtype: object
>>> # 檢視下半年
>>> daterng\[daterng.dt.quarter > 2]
2 2017-09-30
3 2017-12-31
6 2018-09-30
7 2018-12-31
dtype: datetime64[ns]
>>> daterng[daterng.dt.is_year_end]
3 2017-12-31
7 2018-12-31
dtype: datetime64[ns]
以上關於dt的3種方法說明:
-
Series.dt.day_name()
:從日期判斷出所處星期數 -
Series.dt.quarter
:從日期判斷所處季節 -
Series.dt.is_year_end
:從日期判斷是否處在年底
其它方法也都是基於datetime的一些變換,並通過變換來檢視具體微觀或者巨集觀日期。
cat物件的使用
Series資料型別:Category
在說cat物件的使用前,先說一下Category
這個資料型別,它的作用很強大。雖然我們沒有經常性的在記憶體中執行上g的資料,但是我們也總會遇到執行幾行程式碼會等待很久的情況。使用Category
資料的一個好處就是:可以很好的節省在時間和空間的消耗。下面我們通過幾個例項來學習一下。
>>> 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
上面我們通過使用sys.getsizeof
來顯示記憶體佔用的情況,數字代表位元組數。還有另一種計算內容佔用的方法:memory\_usage()
,後面會使用。
現在我們將上面colors
的不重複值對映為一組整數,然後再看一下佔用的記憶體。
>>> mapper = {v: k for k, v in enumerate(colors.unique())}
>>> mapper
{'periwinkle': 0, 'mint green': 1, 'burnt orange': 2, 'rose': 3, 'navy': 4}
>>> 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
注:對於以上的整數值對映也可以使用更簡單的pd.factorize()
方法代替。
我們發現上面所佔用的記憶體是使用object型別時的一半。其實,這種情況就類似於Category data
型別內部的原理。
記憶體佔用區別:Categorical所佔用的記憶體與Categorical分類的數量和資料的長度成正比,相反,object所佔用的記憶體則是一個常數乘以資料的長度。
下面是object
記憶體使用和category記憶體使用的情況對比。
>>> colors.memory_usage(index=False, deep=True)
650
>>> colors.astype('category').memory_usage(index=False, deep=True)
495
上面結果是使用object
和Category
兩種情況下記憶體的佔用情況。我們發現效果並沒有我們想象中的那麼好。但是注意Category記憶體是成比例的,如果資料集的資料量很大,但不重複分類(unique)值很少的情況下,那麼Category的記憶體佔用可以節省達到10倍以上,比如下面資料量增大的情況:
>>> manycolors = colors.repeat(10)
>>> len(manycolors)/manycolors.nunique()
20.0
>>> manycolors.memory_usage(index=False, deep=True)
6500
>>> manycolors.astype('category').memory_usage(index=False, deep=True)
585
可以看到,在資料量增加10倍以後,使用Category
所佔內容節省了10倍以上。
除了佔用記憶體節省外,另一個額外的好處是計算效率有了很大的提升。因為對於Category
型別的Series
,str字元的操作發生在.cat.categories
的非重複值上,而並非原Series上的所有元素上。也就是說對於每個非重複值都只做一次操作,然後再向與非重複值同類的值對映過去。
對於Category
的資料型別,可以使用accesso
r的cat物件,以及相應的屬性和方法來操作Category
資料。
>>> ccolors = colors.astype('category')
>>> ccolors.cat.categories
Index(['burnt orange', 'mint green', 'navy', 'periwinkle', 'rose'], dtype='object')
實際上,對於開始的整數型別對映,可以先通過reorder_categories
進行重新排序,然後再使用cat.codes
來實現對整數的對映,來達到同樣的效果。
>>> ccolors.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
dtype型別是Numpy的int8(-127~128)
。可以看出以上只需要一個單位元組就可以在記憶體中包含所有的值。我們開始的做法預設使用了int64
型別,然而通過pandas
的使用可以很智慧的將Category
資料型別變為最小的型別。
讓我們來看一下cat還有什麼其它的屬性和方法可以使用。下面cat的這些屬性基本都是關於檢視和操作Category資料型別的。
>>> [i for i in dir(ccolors.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']
但是Category資料的使用不是很靈活。例如,插入一個之前沒有的值,首先需要將這個值新增到.categories的容器中,然後再新增值。
>>> ccolors.iloc[5] = 'a new color'
# ...
ValueError: Cannot setitem on a Categorical with a new category,
set the categories first
>>> ccolors = ccolors.cat.add\_categories(['a new color'])
>>> ccolors.iloc[5] = 'a new color'
如果你想設定值或重塑資料,而非進行新的運算操作,那麼Category型別不是那麼有用。
二、從clipboard剪下板載入資料
當我們的資料存在excel表裡,或者其它的IDE編輯器中的時候,我們想要通過pandas載入資料。我們通常的做法是先儲存再載入,其實這樣做起來十分繁瑣。一個簡單的方法就是使用pd.read\_clipboard()
直接從電腦的剪下板快取區中提取資料。
這樣我們就可以直接將結構資料轉變為DataFrame或者Series了。excel表中資料是這樣的:
在純文字檔案中,比如txt檔案,是這樣的:
a b c d
0 1 inf 1/1/00
2 7.389056099 N/A 5-Jan-13
4 54.59815003 nan 7/24/18
6 403.4287935 None NaT
將上面excel或者txt中的資料選中然後複製,然後使用pandas的read_clipboard()
即可完成到DataFrame的轉換。parse_dates
引數設定為"d",可以自動識別日期,並調整為xxxx-xx-xx
的格式。
>>> df = pd.read_clipboard(na_values=[None], parse_dates=['d'])
>>> df
a b c d
0 0 1.0000 inf 2000-01-01
1 2 7.3891 NaN 2013-01-05
2 4 54.5982 NaN 2018-07-24
3 6 403.4288 NaN NaT
>>> df.dtypes
a int64
b float64
c float64
d datetime64[ns]
dtype: object
三、將pandas物件轉換為“壓縮”格式
在pandas中,我們可以直接將objects
打包成為gzip
, bz2
, zip
, or xz
等壓縮格式,而不必將沒壓縮的檔案放在記憶體中然後進行轉化。來看一個例子如何使用:
>>> 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
匯入檔案,讀取並存為abalone
(DataFrame結構)。當我們要存為壓縮的時候,簡單的使用 to_json()
即可輕鬆完成轉化過程。下面通過設定相應引數將abalone
存為了.gz
格式的壓縮檔案。
abalone.to_json('df.json.gz', orient='records',
lines=True, compression='gzip')
如果我們想知道儲存壓縮檔案的大小,可以通過內建模組os.path,使用getsize方法來檢視檔案的位元組數。下面是兩種格式儲存檔案的大小對比。
>>> import os.path
>>> abalone.to_json('df.json', orient='records', lines=True)
>>> os.path.getsize('df.json') / os.path.getsize('df.json.gz')
11.603035760226396
四、使用"測試模組"製作偽資料
在pandas中,有一個測試模組可以幫助我們生成半真實(偽資料),並進行測試,它就是util.testing
。下面同我們通過一個簡單的例子看一下如何生成資料測試:
>>> 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()
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
上面簡單的使用了
makeTimeDataFrame
和 makeDataFrame
分別生成了一組時間資料和DataFrame的資料。但這只是其中的兩個用法,關於testing
中的方法有大概30多個,如果你想全部瞭解,可以通過檢視dir獲得:
>>> [i for i in dir(tm) if i.startswith('make')]
['makeBoolIndex',
'makeCategoricalIndex',
'makeCustomDataframe',
'makeCustomIndex',
# ...,
'makeTimeSeries',
'makeTimedeltaIndex',
'makeUIntIndex',
'makeUnicodeIndex']
五、從列項中建立DatetimeIndex
也許我們有的時候會遇到這樣的情形(為了說明這種情情況,我使用了product進行交叉迭代的建立了一組關於時間的資料):
>>> 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,它們分別在各個列中,而並非是一個完整日期。那麼如何從這些列中將它們組合在一起並設定為新的index
呢?
通過to_datetime
的使用,我們就可以直接將年月日組合為一個完整的日期,然後賦給索引。程式碼如下:
>>> 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
當然,你可以選擇將原有的年月日列移除,只保留data資料列,然後squeeze
轉換為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
>>> df.index.dtype_str
'datetime64[ns]
更多精彩內容請關注Python資料科學