Найдите все различия между группами в фрейме данных Polars

У меня есть один фрейм данных Polars, и я пытаюсь найти различия (поля, значения которых изменились) в нескольких столбцах между группами по одному ключу. В кадре данных может быть несколько групп и более одного столбца. Группы по сути представляют собой дату и время в формате int (ГГГГММДД).

Как я могу найти строки, в которых есть новое значение (любого) столбца в дату?

Образец данных:

raw_df = pl.DataFrame([
    {'id': 'AAPL','update_time': 20241112,'status':'trading', 'underlying': 'y'},
    {'id': 'MSFT','update_time': 20241113,'status': 'trading', 'underlying': 'x'},
    {'id': 'NVDA','update_time': 20241112,'status': 'trading', 'underlying': 'z'},
    {'id': 'MSFT','update_time': 20241112,'status': 'pending','underlying': 'x'},
    {'id': 'AAPL','update_time': 20241113,'status': 'trading', 'underlying': 'y'},
    {'id': 'NVDA','update_time': 20241113,'status': 'trading', 'underlying': 'z'},
    {'id': 'TSLA','update_time': 20241112,'status': 'closed', 'underlying': 'v'},
    ]
)

expected_df = pl.DataFrame([
    {'id': 'MSFT','update_time': 20241112,'status':'pending', 'underlying': 'x'},
    {'id': 'MSFT','update_time': 20241113,'status': 'trading', 'underlying': 'x'},
    ]
)

Ниже показано, как выглядит ввод данных.

shape: (7, 4)
┌──────┬─────────────┬─────────┬────────────┐
│ id   ┆ update_time ┆ status  ┆ underlying │
│ ---  ┆ ---         ┆ ---     ┆ ---        │
│ str  ┆ i64         ┆ str     ┆ str        │
╞══════╪═════════════╪═════════╪════════════╡
│ AAPL ┆ 20241112    ┆ trading ┆ y          │
│ MSFT ┆ 20241113    ┆ trading ┆ x          │
│ NVDA ┆ 20241112    ┆ trading ┆ z          │
│ MSFT ┆ 20241112    ┆ pending ┆ x          │
│ AAPL ┆ 20241113    ┆ trading ┆ y          │
│ NVDA ┆ 20241113    ┆ trading ┆ z          │
│ TSLA ┆ 20241112    ┆ closed  ┆ v          │
└──────┴─────────────┴─────────┴────────────┘

И ожидаемый результат ниже, показывающий идентификатор, время обновления и поле, которое изменилось. Если изменено/обновлено более одного поля, в идеале оно должно перейти в новую строку.

Я пытаюсь найти измененные поля, сгруппированные по «update_time» по ключу «id». Единственное предостережение: могут быть идентификаторы, которые присутствуют в группе, но не присутствуют в другой группе, например «TSLA». Следовательно, эти идентификаторы, которые не являются общими или пересекаются между группами, можно игнорировать. Поскольку статус изменился только у MSFT, его следует отфильтровать по тем двум строкам, в которых он был обновлен. Изменения полей следует выполнять только для всех остальных столбцов, кроме update_time, который мы используем для группировки.

shape: (2, 3)
┌──────┬─────────────┬─────────┐
│ id   ┆ update_time ┆ status  │
│ ---  ┆ ---         ┆ ---     │
│ str  ┆ i64         ┆ str     │
╞══════╪═════════════╪═════════╡
│ MSFT ┆ 20241112    ┆ pending │
│ MSFT ┆ 20241113    ┆ trading │
└──────┴─────────────┴─────────┴

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

def find_updated_field_differences(df):
        columns_to_check = [col for col in df.columns if col != 'id' and col != 'update_time']

        sorted_df = df.sort('update_time')
        grouped_df = sorted_df.groupby(["update_time"])
        
        result_data = []
        
        for group_key, group_df in grouped_df:
            print(group_df)
    
            for col in columns_to_check:

                group_df = group_df.with_columns(
                    (pl.col(col) != pl.col(col).shift()).alias(f"{col}_changed")
                )
            
            differing_rows = group_df.filter(
                pl.any([pl.col(f"{col}_changed") for col in columns_to_check])
            )


            result_data.append(differing_rows)
        
        differing_df = pl.concat(result_data)
        
        differing_df = differing_df.sort("id")
        
        return differing_df

Для меня вопрос не совсем ясен. Каков ожидаемый результат, если столбец меняется между двумя значениями (один и тот же идентификатор, разные даты)? Ожидаем ли мы, что будут сохранены все строки или только две строки с двумя уникальными значениями?

Hericks 16.04.2024 10:29

@Hericks Спасибо, что заметили это, следует сравнивать каждые две группы. Сравнивая с предыдущей датой. Следовательно, можно ожидать появления новой строки в MSFT 20241114 и в ожидании (если эти два изменения являются изменениями). Следует сохранять только те строки, в которых имеются уникальные обновления/изменения. еще раз спасибо

MantleMan 16.04.2024 10:54
Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
2
166
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

Ваш подход немного усложняет ситуацию. Я предлагаю сначала отсортировать данные по id и update_time, а затем сдвинуть данные для подготовки к сравнению. После этого вы сможете определить строки, в которых id одинаковый, но есть разница:

import polars as pl

def find_updated_field_differences(df):
    sorted_df = df.sort(['id', 'update_time'])
    
    shifted_df = sorted_df.shift(-1)

    mask = (
        (sorted_df['id'] == shifted_df['id']) &
        ((sorted_df['status'] != shifted_df['status']) | (sorted_df['underlying'] != shifted_df['underlying']))
    )

    start_changes = sorted_df.filter(mask)
    end_changes = sorted_df.shift(-1).filter(mask)
    differing_df = pl.concat([start_changes, end_changes]).unique()

    return differing_df.sort(['id', 'update_time'])

raw_df = pl.DataFrame([
    {'id': 'AAPL', 'update_time': 20241112, 'status': 'trading', 'underlying': 'y'},
    {'id': 'MSFT', 'update_time': 20241113, 'status': 'trading', 'underlying': 'x'},
    {'id': 'NVDA', 'update_time': 20241112, 'status': 'trading', 'underlying': 'z'},
    {'id': 'MSFT', 'update_time': 20241112, 'status': 'pending', 'underlying': 'x'},
    {'id': 'AAPL', 'update_time': 20241113, 'status': 'trading', 'underlying': 'y'},
    {'id': 'NVDA', 'update_time': 20241113, 'status': 'trading', 'underlying': 'z'},
    {'id': 'TSLA', 'update_time': 20241112, 'status': 'closed', 'underlying': 'v'},
])

result_df = find_updated_field_differences(raw_df)
print(result_df)

что дает вам

shape: (2, 4)
┌──────┬─────────────┬─────────┬────────────┐
│ id   ┆ update_time ┆ status  ┆ underlying │
│ ---  ┆ ---         ┆ ---     ┆ ---        │
│ str  ┆ i64         ┆ str     ┆ str        │
╞══════╪═════════════╪═════════╪════════════╡
│ MSFT ┆ 20241112    ┆ pending ┆ x          │
│ MSFT ┆ 20241113    ┆ trading ┆ x          │

В общем, похоже, что вы хотите сохранить все уникальные строки (игнорируя update_time) в группе, определенной id, если в соответствующей группе есть хотя бы две уникальные строки (т. е. произошло изменение).

Этого можно достичь следующим образом.

(
    raw_df
    .sort("update_time")
    .unique(subset=["id", "status", "underlying"], keep = "first", maintain_order=True)
    .filter(
        pl.len().over("id") > 1
    )
)
shape: (2, 4)
┌──────┬─────────────┬─────────┬────────────┐
│ id   ┆ update_time ┆ status  ┆ underlying │
│ ---  ┆ ---         ┆ ---     ┆ ---        │
│ str  ┆ i64         ┆ str     ┆ str        │
╞══════╪═════════════╪═════════╪════════════╡
│ MSFT ┆ 20241112    ┆ pending ┆ x          │
│ MSFT ┆ 20241113    ┆ trading ┆ x          │
└──────┴─────────────┴─────────┴────────────┘
  • pl.DataFrame.unique удаляет повторяющиеся строки. Параметр subset указывает, какие столбцы следует учитывать при сравнении двух строк как возможные дубликаты.
  • pl.len().over("id") подсчитывает количество уникальных строк в каждой группе, определенной id.

Сортировка по update_time и установка параметров keep = "first", maintain_order=True при вызове pl.DataFrame.unique не являются обязательными. Я использую его, чтобы гарантировать сохранение первого (во времени) появления каждой уникальной строки (для каждой группы, определенной id).

В Polars есть специальная функция, упрощающая проверку col != col.shift().

Кодирование длины серии: .rle_id()

df.with_columns(
   pl.exclude("id", "update_time").rle_id().over("id").name.suffix("_change")
)
shape: (7, 6)
┌──────┬─────────────┬─────────┬────────────┬───────────────┬───────────────────┐
│ id   ┆ update_time ┆ status  ┆ underlying ┆ status_change ┆ underlying_change │
│ ---  ┆ ---         ┆ ---     ┆ ---        ┆ ---           ┆ ---               │
│ str  ┆ i64         ┆ str     ┆ str        ┆ u32           ┆ u32               │
╞══════╪═════════════╪═════════╪════════════╪═══════════════╪═══════════════════╡
│ AAPL ┆ 20241112    ┆ trading ┆ y          ┆ 0             ┆ 0                 │
│ MSFT ┆ 20241113    ┆ trading ┆ x          ┆ 0             ┆ 0                 │
│ NVDA ┆ 20241112    ┆ trading ┆ z          ┆ 0             ┆ 0                 │
│ MSFT ┆ 20241112    ┆ pending ┆ x          ┆ 1             ┆ 0                 │
│ AAPL ┆ 20241113    ┆ trading ┆ y          ┆ 0             ┆ 0                 │
│ NVDA ┆ 20241113    ┆ trading ┆ z          ┆ 0             ┆ 0                 │
│ TSLA ┆ 20241112    ┆ closed  ┆ v          ┆ 0             ┆ 0                 │
└──────┴─────────────┴─────────┴────────────┴───────────────┴───────────────────┘

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

то есть rle_id > 0

df.filter(
   pl.any_horizontal(
      (pl.exclude("id", "update_time").rle_id() > 0).any().over("id")
   ) 
)
shape: (2, 4)
┌──────┬─────────────┬─────────┬────────────┐
│ id   ┆ update_time ┆ status  ┆ underlying │
│ ---  ┆ ---         ┆ ---     ┆ ---        │
│ str  ┆ i64         ┆ str     ┆ str        │
╞══════╪═════════════╪═════════╪════════════╡
│ MSFT ┆ 20241113    ┆ trading ┆ x          │
│ MSFT ┆ 20241112    ┆ pending ┆ x          │
└──────┴─────────────┴─────────┴────────────┘

Спасибо, это самое элегантное решение. Но странно, что у меня возникает невоспроизводимая ошибка, которая показывает, что «поток <unnamed>» запаниковал из-за «неудавшегося утверждения: (left == right)» левая и правая части должны иметь одинаковую длину», D:\bld\polars_1690884875212_build_env\.cargo\git\ checkouts\a‌​rrow2-8a2ad61d972656‌​80\d5c78e7\src\compu‌​te\boolean_kleene.rs‌​:24:5 Похоже на внутреннюю ошибку, я не слишком знаком с Rust. Эта ошибка (или нет) иногда случается в одной и той же схеме, поэтому я озадачен. Тем не менее, это решение работает с упрощенными данными.

MantleMan 17.04.2024 06:04

Определенно внутренняя ошибка — посмотрим, смогу ли я воспроизвести ее на более крупном примере.

jqurious 17.04.2024 14:43

Мне не удалось воспроизвести 0.20.22-rc.1 — если вам удастся создать воспроизводимый пример, сообщите об ошибке. github.com/pola-rs/polars/issues

jqurious 17.04.2024 16:46

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

Как в каждом столбце фрейма данных узнать продолжительность существования каждого уникального значения в столбце?
Модель логистической регрессии со 100-процентной точностью
Как переименовывать столбцы в последовательном порядке, т.е. (1, 2, 3 и т.д.)
Почему pandas read_html автоматически удаляет десятичный разделитель?
Поиск шаблонов разрывов в строковых последовательностях значений и NA
Сохраните первый экземпляр столбца и удалите остальные, используя частичный текст в имени столбца
Как найти первую строку, соответствующую условиям маски для каждой группы?
Объедините два фрейма данных с масштабируемыми столбцами, разместив второй фрейм данных непосредственно под первым фреймом данных, не удаляя ключи
Применение strsplit() к data.frame приводит к неожиданному выводу
Более быстрый способ конвертировать результаты DuckDB в фрейм данных, который поддерживается потоком?