(2024年5月)学习pandas(最基础操作)

This article is categorized as "Garbage" . It should NEVER be appeared in your search engine's results.


Table of Contents

前置内容


使用https://web.stanford.edu/class/archive/cs/cs109/cs109.1166/stuff/titanic.csv


一共888行,其中第1行是csv head,所以数据已公示887条,注意:

wc -l titanic.csv

887

这是因为命令wc -l并不是统计行数,而是统计有多少换行符。由于这个csv文件的最后一行没有换行符,所以会返回887


读文件

读取csv,获取基本信息

获取所有column name

获取所有column name(as a list)

以后会用得上

判断df内容相同

判断两个dataframe内容完全相同

使用 df1.equals(df2)来判断 

series和dataframe

区分

先区分pandas.series和pandas.dataframe

series转list以及numpy array

用list(series)或者series.to_list()就可以转换为list

转换为list以后当然就可以直接用np.array(list)转换为numpy array,当然也有直接转换的方法to_numpy()

series转dataframe

series可以转成dataframe,但记得转置:

series转dict

在之前的一系列操作(series转list, numpy array等)很容易让人误以为series就是一个像list的对象,用自然数作为索引下标。实际上并不是的:

实际上我们获取的series都可以转换为dict,只是在之前的场景下我们用不到series的"自然数key",但现在我们需要用到series的“字符串key“:

List转Series

其实就是反过来的操作,但要注意,创建pandas.Series数据的时候可以指定一些东西,可以解决很多【合并】方面的问题:

1:比如作为column name,常见于“把Series合并为新的一列“:

(下面这张图来自后面的目录【Dataframe合并List,List作为新的一列】)

2:又比如作为"index",常见于“把Series合并为新的一行“

(下面这张图来自后面的目录【合并多行(series index带来的问题)】)

row index

来自reset_index的问题

这个问题是写完这篇笔记后引入的。具体来说就是:

由于本篇笔记基本都在分析titanic.csv,涉及到的row index都是0, 1, 2, 3...的自然数(用pandas的称呼叫做sequential index)。但有些时候一旦涉及到删除行+reset_index,一些奇怪的问题就会出现。

example 1:🔗 [pandas.DataFrame.reset_index — pandas 2.2.2 documentation] https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.reset_index.html

来自drop的问题

example 2:

来自本篇笔记后面的例子:

下面的语句本质上是在drop row index为0, 1, 2, 3, 886的这几行(而不是在drop物理意义/iloc意义上的这几行),所以第二次drop的时候会出问题:

来自iloc, loc和at的问题

dataframe是允许重复row index的!所以很多时候会遇到奇怪的dataframe,比如下面这张图里的dataframe,row index为0的行有2行!

(这张图来自后面的目录【dataframe合并List,List作为新的一行】,所以这个dataframe长得有点奇怪,因为最后加上来的这一行全部都是float类型的数据)

如果用iloc试图获取“物理意义上的第887行“,就会获得最后一行:

如果用loc试图获取“row index为0的行“,就会获得2行:

如果想获取【第xx行+y列】的数据,使用下面的方法会获取到2个数值(第一行和最后一行),说明下面的2种方法都是基于row index(而不是物理index)的:

如果要想真-获取【最后一行的column 'Name'的那一个数值】,就需要先使用iloc,然后用列名获取:

上面这种dataframe会干扰我们的程序(一般情况下,只有“有意义”的Index才有重复的意义),解决方法是reset_index():

iloc操作

基本介绍

iloc操作的是【物理意义上的索引】(见上面有关row index的说明)


先写iloc操作,是因为iloc只涉及索引下标,最容易学

学了iloc以后再学loc

下面会涉及到这些写法:

df.iloc[0],等价于df.iloc[0, :]
df.iloc[0:0],等价于df.iloc[0:0, :]
df.iloc[0:1],等价于df.iloc[0:1, :]
df.iloc[0, :]
df.iloc[0:1, :]
df.iloc[:, 1]
df.iloc[:, 1:2]

获取一行(pandas series)

df.iloc[x] 获取第x行,这里返回的是pandas.series

完整地写应该写成df.iloc[x, :]

一旦获取了series类型的数据,它的形态就会发生改变,不再是”一行“的样子了,而是“一列”(这里涉及到后面的内容:合并多行)

获取一行(dataframe)

df.iloc[[x]] 获取第x行,这里返回的是pandas.dataframe

完整地写应该写成df.iloc[[x], :]

获取多行(连续)

通过df.iloc[x:y]获取的是dataframe,注意df.iloc[0:0], df.iloc[1:1]这样的语句会返回一个空dataframe(和python list slice逻辑一样)

获取多行(离散)

通过df.iloc[[x, y, z], :]获取多行,返回一个x, y, z拼起来的dataframe

获取一列/多列

iloc的列操作其实不常见,因为平时写列操作用的基本上是列名(column name),很少用index来表示第几列

df.iloc[:, -1]可能有点用,可以在构建动态dataframe的过程中始终拿取最后一列

逻辑上和iloc获取一行/多行基本一致,只是换了个地方放置index

同样也要注意到返回series和dataframe的区别

df.iloc[:, 2] # 一列,返回series
df.iloc[:, [2]] # 一列,返回dataframe
df.iloc[:, [2,3,6]] # 多个离散的列拼成的dataframe
df.iloc[:, 0:2] # 连续的列拼成的dataframe

非iloc操作

使用列名获取一列或多列

获取多列:

使用loc获得一列或多列

实际上loc能获取行,但这里暂时不用管它,目前涉及到的所有dataframe都是用auto increment int作为索引下标的。

错误写法:

正确写法:

获得/更改 第x行yyy列的数值

这里的“第x行”容易引起歧义,可以被解释为row index,还可以被解释为物理意义上/iloc意义上的index:

row index

常用

目前2种方法:使用df.loc(实际上可以选择多行、多列的多个元素),或者使用df.at(只能选择一个元素),两者在“选择单个元素”这方面的用法相同:

df.loc[row_index, "column_name"],以及df.at[row_index, "column_name"]

比如要获取第0行,"name"列的数值

需要更改数值的时候直接写在df.at/df.loc后面即可:

df.at[row_index, "column_name"] = (value)

df.loc[row_index, "column_name"] = (value)

物理意义的index

如前面的笔记【来自iloc, loc和at的问题】所述,需要先用iloc,再用column name获取:

(下面的图片来自前面目录【来自iloc, loc和at的问题】的笔记)

其他loc/iloc用法

loc和iloc差不多就学到这里,其他用法以后需要了再补:

https://sparkbyexamples.com/pandas/pandas-difference-between-loc-vs-iloc-in-dataframe/

通过concat合并行/列

容易出现的问题

最容易出现的问题就是dataframe/series混在一起/分不清导致的结构混乱。尤其是在试图合并多行的时候,如果混入了pandas.series类型的数据,整个合并就会直接变成失败的类型。

合并多行(dataframe)

合并多行(按行合并多个结构相同的dataframe)

使用pd.concat([row_df1, row_df2, row_df3, ...]),这里row_df1, row_df2, ...是pandas.dataframe类型的数据

合并多行(series和dataframe)

首先要明确一点,就是:pd.concat([r1, r2, ...])的时候,r1一定要是dataframe类型的一行,而不能是series类型的一行。

错误示范

正确方法之一:把所有series变成dataframe(一行),然后再参与多行的concat

合并多列(dataframe和series)

注意:这部分内容和下面的目录【Dataframe合并List,List作为新的一行】本质上是在解决相同的问题

上面有关行合并的例子里,series和dataframe数据是不能混合在一起进行concat行合并的,但列合并就没有这些限制了:

使用pd.concat([col1, col2, col3, ...], axis=1),col1, col2, ... 既可以是series,也可以是dataframe

example:合并2列(2个dataframe)

example:合并3列(dataframe+series的混合)

example: 合并3列(全部都是series)

往df加List,List作为新的一列

如果前面有关【合并多行/多列/dataframe/series】的限制搞懂了,现在应该是可以很容易把python List(或者numpy array)类型的数据结构糊上来的。要注意,生成Series的时候最好指定name,这样合并以后就直接有column name了:

往df加List,List作为新的一行

常见错误

新的一行则要难一些,因为涉及到了很多新的内容,比如“列名指定问题”,首先,由一个List直接变为“一行dataframe”而不经过任何column name命名,这样的dataframe是无法正确合并到原本的df里面去的:

错误的方法

强行合并会带来这样的df:

正确方法1:指定dataframe column name

路线:List -> dataframe(指定dataframe column name) -> concat

正确的方法:使用list创建dataframe(一行)的时候指定column names,且与原本的dataframe保持一致:(这里有columns=df.columns的方法,可以直接指定所有列名,不需要一个一个自己去定义)

正确方法2:指定Series index

路线:List -> Series(指定Series index) -> Dataframe -> concat

pd.Series([v1, v2, v3, v4], index = ['A', 'B', 'C', 'D'])

其实相对于上面的方法(指定dataframe column names)还多了一层,使用的也是df.columns,所以一般不用这种方法

合并/过滤大量的列

假设我事先存好了一个List of column names,格式如下:

col_lst = ['Pclass', 'Name', 'Sex', 'Age']

现在我要基于这个col_lst过滤出一个仅包含这些column names的列。

方法1:直接暴力for循环 + concat:先构建一个空df,然后一列一列地往上面糊

方法2:使用df.filter()


再来一个例子:

场景:把所有numeric columns抽出来单独组成一个dataframe:

方法1:

步骤是,先构建一个空df,然后一列一列地往上面糊:

当然df.filter()可以更简单:

NaN判断

鉴于这部分内容经常容易写错,特意放在前面

(单个数值判断)使用pd.isna(object)

类SQL查询操作

select * where

select * from df_table where Sex='female' and Age>=48;

df[(df['Sex'] == 'female') & (df['Age'] >=48)]

如果是OR逻辑:

select * from df_table where Sex='female' or Age>=48;

df[(df['Sex'] == 'female') | (df['Age'] >=48)]

特别注意:使用&或者|逻辑符号时,两边的括号()不能省略!

df[ (df['Sex'] == 'female') | (df['Age'] >=48) ] # 正确
df[df['Sex'] == 'female'| df['Age'] >=48] # 错误

还有另一种基于loc的写法:

select (column1, column2) where

select Sex, Age from df_table where Sex='female' and Age>=48;

其实就是把上一条(select * where)的语句再包装一层column selection:

df[(df['Sex'] == 'female') & (df['Age'] >= 48)][['Sex', 'Age']]

select COUNT(*) where

其实就是把上面select * where的代码最外面包一层len()

len(df[(df['Sex'] == 'female') & (df['Age'] >= 48)])

select distinct(查找一列/多列的不重复元素)

简单统计很有用

使用groupby,但目前暂时不需要复杂的groupby用法,只需要:1,获取distinct values;2,获取COUNT(distinct values)

通过df.groupby(['xxxx']).groups获取的是一个dict,其中key是那些不重复的元素,value是这些元素出现的index

转换成list类型也可以:

[k for k in df.groupby(['Sex']).groups.keys()]

groupby里面可以填入不止一个column name:

group by

实际上在上一个目录里已经说了使用pandas groupby,如果只是想跑程序的时候中途临时看看结果,也可以使用value_counts():

order by

select * from df_table where Sex='female' and Age>=48 order by Age DESC;

分了2步骤,第一步和上面的一样,第二步排序需要用到sort_values()

假设我现在手里只有按照Age DESC排序好的df_filtered,我想重新把它按照row index排列:

使用sort_index()即可:


LIKE %xxx% 模糊查询(StringMethods)

select * from df_table where Name like '%James%';

使用 .str 把Series数据转换为StringMethods,然后接常见的string判断方法:


LIKE %xxx% 模糊查询(apply lambda)

这里其实提前用到了apply lambda方法,但实际上这不是apply lambda最常见的使用场景。更常见的使用场景在后面(使用lambda批量处理行/列的元素)

lambda方法:对每一行的指定列元素进行筛选:

还是上面的sql

select * from df_table where Name like '%James%';

这里用到了df.apply

axis=1看起来有点反直觉,目前只能强行记忆这个写法

还能用lambda实现前面的多重where AND查询:

select * from df_table where Sex='female' and Age>=48 order by Age DESC;

如果过滤的机制比较复杂,可以单独拿到一个function里面去写:

统计

统计某一列的平均/最大/最小

如果要用已经掌握的知识,可以用df->series->numpy array->np.average(), np.sum(), np.max(), np.min()等方法进行统计


统计整张表的所有列各自的平均数

使用df.mean或np.average

统计整张表的所有列各自的平均数(前提是此列为数字列)

统计每行也可以,但绝大部分场景下并没有什么用

当然我们也想用自动化的方法把这些数字列都抽出来给numpy进行进一步的统计,而不是用df.mean():

这里参考了🔗 [python - How do I find numeric columns in Pandas? - Stack Overflow] https://stackoverflow.com/questions/25039626/how-do-i-find-numeric-columns-in-pandas

针对特殊类型

有的时候我们可能会读到这样的csv:

这种情况下df.mean会直接跳过这一列。目前看来唯一的解决方案是:写一个自定义函数,一个一个cell去读。

下面的代码仅仅是一个例子,并不怎么通用,因为有的时候还要判断更多东西,有的时候可能不需要这么复杂,总之遇到问题可以先debug,看看是什么奇怪的数值导致程序出问题了,然后根据这个特殊的数值单独加入特殊的判断代码。只要能跑出结果就行。实在不放心可以用一个正常的csv测试一下(看看这个函数处理正常的float64 column是否和df.mean结果一样):

column dtype判断

介绍

在上面统计每列各自平均数的笔迹当中,为了找出所有的numeric columns而使用了df.select_dtypes()方法,这里还是有必要再补充一点对column dtype的判断方法

在本篇笔记稍早一些的地方(series转dict的那部分)学到了用dict(df.dtypes)存储每列的dtype:

现在我们要对这个dict的key/value进行dtype判断:

方法1:pandas.api.types

方法1:使用pandas.api.types.is_xxxx进行判断:🔗 [pandas.api.types.is_object_dtype — pandas 2.2.2 documentation] https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.api.types.is_object_dtype.html

优点:对大部分场景都省事省力(比如is_string_dtype会帮你检查这一列的objects是否全都是string类型):

方法2:dtype==np.xxx

方法2:使用dtype==np.int64(等)进行判断,其中object(也就是dtype('0'))需要用np.object_进行判断:

增加(空白的)一行/一列

新增一列(空列)

目前用这个就行:

df['new_column'] = pd.Series()  # 可以限制dtype,见稍后的内容

限制dtype有的时候有作用(比如声明了dtype=float64,然后往里面插string就会警告/以后会报错),有的时候没作用(声明了dtype=int64以后依然可以往里面插float数值,而且插入的float数值不会变成int)

新增一行(空行)

这里只考虑简单情形:df的row index按照自然数0, 1, 2, ...排列,没有跳跃(比如从100跳到103)

另外:上面这种用df.loc的写法其实自由度比较大:

为了以防万一,可以在写这句话的前/后 重排row index:

删除行/删除列

删除行/删除列,本质上也是一种查询,因为df.drop()和df.filter()并不直接修改df

使用df.drop()

写在前面:df.drop()可以看成df.filter()的相反作用

需要注意的是,因为df.drop()并不修改df,而是返回另一个df,所以不需要事先deepcopy备份原本的df. df.filter()也是这样:

删除列,使用axis=1

删除行,使用axis=0

需要特别注意的是,

df.drop([0, 1, 2, 3, 886], axis=0)

实际上去掉的是index为0, 1, 2, 3, 886的这几行,而不是第0, 1, 2, 3, 886行(见本篇笔记的前面部分:row index)

所以第二次执行这句话的时候就会出问题(因为index为0, 1, 2, 886的这几行已经被删掉了)

那如果我就是想删掉“iloc意义上的第0行”,应该怎么做?

配合df.index:


使用df.drop()和df.filter()实现之前的一个要求:

把所有numeric columns抽出来单独组成一个dataframe;把所有non-numeric columns抽出来单独组成一个dataframe:

当然这个df.drop()在这里还可以使用df.filter()替换,结果刚好反过来:

df.apply(lambda)

对一整列的每个元素都进行操作

简单的操作不需要用到lambda

复杂点的操作可能就需要了:

对一整行的每个元素都进行操作(不常用)

(这里仅仅为了演示lambda的用法,实际操作效果没有实际意义)对每一行row的所有元素x都执行x = x*2(如果是string就复制一份,比如"abc"变成"abcabc",如果是数字就乘以2,比如44变成88)

要用到2个lambda

或者还可以直接用df.map():

筛选特定的列(不常用)

似乎没啥意义,反正我看不出相比于df.loc它有什么独特价值


 Last Modified in 2024-11-28 

Leave a Comment Anonymous comment is allowed / 允许匿名评论