pandas进阶系列根据datawhale远昊大佬的joyful pandas教程写一些自己的心得和补充,本文部分引用了原教程,并参考了《利用Python进行数据分析》、numpy官网、pandas官网
为了方便自己回顾和助教审阅,每一节先将原教程重要知识点罗列一下帮助自己回顾,在其后写一些自己的心得以及我的习题计算过程
本文的的函数总结中,
库函数使用 包名+函数名 命名,如pd.read_csv()
类方法用 类名简写或x + 函数名 命名,如df.to_csv()
另注:本文是对joyful pandas教程的延伸,完整理解需先阅读joyful pandas教程第二章
TODO:目前还有一道习题没做,已经做的习题还没总结方法及与答案做比较,21号晚上补
在上一个教程结束后我翻阅了《利用python进行数据分析》进一步学习了numpy相关的内容,有一些收获,暂时先放在这一章和大家分享
经过最近的学习,对notebook的操作更快了些,主要是记了几个快捷键,让自己效率大大提升,和大家快速分享一下,通过几个快捷键解放双手再也不用鼠标
ESC
切换成命令模式,使用 ENTER
切换成编辑模式m
转换单元格为Markdown单元格,y
将单元格转换为代码单元格a
在当前单元格上方新建单元格, b
在当前单元格下方新建单元格alt+enter
可以在运行当前单元格后在下方新建一个单元格也很常用d d
(和vim删除一行的操作一样)可以删除当前单元格,使用 z
可以撤销删除,试了下z也可以撤销好几次的删除,上限有多少不清楚(我试了下撤销11次都没问题)不查不知道,之前误删了好多次,都不知道用z然后就像个傻子一样再打一遍…1,2,3...
可以在第一行直接加入一级、二级、三级…标题这些是我现在经常用到的命令,这些小技巧有点基于个人经验而谈,不过记住这些命令就不用鼠标了打起来很流畅。
更多notebook快捷键可以在命令模式下按 h
查询
在上一节的练习题中,远昊大佬的习题让我们充分感受到了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])
函数总结
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()
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
中具有两种基本的数据存储结构,存储一维values
的Series
和存储二维values
的DataFrame
,在这两种结构上定义了很多的属性和方法。
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类的属性而不是方法,因此调用时没有括号
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
这节相对于原教程没有多少新增加的内容,主要是做了习题,由于这一块掌握得还不好需要随时巩固所以保留了原文
pandas
中有3类窗口,分别是滑动窗口rolling
、扩张窗口expanding
以及指数加权窗口ewm
。
要使用滑窗函数,就必须先要对一个序列使用.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
扩张窗口又称累计窗口,可以理解为一个动态长度的窗口,其窗口的大小就是从序列开始处到具体操作的对应位置,其使用的聚合函数会作用于这些逐步扩张的窗口上。具体地说,设序列为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之后加相对应的聚合函数,从结果上来看是对的,但是还有两个很讨厌的疑惑:
今天太晚了,明天打算偷机看一下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
现有一份口袋妖怪的数据集,下面进行一些背景说明:
#
代表全国图鉴编号,不同行存在相同数字则表示为该妖怪的不同状态
妖怪具有单属性和双属性两种,对于单属性的妖怪,Type 2
为缺失值
Total, HP, Attack, Defense, Sp. Atk, Sp. Def, Speed
分别代表种族值、体力、物攻、防御、特攻、特防、速度,其中种族值为后6项之和
对HP, Attack, Defense, Sp. Atk, Sp. Def, Speed
进行加总,验证是否为Total
值。
对于#
重复的妖怪只保留第一条记录,解决以下问题:
Series
: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