1. 从“去重”到“洞察”np.unique() 的降维打击如果你用 Python 处理过数据尤其是数值型数据那么numpy库几乎是你绕不开的基石。而在numpy的众多函数中np.unique()绝对是一个被严重低估的“瑞士军刀”。很多人对它的认知停留在“给数组去重”这就像把一台超级计算机只用来做加减法。今天我们就来彻底拆解np.unique()看看这个看似简单的函数如何在数据清洗、统计分析、特征工程乃至图像处理中实现从“去重”到“洞察”的降维打击。简单来说np.unique()的核心功能是找出数组中的唯一值去重并排序。但它的强大之处在于它不仅仅返回一个去重后的列表。通过几个关键参数它能同步返回这些唯一值在原数组中的索引、出现次数甚至能帮你重构一个“逆索引”数组让你能轻松地将去重后的结果映射回原始数据。无论是统计一个用户列表里有多少个独立用户分析一张图片使用了多少种颜色还是快速计算一个数据集中各个类别的频次np.unique()都能用一行代码优雅地解决。这篇文章适合所有使用 Python 进行数据处理的朋友无论你是刚入门的新手还是已经写过不少数据分析脚本的老手。我们将从最基础的用法讲起逐步深入到高级参数和实战场景并结合我踩过的坑和总结的技巧让你真正掌握这个高效的工具。你会发现用好np.unique()能让你的代码更简洁运行更高效数据洞察也更清晰。2. 基础用法拆解不止是返回唯一值让我们先从函数签名开始理解它的基本能力。np.unique(ar, return_indexFalse, return_inverseFalse, return_countsFalse, axisNone)。这个函数的核心输入是ar即你的输入数组它可以是任何维度的 numpy 数组甚至列表、元组等能被转换为数组的序列。2.1 核心功能去重与排序最基础的调用就是只传入数组。np.unique()会做两件事1. 找出所有不重复的元素2. 将这些元素按升序排列。import numpy as np # 示例1一维数组 arr_1d np.array([3, 1, 2, 3, 3, 2, 1, 5]) unique_vals np.unique(arr_1d) print(unique_vals) # 输出[1 2 3 5]这里有一个非常重要的细节返回的总是排序后的结果。[1, 2, 3, 5]而不是[3, 1, 2, 5]。这个特性在很多时候非常方便比如你需要将类别数据编码为有序的整数时。但如果你需要保留元素首次出现的顺序就需要用到return_index参数我们稍后会讲。对于多维数组默认行为axisNone是先将数组展平flatten再对整个展平后的一维序列进行去重排序。# 示例2二维数组默认展平 arr_2d np.array([[1, 2, 2], [3, 1, 4]]) unique_vals_flat np.unique(arr_2d) print(unique_vals_flat) # 输出[1 2 3 4]2.2 参数 axis指定去重维度axis参数是理解np.unique()对多维数组处理的关键。当axis被指定时函数将沿着该轴寻找唯一的子数组。# 示例3沿行去重axis0 arr_2d np.array([[1, 2, 3], [4, 5, 6], [1, 2, 3], # 与第一行重复 [7, 8, 9]]) unique_rows np.unique(arr_2d, axis0) print(unique_rows) # 输出 # [[1 2 3] # [4 5 6] # [7 8 9]] # 重复的第一行被去除了。 # 示例4沿列去重axis1 arr_2d_col np.array([[1, 2, 1, 2], [3, 4, 3, 4]]) unique_cols np.unique(arr_2d_col, axis1) print(unique_cols) # 输出 # [[1 2] # [3 4]] # 第0列和第2列重复第1列和第3列重复各保留一列。这个功能在数据处理中非常实用。例如你有一个数据集每一行是一个样本每一列是一个特征。如果某些样本行的所有特征值完全相同那么它们很可能是重复记录可以用axis0快速找出唯一的样本。同样如果某些特征列在所有样本上的值完全一样例如一个常数列这个特征可能没有信息量可以用axis1来检查。注意axis参数是在numpy 1.13.0版本中引入的。如果你在使用较旧的代码库或环境需要注意兼容性。使用前可以用print(np.__version__)检查你的 numpy 版本。3. 高级返回参数获取元数据赋能复杂操作np.unique()的真正威力在于它的return_*系列参数。它们允许你在一次函数调用中不仅得到唯一值还能获得关于这些唯一值在原数组中位置的丰富元数据从而避免后续繁琐的循环查找。3.1 return_index定位首次出现位置return_indexTrue会返回一个索引数组指示每个唯一值在原始输入数组中第一次出现的位置。arr np.array([b, c, a, b, c, a, a]) unique_vals, indices np.unique(arr, return_indexTrue) print(唯一值:, unique_vals) # [a b c] (已排序) print(首次出现索引:, indices) # [2 0 1] # 验证arr[indices] 应该等于 unique_vals print(arr[indices]) # [a b c]应用场景当你需要去重但又希望保留某种“原始顺序”时这个参数就派上用场了。例如你有一个按时间戳记录的事件列表里面有很多重复事件类型。你想获取所有出现过的事件类型但希望按照它们首次出现的时间顺序来排列而不是按字母排序。这时你可以结合return_index和索引排序来实现event_types np.array([login, purchase, login, view, purchase, logout, view]) unique_types, first_indices np.unique(event_types, return_indexTrue) # 按照首次出现的索引排序 order_preserved_unique unique_types[np.argsort(first_indices)] print(order_preserved_unique) # [login purchase view logout]3.2 return_inverse重构原始数组的“地图”这是最强大也最容易被忽视的参数之一。return_inverseTrue会返回一个整数数组inverse其长度与原始输入数组ar相同。对于ar中的每个元素inverse中对应位置的值表示该元素在unique_vals数组中的索引。换句话说unique_vals[inverse]能完美地重构出原始数组ar。arr np.array([10, 20, 30, 20, 10, 40]) unique_vals, inverse_indices np.unique(arr, return_inverseTrue) print(原始数组:, arr) # [10 20 30 20 10 40] print(唯一值:, unique_vals) # [10 20 30 40] print(逆索引:, inverse_indices) # [0 1 2 1 0 3] # 重构验证 reconstructed unique_vals[inverse_indices] print(重构数组:, reconstructed) # [10 20 30 20 10 40] print(是否相等:, np.array_equal(arr, reconstructed)) # True应用场景数据编码与分组聚合。这是return_inverse的杀手级应用。标签编码Label Encoding在机器学习中我们经常需要将字符串类型的分类标签如‘高’‘中’‘低’转换为整数如0, 1, 2。np.unique的return_inverse可以直接完成这个操作并且保证编码的一致性。categories np.array([medium, high, low, high, low, medium]) unique_cats, encoded_labels np.unique(categories, return_inverseTrue) print(类别映射:, dict(zip(unique_cats, range(len(unique_cats))))) # {high:0, low:1, medium:2} print(编码结果:, encoded_labels) # [2 0 1 0 1 2]高效分组统计结合numpy的聚合函数可以不用pandas就实现类似groupby的操作。例如计算每个唯一值的和或均值。values np.array([5.1, 3.5, 5.1, 4.9, 3.5, 3.5]) groups np.array([A, B, A, C, B, B]) # 分组标签 unique_groups, inverse np.unique(groups, return_inverseTrue) # 现在 inverse [0, 1, 0, 2, 1, 1] # 我们可以利用广播和布尔索引进行分组求和 group_sums np.zeros(len(unique_groups)) for i in range(len(unique_groups)): group_sums[i] values[inverse i].sum() # 更向量化的方式推荐 group_sums np.bincount(inverse, weightsvalues) print(分组:, unique_groups) # [A B C] print(组内和:, group_sums) # [10.2 10.5 4.9]3.3 return_counts一键频次统计return_countsTrue直接返回每个唯一值在原始数组中出现的次数顺序与unique_vals一一对应。arr np.array([apple, banana, apple, orange, banana, banana]) unique_vals, counts np.unique(arr, return_countsTrue) print(唯一值:, unique_vals) # [apple banana orange] print(出现次数:, counts) # [2 3 1] # 可以方便地组合查看 for item, count in zip(unique_vals, counts): print(f{item}: {count}次)应用场景这是数据探索性分析EDA中最常用的功能之一。快速查看数据分布、发现异常值例如某个类别只有1次出现可能是录入错误、计算类别不平衡度等。# 计算类别不平衡比例 ratios counts / counts.sum() print(类别占比:, ratios) # [0.33333333 0.5 0.16666667]3.4 组合使用一站式获取所有信息你可以同时指定多个return_*参数一次性获取所有需要的信息。返回的是一个元组顺序是(唯一值数组 索引数组可选 逆索引数组可选 计数数组可选)。arr np.array([7, 5, 7, 8, 5, 5]) result np.unique(arr, return_indexTrue, return_inverseTrue, return_countsTrue) unique_vals, indices, inverse, counts result print(f唯一值: {unique_vals}) # [5 7 8] print(f首次出现索引: {indices}) # [1 0 3] - 5在索引17在索引08在索引3 print(f逆索引: {inverse}) # [1 0 1 2 0 0] - 映射到unique_vals的索引 print(f计数: {counts}) # [3 2 1] - 5出现3次7出现2次8出现1次这种“一站式”处理极大地提升了代码的简洁性和运行效率避免了为获取不同信息而多次调用函数或编写循环。4. 实战场景与性能陷阱了解了所有参数后我们来看看np.unique()在真实场景中如何应用以及一些需要注意的性能和细节问题。4.1 场景一图像颜色量化与调色板提取假设你有一张RGB图片存储为一个形状为(height, width, 3)的 numpy 数组。你想知道这张图片一共用了多少种不同的颜色或者提取出它的调色板所有唯一颜色。# 假设 img 是一个 numpy 数组形状为 (H, W, 3) dtypenp.uint8 # 将三维的像素数组重塑为二维 (H*W, 3)每一行是一个RGB颜色向量 pixels img.reshape(-1, 3) # 沿着行轴axis0寻找唯一的颜色向量 unique_colors np.unique(pixels, axis0) print(f图像使用了 {len(unique_colors)} 种唯一颜色。) # unique_colors 就是图像的调色板踩坑提醒对于大型图像例如 4K 图片有约 800 万个像素直接对(8000000, 3)的数组进行np.unique操作可能会消耗大量内存和计算时间。在实际应用中可能需要先对图像进行下采样或者使用更专业的图像处理库如 OpenCV的颜色量化方法。4.2 场景二基于多列的组合键去重在数据处理中我们经常需要根据多列多个字段的组合来判定数据行是否重复。np.unique的axis参数可以完美处理这种情况但前提是需要将多列组合成一个结构化数组或视图。一种更通用的方法是利用pandas的DataFrame.drop_duplicates()。但如果你坚持用纯numpy可以这样做# 假设有一个二维数组 data形状 (N, M)我们想根据前两列去重 data np.array([[1, 10, A], [2, 20, B], [1, 10, C], # 与前两行前两列重复 [3, 30, D]]) # 方法创建一个关于前两列的“视图”但需要处理成可哈希/可比较的形式 # 技巧将数值列转换为结构化数据类型 compound_key data[:, :2].copy().view([(col0, data.dtype), (col1, data.dtype)]).squeeze() # 现在 compound_key 是一个一维的结构化数组 unique_keys, indices np.unique(compound_key, return_indexTrue) unique_rows data[indices] # 根据首次出现的索引获取唯一的行 print(unique_rows) # 输出 # [[1 10 A] # [2 20 B] # [3 30 D]]注意这种方法有点“黑魔法”的味道涉及到numpy的结构化数组和视图对数据类型有要求且不易读。对于复杂的多列去重强烈建议使用 pandas代码会清晰得多df.drop_duplicates(subset[‘col1‘ ‘col2‘])。4.3 性能考量与替代方案np.unique()的内部实现包含了排序步骤这也是它能返回有序结果的原因其时间复杂度通常是O(N log N)其中 N 是输入数组的元素数量或指定轴后的子数组数量。对于非常大的数组这可能成为瓶颈。什么时候需要考虑替代方案只需要判断是否有重复不需要具体值如果仅仅想知道数组里有没有重复元素而不关心是哪些元素重复使用len(np.unique(arr)) len(arr)是低效的。因为np.unique完成了排序和去重全部工作。一个更快的替代方法是利用 Python 原生集合set的哈希特性但要注意集合是无序的且对于 numpy 数组需要先转换为列表或使用arr.flatten().tolist()这也有转换开销。对于一维数组可以简单比较len(set(arr)) len(arr)。处理超大数组且内存敏感np.unique默认返回排序后的数组排序过程可能需要额外内存。如果原始数组已经很大并且你只需要唯一值而不关心顺序理论上存在更节省内存的流式算法但在numpy生态中通常还是用它。需要保持原始顺序的去重如前所述np.unique默认排序。如果你需要保留元素首次出现的顺序标准做法是结合return_indexarr np.array([‘x‘, ‘a‘, ‘x‘, ‘b‘, ‘a‘]) unique_vals, indices np.unique(arr, return_indexTrue) order_preserved arr[np.sort(indices)] print(order_preserved) # [‘x‘ ‘a‘ ‘b‘]但这依然进行了排序操作。对于一维数组一个纯 Python 的、基于字典插入顺序Python 3.7 字典保证有序的方法可能在某些情况下更快但丧失了向量化计算的优势list(dict.fromkeys(arr.tolist()))核心建议在绝大多数情况下np.unique()因其接口的简洁性和功能的全面性都是首选。只有在性能 profiling 明确显示它是热点且数据规模极大时才值得去寻找和实现更复杂的替代方案。5. 常见错误与疑难解答即使是一个成熟的函数在使用中也难免会遇到问题。下面是我总结的几个常见“坑点”。5.1 数据类型与结构化数组的陷阱np.unique()对数据类型很敏感。特别是当数组包含浮点数时直接去重可能会因为浮点精度问题导致意外结果。# 浮点数精度问题 float_arr np.array([0.1 0.2, 0.3]) print(0.1 0.2 0.3) # False (浮点运算误差) unique_float np.unique(float_arr) print(unique_float) # 可能会输出 [0.3, 0.30000000000000004] 两个“不同”的值解决方案在比较或去重前对浮点数进行舍入或使用容差比较。np.unique本身不提供容差参数。一种做法是先四舍五入到指定位数decimals 10 rounded_arr np.round(float_arr, decimals) unique_rounded np.unique(rounded_arr)对于结构化数组例如记录数组np.unique(axis0)可以正常工作因为它会比较整个元组每一行。但如果你创建视图的方式不对如前面多列去重的例子可能会得到意想不到的结果。5.2 空数组与 NaN 值的处理空数组np.unique([])会返回一个空的同类型数组array([])。这通常是符合预期的。NaN 值NaN(Not a Number) 在numpy中有一个重要特性NaN ! NaN。这意味着np.unique无法将多个NaN值识别为相同的元素。arr_with_nan np.array([1.0, np.nan, 2.0, np.nan, 1.0]) unique_with_nan np.unique(arr_with_nan) print(unique_with_nan) # 输出可能为[ 1. 2. nan nan] 两个 nan 都被保留解决方案如果希望将NaN视为相同的值进行去重需要在调用np.unique前先处理NaN。一种常见方法是用一个不可能出现的值如np.inf临时替换NaN去重后再换回来如果需要。更稳健的做法是使用pandas的pd.unique()它默认会将所有NaN视为相同。import pandas as pd unique_pandas pd.unique(arr_with_nan) print(unique_pandas) # 输出[ 1. 2. nan] (注意pandas的去重不排序)5.3 与 pandas.Series.unique() 的对比pandas的Series.unique()方法也是一个常用的去重工具。它们的主要区别在于特性np.unique(ar)pd.Series.unique()输入numpy 数组或类数组pandas Series输出顺序始终排序升序保持原始出现顺序处理 NaN多个 NaN 被视为不同多个 NaN 被视为相同返回一个 NaN返回类型numpy 数组numpy 数组额外功能可通过参数返回索引、计数、逆索引等仅返回唯一值数组如何选择如果你需要排序结果、计数、逆映射等高级功能或者正在处理纯numpy数组用np.unique。如果你在处理pandas的Series或DataFrame的一列并且希望保留原始顺序用pd.Series.unique()。如果你的数据包含NaN且希望它们被正确去重pd.Series.unique()行为更符合直觉。6. 举一反三在更复杂数据上的应用思路np.unique()的基本逻辑是“比较和归类”。我们可以将这个思路扩展到更复杂的数据对象上虽然不能直接调用但可以借鉴其思想。例如你有一个列表里面包含多个字典你想找出所有独特的字典基于键值对完全匹配。由于字典是不可哈希的除非是frozenset形式的键值对不能直接扔进set或np.unique。但你可以先将每个字典转换成一个可哈希的表示比如排序后的元组序列的元组list_of_dicts [{a:1, b:2}, {b:2, a:1}, {c:3}] # 前两个字典相同 # 转换为可哈希的表示形式 tuple_reprs [tuple(sorted(d.items())) for d in list_of_dicts] # 使用np.unique的return_index功能找到唯一字典的索引 _, unique_indices np.unique(tuple_reprs, axis0, return_indexTrue) unique_dicts [list_of_dicts[i] for i in sorted(unique_indices)] # 按首次出现顺序 print(unique_dicts) # [{a: 1, b: 2}, {c: 3}]这个例子展示了np.unique的核心思想——通过定义“唯一性”的判别标准在这里是转换后的元组我们可以利用它强大的向量化能力来处理复杂对象。关键在于如何将你的数据有效地转换为numpy能够理解和高效比较的数值形式。最后我个人在长期使用中的体会是np.unique()是我数据预处理工具箱中最常被翻牌的几件工具之一。它的设计体现了numpy哲学通过提供丰富、一致的接口将复杂的逻辑封装在简单的函数调用背后。下次当你需要对数据进行归类、统计或编码时别急着写for循环先想想np.unique()能不能帮你一行搞定。很多时候答案都是肯定的。