Pull to refresh

Кто за всех решил, что python удобен для «гражданской» аналитики?

Reading time8 min
Views35K

*«Гарри Поттер и философский камень», (2001)*
«Гарри Поттер и философский камень», (2001)


ИТ-шником (программистом) нынче быть привлекательно. Дата саентистом тоже неплохо. Создаются и множатся курсы. Только вот они все однобокие. Несмотря на большое количество языков, большое количество технологий и алгоритмов, несмотря на весь накопленный в ИТ области багаж, 99% датасаенс курсов строятся по пути python-pandas.


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


Вся аргументация «за питон» строится исключительно по принципу «не думать», «рука рынка, «ну у нас же уже есть в проде 10 строк кода на питоне, что же делать?». Хотя элементарные технологические тесты и оценка экономической эффективности частенько дают неопровержимые доказательства, что DS питон является безответным поглотителем доли ИТ бюджета компаний. Взглянем ниже более пристально на отдельные моменты.


Все предыдущие публикации.


ООП и DS плохо совместимы


Классическая схема работы с данными и их анализ строится по функциональному подходу. Есть набор датасетов, который вполне может быть размером 50-80% RAM. К этому набору последовательно применяются различные инструменты и преобразования. Датасеты эти структурированные: массивы (вектора), матрицы, списки, таблицы. Все оптимальные подходы давно уже отработаны в классическом Computer Science. Но ведь нет, надо идти своим путем. Применительно же к простым прямоугольным данным введение прослойки в ООП на ровном месте увеличивает накладные расходы на CPU и RAM, иногда до неприемлемых масштабов.


Концепция ООП, когда общие данные инкапсулируются, прячутся, к ним создается куча методов для усложнения жизни, на самом первом шаге принудительно надевается ярмом на аналитика, который вообще далек от программирования. В DS во главе стоят алгоритмы и расчеты, а не игры в наследования и подборы методов. Да и сама по себе концепция ООП не является безусловным добром. «Чем быстрее вы забудете ООП, тем лучше для вас и ваших программ».
Конечно, не сам ООП плох, плохо его неуместное и неуемное применение. Модели, графики, конекты к БД и т.д. — отдельные самодостаточные сущности, их не трогаем.


В R есть совсем упрощенный элемент ООП в виде возможности добавления атрибута "class" и использования S3 диспетчеризации, применяемой для переопределения функций для разных классов. Например, перегрузка функции print для разных объектов. Например, есть обычная переменная типа матрица на 10 Гб и у нее добавляется атрибут class = matrix. И есть data.frame с атрибутом class = data.frame. Пишем print(a), в зависимости от значения атрибута объекта будет вызвана функция print.matrix или print.data.frame. Просто, дешево, сердито.


Конечно, есть и более развитые варианты — S4 и R6, но это все не особо актуально при работе с данными.


Недостаточность базовых типов данных


Изначально питон придумывался как язык общего пользования для задач далеких от DS. Сложная логика с небольшими данными. Поэтому в питоне отсуствует нативная поддержка базовых для DS типов. Например, См. Built-in Types. В базисе нет чрезвычайно важных типов как настоящий числовой массив без обвеса (Numpy для этого придумали астрономы), дата и время (datetime и сателлиты, а также numpy datetime array). Особого внимания заслуживает отсутствие полноценной поддержки отсутствующих данных, см. подробное объяснение проблем в «Python Data Science Handbook». Handling Missing Data. Все эти типы в настоящей аналитической системе должны быть 1-st class citizen, а не выступать в качестве суррогатов и костылей. Неконсистентность и множественность суррогатов базовых типов данных, отсутствие согласованных библиотек и унифицированных подходов вызывают постоянные проблемы в аналитике, вне зависимости от того, сколько лет стажа за плечами.


В R в этом смысле все прекрасно. Все базовые типы встроены в язык. integer, logical, numeric, character, missing data, категориальные переменные, списки и массивы (одномерные и матрицы), Date, POSIXct. Причем массивы — честные С-шные массивы. Просто цельный кусок памяти с индексацией элементов по смещению. Кратко можно поглядеть, например, здесь: Advanced R. Vectors. Можно сказать, что не хватает нативной поддержки integer64, но это не является серьезной проблемой. Требуется не так часто и есть пакеты, которые позволяют использовать float для использования его как integer64 — float же как раз использует 8 байт. Можно почитать хорошую популярную публикацию «Floats have 15-digit accuracy in their normal range».


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


Векторизация — это очень математическое (физическое) понятие. Много лет учились складывать вектора сил, учились стрелочки сверху писать над буквами. Приходим в питон и откатываемся в 18-ый век.


В базовом питоне ее в контексте DS просто нет. Циклы и list comprehension. А еще супер медленный apply и лямбда функции. Для тривиальнейших операций.


Есть одна отдушина. Поскольку Numpy писали астрономы, см. «Array programming with NumPy», то Numpy написан с учетом векторизации, люди от науки понимали ценность этого подхода. Но возможности ее применения достаточно ограничены.


В R же векторизация является базисом языка. Скорость расчетов, компактность кода, универсальность применения.


Pandas


Пандас является одной из самых мрачных задумок. Медленный; ресурсоемкий; однопоточный; в одних частях функционально избыточный, в других — недостаточный; ОО-ориентированный, неочевидный и далее по списку.


Колонки


Что представляет собой data.frame в классическом Computer Science? Это список указателей на массивы. Массивы — это колонки. Ближайший аналог — навесные шкафы на кухне, смонтированную на одну общую шину. Так было на ассемблере, так было на С/С++, Паскале и т.д. Во всех языках, которые предполагали работу в ограниченных аппаратных ресурсах.


Что имеем в пандасе? Да ничего подобного. Поддерживается странная концепция эквивалентности работы как по вертикали, так и по горизонтали. Что приводит к снижению качества и скорости работы в обоих случаях. Да, формально колонки могут быть организованы как массивы Numpy, которые как раз и являются нормальной реализацией настоящих массивов, как они должны были быть сделаны. Но это тянет за собой ряд проблем.


  • Первая — над базовым питоном создается два различных пакета со своими мантейнерами, планами развития, типами даных, функциями и т.д. Консистентность оставляет желать лучшего.
  • Вторая — колонки (массивы) легко могут оказаться с типом object, что разрушает концепцию массива как единого куска памяти. Таблица рассыпается на атомарные объекты. Типичный случай можно посмотреть на следующем кусочке. И питон нисколько не возмутится и позволит спокойно вытворять такие вещи.
  • Третья — попытка BlockManager решать за разработчика и консолидировать однотипные колонки в одну кучу (странный концепт) привели к полному фиаско, что в производительности, что в требованиях к ресурсам. Ссылки в конце текста.

import pandas as pd
from ppretty import ppretty
from objexplore import explore

# initialize data of lists.
data = {'Name': ['Tom', 'nick', 'krish', 'jack', 'Nobody'],
        'Age': [20, 21, 19, 18, 'Zero']}

# Create DataFrame
df = pd.DataFrame(data)

# Print the output.
df

print(ppretty(df))
print(ppretty(df.Age))
explore(df.Age)

Индекс


Индекс является постоянной болевой точкой для аналитиков. Изначально придуманный для удобства, он начинает постоянно мешать. Подкапотная логика, требующая постоянного ресета индекса; синхронизация индексов колонок; множественная логика по фильтрации и выборке; постоянные вопросы из серии loc vs iloc, мультииндекс и т.д.


Проблема, которая создана на абсолютно ровном месте. В классических таблицах / data.frame индекс может быть, а может и не быть, чаще всего он позволяет ускорять выборку и не более. Для этих задач его и придумывали, а не для культа индекса. Дополнительно, массивы в памяти можно физически сортировать. Для большого набора задач работа с упорядоченными данными на больших объемах позволяет получать прирост в скорости в несколько порядков, в т.ч. за счет конвейерной специфики работы современных компьютеров. Можно почитать интересный технологический Daniel Lemire's blog, в частности статьи про сортировки, например «To improve your indexes: sort your tables!», «Sorting is fast and useful». После этого бессмысленность и бесполезность реализации индекса в пандасах становится очевидной.


В R, как это ни удивительно слышать питонистам, data.frame и data.table устроены именно так, как написано в начале — список указателей на вектора одного типа, см., например Advanced R. Vectors. Можно было бы возразить, что нельзя произвольные объекты в колонку включить, но и тут шах и мат. Есть такое понятие: List columns. Колонка представляет собой список (базовый тип), элементы которого могут быть чем угодно.


Copy-paste


Классический антипаттерн, когда необходимо многократно упоминать имя фрейма с которым работаешь. Пандас, увы, заставляет работать именно так.
Вот типичная конструкция (это еще без шампура операций):
df[(df.val < 0.5) | (df.val2 == 7)]


Необходимо постоянно упоминать датафрейм, хотя уже и ежу ясно, что мы работаем в контексте. Поменял позже имя фрейма, в 99 местах исправил, в одном забыл — все не работает. Читаемость тоже от этого никак не улучшается. Эмуляция языка запросов через метод query — антагонист всех остальных методов, но он не обладает универсальностью.


В R же все просто и лаконично. Механизм NSE (Non-Standard Evaluation) вкупе с пайпами позволяет упомянуть имя датафрейма только в первой строчке и больше его не использовать.
Правда ведь так проще и понятее?
df[val < 0.5 | val2 == 7]


Отсутствие полноценной конвейерной обработки (pipe)


Pipes были придуманы очень давно, годах в 70-х, в unix. Чрезвычайно удобный инструмент. Позволяет передавать выходные потоки одних функций на входные потоки других. Без разрывов, без заведения ненужных временных переменных.


В питоне нет ничего такого на уровне языковых конструкций. Пакеты типа pipes или перенос длинных операций по . являются костылем. Особую радость доставляют протуберанцы ООП, когда нужно через точку постоянно иерархически получать доступ к данным или методам. Самое печальное, что все замешано в кучу. Тут мы получили доступ к данным, теперь мы применили метод, теперь опять достали из ответа метод и опять применили уже его метод. Это элементарно нечитаемо.


В R все прекрасно. С версии 4.1 даже есть нативный пайп |>, встроенный в синтаксис языка. Но давно существующий пакет magrittr обладает широким спектром различных пайпов и позволяет очень элегантно и компактно проводить сложные расчеты. Код при этом становится предельно понятным даже при беглом взгляде.


Производительность


Про практическую непригодность pandas для данных чуть больше небольших давно известно но старательно умалчивается.


Упоминал здесь: «R vs Python в продуктивном контуре», еще раз конспективно:



Про Dask, polars,… не говорим — это другая весовая категория и на подобные решения найдется масса альтернативных нейтральных ответов. Тот же Clickhouse.


R data.table молотит десяки и сотни гигов без каких-либо проблем.


Бенчмарки


В DS задачх очень часто бывает нужно протестировать различные подходы к вычислениям, чтобы выбрать оптимальный вариант или найти узкое место. Отнюдь не модуль, скрипт целиком или программный комплекс. Всего лишь функция, пара строк преобразований или еще что-то такое малое. В питоне ничего для этого нет. Многие могут сказать, а как же %timeit?


df = pd.DataFrame(np.random.randint(0,1000,size=(1000, 4)), columns=list('ABCD'))

%timeit df['A'].map('{:0>15}'.format)                 best of 5: 437 µs per loop
%timeit df['A'].map(lambda x: f'{x:0>15}')            best of 5: 565 µs per loop
%timeit df['A'].astype(str).str.zfill(15)             best of 5: 1.42 ms per loop
%timeit [f'{x:0>15}' for x in df['A'].values]         best of 5: 466 µs per loop

Ответ — да никак, это просто псеводнаучная профанация.


  1. Требует юпитер ноутбука, нельзя в консоли запустить.
  2. Неправильная методика эксперимента. тесты гонятся не вперемешку, а блоками. результат может быть подвержен внешним воздействиям (иные процессы на машине).
  3. Среднее — не лучший показатель, нужно распределения смотреть. квартили или медиану и мин/макс.
  4. Нет информации по потреблению памяти, а это важно.
  5. Среднее по n минимальным (5 из 1000) — это вообще треш метрика. На этот показатель полагаться нельзя. Статистики рыдают.

В R пакетов для бенчмарка масса и они крайне удобные. Один из лучших на настоящий момент — bench. Просто поглядите, как это должно быть сделано по уму.


Заключение


Упоминать можно многое, практически во всех концептах видны зияющие дыры. И не секрет, что большая часть DS пакетов в питоне «слизаны» с R. О чем в документации на пакеты честно пишется. Вот есть хорошие расширенные подборки с примерами, можно тоже взглянуть:



И, конечно же, бессменный тест «Database-like ops benchmark», безусловно доказывающий блеск и нищету связки python-pandas.


Так что всем, кто решил погружаться в DS через питон — есть смысл подумать, а действительно ли это будет жар-птица в руках. Чаще всего в обычных задачах это все выглядит как ободраный воробей.


Заглядывайте в группу, задавайте вопросы.


Предыдущая публикация — «Разработчики и колпак».


P.S. Уточнение по результатам комментариев. Фокус здесь на «civil data science». Не придумывайте, пожалуйста петабайты данных, MLOps и highload прод. В тексте ничего про это нет и эти вопросы даже не затрагивались. Это не значит, что вопросы не имеют ответов. Это значит, что текст про другие задачи.

Tags:
Hubs:
Total votes 64: ↑48 and ↓16+32
Comments238

Articles