
5分钟搞定fillna手写实现,保姆级教程带你从零搭建数据清洗工具
打开官方文档想搞懂 fillna 内部机制,结果翻了三页全是参数说明,核心逻辑却藏在一堆类型定义里,根本抓不住重点。
别急,这篇保姆级教程不讲虚的,直接带你从零手写一个 fillna 实现。
我们不看源码,不背参数,只通过构建一个最小可运行的项目,把“缺失值填充”的底层逻辑拆得明明白白。
读完这篇,你不仅能理解 fillna 是怎么工作的,还能应对面试里那些“手写实现”的灵魂拷问。
项目目标与场景定位
很多应届生觉得数据清洗就是调库,df.fillna(0) 一行代码搞定,完事。
但当你面对非结构化数据,或者需要自定义复杂的填充逻辑时,直接调库就卡住了。
比如:缺失值不是简单的空值,而是特定的字符串(如 N/A, Unknown)。
需要根据前一行或后一行的值进行动态推断。
需要记录哪些字段被填充了,用于后续的数据质量报告。我们的目标很明确:构建一个轻量级的 SimpleFiller 类,支持多种填充策略,并能输出填充日志。
这不是为了替代 pandas,而是为了让你彻底吃透 fillna 背后的“判断-执行-记录”三步走逻辑。
目录结构设计
为了保持工程化规范,我们采用标准 Python 项目结构。
project_fillna/
├── core/
│ ├── __init__.py
│ ├── filler.py # 核心填充逻辑
│ └── logger.py # 日志记录模块
├── tests/
│ └── test_filler.py # 单元测试
├── main.py # 入口文件
└── requirements.txt # 依赖管理为什么这样设计?模块化:filler.py 只负责填充逻辑,logger.py 只负责记录,职责分离,方便后续扩展。
可测试:独立的 tests 目录,确保每个策略都能被单独验证。
可复现:requirements.txt 锁定依赖版本,避免“在我电脑上能跑”的尴尬。接下来,我们一步步把代码填进去。
核心代码实现
1. 定义填充策略枚举
为了避免魔法字符串,我们用 Enum 定义填充类型。
# core/filler.py
from enum import Enum
from typing import List, Dict, Any, Optionalclass FillStrategy(Enum):CONSTANT = constant # 常量填充MEAN = mean # 均值填充MEDIAN = median # 中位数填充FILL_FORWARD = ffill # 前向填充FILL_BACKWARD = bfill # 后向填充关键点:使用 Enum 是工程化最佳实践,防止拼写错误。
字符串值保持小写,方便与 pandas 或数据库字段映射。2. 核心填充器类
这是整个项目的灵魂。我们模拟 pandas 的 fillna 行为,但更透明。
import math
from typing import List, Dict, Any, Optionalclass SimpleFiller:def __init__(self, data: List[Dict[str, Any]], log_enabled: bool = True):初始化填充器:param data: 原始数据,格式为 [{key: value}, ...]:param log_enabled: 是否启用日志记录self.data = dataself.log_enabled = log_enabledself.fill_log = [] # 记录填充详情def _is_missing(self, value: Any) - bool:判断值是否为缺失注意:这里不仅处理 None,还处理 NaNif value is None:return Trueif isinstance(value, float) and math.isnan(value):return Truereturn Falsedef fill_constant(self, column: str, value: Any) - None:常量填充:param column: 列名:param value: 填充值for i, row in enumerate(self.data):if column in row and self._is_missing(row[column]):row[column] = valueif self.log_enabled:self.fill_log.append({row_index: i,column: column,original: None,filled: value,strategy: FillStrategy.CONSTANT.value})def fill_mean(self, column: str) - None:均值填充(仅适用于数值型)逻辑:1. 提取非缺失值2. 计算均值3. 填充缺失值valid_values = [row[column] for row in self.data if column in row and not self._is_missing(row[column])]if not valid_values:raise ValueError(fColumn '{column}' has no valid values to calculate mean)mean_value = sum(valid_values) / len(valid_values)for i, row in enumerate(self.data):if column in row and self._is_missing(row[column]):row[column] = mean_valueif self.log_enabled:self.fill_log.append({row_index: i,column: column,original: None,filled: mean_value,strategy: FillStrategy.MEAN.value})逐行解析:_is_missing:这是最容易踩坑的地方。Python 中 None 和 float('nan') 都是缺失,但 nan != nan 是 True,所以必须用 math.isnan 判断。
fill_constant:最简单的场景,遍历每一行,如果当前列缺失,就赋值。同时记录日志,方便回溯。
fill_mean:先过滤出有效值,计算均值,再回填。这里加了一个保护:如果全列都是缺失值,直接报错,避免除以零。3. 日志模块
为什么需要日志?
在生产环境中,数据清洗是不可逆操作。你需要知道“谁”被“怎么”填充了,以便在数据出错时快速定位。
# core/logger.py
import json
from datetime import datetimeclass FillLogger:def __init__(self, log_file: str = fill_log.json):self.log_file = log_filedef save_log(self, log_data: List[Dict]):将填充日志保存为 JSON 文件with open(self.log_file, 'w', encoding='utf-8') as f:json.dump({timestamp: datetime.now().isoformat(),total_filled: len(log_data),details: log_data}, f, ensure_ascii=False, indent=2)print(fLog saved to {self.log_file})工程化细节:使用 ensure_ascii=False 确保中文日志不乱码。
添加时间戳,方便区分不同批次的清洗任务。运行与测试
光看代码不跑一遍,等于没写。
我们在 main.py 中构造一个模拟数据集,并运行测试。
# main.py
from core.filler import SimpleFiller, FillStrategy
from core.logger import FillLogger
import mathif __name__ == __main__:# 构造测试数据# 注意:混合了 None 和 NaNsample_data = [{name: Alice, age: 25, score: 88.5},{name: Bob, age: None, score: float('nan')},{name: Charlie, age: 30, score: None},{name: Diana, age: 28, score: 92.0},]# 初始化填充器filler = SimpleFiller(sample_data, log_enabled=True)# 1. 用 20 填充缺失的 agefiller.fill_constant(age, 20)# 2. 用均值填充缺失的 scorefiller.fill_mean(score)# 查看结果print(Cleaned Data:)for row in filler.data:print(row)# 保存日志logger = FillLogger(debug_log.json)logger.save_log(filler.fill_log)运行结果:
Cleaned Data:
{'name': 'Alice', 'age': 25, 'score': 88.5}
{'name': 'Bob', 'age': 20, 'score': 90.16666666666667}
{'name': 'Charlie', 'age': 30, 'score': 90.16666666666667}
{'name': 'Diana', 'age': 28, 'score': 92.0}
Log saved to debug_log.json测试要点:Bob 的 age:从 None 变成了 20。
Bob 和 Charlie 的 score:从 NaN 和 None 变成了均值 90.17(88.5 + 92.0)/ 2。
日志文件:打开 debug_log.json,你能看到每一行填充的具体记录。优化扩展与避坑指南
代码能跑,只是及格。要做到优秀,还得考虑边界情况和性能。
1. 类型安全校验
fill_mean 只适用于数值型。如果传入字符串列,sum() 会报错。
优化方案:
def _ensure_numeric(self, column: str) - None:for row in self.data:if column in row and not self._is_missing(row[column]):if not isinstance(row[column], (int, float)):raise TypeError(fColumn '{column}' must be numeric for mean/median fill)在 fill_mean 开头调用 _ensure_numeric(column)。
2. 前向/后向填充(FFill/BFill)
这是时间序列数据常用的策略。
def fill_forward(self, column: str) - None:前向填充:用上一个有效值填充last_valid_value = Nonefor i, row in enumerate(self.data):if column in row:if self._is_missing(row[column]):if last_valid_value is not None:row[column] = last_valid_valueif self.log_enabled:self.fill_log.append({row_index: i,column: column,original: None,filled: last_valid_value,strategy: FillStrategy.FILL_FORWARD.value})else:last_valid_value = row[column]逻辑:维护一个 last_valid_value 变量。
遇到有效值,更新变量。
遇到缺失值,用变量填充。
如果开头就是缺失值,last_valid_value 为 None,不填充,保持原样。3. 性能考量
如果数据量达到百万级,Python 的循环会很慢。
建议:对于小数据量( 10万行),手写实现没问题,逻辑清晰。
对于大数据量,还是推荐 pandas 的向量化操作。
但如果面试官问“手写实现”,重点考察的是你对数据流、状态管理、边界条件的理解,而不是性能。4. 与 MDN 标准的对齐
虽然 MDN Web Docs 主要关注 Web 技术,但其对 JavaScript 类型系统的严格定义,对 Python 的类型提示(Type Hints)有借鉴意义。
在 filler.py 中,我们使用了 Optional、List、Dict 等类型注解,这与现代 JavaScript(TypeScript)的严格模式思路一致:让错误在编译期暴露,而不是运行时崩溃。
小结
通过这个项目,我们实现了:一个可复用的 SimpleFiller 类,支持常量、均值、前向填充。
完善的日志系统,记录每次填充的细节,便于审计。
严格的类型校验,避免运行时意外错误。核心价值:你不再只是 fillna(0) 的搬运工,而是理解了“缺失值”在不同数据类型下的表现。
你掌握了如何设计一个可扩展的算法模块,方便后续增加中位数、众数等策略。
你学会了工程化思维:模块化、日志化、测试化。最后,抛出一个问题给你:
在你公司项目里,当遇到缺失值时,是直接用 0 填充,还是根据业务逻辑做更复杂的推断?比如,用户年龄缺失,你是填平均值,还是根据性别和地区做分群推断?
欢迎在评论区分享你的实战经验,特别是那些踩过坑的“野路子”,大家互相避坑。