【问题标题】:Fastest way to make python Object out of numpy array rows用numpy数组行制作python对象的最快方法
【发布时间】:2019-04-11 18:04:24
【问题描述】:

我需要从 numpy 数组(或 pandas 数据框)中创建一个对象列表。每行包含对象的所有属性值(参见示例)。

import numpy as np

class Dog:

def __init__(self, weight, height, width, girth):
    self.weight = weight
    self.height = height
    self.width = width
    self.girth = girth


dogs = np.array([[5, 100, 50, 80], [4, 80, 30, 70], [7, 120, 60, 90], [2, 50, 30, 50]])

# list comprehension with idexes
dog_list = [Dog(dogs[i][0], dogs[i][1], dogs[i][2], dogs[i][3]) for i in range(len(dogs))]

我的真实数据当然要大得多(多达一百万行,5 列),因此逐行迭代并查找正确的索引需要很长时间。有没有办法对此进行矢量化或通常使其更高效/更快?我试着自己寻找方法,但我找不到任何可翻译的东西,至少在我的专业水平上是这样。

保留行的顺序非常重要,所以如果这不起作用,我想我将不得不忍受缓慢的操作。

干杯!

编辑 - 关于 np.vectorize 的问题:

这是我实际代码的一部分以及一些实际数据:

将 numpy 导入为 np

class Particle:
    TrackID = 0
    def __init__(self, uniq_ident, intensity, sigma, chi2, past_nn_ident, past_distance, aligned_x, aligned_y, NeNA):
        self.uniq_ident = uniq_ident
        self.intensity = intensity
        self.sigma = sigma
        self.chi2 = chi2
        self.past_nn_ident = past_nn_ident
        self.past_distance = past_distance
        self.aligned_y = aligned_y
        self.aligned_x = aligned_x
        self.NeNA = NeNA
        self.new_track_length = 1
        self.quality_pass = True  
        self.re_seeder(self.NeNA)


def re_seeder(self, NeNA):

    if np.isnan(self.past_nn_ident):  
        self.newseed = True            
        self.new_track_id = Particle.TrackID
        print(self.new_track_id)
        Particle.TrackID += 1

    else:
        self.newseed = False
        self.new_track_id = None

data = np.array([[0.00000000e+00, 2.98863746e+03, 2.11794100e+02, 1.02241467e+04, np.NaN,np.NaN, 9.00081968e+02, 2.52456745e+04, 1.50000000e+01],
       [1.00000000e+00, 2.80583577e+03, 4.66145720e+02, 6.05642671e+03, np.NaN, np.NaN, 8.27249728e+02, 2.26365501e+04, 1.50000000e+01],
       [2.00000000e+00, 5.28702810e+02, 3.30889610e+02, 5.10632793e+03, np.NaN, np.NaN, 6.03337243e+03, 6.52702811e+04, 1.50000000e+01],
       [3.00000000e+00, 3.56128350e+02, 1.38663730e+02, 3.37923885e+03, np.NaN, np.NaN, 6.43263261e+03, 6.14788766e+04, 1.50000000e+01],
       [4.00000000e+00, 9.10148200e+01, 8.30057400e+01, 4.31205993e+03, np.NaN, np.NaN, 7.63955009e+03, 6.08925862e+04, 1.50000000e+01]])

Particle.TrackID = 0
particles = np.vectorize(Particle)(*data.transpose())

l = [p.new_track_id for p in particles]

奇怪的是,ree_seeder 函数“print(self.new_track_id)”中的 print 语句,它会打印 0、1、2、3、4、5。

如果我随后取出粒子对象并从它们的 new_track_id 属性“l = [p.new_track_id for p inparticle]”中列出一个列表,则值为 1、2、3、4、5。

所以在某个地方,不知何故,第一个对象要么丢失,要么被重写,要么我不明白。

【问题讨论】:

  • 不确定这是否更快但更简单:dog_list = [Dog(*row) for row in dogs]
  • 更好[Dog(*x) for x in dogs.tolist()]
  • 谢谢,这些至少应该让我的代码更干净!
  • 向量化类构造函数给你另一个提升:dog_list = np.vectorize(Dog)(*dogs.transpose())
  • @Jeronimo 天哪,这只是将我的代码从 50 秒加速到 1.3 秒:D 非常感谢!

标签: python-3.x pandas numpy oop vectorization


【解决方案1】:

只要您坚持构建 Python 对象,您就不会获得很大的效率/速度提升。有了这么多项目,将数据保存在 numpy 数组中会更好。如果您想要更好的属性访问,您可以将数组转换为记录数组 (recarray),这将允许您命名列(如 weightheight 等),同时仍然在 numpy 中保存数据数组。

dog_t = np.dtype([
    ('weight', int),
    ('height', int),
    ('width', int),
    ('girth', int)
])

dogs = np.array([
    (5, 100, 50, 80),
    (4, 80, 30, 70),
    (7, 120, 60, 90),
    (2, 50, 30, 50),
], dtype=dog_t)

dogs_recarray = dogs.view(np.recarray)

print(dogs_recarray.weight)
print(dogs_recarray[2].height)

如果需要,您还可以混合和匹配数据类型(例如,如果某些列是整数,而其他列是浮点数)。使用此代码时请注意,dogs 数组中的项目需要在元组中指定(使用 ()),而不是在列表中以正确应用数据类型。

【讨论】:

  • 谢谢!不幸的是,在这种情况下我必须使用对象(这需要进行一些重大的重新设计,而且时间投资不值得)所以我想我不得不忍受它有点慢。至少我现在知道我不需要继续搜索了!
  • 您可能仍然会发现一些可以提供帮助的调整。 @jeronimo 留下了关于使用 np.vectorize 的评论,这可能很有用。有了这么多对象,在 Dog 类上使用插槽可能也会有所帮助
【解决方案2】:

Multiprocessing 可能值得一看。

from multiprocessing import Pool dog_list = []

将对象附加到列表的功能:

def append_dog(i): dog_list.append(Dog(*dogs[i]))

让多个worker并行追加到这个列表中:

number_of_workers = 4 pool = Pool(processes=number_of_workers) pool.map_async(append_dog, range(len(dogs)))

或者作为一个较短的版本:

from multiprocessing import Pool
number_of_workers = 4
pool = Pool(processes=number_of_workers)
pool.map_async(lambda i: dog_list.append(Dog(*dogs[i])), range(len(dogs)))

【讨论】:

  • 谢谢!我最终使用了 Jeronimo 提出的 np.vectorize 函数,但这非常适合我正在做的另一件事。
  • 在这种情况下使用进程池会引入过多的序列化和反序列化开销。
【解决方案3】:

用一个简单的类:

class Foo():
    _id = 0
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
        self.id = self._id
        Foo._id += 1
    def __repr__(self):
        return '<Foo %s>'%self.id


In [23]: arr = np.arange(12).reshape(4,3)

简单的列表理解:

In [24]: [Foo(*xyz) for xyz in arr]
Out[24]: [<Foo 0>, <Foo 1>, <Foo 2>, <Foo 3>]

默认使用vectorize

In [26]: np.vectorize(Foo)(*arr.T)
Out[26]: array([<Foo 5>, <Foo 6>, <Foo 7>, <Foo 8>], dtype=object)

请注意,Foo 4 已被跳过。 vectorize 执行试算以确定返回 dtype(此处为 object)。 (这给其他用户带来了问题。)我们可以通过指定otypes 来解决这个问题。还有一个cache 参数可能会起作用,但我没有玩过。

In [27]: np.vectorize(Foo,otypes=[object])(*arr.T)
Out[27]: array([<Foo 9>, <Foo 10>, <Foo 11>, <Foo 12>], dtype=object)

在内部 vectorize 使用 frompyfunc,在这种情况下也可以正常工作,而且根据我的经验更快:

In [28]: np.frompyfunc(Foo, 3,1)(*arr.T)
Out[28]: array([<Foo 13>, <Foo 14>, <Foo 15>, <Foo 16>], dtype=object)

通常vectorize/frompyfunc 将“标量”值传递给函数,迭代二维数组的所有元素。但是使用*arr.T 是一种巧妙的传递行的方式——实际上是一维元组数组。

In [31]: list(zip(*arr.T)) 
Out[31]: [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11)]

一些比较时间:

In [32]: Foo._id=0
In [33]: timeit [Foo(*xyz) for xyz in arr]
14.2 µs ± 17.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [34]: Foo._id=0
In [35]: timeit np.vectorize(Foo,otypes=[object])(*arr.T)
44.9 µs ± 108 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [36]: Foo._id=0
In [37]: timeit np.frompyfunc(Foo, 3,1)(*arr.T)
15.6 µs ± 18.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

这与我过去的时间安排一致。 vectorize 很慢。 frompyfunc 在列表理解方面具有竞争力,有时甚至快 2 倍。将列表理解包装在数组中会减慢速度,例如np.array([Foo(*xyz)...]).

还有你原来的列表理解:

In [40]: timeit [Foo(arr[i][0],arr[i][1],arr[i][2]) for i in range(len(arr))]
10.1 µs ± 80 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

这样更快!因此,如果您的目标是列表而不是数组,我认为使用 numpy 工具没有意义。

当然,在一个小例子上的这些时间需要谨慎看待。

【讨论】:

  • 有趣。你能用一个大数组再次做这些计时吗,比如np.random.randint(1, 100, (1000000, 4))
  • @Jeronimo, hpaulj 我认为除了持续的开销之外,还有一个重要的成本是 numpy 的缓慢__getitem__vectorizefrompyfunc.tolist 都避免了这种情况,因此可以比其他方法进行类似且更好的扩展。对于小型阵列 .tolist 似乎最快,对于大型阵列 frompyfunc
  • 有趣,解开了谜团,非常感谢!那个失踪的人快把我逼疯了! Vectorize 和 frompyfunc 绝对比我的列表理解要快得多,至少在更大的数据集上是这样。一个完整的数据集使用 vectorize 大约需要 8.7 秒,使用 frompyfunc 大约需要 9.1 秒。同一个文件需要 33 秒才能完成列表理解。
猜你喜欢
  • 1970-01-01
  • 2022-01-26
  • 2015-07-27
  • 2021-11-15
  • 2011-11-12
  • 2021-11-11
  • 2018-10-02
相关资源
最近更新 更多