01 引言
在 python 开发中,我们常常会遇到需要表示“缺失值”的场景。无论是处理 api 返回的数据、解析用户输入,还是管理缓存状态,开发者们的第一反应往往是使用 none。然而,随着代码规模的增长和业务逻辑的复杂化,none 的滥用却可能悄无声息地埋下隐患。
比如在这样一个场景:你正在编写一个用户信息处理函数,当用户未提供邮箱时,你希望回退到一个默认地址。于是,你写下了这样的代码:
def send_notification(to=none): if to is none: to = "default@example.com" # 发送邮件...
表面上,这段代码逻辑清晰,但问题在于,none 在这里承载了多重含义:它可能表示用户确实没有提供邮箱,也可能是某个中间步骤尚未初始化数据,甚至可能是开发者故意传递的合法值。这种语义上的模糊性,会在后续维护中逐渐显现出它的破坏力——比如,当另一个开发者试图区分“未设置邮箱”和“主动取消订阅”时,none 根本无法提供足够的信息。
更糟糕的是,none 与 python 中其他“假值”(如空字符串""、数字 0、布尔值 false)的行为高度相似。一个本应检查数据是否存在的条件判断,可能因为某个意外传入的 0 而错误地执行了回退逻辑。这类问题在调试时尤其棘手,因为日志中只会显示一个孤零零的 none,而无法告诉你它究竟代表什么。
这就是为什么我们需要更好的工具。与其依赖none
这一模糊的占位符,python 开发者可以通过**自定义哨兵对象(sentinel objects)**来明确表达意图。哨兵对象是独一无二的实例,专门用于标记“缺失”或“未初始化”状态,既避免了与合法值的冲突,又能让代码逻辑一目了然。接下来的内容,我们将深入探讨如何用哨兵对象重构代码,从而让缺失值的处理变得更安全、更可维护。
02 none 的局限性
2.1 一个值,多重含义
none 最常见的滥用场景之一,就是被迫承担多种不同的含义。例如,在初始化一个对象属性时,开发者可能会这样写:
class userprofile: def __init__(self): self.cache = none # 表示"稍后填充"
这里的 none 仅仅表示“缓存尚未加载”,但同一段代码的其他部分可能会误以为 none 代表“缓存已被清空”或“缓存不可用”。这种模糊性使得代码的可读性下降,尤其是在团队协作时,不同的开发者可能会对 none 的含义做出不同的假设。
更复杂的情况出现在 api 或数据处理中。假设我们有一个函数,负责解析用户的订阅状态:
def get_subscription_status(user): status = user.get("subscription", none) if status is none: return "inactive" # 是用户未订阅,还是数据缺失?
此时,none 可能代表两种完全不同的情况:
- 数据缺失(用户记录中没有 subscription 字段);
- 显式取消(用户主动退订,字段被设为 none)。 如果业务逻辑要求区分这两种情况,none 显然无法胜任。
2.2 与 python 假值的冲突
none 的另一个问题在于,它和 python 中的其他“假值”(falsy values)行为相似,容易导致意外的逻辑错误。例如:
def process_value(value): if not value: # 不仅检查none,还会过滤0、""、false等 value = default_value
2.3 难以追溯的空值来源
当系统出现问题时,日志中的 none 往往无法提供足够的上下文。例如,在数据处理流水线中,某个字段突然变成 none,开发者需要排查:
- 是上游数据源遗漏了这个字段?
- 是某个中间步骤显式清空了它?
- 还是代码逻辑错误地覆盖了原有值?
如果使用自定义哨兵,就能在日志中清晰区分不同的空值状态,大幅缩短调试时间。
03 哨兵对象解决方案
在认识到 none 的种种局限性后,我们需要一种更精确、更安全的替代方案。哨兵对象(sentinel objects)正是为此而生,它通过创建一个独特的、不可混淆的对象实例,为缺失值提供了明确的语义表达。
哨兵对象最简单的实现方式就是创建一个普通的 object 实例:
missing = object()
由于 python 中每个 object()都会生成一个全新的唯一标识,missing 对象不会与任何其他值产生冲突。在实际使用时,我们可以清晰地表达意图:
def get_config_value(key): value = config.get(key, missing) if value is missing: raise configerror(f"missing required config: {key}")
这种方式的优势显而易见:首先,它完全避免了与 none、false、0 等其他“假值”的混淆;其次,代码的意图变得极其明确 - 我们不是在检查某个值是否为 none,而是在确认这个配置项是否真的存在。
为了使哨兵对象在调试和日志记录时更加友好,我们可以进一步优化其实现:
class _missing: def __repr__(self): return "<missing>" missing = _missing()
这个增强版本在打印或记录日志时,会显示有意义的<missing>
标识,而不是默认的 object 表示形式。
我们还可以创建哨兵家族:
class sentinel: def __init__(self, name): self.name = name def __repr__(self): return f"<{self.name}>" missing = sentinel("missing") unset = sentinel("unset") deleted = sentinel("deleted")
python 内置的 ellipsis 对象(...)也可以作为轻量级的哨兵值使用:
def process_data(data=...): if data is ...: data = load_default_data()
ellipsis 作为哨兵有其独特优势:它是 python 内置的单例对象,内存占用极小;在类型提示中也有特定用途,因此对类型检查器友好。不过需要注意的是,过度使用 ellipsis 可能会降低代码可读性,建议在团队内部达成明确的使用约定。
04 方法补充
python实现数据集缺失值处理
1. 直接删除
当缺失值的个数只占整体很小一部分的时候,可直接删除缺失值。但是如果缺失值占比上升,这种缺失值处理方法误差就很大了。在采用删除法处理缺失值时,需要首先检测样本总体中缺失值的个数。python中统计缺失值的方法如下:
import numpy as np import pandas as pd data = pd.read_csv('data.csv',encoding='gbk') # 将空值形式的缺失值转换成可识别的类型 data = data.replace(' ', np.nan) print(data.columns)#['id', 'label', 'a', 'b', 'c', 'd'] #将每列中缺失值的个数统计出来 null_all = data.isnull().sum() #id 0 #label 0 #a 7 #b 3 #c 3 #d 8 #查看a列有缺失值的数据 a_null = data[pd.isnull(data['a'])] #a列缺失占比 a_ratio = len(data[pd.isnull(data['a'])])/len(data) #0.0007 #丢弃缺失值,将存在缺失值的行丢失 new_drop = data.dropna(axis=0) print(new_drop.shape)#(9981,6) #丢弃某几列有缺失值的行 new_drop2 = data.dropna(axis=0, subset=['a','b']) print(new_drop2.shape)#(9990,6)
上述数据缺失值较少,可直接删除。注意,在计算缺失值时,对于缺失值不是nan的要用replace()函数替换成nan格式,否则pd.isnull()检测不出来。
2.使用一个全局常量填充缺失值
可以用一个常数('unknow’或者负无限大)来填充缺失值。但是如果缺失值较多,都用’unknow’来填充的话,数据挖掘程序会觉得’unknow’是一个有趣的概念。该方法很简单,但十分不可靠。python实现如下:
#用0填充缺失值 fill_data = data.fillna('unknow') print(fill_data.isnull().sum()) #out id 0 label 0 a 0 b 0 c 0 d 0
3.均值、众数、中位数填充
根据样本之间的相似性填补缺失值是指用这些缺失值最可能的值来填补它们,通常使用能代表变量中心趋势的值进行填补,代表变量中心趋势的指标包括平均值、中位数、众数等,那么我们采用哪些指标来填补缺失值呢?
分布类型 | 填充值 | 原因 |
---|---|---|
近正态分布 | 平均值 | 所有观测值都较好地聚集在平均值周围 |
偏态分布 | 中位数 | 偏态分布的大部分值都聚集在变量分布的一侧,中位数是更好地代表数据中心趋势的指标 |
有离群点的分布 | 中位数 | 中位数是更好地代表数据中心趋势的指标 |
名义变量 | 众数 | 名义变量无大小、顺序之分,不能加减乘除。如性别 |
python实现如下:
#均值填充 data['a'] = data['a'].fillna(data['a'].mean()) #中位数填充 data['a'] = data['a'].fillna(data['a'].median()) #众数填充 data['a'] = data['a'].fillna(stats.mode(data['a'])[0][0]) #用前一个数据进行填充 data['a'] = data['a'].fillna(method='pad') #用后一个数据进行填充 data['a'] = data['a'].fillna(method='bfill')
imputer提供了缺失数值处理的基本策略,比如使用缺失数值所在行或列的均值、中位数、众数来替代缺失值。
from sklearn.preprocessing import imputer imr = imputer(missing_values='nan', strategy='mean', axis=0) imr = imr.fit(data.values) imputed_data = pd.dataframe(imr.transform(data.values)) print(imputed_data[0:15])
参数 | 描述 |
---|---|
missing_values | int或’nan’,默认nan(string类型) |
strategy | mean,默认平均值填补;可选,median(中位数),most_frequent(众数) |
axis | 指定轴向。axis=0,列向(默认);axis=1,行向 |
verbose | int默认值为0 |
copy | 默认true:创建数据集的副本;false:在任何地方都可进行插值 |
4. 插值法、knn填充
插值法
interpolate()插值法,计算的是缺失值前一个值和后一个值的平均数。
data['a'] = data['a'].interpolate()
knn填充
from fancyimpute import knn fill_knn = knn(k=3).fit_transform(data) data = pd.dataframe(fill_knn) print(data.head()) #out 0 1 2 3 4 5 0 111.0 0.0 2.0 360.0 4.000000 1.0 1 112.0 1.0 9.0 1080.0 3.000000 1.0 2 113.0 1.0 9.0 1080.0 2.000000 1.0 3 114.0 0.0 1.0 360.0 *3.862873 *1.0 4 115.0 0.0 1.0 270.0 5.000000 1.0
5.随机森林填充
from sklearn.ensemble import randomforestregressor #提取已有的数据特征 process_df = data.ix[:, [1, 2, 3, 4, 5]] # 分成已知该特征和未知该特征两部分 known = process_df[process_df.c.notnull()].as_matrix() uknown = process_df[process_df.c.isnull()].as_matrix() # x为特征属性值 x = known[:, 1:3] # print(x[0:10]) # y为结果标签 y = known[:, 0] print(y) # 训练模型 rf = randomforestregressor(random_state=0, n_estimators=200, max_depth=3, n_jobs=-1) rf.fit(x, y) # 预测缺失值 predicted = rf.predict(uknown[:, 1:3]) print(predicted) #将预测值填补原缺失值 data.loc[(data.c.isnull()), 'c'] = predicted print(data[0:10])
到此这篇关于python中处理缺失值的有效方法详解的文章就介绍到这了,更多相关python处理缺失值内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论