用 pandas 按样本 ID 连接蛋白质序列与标签,避免数据错位
序列表和标签表即使行数相同,样本顺序也可能不同。如果按行位置直接附加标签,模型可能在没有报错的情况下学到错误的目标。
这篇教程用 sample_id 连接两个 CSV 文件,检查每个样本在两边都恰好出现一次,通过后才写出训练表。它可以接在 FASTA 检查之后,也与 SHA-256 清单教程互补:校验和记录使用了哪些文件,连接检查确认文件中的记录如何对应。
1. 先定义要匹配的记录
本例中,一个 sample_id 对应一条序列和一个分类标签。两个输入必须分别包含以下两列,不能多列或少列:
| 文件 | 列名 | 要求 |
|---|---|---|
sequences.csv |
sample_id,sequence |
每个唯一样本 ID 对应一条非空序列 |
labels.csv |
sample_id,label |
每个唯一样本 ID 对应一个非空标签 |
标识符按区分大小写的字符串处理,001 和 1 是不同 ID。脚本会拒绝首尾带空白的 ID,不会自动裁剪。它也允许字面值为 NA 的 ID,并单独检查空字段。
连接真实数据前,应先确定标识规则。如果一个蛋白质有多种实验条件下的测量,单独的 protein_id 可能无法标识一次测量。应使用合适的测量 ID,或明确定义蛋白质、实验方法和条件的组合。不要为了通过一对一检查而直接合并重复测量。
2. 安装依赖并创建示例文件
在新的工作目录中使用 Python 3.11 或更新版本。示例已在 macOS 的 Python 3.14.7 和 pandas 3.0.6 上测试。以下 Shell 命令适用于 Bash 或 Zsh。
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "pandas==3.0.6"
下载 join_labels.py,或把后面的完整代码保存为该文件。然后创建两个小型 CSV 文件:
python join_labels.py --help
python - <<'PY'
from pathlib import Path
Path("sequences.csv").write_text(
"sample_id,sequence\n001,ACDE\n002,FGHI\nNA,KLMN\n",
encoding="utf-8",
)
Path("labels.csv").write_text(
"sample_id,label\nNA,positive\n001,negative\n002,positive\n",
encoding="utf-8",
)
PY
python join_labels.py sequences.csv labels.csv joined.csv
序列和标签均为演示连接而构造,不代表生物学实验结果。标签顺序被有意打乱。预期输出:
OK: 3 sequences, 3 labels, 3 matched rows
生成的 joined.csv 内容如下:
sample_id,sequence,label
001,ACDE,negative
002,FGHI,positive
NA,KLMN,positive
输出保留序列表的行顺序,并为每个 ID 附上对应标签。仅凭两张表行数相同,无法证明这种对应关系。
3. 明确读取 ID,再检查连接
"""Join tidy sequence and label CSVs by sample_id, with a strict 1:1 policy."""
import argparse
from pathlib import Path
import sys
import warnings
import pandas as pd
def read_table(path, value_column):
with warnings.catch_warnings():
warnings.simplefilter("error", pd.errors.ParserWarning)
table = pd.read_csv(path, dtype="string", keep_default_na=False,
skip_blank_lines=False, index_col=False)
columns = ["sample_id", value_column]
if len(table.columns) != 2 or set(table.columns) != set(columns):
raise ValueError(f"{path}: expected exactly {columns}")
table = table[columns]
if table.empty:
raise ValueError(f"{path}: no data rows")
for column in columns:
values = table[column]
if values.isna().any() or values.str.strip().eq("").any():
raise ValueError(f"{path}: blank {column}")
ids = table["sample_id"]
if ids.ne(ids.str.strip()).any():
raise ValueError(f"{path}: sample_id has surrounding whitespace")
duplicates = ids[ids.duplicated(keep=False)].unique().tolist()
if duplicates:
raise ValueError(f"{path}: duplicate sample_id: {duplicates[:5]}")
return table
def join_tables(sequences, labels):
audit = sequences.merge(labels, on="sample_id", how="outer",
validate="one_to_one", indicator=True)
missing = audit.loc[audit["_merge"].eq("left_only"), "sample_id"].tolist()
extra = audit.loc[audit["_merge"].eq("right_only"), "sample_id"].tolist()
if missing or extra:
raise ValueError(f"ID mismatch: missing labels={missing[:5]}; "
f"labels without sequences={extra[:5]}")
# Use the left join only after the full coverage audit has passed.
return sequences.merge(labels, on="sample_id", how="left", sort=False,
validate="one_to_one")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("sequences", type=Path)
parser.add_argument("labels", type=Path)
parser.add_argument("output", type=Path)
args = parser.parse_args()
try:
sequences = read_table(args.sequences, "sequence")
labels = read_table(args.labels, "label")
joined = join_tables(sequences, labels)
# Exclusive creation preserves earlier outputs and the input files.
with args.output.open("x", encoding="utf-8", newline="") as handle:
joined.to_csv(handle, index=False, lineterminator="\n")
print(f"OK: {len(joined)} sequences, {len(labels)} labels, "
f"{len(joined)} matched rows")
return 0
except (OSError, ValueError, pd.errors.ParserError,
pd.errors.ParserWarning, pd.errors.EmptyDataError,
pd.errors.MergeError) as error:
print(f"ERROR: {error}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())
read_csv 文档说明了这里使用的解析选项。dtype="string" 保留前导零,keep_default_na=False 保留 NA 等字面文本,再由脚本自行拒绝空字段。skip_blank_lines=False 让空记录进入检查;解析警告会被当作错误处理,避免字段数量警告被忽略。
本例把标签保留为字符串。如果源系统用 NA、NULL 或其他标记表示标签缺失,需要在连接前明确加入相应规则。回归任务还需要数值转换、有限值检查,以及与任务有关的取值范围检查。非空文本并不等于有效目标值。
连接分两步进行。外连接检查通过 validate="one_to_one" 要求两边的键唯一,通过 indicator=True 找出只存在于一边的 ID。检查通过后,再用左连接按序列表顺序构造最终结果。merge API 文档说明了这些选项,并提醒空连接键可能互相匹配,因此必须提前拒绝缺失 ID。
4. 确认缺失记录会明确报错
创建一份缺少样本 002 的标签文件,保留原始文件:
python - <<'PY'
from pathlib import Path
Path("labels-missing.csv").write_text(
"sample_id,label\nNA,positive\n001,negative\n",
encoding="utf-8",
)
PY
python join_labels.py sequences.csv labels-missing.csv rejected.csv
printf 'exit code: %s\n' "$?"
预期输出:
ERROR: ID mismatch: missing labels=['002']; labels without sequences=[]
exit code: 2
程序不会创建 rejected.csv。它也会拒绝重复 ID、只存在于标签表的 ID、空字段和错误的列结构。每类错误最多展示五个示例 ID,用于定位问题,不是完整的检查结果导出。
成功运行的退出码为 0,数据检查或文件错误的退出码为 2。输出只允许新建文件,因此再次使用 joined.csv 运行成功示例,会提示 File exists 并保留原结果。如果有意修订数据,应使用新的输出文件名。
5. 避免用修补操作掩盖问题
内连接可能丢掉未匹配记录。左连接可能保留序列行,却留下缺失标签。先做外连接检查,可以在生成训练表之前看清两种方向的不匹配。
如果某个 ID 在两边都出现两次,多对多连接可能为它生成四行结果。pandas 连接指南解释了这种笛卡尔积扩展。不核对含义就删除重复项,可能掩盖互相冲突的测量,或错误合并不同样本。
对于真实的重复测量,要先定义分析单位。多条测量对应同一条序列记录时,多对一连接可能合适,但这属于另一种数据约定。应保留测量标识,并规定模型评估时如何把这些重复测量放在同一组。
脚本会把两张表读入内存,并构建中间检查表,适合能够放入内存的整洁数据表。它检查记录对应关系,不检查序列是否有效、实验标签是否正确或样本是否具有生物学独立性。不同样本 ID 仍然可能对应相同蛋白质序列。
6. 连接后继续保留对应关系
把 joined.csv 与源表一起保存,并将这三个文件纳入数据集校验和清单。同时记录连接脚本和依赖版本。
生成嵌入向量时,保留每一行向量对应的样本 ID。如果分批处理改变了顺序,拟合模型之前应依据这些 ID 重新排列标签。一份正确连接的 CSV 无法恢复已经丢失行映射的无标识嵌入矩阵。
接着应用按组划分策略。连接键唯一,并不能阻止相同序列、相关蛋白质或重复测量同时出现在训练集和测试集中。
7. 常见问题
| 报错或现象 | 处理办法 |
|---|---|
duplicate sample_id |
检查这些 ID 的全部记录。修复意外重复,或为真实重复测量重新设计键。 |
missing labels |
检查标签导出和预期样本集合。如果有意过滤样本,先记录排除规则再重跑。 |
labels without sequences |
检查两个文件是否来自不同数据版本,或上游是否丢弃了序列记录。 |
sample_id has surrounding whitespace |
确认对应关系后修正源 ID。不要盲目裁剪并合并原本不同的标识符。 |
blank label 或 blank sequence |
修复记录,或按明确记录的规则将其排除。 |
expected exactly 或解析错误 |
检查表头和分隔符,导出只包含所需两列的逗号分隔文件。 |
File exists |
保留旧结果,换用新的输出文件名。 |
示例已检查打乱标签顺序、前导零和字面值 NA 的 ID、两边各自的重复键、未匹配 ID、空字段、空白字符、格式错误的记录、空输入和覆盖保护。测试也核对了最终标签内容与行顺序。
让标识符始终跟着数据
按明确定义的样本标识连接,检查预期的对应关系,并在写出训练输入前处理未匹配记录。特征提取和评估阶段也要保留这些标识。这样才能在错误的标签对应变成模型结果之前,及时发现问题。
封面为概念插画。pandas 文档核对日期:2026 年 9 月 27 日。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/pandas-protein-label-join.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。