Pandas

在计算机编程中，pandas是用于数据操纵和分析的Python软件库。它建造在NumPy基础上，并为操纵数值表格和时间序列，提供了数据结构和运算操作。它是在三条款BSD许可证下发行的自由软件。它的名字衍生自术语“面板数据”（panel data），这是计量经济学的术语，即包括了对同一个体在某个时期内多个时间点的观测的数据集。它的名字还可解释为对短语“Python data analysis”的玩笑。
历史.
2008年，原作者开始在AQR资本管理公司制作pandas，用来满足在财务数据上进行时，对高性能、灵活工具的需要。2009年，他在离开AQR之前，说服管理者允许他将这个软件库开放源代码。下面是其开发过程的时间线：
数据模型.
pandas的序列（codice_1）是一维的加标签数据结构，它能够持有任何数据类型，如整数、字符串、浮点数和Python对象等，轴标签在集体上称为索引（codice_2）。序列表现得非常类似于NumPy的codice_3数据结构，并且是大多数NumPy函数的有效实际参数。
pandas提供了类似于R语言中codice_4对象的数据帧（codice_5），它是二维的加标签数据结构，其诸纵列潜在的可能具有不同的类型；数据帧就像是电子表格或SQL表，或者是序列的字典，这种格局也叫做（SoA）。pandas允许各种数据操纵运算操作，比如选择、归并和重制形状，还有数据清洗和特征。
主要特征.
pandas提供了快速而高效的数据帧对象，用于凭借其集成的索引进行数据操纵。它的主要特征有：
pandas经过了高度的性能优化，关键代码路径用Cython或C语言写成。pandas可以利用PyArrow来扩展功能并增进各种API的性能。pandas的缺省绘图后端是matplotlib，还可以扩展上其他第三方后端，比如Plotly Express。
SQL OLAP列式数据库DuckDB，可以在pandas数据帧上执行SQL。
示例.
基本运算.
在下面的梗概示例中，展示针对数据帧的纵列和横行的基本运算：
<syntaxhighlight lang="pycon">
Â»> import pandas as pd
Â»> import numpy as np
Â»> import matplotlib.pyplot as plt
Â»> 
Â»> data = {
... 'place': ['A']*3 + ['B']*3 + ['C']*3 + ['D']*3,
... 'date': pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03'] * 4),
... 'value': + 0.1 for x in range(12)
Â»> 
Â»> df = pd.DataFrame(data)
Â»> type(df['value']) == pd.Series
True
Â»>
Â»> df['value1'] = df['value'] + 0.1
Â»> df
0 A 2023-01-01 0.1 0.2
1 A 2023-01-02 1.1 1.2
2 A 2023-01-03 2.1 2.2
3 B 2023-01-01 3.1 3.2
4 B 2023-01-02 4.1 4.2
5 B 2023-01-03 5.1 5.2
6 C 2023-01-01 6.1 6.2
7 C 2023-01-02 7.1 7.2
8 C 2023-01-03 8.1 8.2
9 D 2023-01-01 9.1 9.2
10 D 2023-01-02 10.1 10.2
11 D 2023-01-03 11.1 11.2
Â»> 
Â»> df.index
RangeIndex(start=0, stop=12, step=1)
Â»> 
Â»> df.columns
Index(['place', 'date', 'value', 'value1'], dtype='object')
Â»> 
Â»> df.loc 
1 1.1 1.2
2 2.1 2.2
Â»> 
Â»> df.at'value'
1.1
Â»> 
Â»> df['value'].loc
1.1
Â»> 
Â»> df'value','value1'.to_numpy().mean().round(2)
np.float64(5.65)
Â»> df'value','value1'.to_numpy().mean(axis=0)
array(5.7)
Â»> df'value','value1'.to_numpy().mean(axis=1)
array([ 0.15, 1.15, 2.15, 3.15, 4.15, 5.15, 6.15, 7.15, 8.15,
Â»> 
Â»> df[(df['value']/2 > 1) & (df['value1'] < 3)]
2 A 2023-01-03 2.1 2.2
Â»> 
Â»> df.query('value/2 > 1 & value1 < 3')
2 A 2023-01-03 2.1 2.2
Â»>
Â»> df== 1.1['value1'].round(2).item()
1.2
Â»>
</syntaxhighlight>
数据帧格式.
数据帧中的数据经常存储为两种格式：堆叠（stack）格式或记录（record）格式。在堆叠格式中，针对每个主题（subject）在适用情况下有多个横行，故而也称为“长”格式。在记录格式中，针对每个主题典型地有一个横行，故而也称为“宽”格式。在这个例子中，如果要对纵列codice_11标识出的每个唯一的变量（codice_12）进行时间序列运算，更好的表示形式为：诸纵列都对应唯一的变量，即对应不同的观测地点或观测者，而日期索引（codice_13）标识出每个（不可细分的）个体观测。为此使用codice_14，将数据帧从堆叠格式重制形状为记录格式：
<syntaxhighlight lang="pycon">
Â»> df.drop(4, 8).pivot(index='date', columns='place')
place A B C D A B C D
date 
2023-01-01 NaN 3.1 6.1 9.1 NaN 3.2 6.2 9.2
2023-01-02 1.1 NaN 7.1 10.1 1.2 NaN 7.2 10.2
2023-01-03 2.1 5.1 NaN 11.1 2.2 5.2 NaN 11.2
Â»> 
</syntaxhighlight>
这里删除了三个横行展示了codice_6被用来表示缺失数据。这里在codice_14的输入数据帧的诸纵列中，通过codice_2参数指定了用作索引的纵列（codice_13），通过codice_19参数指定了用作变量的纵列（codice_11），但没有通过指定codice_21参数于多个值纵列（codice_22）里选取其中之一，故而结果数据帧的诸纵列被纳入层级式索引（即多重索引codice_23），其最顶层指示出各自的值纵列，即依据观测量的不同而进行顶层分组。
串接运算.
使用codice_24对数据帧进行运算：
<syntaxhighlight lang="pycon">
Â»> df1 = df.drop(columns='value').rename(columns={'value1': 'value'})
Â»> df1 = pd.concat(df1, ignore_index=True)
Â»> df1.shape
Â»> 
Â»> data1 = [
... ('A', pd.to_datetime('2023-01-01'), 0.3),
... ('A', pd.to_datetime('2023-01-02'), 1.3)
Â»> 
Â»> new_rows = pd.DataFrame(data1, columns=['place', 'date', 'value'])
Â»> pd.concat(new_rows, ignore_index=True).tail(3)
23 D 2023-01-03 11.2
24 A 2023-01-01 0.3
25 A 2023-01-02 1.3
Â»> 
Â»> df_A = df1[df1['place']=='A'].drop(columns='place')
Â»> df_A
0 2023-01-01 0.1
1 2023-01-02 1.1
2 2023-01-03 2.1
12 2023-01-01 0.2
13 2023-01-02 1.2
14 2023-01-03 2.2
Â»> 
Â»> df_B = df1[df1['place']=='B'].drop(columns='place')
Â»> df_C = df1[df1['place']=='C'].drop(columns='place')
Â»> df_D = df1[df1['place']=='D'].drop(columns='place')
Â»> df1 = pd.concat(df_B, df_C, df_D, keys=['A', 'B', 'C', 'D']) \
... .droplevel(1).rename_axis('place').reset_index()
Â»> 
</syntaxhighlight>
分组聚合运算.
使用codice_25和紧随其后的codice_26，对数据帧进行并运算：
<syntaxhighlight lang="pycon">
Â»> df2 = df1.groupby(['date', 'place']).agg({'value': 'sum'})
Â»> df2
date place 
2023-01-01 A 0.3
2023-01-02 A 2.3
2023-01-03 A 4.3
Â»> 
Â»> df2.reorder_levels(['place', 'date']).sort_index()
place date 
A 2023-01-01 0.3
B 2023-01-01 6.3
C 2023-01-01 12.3
D 2023-01-01 18.3
Â»> 
Â»> df2.shape
Â»> 
Â»> df2.index
MultiIndex([('2023-01-01', 'A'),
Â»> 
Â»> df2.columns
Index(['value'], dtype='object')
Â»> 
Â»> df2.loc[('2023-01-02', 'A')]
value 2.3
Name: (2023-01-02 00:00:00, A), dtype: float64
Â»> 
Â»> df2.loc['2023-01-02']
place 
A 2.3
B 8.3
C 14.3
D 20.3
Â»> 
Â»> df2.xs('A', level='place')
date 
2023-01-01 0.3
2023-01-02 2.3
2023-01-03 4.3
Â»>
</syntaxhighlight>
枢轴聚合运算.
使用codice_27，对数据帧进行枢轴聚合运算：
<syntaxhighlight lang="pycon">
Â»> df3 = df1.pivot_table(index='date', columns='place', aggfunc='sum')
Â»> df3
place A B C D
date 
2023-01-01 0.3 6.3 12.3 18.3
2023-01-02 2.3 8.3 14.3 20.3
2023-01-03 4.3 10.3 16.3 22.3
Â»> 
Â»> df3.shape
Â»> 
Â»> df3.to_numpy()
array([[ 0.3, 6.3, 12.3, 18.3],
Â»> 
Â»> df3.index
DatetimeIndex(['2023-01-01', '2023-01-02', '2023-01-03'], dtype='datetime64', name='date', freq=None)
Â»> 
Â»> df3.columns
MultiIndex([('value', 'A'),
Â»> 
Â»> df1.pivot_table(index='date', columns='place', values='value', aggfunc='sum').columns
Index(['A', 'B', 'C', 'D'], dtype='object', name='place')
Â»> 
Â»> df3['value'].columns
Index(['A', 'B', 'C', 'D'], dtype='object', name='place')
Â»> 
Â»> df3[('value', 'A')]
date
2023-01-01 0.3
2023-01-02 2.3
2023-01-03 4.3
Name: (value, A), dtype: float64
Â»> 
Â»> df3.stack().shape
Â»> df2.unstack().shape
Â»> 
</syntaxhighlight>
归并运算.
使用codice_28，对数据帧进行归并运算：
<syntaxhighlight lang="pycon">
Â»> sr_A = df3[('value', 'A')]
Â»> sr_B = df3[('value', 'B')]
Â»> sr_C = df3[('value', 'C')]
Â»> sr_D = df3[('value', 'D')]
Â»> merge_date = lambda x, y: pd.merge(x, y, on='date')
Â»> df3 = merge_date(merge_date(sr_A, sr_B), merge_date(sr_C, sr_D)) \
... .rename_axis('place', axis=1)
Â»> 
Â»> pd.merge(df_A, df_B.drop('2023-01-02'), on='date')
date 
2023-01-01 0.3 6.3
2023-01-03 4.3 10.3
Â»> 
Â»> pd.merge(df_A, df_B.drop('2023-01-02'), on='date', how='left')
date 
2023-01-01 0.3 6.3
2023-01-02 2.3 NaN
2023-01-03 4.3 10.3
Â»> 
</syntaxhighlight>
绘图输出.
用matplotlib为数据帧绘制条形图：
<syntaxhighlight lang="pycon">
Â»> df_axis0, df_axis1 = df3.shape
Â»> df_columns = df3.to_dict(orient='list')
Â»> df_index = df3.index.to_list()
Â»> x = np.arange(df_axis0)
Â»> width = 1/(df_axis1 + 1)
Â»> mult = 0
Â»> 
Â»> fig, ax = plt.subplots(layout='constrained')
Â»> for name, value in df_columns.items():
... offset = width * mult
... rects = ax.bar(x + offset, value, width, label=name)
... ax.bar_label(rects, padding=3)
... mult += 1
3, '0.3'), Text(0, 3, '2.3'), Text(0, 3, '4.3')
3, '6.3'), Text(0, 3, '8.3'), Text(0, 3, '10.3')
3, '12.3'), Text(0, 3, '14.3'), Text(0, 3, '16.3')
3, '18.3'), Text(0, 3, '20.3'), Text(0, 3, '22.3')
Â»> ax.set_xticks(x + width*(df_axis1 - 1)/2,
... for x in df_index)
[<matplotlib.axis.XTick object at 0x7bb6925408f0>, <matplotlib.axis.XTick object at 0x7bb692541700>, <matplotlib.axis.XTick object at 0x7bb6925421e0>]
Â»> ax.legend()
<matplotlib.legend.Legend object at 0x7bb696123aa0>
Â»> ax.grid(axis='y', linestyle=':')
Â»> ax.set_axisbelow(True)
Â»> plt.show()
Â»>
</syntaxhighlight>
CSV文件操作.
导出和导入CSV文件：
<syntaxhighlight lang="pycon">
Â»> df2.to_csv('dftest.csv', float_format='%.1f')
Â»> 
Â»> df4 = pd.read_csv('dftest.csv', index_col=[0,1])
Â»> df4.shape
</syntaxhighlight>
在不指定codice_29参数即采用其缺省值之时，从导入文件标头即第一横行推导出数据的纵列名字。在导入文件时采用codice_30参数，指定用作索引即横行标签的纵列。在标头的纵列数大于数据记录的纵列数之时，标头末位多出的纵列被设置为codice_6；在标头的纵列数小于数据记录的纵列数之时，数据记录首位多出的纵列被用作索引；在标头的纵列数等于数据记录的纵列数，并且未指定索引列之时，则自动增加默认的没有名字的范围索引作为索引；在标头的纵列数等于数据记录的纵列数，并且已指定索引列之时，采用指定纵列作为索引。
在导入文件时可以通过codice_32参数指定选用的纵列子集。如果所导入的文件没有标头，则需要指定codice_33，还可以使用codice_34参数，为导入数据指定纵列名字。如果所导入的文件有标头，并且要使用codice_34参数指定替代它的纵列名字，则需要同时指定codice_36。在导出文件时索引总是被导出的，可以使用codice_37参数，为没有名字的索引纵列补充指定其名字。在导入pandas所导出的文件之时，需要指定索引列来避免再次自动增加缺省索引。
当数据帧是记录格式，故而codice_19方向采用了多重索引之时，所导出的CSV文件的标头有多个横行，以这里的数据帧codice_39为例，在导入它之时需要指定codice_40和codice_41，但不能同时使用codice_32或codice_34参数：
<syntaxhighlight lang="pycon">
Â»> df3.to_csv('dftest.csv', float_format='%.1f')
Â»> 
Â»> df4 = pd.read_csv('dftest.csv', header=1, index_col=0)
Â»> df4.shape
</syntaxhighlight>
使用util-linux工具组成员codice_44来查看导出的CSV文件：
<syntaxhighlight lang="console">
$ cat dftest.csv | column -s, -o, -t 
place ,A ,B ,C ,D
date , , , ,
2023-01-01,0.3 ,6.3 ,12.3 ,18.3
2023-01-02,2.3 ,8.3 ,14.3 ,20.3
2023-01-03,4.3 ,10.3 ,16.3 ,22.3
</syntaxhighlight>
这里的前两横行的除了第一纵列之外的纵列，指定多层索引的纵列名字为codice_45。第一纵列的前两单元，指定了诸纵列codice_19的多重索引自身的名字列表为codice_11，而第一纵列的第三单元，指定了索引codice_2的日期时间索引自身的名字为codice_13。
JSON文件操作.
导出和导入JSON文件：
<syntaxhighlight lang="pycon">
Â»> df3.to_json('dftest.json', orient='index', date_format='iso', date_unit='s')
Â»> 
Â»> df4 = pd.read_json('dftest.json', orient='index')
Â»> df4.shape
Â»> 
Â»> df4.columns
Index(['('value', 'A')', '('value', 'B')', '('value', 'C')', '('value', 'D')'], dtype='object')
Â»> 
Â»> df4.columns = pd.MultiIndex.from_tuples(for x in df4.columns)
Â»> df4.columns.names = 'place'
Â»> df4.index.name = 'date'
</syntaxhighlight>
这里指定了codice_50（方向）为codice_51（索引），即采用次序；这里指定了日期时间格式为ISO 8601标准格式，并且指定了时间单位为秒。JSON对象的键只能是字符串，在导出为JSON文件之时，元组被转换成字符串；而在导入它之时，需要通过codice_52将其再转换为元组。
使用jq语言实现codice_53来查看导出的JSON文件：
<syntaxhighlight lang="console">
$ cat dftest.json | jq
</syntaxhighlight>
在导出的数据中，不包含诸纵列codice_19的多重索引自身的名字列表即codice_11，不包含索引codice_2自身的名字即codice_13。
HDF5文件操作.
导出和导入HDF5文件基于了PyTables：
<syntaxhighlight lang="pycon">
Â»> df3.to_hdf('dftest.h5', key='df3', mode='w')
Â»> df.to_hdf('dftest.h5', key='df', mode='a')
Â»> 
Â»> df4 = pd.read_hdf('dftest.h5', key='df3')
Â»> df4.shape
</syntaxhighlight>
这里通过键（codice_58）参数，指定了与数据帧相对应的在HDF5文件中的群组（Group），对它采用了缺省的codice_59（固定）存储格式。文件打开模态codice_60是为“写”（write）即“新建”，而打开模态codice_61是为“附加”（append）。
使用codice_62工具组成员codice_63来查看导出的HDF5文件：
<syntaxhighlight lang="console">
$ h5ls dftest.h5
df Group
df3 Group
$ h5ls -r -d dftest.h5/df3
</syntaxhighlight>
这里的HDF5文件中的日期时间表示，是以纳秒为单位的UNIX时间的时间戳。这种存储格式保存了数据帧的两个轴和所有的块，它采用的轴编号codice_64和codice_65与编程API所用的NumPy轴编号相反。这里的codice_39存储只有一个块，这个块的codice_67的内容同于codice_68。下面查看codice_69存储的情况，它的四个纵列被整合（codice_70）为三个块，其codice_67的并集同于codice_68:
<syntaxhighlight lang="console">
$ h5ls -r dftest.h5/df
</syntaxhighlight>
保存HDF5文件还可采用codice_73（表格）格式，HDF5文件中这种存储格式的群组，可以直接在其上进行查询和过滤：
<syntaxhighlight lang="pycon">
Â»> df3.to_hdf('dftest.h5', key='df3t', format='table', mode='a')
Â»> 
Â»> pd.read_hdf('dftest.h5', key='df3t', where='index > 20230101', columns=[('value', 'A'), ('value', 'C')])
place A C
date 
2023-01-02 2.3 14.3
2023-01-03 4.3 16.3
</syntaxhighlight>
查看HDF5文件中的这种存储格式：
<syntaxhighlight lang="console">
$ h5ls dftest.h5/df3t
_i_table Group
$ h5ls -r -d dftest.h5/df3t/table
</syntaxhighlight>
这里的codice_74群组存储了PyTables的codice_75模块所存取的诸多内容。
netCDF文件操作.
导出和导入netCDF文件可以借助codice_76，它依赖于pandas，它通过netcdf4-python支持导入导出netCDF-4格式数据，通过SciPy支持其他版本netCDF格式。codice_76能够在自身的数据阵列（codice_78）与pandas的序列之间，在自身的数据集（codice_79）与pandas的数据帧之间，进行相互转换：
<syntaxhighlight lang="pycon">
Â»> import xarray as xr
Â»> 
Â»> df2.to_xarray()
<xarray.Dataset> Size: 152B
Dimensions: (date: 3, place: 4)
Coordinates:
Data variables:
Â»> 
Â»> df3.to_xarray()
<xarray.Dataset> Size: 120B
Dimensions: (date: 3)
Coordinates:
Data variables:
Â»> 
Â»> df2.to_xarray().to_netcdf('dftest.nc')
Â»> 
Â»> df4 = xr.open_dataset('dftest.nc').to_dataframe()
Â»> df4.shape
</syntaxhighlight>
这里的从堆叠格式的数据帧codice_80转换出来的数据集可以导出至netCDF文件，而从记录格式的数据帧codice_39转换出来的数据集因变量名字而不能直接导出至netCDF文件。
使用codice_82工具组成员codice_83来查看导出的netCDF文件：
<syntaxhighlight lang="console">
$ ncdump dftest.nc
netcdf dftest {
dimensions:
 date = 3 ;
 place = 4 ;
variables:
 double value(date, place) ;
 value:_FillValue = NaN ;
 int64 date(date) ;
 date:units = "days since 2023-01-01 00:00:00" ;
 date:calendar = "proleptic_gregorian" ;
 string place(place) ;
data:
$ ncdump -k dftest.nc
netCDF-4
$ h5ls -r -d dftest.nc
/ Group
</syntaxhighlight>
这里的日期时间表示遵循了</ref>，采用的时间单位为距离某个指定的开始日期时间的日数，历法为前推格里高利历。codice_83的输出所称谓的变量，代表相同类型的值的多维阵列。这里有三个变量：codice_85是数据变量，codice_86和codice_87是坐标变量。变量声明指定了变量的数据类型、名字和以维度名字列表描述的形状，例如codice_88，维度名字可以是codice_89等等，而这里的维度名字同于对应的坐标变量名字。
SQL关系数据库操作.
下面通过SQLAlchemy将数据帧导出和导入于SQL关系数据库SQLite：
<syntaxhighlight lang="pycon">
Â»> from sqlalchemy import create_engine
Â»> engine = create_engine("sqlite:///dftest.db")
Â»> 
Â»> df2.to_sql(name='df2', con=engine)
12
Â»> 
Â»> with engine.connect() as conn:
... df4 = pd.read_sql('SELECT * FROM df2', conn)
Â»> 
Â»> df4.shape
Â»> df4['date'] = pd.to_datetime(df4['date'])
Â»> df4 = df4.set_index(['date', 'place'])
Â»> df4.shape
</syntaxhighlight>
使用SQLite的命令行界面来查看导出的数据库文件：
<syntaxhighlight lang="console">
$ echo '.tables' | sqlite3 dftest.db
df2
$ echo '.schema df2' | sqlite3 dftest.db
CREATE TABLE df2 (
 date DATETIME, 
 place TEXT, 
 value FLOAT
CREATE INDEX ix_df2_date ON df2 (date);
CREATE INDEX ix_df2_place ON df2 (place);
$ echo 'SELECT * FROM df2' | sqlite3 dftest.db
2023-01-01 00:00:00.000000|A|0.3
2023-01-01 00:00:00.000000|B|6.3
2023-01-01 00:00:00.000000|C|12.3
2023-01-01 00:00:00.000000|D|18.3
2023-01-02 00:00:00.000000|A|2.3
2023-01-02 00:00:00.000000|B|8.3
2023-01-02 00:00:00.000000|C|14.3
2023-01-02 00:00:00.000000|D|20.3
2023-01-03 00:00:00.000000|A|4.3
2023-01-03 00:00:00.000000|B|10.3
2023-01-03 00:00:00.000000|C|16.3
2023-01-03 00:00:00.000000|D|22.3
</syntaxhighlight>
SQLite将SQL的数据类型codice_90硬性指定为亲和codice_91，而codice_91可以按其适合情况转换为五个存储类别（codice_93、codice_94、codice_95、codice_96或codice_97）之一，这里的数据帧codice_80的codice_86纵列中的数据，被转换成为codice_94、codice_95和codice_96三者中的codice_96即文本字符串。