封装向量化函数 - 与 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 通过让我清楚地了解额外的功能和机会,帮助我进一步探索这个问题。这帮助我更清楚地定义了我的竞争目标,并且能够开始优先考虑它们。
代码应该简洁/紧凑且可维护,不要充满样板,包括...
- 调用函数
- 利用结果
- 函数实现本身
函数性能不应受到影响
- 慢 5% 但在其他方面更好可能是可以接受的
- 慢 100% 可能是永远不能接受的
实现应尽可能与数据类型无关
- 一个标量函数和一个向量函数不太理想
这让我想到了我目前首选的实现/风格...
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
请发表评论或批评。
我一直在重构一些代码,并用它来探索如何在使用 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 通过让我清楚地了解额外的功能和机会,帮助我进一步探索这个问题。这帮助我更清楚地定义了我的竞争目标,并且能够开始优先考虑它们。
代码应该简洁/紧凑且可维护,不要充满样板,包括...
- 调用函数
- 利用结果
- 函数实现本身
函数性能不应受到影响
- 慢 5% 但在其他方面更好可能是可以接受的
- 慢 100% 可能是永远不能接受的
实现应尽可能与数据类型无关
- 一个标量函数和一个向量函数不太理想
这让我想到了我目前首选的实现/风格...
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
请发表评论或批评。