Как объединить два полярных фрейма данных с несколькими типами условий? (Равенства и сравнения)

Я пытаюсь преобразовать SQL-запрос в полярный код и застрял на одной строке запроса, которая в основном объединяет две таблицы с определенными условиями. Вот пример SQL-запроса, над которым я работал:

SELECT * 
FROM table_1 as tab1
LEFT JOIN table_2 as tab2
ON tab1.article_no = tab2.article_no -- condition 1
   AND DIV(tab1.variant_no, 1000) = tab2.variant_no -- condition 2
   AND tab1.date_of_day BETWEEN tab2.date_from AND tab2.date_to -- condition 3

Итак, при выполнении LEFT JOIN есть три условия, два из которых являются проверками на равенство, а одно проверяет, находится ли date_of_day между двумя другими столбцами дат из table_2.

В полярах, когда вам нужно соединить два фрейма данных/ленивых фреймов, вы должны использовать либо tab_1.join(tab_2, on=[certain_column]), либо tab_1.join(tab_2, left_on=[certain_column], right_on=[certain_column]).

Мы не можем использовать on= и [left_on, right_on] вместе, но в данном случае я не могу поместить все три условия ни в on=, ни в left_on, right_on, поэтому я не уверен, как мне этого добиться.

Хотя я пробовал делать сравнение дат отдельно после объединения с первыми двумя условиями:

table_1 = pl.read_csv('table_1.csv')
table_2 = pl.read_csv('table_2.csv')

table_joined = (
    table_1
    .join(
        table_2, 
        how='left', 
        left_on=[pl.col('article_no', pl.col('variant_no')//1000)],  #Giving first two conditions in join
        right_on=[pl.col('article_no'), pl.col('variant_no')], 
        suffix = "_tab2"
    )
)

columns_to_check = ['price_1', 'price_2', 'price_3']

final_df = (
    table_joined
    .with_columns(
        [
            pl.when(
                (pl.col('date_of_day').ge(pl.col('date_from'))) &   # giving the third condition after joining, and converting the other columns to None since it's a left join
                (pl.col('date_of_day').le(pl.col('date_to')))
            ).then(pl.col(col)).otherwise(None).alias(col)
            for col in columns_to_check   # using for loop to convert other columns to None (Null)
        ]
    )
)

Хотя этот метод работает нормально, он требует много памяти при работе с миллионами строк, а также это не очень последовательный способ соединения (потому что иногда в конечном кадре данных бывает больше строк, чем ожидалось). Я пробовал искать в Интернете способы задать несколько типов условий, но не нашел таких примеров.

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

Может кто-нибудь помочь мне здесь. Заранее спасибо.

Редактировать -

Ниже приведен пример данных и кода:

import polars as pl
from datetime import date

table_1_data = {
    "article_no": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
    "variant_no": [5000, 6000, 7000, 8000, 9000, 6000, 4000, 6000],
    "date_of_day": [date(2023, 1, 2), date(2023, 1, 12), date(2023, 1, 15), date(2023, 2, 5), date(2023, 3, 15), date(2023, 1, 2), date(2023, 1, 12), date(2023, 1, 15)],
    "quantity": [10, 15, 20, 25, 30, 45, 50, 60]
}

table_1 = pl.DataFrame(table_1_data)

table_2_data = {
    "article_no": [1001, 1002, 1003, 1004, 1006],
    "variant_no": [5, 6, 7, 8, 9],
    "date_from": [date(2023, 1, 1), date(2023, 1, 10), date(2023, 1, 1), date(2023, 1, 1), date(2023, 3, 1)],
    "date_to": [date(2023, 2, 1), date(2023, 1, 15), date(2023, 1, 10), date(2023, 1, 15), date(2023, 3, 31)],
    "price": [100, 110, 120, 130, 140]
}

table_2 = pl.DataFrame(table_2_data)

# Print sample data
print("Table 1:")
print(table_1)
print("\nTable 2:")
print(table_2)

joined_table = (
    table_1
    .join(table_2, 
          how='left', 
          left_on=[pl.col('article_no'), pl.col('variant_no')//1000],
          right_on=[pl.col('article_no'), pl.col('variant_no')],
          suffix='_tab2'
    )
    .select('article_no', 'variant_no', 'date_of_day', 'quantity', 'price')
)

print('\nJoined Table:')
print(joined_table)

exptected = {
    "article_no": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
    "variant_no": [5000, 6000, 7000, 8000, 9000, 6000, 4000, 6000],
    "date_of_day": [date(2023, 1, 2), date(2023, 1, 12), date(2023, 1, 15), date(2023, 2, 5), date(2023, 3, 15), date(2023, 1, 2), date(2023, 1, 12), date(2023, 1, 15)],
    "quantity": [10, 15, 20, 25, 30, 45, 50, 60],
    "price": [100, 110, None, None, None, None, None, None]
}

exptected = pl.DataFrame(exptected)

print('\nExpected Output:')
print(exptected)

Вывод этого кода:

Table 1:
shape: (8, 4)
┌────────────┬────────────┬─────────────┬──────────┐
│ article_no ┆ variant_no ┆ date_of_day ┆ quantity │
│ ---        ┆ ---        ┆ ---         ┆ ---      │
│ i64        ┆ i64        ┆ date        ┆ i64      │
╞════════════╪════════════╪═════════════╪══════════╡
│ 1001       ┆ 5000       ┆ 2023-01-02  ┆ 10       │
│ 1002       ┆ 6000       ┆ 2023-01-12  ┆ 15       │
│ 1003       ┆ 7000       ┆ 2023-01-15  ┆ 20       │
│ 1004       ┆ 8000       ┆ 2023-02-05  ┆ 25       │
│ 1005       ┆ 9000       ┆ 2023-03-15  ┆ 30       │
│ 1006       ┆ 6000       ┆ 2023-01-02  ┆ 45       │
│ 1007       ┆ 4000       ┆ 2023-01-12  ┆ 50       │
│ 1008       ┆ 6000       ┆ 2023-01-15  ┆ 60       │
└────────────┴────────────┴─────────────┴──────────┘

Table 2:
shape: (5, 5)
┌────────────┬────────────┬────────────┬────────────┬───────┐
│ article_no ┆ variant_no ┆ date_from  ┆ date_to    ┆ price │
│ ---        ┆ ---        ┆ ---        ┆ ---        ┆ ---   │
│ i64        ┆ i64        ┆ date       ┆ date       ┆ i64   │
╞════════════╪════════════╪════════════╪════════════╪═══════╡
│ 1001       ┆ 5          ┆ 2023-01-01 ┆ 2023-02-01 ┆ 100   │
│ 1002       ┆ 6          ┆ 2023-01-10 ┆ 2023-01-15 ┆ 110   │
│ 1003       ┆ 7          ┆ 2023-01-01 ┆ 2023-01-10 ┆ 120   │
│ 1004       ┆ 8          ┆ 2023-01-01 ┆ 2023-01-15 ┆ 130   │
│ 1006       ┆ 9          ┆ 2023-03-01 ┆ 2023-03-31 ┆ 140   │
└────────────┴────────────┴────────────┴────────────┴───────┘

Joined Table:
shape: (8, 5)
┌────────────┬────────────┬─────────────┬──────────┬───────┐
│ article_no ┆ variant_no ┆ date_of_day ┆ quantity ┆ price │
│ ---        ┆ ---        ┆ ---         ┆ ---      ┆ ---   │
│ i64        ┆ i64        ┆ date        ┆ i64      ┆ i64   │
╞════════════╪════════════╪═════════════╪══════════╪═══════╡
│ 1001       ┆ 5000       ┆ 2023-01-02  ┆ 10       ┆ 100   │
│ 1002       ┆ 6000       ┆ 2023-01-12  ┆ 15       ┆ 110   │
│ 1003       ┆ 7000       ┆ 2023-01-15  ┆ 20       ┆ 120   │
│ 1004       ┆ 8000       ┆ 2023-02-05  ┆ 25       ┆ 130   │
│ 1005       ┆ 9000       ┆ 2023-03-15  ┆ 30       ┆ null  │
│ 1006       ┆ 6000       ┆ 2023-01-02  ┆ 45       ┆ null  │
│ 1007       ┆ 4000       ┆ 2023-01-12  ┆ 50       ┆ null  │
│ 1008       ┆ 6000       ┆ 2023-01-15  ┆ 60       ┆ null  │
└────────────┴────────────┴─────────────┴──────────┴───────┘

Expected Output:
shape: (8, 5)
┌────────────┬────────────┬─────────────┬──────────┬───────┐
│ article_no ┆ variant_no ┆ date_of_day ┆ quantity ┆ price │
│ ---        ┆ ---        ┆ ---         ┆ ---      ┆ ---   │
│ i64        ┆ i64        ┆ date        ┆ i64      ┆ i64   │
╞════════════╪════════════╪═════════════╪══════════╪═══════╡
│ 1001       ┆ 5000       ┆ 2023-01-02  ┆ 10       ┆ 100   │
│ 1002       ┆ 6000       ┆ 2023-01-12  ┆ 15       ┆ 110   │
│ 1003       ┆ 7000       ┆ 2023-01-15  ┆ 20       ┆ null  │
│ 1004       ┆ 8000       ┆ 2023-02-05  ┆ 25       ┆ null  │
│ 1005       ┆ 9000       ┆ 2023-03-15  ┆ 30       ┆ null  │
│ 1006       ┆ 6000       ┆ 2023-01-02  ┆ 45       ┆ null  │
│ 1007       ┆ 4000       ┆ 2023-01-12  ┆ 50       ┆ null  │
│ 1008       ┆ 6000       ┆ 2023-01-15  ┆ 60       ┆ null  │
└────────────┴────────────┴─────────────┴──────────┴───────┘


здесь в выводе строки номер 3 и 4 не удовлетворяют третьему условию, то есть date_from <= date_of_day <= date_to, поэтому столбцы из table_2 должны быть Null.

Можете ли вы опубликовать небольшие образцы CSV-файлов и ожидаемый результат? stackoverflow.com/help/minimal-reproducible-example

Dogbert 07.08.2024 09:31

Polars пока не поддерживает BETWEEN присоединения. github.com/pola-rs/polars/issues/10068

jqurious 07.08.2024 10:21

Привет @Dogbert, я отредактировал свой вопрос, указав пример кода, выходные данные и ожидаемый результат, спасибо.

Solomon Papathoti Leo 09.08.2024 09:01

Согласно обновлению: вы можете добавить обнуление после join, чтобы получить ожидаемый результат, например. pl.when(pl.col("date_of_day").is_between("date_from", "date_to")).then(pl.col("date_from", "date_to", "price")) — но по сути это то, что уже делает ваш первоначальный пример.

jqurious 09.08.2024 09:21

Пока Polars не добавит надлежащую поддержку неэквивалентного соединения, общая рекомендация состоит в том, чтобы использовать альтернативный инструмент, например,uckdb, который может легко конвертировать в/из фреймы данных Polars. Смотрите второй пример: stackoverflow.com/a/77885953

jqurious 09.08.2024 13:48

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

Solomon Papathoti Leo 09.08.2024 14:49
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
6
107
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В полярах у нас есть join_asof, который очень похож на левое соединение.

result = (
    table_1.lazy()
    .join(
        table_2.lazy(),
        how = "left",
        left_on = "article_no",
        right_on = "article_no"
    )
    .filter(
        (pl.col("variant_no") // 1000 == pl.col("variant_no_right")) &
        (pl.col("date_of_day") >= pl.col("date_from")) &
        (pl.col("date_of_day") <= pl.col("date_to"))
    )
    .with_columns([
        pl.when(pl.col("date_to").is_null())
        .then(None)
        .otherwise(pl.col("price"))
        .alias("price")
    ])
    .collect()
)

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

import polars as pl
from datetime import date


table_1_data = {
    "article_no": [1001, 1002, 1003, 1004, 1005],
    "variant_no": [5000, 6000, 7000, 8000, 9000],
    "date_of_day": [date(2023, 1, 2), date(2023, 1, 12), date(2023, 1, 15), date(2023, 2, 5), date(2023, 3, 15)],
    "quantity": [10, 15, 20, 25, 30]
}

table_1 = pl.DataFrame(table_1_data)

table_2_data = {
    "article_no": [1001, 1002, 1003, 1004, 1006],
    "variant_no": [5, 6, 7, 8, 9],
    "date_from": [date(2023, 1, 1), date(2023, 1, 10), date(2023, 1, 1), date(2023, 2, 1), date(2023, 3, 1)],
    "date_to": [date(2023, 2, 1), date(2023, 1, 15), date(2023, 2, 1), date(2023, 2, 15), date(2023, 3, 31)],
    "price": [100, 110, 120, 130, 140]
}

table_2 = pl.DataFrame(table_2_data)

# Print sample data
print("Table 1:")
print(table_1)
print("\nTable 2:")
print(table_2)

result = (
    table_1.lazy()
    .join(
        table_2.lazy(),
        how = "left",
        left_on = "article_no",
        right_on = "article_no"
    )
    .filter(
        (pl.col("variant_no") // 1000 == pl.col("variant_no_right")) &
        (pl.col("date_of_day") >= pl.col("date_from")) &
        (pl.col("date_of_day") <= pl.col("date_to"))
    )
    .with_columns([
        pl.when(pl.col("date_to").is_null())
        .then(None)
        .otherwise(pl.col("price"))
        .alias("price")
    ])
    .collect()
)
print("\nJoin Result:")
print(result)

Обновлен код в соответствии с обновленным ожидаемым результатом в вопросе,

joined_table = (
    table_1
    .join(
        table_2,
        how='left',
        left_on=[pl.col('article_no'), pl.col('variant_no')//1000],
        right_on=[pl.col('article_no'), pl.col('variant_no')]
    )
    .with_columns([
        pl.when(
            (pl.col('date_of_day') >= pl.col('date_from')) &
            (pl.col('date_of_day') <= pl.col('date_to'))
        ).then(pl.col('price')).otherwise(None).alias('price')
    ])
    .select('article_no', 'variant_no', 'date_of_day', 'quantity', 'price')
)

Поправьте меня, если я ошибаюсь, но если мы будем использовать .filter(), то по сути будут удалены строки, которые не соответствуют условиям, вместо того, чтобы обнулить их. Но если .join_asof() сохраняет строки, не соответствующие условиям, то стоит попробовать.

Solomon Papathoti Leo 07.08.2024 11:06

Я мог видеть 3 условия в вашем запросе sql: левое соединение больше похоже на внутреннее соединение, если оно удовлетворяет всем трем условиям. не могли бы вы поделиться примером входных данных и желаемым результатом?

No Name 07.08.2024 11:50

Это сработало? какое-нибудь обновление?

No Name 07.08.2024 12:52

Спасибо за это, я также отредактировал свой вопрос, указав пример кода, выходные данные и ожидаемый результат. пожалуйста, проверьте

Solomon Papathoti Leo 09.08.2024 09:02

Обновил мой ответ, проверьте и примите, если хотите.

No Name 09.08.2024 18:12

Спасибо, это сработало. Поскольку мне нужно было обнулить несколько столбцов, а не только один столбец цен, я использовал .then(pl.col('price_1', 'price_2', 'price_3').otherwise(None)

Solomon Papathoti Leo 12.08.2024 08:17

Другие вопросы по теме