У меня есть некоторые частично столбчатые данные, подобные этим:
"hello", "2024 JAN", "2024 FEB"
"a", 0, 1
Если бы он был чисто столбчатым, он выглядел бы так:
"hello", "year", "month", "value"
"a", 2024, "JAN", 0
"a", 2024, "FEB", 1
Предположим, что данные представлены в виде массива numpy, например:
import numpy as np
data = np.array([["hello", "2024 JAN", "2024 FEB"], ["a", "0", "1"]], dtype = "<U")
data
array([['hello', '2024 JAN', '2024 FEB'],
['a', '0', '1']], dtype='<U8')
Представьте также, что я создал таблицу:
import duckdb as ddb
conn = ddb.connect("hello.db")
conn.execute("CREATE TABLE columnar (hello VARCHAR, year UINTEGER, month VARCHAR, value VARCHAR);")
Как я могу эффективно вставить data
в таблицу DuckDB columnar
?
Наивным/простым способом было бы грубое преобразование данных в столбчатый формат в памяти в Python перед вставкой их в таблицу DuckDB.
Здесь я имею в виду конкретно:
import re
data_header = data[0]
data_proper = data[1:]
date_pattern = re.compile(r"(?P<year>[\d]+) (?P<month>JAN|FEB)")
common_labels: list[str] = []
known_years: set[int] = set()
known_months: set[str] = set()
header_to_date: Dict[str, tuple[int, str]] = dict()
for header in data_header:
if matches := date_pattern.match(header):
year, month = int(matches["year"]), str(matches["month"])
known_years.add(year)
known_months.add(month)
header_to_date[header] = (year, month)
else:
common_labels.append(header)
# hello, year, month, value
new_rows_per_old_row = len(known_years) * len(known_months)
new_headers = ["year", "month", "value"]
purely_columnar = np.empty(
(
1 + data_proper.shape[0] * new_rows_per_old_row,
len(common_labels) + len(new_headers),
),
dtype=np.object_,
)
purely_columnar[0] = common_labels + ["year", "month", "value"]
for rx, row in enumerate(data_proper):
common_data = []
ym_data = []
for header, element in zip(data_header, row):
if header in common_labels:
common_data.append(element)
else:
year, month = header_to_date[header]
ym_data.append([year, month, element])
for yx, year_month_value in enumerate(ym_data):
purely_columnar[
1 + rx * new_rows_per_old_row + yx, : len(common_labels)
] = common_data
purely_columnar[
1 + rx * new_rows_per_old_row + yx, len(common_labels) :
] = year_month_value
print(f"{purely_columnar=}")
purely_columnar=
array([[np.str_('hello'), 'year', 'month', 'value'],
[np.str_('a'), 2024, 'JAN', np.str_('0')],
[np.str_('a'), 2024, 'FEB', np.str_('1')]], dtype=object)
Теперь хранить эти данные в DuckDB достаточно легко:
purely_columnar_data = np.transpose(purely_columnar[1:])
conn.execute(
"""INSERT INTO columnar
SELECT * FROM purely_columnar_data
"""
)
conn.sql("SELECT * FROM columnar")
┌─────────┬────────┬─────────┬─────────┐
│ hello │ year │ month │ value │
│ varchar │ uint32 │ varchar │ varchar │
├─────────┼────────┼─────────┼─────────┤
│ a │ 2024 │ JAN │ 0 │
│ a │ 2024 │ FEB │ 1 │
└─────────┴────────┴─────────┴─────────┘
Но есть ли какой-либо другой способ, с помощью которого я могу вставить данные в DuckDB в чисто столбчатой форме, кроме предварительного перебора данных в чисто столбчатую форму?
Примечание. Я отметил этот вопрос postgresql
, потому что диалект SQL DuckDB очень похож на диалект PostgreSQL.
Возможно, будет полезно опубликовать код Python/pandas, который выполняет нужное вам преобразование. Это может помочь идентифицировать эквивалент SQL.
@jqurious Я новичок в SQL, поэтому не совсем понимаю ваш «простой способ». Я обновил вопрос, указав, по моему мнению, «простой способ» (грубое преобразование данных в Python в «чисто столбчатую форму»).
@IanGow Я обновил вопрос этим! Я не использую pandas
; просто Python и numpy
.
Если вы действительно начинаете с необработанного текста и используете регулярное выражение для извлечения определенных частей - это может быть «проблемой XY». Вы можете просто использовать read_text() для загрузки данных в DuckDB и использовать его собственные функции регулярных выражений вместо того, чтобы делать это в Python.
@bzm3r Круто. Кажется, вы получили ответ.
Примечание. Я добавил еще одну строку ("b",1,0
), чтобы сделать данные более информативными и было легче увидеть, что происходит.
По сути, у вас есть «повернутый» набор данных:
D SELECT * FROM 'pivoted-data.csv';
┌─────────┬──────────┬──────────┐
│ hello │ 2024 JAN │ 2024 FEB │
│ varchar │ int64 │ int64 │
├─────────┼──────────┼──────────┤
│ a │ 0 │ 1 │
│ b │ 1 │ 0 │
└─────────┴──────────┴──────────┘
Итак UNPIVOT это:
D SELECT hello, string_split(month, ' ')[1]::INTEGER AS year, string_split(month, ' ')[2] AS month, value
FROM (UNPIVOT 'pivoted-data.csv' ON '2024 JAN', '2024 FEB' INTO NAME month VALUE value);
┌─────────┬───────┬─────────┬───────┐
│ hello │ year │ month │ value │
│ varchar │ int32 │ varchar │ int64 │
├─────────┼───────┼─────────┼───────┤
│ a │ 2024 │ JAN │ 0 │
│ a │ 2024 │ FEB │ 1 │
│ b │ 2024 │ JAN │ 1 │
│ b │ 2024 │ FEB │ 0 │
└─────────┴───────┴─────────┴───────┘
После этого вы можете использовать одну из строковых функций для дальнейшего анализа month
.
Обновлено: у меня была проблема с разделением строки, это не самый эффективный способ, но я решил, что оптимизатор запросов, вероятно, видит дублирование кода и оптимизирует его.
EDIT2: Хорошо, EXPLAIN
подтверждает, что дублирование оптимизировано:
┌─────────────────────────────┐
│┌───────────────────────────┐│
││ Physical Plan ││
│└───────────────────────────┘│
└─────────────────────────────┘
┌───────────────────────────┐
│ PROJECTION │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ hello │
│ year │
│ month │
│ value │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ PROJECTION │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ hello │
│ string_split(month, ' ') │
│ value │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ PROJECTION │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ #0 │
│ #3 │
│ #4 │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ (value IS NOT NULL) │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ EC: 1 │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ UNNEST │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ READ_CSV_AUTO │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ hello │
│ 2024 JAN │
│ 2024 FEB │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ EC: 3 │
└───────────────────────────┘
Как EXPLAIN
подтверждает, что дублирование кода исчезло?
@bzm3r Посмотрите на вторую проекцию сверху, только одна string_split
, а в верхней осуществляется доступ только к элементам (по заданным именам).
Небольшой вариант ответа @suvayu:
SELECT hello, CAST(strptime(month, '%Y %b') AS DATE) AS month, value
FROM (
UNPIVOT 'pivoted-data.csv'
ON COLUMNS(* EXCLUDE (hello))
INTO NAME month VALUE value);
Два различия заключаются в следующем:
strptime()
для анализа даты.COLUMNS
, чтобы сделать запрос более динамичным (я предполагаю, что в реальном наборе данных больше дат).
Какой простой способ вы имеете в виду? Вы имеете в виду что-то вроде
first = data[: :1].T; rest = data[:, :1]; duckdb.sql("from first positional join rest")
- (на мгновение игнорируя разделение даты)