Pandas

and other cool things

 

Катаев Денис

AI Center - Chatbot CTO

Кто я

4 команды, 30 человек, ищем еще

Продукты:

  • Ассистент Олег
  • Бот поддержки

Активности:

  • Куча докладов, особенно про SQLAlchemy
  • Самая большая команда в ТЦР Екб

Effective pandas

Книга ->

 

Интервью с основными идеями

Типичный код на pandas

Нравится? Читаемо?

chaining

jQuery style lol

.pipe(func, args, kw, ...)

.assign(new=lambda df: ...)

  • Создание колонок без warning'ов copy что-то там
  • Можно chain'ить
  • Не мутирует данные!

  • Вообще мутация данных не очень

How NOT to deal with SettingWithCopyWarning in Pandas:

Еще больше

Я думаю у него цель в 1 строку всё сделать

Корректные типы

если в df.info() есть object, это в целом красный флаг

ну кроме что строк

 

Есть типы unsigned которые занимают в 2 раза меньше

Есть типы уменьшенной размерности uint8, float16 и т.д

.astype('category')

Всё просто

  1. Меньше типы
  2. меньше памяти
  3. быстрей операции

.query(...)

Фильтруем df по простым условиям

Порой удобно фильтровать в цепочке вызовов 

Внутри есть eval

kek = "asd"
df.query("lol = @kek")
df.query("lol = 'lol'")

Чуточку PHP-style правда

Синтаксис с @kek ломает читаемость, линтеры и автоматические рефакторинг

Выборка

через .loc[...]

.loc[lambda df: ...]

Минус еще один повод иметь ссылку на df{1,2,..._tmp}

Опасайтесь банкнот банка приколов

.loc[index, cols]

index? Что такое индекс?

Иерархичные индексы

MultiIndex в индексе или в колонках

 

Это не для новичков, но корректное использование индексов существенно ускоряет выборки

 

Если часто берете данные по каким-то полям,

почему бы не засунуть это в индекс?

Кандат для h.index

.loc[df.f1 == '..' & df.f2 == '@@']

.stack(...) & unstack(...)

Очень крутая штука

связанная с иерархическим индексом

.to_frame(...)

Превращает колонку в df

.to_frame(name='...')

Задаёт имя колонке в df для той серии

Пример

.groupby([...])['f1'].mean().to_frame('f1_mean')

df.join

Эффективно соединяет данные по индексу

 

df.merge более универсальный метод, может работать так же, но более многословен в некоторых ситуациях

pd.concat([df1, df2, ...])

Соединяет несколько df

есть аргумент axis позволяет соединять так и сяк

Пример где нужен pd.concat

ну и .pipe() если честно

.apply

Две большие разницы

df.apply(lambda df: ...)
df[...].apply(lambda val: ...)
  • В первом случае можно использовать векторизацию
  • Во втором тоже, но только если np.ufunc
    • lambda уже не прокатит

Series.apply

Пруф что используется векторизированный код 

Что тут не так?

  • берем колонку 'intent_id'
  • заменяем значения на UNK там где 'answer' == 'yes'
  • почему у колонки answer не bool тип
  • Series.where(cond, other, ...)
df.assign(
  true_intent_id=lambda df: df['intent_id'].where(df['asnwer'] != 'yes', UNK)
)

Как то так:

Метод полностью векторизирован

.apply vs .assign

  • Применяется к df и series давая разный результат
  • Возвращает серию которую еще куда то надо засунуть...
  • И этим разрывает channing
  • Легко сломать векторизацию
  • Применяется только к df 
  • Сам создает колонки
  • Можно использовать друг за другом, объединяя последовательно разные колонки!

Vectorization

Что это вообще?!

Одна из причин

почему питон медленный и почему быстрый

import pandas as pd
import numpy as np
import perfplot


def some_apply(x):
    return np.log(x)


out = perfplot.bench(
    setup=lambda n: pd.DataFrame(dict(kek=np.random.rand(n))),
    kernels=[
        lambda df: df['kek'].apply(some_apply),
        lambda df: df['kek'].apply(lambda x: np.log(x)),
        lambda df: df['kek'].apply(np.log),
    ],
    labels=[
        "series_apply_func",
        "series_apply_lambda",
        "series_apply_lambda_ufunc"
    ],
    n_range=[2 ** k for k in range(20)],
    xlabel="len(a)",
)
out.save("perf.png")

Векторизация

Короче говоря

  • Не применяйте .apply к колонкам
    • df.apply(..., axis=1) вообще superslow
  • Старайтесь не итерироваться по элементам
  • Когда применяете всё же .apply думайте к чему
    • к df значит в lambda придет df, а не col или row
    • на это вообще линтер надо написать!
  • преобразования колонки с текстом таким образом не ускорить, но это не повод лепить везде .apply(lambda ...)

Series.str.{...}

Векторизованные методы

LOL

https://github.com/pandas-dev/pandas/issues/35864

Что делать?

https://www.gresearch.co.uk/blog/article/faster-string-processing-in-pandas/

В посте разбирает скорость операций над 100 миллион строк

spaCy

spaCy’s core data structures are implemented as Cython cdef

Pandas 2.0 & Arrow

pandas.Series(['foo', 'bar', 'foobar'], dtype='string[pyarrow]')

Типы вместе с pandera

pydantic для pandas

Описываем схему

И mypy даёт по жопе

Пример схемы

Схемы и типы

  1. на весь df
  2. на колонку
  3. самописные проверки
  4. hypothesis например two_sample_test

Python 3.11

  • PEP 646 -- Variadic Generics (Status: Accepted)

Ждем линтеров на длину и ширину данных

Тесты без говняка

pytest-snapshot

Почему писать тесты на ETL противно?

Сложно зафиксировать эталон, потому что он будет меняться вместе с ETL

 

Допустим есть код

  1. на вход df
  2. на выход df
  3. берем любой валидный входящий dataset
  4. дампим валидную часть в git в виде csv
  5. читаем его в тесте в pd.read_csv(...)
  6. применяем код
  7. дампим выход на диск в csv
  8. ...
  9. через дни, недели, месяцы, годы ...
  10. рефакторим код
  11. проверяем что вывод такой же!

Защита от случайного изменения поведения

Цель улучшить UX когда изменения сознательные!

pytest-snapshot

pytest --snapshot-update

def test_knn_result(tmp_path, snapshot):
    snapshot.snapshot_dir = ROOT_DIR / 'snapshots'
    dataset = ROOT_DIR / 'data' / 'dataset_junior_test.csv'

    df = pd.read_csv(dataset)

    builder = KnnReportBuilder(df=df, ...)
    assert builder.fit_knn() is None

    result = builder.predict()
    labels_consistency = (
      builder.
      calculate_consistency(result, column='label')
      .to_frame('score')
    )

    snapshot.assert_match(
      to_snapshot(labels_consistency), 
      'labels_consistency.jsonl'
    )

Путь до эталона, чтоб он мог туда записать

def to_snapshot(df: pd.DataFrame) -> str:
    return (
        df.reset_index()
        .to_json(lines=True,
                 orient='records',
                 force_ascii=False)
    )

Сравним со старым тестом

Тестирование

  1. фиксируем seed если надо
  2. пробуем несколько датасетов если надо
  3. воспроизводимые тесты
  4. легко исправляются после изменения кода

Другой пример

Генерация случайных данных, тест проходит

Прикол в функции

Пустой df, т.к в генерации не учли колонку type

Тест проходил

но только на 1.1.4

В 1.5 добавили проверку что нельзя вставить пустой df

А prod_data.apply(...) как раз нулевой длины

Решение?

Я удолил тест

 

  1. Можно было взять реальный пример
  2. Проверка схемы тоже помогла бы

Почему тесты это важно?

  1. Это единственный способ делать код поддерживаемым
  2. Сами тесты это код, т.е тесты должны быть тоже поддерживаемыми чтоб они работали
  3. Рефакторинг без тестов очень сложный
  4. Без тестов не хочется менять код, если ты его не меняешь ты умираешь

Итого

  1. пишем красивый и понятный код на пандас
  2. пишем код который несколько быстрее
  3. пишем типы и валидацию схемы
  4. пишем удобные тесты

Требуем pandas 2.0

в helicopter и zero copy между dwh и python

 

подписывайте петицию (её нет)

Всем спасибо

Pandas

By Denis Kataev

Pandas

  • 63