Pandas進階貳 pandas基礎

嫌疑人Y的執事發表於2020-12-20

Pandas進階貳 pandas基礎

pandas進階系列根據datawhale遠昊大佬的joyful pandas教程寫一些自己的心得和補充,本文部分引用了原教程,並參考了《利用Python進行資料分析》、numpy官網pandas官網
為了方便自己回顧和助教審閱,每一節先將原教程重要知識點羅列一下幫助自己回顧,在其後寫一些自己的心得以及我的習題計算過程
本文的的函式總結中,
庫函式使用 包名+函式名 命名,如pd.read_csv()
類方法用 類名簡寫或x + 函式名 命名,如df.to_csv()

另注:本文是對joyful pandas教程的延伸,完整理解需先閱讀joyful pandas教程第二章

TODO:目前還有一道習題沒做,已經做的習題還沒總結方法及與答案做比較,21號晚上補

補充內容

在上一個教程結束後我翻閱了《利用python進行資料分析》進一步學習了numpy相關的內容,有一些收穫,暫時先放在這一章和大家分享

notebook 操作

經過最近的學習,對notebook的操作更快了些,主要是記了幾個快捷鍵,讓自己效率大大提升,和大家快速分享一下,通過幾個快捷鍵解放雙手再也不用滑鼠

  • notebook的每個單元格的讀寫模式和vim有些類似,使用 ESC 切換成命令模式,使用 ENTER 切換成編輯模式
  • 在命令模式下,可以通過 m 轉換單元格為Markdown單元格,y 將單元格轉換為程式碼單元格
  • 在命令模式下,可以通過 a 在當前單元格上方新建單元格, b 在當前單元格下方新建單元格
  • 另外除了常用的shift+enter, ctrl+enter外,還有 alt+enter 可以在執行當前單元格後在下方新建一個單元格也很常用
  • 在命令模式下,通過 d d (和vim刪除一行的操作一樣)可以刪除當前單元格,使用 z 可以撤銷刪除,試了下z也可以撤銷好幾次的刪除,上限有多少不清楚(我試了下撤銷11次都沒問題)不查不知道,之前誤刪了好多次,都不知道用z然後就像個傻子一樣再打一遍…
  • 在命令模式下,鍵入 1,2,3... 可以在第一行直接加入一級、二級、三級…標題

這些是我現在經常用到的命令,這些小技巧有點基於個人經驗而談,不過記住這些命令就不用滑鼠了打起來很流暢。
更多notebook快捷鍵可以在命令模式下按 h 查詢

numpy陣列索引原理

在上一節的練習題中,遠昊大佬的習題讓我們充分感受到了numpy的高效與靈活,那麼為什麼numpy可以計算得這麼快呢?
首先,np.ndarray與python的list相比,ndarray中的所有資料都是相同型別的,而list中的資料可以是各種型別的,因此ndarray效率更高;
另外也是非常重要的一點是,ndarray的本質是一個資料塊的檢視
我們通過觀察ndarray的內部結構來詳細瞭解以下,ndarray這個類的屬性包含以下這些(連結中有全部屬性,這裡我摘抄一些重點的):

attributesdescription
strides跨到下一個元素所需要的位元組數
size陣列中元素數量
dtype資料型別
data資料塊的起始位置

學過C/C++的同學有沒有熟悉的感覺!這些屬性感覺就是新建了一個陣列,然後建了一個指向陣列元素型別的指標嘛!(我是這麼覺得的,助教大大可以審閱一下看對不對)

因此ndarray的索引和切片並不是新開闢了一個記憶體空間去存資料,而只是一種檢視,即改變了原有資料的訪問方式。

具體舉個例子驗證一下:
現有陣列a,陣列b的索引方式是b=a[2:8:2],即b是a的第三個元素、第五個元素、第七個元素,按照剛剛的想法,b的實現邏輯應該是根據data、strides和b給出的起始索引,將指標移到第一個要讀取的元素的位置,將這個位置賦值給b的data,緊接著根據步長和strides,決定b的每個元素要讀取的步長,賦值給b的strides,所以生成b的時候完全沒有資料的遷移,僅僅是根據a的屬性生成了b的屬性
下面的例子驗證了這個想法

import numpy as np
import pandas as pd
a = np.arange(10)
b = a[2:8:2]
b[-1] = 999
print(f'a strides: {a.strides}')
print(f'b strides: {b.strides}')
a
a strides: (8,)
b strides: (16,)





array([  0,   1,   2,   3,   4,   5, 999,   7,   8,   9])

Pandas 檔案讀寫

函式總結

functiondescription
pd.read_csv()
pd.read_table()預設以\t為分隔符,可以自定義
df.to_csv()預設csv,可以自定義分隔符
df.to_markdown()天哪還有這種神奇函式(需要安裝tabulate包)
常用引數
index_col用作索引的列號列名
usecols選擇列
header定義列名
nrows讀取前n行
parse_dates將某些列解析為datetime

我目前經常會用到的讀函式就是read_csv,不過根據read_table給出的引數sep, 說明read_csv也可以通過read_table實現,展示一下:

df_txt = pd.read_table('../data/my_csv.csv', sep=',')
df_txt
col1col2col3col4col5
02a1.4apple2020/1/1
13b3.4banana2020/1/2
26c2.5orange2020/1/5
35d3.2lemon2020/1/7

另外可以看出遠昊大佬構造的表可謂用心良苦,每一列的資料型別由常識來判斷都是不同的,通過常識判斷,這五列在一般上下文中的資料型別應該是整形、字元型、浮點數、字串、日期,那在不做任何預處理的情況下,看看pandas是如何認識資料的:

df_txt.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4 entries, 0 to 3
Data columns (total 5 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   col1    4 non-null      int64  
 1   col2    4 non-null      object 
 2   col3    4 non-null      float64
 3   col4    4 non-null      object 
 4   col5    4 non-null      object 
dtypes: float64(1), int64(1), object(3)
memory usage: 288.0+ bytes

可以看出,對於整形和浮點型,pandas預設將其標記為int64float64, 而其他型別一概標記為object,因此在具體專案中對其他資料要分別定義好資料型別,對整形和浮點型應該根據語義或者資料,確定其具體的更小的資料型別以壓縮資料。

資料寫入
一般在資料寫入中,最常用的操作是把index設定為False,特別當索引沒有特殊意義的時候,這樣的行為能把索引在儲存的時候去除。
這點要特別注意,如果不設定為False,讀取時會多一個Unnamed:0列,我打比賽做特徵工程的時候多次犯了這個錯誤提醒大家特別要小心:(
示例如下:

df_csv.to_csv('../data/my_csv_saved.csv')
df_unindexed = pd.read_csv('../data/my_csv_saved.csv')
df_unindexed.head(1)
Unnamed: 0col1col2col3col4col5
002a1.4apple2020/1/1

Pandas 基本資料結構

pandas中具有兩種基本的資料儲存結構,儲存一維valuesSeries和儲存二維valuesDataFrame,在這兩種結構上定義了很多的屬性和方法。

1. Series

Series一般由四個部分組成,分別是序列的值data、索引index、儲存型別dtype、序列的名字name。其中,索引也可以指定它的名字,預設為空。

s = pd.Series(data = [100, 'a', {'dic1':5}],
              index = pd.Index(['id1', 20, 'third'], name='my_idx'),
              dtype = 'object',
              name = 'my_name')
s
my_idx
id1              100
20                 a
third    {'dic1': 5}
Name: my_name, dtype: object
s.values
array([100, 'a', {'dic1': 5}], dtype=object)
s.index
Index(['id1', 20, 'third'], dtype='object', name='my_idx')

這裡特別注意的是,
values, index, dtype, name, shape等都是pd.Series類的屬性而不是方法,因此呼叫時沒有括號

2. DataFrame

DataFrameSeries的基礎上增加了列索引,一個資料框可以由二維的data與行列索引來構造:

data = [[1, 'a', 1.2], [2, 'b', 2.2], [3, 'c', 3.2]]
df = pd.DataFrame(data = data,
                  index = ['row_%d'%i for i in range(3)],
                  columns=['col_0', 'col_1', 'col_2'])
df
col_0col_1col_2
row_01a1.2
row_12b2.2
row_23c3.2

特別注意的是
DataFrame中可以用[col_name][col_list]來取出相應的列與由多個列組成的表,結果分別為SeriesDataFrame

以下和原文示例稍有差別,主要展示即使只選取一列也可以構造DataFrame,只需要使用col_list

type(df['col_0'])
pandas.core.series.Series
type(df[['col_0']])
pandas.core.frame.DataFrame

三、常用基本函式

functiondescription
1.彙總函式
head,tail預覽首,尾n行
info表的資訊概括
describe表各列的統計概括
pandas-profiling包更全面的資料彙總
2,特徵統計函式(聚合函式)
sum,mean,std,max…
quantile分位數
count非缺失值個數
idxmax,idxmin最值索引
3.唯一值函式
unique列的唯一值列表
nunique列的唯一值個數
value_counts列的值與頻次
drop_duplicates去重
4.替換函式
replace通過字典或兩個列表替換,引數ffill,bfill決定用之前(之後)最近非被替換值替換
where符合條件保留
mask符合條件去除
clip兩邊咔嚓
5.排序函式
sort_values根據列排序,可選定先後排的列
sort_index根據索引排序,多級索引時可以選定先後排的索引

我用到的關於drop_duplicates的一個用法——求差集
pandas沒有內建DF求差集的方法,但可以通過drop_duplicates實現
如下:

name = df['Name'].drop_duplicates()
name.value_counts()
Chengli Sun      1
Gaojuan Qin      1
Juan Qin         1
Qiang Zhou       1
Yanqiang Xu      1
                ..
Changquan Han    1
Gaoli Wu         1
Yanmei Qian      1
Xiaopeng Sun     1
Xiaofeng You     1
Name: Name, Length: 170, dtype: int64
del_name = pd.Series(['Yanli Zhang', 'Feng Yang', 'Yanfeng Han', 'Xiaofeng You'])
name = name.append(del_name).append(del_name).drop_duplicates(keep=False)
name.value_counts()
Chengli Sun      1
Peng You         1
Juan Qin         1
Yanqiang Xu      1
Feng Zhao        1
                ..
Chunqiang Chu    1
Changquan Han    1
Gaoli Wu         1
Yanmei Qian      1
Feng Zheng       1
Length: 166, dtype: int64

由上面結果看出,name的數量從170減少到166個,減掉了指定的四個
令所求集合C=A-B,本演算法先求A+B+B,再使用drop_duplicates,並指定引數為keep=False保證重複的項被刪除,單獨在B中的項由於被加了兩次所以會被刪除,AB中都存在的顯然也會被刪除,故保留了A-B

s.clip(0, 2) # 前兩個數分別表示上下截斷邊界
0    0.0000
1    1.2345
2    2.0000
3    0.0000
dtype: float64
s = pd.Series([-1, 1.2345, 100, -50])
s
0     -1.0000
1      1.2345
2    100.0000
3    -50.0000
dtype: float64

【練一練】

在 clip 中,超過邊界的只能截斷為邊界值,如果要把超出邊界的替換為自定義的值,應當如何做?

【我的思路】

假設自定義值為-999,可以用mask在兩邊各截一下

s.mask(s<0, -999).mask(s>2, -999)
0   -999.0000
1      1.2345
2   -999.0000
3   -999.0000
dtype: float64

【END】

四、視窗物件

這節相對於原教程沒有多少新增加的內容,主要是做了習題,由於這一塊掌握得還不好需要隨時鞏固所以保留了原文

pandas中有3類視窗,分別是滑動視窗rolling、擴張視窗expanding以及指數加權視窗ewm

1. 滑窗物件

要使用滑窗函式,就必須先要對一個序列使用.rolling得到滑窗物件,其最重要的引數為視窗大小window

s = pd.Series([1,2,3,4,5])
roller = s.rolling(window = 3)
cen_roller = s.rolling(window = 3, center=True)

試了下center=True,和預計的效果一樣,會把當前數作為window的中心來處理,看一下效果:

roller.mean()
0    NaN
1    NaN
2    2.0
3    3.0
4    4.0
dtype: float64
cen_roller.sum()
0     NaN
1     6.0
2     9.0
3    12.0
4     NaN
dtype: float64

shift, diff, pct_change是一組類滑窗函式,它們的公共引數為periods=n,預設為1,分別表示取向前第n個元素的值、與向前第n個元素做差(與Numpy中不同,後者表示n階差分)、與向前第n個元素相比計算增長率。這裡的n可以為負,表示反方向的類似操作。

s = pd.Series([1,3,6,10,15])
s.shift(2)
0    NaN
1    NaN
2    1.0
3    3.0
4    6.0
dtype: float64
s.diff(3)
0     NaN
1     NaN
2     NaN
3     9.0
4    12.0
dtype: float64
s.pct_change()
0         NaN
1    2.000000
2    1.000000
3    0.666667
4    0.500000
dtype: float64
s.shift(-1)
0     3.0
1     6.0
2    10.0
3    15.0
4     NaN
dtype: float64
%%timeit
s.diff(2)
89.7 µs ± 4.06 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

將其視作類滑窗函式的原因是,它們的功能可以用視窗大小為n+1rolling方法等價代替:

%%timeit
s.rolling(3).apply(lambda x:list(x)[0]) # s.shift(2)
549 µs ± 77.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
 s.rolling(4).apply(lambda x:list(x)[-1]-list(x)[0]) # s.diff(3)
0     NaN
1     NaN
2     NaN
3     9.0
4    12.0
dtype: float64
def my_pct(x):
     L = list(x)
     return L[-1]/L[0]-1
s.rolling(2).apply(my_pct) # s.pct_change()
0         NaN
1    2.000000
2    1.000000
3    0.666667
4    0.500000
dtype: float64

【練一練】

rolling物件的預設視窗方向都是向前的,某些情況下使用者需要向後的視窗,例如對1,2,3設定向後視窗為2的sum操作,結果為3,5,NaN,此時應該如何實現向後的滑窗操作?(提示:使用shift

【我的思路】

實在沒想出來shift怎麼做…但是感覺用倒序的方法好像挺容易想的,對逆序後的原序列按向前滑窗就相當於向後滑窗了,不過不知道這種方法速度會不會比shift慢,所以shift到底怎麼做o.o…

a = pd.Series([1,2,3])
a[::-1].rolling(2).sum()[::-1]
0    3.0
1    5.0
2    NaN
dtype: float64

【END】

2. 擴張視窗

擴張視窗又稱累計視窗,可以理解為一個動態長度的視窗,其視窗的大小就是從序列開始處到具體操作的對應位置,其使用的聚合函式會作用於這些逐步擴張的視窗上。具體地說,設序列為a1, a2, a3, a4,則其每個位置對應的視窗即[a1]、[a1, a2]、[a1, a2, a3]、[a1, a2, a3, a4]。

s = pd.Series([1, 6, 3, 10])
s.expanding().mean()
0    1.000000
1    3.500000
2    3.333333
3    5.000000
dtype: float64

【練一練】(我稍微改了下習題資料,更好糾錯一點)

cummax, cumsum, cumprod函式是典型的類擴張視窗函式,請使用expanding物件依次實現它們。

【我的思路】

我先試了下最直觀的解法,就是expanding之後加相對應的聚合函式,從結果上來看是對的,但是還有兩個很討厭的疑惑:

  • 我用expanding做出來的dtype都是float64,所以內建函式是怎麼做到不改變資料型別的???【未解決】
  • 我覺得用我的方法做從原理上看感覺好像很慢,我測了一下也確實很慢,感覺應該是每擴張一步都要從頭計算的原因,有沒有隻計算新進入元素的方法???【未解決】

今天太晚了,明天打算偷機看一下cumsum原碼怎麼做的,剛大致看了下,幾個cum其實都調的一個函式,就改了個引數

%%timeit
s.cummax()
52.9 µs ± 2.81 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%%timeit
s.expanding().max()
386 µs ± 111 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
s.cumsum()
0     1
1     7
2    10
3    20
dtype: int64
s.expanding().sum()
0     1.0
1     7.0
2    10.0
3    20.0
dtype: float64
s.cumprod()
0      1
1      6
2     18
3    180
dtype: int64
s.expanding().apply(lambda x: np.prod(x))
0      1.0
1      6.0
2     18.0
3    180.0
dtype: float64

【END】

五、練習

Ex1:口袋妖怪資料集

現有一份口袋妖怪的資料集,下面進行一些背景說明:

  • #代表全國圖鑑編號,不同行存在相同數字則表示為該妖怪的不同狀態

  • 妖怪具有單屬性和雙屬性兩種,對於單屬性的妖怪,Type 2為缺失值

  • Total, HP, Attack, Defense, Sp. Atk, Sp. Def, Speed分別代表種族值、體力、物攻、防禦、特攻、特防、速度,其中種族值為後6項之和

  1. HP, Attack, Defense, Sp. Atk, Sp. Def, Speed進行加總,驗證是否為Total值。

  2. 對於#重複的妖怪只保留第一條記錄,解決以下問題:

  • 求第一屬性的種類數量和前三多數量對應的種類
  • 求第一屬性和第二屬性的組合種類
  • 求尚未出現過的屬性組合
  1. 按照下述要求,構造Series
  • 取出物攻,超過120的替換為high,不足50的替換為low,否則設為mid
  • 取出第一屬性,分別用replaceapply替換所有字母為大寫
  • 求每個妖怪六項能力的離差,即所有能力中偏離中位數最大的值,新增到df並從大到小排序
df = pd.read_csv('../data/pokemon.csv')
df.head(3)
#NameType 1Type 2TotalHPAttackDefenseSp. AtkSp. DefSpeed
01BulbasaurGrassPoison318454949656545
12IvysaurGrassPoison405606263808060
23VenusaurGrassPoison52580828310010080
#1 對HP, Attack, Defense, Sp. Atk, Sp. Def, Speed進行加總,驗證是否為Total值。
(df.drop(columns=['#', 'Name', 'Type 1', 'Type 2', 'Total']).sum(axis=1) == df['Total']).sum()
#統計了下各行的和,結果和total相等的求sum,值為800說明所有值相加確實都為total
800
#2
a = df.drop_duplicates(['#'])['Type 1'].value_counts()
print(f'type 1種類數量:{len(a)}\n 前三多種類:\n{a[:3]}')
b = df.drop_duplicates(['#']).drop_duplicates(['Type 1', 'Type 2'])['#'].count()
print(f'組合種類數: {b}')
types = list(df['Type 1'].append(df['Type 2'].dropna()).drop_duplicates().values)
import itertools
types = list(itertools.permutations(types, 2))
types = pd.DataFrame(types, columns=['Type 1', 'Type 2'])
exists = df.drop_duplicates(['#']).drop_duplicates(['Type 1', 'Type 2']).dropna()[['Type 1', 'Type 2']]
types.append(exists).append(exists).drop_duplicates(keep=False)
type 1種類數量:18
 前三多種類:
Water     105
Normal     93
Grass      66
Name: Type 1, dtype: int64
組合種類數: 143
Type 1Type 2
0GrassFire
1GrassWater
2GrassBug
3GrassNormal
5GrassElectric
.........
300FlyingRock
301FlyingGhost
302FlyingIce
304FlyingDark
305FlyingSteel

181 rows × 2 columns

s = df['Attack']
res = s.mask(s>120, 'high').mask(s<50, 'low')
res = res.apply(lambda x:x if x=='low' or x=='high' else 'mid')
res.value_counts()
mid     579
low     133
high     88
Name: Attack, dtype: int64
#這道題沒做出來,再思考一下
df['de'] = df.drop(columns=['#', 'Name', 'Type 1', 'Type 2', 'Total']).apply(lambda x:np.max((x-x.median()).abs()), 1)
df.sort_values(by='de', ascending=False).head()
#NameType 1Type 2TotalHPAttackDefenseSp. AtkSp. DefSpeedde
230213ShuckleBugRock5052010230102305215.0
121113ChanseyNormalNaN450250553510550207.5
261242BlisseyNormalNaN54025510107513555190.0
333306AggronMega AggronSteelNaN63070140230608050155.0
224208SteelixMega SteelixSteelGround61075125230559530145.0

後記

why named pandas? – Panel Data
起隊名的時候本來也想起個熊貓相關的名字,然後查了一下pandas名字的由來,原來是來自panel data…很合理又很失望:(
不過並不妨礙我們繼續喜歡pandas? ?

相關文章