资讯动态

LLM开发必备:用Pandas搞定80%数据处理难题

发布时间:2026/10/14 14:31:30 来源:尧图企业网站定制
做 LLM 相关开发这一年多我最大的感受是模型架构本身已经不是最耗时间的环节了真正让人头秃的是数据。不管是微调一个开源底座模型还是给 RAG 应用准备知识库又或者是做 SFT 指令数据每天打交道最多的库就是 Pandas。你要是跟我一样从初学者的角度把 Pandas 用熟会发现它几乎能覆盖 LLM 开发中 80% 的数据处理需求。这篇文章不是给你念 API 文档而是把我自己在 LLM 项目里用 Pandas 处理数据的完整思路、常用代码片段、以及踩过的坑都整理出来。从最基础的读写数据开始一路讲到 Token 统计、Embedding 向量管理、训练集构建这些进阶操作。无论你是刚入门想找方向还是已经写了几个预处理脚本但总觉得不够系统这篇内容都值得你花二十分钟看完很多细节我是真金白银换来的教训。1. LLM 开发为什么绕不开 Pandas1.1 数据在 LLM 项目里的真实占比跟你讲个我自己的经历。去年我做了一个垂直领域的对话机器人项目前前后后大概花了三个月其中调模型的 Prompt 用了不到一周真正让我加班到凌晨的全是在处理数据把不同渠道收集来的问答记录去重、清洗、格式化再按业务场景分类打标最后还要统计各个类别的样本量是不是均衡。这个比例其实在行业里很常见。模型就像一台发动机数据就是燃料。你拿劣质燃料去跑发动机再好也白搭。放在 LLM 开发里就是数据质量决定了模型表现的上限Prompt 和微调只是在逼近这个上限而已。Pandas 之所以在 LLM 开发里这么重要是因为它的 DataFrame 结构天生适合做表格型数据的处理。而 LLM 的数据无论是文本语料、指令对、还是向量化之后的 Embedding本质上都能表达成表格行是样本列是特征。比如指令微调数据每行就是一条指令样本包含 instruction、input、output 三列RAG 知识库的数据每行就是一段文本和它的元信息评估集数据每行就是一个测试用例和预期答案。1.2 从原始文本到训练语料的典型链路我总结了一下LLM 项目里数据处理大概走这么一条链路数据采集 → 数据清洗 → 格式转换 → 统计分析 → 数据拆分 → 质量抽检。拿我曾经做的那个对话机器人举例。原始数据是从某企业内部系统导出的对话日志长什么样有缺失的字段、有重复的问答、有包含敏感信息的记录还有大量闲聊内容跟业务无关。这种数据直接拿去微调模型学到的全是噪音。这时候 Pandas 就派上用场了。我可以用drop_duplicates()去掉完全重复的样本用dropna()处理缺失值用str.contains()配合正则把无关内容筛掉再用groupby()看各个类别的分布情况。整个过程像流水线一样每一步都在 DataFrame 上操作清晰又高效。这个能力在 LLM 开发里尤其稀缺。因为 LLM 的数据处理不像传统机器学习那样特征工程做好就完事了。它需要你在文本层面做大量精细操作比如判断两段话是不是语义重复、需要按 Token 数截断超长文本、需要把原始对话改造成指令格式。没有 Pandas这些工作就得写一堆纯 Python 循环代码又长又难维护。有了 Pandas大部分操作变成了一行方法调用。2. 初学必先掌握的 Pandas 核心操作2.1 读取数据别只会 read_csv我见过很多初学者数据文件一进来就pd.read_csv()一把梭。LLM 项目里的数据格式其实五花八门JSON、JSONL、Parquet、Excel 都常见。尤其是 JSONL在 LLM 数据里太常见了——每条数据一行格式灵活适合流式处理。import pandas as pd # 读取 JSONL这个在 LLM 项目里是最常见的 df pd.read_json(sft_data.jsonl, linesTrue) # 读取 Parquet如果你处理的是大规模向量数据 df pd.read_parquet(embeddings.parquet) # 从 JSON 字符串列展开成多列 df pd.read_json(raw_data.json)这里有个容易踩坑的地方JSONL 文件如果嵌套层级很深直接读进来会得到一堆 dict 对象列。这时候需要配合json_normalize()把嵌套结构展开。我处理过一批第三方 API 返回的数据每条记录里面套了两层options列表不展开根本没法分析。from pandas import json_normalize records [...] # 原始 JSON 数组 df json_normalize(records, sep_)sep_的作用是让嵌套字段的层级路径用下划线连接比如options_0_text这样列名不会带点号后续用df[options_0_text]访问也方便。2.2 筛选与清洗把脏数据挡在模型之前数据清洗在我眼里是 Pandas 最核心的战场。LLM 数据的脏跟传统数据的脏还不太一样。它不只是有空值、有重复还有格式混乱、混入非目标语言、包含 HTML 标签、有超长文本截断了一半等等。空值和重复值是最基础的处理起来也简单# 丢到全为空的行 df.dropna(howall, inplaceTrue) # 子集去重留下第一条 df.drop_duplicates(subset[instruction, output], keepfirst, inplaceTrue)但实际项目里更麻烦的是那些看起来不一样、其实是一回事的重复数据。比如问答对里有人问“怎么退款”有人问“退款流程是啥”这两条从字面上看完全不同但对模型训练来说就是重复样本。这种语义层面的去重单纯靠 Pandas 做不到需要引入 Embedding 相似度计算。我的做法是先用 Pandas 把文本列批量向量化再用向量相似度矩阵筛掉高相似度的样本。这个后面在 Embedding 管理那节详细说。清洗这一步还经常用正则配合str方法# 去掉 HTML 标签 df[text] df[text].str.replace(r[^], , regexTrue) # 去掉多余的空白字符 df[text] df[text].str.replace(r\s, , regexTrue).str.strip() # 过滤掉包含敏感词的样本举例replace 为你的敏感词库 sensitive_words [词A, 词B, 词C] mask df[text].str.contains(|.join(sensitive_words)) df df[~mask]这里面有个性能优化点值得说用str.replace()带regexTrue比用 Python 原生的re.sub()配合apply()快很多因为 Pandas 的字符串方法底层是向量化的。数据量上万条之后apply()和向量化操作的差距非常明显。我一开始习惯用df[text].apply(lambda x: re.sub(...))处理 10 万条数据跑了快一分钟换成str.replace之后几秒钟就搞定了。2.3 apply 与向量化从循环思维里跳出来很多从纯 Python 转过来的初学者习惯性写for i in range(len(df))这种循环。在 Pandas 里这是最不推荐的做法除非万不得已。原因很简单Python 循环逐行访问 DataFrame每一步都有类型检查、索引开销效率极低。正确的方式是用apply()处理逐行逻辑用向量化操作处理列级逻辑。# 不推荐纯粹 Python 循环 for idx, row in df.iterrows(): df.loc[idx, text_len] len(row[text]) # 推荐apply 逐行处理 df[text_len] df[text].apply(lambda x: len(x)) # 更推荐直接 len 函数配合 str df[text_len] df[text].str.len()在 LLM 数据预处理里apply()最常见的场景就是对每条文本做统一处理比如加 Prompt 模板、拼接上下文、调用 Tokenizer 计算 Token 数。df[prompt] df[input].apply(lambda x: f请回答以下问题{x}) def count_tokens(text): # 这里调用你的 tokenizer return len(tokenizer.encode(text)) df[token_count] df[text].apply(count_tokens)要注意的是apply()传进去的函数里不要做全局状态的修改否则并行化的时候会出问题。如果你想用多进程加速Pandas 的apply()支持传入args参数或者你可以试试swifter这个库它能在apply()基础上自动选择并行方式。3. LLM 场景下的高级数据处理技巧3.1 Token 统计与成本预估这个是我觉得很多教程都不会讲、但实际项目里特别实用的点。做 LLM 开发Token 是什么就是模型处理文本的最小单位大概可以理解成半个词。你调用 API、你微调模型、你做推理都是按 Token 计费的。所以提前估算 Token 数量直接关系到你的项目预算和方案选型。用 Pandas 做 Token 统计很方便。先给每条文本算 Token 数然后汇总统计# 假设 df 里有一列 text 是你要发给模型的内容 df[token_count] df[text].apply(lambda x: len(tokenizer.encode(x))) # 总 Token 数、平均 Token 数、最大最小 total_tokens df[token_count].sum() mean_tokens df[token_count].mean() max_tokens df[token_count].max() min_tokens df[token_count].min()当时我做 RAG 知识库的时候要评估 5 万篇文档的向量化成本。用 Pandas 统计完发现平均每篇文档 1200 Token总计 6000 万 Token。按当时的 Embedding 接口价格一算光向量化一次就要花不少钱而且后续增量更新也要持续投入。这个数字直接影响了技术选型——后来我们改用更经济的模型并且做了分段向量化大大压缩了成本。Token 均值还能帮你判断 Prompt 设计是否合理。我有一次调试一个分类任务发现输入 Prompt 平均 1500 Token但其中 90% 都是背景说明文本真正需要模型判断的只有最后两句话。这种设计既费钱又容易让模型注意力分散。用 Pandas 按场景分组统计 Token 分布能很直观地发现问题。3.2 长文本处理与截断策略LLM 有上下文窗口限制比如有的模型是 4K、8K、32K。实际业务里的文档经常超过这个长度。那个经典的问题就来了直接截断会丢失信息不截断又放不下。我用 Pandas 做过一套滑动窗口式的长文本拆分方案。核心思路是把一条长文本按固定 Token 数量切成多段每段之间保留一定的重叠区域避免切在语义边界上。这样既能保证每段都在上下文窗口内又能保持文本的连贯性。def split_long_text(text, chunk_size800, overlap100): tokens tokenizer.encode(text) chunks [] start 0 while start len(tokens): end min(start chunk_size, len(tokens)) chunk tokenizer.decode(tokens[start:end]) chunks.append(chunk) if end len(tokens): break start end - overlap return chunks # 每条原始文档展开成多行 df_exploded df.assign(chunksdf[text].apply(split_long_text)).explode(chunks)里面用了一个 Pandas 很有用的功能explode()。它能把一个列表类型的单元格拆成多行其他列自动复制。这个操作在 LLM 数据处理里非常常用。比如你有 5 万篇文章切完之后变成 12 万个片段每行一个片段。后续做 Embedding、建索引都是在这张展开后的表上进行。还有一点要注意不同 Tokenizer 的切分方式不同tokenizer.encode()返回的是 Token ID 列表decode()把 Token ID 列表还原成文本。这两个方法配对使用才能保证切出来的片段是合法的文本不会出现半个词的情况。3.3 Embedding 向量管理向量检索是 RAG 的基础。每个文本片段经过 Embedding 模型变成一串浮点数通常是几百到几千维。Pandas 处理向量数据的方式跟普通列不太一样。我的常见做法是把向量存在一个 list 类型的列里然后利用 Pandas 的apply()做运算。# 假设 df 有文本片段和对应的向量列 df[embedding] df[text].apply(lambda x: embed_model.encode(x)) # 计算每个向量跟某个查询向量的余弦相似度 query_vec embed_model.encode(怎么申请退款) def cosine_sim(v): dot sum(a * b for a, b in zip(v, query_vec)) norm_v sum(a * a for a in v) ** 0.5 norm_q sum(a * a for a in query_vec) ** 0.5 return dot / (norm_v * norm_q) df[similarity] df[embedding].apply(cosine_sim) df.sort_values(similarity, ascendingFalse, inplaceTrue)这个写法数据量小的时候没问题比如几千条。但是向量数据一上万用 Python 循环算相似度就很吃力了。我处理过 20 万条向量一个查询跑下来要几分钟完全没法接受。后来我的做法是向量列在 Pandas 里只做存储和管理真算相似度的时候把整个向量列转成 NumPy 矩阵一次矩阵乘法搞定所有相似度。import numpy as np # 把向量列转成二维矩阵 matrix np.array(df[embedding].tolist()) # 批量计算余弦相似度假设 query_vec 已经归一化 query_norm query_vec / np.linalg.norm(query_vec) matrix_norm matrix / np.linalg.norm(matrix, axis1, keepdimsTrue) sims matrix_norm query_norm # 把相似度放回 DataFrame df[similarity] sims20 万条向量整个计算过程不到一秒。这就是向量化思维带来的差距。Pandas 和 NumPy 配合使用各司其职Pandas 负责数据管理、筛选、分组NumPy 负责大规模数值计算。还有一个小技巧如果你要存 Embedding 到磁盘不要直接存涵 Python 列表的 CSV那样文件巨大且读写极慢。建议把向量列单独抽出来存成.npy文件文本和元信息存 Parquet。用 Pandas 处理的时候再合并到一起。# 保存向量 np.save(embeddings.npy, np.array(df[embedding].tolist())) # 保存元信息 df.drop(columns[embedding]).to_parquet(meta.parquet)4. 训练/评估数据集构建的完整流程4.1 数据划分别让训练集和测试集互相串门做 LLM 微调最容易被忽视的问题就是数据泄漏。什么叫数据泄漏就是训练集和测试集之间存在重叠或高度相似的样本。模型在训练时已经见过测试集的内容评估出来的指标虚高上线之后立刻被打回原形。我当时做一个分类微调任务按 8:2 划分训练集和测试集前期评估准确率有 95%上线之后直接掉到 70%。排查了半天最后发现是原始数据里同一个问题出现了多次只是措辞略有不同。训练集和测试集各分到了一部分模型相当于开卷考试。解决思路有两个层面。第一层是字面去重这个 Pandas 直接能做df.drop_duplicates(subset[instruction], inplaceTrue)第二层是语义去重。我后来养成了一个习惯每次划分数据之前先做一次 Embedding 相似度检查。用 3.3 节的方法把每条样本的向量都算出来然后两两比对相似度把相似度超过 0.9 的样本合并到同一个集合里再去做随机划分。这样能最大程度避免换了个说法但其实是一道题的泄漏。from sklearn.model_selection import train_test_split train_df, eval_df train_test_split( df, test_size0.2, random_state42, stratifydf[label] )stratify参数也很重要。如果你的数据类别不平衡比如正样本 90%负样本 10%不做分层抽样的话小类样本可能全部跑到测试集里或者全留在训练集里。stratifydf[label]能保证训练集和测试集的类别分布跟原始数据一致。4.2 指令微调数据格式转换SFT监督微调的数据格式这几年经历了不少变化但核心结构是一致的一条训练样本就是一个指令、一个输入可选、一个输出。用 Pandas 把原始数据统一成这种格式是我的日常操作。def to_sft_format(row): if pd.isna(row[input]) or row[input] : prompt f### 指令{row[instruction]}\n\n### 回答 else: prompt f### 指令{row[instruction]}\n\n### 输入{row[input]}\n\n### 回答 return { prompt: prompt, response: row[output], label: row[label] if label in row else None } sft_data df.apply(to_sft_format, axis1).tolist()这里有个细节容易踩坑apply(axis1)会把每一行作为一个 Series 传给函数性能并不算好。数据量超过 10 万条的时候建议用itertuples()代替sft_data [] for row in df.itertuples(indexFalse): if row.input is None or row.input : prompt f### 指令{row.instruction}\n\n### 回答 else: prompt f### 指令{row.instruction}\n\n### 输入{row.input}\n\n### 回答 sft_data.append({prompt: prompt, response: row.output})itertuples()生成的是命名元组属性访问比apply(axis1)里的row[xxx]快一个数量级。在几万条数据的场景下体感差别很明显。生成好之后把列表转换成 DataFrame再按需求导出成 JSONLsft_df pd.DataFrame(sft_data) sft_df.to_json(sft_format.jsonl, orientrecords, linesTrue, force_asciiFalse)force_asciiFalse这个参数别漏了。默认情况下 Pandas 导出 JSON 会把中文转成\uXXXX转义序列文件看着费劲不说部分微调框架读取的时候还可能出问题。4.3 数据质量评估分布、长度、标签平衡数据整理完之后正式训练之前我习惯用 Pandas 做一轮质量体检。这个过程不复杂但对后续训练效果影响巨大。先看长度分布df[prompt_len] df[prompt].str.len() df[response_len] df[response].str.len() print(df[[prompt_len, response_len]].describe())describe()会输出每列的平均值、标准差、最小值、最大值、四分位数。我一般重点看两个指标回答长度是不是有异常值有的回答只有一两个字大概率是脏数据Prompt 长度是不是在合理区间内。再看标签分布df[label].value_counts(normalizeTrue)normalizeTrue会输出占比而不是单纯的数量。比如你看到某个类别占比 0.2%而其他类别都在 10% 以上说明这个类别的样本量严重不足。对这种小类要么补充采集要么在训练时提高它的采样权重否则模型基本学不会这类样本。有一个我常用的可视化辅助手段是配合matplotlib画长度分布直方图import matplotlib.pyplot as plt plt.figure(figsize(10, 4)) plt.hist(df[response_len], bins50) plt.xlabel(Response Length) plt.ylabel(Count) plt.show()直方图能非常直观地暴露数据长尾问题。我曾经处理过一份数据大部分回答在 100~300 字但有一小撮回答超过了 1000 字点开一看是把参考文档整段复制进去了。这种异常样本如果不筛掉模型很容易学会回答越权的坏习惯。5. 实战处理一份真实的 LLM 训练语料5.1 场景设定与数据说明拿我最近做的一个项目举例。某公司想做一个内部客服助手需要基于他们的历史工单记录微调一个模型。原始工单数据是从客服系统导出的包括工单号、用户问题、客服回复、工单分类、处理时间等字段。数据总量大约 8 万条格式是.csv。这个项目的数据痛点很典型大量无效短句比如你好谢谢工单分类是文本描述不统一比如退款问题有人写成退款部分客服回复是从模板复制的重复率很高还有涉及到隐私的部分需要脱敏处理。整个处理流程我拆成了四步全程用 Pandas 完成。5.2 完整处理流程代码import pandas as pd import re # 1. 读取原始数据 raw_df pd.read_csv(tickets_2024.csv, encodingutf-8) # 2. 基础清洗 # 去掉完全重复的工单 raw_df.drop_duplicates(subset[ticket_id], inplaceTrue) # 去掉问题或回复为空的记录 raw_df.dropna(subset[question, answer], inplaceTrue) # 去掉无效短句问题或回复少于5个字符 raw_df raw_df[raw_df[question].str.len() 5] raw_df raw_df[raw_df[answer].str.len() 5] # 3. 文本规范化 raw_df[question] raw_df[question].str.replace(r\s, , regexTrue).str.strip() raw_df[answer] raw_df[answer].str.replace(r\s, , regexTrue).str.strip() # 4. 分类字段统一 def normalize_category(cat): if pd.isna(cat): return 其他 cat str(cat).strip() if 退款 in cat or 退货 in cat: return 售后 if 物流 in cat or 快递 in cat: return 物流 if 账号 in cat or 登录 in cat: return 账号 return cat raw_df[category] raw_df[category].apply(normalize_category) # 5. 按类别查看分布 print(raw_df[category].value_counts()) # 6. 按用户问题做语义去重前的字面去重 raw_df.drop_duplicates(subset[question], keepfirst, inplaceTrue) # 7. 添加 Token 估算列方便后续控制训练长度 def estimate_tokens(text): # 中文场景下一个字符约对应 1~1.5 个 token return int(len(text) * 1.2) raw_df[question_tokens] raw_df[question].apply(estimate_tokens) raw_df[answer_tokens] raw_df[answer].apply(estimate_tokens) # 8. 过滤超长样本比如超过 2000 token 的回复可能是复制粘贴 raw_df raw_df[raw_df[answer_tokens] 2000] # 9. 只保留后续训练需要的列 final_df raw_df[[question, answer, category]].reset_index(dropTrue) # 10. 导出为 JSONL final_df.to_json(clean_tickets.jsonl, orientrecords, linesTrue, force_asciiFalse)5.3 过程中的关键观察与结果这个流程跑完之后8 万条原始数据剩下了 5.6 万条过滤率大约 30%。其中去掉的主要是无效短句、重复工单和超长回复。这个过滤率在真实项目里非常正常甚至有些更脏的数据能过滤掉一半以上。清洗过程中有个意外发现分类字段不统一的严重程度远超预期。原数据里退款和退货被分成两个类但业务上其实是一个流程物流问题里混了大量快递在哪的相似提问。用normalize_category合并之后类别数从 27 个降到了 9 个每个类别都积累了足够的样本量。这对后续分类微调或者指令微调都特别有利。不过我得说句实话这个流程虽然跑通很顺利但第一次做的时候每一步都踩过坑。比如最开始我用dropna(subset[question, answer])没加subset直接把含任何空值的行全删了结果把很多有分类但没有回答的模板数据也删掉了白白损失了一批样本。6. 避坑指南与性能优化6.1 新手常见的五个 Pandas 坑我从实际项目里踩过的坑里挑了五个最容易反复出现的。整理成一张表有类似问题的直接对着查就行。问题表现根本原因解决方案dropna 删掉了不该删的行没有指定 subset把所有含空值的行都删了显式添加subset[列名]读完 JSON 后列名带点号嵌套字段没展开点号路径留在列名里用json_normalize展开export 到 JSONL 中文变成转义字符没有加force_asciiFalse导出时加上force_asciiFalse处理 10 万条数据卡到怀疑人生用 iterrows 或 apply 循环逐行操作改成向量化操作或itertuples修改 DataFrame 后原表也变了浅拷贝导致的 SettingWithCopyWarning用.copy()显式深拷贝copy()这个问题很隐蔽。我有一次做数据清洗写了df2 df[df[label] A]然后对df2做修改结果发现df也跟着变了。原因是df2只是df的一个视图View修改它会影响原表。解决办法是加.copy()df2 df[df[label] A].copy()养成这个习惯之后我再也没有因为副本问题半夜爬起来回滚数据。6.2 大数据量下的性能优化技巧LLM 数据处理经常要面对几十万甚至上百万条记录性能问题躲不开。我在实际项目里常用的优化思路有三条。第一条能用内置方法就不用 apply。str.replace、str.contains、str.extract这些内置字符串方法都是向量化实现底层走的是 C 循环比 Python 级别的 apply 快一个量级。第二条处理超大文件时不要一次读入内存。用read_csv的chunksize参数分块读取逐块清洗后追加写入。chunk_size 10000 output_file cleaned_all.csv for chunk in pd.read_csv(huge_file.csv, chunksizechunk_size): # 清洗流程 chunk chunk.dropna(subset[text]) chunk[text] chunk[text].str.strip() # 追加写入 if os.path.exists(output_file): chunk.to_csv(output_file, modea, headerFalse, indexFalse) else: chunk.to_csv(output_file, indexFalse)第三条对列进行筛选只保留需要的列。有时候原始数据有一堆无关列比如工单号、处理时间、操作员 ID这些对模型训练没有用。越早 drop 掉内存压力越小。还有一条我看很多人忽略Pandas 的字符串列默认是 object 类型会占用大量内存。如果你确定某个列只包含有限几种取值可以转成 category 类型df[category] df[category].astype(category)这一行操作在某些场景下能省掉 70% 的内存占用。处理几百万行数据的时候这个优化的收益非常直观。6.3 性能之外保证可复现性还有一点跟性能无关但我觉得值得单独说一下Pandas 的操作最好写成脚本而不是在 Notebook 里随手执行。我在项目里习惯把所有数据清洗流程写成一个.py文件输入输出路径用参数控制。这样每次拿到新数据跑一遍脚本就能得到同样的清洗结果不用从头点一遍。这个习惯帮了我大忙。有一次负责的模型要复现实验结果团队里有人直接用了一份被重复清洗过的数据做训练指标完全对不上。后来我拿出清洗脚本重新跑了一遍原始数据才发现是数据预处理不一致导致的。从那以后数据清洗流程纳入版本管理就成了我项目的硬性要求。做了这么多 LLM 项目我的体会是Pandas 的价值不在于它有多酷而在于它让数据处理变得可检查、可复现、可迭代。你写的每一个清洗步骤都可以随时回看中间结果你改动的每一行代码都可以立刻看到它对数据分布的影响。这种掌控感在 LLM 开发这种数据主导的项目里特别重要。最后再分享一个小技巧。每次处理完数据我都会顺手生成一份简单的数据报告总量、类别分布、平均长度、异常样本数量。这份报告既方便自己复盘也方便跟团队同步数据情况。用 Pandas 写这个报告基本就是几行代码的事但它的价值远比你想象的更大。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑