— Иван Иваныч Иванов с утра ходит без штанов!
— А Иванов Иван Иваныч одевает штаны на ночь!
«Афоня» (1975)


Множество курсов, призванных подготовить DS специалистов «за полгода», создают впечатление, что уж сертифицированным датамайнером стать достаточно просто. А что? Немного основ DS языка, немного по структуре данных, немного по различным преобразованиям данных, немного SQL, немного математики (в ML не погружаемся, только знакомимся), немного визуализации, немного HTML+JS+CSS. Специалист готов?


На практике оказывается, что маловато будет.


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


Зайдем с конца. «Чего не хватает?»


Далее не будет никаких откровений, опять наборы прописных истин в небольшом приземлении на матчасть, которые настойчиво игнорируются аналитиками. Классическим датамайнерам катастрофически не хватает навыков для работы вдолгую. Что для работы в команде, что для одиночной деятельности? И речь даже не идет о продуктиве, где все куда жестче, а только фаза исследовательских разработок.


Что это за навыки? Поскольку DS стек построен на основе языков программирования, а не на мышевозне, то для создания логичных, стройных и понятных артефактов необходимо использовать ряд полезных практик, выработанных за многие десятилетия в отрасли разработки ПО.


Каков классический контраргумент? «Да мы тут это, того, времени нет, разовый запрос, ad-hoc, памяти завались, результат же есть, ну мы считаем что нам дали все хорошее, я один с этим вожусь, я не программист, как-нибудь потом ...»


Можно согласиться, все звучит разумно, взвешенно. По факту оказывается, что:


  • вовсе не одноразовый запрос, а пока корпоративные BI-щики не освободятся и им бюджет не нарежут (несколько лет);
  • через неделю бизнес придет с похожим вопросом и бешеной входной вариативностью;
  • неожиданно придется разбираться в артефактах уволившихся коллег;
  • вдруг входные данные стали поступать кривые, а как выглядели правильные — никто и не вспомнит;
  • внезапно кончились и память и CPU, а тут еще и санкции и квоты на железо, а посчитать надо «еще вчера»;
  • вчера считалось, а сегодня падает без понимания места и причин;
  • обновили окружение и считается по-другому;
  • случайно на джойнах раздуплили данные (и не заметили) и наврали бизнесу;
  • груз продолжающих жить «ad-hoc» одноразовых аналитик поглощает 150% времени;
  • и многое другое.

Затронем ниже ряд моментов, которые приходится повторять раз за разом. Обусловлены эти моменты понятием «жизненого цикла» продукта, каковым можно считать даже один отдельный скрипт. Акценты поставлены на R, хотя есть фрагменты и для Python, а многие принципы вообще от языка зависят слабо.


Система контроля версий


Совершенно неважно, работаете ли в одиночку или в команде, над одним скриптом или над большой совокупностью. Система контроля версий (не путать с СКВ, назовем просто git) даже в простом варианте без веток и слияний позволяет сохранять ход мысли как в журнале, делать консистентные срезы кода, данных, документации, конфиигурационных файлов, делить большую задачу на части и выполнять ее законченными шагами. Всегда можно вернуться на любой шаг назад и выполнить код в том виде и на том формате данных, как это было много лун назад.


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


Использование триггеров в git позволяет автоматизировать вывод моделей, отчетов, сайтов, приложений в прод при полном исключении аналитика из этого процесса.


Все что нужно — запомнить 4 базовых команды git clone, git commit, git push, git pull. Остальное приложится. Не такая уж и сильная нагрузка для человека, погруженного в DS. Полученный же положительный эффект превысит все ожидания.


Для пользователей R & RStudio Jennifer Bryan написан прекрасный манускрипт «Happy Git and GitHub for the useR». Пользователи питона могут почитать краткие статьи на своих же ресурсах, например, «Просто про Git» или ознакомиться с настройками в IDE, например «Git в Visual Studio Code»


Code conventions


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


Есть различные практики и рекомендации, универсального решения не существует. Можно, для начала, опереться на существующие для R наработки:



Кроме бумажных инструкций есть утилиты для автоматического переформатирования кода в соответствии с выбранными соглашениями (а еще есть средства IDE), такие как, R styler.


Также, существуют средства статического анализа кода (линтеры). Даже для динамических языков, каковыми являются R и Python, статический (т.е. исходник без выполнения) анализ кода позволяет обнаружить множество возможных проблем и несоответствий стилю кодирования.



Можно посмотреть хороший доклад Jennifer Bryan «Code Smells and Feels»


Ясность мышления, чистота кода, пайпы


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


Можно это оправдать тем, что был творческий поиск решения. Ну конечно же был, только кого этот бардак интересует? Решение найдено? Самое время все прибрать, оформить по уму и от макета перейти к решению, которое можно показывать другому человеку (или себе из 2025 года).


Вот самые популярные ошибки и способы их решения.


Использование лишних переменных


Схематичный принцип следующий:


a <- df$val
b <- sin(a)
c <- sum(b^2)

# упрощенная формула
c <- sum(sin(df$val)^2)

Чем плохи лишние переменные?


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


Во-вторых, в DS такое часто проворачивают нет с простой атомарной переменной, а с датафреймами, которые могут весить весьма и весьма много (гигабайты или даже десятки гигабайт). Требуется время на выделение памяти, копирование. Да и сама опреативная память требуется в кратно бОльшем размере. Временные же переменные редко кто удаляет. Память начинает утекать стремительно и вот возникает затык, когда скрипт (приложение) падает на сервере с XYZ Gb RAM, хотя внутри считается «2+2».


В-третьих, иерархия окружений задает поиск имени переменной по дереву от локального окружения до глобальное. Если, по какой-либо причине, в локальном окружение забыли присвоить переменной b значение, а в глобальном окружении переменная b определена, то такую ошибку можно пропустить и даже не обнаружит. Результаты будут плачевными. И тут не спасает аккуаратность при первичном написании кода. Последующий рефакторинг очень легко может внести такую ошибку. Или код сложный, с ветвлением, и переменная получает свое значение в одной ветке. Поэтому каждое использование локальной переменной заставляет листать весь код и убеждаться, что переменная определена.


Вот просто пример. В том числе, пример того, что несколько строчек кода понятнее абзаца текста.


a <- 5

ff <-function(flag){
  if (flag) a <- 10
  print(a)
}

ff(TRUE)
ff(FALSE)

# [1] 10
# [1] 5

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


Пайпы (конвейеры)


Пайпы придумали очень давно, в linux они используются для конкатенации атомарных утилит, когда результат одной утилиты передается на вход другой.


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



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


В R теперь есть нативный пайп |>, который более служит для удобства записи и отладки, поскольку в коде x |> f() просто преобразуется в f(x). Пакет magrittr, появившийся много лет назад, выглядит куда более удобным. И не только потому, что можно в базовом пайпе управлять местом передаваемого параметра (через .), но и тем, что определяется несколько дополнительных видов пайпов. Примеры ниже, взяты из статьи «The Four Pipes of magrittr».


● Базовый оператор %>%


Самый распространённый пайплан оператор, который известен всем пользователям tidyverse.


mtcars %>%
    filter(mpg > 30) %>% 
    select(mpg:wt)

● Tee Pipe %T>%


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


iris %T>%
  plot %>%
  group_by(Species) %>%
  summarize(
    MaxSepalLength = max(Sepal.Length), 
    MinSepalLength = min(Sepal.Length)
  )

● Exposition Pipe %$%


Данный оператор позволяет избежать дублирования имени объекта по примеру того, как это реализовано в tidyverse. Например, работая с базовым data.frame, для фильтрации данных внутри квадратных скобок вам необходимо дублировать имя data.frame.


iris[iris$Sepal.Length < 5.0, ]

Оператор %$% позволяет избежать этого дублирования:


iris %$% iris[Sepal.Length < 5.0, ]

Ещё один пример использования:


iris %$% plot(Sepal.Length, Sepal.Width)

● Оператор присваивания %<>%


Данный оператор переопределяет значение первого объекта цепочки.


x <- c(1,2,3,4)
x %<>% sum

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


  • бесполезные накладные расходы во время исполнения;
  • невозможно отлаживать. И тот и другой аргументы пусты как банка из-под прошлогодних огурцов.

Про накладные расходы. Во-первых, вместо повторения фейков можно самостоятельно измерить и убедиться в этих расходах именно на вашей машине. Не велик бенчмарк. Счет пойдет на миллисекунды максимум. А то и на микросекунды. Во-вторых, надо оценивать не абсолютное, а относительно время. пайплайны строятся таким образом, что все вычисления проводятся в шагах, а не переходах. И если шаг выполняется, например, 100 секунд, а пайп исполняется 1 мс, то накладные составляют $10^-5$. Разговоры и убеждения больше времени занимают. А уж насколько код становится прозрачнее и понятнее с пайпами.


Про сложности отладки. Такая же городская легенда. Вариантов масса. Есть плагины для RStudio, декомпозирующие пайплайн:



Есть пакет tidylog для фонового логирования tidyverse операций, что очень удобно в пайплайне.


Можно самостоятельно логировать в пайплайне с применением оператора %T>%, см. выше. Можно просто выделить кусок в IDE и исполнить. Масса вариантов, главное не твердить как мантру чьи-то вброшенные фейки.


Самоцитирование (copy-paste)


Про метод написания копированием исписаны тонны страниц. Но насущность этого вопроса никак не меняется с течением времени. Сделали копирование кода — сразу думайте про обобщение и преобразование в функцию. Не сейчас, так потом, но обязательно наступите на грабли рассинхрона и в 99 местах поменяете имя переменной, а в 100-м забудете. И все поедет не туда. Функции хороши еще для функционального программирования, которое при анализе данных более чем уместно. А желающие структурировать свою деятельность еще четче, могут приступить к компоновке функций в собственные пакеты. Сейчас уже нет ничего сложного, достаточно один раз разобраться в написанных пакетах-хелперах, книгах, докладах.


Кстати, в DS эта техника очень распространена. Даже не погружаясь в функции. Например, в pandas нет нормального NSE (non-standard evaluation), поэтому подобные записи с многократным упоминанием датафрейма и колонок — бич аналитического кода.


filtered_df = df[(df["A"] < 1) & (df["B"] == 1)]

Трудно читать, трудно поддерживать.


В R с этим все хорошо, есть tidyverse и data.table, которые позволяют писать без повторения, используя механизм NSE. Но надо его использовать, не писать в стиле базового R. 22 год 21 века на дворе, пора бы уже переходить с базового функционала на переднюю грань.


Применение элементов безопасного программирования (defensive programming)


Даже в самом простом виде эта техника дает положительный эффект, особенно в DS. «На пальцах» суть заключается в том, что мы не верим ничему и в каждой функции проверяем (ставим ассерты) на соответствие входных параметров определенным правилам. И это гораздо проще, понятнее и безопаснее, чем писать длинное текстовое описание, которое потом и читать никто не будет. Assert куда надежнее, не выполняется правило, программа останавливается с диагностическим сообщением — разбирайтесь почему так получилось.


Вот типичный пример проверки. 3-5 строчек, которые сэкономят в будущем десятки часов поисков. Пакет checkmate быстр и богат по функционалу.


foldEdgesToTree <- function(df, root = "START"){
  # Для визуализации дерева средствами `echarts` нам потребуется подготовить структуру -- 
  # свернуть таблицу во вложенные фреймы.

  checkmate::assertDataFrame(df, min.rows = 1L)
  checkmate::assertNames(names(df), must.include = c("from", "to"), disjunct.from = "root")
  checkmate::assertDataFrame(df[from == root], min.rows = 1L)

  # аналитический код
  ...
}

И не обязательно только входом ограничиваться, можно и в процессе вычислений проверять. Ситуация усложняется еще тем, что часто функция имеет дело с «просто датафреймом», который сам по себе «вещь в себе». С набором колонок, определенными ограничениями на содержание и типы этих колонок и т.д. Ну как, не обладая доступом к данным можно понять, что во входном датафрейме должна быть колонка price и не должно быть колонки amount, иначе потом джойн порвет? Да никак.


Пример технической валидации структуры датафрейма приводил в «Конструктивные элементы надежного enterprise R приложения», ничего концептуально не поменялось за это время.


ff <- function(dataframe1, dataframe2){
  # достали имя текущей функции для задач логирования
  calledFun <- deparse(as.list(sys.call())[[1]])
  tic("Calculating XYZ")

  # проверяем содержимое всех входных дата фреймов (class, а не typeof, чтобы Date отловить)
  list(dataframe1=c("name :: character", "val :: numeric", "ship_date :: Date"),
       dataframe2=c("out :: character", "label :: character")) %>%
    purrr::iwalk(~{
      flog.info(glue::glue("Function {calledFun}: checking '{.y}' parameter with expected structure '{collapse(.x, sep=', ')}'"))
      rlang::eval_bare(rlang::sym(.y)) %>%
        assertDataFrame(min.rows=1, min.cols=length(.x)) %>%
        {assertSetEqual(.x, stri_join(names(.), map_chr(., class), sep=" :: "), .var.name=.y)}
      # {assertSubset(.x, stri_join(names(.), map_chr(., typeof), sep=" :: "))}
    })

  # аналитический код
}

Можно неплохую книгу почитать по затронутому вопросу и связанным: Offensive Programming Book


Бенчмарки


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



Документирование кода


Комментарии важны и полезны. Главное, чтобы комментарии были содержательные и по делу. Вещи, наподобие таких пусты:


n <- n + 1 # increment n

Непринципиально на каком языке пишет комментарии начинающий аналитик, если в команде договорились о форме — надо её поддерживать. А еще, есть технология генерации документации на основе исходного кода. Этой технологии уже больше 25 лет, называется doxygen. В R есть специализированный пакет-хелпер roxygen2 и поддержка процесса генерации документации непосредственно в RStudio IDE «Writing Package Documentation». Пишите документацию. «Вы из будущего» будет не раз благодарен оставленным комментариям.


Reprex


Очень часто получается так, что когда нужна помощь, аналитики начинают обмениваться какими-то пустыми словесными описаниями, фотографиями экрана и вопросами в перпендикулярных темах.


Вот типичный вопрос.


Товарищи, здравствуйте!
Вот скажите, а почему может так случиться, что не получается     
конвертировать колонку датафрейма из  типа О(object) в string? 
Делаю так: 
df['col_name']=df['col_name'].astype(str)
df['col_name']=df['col_name'].astype('str')
df['col_name']=df['col_name'].to_string()
Ничего не помогает. Как был тип O, так и остаётся(((
Гугл молчит, у всех всё получается (((


Долго, муторно, беспощадно.


Да и картинка знакома всем, только в детстве ее показыют чуть по-другому.



Для того, чтобы не мучать ни себя ни других, не заставлять людей выспрашивать многократно детали, получать быстро точный ответ, была придумана техника reprex и соответствующий одноименный пакет. По ссылке статьи, видео, описание функций, примеры и много всякого. Ничего хитрого, ReprEx = reproducible example, т.е. минимальный исполняемый пример, воспроизводящий ситуацию или вопрос.


Очень толковый диалог получается. reprex вопрос — reprex ответ. Ну еще sessionInfo() может проскочить для полноты картины. А аргументы про «тонкую натуру» и «мы же человеки» можно приберечь для более подходящих ситуаций, тут она является чужеродным элементом.


По мотива R пакета потом и в питонское сообщество завезли аналогичный пакет reprexpy. Просто потому, что реально крайне полезный и экономящий много-много часов и нервов. Возражать бесполезно.


Окружение в ОС


По сценарию ежедневно возникает масса вопросов «ой, пакет в ноутбук не ставится», «ой, тут какая-то ошибка с версиями пакетов», «ой, питон не той версии» и т.д.


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


В R все достаточно просто и системно устроено. Нужно просто один раз прочитать и разобраться.


  • Есть установленные в систему интерпретаторы языка (может быть несколько версий).
  • Есть системная библиотека пакетов в директории исполняемого файла интерпретатора.
  • Есть локальная библиотека пакетов конкретного пользователя.
  • Есть виртуальное проектное окружение с версиями пакетов, необходимыми для исполнения именно этого проекта.

Пути установки библиотек можно посмотреть командой .libPaths().
Для виртуального окружения используются пакеты renv, а раньше (и еще в Posit Connect) packrat


Для правильной адресации к файлам в проекте, используйте пакет here и методологию Project-oriented workflow


Ознакомление со смежными технологиями


При анализе данных одной из популярных задач является загрузка этих данных и вытаскивание нужных фрагментов в исходном или измененном виде. Подход в лоб, когда все преобразуется в датафреймы или датафреймы с вложенными списками или просто во вложенные списки а потом начинается «безумное чаепитие» — очень тоскливое зрелище.


Любому аналитику данных стоит отдельно ознакомиться с соответствующими технологиями и инструментами по работе с данными, записанными в различных форматах. В частности, стоит ознакомиться с


  • парсерами dom/sax xml, преобразованием с помощью xslt, позиционированием и выборкой с помощью xpath;
  • быстрыми парсерами json, языком трансформации jq;
  • регулярными выражениями regexp.

Более детально затрагивал эти вопросы здесь:



А еще очень полезно почитать книгу «The Pragmatic Programmer, 20th Anniversary Edition», есть перевод на русский, «Программист-прагматик».


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


Предыдущая публикация — «Jira, Jirа! Повернись к лесу задом, ко мне передом»

Комментарии (0)