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差不多就学到这里,其他用法以后需要了再补:
通过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;
还有另一种基于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它有什么独特价值