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这个类的属性包含以下这些(链接中有全部属性,这里我摘抄一些重点的):

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🐼 😃

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值