如何在Python中高效优化每行含字符串数组的Pandas序列的序数编码?
问题背景
我有一个Pandas序列,每行存储一个字符串数组,示例如下:
0 [] 1 [] 2 [] 3 [] 4 [0007969760, 0007910220, 0007910309] ... 243223 [] 243224 [0009403370] 243225 [0009403370, 0007190939] 243226 [] 243227 [] 名称: Item History, 长度: 243228, 数据类型: object
我的目标是在此基础上执行简单的序数编码,同时尽可能兼顾时间与内存效率,且需满足以下要求:
- 空列表需用唯一整数标记(例如,若有100个唯一字符串,空列表可编码为
[101]); - 编码规则需可保存,以便未来对其他列表执行相同编码;
- 若未来列表包含初始数据中未出现的字符串,需用单独整数标记该未知项。
为何弃用sklearn的OrdinalEncoder?
一方面它不支持未知项处理,另一方面逐行应用时速度极慢——我们需先在所有不同字符串的合并数组上拟合,再用Series.apply(lambda x: oe.transform(x))逐行转换,因为它会为每行构建映射表,虽单次调用仅约0.01秒,但面对海量数据仍过慢。
现有尝试与性能
我尝试将字典推导移到逐行处理之外,先构建映射表再遍历行,实现了如下原生Python函数:
def encode_labels(X, table, noHistory, unknownItem): res = np.empty(len(X), dtype=np.ndarray) for i in range(len(X)): if len(X[i]) == 0: res[i] = np.array([noHistory]) else: res[i] = np.empty(len(X[i]), dtype=np.ndarray) for j in range(len(X[i])): try: res[i][j] = table[X[i][j]] except KeyError: res[i][j] = unknownItem return res
该方案比逐行.apply()快很多,但仍非最优。我将其Cython化并做了其他优化,获得了一定速度提升,但未达到数量级的改进:
%%cython cimport numpy as cnp import numpy as np from cpython cimport array import array cpdef list encode_labels_cy(cnp.ndarray X, dict table, int noHistory, int unknownItem, array.array rowLengths): cdef int[:] crc = rowLengths cdef list flattenedX = [] cdef Py_ssize_t i, j cdef list row = [] for row in X: if len(row)==0: flattenedX.append('ZZ') else: flattenedX.extend(row) cdef Py_ssize_t lenX = len(flattenedX) cdef array.array res = array.array('i', [0]*lenX) cdef int[:] cres = res i=0 while i < lenX: try: cres[i] = table[flattenedX[i]] except KeyError: cres[i] = unknownItem i += 1 cdef list pyres = [] cdef Py_ssize_t s = 0 for k in crc: pyres.append(res[s:s+k]) s+= k return pyres
性能测试结果(基于5000行样本)
# classes是{字符串:整数}的映射字典,noHistory和unknownItem为对应标记值 %timeit encode_labels(X.values, classes, noHistory, unknownItem) %timeit encode_labels_cy(X.values, classes, noHistory, unknownItem, array.array('i', [1 if x == 0 else x for x in [len(j) for j in X]]))
输出:
50.4 ms ± 2.76 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
11.2 ms ± 1.11 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
更新:我用ctypes实现了一个版本,速度比逐行.apply()和原生Python版本快,但仍慢于Cython(这与我的预期不符!)
优化诉求
我想请教:如何进一步提升编码速度,同时尽可能降低内存占用?方案不局限于纯Python,Cython、ctypes等均可。由于该代码是神经网络预处理的一部分,还有GPU可用,若能利用GPU加速更佳。多进程也是可选方向,但该方式需为每个进程复制字符串-整数映射表,既生成缓慢又占用大量内存。
测试数据生成代码
可生成与我数据集格式类似的输入:
import numpy as np import pandas as pd a = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] X = pd.Series([[a[np.random.randint(0, 26)] for i in range(np.random.randint(0, 10))] for j in range(5000)]) classes = dict(zip(a, np.arange(0, 26))) unknownItem = 26 noHistory = 27
内容的提问来源于stack exchange,提问作者Dan Scally

