Spec-Zone.ru › pandas 2

Масштабирование для больших наборов данных

pandas предоставляет структуры данных для аналитики в оперативной памяти, что делает использование pandas для анализа наборов данных, превышающих объем оперативной памяти, несколько сложным. Даже наборы данных, занимающие значительную часть памяти, становятся неудобными, так как некоторые операции pandas требуют промежуточных копий.

Этот документ содержит несколько рекомендаций по масштабированию вашего анализа для больших наборов данных. Это дополнение к Улучшение производительности, которое фокусируется на ускорении анализа для наборов данных, помещающихся в оперативную память.

Загрузка меньшего объема данных

Предположим, что наш исходный набор данных на диске содержит множество столбцов.

In [1]: import pandas as pd

In [2]: import numpy as np

In [3]: def make_timeseries(start="2000-01-01", end="2000-12-31", freq="1D", seed=None):
   ...:     index = pd.date_range(start=start, end=end, freq=freq, name="timestamp")
   ...:     n = len(index)
   ...:     state = np.random.RandomState(seed)
   ...:     columns = {
   ...:         "name": state.choice(["Alice", "Bob", "Charlie"], size=n),
   ...:         "id": state.poisson(1000, size=n),
   ...:         "x": state.rand(n) * 2 - 1,
   ...:         "y": state.rand(n) * 2 - 1,
   ...:     }
   ...:     df = pd.DataFrame(columns, index=index, columns=sorted(columns))
   ...:     if df.index[-1] == end:
   ...:         df = df.iloc[:-1]
   ...:     return df
   ...: 

In [4]: timeseries = [
   ...:     make_timeseries(freq="1min", seed=i).rename(columns=lambda x: f"{x}_{i}")
   ...:     for i in range(10)
   ...: ]
   ...: 

In [5]: ts_wide = pd.concat(timeseries, axis=1)

In [6]: ts_wide.head()
Out[6]: 
                     id_0 name_0       x_0  ...   name_9       x_9       y_9
timestamp                                   ...                             
2000-01-01 00:00:00   977  Alice -0.821225  ...  Charlie -0.957208 -0.757508
2000-01-01 00:01:00  1018    Bob -0.219182  ...    Alice -0.414445 -0.100298
2000-01-01 00:02:00   927  Alice  0.660908  ...  Charlie -0.325838  0.581859
2000-01-01 00:03:00   997    Bob -0.852458  ...      Bob  0.992033 -0.686692
2000-01-01 00:04:00   965    Bob  0.717283  ...  Charlie -0.924556 -0.184161

[5 rows x 40 columns]

In [7]: ts_wide.to_parquet("timeseries_wide.parquet")

Для загрузки необходимых столбцов у нас есть два варианта. Вариант 1 загружает все данные, а затем фильтрует их до нужных.

In [8]: columns = ["id_0", "name_0", "x_0", "y_0"]

In [9]: pd.read_parquet("timeseries_wide.parquet")[columns]
Out[9]: 
                     id_0 name_0       x_0       y_0
timestamp                                           
2000-01-01 00:00:00   977  Alice -0.821225  0.906222
2000-01-01 00:01:00  1018    Bob -0.219182  0.350855
2000-01-01 00:02:00   927  Alice  0.660908 -0.798511
2000-01-01 00:03:00   997    Bob -0.852458  0.735260
2000-01-01 00:04:00   965    Bob  0.717283  0.393391
...                   ...    ...       ...       ...
2000-12-30 23:56:00  1037    Bob -0.814321  0.612836
2000-12-30 23:57:00   980    Bob  0.232195 -0.618828
2000-12-30 23:58:00   965  Alice -0.231131  0.026310
2000-12-30 23:59:00   984  Alice  0.942819  0.853128
2000-12-31 00:00:00  1003  Alice  0.201125 -0.136655

[525601 rows x 4 columns]

Вариант 2 загружает только запрошенные столбцы.

In [10]: pd.read_parquet("timeseries_wide.parquet", columns=columns)
Out[10]: 
                     id_0 name_0       x_0       y_0
timestamp                                           
2000-01-01 00:00:00   977  Alice -0.821225  0.906222
2000-01-01 00:01:00  1018    Bob -0.219182  0.350855
2000-01-01 00:02:00   927  Alice  0.660908 -0.798511
2000-01-01 00:03:00   997    Bob -0.852458  0.735260
2000-01-01 00:04:00   965    Bob  0.717283  0.393391
...                   ...    ...       ...       ...
2000-12-30 23:56:00  1037    Bob -0.814321  0.612836
2000-12-30 23:57:00   980    Bob  0.232195 -0.618828
2000-12-30 23:58:00   965  Alice -0.231131  0.026310
2000-12-30 23:59:00   984  Alice  0.942819  0.853128
2000-12-31 00:00:00  1003  Alice  0.201125 -0.136655

[525601 rows x 4 columns]

Если мы измерим использование памяти для этих двух вызовов, мы увидим, что указание columns в данном случае использует примерно в 10 раз меньше памяти.

С помощью pandas.read_csv() вы можете указать usecols для ограничения столбцов, считываемых в оперативную память. Не все форматы файлов, которые могут быть прочитаны pandas, предоставляют возможность считывания подмножества столбцов.

Использование эффективных типов данных

По умолчанию типы данных pandas не являются наиболее эффективными с точки зрения использования памяти. Это особенно верно для столбцов текстовых данных с относительно небольшим количеством уникальных значений (часто называемых данными «низкой кардинальности»). Использование более эффективных типов данных позволяет хранить большие наборы данных в оперативной памяти.

In [11]: ts = make_timeseries(freq="30s", seed=0)

In [12]: ts.to_parquet("timeseries.parquet")

In [13]: ts = pd.read_parquet("timeseries.parquet")

In [14]: ts
Out[14]: 
                       id     name         x         y
timestamp                                             
2000-01-01 00:00:00  1041    Alice  0.889987  0.281011
2000-01-01 00:00:30   988      Bob -0.455299  0.488153
2000-01-01 00:01:00  1018    Alice  0.096061  0.580473
2000-01-01 00:01:30   992      Bob  0.142482  0.041665
2000-01-01 00:02:00   960      Bob -0.036235  0.802159
...                   ...      ...       ...       ...
2000-12-30 23:58:00  1022    Alice  0.266191  0.875579
2000-12-30 23:58:30   974    Alice -0.009826  0.413686
2000-12-30 23:59:00  1028  Charlie  0.307108 -0.656789
2000-12-30 23:59:30  1002    Alice  0.202602  0.541335
2000-12-31 00:00:00   987    Alice  0.200832  0.615972

[1051201 rows x 4 columns]

Теперь давайте рассмотрим типы данных и использование памяти, чтобы понять, на что следует обратить внимание.

In [15]: ts.dtypes
Out[15]: 
id        int64
name     object
x       float64
y       float64
dtype: object
In [16]: ts.memory_usage(deep=True)  # memory usage in bytes
Out[16]: 
Index     8409608
id        8409608
name     65176434
x         8409608
y         8409608
dtype: int64

Столбец name занимает гораздо больше памяти, чем любой другой. Он содержит лишь несколько уникальных значений, поэтому он является хорошим кандидатом для преобразования в pandas.Categorical. С pandas.Categorical мы храним каждое уникальное имя один раз и используем эффективные целые числа, чтобы знать, какое именно имя используется в каждой строке.

In [17]: ts2 = ts.copy()

In [18]: ts2["name"] = ts2["name"].astype("category")

In [19]: ts2.memory_usage(deep=True)
Out[19]: 
Index    8409608
id       8409608
name     1051495
x        8409608
y        8409608
dtype: int64

Мы можем немного продвинуться и понизить точность числовых столбцов до их минимальных типов, используя pandas.to_numeric().

In [20]: ts2["id"] = pd.to_numeric(ts2["id"], downcast="unsigned")

In [21]: ts2[["x", "y"]] = ts2[["x", "y"]].apply(pd.to_numeric, downcast="float")

In [22]: ts2.dtypes
Out[22]: 
id        uint16
name    category
x        float32
y        float32
dtype: object
In [23]: ts2.memory_usage(deep=True)
Out[23]: 
Index    8409608
id       2102402
name     1051495
x        4204804
y        4204804
dtype: int64
In [24]: reduction = ts2.memory_usage(deep=True).sum() / ts.memory_usage(deep=True).sum()

In [25]: print(f"{reduction:0.2f}")
0.20

В итоге мы сократили занимаемое этой областью данных место в оперативной памяти до 1/5 от первоначального значения.

См. Данные категорий для получения дополнительной информации о pandas.Categorical и типы для общего обзора всех типов данных pandas.

Использование чанков

Некоторые рабочие нагрузки можно выполнить с помощью чанков, разбивая большую задачу на множество мелких. Например, преобразование отдельных файлов CSV в Parquet-файлы и повторение этого действия для каждого файла в каталоге. Пока каждый чанк помещается в оперативную память, вы можете работать с наборами данных, значительно превышающими объем оперативной памяти.

Примечание

Чанки хорошо работают, когда операция, которую вы выполняете, требует нулевого или минимального взаимодействия между чанками. Для более сложных рабочих процессов лучше использовать другие библиотеки.

Предположим, что у нас есть еще больший «логический набор данных» на диске, который представляет собой каталог Parquet-файлов. Каждый файл в каталоге представляет отдельный год всего набора данных.

In [26]: import pathlib

In [27]: N = 12

In [28]: starts = [f"20{i:>02d}-01-01" for i in range(N)]

In [29]: ends = [f"20{i:>02d}-12-13" for i in range(N)]

In [30]: pathlib.Path("data/timeseries").mkdir(exist_ok=True)

In [31]: for i, (start, end) in enumerate(zip(starts, ends)):
   ....:     ts = make_timeseries(start=start, end=end, freq="1min", seed=i)
   ....:     ts.to_parquet(f"data/timeseries/ts-{i:0>2d}.parquet")
   ....: 
data
└── timeseries
    ├── ts-00.parquet
    ├── ts-01.parquet
    ├── ts-02.parquet
    ├── ts-03.parquet
    ├── ts-04.parquet
    ├── ts-05.parquet
    ├── ts-06.parquet
    ├── ts-07.parquet
    ├── ts-08.parquet
    ├── ts-09.parquet
    ├── ts-10.parquet
    └── ts-11.parquet

Теперь мы реализуем внеоператорную pandas.Series.value_counts(). Максимальное использование памяти в этом рабочем процессе равно самому большому чанку плюс небольшой ряд, хранящий подсчеты уникальных значений до этого момента. Пока каждый отдельный файл помещается в оперативную память, это будет работать с наборами данных произвольного размера.

In [32]: %%time
   ....: files = pathlib.Path("data/timeseries/").glob("ts*.parquet")
   ....: counts = pd.Series(dtype=int)
   ....: for path in files:
   ....:     df = pd.read_parquet(path)
   ....:     counts = counts.add(df["name"].value_counts(), fill_value=0)
   ....: counts.astype(int)
   ....: 
CPU times: user 760 ms, sys: 26.1 ms, total: 786 ms
Wall time: 559 ms
Out[32]: 
name
Alice      1994645
Bob        1993692
Charlie    1994875
dtype: int64

Некоторые читатели, например pandas.read_csv(), предлагают параметры для управления chunksize при чтении одного файла.

Ручное чанкирование — это приемлемый вариант для рабочих процессов, которые не требуют слишком сложных операций. Некоторые операции, такие как pandas.DataFrame.groupby(), намного сложнее выполнять в режиме чанков. В таких случаях вам может быть лучше переключиться на другую библиотеку, которая реализует эти внеоператорные алгоритмы за вас.

Использование других библиотек

Существуют другие библиотеки, которые предоставляют аналогичные API pandas и хорошо взаимодействуют с pandas DataFrame, а также могут предоставить вам возможность масштабирования обработки и анализа больших наборов данных с помощью параллельного выполнения, распределенной памяти, кластеризации и т. д. Дополнительную информацию можно найти на странице экосистемы экосистемы.

© 2008–2022, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/2.2.2/user_guide/scale.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API