Код: Выделить всё
df = pl.DataFrame({
"letters": ["A", "B", "C", "D", "E", "F", "G", "H"],
"values": ["aa", "bb", "cc", "dd", "ee", "ff", "gg", "hh"]
})
print(df)
shape: (8, 2)
┌─────────┬────────┐
│ letters ┆ values │
│ --- ┆ --- │
│ str ┆ str │
╞═════════╪════════╡
│ A ┆ aa │
│ B ┆ bb │
│ C ┆ cc │
│ D ┆ dd │
│ E ┆ ee │
│ F ┆ ff │
│ G ┆ gg │
│ H ┆ hh │
└─────────┴────────┘
Код: Выделить всё
┌─────────┬────────────────────────────────┐
│ letters ┆ output │
│ --- ┆ --- │
│ str ┆ list[str] │
╞═════════╪════════════════════════════════╡
│ D ┆ ["bb", "cc", "dd", "ee", "ff"] │
│ F ┆ ["dd", "ee", "ff", "gg", "hh"] │
└─────────┴────────────────────────────────┘
РЕДАКТИРОВАТЬ: Чтобы сделать вопрос более явным, вот запрос в синтаксисе SQL DuckDB, который дает правильный ответ (и я хотел бы знать, как перевести его в Polars):
Код: Выделить всё
df_table = df.to_arrow()
con = duckdb.connect()
query = """
SELECT
letters,
list(values) OVER (
ROWS BETWEEN 2 PRECEDING
AND 2 FOLLOWING
) as combined
FROM df_table
QUALIFY letters in ('D', 'F')
"""
print(pl.from_arrow(con.execute(query).arrow()))
shape: (2, 2)
┌─────────┬────────────────────────┐
│ letters ┆ combined │
│ --- ┆ --- │
│ str ┆ list[str] │
╞═════════╪════════════════════════╡
│ D ┆ ["bb", "cc", ... "ff"] │
│ F ┆ ["dd", "ee", ... "hh"] │
└─────────┴────────────────────────┘
Я запустил предлагаемые решения в блокноте Jupyter на одном из компьютеров Amazon ml.c5.xlarge. Пока ноутбук работал, я также держал htop открытым в терминале, чтобы наблюдать за использованием процессора и памяти. В наборе данных было более 12 миллионов строк.
Я запускал оба решения с помощью как активного, так и ленивого API. На всякий случай я также попробовал использовать простой цикл for Python для извлечения срезов после идентификации интересующих строк, а также DuckDB.
Сводная таблица
Polars продемонстрировал действительно высокую производительность и разумное использование памяти (с помощью метода @jqurious') благодаря умной реализации .shift() без копирования. Удивительно, но хорошо продуманный цикл for в Python справился с такой же задачей. DuckDB показала довольно плохие результаты как по скорости, так и по использованию памяти.
Ни Polars, ни DuckDB не используют для своей работы более одного ядра. Не уверен, связано ли это с отсутствием оптимизации или эта проблема просто поддается распараллеливанию. Я полагаю, что мы фильтруем только один столбец, а затем берем фрагменты этого же столбца, поэтому несколько потоков не могут сделать многого.
метод
использование процессора
использование памяти
время
ΩΠΟΚΕΚΡΥΜΜΕΝΟΣ
одноядерный
взрыв
jqurious
одноядерный
2,53–2,53 ГБ
4,63 с
(smart) для цикла
одноядерный
2,53G до 2,58 ГБ
4,91 с
DuckDB
одноядерный
от 1,62 ГБ до 6,13 ГБ
38,6 с
- Использование процессора показывает, были ли задействованы несколько ядер во время операции.
- Использование памяти показывает, сколько памяти использовалось до операции, а также максимальное использование памяти во время операции.
Код: Выделить всё
preceding = 2
following = 2
look_around = [pl.col("body").shift(-i)
for i in range(-preceding, following + 1)]
(
df
.with_columns(
pl.when(pl.col('body').str.contains(regex))
.then(pl.concat_list(look_around))
.alias('combined')
)
.filter(pl.col('combined').is_not_null())
)
Решение @jqurious
Код: Выделить всё
preceding = 2
following = 2
look_around = [
pl.col("body").shift(-i).alias(f"lag_{i}") for i in range(-preceding, following + 1)
]
(
df
.with_columns(
look_around
)
.filter(pl.col("body").str.contains(regex))
.select(
pl.col("body"),
pl.concat_list([f"lag_{i}" for i in range(-2, 3)]).alias("output")
)
)
- готовность:
использование процессора: одноядерное - использование памяти: 2,53 ГБ -> 2,53 ГБ
- время: 4,63 с ± 6,6 мс на цикл (среднее значение ± стандартное dev. из 7 запусков, по 1 циклу каждый)
- использование процессора: одноядерное
- использование памяти: 2,53 ГБ -> 2,53 ГБ
- время: 4,63 с ± 3,85 мс на цикл (среднее ± стандартное отклонение для 7 запусков, по 1 циклу каждый)
(Smart) Python для цикла
Код: Выделить всё
preceding = 2
following = 2
output = []
indices = df.with_row_index().select(
pl.col("index").filter(pl.col("body").str.contains(regex))
)["index"]
for idx, x in enumerate(indices):
offset = max(0, x - preceding)
length = preceding + following + 1
output.append(df["body"].slice(offset, length))
- Использование процессора: одноядерное
- Использование памяти: 2,53 ГБ -> 2,58 ГБ
- время: 4,91 с ± 24,5 мс на цикл (среднее ± стандартное отклонение для 7 запусков по 1 циклу каждый)
Обратите внимание, что я сначала преобразовал df в Arrow.Table, прежде чем запускать запрос, чтобы DuckDB мог напрямую воздействовать на него. Кроме того, я не уверен, что преобразование результата обратно в Arrow требует огромного количества вычислений и несправедливо по отношению к нему.
Код: Выделить всё
preceding = 2
following = 2
query = f"""
SELECT
body,
list(body) OVER (
ROWS BETWEEN {preceding} PRECEDING
AND {following} FOLLOWING
) as combined
FROM df_table
QUALIFY regexp_matches(body, '{regex}')
"""
result = con.execute(query).arrow()
- первая попытка:
процессор: одноядерный - память: 2,53 ГБ -> 6,93 ГБ -> сбой!
- время: нет
- процессор: одноядерный
- память: 1,62 ГБ -> 6,13 ГБ
- время: 38,6 с ± 311 мс на цикл (среднее ± стандартное отклонение для 7 циклов, по 1 циклу каждый)
Подробнее здесь: https://stackoverflow.com/questions/745 ... -condition
Мобильная версия