春江暮客

春江暮客的个人学习分享网站

用 pandas 按样本 ID 连接蛋白质序列与标签,避免数据错位

2026-09-27 技术
用 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 日。

友情链接

其它