У меня есть таблица MySQL 5.7, в которой хранятся продукты. Столбцы узлов относятся к категориям и подкатегориям. Среди этих узлов только node_0 и node_4 гарантированно будут не NULL, тогда как остальные могут иметь или не иметь NULL. Эта таблица будет использоваться для обработки продуктов в пакетах на основе уникальных комбинаций значений их узлов (unique_cat). В каждой уникальной комбинации может быть от 1 до 50 товаров. Имена отдельных узлов не уникальны (т. е. node_i может иметь то же имя, что и node_k), но комбинации этих узлов уникальны.
Один воспроизводимый пример выглядит следующим образом:
CREATE TABLE products (
product_id int(11) NOT NULL AUTO_INCREMENT,
node_0 varchar(400) DEFAULT NULL,
node_1 varchar(400) DEFAULT NULL,
node_2 varchar(400) DEFAULT NULL,
node_3 varchar(400) DEFAULT NULL,
node_4 varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
);
INSERT INTO products (node_0, node_1, node_2, node_3, node_4) VALUES
('a_0', NULL, NULL, NULL, 'a_1'),
('a_0', NULL, NULL, NULL, 'a_1'),
('a_2', 'a_1', NULL, NULL, 'a_1'),
('a_0', NULL, NULL, 'a_3', 'a_2'),
('a_3', NULL, NULL, 'a_0', 'a_2'),
('a_0', NULL, NULL, NULL, 'a_2'),
('a_2', 'a_1', NULL, NULL, 'a_1')
Мне нужно создать новый столбец unique_cat, который будет числом, представляющим различные значения комбинации node_0, node_1, node_2, node_3 и node_4. Я не уверен, как обрабатывать значения NULL в этом контексте. Ожидаемый результат:
| node_0 | node_1 | node_2 | node_3 | node_4 | unique_cat |
|--------|--------|--------|--------|--------|------------|
| a_0 | NULL | NULL | NULL | a_1 | 0 |
| a_0 | NULL | NULL | NULL | a_1 | 0 |
| a_2 | a_1 | NULL | NULL | a_1 | 1 |
| a_0 | NULL | NULL | a_3 | a_2 | 2 |
| a_3 | NULL | NULL | a_0 | a_2 | 3 |
| a_0 | NULL | NULL | NULL | a_2 | 4 |
| a_2 | a_1 | NULL | NULL | a_1 | 1 |
При работе только с узлами node_0 и node_4 работает следующий подход:
# Create a unique combination of node_0,node_4 with unique IDs
unique_cat_df = df \
.select("node_0", "node_4") \
.distinct() \
.withColumn("unique_cat", monotonically_increasing_id())
# Join the unique combinations back to the original DataFrame
df_with_cat_ids = df.join(
unique_cat_df,
on=["node_0", "node_4"],
how = "left"
)
Однако когда я пытаюсь включить узлы, которые могут иметь значение NULL, это работает не так, как ожидалось. Вот что я пробовал:
placeholder = "___NULL___"
df = df_0 \
.withColumn("node_2", F.when(col("node_2").isNull(), placeholder).otherwise(col("node_2"))) \
.withColumn("node_3", F.when(col("node_3").isNull(), placeholder).otherwise(col("node_3")))
# Select columns and create distinct combinations with a unique ID
unique_combinations_df = df \
.select("node_0", "node_1", "node_2", "node_3", "node_4") \
.distinct() \
.withColumn("unique_cat", monotonically_increasing_id())
# Join the unique combinations back to the original DataFrame
df_with_ids = lastest_data_df_2.join(
unique_combinations_df,
on=["current_node", "node_1", "node_2", "node_3", "root_node"],
how = "left"
)
Этот подход не работает. Есть идеи или предложения? Спасибо!
Обновление: решено!
После долгих головоломок, перерывов на кофе и размышлений о смысле жизни я обнаружил причину, по которой мой код не работает должным образом:
🥁 барабанная дробь 🥁
Я забыл заменить значения NULL в node_1! 🚨
Как мне стыдно... 🙂
unique_cat — это просто число, которое классифицирует комбинации node_0,...., node_4. Создано с использованием отличающихся(node_0,....,node_4)
То есть это просто последовательный индекс, создаваемый для каждой новой комбинации?
Да, это именно так. Я не знаю, почему он возвращает unique_cat с NULL при попытке получить все узлы.
Я не знаю, как это сделать, но почти наверняка будет проще, если вы нормализуете данные вместо использования нескольких столбцов.
О, я только что понял, что забыл добавить node_1 в свой код! 😅 Кстати, планирую последовать вашему совету по нормализации. Как вы думаете, что-то подобное может сработать? Продукты: (product_id, Product_name), узлы: (node_id, Product_id, node_level, node_value) -> основной (node_id), внешний (product_id).
Пожалуйста, создайте ответ под этим сообщением, который подойдет вам и решит этот вопрос, и примите его, чтобы он также мог помочь другим.
Есть еще несколько прямых способов сделать это без использования placeholder
и distinct()
, но лучший метод будет зависеть от версии Spark. Какую версию Spark вы используете?
Я добавил ответ, чтобы продемонстрировать идею наличия выходного столбца, представляющего уникальные значения узлов. Ответ ниже включает эту концепцию, а также предоставляет вам запрошенный результат для столбца unique_cagetory.
df_array = df.withColumn("node_array", F.array(F.col("node_0"),
F.col("node_1"),
F.col("node_2"),
F.col("node_3"),
F.col("node_4"))
)\
.withColumn("node_array_clean", F.filter(F.col("node_array"), lambda x: x.isNotNull()))\
.withColumn("node_array_distinct", F.array_distinct(F.col("node_array_clean")))\
.withColumn("unique_node_values", F.array_sort(F.col("node_array_distinct")))
df_array.show(truncate=False)
Выход
Что это делает,
Порядок действий:
node_array
-> node_array_clean
-> node_array_distinct
-> unique_node_values
[a_2, a_1, null, null, a_1]
-> [a_2, a_1, a_1]
-> [a_2, a_1]
-> [a_1, a_2]
node_array
: использует F.array
для сбора значений для столбцов.node_array_clean
: использует F.filter
для удаления нулей из собранных значений.
node_array_distinct
: использует F.array_distinct
для удаления дубликатов.
unique_node_values
: использует F.array_sort
для упорядочивания значений в массиве.
Почему это полезно,
Столбец unique_node_values
представляет собой упорядоченный список уникальных значений для каждой строки. Это дает нам возможность теперь искать все строки, имеющие определенное значение, с помощью такого оператора, как:
df_array.select("product_id", "unique_node_values")\
.filter(F.array_contains(F.col("unique_node_values"), "a_1"))\
.show()
[a_0, a_1]
2
[a_0, a_1]
3
[a_1, a_2]
7
[a_1, a_2]
df_clean = df_array.drop("node_array")\
.drop("node_array_clean")\
.drop("node_array_distinct")
Другой вариант — вложить функции шага 1, чтобы создать только один столбец. В этом ответе я создал несколько столбцов, чтобы было легче следить за ним.
Определите оконную функцию, которая будет использоваться при ранжировании, мы можем упорядочить по ранее созданному unique_node_values
.
window_spec = Window.partitionBy().orderBy("unique_node_values")
Выбор между rank
и dense_rank
будет зависеть от вашего варианта использования финального unique_category
, ниже вы можете увидеть, как различаются их результаты.
Пример использования rank
df_ranked = df_clean.withColumn("unique_category", F.rank().over(window_spec))
df_ranked.show(truncate=False)
Пример использования dense_rank
df_ranked = df_clean.withColumn("unique_category", F.dense_rank().over(window_spec))
df_ranked.show(truncate=False)
Соображения
При использовании rank
, dense_rank
и monotonically_increasing_id
следует учитывать, что эти столбцы идентификаторов могут измениться, если входные данные различаются. Значение 1
может представлять собой комбинацию узлов [a_0, a_1]
с имеющимися у вас сегодня данными. Если данные изменяются, значение 1
может быть присвоено для представления другой комбинации узлов (например, [a_0]
).
Спасибо за Ваш ответ. Предположим, существует массив [a1, Null, Null, Null, a2]. Будет ли этот подход считать ту же комбинацию, что и [Null, a1, Null, Null, a2]? Мне нужно, чтобы это рассматривалось как другая комбинация. Можете ли вы дать мне подсказку?
При таком подходе они будут считаться одинаковыми. Если вы хотите, чтобы они считались разными, вы можете использовать столбец node_array из шага 2 шага 3, чтобы создать столбец идентификатора unique_category.
Я не понимаю, как вы рассчитываете
unique_cat
в своем примере. В строке 6 есть два значения:a_0
иa_2
, как получитьunique_cat = 4
?