Улучшение панд (PyTables?) Таблица HDF5 записи
я использую панд для исследований уже около двух месяцев с большим эффектом. С большим количеством наборов данных событий трассировки среднего размера pandas + PyTables (интерфейс HDF5) делает огромную работу, позволяя мне обрабатывать гетерогенные данные, используя все инструменты Python, которые я знаю и люблю.
вообще говоря, я использую фиксированный (ранее "Storer") формат в PyTables, так как мой рабочий процесс записывается один раз, читается много, и многие из моих наборов данных имеют такой размер, что я могу загрузить 50-100 их в память за один раз без серьезных недостатков. (NB: я делаю большую часть своей работы на машинах серверного класса Opteron с системной памятью 128GB+.)
однако для больших наборов данных (500 МБ и выше) я хотел бы иметь возможность использовать более масштабируемые возможности произвольного доступа и запросов формата "таблицы" PyTables, чтобы я мог выполнять свои запросы из памяти, а затем загружать гораздо меньший набор результатов в память для обработки. Самой большой проблемой здесь, однако, писать спектакль. Да, как я уже сказал, мой рабочий процесс-писать один раз, читать-много, но относительное время по-прежнему неприемлемо.
например, недавно я запустил большую факторизацию Cholesky, которая заняла 3 минуты 8 секунд (188 секунд) на моей 48-ядерном компьютере. При этом генерируется файл трассировки ~2.2 Гб-трассировка генерируется параллельно с программой, поэтому дополнительного "времени создания трассировки" нет."
начальное преобразование моего двоичного файла трассировки в pandas/PyTables формат занимает приличный кусок времени, но в основном потому, что двоичный формат намеренно выходит из строя, чтобы уменьшить влияние производительности самого генератора трассировки. Это также не имеет отношения к потере производительности при переходе из формата хранилища в формат таблицы.
мои тесты были первоначально запущены с pandas 0.12, numpy 1.7.1, PyTables 2.4.0 и numexpr 0.20.1. Моя 48-ядерная машина работает на 2.8 GHz на ядро, и я пишу в файловую систему ext3, которая, вероятно (но не обязательно) на SSD.
я могу записать весь набор данных в файл формата HDF5 хранилища (результирующий размер файла: 3,3 ГБ) за 7,1 секунды. Тот же набор данных, записанный в формат таблицы (размер результирующего файла также 3,3 ГБ), занимает 178,7 секунды для записи.
код выглядит следующим образом:
with Timer() as t:
store = pd.HDFStore('test_storer.h5', 'w')
store.put('events', events_dataset, table=False, append=False)
print('Fixed format write took ' + str(t.interval))
with Timer() as t:
store = pd.HDFStore('test_table.h5', 'w')
store.put('events', events_dataset, table=True, append=False)
print('Table format write took ' + str(t.interval))
и выход просто
Fixed format write took 7.1
Table format write took 178.7
мой набор данных имеет 28,880,943 строки, а столбцы являются основными типами данных:
node_id int64
thread_id int64
handle_id int64
type int64
begin int64
end int64
duration int64
flags int64
unique_id int64
id int64
DSTL_LS_FULL float64
L2_DMISS float64
L3_MISS float64
kernel_type float64
dtype: object
...так что я не думаю, что должны быть какие-либо проблемы с данными со скоростью записи.
я также попытался добавить сжатие BLOSC, чтобы исключить любые странные проблемы ввода-вывода, которые могут повлиять на тот или иной сценарий, но сжатие, похоже, снижает производительность обоих одинаково.
теперь я понимаю, что документация pandas говорит, что формат хранилища предлагает значительно быстрее пишет и немного быстрее читает. (Я действительно испытываю более быстрые чтения, как чтение хранилища формат, кажется, занимает около 2,5 секунд, в то время как чтение формата таблицы занимает около 10 секунд.) Но действительно кажется чрезмерным, что запись в формате таблицы должна занимать в 25 раз больше времени, чем запись в формате хранилища.
может ли кто-либо из людей, связанных с PyTables или pandas объяснить архитектурные (или иначе) причины, почему запись в запрашиваемый формат (который явно требует очень мало дополнительных данных) должна занять на порядок больше времени? И есть ли надежда на улучшение это в будущем? Я бы хотел внести свой вклад в тот или иной проект, поскольку моя область-высокопроизводительные вычисления, и я вижу значительный вариант использования для обоих проектов в этой области.... но было бы полезно, чтобы получить некоторые разъяснения по вопросам первого, и/или некоторые советы о том, как ускорить от тех, кто знает, как построена система.
EDIT:
запуск предыдущих тестов с %prun в IPython дает следующее (Несколько уменьшенное для удобства чтения) вывод профиля для хранилища / фиксированный формат:
%prun -l 20 profile.events.to_hdf('test.h5', 'events', table=False, append=False)
3223 function calls (3222 primitive calls) in 7.385 seconds
Ordered by: internal time
List reduced from 208 to 20 due to restriction <20>
ncalls tottime percall cumtime percall filename:lineno(function)
6 7.127 1.188 7.128 1.188 {method '_createArray' of 'tables.hdf5Extension.Array' objects}
1 0.242 0.242 0.242 0.242 {method '_closeFile' of 'tables.hdf5Extension.File' objects}
1 0.003 0.003 0.003 0.003 {method '_g_new' of 'tables.hdf5Extension.File' objects}
46 0.001 0.000 0.001 0.000 {method 'reduce' of 'numpy.ufunc' objects}
и следующее для формата таблиц:
%prun -l 40 profile.events.to_hdf('test.h5', 'events', table=True, append=False, chunksize=1000000)
499082 function calls (499040 primitive calls) in 188.981 seconds
Ordered by: internal time
List reduced from 526 to 40 due to restriction <40>
ncalls tottime percall cumtime percall filename:lineno(function)
29 92.018 3.173 92.018 3.173 {pandas.lib.create_hdf_rows_2d}
640 20.987 0.033 20.987 0.033 {method '_append' of 'tables.hdf5Extension.Array' objects}
29 19.256 0.664 19.256 0.664 {method '_append_records' of 'tables.tableExtension.Table' objects}
406 19.182 0.047 19.182 0.047 {method '_g_writeSlice' of 'tables.hdf5Extension.Array' objects}
14244 10.646 0.001 10.646 0.001 {method '_g_readSlice' of 'tables.hdf5Extension.Array' objects}
472 10.359 0.022 10.359 0.022 {method 'copy' of 'numpy.ndarray' objects}
80 3.409 0.043 3.409 0.043 {tables.indexesExtension.keysort}
2 3.023 1.512 3.023 1.512 common.py:134(_isnull_ndarraylike)
41 2.489 0.061 2.533 0.062 {method '_fillCol' of 'tables.tableExtension.Row' objects}
87 2.401 0.028 2.401 0.028 {method 'astype' of 'numpy.ndarray' objects}
30 1.880 0.063 1.880 0.063 {method '_g_flush' of 'tables.hdf5Extension.Leaf' objects}
282 0.824 0.003 0.824 0.003 {method 'reduce' of 'numpy.ufunc' objects}
41 0.537 0.013 0.668 0.016 index.py:607(final_idx32)
14490 0.385 0.000 0.712 0.000 array.py:342(_interpret_indexing)
39 0.279 0.007 19.635 0.503 index.py:1219(reorder_slice)
2 0.256 0.128 10.063 5.031 index.py:1099(get_neworder)
1 0.090 0.090 119.392 119.392 pytables.py:3016(write_data)
57842 0.087 0.000 0.087 0.000 {numpy.core.multiarray.empty}
28570 0.062 0.000 0.107 0.000 utils.py:42(is_idx)
14164 0.062 0.000 7.181 0.001 array.py:711(_readSlice)
EDIT 2:
запуск снова с предварительной версией pandas 0.13 (вытащил 20 ноября 2013 года около 11:00 EST), время записи для формата таблиц значительно улучшилось, но все еще не сравнивается "разумно" со скоростью записи в формате Storer/Fixed.
%prun -l 40 profile.events.to_hdf('test.h5', 'events', table=True, append=False, chunksize=1000000)
499748 function calls (499720 primitive calls) in 117.187 seconds
Ordered by: internal time
List reduced from 539 to 20 due to restriction <20>
ncalls tottime percall cumtime percall filename:lineno(function)
640 22.010 0.034 22.010 0.034 {method '_append' of 'tables.hdf5Extension.Array' objects}
29 20.782 0.717 20.782 0.717 {method '_append_records' of 'tables.tableExtension.Table' objects}
406 19.248 0.047 19.248 0.047 {method '_g_writeSlice' of 'tables.hdf5Extension.Array' objects}
14244 10.685 0.001 10.685 0.001 {method '_g_readSlice' of 'tables.hdf5Extension.Array' objects}
472 10.439 0.022 10.439 0.022 {method 'copy' of 'numpy.ndarray' objects}
30 7.356 0.245 7.356 0.245 {method '_g_flush' of 'tables.hdf5Extension.Leaf' objects}
29 7.161 0.247 37.609 1.297 pytables.py:3498(write_data_chunk)
2 3.888 1.944 3.888 1.944 common.py:197(_isnull_ndarraylike)
80 3.581 0.045 3.581 0.045 {tables.indexesExtension.keysort}
41 3.248 0.079 3.294 0.080 {method '_fillCol' of 'tables.tableExtension.Row' objects}
34 2.744 0.081 2.744 0.081 {method 'ravel' of 'numpy.ndarray' objects}
115 2.591 0.023 2.591 0.023 {method 'astype' of 'numpy.ndarray' objects}
270 0.875 0.003 0.875 0.003 {method 'reduce' of 'numpy.ufunc' objects}
41 0.560 0.014 0.732 0.018 index.py:607(final_idx32)
14490 0.387 0.000 0.712 0.000 array.py:342(_interpret_indexing)
39 0.303 0.008 19.617 0.503 index.py:1219(reorder_slice)
2 0.288 0.144 10.299 5.149 index.py:1099(get_neworder)
57871 0.087 0.000 0.087 0.000 {numpy.core.multiarray.empty}
1 0.084 0.084 45.266 45.266 pytables.py:3424(write_data)
1 0.080 0.080 55.542 55.542 pytables.py:3385(write)
я заметил во время выполнения этих тестов, что там длительные периоды, когда запись кажется "паузой" (файл на диске не активно растет), и все же есть также низкое использование процессора в течение некоторых из этих периодов.
я начинаю подозревать, что некоторые известные ограничения ext3 могут плохо взаимодействовать с панд или PyTables. Ext3 и другие файловые системы, не основанные на экстенте, иногда пытаются быстро разорвать связь с большими файлами, и аналогичная производительность системы (низкое использование процессора, но долгое время ожидания) очевидна даже во время простой " rm " файла 1GB, например.
чтобы уточнить, в каждом тестовом случае я обязательно удалил существующий файл, если таковой имеется, перед запуском теста, чтобы не нести никакого штрафа за удаление/перезапись файла ext3.
однако при повторном запуске этого теста с index=None производительность резко улучшается (~50s против ~120 при индексировании). Таким образом, казалось бы, либо этот процесс по-прежнему привязан к процессору (моя система имеет относительно старые процессоры AMD Opteron Istanbul под управлением @ 2.8 GHz, хотя это так также есть 8 сокетов с 6 основными процессорами в каждом, все, кроме одного из которых, конечно, сидят без дела во время записи), или что существует некоторый конфликт между тем, как PyTables или pandas пытается манипулировать/читать/анализировать файл, когда уже частично или полностью в файловой системе, что вызывает патологически плохое поведение ввода-вывода при индексировании.
EDIT 3:
@Jeff предложил тесты на меньшем наборе данных (1.3 Гб на диске) после обновления PyTables с 2.4 до 3.0.0, привели меня сюда:
In [7]: %timeit f(df)
1 loops, best of 3: 3.7 s per loop
In [8]: %timeit f2(df) # where chunksize= 2 000 000
1 loops, best of 3: 13.8 s per loop
In [9]: %timeit f3(df) # where chunksize= 2 000 000
1 loops, best of 3: 43.4 s per loop
на самом деле, моя производительность, похоже, превосходит его во всех сценариях, за исключением случаев, когда индексирование включено (по умолчанию). Однако индексирование по-прежнему кажется убийцей, и если я интерпретирую вывод из top
и ls
когда я запускаю эти тесты, остаются периоды времени, когда не происходит существенной обработки или записи файлов (т. е. использование ЦП для процесса Python близко к 0, а размер файла остается постоянный.) Я могу только предположить, что это чтение файлов. Почему чтение файлов вызывает замедление, мне трудно понять, так как я могу надежно загрузить весь файл 3+ GB с этого диска в память менее чем за 3 секунды. Если они не читают файлы, то что система "ждет"? (Никто больше не входит в систему, и нет никакой другой активности файловой системы.)
на данный момент, с обновленными версиями соответствующих модулей python, производительность для моего исходного набора данных до следующей цифры. Особый интерес представляет системное время, которое, как я предполагаю, является по крайней мере верхней границей времени, потраченного на выполнение ввода-вывода, и время стены, которое, возможно, объясняет эти таинственные периоды без записи/без активности процессора.
In [28]: %time f(profile.events)
CPU times: user 0 ns, sys: 7.16 s, total: 7.16 s
Wall time: 7.51 s
In [29]: %time f2(profile.events)
CPU times: user 18.7 s, sys: 14 s, total: 32.7 s
Wall time: 47.2 s
In [31]: %time f3(profile.events)
CPU times: user 1min 18s, sys: 14.4 s, total: 1min 32s
Wall time: 2min 5s
тем не менее, похоже, что индексирование вызывает значительное замедление для моего варианта использования. Возможно, я должен попытаться ограничить индексированные поля, а не просто выполнять случай по умолчанию (который вполне может быть индексирование всех полей в фрейме данных)? Я не уверен, как это может повлиять на время запроса, особенно в случаях, когда запрос выбирает на основе неиндексированного поля.
по запросу Джеффа, ptdump результирующего файла.
ptdump -av test.h5
/ (RootGroup) ''
/._v_attrs (AttributeSet), 4 attributes:
[CLASS := 'GROUP',
PYTABLES_FORMAT_VERSION := '2.1',
TITLE := '',
VERSION := '1.0']
/df (Group) ''
/df._v_attrs (AttributeSet), 14 attributes:
[CLASS := 'GROUP',
TITLE := '',
VERSION := '1.0',
data_columns := [],
encoding := None,
index_cols := [(0, 'index')],
info := {1: {'type': 'Index', 'names': [None]}, 'index': {}},
levels := 1,
nan_rep := 'nan',
non_index_axes :=
[(1, ['node_id', 'thread_id', 'handle_id', 'type', 'begin', 'end', 'duration', 'flags', 'unique_id', 'id', 'DSTL_LS_FULL', 'L2_DMISS', 'L3_MISS', 'kernel_type'])],
pandas_type := 'frame_table',
pandas_version := '0.10.1',
table_type := 'appendable_frame',
values_cols := ['values_block_0', 'values_block_1']]
/df/table (Table(28880943,)) ''
description := {
"index": Int64Col(shape=(), dflt=0, pos=0),
"values_block_0": Int64Col(shape=(10,), dflt=0, pos=1),
"values_block_1": Float64Col(shape=(4,), dflt=0.0, pos=2)}
byteorder := 'little'
chunkshape := (4369,)
autoindex := True
colindexes := {
"index": Index(6, medium, shuffle, zlib(1)).is_csi=False}
/df/table._v_attrs (AttributeSet), 15 attributes:
[CLASS := 'TABLE',
FIELD_0_FILL := 0,
FIELD_0_NAME := 'index',
FIELD_1_FILL := 0,
FIELD_1_NAME := 'values_block_0',
FIELD_2_FILL := 0.0,
FIELD_2_NAME := 'values_block_1',
NROWS := 28880943,
TITLE := '',
VERSION := '2.7',
index_kind := 'integer',
values_block_0_dtype := 'int64',
values_block_0_kind := ['node_id', 'thread_id', 'handle_id', 'type', 'begin', 'end', 'duration', 'flags', 'unique_id', 'id'],
values_block_1_dtype := 'float64',
values_block_1_kind := ['DSTL_LS_FULL', 'L2_DMISS', 'L3_MISS', 'kernel_type']]
и еще %prun с обновленными модулями и полным набором данных:
%prun -l 25 %time f3(profile.events)
CPU times: user 1min 14s, sys: 16.2 s, total: 1min 30s
Wall time: 1min 48s
542678 function calls (542650 primitive calls) in 108.678 seconds
Ordered by: internal time
List reduced from 629 to 25 due to restriction <25>
ncalls tottime percall cumtime percall filename:lineno(function)
640 23.633 0.037 23.633 0.037 {method '_append' of 'tables.hdf5extension.Array' objects}
15 20.852 1.390 20.852 1.390 {method '_append_records' of 'tables.tableextension.Table' objects}
406 19.584 0.048 19.584 0.048 {method '_g_write_slice' of 'tables.hdf5extension.Array' objects}
14244 10.591 0.001 10.591 0.001 {method '_g_read_slice' of 'tables.hdf5extension.Array' objects}
458 9.693 0.021 9.693 0.021 {method 'copy' of 'numpy.ndarray' objects}
15 6.350 0.423 30.989 2.066 pytables.py:3498(write_data_chunk)
80 3.496 0.044 3.496 0.044 {tables.indexesextension.keysort}
41 3.335 0.081 3.376 0.082 {method '_fill_col' of 'tables.tableextension.Row' objects}
20 2.551 0.128 2.551 0.128 {method 'ravel' of 'numpy.ndarray' objects}
101 2.449 0.024 2.449 0.024 {method 'astype' of 'numpy.ndarray' objects}
16 1.789 0.112 1.789 0.112 {method '_g_flush' of 'tables.hdf5extension.Leaf' objects}
2 1.728 0.864 1.728 0.864 common.py:197(_isnull_ndarraylike)
41 0.586 0.014 0.842 0.021 index.py:637(final_idx32)
14490 0.292 0.000 0.616 0.000 array.py:368(_interpret_indexing)
2 0.283 0.142 10.267 5.134 index.py:1158(get_neworder)
274 0.251 0.001 0.251 0.001 {method 'reduce' of 'numpy.ufunc' objects}
39 0.174 0.004 19.373 0.497 index.py:1280(reorder_slice)
57857 0.085 0.000 0.085 0.000 {numpy.core.multiarray.empty}
1 0.083 0.083 35.657 35.657 pytables.py:3424(write_data)
1 0.065 0.065 45.338 45.338 pytables.py:3385(write)
14164 0.065 0.000 7.831 0.001 array.py:615(__getitem__)
28570 0.062 0.000 0.108 0.000 utils.py:47(is_idx)
47 0.055 0.001 0.055 0.001 {numpy.core.multiarray.arange}
28570 0.050 0.000 0.090 0.000 leaf.py:397(_process_range)
87797 0.048 0.000 0.048 0.000 {isinstance}
2 ответов
Это интересная дискуссия. Я думаю, что Питер получает потрясающую производительность для фиксированного формата, потому что формат пишет в одном кадре, а также что у него действительно хороший SSD (он может писать более 450 МБ/с).
добавление в таблицу является более сложной операцией (набор данных должен быть увеличен, и новые записи должны быть проверены, чтобы мы могли убедиться, что они следуют схеме таблицы). Вот почему добавление строк в таблицы обычно медленнее (но все же, Джефф получает ~ 70 МБ/с, что довольно хорошо). То, что Джефф получает больше скорости, чем Питер, вероятно, связано с тем, что у него лучший процессор.
наконец, индексирование в PyTables использует один процессор, да, и это обычно дорогостоящая операция, поэтому вы должны действительно отключить его, если вы не собираетесь запрашивать данные на диске.
вот аналогичное сравнение, которое я только что сделал. Его около 1/3 строк данных 10M. Окончательный размер abou 1.3 GB
Я определяю 3 функции времени:
Проверьте фиксированный формат (называемый Storer в 0.12). Это пишет в формате массива PyTables
def f(df):
store = pd.HDFStore('test.h5','w')
store['df'] = df
store.close()
запись в формате таблицы, используя формат таблицы PyTables. Не создавайте индекс.
def f2(df):
store = pd.HDFStore('test.h5','w')
store.append('df',df,index=False)
store.close()
то же, что и f2, но создайте индекс (который обычно готово)
def f3(df):
store = pd.HDFStore('test.h5','w')
store.append('df',df)
store.close()
создать рамки
In [25]: df = concat([DataFrame(np.random.randn(10000000,10)),DataFrame(np.random.randint(0,10,size=50000000).reshape(10000000,5))],axis=1)
In [26]: df
Out[26]:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 10000000 entries, 0 to 9999999
Columns: 15 entries, 0 to 4
dtypes: float64(10), int64(5)
v0.12.0
In [27]: %timeit f(df)
1 loops, best of 3: 14.7 s per loop
In [28]: %timeit f2(df)
1 loops, best of 3: 32 s per loop
In [29]: %timeit f3(df)
1 loops, best of 3: 40.1 s per loop
master/v0.13.0
In [5]: %timeit f(df)
1 loops, best of 3: 12.9 s per loop
In [6]: %timeit f2(df)
1 loops, best of 3: 17.5 s per loop
In [7]: %timeit f3(df)
1 loops, best of 3: 24.3 s per loop
Timing работает с тем же файлом, что и OP (ссылка ниже)
In [4]: df = pd.read_hdf('test.h5','df')
In [5]: df
Out[5]:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 28880943 entries, 0 to 28880942
Columns: 14 entries, node_id to kernel_type
dtypes: float64(4), int64(10)
как f1, фиксированный формат
In [6]: %timeit df.to_hdf('test.hdf','df',mode='w')
1 loops, best of 3: 36.2 s per loop
как f2, формат таблицы, без индекса
In [7]: %timeit df.to_hdf('test.hdf','df',mode='w',format='table',index=False)
1 loops, best of 3: 45 s per loop
In [8]: %timeit df.to_hdf('test.hdf','df',mode='w',format='table',index=False,chunksize=2000000)
1 loops, best of 3: 44.5 s per loop
как f3, формат таблицы с индексом
In [9]: %timeit df.to_hdf('test.hdf','df',mode='w',format='table',chunksize=2000000)
1 loops, best of 3: 1min 36s per loop
как f3, формат таблицы с индексом, сжатый с blosc
In [10]: %timeit df.to_hdf('test.hdf','df',mode='w',format='table',chunksize=2000000,complib='blosc')
1 loops, best of 3: 46.5 s per loop
In [11]: %timeit pd.read_hdf('test.hdf','df')
1 loops, best of 3: 10.8 s per loop
показать исходный файл (тест.H5, и сжатый, тест.hdf)
In [13]: !ls -ltr test.h*
-rw-r--r-- 1 jreback users 3471518282 Nov 20 18:20 test.h5
-rw-rw-r-- 1 jreback users 649327780 Nov 20 21:17 test.hdf
несколько моментов, чтобы отметить.
-
не создание индекса может сделать нетривиальную разницу во времени. Я тоже считаю, что если у вас есть строковый индекс, это может существенно ухудшить время. Тем не менее, вы всегда хотите создать индекс, чтобы сделать поиск очень быстрым.
вы не включили, что ваш индекс, ни его сортировка (хотя я думаю, что это делает небольшой разница.)
штраф за запись в моих примерах примерно 2x (хотя я видел, что он несколько больше, включая время индекса). Таким образом, ваши 7s (1/2 моего времени), для 3x число, которое я пишу, довольно подозрительно. Я использую достаточно быстрый дисковый массив. Если вы использовали флэш-диск на основе, то это возможно, хотя.
master / v0.13.0 (выпуск очень скоро), улучшает время записи в таблицах по существу.
вы можете попробовать задание
chunksize
параметр на большее число при записи данных (по умолчанию 100000). Целью "относительно" низкого числа является постоянное использование памяти. (например, если больше, вы будете использовать больше памяти, теоретически она должна писать быстрее).таблицы предлагаем 2 преимущества над фиксированном формате: 1) получение запроса и 2) appendability. Чтение всей таблицы не занимает преимущество любого из них, поэтому, если вы хотите прочитать только всю таблицу, рекомендуется фиксированный формат. (По моему опыту, гибкость таблиц значительно перевешивает штраф за запись, но YMMV)
суть в том, чтобы повторить тайминги (используйте ipython, поскольку он будет запускать несколько тестов). Если вы можете воспроизвести свои результаты, то PLS опубликует %prun, и я посмотрю.
обновление:
таким образом, рекомендуемый способ для таблицы такого размера-сжатие с blosc и используйте pandas master / 0.13.0 вместе с PyTables 3.0.0