Python: более эффективная структура данных, чем вложенный словарь словарей массивов?

Я пишу программу python-3.10, которая предсказывает временные ряды различных свойств для большого количества объектов. Мой текущий выбор структуры данных для сбора результатов внутри кода, а затем для записи в файлы — это вложенный словарь словарей массивов. Например, для двух объектов с временными рядами из 3 свойств:

properties = {'obj1':{'time':np.arange(10),'x':np.random.randn(10),'vx':np.random.randn(10)},
'obj2': {'time':np.arange(15),'x':np.random.randn(15),'vx':np.random.randn(15)}}

Причина, по которой мне нравится этот формат вложенного словаря, заключается в том, что доступ к нему интуитивно понятен: внешний ключ — это имя объекта, а внутренние ключи — это имена свойств. Элементы, соответствующие каждому из внутренних ключей, представляют собой пустые массивы, дающие значение некоторого свойства как функцию времени. Мой фактический код генерирует dict из ~ 100 000 объектов (внешних ключей), каждый из которых имеет ~ 100 свойств (внутренние ключи), записанных примерно в 1000 раз (массивы с плавающей запятой).

Я заметил, что когда я делаю np.savez('filename.npz',**properties) в своем собственном огромном словаре свойств (или его подмножествах), это занимает некоторое время, а размер выходного файла составляет несколько ГБ (вероятно, потому, что np.savez вызывает pickle под капотом, так как мой вложенный словарь это не массив).

Существует ли более эффективная структура данных, широко применимая для моего варианта использования? Стоит ли переключаться с моего вложенного dict на кадры данных pandas, numpy ndarrays или массивы записей или список каких-то табличных объектов? Было бы неплохо иметь возможность сохранять/загружать файл в формате двоичного вывода, который сохраняет сопоставление имен объектов с их свойствами dict/array/table/dataframe и, конечно же, именами каждого из массивов временных рядов свойств. .

Вы обеспокоены в основном "эффективностью" сохранения/загрузки, или есть расчеты, которые вы делаете с массивами, которые работали бы лучше, если бы они были объединены в один многомерный массив? Все ли самые внутренние массивы имеют одинаковую форму?

hpaulj 07.01.2023 21:42

С этим savez он создает файл npy для каждого внешнего ключа. И да, каждый файл будет рассолом dict с внутренними ключами. Я не думаю, что это слишком сильно повредит файловому пространству, хотя я не исследовал использование памяти dict pickle. pickle массива — это его savenpy файл.

hpaulj 07.01.2023 21:46

Меня интересует эффективность сохранения (скорость записи и размер файла) и просто узнать, является ли мой вложенный подход к диктовке разумным или нет. Внутренние массивы имеют одинаковый размер да. Таким образом, я мог бы просто создать огромный трехмерный массив numpy, где каждый столбец представляет собой другое свойство, каждая строка дает значения свойств в разное время, и вы складываете эти двухмерные массивы для разных объектов в 3-м измерении. это должна быть более эффективная структура данных, но я хотел бы сохранить «заголовок» имени каждого столбца и идентификатор каждого объекта по 3-й оси. Некоторые столбцы могут быть строками или иметь nan/inf

quantumflash 07.01.2023 22:41

массивы не имеют «заголовков». Датафреймы делают. Но является ли фрейм с элементами массива более «эффективным», неизвестно.

hpaulj 07.01.2023 23:07

Зная размеры, вы можете оценить общее использование памяти, по крайней мере, для данных массива, а вместе с ним и общее количество файлов. Например. 100000*100*1000*8/1e9 составляет 80 ГБ данных. Я предполагаю, что вложенная dict часть хранилища будет измеряться в МБ, размер keys, в памяти dict использовать какую-то хеш-таблицу сортировки, которая занимает некоторое место, но я не знаю, как это закодировано, если вообще в pickle. При распаковке может потребоваться воссоздание хеш-таблицы из эквивалентных списков (list(d.items())).

hpaulj 07.01.2023 23:21

Хм интересно. Я думаю, что одна вещь, которую я хочу изучить, это как преобразовать мой вложенный дикт в фрейм данных pandas с мультииндексацией (чтобы, как я уже сказал, я все еще мог получить доступ к 2D-массиву каждого объекта, используя его имя, а затем фрейм данных также может позвольте мне сохранить имена, связанные с каждым из моих столбцов, а не просто числовые индексы). И затем, наконец, просто используйте собственную функцию .to_hdf pandas, чтобы сохранить фрейм данных в файл hdf5. (И я могу разделить свой большой файл размером 80 ГБ на отдельные файлы с группами объектов, чтобы ограничить размер отдельного файла.) Таким образом, я также получаю сжимаемость hdf5.

quantumflash 07.01.2023 23:58

комментарии к принятому ответу здесь актуальны (о вложенном дикторе против мультииндексного фрейма данных pandas): stackoverflow.com/questions/22661764/…

quantumflash 07.01.2023 23:59

Использование dicts не является хорошей идеей для больших наборов данных: это потребляет много памяти (из-за повторяющихся ключей), а объекты dicts неэффективны. Кадр данных Pandas намного компактнее и часто более эффективен, за исключением строк или других столбцов на основе объектов. Здесь похоже, что содержимое представляет собой массив Numpy разного размера, поэтому Pandas сохранит их как объект. Таким образом, это будет еще немного компактнее, чем dict (нет необходимости повторять ключ для каждой строки), но неэффективно из-за объектов, хранящихся в каждом столбце. Зубчатые массивы также неэффективны в Numpy.

Jérôme Richard 08.01.2023 00:34

Хотя существуют более компактные и более эффективные структуры данных, они также могут быть значительно менее удобными для пользователя. Кроме того, для информации не хватает: что вы планируете делать со структурой данных? Только читается или планируете менять, и если да, то как? Все ли массивы типа float? Возможно ли для вас уменьшить точность с 64-битных чисел с плавающей точкой до 362-битных? Какую операцию вы планируете делать на нем?

Jérôme Richard 08.01.2023 00:39

@JérômeRichard, я не уверен, что этот дикт настолько неэффективен, особенно в таком случае. Если все ключи субдиктов одинаковы, и особенно если они представляют собой короткие строки, как в примере, они не будут занимать много памяти. Каждый подраздел будет просто иметь ссылки на один и тот же небольшой набор строк. В своем ответе я обнаружил, что pickle.dumps для одного из этих субдиктов примерно того же размера, что и его list(dd.values()), и меньше, чем фрейм данных или даже повторный перенос.

hpaulj 08.01.2023 01:16

@hpaulj Дело в том, что использование рассола в первую очередь, безусловно, неэффективно, хотя для диктов не намного лучше. Я не думаю, что он эффективно записывает массив Numpy (это то, на что указывает ваш ответ). Похоже, что данные также неэффективно хранятся. Похоже, что данные хранятся как объекты, например, для списков. Для кадра данных HDF5, вероятно, лучше, хотя он может быть не оптимален для небольших массивов и в этом случае. Для датафреймов есть паркет, который должен быть лучше.

Jérôme Richard 08.01.2023 02:53

Возможно, вместо этого внутренние словари могли бы быть namedtuple. После того, как вы собрали данные, вы создаете один массив numpy. Тогда есть более эффективные способы хранения, например, паркет.

tdelaney 08.01.2023 03:32

@JérômeRichard, хотя np.save этого массива составляет 612 байт; массив достаточно мал, чтобы заголовок npy добавлял 50%. Я предполагал или читал, что, кроме заголовков, рассол массивов numpy был таким же, как np.save.

hpaulj 08.01.2023 03:33
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
13
109
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Давайте посмотрим на ваше значение obj2, дикт:

In [307]: dd = {'time':np.arange(15),'x':np.random.randn(15),'vx':np.random.randn(15)}

In [308]: dd
Out[308]: 
{'time': array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14]),
 'x': array([-0.48197915,  0.15597792,  0.44113401,  1.38062753, -1.21273378,
        -1.27120008,  1.53072667,  1.9799255 ,  0.13647925, -1.37056793,
        -2.06470784,  0.92314969,  0.30885371,  0.64860014,  1.30273519]),
 'vx': array([-1.60228105, -1.49163002, -1.17061046, -0.09267467, -0.94133092,
         1.86391024,  1.006901  , -0.16168439,  1.5180135 , -1.16436363,
        -0.20254291, -1.60280149, -1.91749387,  0.25366602, -1.61993012])}

Из этого легко сделать фрейм данных:

In [309]: df = pd.DataFrame(dd)

In [310]: df
Out[310]: 
    time         x        vx
0      0 -0.481979 -1.602281
1      1  0.155978 -1.491630
2      2  0.441134 -1.170610
3      3  1.380628 -0.092675
4      4 -1.212734 -0.941331
5      5 -1.271200  1.863910
6      6  1.530727  1.006901
7      7  1.979926 -0.161684
8      8  0.136479  1.518014
9      9 -1.370568 -1.164364
10    10 -2.064708 -0.202543
11    11  0.923150 -1.602801
12    12  0.308854 -1.917494
13    13  0.648600  0.253666
14    14  1.302735 -1.619930

Мы также можем сделать структурированный массив из этого фрейма. Я также мог бы сделать массив непосредственно из вашего словаря, определив тот же составной тип dtype. Но так как у меня уже есть рама, я пойду по этому пути. Различие между структурированным массивом и переназначением незначительно.

In [312]: arr = df.to_records()

In [313]: arr
Out[313]: 
rec.array([( 0,  0, -0.48197915, -1.60228105),
           ( 1,  1,  0.15597792, -1.49163002),
           ( 2,  2,  0.44113401, -1.17061046),
           ( 3,  3,  1.38062753, -0.09267467),
           ( 4,  4, -1.21273378, -0.94133092),
           ( 5,  5, -1.27120008,  1.86391024),
           ( 6,  6,  1.53072667,  1.006901  ),
           ( 7,  7,  1.9799255 , -0.16168439),
           ( 8,  8,  0.13647925,  1.5180135 ),
           ( 9,  9, -1.37056793, -1.16436363),
           (10, 10, -2.06470784, -0.20254291),
           (11, 11,  0.92314969, -1.60280149),
           (12, 12,  0.30885371, -1.91749387),
           (13, 13,  0.64860014,  0.25366602),
           (14, 14,  1.30273519, -1.61993012)],
          dtype=[('index', '<i8'), ('time', '<i4'), ('x', '<f8'), ('vx', '<f8')])

Теперь давайте сравним строки огурцов:

In [314]: import pickle

In [315]: len(pickle.dumps(dd))
Out[315]: 561

In [316]: len(pickle.dumps(df))      # df.to_pickle makes a 1079 byte file
Out[316]: 1052

In [317]: len(pickle.dumps(arr))     # arr.nbytes is 420
Out[317]: 738                        # np.save writes a 612 byte file

И другая кодировка - список:

In [318]: alist = list(dd.items())
In [319]: alist
Out[319]: 
[('time', array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])),
 ('x',
  array([-0.48197915,  0.15597792,  0.44113401,  1.38062753, -1.21273378,
         -1.27120008,  1.53072667,  1.9799255 ,  0.13647925, -1.37056793,
         -2.06470784,  0.92314969,  0.30885371,  0.64860014,  1.30273519])),
 ('vx',
  array([-1.60228105, -1.49163002, -1.17061046, -0.09267467, -0.94133092,
          1.86391024,  1.006901  , -0.16168439,  1.5180135 , -1.16436363,
         -0.20254291, -1.60280149, -1.91749387,  0.25366602, -1.61993012]))]
In [320]: len(pickle.dumps(alist))
Out[320]: 567

Спасибо! Что, если бы я хотел преобразовать свой вложенный словарь properties во «вложенный фрейм данных панд» (или, я думаю, серию кадров данных?). Была бы это эффективной структурой данных, особенно если бы все объекты имели массивы одинакового размера, так что вы могли бы просто складывать по 3-му измерению? Другими словами: ключ внешнего фрейма данных/серии = объект #/строка, а значение для любого объекта представляет собой 2D-фрейм данных, такой как ваш df выше, и далее предположим, что все объекты имеют столбцы одинаковой длины для своих собственных df. Я бы, наверное, сохранил как hdf5 (с сжатием или без, в зависимости от времени сохранения/загрузки).

quantumflash 17.01.2023 00:49

Возможно, вам нужна мультииндексация pandas. Ячейки Pandas (серии) могут быть типа объекта dtype и содержать списки, массивы или строки, но они неэффективны (ничего похожего на многомерные массивы), и их может быть сложно сохранить/загрузить (csv формат файла по своей сути 2d). Но это далеко за пределами моего опыта работы с пандами.

hpaulj 17.01.2023 01:29

Другие вопросы по теме

Как я могу добавить новый столбец в фрейм данных (df1), который представляет собой сумму нескольких значений поиска из df1 в другом фрейме данных (df2)
Как применить пользовательскую функцию к xarray.DataArray.coarsen.reduce()?
Явная передача скаляра
Сравнивая значения N столбцов данных друг с другом и проверяя, находятся ли они в порядке возрастания или убывания
Как заменить список/кортеж словарями в рабочем коде, чтобы повысить его производительность?
Как найти все уникальные комбинации кортежа размера k, используя один элемент из каждого списка из n списков
RuntimeError: размеры массивов должны совпадать, за исключением измерения 1
Есть ли способ избежать неточных результатов при использовании линейной алгебры с numpy?
Как создать список файлов DICOM и преобразовать его в один массив numpy .npy?
Векторизация функции поиска локальных минимумов и максимумов в двумерном массиве со строгим сравнением

Похожие вопросы