文章内容导引
五、dict、series以及dataframe三者之间的转换
5.1 dict/series/dataframe的数据形式
一、前言
作为一名运筹优化算法小白,在实习期间主要基于python语言开发一个自动化智能排程项目的算法,在开发定制化的元启发式算法过程中,数据预处理与清洗、数学模型的抽象化以及调用OR-tools求解器建模占了大头。而在使用pandas库处理数据过程中,一开始用的时候要么忘记对应的用法,要么理解错误某些方法和对象的底层运行逻辑,也是因为自己的不熟练,走了贼多弯路,写代码半小时,小问题debug代码花半天。最近算法部署上线稳定后,也想抽空来梳理梳理常用的pandas库用法以及遇到的小坑,希望看到的小伙伴能少走些弯路。
注:1)下文仅零散记录自己在编算法过程中用到的一些pandas库用法,若要系统的学习pandas库的使用语法,请自行查阅其他参考资料和相关书籍;2)本文的阅读需要读者熟悉python的基础语法,以及对numpy、series以及dataframe等数据类型有一定了解,没了解过的小伙伴可以先自行查阅资料学习相关的概念和定义声明方式。
二、数据读取与导出
在解决实际问题过程中,一个问题的相关信息大多可以整理成excel格式宽表信息,每一行代表某个需要决策的元素(如TSP问题中的一个客户点),每一列即该决策元素相关信息(如客户点坐标,需求等等)。而在编程建模前,往往就需要读取这些宽表信息,并将其中的信息抽象为数学模型的中的矩阵形式,进而开展建模编程,以下给出了python读取excel和导出excel的相关用法,其中hdf5是一类高维的数据存储格式,在此不作详细介绍,有兴趣的小伙伴可自行学习。
input_Data1 = pd.read_excel() # 从 Excel 文件中读取数据,括号中填写文件路径,只有文件名时,默认读取当前py文件所在路径中的指定文件,若不存在则会报错
input_Data2 = pd.read_hdf() # 从hdf格式的文件读取数据--需要引入第三方库 pip install tables,入参包括:1)hdf5文件所在路径,2)hdf5文件下存放具体数据的路径
output_Data.to_csv() # 将数据写入 CSV 文件,入参中填写文件路径,文件名称,没有文件路径时,默认导出到和当前py文件相同路径下的文件夹
output_Data.to_excel() # 将数据写入 Excel 文件--需要引入第三方依赖库:pip install openpyxl xlrd xlsxwriter
# 导出文件具体例子:将文件名为“test_data_improve”的dataframe数据对象导出到当前.py文件同目录下,导出的excel中,不展示每一行数据在dataframe中的行索引
test_data_improve.to_excel("test_data_improve.xlsx",index=False)
# 读取文件具体例子:从当前.py文件同目录下读取文件名为“temp_data4.xls”的excel文件,并定义为test_data
test_data = pd.read_excel('temp_data4.xlsx')
# 读取高维数据文件hdf5中某个路径下存放的excel文件
raw_data = pd.read_hdf(algo_data.store, algo_data.raw_data)
- to_excel() 的括号中通常传入2个参数,即:1)文件导出路径(包含文件名);2)index=True/False,其中index=True,代表导出的文件展示数据的行索引号,index=False代表不展示,即下面图片中红框框出来的第一列索引值。
- 默认导出路径为主程序py文件所在的目录,当需要修改导出文件的路径时,笔者习惯性地封装了如下的简单函数来方便dataframe对象的输出
def output_data(algo_path, data, file_name):
'''
将算法过程中的输出结果,默认存放到指定的临时文件夹中
:param algo_data: 路径
:param data: 导出数据
:param file_name: 导出的文件名
:return:
'''
store_path = os.path.join(algo_path, file_name)
data.to_excel(store_path, index=False)
- read_excel()默认读取当前py文件所在同目录下的文件,若文件不存在,则会报错,若需要读取不同目录下的文件,则需要在文件名前增加对应的路径,此时可以封装一个如下函数来读取某个路径下某个文件名的excel文件
def read_data(algo_path, file_name):
'''
读取指定路径下的excel文件,并返回dataframe
:param algo_path:文件所在的路径
:param file_name:文件名称
:return:
'''
store_path = os.path.join(algo_path, file_name) # 将路径和文件名相连,形成读取路径入参
df = pd.read_excel(store_path) # 读取对应路径下的对应文件
return df
Note:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- 1)执行带有to_excel()的程序时,在导出路径下,与导出对象同名称的.xlsx文件不能开着。
- 2)执行read_excel()函数时,对源文件不作修改的情况下,调试时打开文件不影响程序执行。
三、数据概要信息
将excel中的信息转化为dataframe数据结构后,我们可以使用如下的方法来快速获得该数据列表的相关摘要信息。
data = pd.read_excel("input_data.xlsx") # 读取excel数据
data.head() # 查看数据框的前几行,默认为5行,括号中为查看数据的行数
data.tail() # 查看数据框的后几行,默认为5行,括号中为查看数据的行数
data.shape # 获取数据框的行数和列数,得到一个包含行数和列数的元组
data.shape[0] # 获取该表的总行数,不包括列头的那一行
data.shape[1] # 获取该表的总列数,不包括行索引的那一列
data.describe() # 生成数据的统计摘要信息
data.info() # 显示数据框的基本信息
四、数据清洗和预处理
为甲方开发智能决策系统时,算法的输入数据往往来自于客户业务系统的数据库,其中不免存在一些“脏数据”:譬如某一列的字段数据类型与预期不一致,某一列必填字段存在缺失值,这些脏数据的存在很有可能会导致后续的代码运行报错,因此最好在算法开发之前,建立数据清洗的预处理模块,将导入的源数据(raw_data)转变为算法开发过程中便于检索的基础数据(basic_data)。此外为方便算法开发,往往也需要修改导入数据的列名,或截取其中的部分列名字段即可(譬如笔者拿到的基础视图有将近上百列业务字段,但实际可能只需要其中二十多列的字段)。
针对上述应用背景,笔者在数据清洗和预处理环节主要用到的一些函数和使用方法如下,供读者参考~
4.1 修改某列的数据类型并处理缺失值
- 利用.astype()方法统一某一列字段的数据类型,防止后续出现数据类型报错
- 转化为float后,可以将空值替换为'0'或者空字符串,可作为缺失值处理的一种简便方式
- 若不进行上述方式处理时,当dataframe是由.xlsx文件导入时,空值将被dataframe默认处理为"nan/NAN",极有可能对后续的数据统计和处理带来影响
# 示例一:
temp_data['columnA']= temp_data['columnA'].astype(float) # 将对应的列均专转化float格式
temp_data['columnA']= temp_data['columnA'].str.replace(' ', "0") # 将空值转化为字符串“0”
# 示例二:
temp_data['备注'] = temp_data['备注'].astype(str).fillna('')
4.2 删除列/行数据
(1)根据自定义条件删除对应的行数据
- 删除A列值=T1,B列值=SG4的行向量数据
-
为了让原表发生更改,下文的代码将删除后的结果重新赋值给原表。如果不希望改变原表,可以将删除后的结果赋值给一个新的变量
-
使用.drop()函数时,不改变源对象(只是生成一个副本对象,不进行原地修改)
data = data.drop(data2[(data2['A'] == "T1") & (data2['B'] == "SG4")].index)
(2)根据列名删除列数据
- 删除原来dataframe对象中,列名=columnA的那列数据
- 删除列数据时,需要指定axis=1,表示对列向量进行删除
df = df.drop('columnA', axis=1)
4.3 判断某一列是否存在缺省值
- 使用.isnull()/.isna()与.any()函数来快速判断某一列是否存在空值
import pandas as pd
# 创建一个DataFrame
data = {'A': [1, 2, 3, None, 5],
'B': ['a', 'b', 'c', 'd', 'e']}
df = pd.DataFrame(data)
# 判断列'A'是否存在空值,或采用df['A'].isna().any()
if df['A'].isnull().any():
print("列'A'存在空值")
else:
print("列'A'不存在空值")
4.4 预处理列数据字段,并修改某一列的所在位置
(1)读取指定列的数据并作列名转换
- 由于业务系统中源data的列名字段过多,当我们开发算法过程中发现仅需要提取其中某些列的数据时,可采用如下方式进行指定列的提取和列名字段的映射转换:
- 笔者建议new_columns这类涉及到业务字段与算法开发字段进行字段名映射关系的内容,可以单独写到一个json文件中进行维护,方便对接接口开发的同事参考或自己进行日常维护改动。
name_list = ['A','B','C','D','E','F']
temp_data = temp_data[name_list] # 只提取指定列
new_columns = {'A':"姓名",'B':"年龄",'C':"性别",'D':"身高",'E':"体重",'F':"学历"}
temp_data.rename(columns=new_columns, inplace=True) # 进行列名字段的转换
(2)修改某一列的排序位置
- 作为一名强迫症患者,笔者在数据处理过程中,偶尔希望某个字段排在某个字段前/后,方便在输出该dataframe时,易于人工审查算法的求解结果,故笔者封装了如下的函数特用于某一列的位置转变,供小伙伴们参考~
- Note:如果想一开始就对源列表的字段名排列顺序,只需要在上文的name_list中将需要用到的列名按照指定的顺序进行排列即可
def change_column_location(input_data,change_demand):
'''
改变给定dataframe中,某一列数据的所在位置,将指定列插入到第change_idx之前
:param input_data: 给定的原始数据
:param change_demand: 传入一个dict,key为需要改变的列名字段,value为插入的列索引
:return:
'''
for column_name,change_idx in change_demand.items():
first_column = input_data.pop(column_name)
input_data.insert(change_idx, column_name, first_column)
return input_data
五、dict、series以及dataframe三者之间的转换
笔者在使用pandas库对数据进行预处理、分析、统计、排序等操作的过程中,各类操作返回的对象或需要生成的对象基本在dict、series以及dataframe这类对象直接进行转换,刚开始按照算法设计框架进行数据处理和代码层面的问题抽象时,时常因搞混当前新赋值对象是什么类型而出bug,亦或将dict/series的对象合并到dataframe中而出错,在经历过一段时间的尝试和试错后,也有了一些理解和体会。
5.1 dict/series/dataframe的数据形式
为便于读者理解和区分三类数据对象,笔者将其特点总结如下:
- dict:以{key:value}形式存储的键-值对,可直接通过 对象[键值] 的方式来索引值
- series: 本质以[{key1:value1},{key2:value2},....] (附带属性name=column,代表该列值索引的内容) 的形式存储一系列的键值对,同样可通过 对象[键] 来索引值,但与dict不同的是,series的键往往为行索引/行标签,属于一维数据。
- dataframe:本质上是将一系列的series进行拼接,按照相同的key值进行了组合,形成类似于[{key1:[value1,value2,....],{key2:[value11,value22,.....]},{key3:[value111,value222,value333,.....]},....],其中key1,key2,key3 可以视作一份excel中的列A,列B,列C,[value1,value2,....]即代表列A这一列的值
- 上述举例中,key和value均指代某一个元素的属性和对应的值,而单个key对应多个value值则指代一个行向量,即 key1:[value1,value2,....] 的形式
- 为了说明dict、series以及dataframe的数据区别和转化过程,笔者基于如下的例子进行示意
# dict格式对象,此时key为列名,value为列向量值
column1 = {'A': [1, 2, 3, 4, 5]}
column2 = {'B': ['a', 'b', 'c', 'd', 'e']}
column3 = {'C': [0.5,0.4,0.6,0.7,1.0]}
# dict格式对象,此时key为列名,value为向量值
test_dict = {'姓名':"nancy",'年龄':26,"身高":100,'体重':150,'性别':"男",}
# series格式对象
series1 = pd.Series([1, 2, 3, 4, 5], name='A')
series2 = pd.Series(['a', 'b', 'c', 'd', 'e'], name='B')
series3 = pd.Series([0.5, 0.4, 0.6, 0.7, 1.0], name='C')
test_ser = pd.Series(test_dict)
print(series1)
print(test_ser)
# dataframe格式对象
c1 = pd.DataFrame(column1)
c2 = pd.DataFrame(column2)
c3 = pd.DataFrame(column3)
df = pd.concat([c1,c2,c3])
test_df = pd.DataFrame(test_ser).T
print(df)
print(test_df)
# 从dataframe取出其中第一行数据
temp_series1= df.iloc[0]
print(temp_series1)
# 从dataframe取出其中第一列数据
temp_series2 = df1.iloc[:,0] # 方法一获取第一列
temp_series2 = df1.loc[:, 'A'] # 方法二获取第一列
print(temp_series2)
代码的运行结果如下,可以得出以下几点结论:
- series的行标签既可以为int类型,也可以为str(即列名的key值)
- 无论从dataframe获取一列还是一行的数据,返回对象均为series,但对应Name属性不相同
- 从dataframe获取一列数据时,需要用行的整数位置来索引对应的数据;而当获取一行数据时,则使用列名来索引对应的数据,此时用法等价于一个dict对象。
# series格式输出
0 1
1 2
2 3
3 4
4 5
Name: A, dtype: int64 # 注意此时使用 series1[] 来索引值时,只能使用int类型的行索引来取值
姓名 nancy
年龄 26
身高 100
体重 150
性别 男
dtype: object # 此时使用 test_ser[] 来索引值时,可直接使用[key值]的方式来索引取值
# dataframe格式输出
A B C
0 1 a 0.5
1 2 b 0.4
2 3 c 0.6
3 4 d 0.7
4 5 e 1.0
姓名 年龄 身高 体重 性别
0 nancy 26 100 150 男
# 从dataframe取出其中的第一行数据--series对象,Name=0(行索引),行标签为原来dataframe的列名
A 1
B a
C 0.5
Name: 0, dtype: object
# 从dataframe取出其中的第一列数据--series对象,Name=‘A’(列名),行标签为原来dataframe的行索引
0 1
1 2
2 3
3 4
4 5
Name: A, dtype: int64
5.2 三种类型之间的转换方式
(1)将多个dict,合并到一个dataframe对象中
column1 = {'A': [1, 2, 3, 4, 5]}
column2 = {'B': ['a', 'b', 'c', 'd', 'e']}
column3 = {'C': [0.5, 0.4, 0.6, 0.7, 1.0]}
# 方法一:dict单独转成dataframe后再进行合并
c1 = pd.DataFrame(column1)
c2 = pd.DataFrame(column2)
c3 = pd.DataFrame(column3)
df1 = pd.concat([c1, c2, c3], axis=1) # 按照列合并
print("df1 如下:\n", df1)
# 方法二:转换为series后,再置于list后进行集中转换
series1 = pd.Series(column1['A'], name='A')
series2 = pd.Series(column2['B'], name='B')
series3 = pd.Series(column3['C'], name='C')
s_list = [series1,series2,series3]
info = []
for i in range(3):
info.append(s_list[i])
df = pd.DataFrame(info).T
print("df 如下:\n", df)
代码运行结果如下:
# 方法一输出结果>>>>>>>>>>>>>>>>
df1 如下:
A B C
0 1 a 0.5
1 2 b 0.4
2 3 c 0.6
3 4 d 0.7
4 5 e 1.0
# 方法二输出结果>>>>>>>>>>>>>>>>
df 如下:
A B C
0 1 a 0.5
1 2 b 0.4
2 3 c 0.6
3 4 d 0.7
4 5 e 1.0
(2)将一个dict/series对象合并到一个dataframe对象中(为data添加一行数据)
- add_row 为一个dict对象,key值与待合并的dataframe对象的列名相同,但需要注意的是,使用此方法时,value值不能为列表,否则转化为dataframe对象时,将被识别为多行数据,此时令index=[1] 将引发程序报错。
- 方法一中:index=[1]代表将给定的dict转化为一个dataframe,其中行向量的行索引属性为1 (可以为任意int值),此时列名为原dict的key值,第一行的数据为对应的value值
- 方法二中:to_frame()方法将Series对象转换为DataFrame对象,然后transpose()方法对该DataFrame对象进行转置操作,即将行和列进行互换。这样,原本作为Series对象中的数据将被转换为一个行向量,而列名将成为DataFrame的索引。
- 方法一更适用于字典中,value值均为单一float或string类型的对象,将其转化为dataframe;当value值出现list时,笔者建议采用方法二的方式,或采用(1)中将多个dict转化为dataframe的方法进行转化
add_row = {"A": 111, 'B': 'f', 'C': 0.9999}
# 方法一:dict直接转化为dataframe后进行合并
new_df = pd.DataFrame(add_row ,index=[1])
df1 = pd.concat([df1, new_df])
print(new_df)
print(df1)
# 方法二:dict转化为series后,再转化为dataframe进行合并
add_row = pd.Series(add_row)
print(add_row)
add_row = add_row.to_frame().transpose()
# 或者采用 add_row = add_row.to_frame().T
print(add_row)
df1 = pd.concat([df1, add_row])
print(df1)
代码结果如下:
- 很显然将dict转化为行数据添加到dataframe使用方法一更直接,此处笔者特意阐述方法二,旨在让读者同时也了解series合并到dataframe中的转换方式
# 方法一运行过程>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# dict直接转化一个dataframe对象
A B C
1 111 f 0.9999
# 将该行数据与原来的dataframe进行合并的结果
A B C
0 1 a 0.50
1 2 b 0.40
2 3 c 0.60
3 4 d 0.70
4 5 e 1.00
1 111 f 0.9999
# 方法二运行过程>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# dict转化为series对象
A 111
B f
C 0.9999
dtype: object
# series对象再转为dataframe对象
A B C
0 111 f 0.9999
# 最后进行dataframe合并
A B C
0 1 a 0.50
1 2 b 0.40
2 3 c 0.60
3 4 d 0.70
4 5 e 1.00
0 111 f 0.9999
(3)将dataframe转换为dict对象
- dataframe转换为dict对象本质为提取其中的某一行/或一列数据,其转换过程仍然是通过先得到series对象,再转换为dict对象
- 从dataframe提取行数据/列数据的方法和介绍详见第六节~
series_obj = df1.iloc[0] # 取dataframe中的第一行数据,得到的是series,行标签为列名
dict_obj = dict(series_obj) # series转化为dict,或者使用dict_obj = series_obj.to_dict()
print(dict_obj)
series_obj = df1.iloc[:,0] # 取dataframe中的第一列数据,得到的是series,行标签为int类型的索引位置
dict_obj = dict(series_obj) # series转化为dict,或者使用dict_obj = series_obj.to_dict()
print(dict_obj)
代码结果如下:
# 行向量转化为dict的结果,key为列名,value为值
{'A': 1, 'B': 'a', 'C': 0.5}
# 列向量转化为dict的结果,key为行索引,value为值
{0: 1, 1: 2, 2: 3, 3: 4, 4: 5}
六、数据提取、索引与筛选
使用pandas库筛选、分析数据的过程其实和使用office软件去处理excel数据是类似的,其区别主要在于前者将各种需要人工逐步点击的数据筛选、提取、过滤或者计算公式的操作全部通过代码实现了自动化处理。下文给出了笔者在进行数据提取或筛选时常用的一些方法和注意要点。
6.1 iloc[] 按照行/列位置索引获取数据
data.iloc[]常用于按照行/列索引来获取指定行、指定列的数据,得到一个新的dataframe对象或series对象,笔者常用的场景如下:
- 使用data.iloc[x,y]的方式即可获取dataframe中行索引为x行,列索引y列的某个值
- 使用data.iloc[x]的方式即可获取dataframe中行索引为x的行数据,返回的是series对象
- 使用data.iloc[:,y]的方式即可获取dataframe中第y列的列数据,返回的是series对象
- 使用data.iloc[x:y]的方式即可获取dataframe中行索引从x到y行的数据,返回的仍然为dataframe对象,这一点同列表的切片操作类似,是左闭右开的。
- 使用data.iloc[[x1,x2]:[y1,y2]]的方式即可获取dataframe中第x1~x2行,y1~y2列的数据,返回的仍然为dataframe对象,这一点同列表的切片操作类似,是左闭右开的。
注意>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- 1)x和y均需要为int类型,否则会报错
- 2)x和y分别为dataframe中的行索引和列索引,但不完全等价于该数据在整个dataframe中的第x行和第y列(绝对位置),因为行索引index是可以修改的,故处理数据时,一定要注意整个列表的行索引是否是从0开始递增的,通常可以用data.reset_index(drop=True,inplace=True)来重置索引;其中当drop=False时,将自动生成一列(列名=index)用于记录重置索引前每一列数据的行索引
- 3)iloc[x,y]中的x和y必须是该dataframe内存在的行索引和列索引,否则会出现索引越界报错
data = pd.read_excel("input_data.xlsx") # 读取excel数据
item1 = data.iloc[0] # 取该DataFrame的第一行数据,返回对象为series格式,key为对应数据的列明,value为对应的值
item2 = data.iloc[-1] # 取该DataFrame的最后一行数据,返回对象为series格式,key为对应数据的列明,value为对应的值
item3 = data.iloc[0,0] # 取该DataFrame中第0行,第0列的数据(其中0行为除列名外的第一行)
data2 = data.iloc[1:5] # 取该DataFrame中第1行到第4行的数据,生成一份新的数据对应data2
6.2 loc[] 按照行/列标签索引来获取数据
data.loc[row_label,column_label] 方法常用于按照指定“标签”去提取数据,其中的row_label和column_label可以是单个标签、标签列表、布尔数组、切片等,分别用于指定行和指定列的位置;使用loc[]函数时,需要注意传入的行标签和列标签必须存在,否则会报错。具体的用法如下:
import pandas as pd
# 创建一个DataFrame
data = {'Name': ['Alice', 'Bob', 'Charlie', 'David'],
'Age': [25, 30, 35, 40],
'Gender': ['F', 'M', 'M', 'M']}
df = pd.DataFrame(data)
# 使用loc[]提取指定行和列的数据
print(df.loc[1, 'Name']) # 提取第2行,Name列的数据,输出Bob
print(df.loc[1:2, ['Name', 'Age']]) # 提取第2行到第3行,Name和Age列的数据
笔者将自己使用loc[]提取数据的高频场景主要有以下两种,供小伙伴们参考:
1)用法一:批量修改某一字段下,符合指定条件的数据
- 当需要综合某一列/某几列的值,来改写另一列的值时,可通过如下方式进行批量修改:
# 根据"后工序"列的值,批量修改"集批后工序"列的值
# 底层逻辑为:利用loc[row_label,column_label]提取到对应的“单元格”,然后将其修改为“s1”
temp_data.loc[(temp_data['后工序'] == 'S1'), '集批后工序'] = 's1'
# 根据"后工序"以及"镀层类型代码"列的值,批量修改"集批后工序"列的值
temp_data.loc[(temp_data['后工序'] == 'D1') & (temp_data['镀层类型代码'] == 'ZF'), '集批后工序'] = 'D1-ZF'
2)用法二:获取符合指定条件的行向量索引
- 当需要对比两份dataframe,相同元素的行索引是否一致时,可通过如下的方式进行判断:
def compare_sequence(self,algo_plan,formal_plan):
'''
对比algo_plan与formal_plan两份数据,判断其中相同MAT_NO的数据索引是否相同
'''
task_1 = algo_plan[algo_plan['MAT_NO'].isin(formal_plan['MAT_NO'])]
task_2 = formal_plan[formal_plan['MAT_NO'].isin(algo_plan['MAT_NO'])]
task_1.reset_index(drop=True,inplace=False)
task_2.reset_index(drop=True, inplace=False)
task1_list = list(task_1['MAT_NO'])
task2_list = list(task_2['MAT_NO'])
for i in range(task_1.shape[0]):
if task1_list[i] != task2_list[i]:
# 前提:每个元素(每一行数据)的MAT_NO标签是唯一的
indices = task_2.loc[task_2['MAT_NO'] == task1_list[i]].index[0]
if i < indices:
move = ">>>向后移动"
else:
move = "<<<向前移动"
- 需要注意的时,使用loc[]方法去提取数据时,通常返回一个dataframe对象,因此使用.index方法时,得到的为一个包含符合条件行的行索引列表(尽管可能只有1个元素),因此还需要用.index[0]的方式来取出所需的行索引
3)用法三:获取指定条件的行数据
- 用法三与用法二类似,均使用loc[]运算来提取满足指定条件下的dataframe对象
- 当我们需要基于某个唯一标识字段,去获取另一个dataframe对象中的行数据时,可采用如下的方法来获取到对应的一行数据(series格式)
- 在如下的代码中,通过data1.loc[]的方式,提取满足指定条件的dataframe对象,再利用iloc[0]的方式获得该dataframe中的首行数据,即可得到对应的series对象
- 使用该方法时,需要注意另一个被查询的dataframe对象中(data1),待查询标识("MAT_NO")必须是唯一的,否则可能无法取到唯一的那行期望数据
item_i = data1.loc[data1['MAT_NO'] == mat_no2].iloc[0] # 找到对应的行数据
6.3 使用[]运算符自定义筛选条件获取数据
- 使用[]方法提取指定条件的数据逻辑同我们使用excel中的“筛选”功能,并且更加的灵活和方便,代码如下:
# ori_data为给定的源数据,假定该数据有“A”、“B”、“C”、“D”列
# A列、B列为int格式,C列、D列为str格式
# 则通过下列语句可以筛选同时满足:1)A列值>=min_thick;2)B列值<=max_thick;3)C列值等于‘value’;4)D列的值属于require_material(列表)
temp_data = ori_data[(ori_data['A'] >= min_thick)
& (ori_data['B'] <= max_thick)
& (ori_data['C'] == 'value'])
& (ori_data['D'].isin(require_materail))]
# 假如我们要提取data的第2、3行和从Price到Sales对应的列 (连续的列,用切片操作)
new_df = data[2:3,'Price':'Sales']
# 假如我们要选取所有的行和Fruits和Sales对应的列
new_df = data[:,'Price':'Sales']
注意>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- []中不同的条件表达式之间,表示“与”逻辑运算使用“&”连接,表示“或”逻辑运算使用“|”连接,若使用'and'或'or'会报错
- 使用data['column'].isin()的方法来筛选对应列中,值属于某几个枚举项的数据
- 切片操作与list的切片类似,但使用[]去提取对应行和列的数据时,为左闭右闭区间,同时列名“标签”之间必须是连续的
6.4 数据提取和筛选方法小结
- iloc[]利用行索引和列索引来提取指定数据,[]中只能为int类型,输入的行列索引之间用“,”分隔
- loc[]利用行标签和列标签来提取指定数据,[]中可以为标签/布尔判断语句
- 利用[]来自定义筛选数据的判断条件,并提取源数据的副本数据,且具有切片功能
- 提取数据和筛选数据可以灵活地结合上述三种方法来使用,但需要注意不同方法返回的对象格式,例如iloc[x]返回的是series,loc["条件表达式"](仅对行数据进行筛选)返回的是dataframe对象,调试代码时建议多利用type()来确定自己真正得到了什么格式的对象
七、数据统计与排序
7.1 基础信息统计
pandas库中有内置函数可以帮助我们直接统计得到某一列数据的基础统计信息,以及对某列数据进行累计求和,非常便捷!
# 常用的统计方法如下:
data[‘columnA’].sum() # 求列名为‘columnA’的总和
data[‘columnB’].mean() # 求列名为‘columnB’的平均值
data[‘columnC’].max() # 求列名为‘columnC’的最大值
data[‘columnD’].max() # 求列名为‘columnD’的最小值
假若想要对某一列的数据进行累加求和,可使用cumsum()函数得到某一列值的累计求和值。并通过“[]”运算符对数据按照某个累计值进行截断处理,如下所示:
# 对某列数据进行累加求和统计,并按照指定值进行数据截断
temp_data['重量累计值'] = temp_data['重量'].cumsum()
temp_data['长度累计值'] = temp_data['长度'].cumsum()
temp_data = temp_data[(temp_data['重量累计值'] <= wt_max) & (temp_data['长度累计值'] <= km_max)]
7.2 自定义排序
pandas库中还有内置的排序函数,能够按照指定列,指定排序方式对数据进行快速排序,但需要注意的是,该排序函数分“拷贝排序”和“原地排序”两种情况,区别在于指定的inplace参数是否为True,具体用法如下所示:
- by=[]中填写的字段顺序代表了排序优先级,即下面代码中先根据x进行降序,在对x列相同值的行数据,按照y值进行降序排序
# 如下展示了两种对数据进行排序的方式,均实现了:将整个表格中的数据按照x,y列的值,进行降序、降序排序
# 第一种排序方式下,源数据并不会改变,而是生成了一个排序后的副本,将其重新赋值给了data
data = data.sort_values(by=['x','y'],ascending=[False,False])
# 第二种排序方式下,源数据会发生改变,即进行了原地排序
data.sort_values(by=['x','y'],ascending=[False,False],inplace=True)
# 注意:上述排序完毕后,数据的行索引也会跟着移动改变!
八、数据的聚类统计:groupby()函数的应用
在数据统计和分析过程中,聚类统计/提取数据往往是使用频率最高的处理方法,而pandas库中自带内置函数groupby(),可以帮助我们快速地对整个DataFrame按照某些字段和统计方法进行聚类分组统计,该函数的基础使用语法如下:
- data.groupby() 对dataframe对象进行分组,此时返回对象格式为“DataFrameGroupBy”
- 利用[]运算符对指定字段进行同组内的数值统计
- 当传入的分组标签唯一时,得到的结果为一个series对象,key为分组的枚举值,value为对应分组内的指标统计值;当传入的分组标签为一个[]时,得到的结果为一个dataframe对象
- 通过上述方式得到的分组统计结果,可使用iloc[]方式逐行进行遍历,但只能得到对应行的统计结果。若需要查询对应的分组标签和分组统计值,建议使用.reset_index()将其转换为dataframe对象,利用iloc[]获得各分组的series对象后,再查询对应的分组标签和分组统计量
# 将源数据根据列“A”进行分组
group_data = data.groupby('A')
# 将源数据根据列“A”以及列“B”进行分组
group_data2 = data.groupby(['A','B'])
# 统计分组后C列数据之和
group_result1 = group_data['C'].sum() # group_result1为series对象,key为A列的值,value为根据A列字段分组后,不同分组的求和值
# 统计分组后D列数据的平均值
group_result2 = group_data['D'].mean() # group_result2为series对象,key为A列的值,value为根据A列字段分组后,不同分组的平均值
# 具体应用:根据集批后工序和排产小类将数据进行分组统计,并查询每一组的分组指标与对应统计量
data = pd.read_excel("test_data.xlsx")
group_df = data.groupby(['集批后工序','排产小类'])
print(group_df)
result_len = group_df ['入口长'].max().reset_index()
for index,row in result_len .iterrows():
print("index = {} 分组指标 = {} - {} 统计量 = {}".format(index,row['集批后工序'],row['排产小类'],row['入口长']))

使用上述方法进行聚类统计的局限性在于:1)只能使用dataframe自带的内置函数对数据进行统计;2)不能同时对不同列按照不同的统计逻辑进行分组处理;3)返回dataframe对象中只带有一组统计量的值。若要使用自定函数同时对多列数据按照不同的统计方法进行分组统计或处理, 可使用聚类统计更为灵活的.agg()方法,代码如下:
# 根据集批后工序字段进行聚类分组,并对“待排产量”列的值进行求和
data1 = data.groupby(['集批后工序']).agg({"待排产量": sum})
# 根据集批后工序字段进行聚类分组,并对“小类”列的字段用;进行合并
data2 = data.groupby('集批后工序').agg({'小类': lambda x: ';'.join(x)})
# 根据集批后工序与排产小类字段进行聚类分组,对“待排产量”的值进行统计求和,对焊接分组字段按照自定义函数group_set进行统计处理
data3 = data.groupby(['集批后工序','排产小类']).agg({"待排产量": sum,'焊接分组':group_set})
# 自定义的数据整合函数
def group_set(column):
class_big = '-'.join(set(list(column)))
return class_big
- groupby可以传入一个包含多字段名的[],即可根据不同列的字段进行分组
- agg的入参为字典对象,key为需要分组/统计分析的列名,value为对指定列进行数据统计/处理的函数
- agg()中传入自定义函数时,函数接收的默认传参为该列的数据,即series对象,key值默认为源数据中的行索引,values值即对应列的值
- 需要注意:通过上述方式返回的对象data1,data2和data3均为dataframe对象,其中行索引为分组的条件值,列索引为对应统计的指标,建议使用.reset_index()对dataframe进行索引转换,具体的差别如下:
- 此时再利用如下代码,即可得到指定分组下的指标统计值的字典对象(方便用key值索引):
group_info = dict(zip(data['集批后工序'], data['待排产量'])) # key值为分组枚举值,value为对应分组下的指标统计值
九、数据的自定义处理函数:apply()函数的应用
apply()函数主要用于对dataframe的数据按照自定义函数进行批量处理和筛选,其基本的用法如下,其中func为自定义的函数,也可以采用lambda x: func 的方式对对应的行数据或列数据应用自定的函数进行批量处理。
# 对每一列应用函数,生成副本dataframe对象后,赋值给data
data = df.apply(func, axis=0)
# 对每一行应用函数,生成副本dataframe对象后,赋值给data
data = df.apply(func, axis=1)
# 具体应用:根据每一行的数据(一个series对象)来定义每一行对应的物料是否属于紧急催货物料,其中check_in_urgent为自定义函数
temp_data['是否属于紧急催货物料'] = temp_data.apply(check_in_urgent,axis=1)
笔者使用apply()函数较多的场景主要有以下两类需求:
(1)用法1:自定义筛选条件
前文提及了利用[]来筛选条件,但[]中能定义的布尔表达式逻辑往往较为简单,当遇到需要基于某列值经过一定处理后的结果进行筛选时,即可结合apply()函数实现一步到位的数据筛选。
在下面的代码中,apply()函数的入参采用匿名函数函数的方式将一系列的条件表达式嵌入在一行代码上,使得代码更为简洁。下文展示代码实现的目的如下:
- 遍历指定的枚举值列表,每个元素定义为roll
- 遍历temp_data中的每行数据,取其中"可用辊期"的值,对其以逗号进行拆分,得到list
- 取每一行数据的“可用辊期”字段,同样以逗号进行分割,得到list
- 取roll元素拆分后的list和每行数据拆分后list的交集,判断其长度是否>0,若是则返回True,意味着该行数据符合筛选条件,否则返回False,表示不满足自定义的筛选条件
for roll in ['1,2', '4', '6', '7,8', '9', '10', '11', '12', '13']:
temp_roll_df = temp_data[temp_data['可用辊期'].apply(lambda x: True if len(set(roll.split(',')) & set(x.split(','))) > 0 else False)]
(2)用法2:批量为每一行数据打标签--基于某几列的字段按照某个操作执行后,返回结果
当我们需要基于一行数据中某几列的字段来为该行数据生成某个标签时,即可使用apply()方法+自定义匿名函数的方式一步到位实现数据打标签的目的,具体例子如下代码所示:
- “是否属于紧急催货物料”是笔者希望给每一行数据打上的标签名称
- 对应的值需要结合每一行的“合同号”字段一级"MAT_NO"字段来判断
- 通过使用apply()+自定义函数的方式来高效地为该dataframe新增一列字段
- 注意!!!当希望函数传入的参数为一行数据(series格式)时,apply()中除了传入自定义函数外,还需要令 axis=1,表示对行数据按照自定义函数操作
temp_data['是否属于紧急催货物料'] = temp_data.apply(self.check_in_urgent,axis=1)
def check_in_urgent(self,row):
if (row['合同号'] in self.params.urgent_order_NO) or (row['MAT_NO'] in self.params.urgent_MAT_NO):
return True
else:
return False
- 使用apply()函数时,也可根据其中的某几列字段应用apply()函数,示例如下:
- 底层逻辑与上面的处理方式一致,只是通过 temp_data[['EAD', '节点位置']] 提前提取了一个只包含EAD和节点位置两列字段的dataframe,用于应用apply()传入的匿名函数,同样需要令axis=1,即仍然是对行数据进行处理
temp_data['物料是否可用'] = temp_data[['EAD', '节点位置']].apply(lambda row: self.available_material(row['EAD'], row['节点位置']), axis=1)
十、数据的合并与重排序:merge()函数的应用
pandas库中的merge()函数主要用于将两个DataFrame对象按照指定的列或索引进行合并,其函数的指定入参有很多,下面简单罗列了笔者自己整理的关于merge()函数的入参和用法,方便读者理解merge函数的底层逻辑。
pd.merge(left, right, how='inner', on=None, left_on=None, right_on=None, left_index=False, right_index=False, sort=True, suffixes=('_x', '_y'), copy=True, indicator=False, validate=None)
-
left
:要合并的左侧DataFrame对象。 -
right
:要合并的右侧DataFrame对象。 -
how
:合并方式,默认为'inner',表示取两个DataFrame对象的交集;还可以取'outer',表示取两个DataFrame对象的并集;或者取'left',表示以左侧DataFrame对象的键为准进行合并;或者取'right',表示以右侧DataFrame对象的键为准进行合并。 -
on
:指定要进行合并的列名。如果希望指定合并的列在左右两个DataFrame中的列名不同,则可以分别使用left_on
和right_on
参数指定左右两个DataFrame对象的列名。 -
left_index
和right_index
:是否使用左侧或右侧DataFrame对象的行索引作为合并键。 -
sort
:是否按照合并键进行排序。 -
suffixes
:如果左右两个DataFrame对象存在相同的列名,可以使用suffixes
参数指定在合并后的列名上添加的后缀。 -
copy
:是否复制数据,默认为True。 -
indicator
:是否在合并后的DataFrame对象中添加一个特殊的列,用于指示每一行的合并方式。 -
validate
:是否检查合并的数据是否符合预期的合并方式。
而在笔者自己开发算法的过程中,实际使用merge函数的场景可归纳如下:
(1)利用merge()函数生成指定顺序的新dataframe
- 使用场景:假定一份data中,每一行数据有唯一的标识“MAT_NO”,当我们想要将这份数据按照某一个决策完毕的序列进行排序,生成一份新的dataframe对象时,即可以采用如下的方式得到排序后的序列
- decision_seq: 按照决策后的顺序存放不同元素MAT_NO这一列值
- input_data : dataframe数据全集
- MAT_NO: 每一行数据的唯一标识列(每一行数据的值都不相同)
-
在下面的语句中,input_data 为数据的全集,decision_seq为存放数据索引顺序的list,on表示合并两个数据集依据的列名(key值),通过执行下列语句,即可按照给定list中的标签排列顺序获取一份新的dataframe对象格式
new_df_sorted = pd.merge(pd.DataFrame(decision_seq, columns=["MAT_NO"]), input_data, on="MAT_NO")
(2)利用merge()函数直接复原源数据的列名字段
- 使用场景:假定data1为经过列名转换、数据筛选、过滤、排序优化后的dataframe,data2为源数据,data1和data2的列名字段存在差异,但标识每行数据唯一性的字段是相同的(譬如“MAT_NO”这一列),此时即可通过merge()函数,将data1的字段还原为源数据data2的字段,而每一行的顺序不变
- all_task_data: 排序优化后的dataframe对象,且列名与源数据不完全一致
- s1_pool_schedule:希望返回的dataframe对象,但希望列名与源数据一直,每一行数据排序与all_task_data一致
- ori_data:给定初始数据的全集
supply_list = ['顺序', "MAT_NO"]
s1_pool_schedule = all_task_data[supply_list] # 此时s1_pool_schedule为只包含"顺序"和"MAT_NO"两列的dataframe对象
s1_pool_schedule = pd.merge(s1_pool_schedule, ori_data) # 根据MAT_NO列,取两个数据集的行数据交集和列名的并集
踩坑点>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- 当不指定“on”参数时,需要merge的两份数据中不能存在“列名相同,值不同”的情况,因为merge的底层逻辑是默认将两份dataframe中相同列名,相同值的行数据取交集得到一份新的dataframe对象,当出现data1中列A与data2中列A的值不同时(即便两份数据中列B的值均相同),对应的行数据即不会出现在合并后的dataframe中,而导致数据合并错误
- 当遇到待合并的两份数据data1和data2中存在多个重复列时,但只希望依据其中某一列作为合并两份数据的依据时,并且后续还需要使用某一份数据中的值,此时则需要指定参数on和suffixes,具体的用法如下所示:
supply_list = ['顺序', "JOB_NO", "备注", "集批后工序", "供料匹配度"]
data1 = all_task_data[supply_list]
merge_data = pd.merge(data1, data2, on='JOB_NO', suffixes=("", "_y"))
- on="JOB_NO" 表示只根据data1和data2两份数据中JOB_NO列的值进行相同数据的合并,若该字段不存在于某一份数据的列名中,则会报错。
- suffixes=("", "_y") 表示在合并后的dataframe对象merge_data中,data1和data2的重复列在其中都被保留,但是data1的列名保持不变,data2的列名增加后缀"_y",此时再索引原来的某个重复列名时,不会出现索引得到的对象为二维数据的情况。
- 不指定suffixes参数时,假定data1与data2中有相同列名"集批后工序",则采用pd.merge()生成的merge_data中,将不存在"集批后工序"字段,而会生成"集批后工序_x","集批后工序_y"这两列字段,从而在操作 merge['集批后工序'] 时报错。
十一、all()、any()、isin()方法
all()与any()函数主要用于判断某一列数据中,是否存在某个值,或者是否都为某个值,具体的应用如下:
# 判断某一列是否存在True值,是的话,返回True
contains_true = df['A'].any() == True
# 判断某一列是否存在False值,是的话,返回为True
contains_false = df['A'].any() == False
# 判断某一列是否全为False,是的话,result 为True
result = df['A'].all() == False
除上述用法外,.all()和.any()还可用于判断两个numpy对象是否完全相等,具体应用如下:
# 排序前MAT_NO 列表,使用.values的获取存放MAT_NO这一列值的numpy对象
change_list_before = copy.deepcopy(need_sort_data['MAT_NO'].values)
# 利用某个自定义函数对need_sort_data(dataframe)进行了排序.......
# 排序前MAT_NO 列表,使用.values的获取存放MAT_NO这一列值的numpy对象
change_list_after = need_sort_data['MAT_NO'].values
# 判断排序前后MAT_NO这一列是否完全相等,完全相等返回True,否则返回False
result1 = (add_list_before == add_list_after).all()
# 判断排序前后MAT_NO这一列是否存在相等的值,存在返回True,否则返回False
result1 = (add_list_before == add_list_after).any()
而.isin()方法则专门用于检查DataFrame或Series中的元素是否包含在指定的列表(list)或数组(numpy)中,因此可以结合[]运算符进行指定条件的筛选,示例如下:
# 用法一、检查dataframe对象中某一列的每个元素是否包含在指定的列表中,返回一个series对象
df = pd.DataFrame({'A': [1, 2, 3, 4, 5],
'B': ['a', 'b', 'c', 'd', 'e']})
result = df['A'].isin([2, 4])
# 此时result可以理解为[(0,False),(1,True),(2,False),(3,True),(4,False)], 每一对括号代表行索引和对应的value值,result[0] = False
# 用法二、检查series对象的每个元素是否包含在指定的列表中,同样返回一个series对象,结果同上
series_obj= pd.Series([1, 2, 3, 4, 5])
result = series_obj.isin([2, 4])
# 在上述用法中本质上df['A']与series_obj是等价的,在使用.isin()时,只需要注意使用的对象格式即可
# 用法三、利用.isin()与[]运算符进行数据过滤,根据MAT_NO这一列字段取数据data1和数据data2的差集
new_data = data1[data1['MAT_NO'].isin(data2['MAT_NO'].values)==False]
十二、value_counts()函数的应用
values_counts()主要用于统计dataframe/series对象中各个枚举值出现的次数,返回一个series对象,其中行索引为对应的枚举值,value为对应统计的次数,且该结果会按照各个枚举值出现次数的大小进行降序排列,因此可以快速利用value_counts()来了解某一组数据的数值分布情况,
12.1 基础用法
# 统计单列中唯一枚举值的次数
alpha = pd.Series(['A', 'B', 'A', 'C', 'B', 'A', 'A', 'B'])
df = pd.DataFrame({'A': ['A', 'B', 'A', 'C', 'B', 'A', 'A', 'B'],
'B': [1, 2, 3, 1, 2, 3, 1, 2]})
alphg_count = alpha.value_counts()
df_count_A = df['A'].value_counts()
代码运行结果如下:
A 4
B 3
C 1
Name: A, dtype: int64
其中行索引为不同的枚举值,value值为对应枚举值的出现次数,可采用如下的方式对统计结果(series格式)进行遍历:
# 方法一:将series的行索引和对应的value转化为元组对,然后用for循环遍历
for row in alpha_count.items():
print(f'枚举值 = {row[0]} 次数 = {row[1]} ')
# 方法二:获取series的行索引列表,然后在for循环使用键-值对的方式来索引
for key in alpha_count.index:
print(f'枚举值 = {key} 次数 = {alpha_count[key]} ')
# 方法三:原理同方法一,转换为字典后,利用key,value进行索引
for key,value in alpha_count.to_dict().items():
print(f'枚举值 = {key} 次数 = {value} ')
除对单列的枚举值进行次数统计外,还可以对多列数据组成的枚举值对进行计数,示例代码和结果如下:
df = pd.DataFrame({'A': ['A', 'B', 'A', 'C', 'B', 'A', 'A', 'B'],
'B': [1, 2, 3, 1, 2, 3, 1, 2]})
value_counts = df.groupby(['A', 'B']).size()
print(type(value_counts))
print(value_counts)
for value in value_counts.items():
print(f'key = {value[0]} value = {value[1]} ')
代码运行结果如下:
<class 'pandas.core.series.Series'>
A B
A 1 2
3 2
B 2 3
C 1 1
dtype: int64
key = ('A', 1) value = 2
key = ('A', 3) value = 2
key = ('B', 2) value = 3
key = ('C', 1) value = 1
12.2 衍生用法
基于value_counts()的基础用法,笔者在数据处理和统计分析过程中的衍生用法归纳如下:
(1)判断某一列数据是否存在值
if df_sorted['备注'].value_counts().sum()>0:
print(“当前备注列有备注信息”)
(2)检测一份dataframe中是否存在重复列
d =pd.DataFrame([])
d['name'] = list(raw_data.columns) # 将源数据的列名存放到dataframe中
result = d['name'].value_counts() # 利用value_counts()统计每一个列名字段的数量
for row in result.items():
if row[1] >1:
print("列 {} 存在重复".format(row[0]))
else:
print("列 {} 是唯一的".format(row[0]))
(3)直接获得某一列中指定枚举值的出现次数
# 获取column列中,值="T1"的次数,若该列不存在"T1",则返回0
count = df['column'].value_counts().get('T1',0)
十三、格式化输出dataframe
作为一个强迫症患者,笔者对于控制台输出有一定格式要求,故上网查找了一些资料后,自行封装了一个用于对齐输出dataframe对象的小函数。
def print_pd(data):
'''
对齐打印输入的dataframe
:param data:
:return:
'''
pd.set_option('display.max_columns', None)
pd.set_option('max_colwidth', 500) # 设置列宽
pd.set_option('display.unicode.ambiguous_as_wide', True)
pd.set_option('display.unicode.east_asian_width', True)
pd.set_option('display.width', 1000) # 设置打印宽度
pd.set_option('display.unicode.east_asian_width', True) # 输出右对齐
print(data)
但当输出的dataframe中出现中英文混杂时,还需要对字体进行设置,才能让输出结果对齐,笔者的设置结果如下图所示。
十四、结语
Pandas库有着大量的内置函数和各种灵活的用法,本帖很难将其详尽罗列,之后若有时间还会接着梳理和补充。
由于笔者也是第一次发帖,若上述总结和梳理过程中有任何描述不当、笔误或代码错误之处,还请各位看到的小伙伴私信我进行纠正,非常欢迎各位的批评指正!
最后,本文所有的代码均为自身实践验证后所得,非直接搬运照抄(除部分示例使用ChatGPT3.5生成以方便示意),所有内容描述均为原创归纳,如需转载,请私戳笔者。