pandas 3.0 科研数据处理教程
Python 3.14.4 + Miniconda + pandas 3.0.2(本教程按 3.0 系列 API 编写,实测于 3.0.5)。
全文代码可在 VS Code 中直接python xxx.py运行;所有示例数据都贴合科研场景。
1. 简介与定位:表格数据处理的标配
pandas 是 Python 数据科学生态里处理结构化表格数据的事实标准。化学中的部分批量表格化数据:
- 实验记录(催化剂名称、温度、TOF、法拉第效率 FE、电流密度)
- DFT 批量计算结果(吸附能、d-band 中心、零点能校正后的自由能)
- 机器学习势函数训练集(构型编号、能量、每个原子的受力)
- MOF 数据库(比表面积、孔径、拓扑类型)
这些本质上都是"行 = 一条记录、列 = 一个字段"的二维表。pandas 提供两个核心对象:
| 对象 | 维度 | 类比 |
|---|---|---|
Series | 一维带标签数组 | Excel 中的"一列",或 numpy 数组 + 索引 |
DataFrame | 二维带标签表格 | 整个 Excel 工作表 / SQL 表 |
pandas 建立在 numpy 之上,但与 numpy 的最大区别是:它有行索引(index)和列名(columns),可以按标签对齐数据,特别适合多张表之间的"按催化剂名称 / 按构型编号"对齐合并。
本教程最重要的前提:你用的是 pandas 3.0,而不是网上大量旧教程里的 1.x / 2.x。3.0 有三处颠覆性的变化,务必先记住(后面有专章细讲):
- Copy-on-Write(写时复制)默认强制开启,无法关闭——链式赋值不再修改原表;
- 字符串列默认用
strdtype(不再是无类型区分的object),缺失值统一变成pd.NA; - 一批旧 API 被彻底删除:
DataFrame.append、DataFrame.applymap、fillna(method=...)等。
2. 安装与版本检查(3.0 的 Copy-on-Write 新特性)
2.1 安装 / 升级
# Miniconda 环境(推荐单独建环境,避免污染 base)
# conda create -n sci python=3.12 -y
# conda activate sci
# pip 方式(你当前是 3.14.4 + pandas 3.0.2)
# pip install -U pandas openpyxl xlrd numpy
Excel 读写需要额外引擎:.xlsx 用 openpyxl,旧版 .xls 用 xlrd。
2.2 版本检查(养成习惯,写进每个脚本开头)
import pandas as pd
import numpy as np
print(pd.__version__) # 3.0.2 / 3.0.5
print(np.__version__)
# 关键:确认 Copy-on-Write 状态(3.0 恒为 True)
print(pd.options.mode.copy_on_write) # True(3.0 起恒为 True,不可关闭)
注意:
pd.options.mode.copy_on_write在 3.0 中已被标记为 deprecated,因为它永远开启、无法关闭,
打印它会触发Pandas4Warning(无害)。你只需知道"CoW 恒开"这一事实即可,无需在脚本里设置它。
2.3 Copy-on-Write(CoW)是什么,为什么对你重要
1.x/2.x 时代,df[df["a"] > 1] 这类切片有时返回"视图"(view),有时返回"副本"(copy),
于是出现臭名昭著的 SettingWithCopyWarning——你改一个切片,有时改了原表、有时没改,全凭运气。
3.0 起 CoW 恒开,规则变得简单且确定:
- 任何"取子集"操作(布尔索引、列选择、切片)返回的是一份惰性副本;
- 你在这个副本上赋值,绝不会反向影响原表;
- 链式赋值(
df[cond]["col"] = x)会抛ChainedAssignmentError警告,并且对原表无效。
正确写法只有一种:用 .loc 一步到位。细节见第 14 章,现在先记住结论。
3. Series 与 DataFrame:创建、索引、属性、dtype
3.1 创建 Series
import pandas as pd
# 由列表创建(索引默认 0,1,2,...)
tof = pd.Series([3.2, 1.8, 0.9, 4.5])
print(tof)
# 指定标签索引
ads_E = pd.Series(
[-0.42, -1.15, -0.87],
index=["Fe-N4", "Co-N4", "Ni-N4"], # 吸附能,单位 eV
name="E_ads",
)
print(ads_E)
print(ads_E["Co-N4"]) # 按标签取值 -> -1.15
print(ads_E.iloc[0]) # 按位置取值 -> -0.42
3.2 创建 DataFrame(科研里最常用:字典方式)
df = pd.DataFrame({
"催化剂": ["Fe-N4", "Co-N4", "Ni-N4", "Cu-N4", "Fe-N4"],
"TOF": [3.2, 1.8, 0.9, 4.5, 3.0], # 转换频率 / s^-1
"FE_CO": [95.2, 88.1, 76.4, 62.3, 91.0], # CO 法拉第效率 %
"温度": [298, 298, 323, 298, 323],
})
print(df)
由 numpy 数组创建(列名 + index 显式给出,避免歧义):
arr = np.array([[3.2, 95.2], [1.8, 88.1], [0.9, 76.4]])
df2 = pd.DataFrame(arr, columns=["TOF", "FE_CO"], index=["Fe", "Co", "Ni"])
print(df2)
3.3 核心属性
print(df.shape) # (5, 4) -> (行数, 列数)
print(df.columns) # Index(['催化剂','TOF','FE_CO','温度'])
print(df.index) # RangeIndex(start=0, stop=5, step=1)
print(df.dtypes) # 每列的 dtype(关键!3.0 里字符串列显示 str)
print(df.size) # 元素总数 20
print(df.ndim) # 2
print(df.empty) # False
3.4 dtype 一览(3.0 的字符串列已是 str)
df = pd.DataFrame({
"材料": ["Fe-N4", "Co-N4", None], # -> str (不再是 object!)
"吸附能": [-0.42, -1.15, -0.87], # -> float64
"原子数": [4, 4, 4], # -> int64
"是否稳定": [True, True, False], # -> bool
})
print(df.dtypes)
# 材料 str
# 吸附能 float64
# 原子数 int64
# 是否稳定 bool
3.0 变化:df["材料"].dtype 显示 str(底层是 StringDtype(storage='python', na_value=nan)),
缺失值用 pd.NA 表示,而不是 object 里的 NaN / None 混杂。这一变化影响后文很多写法。
常用可空类型(astype 时用):
| dtype | 含义 | 缺失值表示 |
|---|---|---|
float64 / float | 双精度浮点 | NaN |
int64 / int | 64 位整数(不能有缺失) | — |
Int64(大写) | 可空整数 | pd.NA |
bool | 布尔 | — |
boolean(小写) | 可空布尔 | pd.NA |
str / string | 字符串(3.0 默认) | pd.NA |
category | 分类(枚举,省内存) | NaN |
datetime64[ns] | 时间戳 | NaT |
4. 读取数据:read_csv / read_excel / read_table
4.1 read_csv(最常用)
# 基础读取
df = pd.read_csv("results.csv")
# 科研数据常见处理参数
df = pd.read_csv(
"results.csv",
sep=",", # 分隔符:, \t ; 或空白 \s+
encoding="utf-8", # Windows 中文数据常见 GBK,报错就换 "gbk"
header=0, # 第 0 行为表头;header=None 表示无表头
names=["id", "TOF", "FE"], # header=None 时手动命名列
index_col=0, # 把第 0 列设为行索引(如构型编号)
usecols=["催化剂", "TOF", "FE_CO"], # 只读需要的列
skiprows=3, # 跳过开头 3 行注释(VASP 输出常见)
comment="#", # 忽略 # 开头的行
na_values=["", "NA", "null"], # 把这些识别为缺失
nrows=1000, # 只读前 1000 行(大文件预览)
dtype={"TOF": float}, # 指定某列类型
)
3.0 里
read_csv的字符串列默认就是strdtype,缺失值直接变pd.NA,无需再手动astype("string")。
4.2 read_table(制表符分隔,等价 read_csv(sep="\t"))
df = pd.read_table("data.txt", sep="\t", encoding="utf-8") # sep 可省略,默认 \t
很多 VASP / 实验仪器导出的 .txt / .dat 是空白或 Tab 分隔,用 sep=r"\s+"(任意空白):
df = pd.read_csv("OUTCAR.energy.dat", sep=r"\s+", header=None,
names=["step", "E_tot", "F_max"], comment="#")
4.3 read_excel
df = pd.read_excel(
"催化实验记录.xlsx",
sheet_name=0, # 0 表示第一张表;也可写 "Sheet1" 或 None(读所有表成 dict)
header=1, # 表头在第 1 行(有些 Excel 顶部有标题行)
usecols="A:E", # 或列表 ["A","B","C"]
dtype={"TOF": float},
)
# 读多个 sheet
sheets = pd.read_excel("book.xlsx", sheet_name=None) # dict: {sheet名: DataFrame}
4.4 快速预览读取结果
print(df.head(3)) # 前 3 行
print(df.shape)
print(df.dtypes)
5. 数据查看与筛选:head / tail / info / describe、loc / iloc / 布尔索引 / query
5.1 概览
df.head() # 前 5 行
df.tail(2) # 后 2 行
df.info() # 每列非空计数 + dtype(检查缺失值首选)
df.describe() # 数值列统计:count/mean/std/min/25%/50%/75%/max
df.describe(include="all") # 包含字符串/分类列
5.2 按标签 loc / 按位置 iloc
df = pd.DataFrame({
"催化剂": ["Fe-N4", "Co-N4", "Ni-N4", "Cu-N4"],
"TOF": [3.2, 1.8, 0.9, 4.5],
"FE_CO": [95.2, 88.1, 76.4, 62.3],
})
df.index = ["A", "B", "C", "D"]
df.loc["B"] # 一行(Series)
df.loc[["A", "C"]] # 多行
df.loc["A", "TOF"] # 单个值 -> 3.2
df.loc["A":"C", "TOF"] # 行切片(含两端) + 单列
df.loc[:, ["TOF", "FE_CO"]] # 所有行,指定列
df.iloc[0] # 第 0 行
df.iloc[:2, 1:] # 前 2 行、第 1 列起
df.iloc[-1, 0] # 最后一行第 0 列
记忆:
loc的切片包含右端点(因为标签语义),iloc切片不含右端点(Python 切片语义)。
5.3 布尔索引(最常用,注意写法)
high = df[df["TOF"] > 2.0] # TOF 大于 2
fe = df[df["催化剂"] == "Fe-N4"] # 等于
multi = df[(df["TOF"] > 2.0) & (df["FE_CO"] > 90)] # 与:&(每个条件都要加括号!)
or_cond = df[(df["催化剂"] == "Fe-N4") | (df["催化剂"] == "Co-N4")] # 或:|
# isin 代替一串 |:
df[df["催化剂"].isin(["Fe-N4", "Co-N4", "Ni-N4"])]
# 数值区间:
df[df["TOF"].between(1.0, 4.0)]
# 排除(取反):
df[~df["催化剂"].isin(["Cu-N4"])]
5.4 query(字符串表达式,可读性好)
df.query("TOF > 2.0")
df.query("TOF > 2.0 and FE_CO > 90")
cat = "Fe-N4"
df.query("催化剂 == @cat") # @ 引用外部变量
5.5 字符串筛选(.str 访问器,3.0 对 str dtype 原生支持)
df[df["催化剂"].str.contains("N4")] # 含 "N4" 的金属
df[df["催化剂"].str.startswith("Fe")] # 以 Fe 开头
df[df["催化剂"].str.contains("Fe", na=False)] # na=False 避免缺失值报错
6. 数据清洗:缺失值、去重、类型转换、rename、字符串处理
6.1 缺失值处理
df = pd.DataFrame({
"催化剂": ["Fe-N4", "Co-N4", None, "Cu-N4", "Fe-N4"],
"TOF": [3.2, np.nan, 0.9, 4.5, 3.0],
"FE_CO": [95.2, 88.1, 76.4, np.nan, 91.0],
})
df.isna() # 布尔表,True=缺失
df.isna().sum() # 每列缺失计数(最常用)
df.notna() # 取反
# 删除含缺失的行
df.dropna() # 默认:任意列缺失就删整行
df.dropna(subset=["TOF"]) # 只按 TOF 列删
df.dropna(how="all") # 整行全空才删
df.dropna(thresh=2) # 至少保留 2 个非空值
df.dropna(axis=1) # 删含缺失的"列"
# 填充缺失
df["TOF"].fillna(0.0) # 填常数
df["TOF"].fillna(df["TOF"].mean()) # 填均值
df["催化剂"].fillna("未知") # 填字符串
# 3.0 变化:fillna(method='ffill') 已删除!用 ffill / bfill
df["TOF"].ffill() # 前向填充(用上一行的值)
df["TOF"].bfill() # 后向填充
6.2 去重(详见第 7 章)
df.drop_duplicates(subset=["催化剂"]) # 按催化剂去重,保留首条
df.drop_duplicates(subset=["催化剂"], keep="last") # 保留最后一条
6.3 类型转换 astype / to_numeric
df["TOF"] = df["TOF"].astype(float) # 转浮点
df["催化剂"] = df["催化剂"].astype(str) # 转字符串(3.0 即 string)
df["原子数"] = df["原子数"].astype("Int64") # 可空整数(大写 I)
df["是否稳定"] = df["是否稳定"].astype(bool)
df["金属"] = df["金属"].astype("category") # 分类,省内存、加速 groupby
# 清洗"混入脏字符"的数值列:先 to_numeric + errors='coerce'(非法值变 NaN)
df["TOF"] = pd.to_numeric(df["TOF"], errors="coerce")
df["TOF"] = df["TOF"].astype(float) # 再统一成 float
6.4 rename 重命名
df = df.rename(columns={"催化剂": "catalyst", "FE_CO": "FE"})
df = df.rename(index={"A": "sample_01"})
# 原地修改(3.0 推荐显式赋值,少用 inplace)
df.columns = ["cat", "tof", "fe"] # 整列名替换
6.5 字符串处理 .str(3.0 里 str dtype 原生支持)
s = df["催化剂"]
s.str.upper() # 转大写
s.str.lower()
s.str.strip() # 去首尾空格
s.str.replace("N4", "N4-单原子") # 替换
s.str.contains("Fe") # 是否包含(布尔)
s.str.startswith("Fe")
s.str.extract(r"([A-Z][a-z]?)") # 正则提取金属元素符号
s.str.len() # 字符串长度
s.str.cat(sep="-") # 拼接
关键点:str dtype 下缺失值是
pd.NA,.str方法对缺失值默认返回pd.NA;
需要布尔结果时加na=False(见 5.5)。
7. 排序与去重
7.1 sort_values
df = pd.DataFrame({
"催化剂": ["Fe-N4", "Co-N4", "Ni-N4", "Cu-N4", "Fe-N4"],
"TOF": [3.2, 1.8, 0.9, 4.5, 3.0],
"FE_CO": [95.2, 88.1, 76.4, 62.3, 91.0],
})
df.sort_values("TOF") # 升序
df.sort_values("TOF", ascending=False) # 降序(活性最高排最前)
df.sort_values(["催化剂", "TOF"], ascending=[True, False]) # 多列排序
df.sort_values("TOF", na_position="last") # 缺失值放最后
7.2 sort_index
df.sort_index() # 按行索引排序
df.sort_index(axis=1) # 按列名排序
7.3 去重 drop_duplicates + 排名 rank
df.drop_duplicates() # 完全相同的行去重
df.drop_duplicates(subset=["催化剂"]) # 指定列去重
df.drop_duplicates(subset=["催化剂"], keep="last")
df.drop_duplicates(subset=["催化剂"], keep=False) # 只要"唯一出现"的行
df["活性排名"] = df["TOF"].rank(ascending=False) # 排名(1=最高)
8. 分组聚合 groupby(重点)
groupby 是科研汇总的核心:把"按催化剂分组求均值/最大/标准差"变成一句话。
8.1 单列分组 + 单聚合
df = pd.DataFrame({
"催化剂": ["Fe-N4", "Co-N4", "Fe-N4", "Ni-N4", "Fe-N4", "Co-N4"],
"批次": ["b1", "b1", "b2", "b1", "b3", "b2"],
"TOF": [3.2, 1.8, 3.0, 0.9, 3.4, 1.6],
"FE_CO": [95.2, 88.1, 91.0, 76.4, 96.0, 87.5],
})
g = df.groupby("催化剂")
print(g["TOF"].mean()) # 每种催化剂的平均 TOF
print(g["TOF"].mean().sort_values(ascending=False)) # 排序后一目了然
8.2 多列分组
print(df.groupby(["催化剂", "批次"])["TOF"].mean())
8.3 agg:一次算多种统计(最常用写法)
# 对单列算多种统计
print(df.groupby("催化剂")["TOF"].agg(["mean", "std", "min", "max", "count"]))
# 对多列分别指定不同统计
summary = df.groupby("催化剂").agg(
TOF_mean=("TOF", "mean"),
TOF_std=("TOF", "std"),
FE_mean=("FE_CO", "mean"),
n=("TOF", "count"),
)
print(summary)
# 对多列应用同一批统计
print(df.groupby("催化剂")[["TOF", "FE_CO"]].agg(["mean", "std"]))
8.4 transform(保持原表行数,做"组内归一化")
# 每组内减去组均值(中心化,常用于构型能量去基准)
df["TOF_centered"] = df.groupby("催化剂")["TOF"].transform(lambda x: x - x.mean())
print(df)
8.5 size 与 count、reset_index
df.groupby("催化剂").size() # 每组行数(含缺失也算)
df.groupby("催化剂")["TOF"].count() # 非缺失计数
df.groupby("催化剂")["TOF"].mean().reset_index() # 转回普通 DataFrame(索引变列)
3.0 默认值提醒:
groupby的observed=True、dropna=True、as_index=True、sort=True均为默认。
分组键里有缺失值默认会被dropna=True排除;分类列未出现的类别默认不保留(observed=True)。
这与 1.x 的旧默认不同,网上旧代码若出现"空分组",多半是这套默认值变化所致。
9. 合并与连接:merge / concat / join
9.1 concat(纵向堆叠 / 横向拼接)
# 纵向堆叠多个实验结果表
df1 = pd.DataFrame({"催化剂": ["Fe-N4"], "TOF": [3.2]})
df2 = pd.DataFrame({"催化剂": ["Co-N4"], "TOF": [1.8]})
df = pd.concat([df1, df2], ignore_index=True) # ignore_index 重置行索引
print(df)
# 横向拼接(列对齐)
dfL = pd.DataFrame({"TOF": [3.2, 1.8]}, index=["Fe", "Co"])
dfR = pd.DataFrame({"FE": [95.2, 88.1]}, index=["Fe", "Co"])
pd.concat([dfL, dfR], axis=1)
# 加来源标签(keys 生成多层索引,追踪每行来自哪张表)
pd.concat([df1, df2], keys=["实验1", "实验2"])
3.0 变化:
df.append()已被删除。纵向加行一律用pd.concat([df, new_row_df], ignore_index=True)。
9.2 merge(按键对齐,类似 SQL JOIN,科研最常用)
perf = pd.DataFrame({
"催化剂": ["Fe-N4", "Co-N4", "Ni-N4"],
"TOF": [3.2, 1.8, 0.9],
})
energy = pd.DataFrame({
"催化剂": ["Fe-N4", "Co-N4", "Cu-N4"],
"E_ads": [-0.42, -1.15, -0.20],
})
# 内连接(只保留两边都有的催化剂)
pd.merge(perf, energy, on="催化剂") # how 默认 'inner'
# 左连接(保留左边全部,右边缺的填 NaN/NA)
pd.merge(perf, energy, on="催化剂", how="left")
# 外连接(两边全保留)
pd.merge(perf, energy, on="催化剂", how="outer")
# 左右键名不同时
pd.merge(perf, energy, left_on="催化剂", right_on="催化剂")
# 多键合并(如 催化剂 + 批次)
pd.merge(perf, energy, on=["催化剂", "批次"])
# 列名冲突自动加后缀
pd.merge(a, b, on="催化剂", suffixes=("_perf", "_ener"))
9.3 join(基于行索引对齐,等价 merge 的索引版)
a = pd.DataFrame({"TOF": [3.2, 1.8]}, index=["Fe", "Co"])
b = pd.DataFrame({"FE": [95.2, 88.1]}, index=["Fe", "Co"])
a.join(b) # 默认左连接,按 index 对齐
a.join(b, how="outer")
10. 新增 / 修改列、apply 与向量化(map / applymap 的取舍)
10.1 新增 / 修改列(用 .loc 或直接赋值)
df["过电位_V"] = df["TOF"] * 0.059 # 直接新增列(向量化,最快)
df["TOF"] = df["TOF"] * 1.0 # 修改整列
# 条件赋值:务必用 .loc(3.0 链式赋值会失效,见第 14 章)
df.loc[df["TOF"] > 3.0, "等级"] = "高活性"
df.loc[df["TOF"] <= 3.0, "等级"] = "低活性"
10.2 Series.map:元素级映射(标签转换、查表)
# 用字典把"金属 -> 族/原子序数"批量映射
metal_map = {"Fe": 26, "Co": 27, "Ni": 28, "Cu": 29}
df["原子序数"] = df["金属"].map(metal_map) # 匹配不到的 -> NaN/NA
# 用函数
df["金属"].map(lambda x: x.upper())
10.3 DataFrame.apply:对行/列做自定义运算(较慢,慎用)
# 按"行"计算:把多列合成一个新值
def compute_overpotential(row):
# 例如 TOF 与 FE 合成一个综合指标
return row["TOF"] / (100 - row["FE_CO"] + 1e-6)
df["综合指标"] = df.apply(compute_overpotential, axis=1) # axis=1 逐行
df["列求和"] = df.apply(np.sum, axis=1)
# 按"列"计算
df.apply(np.max, axis=0)
10.4 3.0 取舍:map 取代 applymap
# 旧:df.applymap(func) —— 3.0 已删除!
# 新:df.map(func) —— 对 DataFrame 每个元素操作
df.map(lambda x: round(x, 2) if isinstance(x, float) else x)
# 注意区分:
# Series.map -> 元素级映射/查表
# DataFrame.map -> 元素级操作(替代旧 applymap)
# DataFrame.apply -> 沿行/列聚合式操作
10.5 向量化优先原则(性能)
# 慢:逐行 apply(Python 循环,几万行就很慢)
# 快:直接向量化(numpy/pandas 底层 C 实现)
df["FE_归一化"] = (df["FE_CO"] - df["FE_CO"].min()) / (df["FE_CO"].max() - df["FE_CO"].min())
# np.where 做条件三目(比 apply 快一个量级)
df["是否高效"] = np.where(df["TOF"] > 2.0, "是", "否")
# pd.cut 分箱(把连续 TOF 切成区间,画分布/分组用)
df["TOF_区间"] = pd.cut(df["TOF"], bins=[0, 1, 2, 3, 5], labels=["低", "中", "高", "超高"])
经验:能
df["col"].str.xxx/np.where/pd.cut/ 直接算术解决的,不要用 apply;
apply 只在逻辑确实无法向量化时兜底。
11. 时间序列:日期解析、resample、滑动平均
催化实验常有时间序列:反应时间 vs 转化率 / 电流 vs 时间(稳定性测试,i-t 曲线)。
11.1 日期解析 to_datetime
df = pd.DataFrame({
"时间": ["2024-03-01 09:00", "2024-03-01 09:15", "2024-03-01 09:30"],
"电流_mA": [12.1, 12.5, 12.3],
})
df["时间"] = pd.to_datetime(df["时间"]) # 字符串 -> datetime64
print(df.dtypes) # datetime64[ns]
# 指定格式(大量数据更快、更稳)
pd.to_datetime(df["时间"], format="%Y-%m-%d %H:%M")
# 非法值处理
pd.to_datetime(df["时间"], errors="coerce") # 非法 -> NaT
# 生成规则时间序列(模拟每 15 分钟采样)
pd.date_range("2024-03-01", periods=96, freq="15min")
11.2 设为索引 + resample 重采样
df = df.set_index("时间") # 时间戳作为行索引
df.resample("1h").mean() # 按小时求平均
df.resample("1h").max()
df.resample("1D").mean() # 按天
df.resample("30min").mean()
# 非均值:count / sum / std 等
df.resample("1h").agg(["mean", "std", "count"])
11.3 滑动平均 rolling(平滑噪声曲线)
df["电流_平滑"] = df["电流_mA"].rolling(window=5, center=True).mean() # 5 点滑动平均
df["电流_std"] = df["电流_mA"].rolling(10).std() # 滑动标准差
11.4 .dt 访问器(提取时间分量)
df["时间"] = pd.to_datetime(df.index) # 或直接对 datetime 列
df["小时"] = df["时间"].dt.hour
df["日期"] = df["时间"].dt.date
df["星期"] = df["时间"].dt.dayofweek
12. 导出:to_csv / to_excel
df.to_csv(
"汇总.csv",
index=False, # 不写行索引(科研表格一般不需要)
sep=",",
encoding="utf-8-sig", # 带 BOM,Excel 打开中文不乱码
float_format="%.4f", # 浮点保留 4 位小数
na_rep="", # 缺失值写成空
)
df.to_excel(
"汇总.xlsx",
sheet_name="催化性能",
index=False,
engine="openpyxl", # .xlsx 必须指定 openpyxl
float_format="%.4f",
)
# 多个 DataFrame 写不同 sheet(需 openpyxl)
with pd.ExcelWriter("报告.xlsx", engine="openpyxl") as writer:
df1.to_excel(writer, sheet_name="TOF汇总", index=False)
df2.to_excel(writer, sheet_name="能量汇总", index=False)
Windows 中文坑:
to_csv用encoding="utf-8-sig"(或"gbk"),
否则用记事本/Excel 直接打开会乱码。脚本里读中文数据时,read_csv报UnicodeDecodeError就换encoding="gbk"。
13. 与 numpy 互转
# DataFrame/Series -> numpy
arr = df[["TOF", "FE_CO"]].to_numpy() # 推荐 to_numpy()
arr2 = df[["TOF", "FE_CO"]].values # 等价,但 to_numpy 更规范
print(arr, arr.dtype)
# numpy -> DataFrame
arr = np.array([[3.2, 95.2], [1.8, 88.1], [0.9, 76.4]])
df = pd.DataFrame(arr, columns=["TOF", "FE_CO"])
# numpy 函数直接作用于 Series/DataFrame(保留索引/列名)
np.log(df["TOF"]) # 对数
np.sqrt(df["TOF"])
np.mean(df["TOF"])
# 能量/受力等浮点数组常用:把整列导出给 numpy 做线性代数
E = df["E_ads"].to_numpy()
print(E.mean(), E.std())
# 注意:str dtype 列 to_numpy() 得到 object 数组,不是 numpy 的 <U 类型
print(df["催化剂"].to_numpy().dtype) # object
14. Copy-on-Write 与链式赋值的正确写法(重点)
这是 3.0 里最需要"改掉旧习惯"的地方。1.x/2.x 教程里的链式赋值写法,在 3.0 会悄悄失效。
14.1 什么是链式赋值(错误写法)
df = pd.DataFrame({"TOF": [3.2, 1.8, 0.9], "FE": [95.2, 88.1, 76.4]})
# 链式赋值:两步——先取子集,再在子集上赋值
df[df["TOF"] > 2.0]["FE"] = 100
# 3.0 会抛 ChainedAssignmentError 警告,且对原表 df 完全无效!
print(df) # FE 列没变
CoW 下,df[df["TOF"] > 2.0] 返回的是一份临时副本,你在这个副本上改 ["FE"],
改的是副本,原表毫发无伤。所以它"不报错但也无效"——这是最危险的坑。
14.2 正确写法:.loc 一步到位
df.loc[df["TOF"] > 2.0, "FE"] = 100 # 单步:同时指定行条件 + 列名
print(df) # 生效了
# 修多列
df.loc[df["TOF"] > 2.0, ["FE", "TOF"]] = 0
# 按标签改单个值
df.loc[1, "FE"] = 99
14.3 视图 vs 副本(CoW 后的确定性语义)
df = pd.DataFrame({"TOF": [3.2, 1.8, 0.9], "FE": [95.2, 88.1, 76.4]})
df2 = df # 直接赋值:同一对象(引用)
df2.loc[0, "TOF"] = 999 # 会改到 df!因为 df2 就是 df
sub = df[df["TOF"] > 1.0] # 取子集:CoW 下是副本
sub["FE"] = 0 # 不影响 df(副本)
print(df) # df 未变
一句话总结:想改原表,永远
.loc[行条件, 列] = 值一步写;不要先切片再赋值。
14.4 其它旧习惯排查
# 旧:df[df.a > 1]['b'] = x -> 3.0 无效,改 .loc
# 旧:df['b'] = df['b'].fillna(method='ffill') -> method 已删,用 .ffill()
# 旧:df = df.append(row) -> 用 pd.concat
# 旧:df.applymap(f) -> 用 df.map(f)
15. 科研实战示例
15.1 多组催化性能实验数据合并汇总
场景:每次实验导出一个 CSV(exp_01.csv、exp_02.csv…),字段相同,需要合并 + 按催化剂汇总。
import pandas as pd
from pathlib import Path
# 1) 读取目录下所有实验 CSV 并纵向合并
files = sorted(Path("data").glob("exp_*.csv"))
frames = []
for f in files:
d = pd.read_csv(f, encoding="utf-8")
d["来源"] = f.stem # 记录来自哪个文件
frames.append(d)
all_data = pd.concat(frames, ignore_index=True)
# 2) 清洗:TOF 转数值、去重
all_data["TOF"] = pd.to_numeric(all_data["TOF"], errors="coerce")
all_data = all_data.drop_duplicates()
# 3) 按催化剂汇总
summary = all_data.groupby("催化剂").agg(
TOF_mean=("TOF", "mean"),
TOF_std=("TOF", "std"),
FE_mean=("FE_CO", "mean"),
n=("TOF", "count"),
).reset_index().sort_values("TOF_mean", ascending=False)
print(summary)
summary.to_csv("汇总.csv", index=False, encoding="utf-8-sig", float_format="%.3f")
15.2 批量 DFT 计算结果表格化
场景:从一批 OUTCAR / OSZICAR / 自定义 .out 文件中抽取"构型名、总能量、吸附能",汇成一张表再计算相对能量。
import pandas as pd
import re
from pathlib import Path
rows = []
for f in Path("calc").glob("*.out"):
text = f.read_text(encoding="utf-8", errors="ignore")
# 示例:解析形如 "E0= -123.4567 eV" 的总能量
m = re.search(r"E0=\s*(-?\d+\.\d+)", text)
name = f.stem # 构型名(如 CO2_on_FeN4)
if m:
rows.append({"构型": name, "E_tot": float(m.group(1))})
df = pd.DataFrame(rows)
# 以最稳定构型为基准,算相对能量
ref = df["E_tot"].min()
df["E_rel"] = df["E_tot"] - ref # 相对能量 eV
df = df.sort_values("E_rel")
# 标注金属元素(从构型名提取,供分组用)
df["金属"] = df["构型"].str.extract(r"(Fe|Co|Ni|Cu)")
print(df[["构型", "金属", "E_tot", "E_rel"]])
# 按金属汇总平均相对能量
print(df.groupby("金属")["E_rel"].agg(["mean", "min", "count"]))
df.to_csv("DFT汇总.csv", index=False, encoding="utf-8-sig")
15.3 数据透视对比(pivot_table / pivot)
场景:把"催化剂 × 温度"的 TOF 数据透视成矩阵,横向对比不同催化剂在不同温度下的活性。
df = pd.DataFrame({
"催化剂": ["Fe-N4", "Fe-N4", "Co-N4", "Co-N4", "Ni-N4", "Ni-N4"],
"温度": [298, 323, 298, 323, 298, 323],
"TOF": [3.2, 4.1, 1.8, 2.3, 0.9, 1.2],
})
# 行=催化剂,列=温度,值=TOF 均值
pivot = df.pivot_table(
index="催化剂",
columns="温度",
values="TOF",
aggfunc="mean",
fill_value=0,
)
print(pivot)
# 简版 pivot(数据无重复键时可用)
pivot2 = df.pivot(index="催化剂", columns="温度", values="TOF")
print(pivot2)
16. 常见坑
-
链式赋值失效(最高频):
df[cond]["col"] = x在 3.0 无效且抛ChainedAssignmentError。一律用.loc。 -
字符串列已是
strdtype:df["col"].dtype显示str而非object,缺失值是pd.NA。
判断缺失用isna(),不要用x == np.nan(pd.NA == np.nan结果是pd.NA,不是 False)。 -
旧 API 已删除:
df.append()→pd.concat;df.applymap()→df.map();
fillna(method='ffill')→.ffill()。遇到AttributeError: 'DataFrame' object has no attribute ...先怀疑这点。 -
布尔索引的多条件必须加括号:
(df["a"] > 1) & (df["b"] < 0),漏括号会因运算符优先级出错。 -
loc与iloc切片右端点语义不同:loc["A":"C"]含 C,iloc[0:3]不含索引 3。 -
as_index=True默认:groupby().mean()结果带分组键做索引,要普通列就用.reset_index()。 -
中文编码:读报
UnicodeDecodeError换encoding="gbk";写 Excel 不乱码用encoding="utf-8-sig"。 -
to_excel缺引擎:.xlsx报错时先pip install openpyxl。 -
pd.NA与np.nan的差异:可空类型(Int64/str/boolean)用pd.NA,数值运算时它们对缺失的传播更严格。
混用两者做sum()等操作结果可能不同,保持"整列类型统一"可避免大部分问题。 -
视图 vs 副本:
df2 = df是引用(改 df2 会改 df);df2 = df[df["a"]>1]是副本(CoW 下改 df2 不影响 df)。
想改原表只能.loc。 -
inplace=True尽量少用:3.0 语义下显式赋值df = df.dropna()更清晰、更安全,
inplace在链式/视图场景容易埋雷。 -
循环逐行改值极慢:
for i, row in df.iterrows(): ...是性能杀手,先想向量化 /.loc批量赋值。
17. 延伸资源
- 官方文档(务必以 3.0 为准,别被 1.x/2.x 旧博客误导):
https://pandas.pydata.org/docs/ - Copy-on-Write 用户指南:https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html
- What's new in 3.0(升级/移除 API 完整清单):https://pandas.pydata.org/docs/whatsnew/v3.0.0.html
- 迁移到 3.0 的官方说明:https://pandas.pydata.org/docs/whatsnew/v3.0.0.html
- 10 minutes to pandas(快速上手):https://pandas.pydata.org/docs/user_guide/10min.html
- 实用速查表:pandas Cheat Sheet(官方 PDF,搜 "pandas cheat sheet")
建议把 pd.read_csv、groupby().agg()、merge、.loc、to_csv 这五个操作练熟,
就足以覆盖你 90% 的科研表格处理需求。遇到具体 API 疑问,优先查 3.0 官方文档。
评论交流
欢迎留下你的想法