封装向量化函数 - 与 Panda DataFrames 一起使用

Encapsulating Vectorised Functions - For Use With Panda DataFrames

我一直在重构一些代码,并用它来探索如何在使用 Pandas 和 Numpy 时构建可维护、灵活、简洁的代码。 (通常我只是短暂地使用它们,我现在处于一个我应该以成为前冲刺为目标的角色。)

我遇到的一个例子是一个函数,有时可以在一列值上调用,有时可以在三列值上调用。使用 Numpy 的矢量化代码很好地封装了它。但是使用起来有点笨拙。

我应该如何"better"编写下面的函数?

def project_unit_space_to_index_space(v, vertices_per_edge):
    return np.rint((v + 1) / 2 * (vertices_per_edge - 1)).astype(int)


input = np.concatenate(([df['x']], [df['y']], [df['z']]), axis=0)

index_space = project_unit_space_to_index_space(input, 42)

magic_space = some_other_transformation_code(index_space, foo, bar)

df['x_'], df['y_'], df['z_'] = magic_space

按照编写的函数可以接受一列数据或多列数据,它仍然可以正确、快速地工作。

return 类型是直接传递给另一个类似结构函数的正确形状,允许我整齐地链接函数。

即使将结果分配回数据框中的新列也不是 "awful",尽管它有点笨拙。

但是将输入打包成一个 np.ndarray 确实非常笨重。


我还没有找到涵盖此内容的任何风格指南。它们遍布 itterrows 和 lambda 表达式等。但是我没有找到封装此类逻辑的最佳实践。


那么,你如何构建上面的代码?


编辑:整理输入的各种选项的时间安排

%timeit test = project_unit_sphere_to_unit_cube(df[['x','y','z']].unstack().to_numpy())                      
# 1.44 ms ± 57.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit test = project_unit_sphere_to_unit_cube(df[['x','y','z']].to_numpy().T)                              
# 558 µs ± 6.25 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit test = project_unit_sphere_to_unit_cube(df[['x','y','z']].transpose().to_numpy())                    
# 817 µs ± 18.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit test = project_unit_sphere_to_unit_cube(np.concatenate(([df['x']], [df['y']], [df['z']]), axis=0))   
# 3.46 ms ± 42.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [101]: df = pd.DataFrame(np.arange(12).reshape(4,3))                         
In [102]: df                                                                    
Out[102]: 
   0   1   2
0  0   1   2
1  3   4   5
2  6   7   8
3  9  10  11

您正在从数据帧的 n 列创建一个 (n,m) 数组:

In [103]: np.concatenate([[df[0]],[df[1]],[df[2]]],0)                           
Out[103]: 
array([[ 0,  3,  6,  9],
       [ 1,  4,  7, 10],
       [ 2,  5,  8, 11]])

一种更紧凑的方法是转置这些列的数组:

In [104]: df.to_numpy().T                                                       
Out[104]: 
array([[ 0,  3,  6,  9],
       [ 1,  4,  7, 10],
       [ 2,  5,  8, 11]])

数据帧有自己的转置:

In [109]: df.transpose().to_numpy()                                             
Out[109]: 
array([[ 0,  3,  6,  9],
       [ 1,  4,  7, 10],
       [ 2,  5,  8, 11]])

您的计算适用于数据框,return使用相同形状和索引的数据框:

In [113]: np.rint((df+1)/2 *(42-1)).astype(int)                                 
Out[113]: 
     0    1    2
0   20   41   62
1   82  102  123
2  144  164  184
3  205  226  246

一些 numpy 函数将输入转换为 numpy 数组和 return 数组。其他人,通过将细节委托给 pandas 方法,可以直接在数据帧上工作,return 数据帧。

我不喜欢接受自己的答案,所以我不会更改已接受的答案。

@hpaulj 通过让我清楚地了解额外的功能和机会,帮助我进一步探索这个问题。这帮助我更清楚地定义了我的竞争目标,并且能够开始优先考虑它们。

  1. 代码应该简洁/紧凑且可维护,不要充满样板,包括...

    • 调用函数
    • 利用结果
    • 函数实现本身
  2. 函数性能不应受到影响

    • 慢 5% 但在其他方面更好可能是可以接受的
    • 慢 100% 可能是永远不能接受的
  3. 实现应尽可能与数据类型无关

    • 一个标量函数和一个向量函数不太理想

这让我想到了我目前首选的实现/风格...

def scale_unit_cube_to_unit_sphere(*values):
    """
    Scales all the inputs (on a row basis for array_line types) such that when
    treated as n-dimensional vectors, their scale is always 1.

    (Divides the vector represented by each row of inputs by that row's
     root-of-sum-of-squares, so as to normalise to a unit magnitude.)

    Examples - Scalar Inputs
    --------

    >>> scale_unit_cube_to_unit_sphere(1, 1, 1)
    [0.5773502691896258, 0.5773502691896258, 0.5773502691896258]

    Examples - Array Like Inputs
    --------

    >>> x = [ 1, 2, 3]
    >>> y = [ 1, 4, 3]
    >>> z = [ 1,-3,-1]
    >>> scale_unit_cube_to_unit_sphere(x, y, z)
    [array([0.57735027, 0.37139068, 0.6882472 ]),
     array([0.57735027, 0.74278135, 0.6882472 ]),
     array([ 0.57735027, -0.55708601, -0.22941573])]

    >>> a = np.array([x, y, z])
    >>> scale_unit_cube_to_unit_sphere(*a)
    [array([0.57735027, 0.37139068, 0.6882472 ]),
     array([0.57735027, 0.74278135, 0.6882472 ]),
     array([ 0.57735027, -0.55708601, -0.22941573])]

    scale_unit_cube_to_unit_sphere(*t)
    >>> t = (x, y, z)
    >>> scale_unit_cube_to_unit_sphere(*t)
    [array([0.57735027, 0.37139068, 0.6882472 ]),
     array([0.57735027, 0.74278135, 0.6882472 ]),
     array([ 0.57735027, -0.55708601, -0.22941573])]

    >>> df = pd.DataFrame(data={'x':x,'y':y,'z':z})
    >>> scale_unit_cube_to_unit_sphere(df['x'], df['y'], df['z'])
    [0    0.577350
     1    0.371391
     2    0.688247
     dtype: float64,
     0    0.577350
     1    0.742781
     2    0.688247
     dtype: float64,
     0    0.577350
     1   -0.557086
     2   -0.229416
     dtype: float64]

    For all array_like inputs, the results can then be utilised in similar
    ways, such as writing them to an existing DataFrame as follows:

    >>> transform = scale_unit_cube_to_unit_sphere(df['x'], df['y'], df['z'])
    >> df['i'], df['j'], df['k'] = transform

    """
    # Scale the position in space to be a unit vector, as on the surface of a sphere
    ################################################################################

    scaler = np.sqrt(sum([np.multiply(v, v) for v in values]))
    return [np.divide(v, scaler) for v in values]

根据文档字符串,这适用于标量、数组、系列等,无论是提供一个标量、三个标量、n 个标量、n 个数组等。

(我还没有一个简洁的方法来传递单个 DataFrame 而不是三个不同的 DataSeries,但现在这是一个低优先级。)

它们也在 "chains" 中工作,例如下面的示例(函数的实现不相关,只是输入到输出的链接模式)...

cube, ix = generate_index_cube(vertices_per_edge)

df = pd.DataFrame(
         data  = {
             'x': cube[0],
             'y': cube[1],
             'z': cube[2],
         },
         index = ix,
     )

unit = scale_index_to_unit(vertices_per_edge, *cube)

distortion = scale_unit_to_distortion(distortion_factor, *unit)

df['a'], df['b'], df['c'] = distortion

sphere = scale_unit_cube_to_unit_sphere(*distortion)

df['i'], df['j'], df['k'] = sphere

recovered_distortion = scale_unit_sphere_to_unit_cube(*sphere)

df['a_'], df['b_'], df['c_'] = recovered_distortion

recovered_cube = scale_unit_to_index(
                     vertices_per_edge,
                     *scale_distortion_to_unit(
                         distortion_factor,
                         *recovered_distortion,
                     ),
                 )

df['x_'], df['y_'], df['z_'] = recovered_cube

print(len(df[np.logical_not(np.isclose(df['a'], df['a_']))]))  # No Differences
print(len(df[np.logical_not(np.isclose(df['b'], df['b_']))]))  # No Differences
print(len(df[np.logical_not(np.isclose(df['c'], df['c_']))]))  # No Differences

print(len(df[np.logical_not(np.isclose(df['x'], df['x_']))]))  # No Differences
print(len(df[np.logical_not(np.isclose(df['y'], df['y_']))]))  # No Differences
print(len(df[np.logical_not(np.isclose(df['z'], df['z_']))]))  # No Differences

请发表评论或批评。