Самый быстрый способ сделать объект Python из строк массива - PullRequest
0 голосов
/ 08 ноября 2018

Мне нужно составить список объектов из массива 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]

Любопытно, что оператор print внутри функции ree_seeder «print (self.new_track_id)» печатает 0, 1, 2, 3, 4, 5.

Если я тогда возьму объекты частиц и составлю список из их атрибутов new_track_id "l = [p.new_track_id для p в частицах]", значения будут 1, 2, 3, 4, 5.

Так что где-то, так или иначе, первый объект либо потерян, либо переписан, либо что-то еще, чего я не понимаю.

Ответы [ 3 ]

0 голосов
/ 08 ноября 2018

Многопроцессорная обработка может стоить посмотреть.

from multiprocessing import Pool dog_list = []

Функция добавления объектов в список:

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

Пусть несколько работников добавляются в этот список параллельно:

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)))
0 голосов
/ 08 ноября 2018

С простым классом:

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 - это умный способ передачи строк - фактически, массив 1d кортежей.

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 tools.

Конечно, эти временные интервалы на небольшом примере нужно рассматривать с осторожностью.

0 голосов
/ 08 ноября 2018

Вы не получите большого прироста эффективности / скорости, если будете настаивать на создании объектов Python. С таким количеством элементов вы будете лучше обслуживаться, если будете хранить данные в массиве numpy. Если вам нужен более качественный доступ к атрибутам, вы можете преобразовать массив в массив записей (recarray), который позволит вам именовать столбцы (например, weight, height и т. Д.), Сохраняя при этом данные в 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 необходимо указывать в кортежах (используя ()), а не в списках для правильного применения типа данных.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...