Pandas進階貳 pandas基礎
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這個類的屬性包含以下這些(連結中有全部屬性,這裡我摘抄一些重點的):
attributes | description |
---|---|
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 檔案讀寫
函式總結
function | description |
---|---|
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
col1 | col2 | col3 | col4 | col5 | |
---|---|---|---|---|---|
0 | 2 | a | 1.4 | apple | 2020/1/1 |
1 | 3 | b | 3.4 | banana | 2020/1/2 |
2 | 6 | c | 2.5 | orange | 2020/1/5 |
3 | 5 | d | 3.2 | lemon | 2020/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預設將其標記為int64
和float64
, 而其他型別一概標記為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: 0 | col1 | col2 | col3 | col4 | col5 | |
---|---|---|---|---|---|---|
0 | 0 | 2 | a | 1.4 | apple | 2020/1/1 |
Pandas 基本資料結構
pandas
中具有兩種基本的資料儲存結構,儲存一維values
的Series
和儲存二維values
的DataFrame
,在這兩種結構上定義了很多的屬性和方法。
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
DataFrame
在Series
的基礎上增加了列索引,一個資料框可以由二維的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_0 | col_1 | col_2 | |
---|---|---|---|
row_0 | 1 | a | 1.2 |
row_1 | 2 | b | 2.2 |
row_2 | 3 | c | 3.2 |
特別注意的是
在DataFrame
中可以用[col_name]
與[col_list]
來取出相應的列與由多個列組成的表,結果分別為Series
和DataFrame
:
以下和原文示例稍有差別,主要展示即使只選取一列也可以構造DataFrame,只需要使用col_list
type(df['col_0'])
pandas.core.series.Series
type(df[['col_0']])
pandas.core.frame.DataFrame
三、常用基本函式
function | description |
---|---|
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+1
的rolling
方法等價代替:
%%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項之和
-
對
HP, Attack, Defense, Sp. Atk, Sp. Def, Speed
進行加總,驗證是否為Total
值。 -
對於
#
重複的妖怪只保留第一條記錄,解決以下問題:
- 求第一屬性的種類數量和前三多數量對應的種類
- 求第一屬性和第二屬性的組合種類
- 求尚未出現過的屬性組合
- 按照下述要求,構造
Series
:
- 取出物攻,超過120的替換為
high
,不足50的替換為low
,否則設為mid
- 取出第一屬性,分別用
replace
和apply
替換所有字母為大寫 - 求每個妖怪六項能力的離差,即所有能力中偏離中位數最大的值,新增到
df
並從大到小排序
df = pd.read_csv('../data/pokemon.csv')
df.head(3)
# | Name | Type 1 | Type 2 | Total | HP | Attack | Defense | Sp. Atk | Sp. Def | Speed | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | Bulbasaur | Grass | Poison | 318 | 45 | 49 | 49 | 65 | 65 | 45 |
1 | 2 | Ivysaur | Grass | Poison | 405 | 60 | 62 | 63 | 80 | 80 | 60 |
2 | 3 | Venusaur | Grass | Poison | 525 | 80 | 82 | 83 | 100 | 100 | 80 |
#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 1 | Type 2 | |
---|---|---|
0 | Grass | Fire |
1 | Grass | Water |
2 | Grass | Bug |
3 | Grass | Normal |
5 | Grass | Electric |
... | ... | ... |
300 | Flying | Rock |
301 | Flying | Ghost |
302 | Flying | Ice |
304 | Flying | Dark |
305 | Flying | Steel |
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()
# | Name | Type 1 | Type 2 | Total | HP | Attack | Defense | Sp. Atk | Sp. Def | Speed | de | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
230 | 213 | Shuckle | Bug | Rock | 505 | 20 | 10 | 230 | 10 | 230 | 5 | 215.0 |
121 | 113 | Chansey | Normal | NaN | 450 | 250 | 5 | 5 | 35 | 105 | 50 | 207.5 |
261 | 242 | Blissey | Normal | NaN | 540 | 255 | 10 | 10 | 75 | 135 | 55 | 190.0 |
333 | 306 | AggronMega Aggron | Steel | NaN | 630 | 70 | 140 | 230 | 60 | 80 | 50 | 155.0 |
224 | 208 | SteelixMega Steelix | Steel | Ground | 610 | 75 | 125 | 230 | 55 | 95 | 30 | 145.0 |
後記
why named pandas? – Panel Data
起隊名的時候本來也想起個熊貓相關的名字,然後查了一下pandas名字的由來,原來是來自panel data…很合理又很失望:(
不過並不妨礙我們繼續喜歡pandas? ?
相關文章
- Pandas基礎
- Pandas 基礎 (2) - Dataframe 基礎
- Pandas基礎介紹
- Pandas基礎學習
- pandas - 基礎屬性
- 【Pandas基礎教程】第02講 Pandas讀取資料
- Pandas 基礎 (18) - Period and PeriodIndexIndex
- 4.pandas基礎使用
- Pandas 基礎 (16) - Holidays
- Pandas 基礎 (17) - to_datetime
- Pandas 基礎 (14) - DatetimeIndex and ResampleIndex
- Pandas 基礎 (12) - Stack 和 Unstack
- Pandas 基礎 (13) - Crosstab 交叉列表取值ROS
- 組隊學習2——pandas基礎
- pandas學習之Python基礎Python
- Pandas 基礎 (9) - 組合方法 merge
- Python 資料處理庫 pandas 進階教程Python
- Pandas 基礎 (11) - 用 melt 做格式轉換
- Pandas 基礎 (8) - 用 concat 組合 dataframe
- Pandas 基礎 (5) - 處理缺失的資料
- Pandas 基礎 (3) - 生成 Dataframe 的幾種方式
- Pandas使用DataFrame進行資料分析比賽進階之路(一)
- Pandas高階教程之:window操作
- Pandas高階教程之:GroupBy用法
- Pandas之:Pandas簡潔教程
- Pandas 基礎 (1) - 初識及安裝 yupyter
- Pandas 基礎 (4) - 讀 / 寫 Excel 和 CSV 檔案Excel
- Pandas庫基礎分析——資料生成和訪問
- Pandas
- Pandas之:Pandas高階教程以鐵達尼號真實資料為例
- Pandas - pandas.Series.pipe 函式函式
- Pandas高階教程之:統計方法
- 使用pandas進行資料分析
- Pandas高階教程之:Dataframe的合併
- Pandas高階教程之:自定義選項
- Pandas高階教程之:時間處理
- Pandas高階教程之:category資料型別Go資料型別
- Pandas高階教程之:處理text資料