Pandas进阶贰 pandas基础

Pandas进阶贰 pandas基础

pandas进阶系列根据datawhale远昊大佬的joyful pandas教程写一些自己的心得和补充,本文部分引用了原教程,并参考了《利用Python进行数据分析》、numpy官网、pandas官网
为了方便自己回顾和助教审阅,每一节先将原教程重要知识点罗列一下帮助自己回顾,在其后写一些自己的心得以及我的习题计算过程
本文的的函数总结中,
库函数使用 包名+函数名 命名,如pd.read_csv()
类方法用 类名简写或x + 函数名 命名,如df.to_csv()

另注:本文是对joyful pandas教程的延伸,完整理解需先阅读joyful pandas教程第二章

PPS:写给助教:task2学得有点晚了,目前上传的这个版本还没完全写好自己要写的内容,现在正在通宵爆肝中,会于12月20日凌晨三点前提交最终版,如果助教这段时间内审核的话希望您能稍微给我宽限一点时间,目前的版本有自己原创的部分,不过还没达到我满意的程度,谢谢!

补充内容

在上一个教程结束后我翻阅了《利用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()

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: 0 col1 col2 col3 col4 col5
0 0 2 a 1.4 apple 2020/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_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]来取出相应的列与由多个列组成的表,结果分别为SeriesDataFrame

以下和原文示例稍有差别,主要展示即使只选取一列也可以构造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.排序函数

我用到的关于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】

5. 排序函数

排序共有两种方式,其一为值排序,其二为索引排序,对应的函数是sort_valuessort_index

为了演示排序函数,下面先利用set_index方法把年级和姓名两列作为索引,多级索引的内容和索引设置的方法将在第三章进行详细讲解。

df_demo = df[['Grade', 'Name', 'Height', 'Weight']].set_index(['Grade','Name'])
df_demo.head(3)
Height Weight
Grade Name
Freshman Gaopeng Yang 158.9 46.0
Changqiang You 166.5 70.0
Senior Mei Sun 188.9 89.0

对身高进行排序,默认参数ascending=True为升序:

df_demo.sort_values('Height').head()
Height Weight
Grade Name
Junior Xiaoli Chu 145.4 34.0
Senior Gaomei Lv 147.3 34.0
Sophomore Peng Han 147.8 34.0
Senior Changli Lv 148.7 41.0
Sophomore Changjuan You 150.5 40.0
df_demo.sort_values('Height', ascending=False).head()
Height Weight
Grade Name
Senior Xiaoqiang Qin 193.9 79.0
Mei Sun 188.9 89.0
Gaoli Zhao 186.5 83.0
Freshman Qiang Han 185.3 87.0
Senior Qiang Zheng 183.9 87.0

在排序中,进场遇到多列排序的问题,比如在体重相同的情况下,对身高进行排序,并且保持身高降序排列,体重升序排列:

df_demo.sort_values(['Weight','Height'],ascending=[True,False]).head()
Height Weight
Grade Name
Sophomore Peng Han 147.8 34.0
Senior Gaomei Lv 147.3 34.0
Junior Xiaoli Chu 145.4 34.0
Sophomore Qiang Zhou 150.5 36.0
Freshman Yanqiang Xu 152.4 38.0

索引排序的用法和值排序完全一致,只不过元素的值在索引中,此时需要指定索引层的名字或者层号,用参数level表示。另外,需要注意的是字符串的排列顺序由字母顺序决定。

df_demo.sort_index(level=['Grade','Name'],ascending=[True,False]).head()
Height Weight
Grade Name
Freshman Yanquan Wang 163.5 55.0
Yanqiang Xu 152.4 38.0
Yanqiang Feng 162.3 51.0
Yanpeng Lv NaN 65.0
Yanli Zhang 165.1 52.0

6. apply方法

apply方法常用于DataFrame的行迭代或者列迭代,它的axis含义与第2小节中的统计聚合函数一致,apply的参数往往是一个以序列为输入的函数。例如对于.mean(),使用apply可以如下地写出:

df_demo = df[['Height', 'Weight']]
def my_mean(x):
     res = x.mean()
     return res
df_demo.apply(my_mean)
Height    163.218033
Weight     55.015873
dtype: float64

同样的,可以利用lambda表达式使得书写简洁,这里的x就指代被调用的df_demo表中逐个输入的序列:

df_demo.apply(lambda x:x.mean())
Height    163.218033
Weight     55.015873
dtype: float64

若指定axis=1,那么每次传入函数的就是行元素组成的Series,其结果与之前的逐行均值结果一致。

df_demo.apply(lambda x:x.mean(), axis=1).head()
0    102.45
1    118.25
2    138.95
3     41.00
4    124.00
dtype: float64

这里再举一个例子:mad函数返回的是一个序列中偏离该序列均值的绝对值大小的均值,例如序列1,3,7,10中,均值为5.25,每一个元素偏离的绝对值为4.25,2.25,1.75,4.75,这个偏离序列的均值为3.25。现在利用apply计算升高和体重的mad指标:

df_demo.apply(lambda x:(x-x.mean()).abs().mean())
Height     6.707229
Weight    10.391870
dtype: float64

这与使用内置的mad函数计算结果一致:

df_demo.mad()
Height     6.707229
Weight    10.391870
dtype: float64

【WARNING】谨慎使用apply

得益于传入自定义函数的处理,apply的自由度很高,但这是以性能为代价的。一般而言,使用pandas的内置函数处理和apply来处理同一个任务,其速度会相差较多,因此只有在确实存在自定义需求的情境下才考虑使用apply

【END】

四、窗口对象

pandas中有3类窗口,分别是滑动窗口rolling、扩张窗口expanding以及指数加权窗口ewm。需要说明的是,以日期偏置为窗口大小的滑动窗口将在第十章讨论,指数加权窗口见本章练习。

1. 滑窗对象

要使用滑窗函数,就必须先要对一个序列使用.rolling得到滑窗对象,其最重要的参数为窗口大小window

s = pd.Series([1,2,3,4,5])
roller = s.rolling(window = 3)
roller
Rolling [window=3,center=False,axis=0]

在得到了滑窗对象后,能够使用相应的聚合函数进行计算,需要注意的是窗口包含当前行所在的元素,例如在第四个位置进行均值运算时,应当计算(2+3+4)/3,而不是(1+2+3)/3:

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

对于滑动相关系数或滑动协方差的计算,可以如下写出:

s2 = pd.Series([1,2,6,16,30])
roller.cov(s2)
0     NaN
1     NaN
2     2.5
3     7.0
4    12.0
dtype: float64
roller.corr(s2)
0         NaN
1         NaN
2    0.944911
3    0.970725
4    0.995402
dtype: float64

此外,还支持使用apply传入自定义函数,其传入值是对应窗口的Series,例如上述的均值函数可以等效表示:

roller.apply(lambda x:x.mean())
0    NaN
1    NaN
2    2.0
3    3.0
4    4.0
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
s.diff(-2)
0   -5.0
1   -7.0
2   -9.0
3    NaN
4    NaN
dtype: float64

将其视作类滑窗函数的原因是,它们的功能可以用窗口大小为n+1rolling方法等价代替:

s.rolling(3).apply(lambda x:list(x)[0]) # s.shift(2)
0    NaN
1    NaN
2    1.0
3    3.0
4    6.0
dtype: float64
 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

【END】

2. 扩张窗口

扩张窗口又称累计窗口,可以理解为一个动态长度的窗口,其窗口的大小就是从序列开始处到具体操作的对应位置,其使用的聚合函数会作用于这些逐步扩张的窗口上。具体地说,设序列为a1, a2, a3, a4,则其每个位置对应的窗口即[a1]、[a1, a2]、[a1, a2, a3]、[a1, a2, a3, a4]。

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

【练一练】

cummax, cumsum, cumprod函数是典型的类扩张窗口函数,请使用expanding对象依次实现它们。

【END】

五、练习

Ex1:口袋妖怪数据集

现有一份口袋妖怪的数据集,下面进行一些背景说明:

  • #代表全国图鉴编号,不同行存在相同数字则表示为该妖怪的不同状态

  • 妖怪具有单属性和双属性两种,对于单属性的妖怪,Type 2为缺失值

  • Total, HP, Attack, Defense, Sp. Atk, Sp. Def, Speed分别代表种族值、体力、物攻、防御、特攻、特防、速度,其中种族值为后6项之和

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值。

  2. 对于#重复的妖怪只保留第一条记录,解决以下问题:

  • 求第一属性的种类数量和前三多数量对应的种类
  • 求第一属性和第二属性的组合种类
  • 求尚未出现过的属性组合
  1. 按照下述要求,构造Series
  • 取出物攻,超过120的替换为high,不足50的替换为low,否则设为mid
  • 取出第一属性,分别用replaceapply替换所有字母为大写
  • 求每个妖怪六项能力的离差,即所有能力中偏离中位数最大的值,添加到df并从大到小排序

Ex2:指数加权窗口

  1. 作为扩张窗口的ewm窗口

在扩张窗口中,用户可以使用各类函数进行历史的累计指标统计,但这些内置的统计函数往往把窗口中的所有元素赋予了同样的权重。事实上,可以给出不同的权重来赋给窗口中的元素,指数加权窗口就是这样一种特殊的扩张窗口。

其中,最重要的参数是alpha,它决定了默认情况下的窗口权重为 w i = ( 1 − α ) i , i ∈ { 0 , 1 , . . . , t } w_i=(1−\alpha)^i,i\in\{0,1,...,t\} wi=(1α)i,i{ 0,1,...,t},其中 i = t i=t i=t表示当前元素, i = 0 i=0 i=0表示序列的第一个元素。

从权重公式可以看出,离开当前值越远则权重越小,若记原序列为 x x x,更新后的当前元素为 y t y_t yt,此时通过加权公式归一化后可知:

$$
\begin{split}y_t &=\frac{\sum_{i=0}^{t} w_i x_{t-i}}{\sum_{i=0}^{t} w_i} \
&=\frac{x_t + (1 - \alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + …

  • (1 - \alpha)^{t} x_{0}}{1 + (1 - \alpha) + (1 - \alpha)^2 + …
  • (1 - \alpha)^{t-1}}\\end{split}
    $$

对于Series而言,可以用ewm对象如下计算指数平滑后的序列:

np.random.seed(0)
s = pd.Series(np.random.randint(-1,2,30).cumsum())
s.head()
0   -1
1   -1
2   -2
3   -2
4   -2
dtype: int32
s.ewm(alpha=0.2).mean().head()
0   -1.000000
1   -1.000000
2   -1.409836
3   -1.609756
4   -1.725845
dtype: float64

请用expanding窗口实现。

  1. 作为滑动窗口的ewm窗口

从第1问中可以看到,ewm作为一种扩张窗口的特例,只能从序列的第一个元素开始加权。现在希望给定一个限制窗口n,只对包含自身最近的n个窗口进行滑动加权平滑。请根据滑窗函数,给出新的wiyt的更新公式,并通过rolling窗口实现这一功能。

后记

why named pandas? – Panel Data
起队名的时候本来也想起个熊猫相关的名字,然后查了一下pandas名字的由来,原来是来自panel data…很合理又很失望:(
不过并不妨碍我们继续喜欢pandas

你可能感兴趣的:(pandas,datawhale,python,数据分析,pandas,大数据)