Разработка → R в enterprise задачах. Хитрости и трюки

i_shutov 17 февраля в 12:51 4,9k

Несмотря на то, что задачи рядового бизнеса очень часто далеки от популярной темы больших данных и машинного обучения и часто связаны с обработкой относительно малых объёмов информации [десятки мегабайт — десятки гигабайт], размазанной в произвольных представлениях по различным видам источников, применение R в качестве основного инструмента позволяет легко и элегантно автоматизировать и ускорить эти задачи.


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



Трюк №1. Загрузка excel данных, отформатированных для восприятия человеком


Ситуация типичная и встречается в любой задаче по несколько раз в день:


  • поступаемые на анализ данные предоставляются в excel файлах совершенно разных версий;
  • файлы, в первую очередь, предназначены для заполнения и просмотра людьми. Поэтому активно используется смешение данных и «удобно читаемого» представления. Будучи импортированными «as is», данные представляют собой месиво информации, к которой невозможно ни адресоваться штатными методами, ни проанализировать. Почти все функции R будут выдавать различные ошибки или результат, который совсем не соответствует ожиданиям;
  • нет никаких гарантий, что идентичное внешнее представление будет идентично по структуре. Вполне возможны сюрпризы в виде скрытых строк и колонок с непредсказуемым содержанием;
  • в подобного рода данных даже иногда прослеживается изначально предложенный и даже структурированный шаблон ввода данных. Однако, за время использования такого информационного обмена, шаблон оброс кучей всяких дополнений и исключений. А если заполняющих несколько (филиалы компании или разные отделы), то каждый старается во что горазд. Даже в рамках одного excel в соседних закладках может использоваться различающийся способ заполнения.

Задача: обеспечить импорт такой желеобразной субстанции в структурированное представление, которое можно обрабатывать стандартными средствами (не только R).


Задача разбивается на несколько последовательных этапов. Сразу отмечу, что для решения задачи я стараюсь максимально использовать подходы, реализуемые Hadley Wickham в его философии «tidyverse». Сделано это по нескольким причинам:


  • фактически, это самое стройное и самосогласованное направление развития современного R. Так или иначе, авторы многих пакетов адаптируют их именно под парадигму работы с tidy data.
  • tidyverse универсальный мощный инструмент. Он содержит 80% функций, необходимых для типовой работы.
  • в случае с excel для простой задачи импорта данных хочется иметь возможность работы на машинах без офиса, в т.ч. на *nix. Т.е. никаких java коннекторов к офису.

Этап 1. Импорт данных


Все бы ничего, и можно было бы использовать просто пакет readxl, но возникают нюансы.


  1. Поскольку лист представляет собой смесь чисел, и текста, в т.ч. хитро оформленные названия столбцов в данных, импортировать все данные надо как текст. Для этого в функции read_excel надо явно указать спецификацию ВСЕХ колонок как текстовую.
  2. Чтобы указать спецификацию для всех колонок необходимо знать количество этих колонок, но количество не в выходном data.frame, а количество, которое воспринимается входным парсером.

На отдельных excel файлах конструкция типа


raw <- read_excel(fname)
ctypes <- rep("text", ncol(raw))
cnames <- str_c("grp_", seq_along(ctypes))
raw <- read_excel(fname,
                  col_types = ctypes,
                  col_names = cnames)

ломается с сообщением "Error: Need one name and type for each column". Верхеуровневое изучение объектов никакого разумного ответа не дает. Чтобы понять, как действовать, надо изучать github\stackoverflow.


Как резюме, проблематика и способ обхода следующие:


  1. Сам по себе excel размер своих таблиц считает не только по содержанию ячеек, но и по оформлению. Закрашенная сбоку ячейка или выставленные рамки автоматом расширят количество колонок "с данными". С точки же зрения выхода функции read_excel этих данных может не быть. Все ещё сильно усугубляется различным поведением разных форматов xls* файлов. Но в 100% случаях для подобных excel файлов количество колонок из read_excel и в исходном файле отличается.

image


  1. Для получения правильного количества колонок необходимо использовать внутренние функции пакета readxl. Способ применения вытекает из анализа содержимого пакета на github. Это не очень хорошо, но позволяет решить проблему. При этом надо подхватывать обе ветки (для .xls и для .xlsx файлов) раздельно, несмотря на то, что read_excel все это скрывает за своим фасадом.

Способ решения задачи демонстрирую на примере приведенного выше формата excel файла:


ncol(read_excel("col_test.xlsx")) # 4 колонки
length(readxl:::xlsx_col_types("col_test.xlsx")) # 5 колонок

ncol(read_excel("col_test.xls")) # 2 колонки
length(readxl:::xlsx_col_types("col_test.xls")) # 5 колонок

Получив правильное число колонок дальше без проблем импортируем согласно документации.


Этап 2. Выбор требуемых на анализ колонок в контексте формата заполнения каждого отдельного листа


Рассматриваем решение задачи на примере подобного excel файла (фрагмент).


image


Видим, что название колонки по сути размазано по диапазону от 1 до 3-х строчек.


Стратегия достаточно простая и заключается в следующем:


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

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


  • repair_names — для исправления имен импортированных колонок;
  • na.locf — для заполнения NA строк последним не-NA встретившимся значением;
  • complete.cases — удаление пустых строк
  • tribble (transposed tibble) — для ручного формирования data.frame по строчкам, а не по колонкам;
  • stri_replace_all_fixed — используем свойства векторизации для пакетного переименования строчек.

raw <- read_excel(...)

# имеем проблему, колонки с NA вместо имени
df0 <- raw %>%
  repair_names(prefix="repaired_", sep="")

# названия колонок размазаны по строкам 2-3. 2-ая -- группирующая, 3-я -- детализирующая
# Надо их слить и переименовать колонки, причем приоритет имеет строка 3, как уточняющая
#name_c2 <- tidyr::gather(df0[1, ], key = name, value = name_c2) # 1-ая колонка ушла в имена
#name_c3 <- tidyr::gather(df0[2, ], key = name, value = name_c3) # 1-ая колонка ушла в имена

# различные виды join не подойдут, поскольку мы хотим оставить все строки, вне зависимости от результата
# сливать по именам опасно, вдруг есть дубли
# names.df <- dplyr::full_join(name_c2, name_c3, by = "name")
names.df <- tibble(name_c2=tidyr::gather(df0[1, ], key=name, value=v)$v,
                   name_c3=tidyr::gather(df0[2, ], key=name, value=v)$v) %>%
  mutate(name_c2 = na.locf(name_c2)) %>%
  # если name_c3 = NA, то результат объединения строк также будет NA, нас это не очень устраивает
  mutate(name.fix = ifelse(is.na(name_c3), name_c2, str_c(name_c2, name_c3, sep=": "))) %>%
  mutate(name.fix = str_replace_all(name.fix, "\r", " ")) %>% # перевод строки
  mutate(name.fix = str_replace_all(name.fix, "\n", " ")) %>% # перевод строки
  mutate(name.fix = str_replace_all(name.fix, "  ", " "))

df1 <- df0
repl.df <- tribble(
  ~pattern, ~replacement,
  "Колонка 1: Параметр 2", "angle_in",
  "Колонка 1: Параметр 3", "speed_diff_in",
  "Колонка 5: Параметр 1: Уточнение а", "slot_in",
  "Артикул", "mark_out"
)
names(df1) <- stri_replace_all_fixed(names.df$name.fix,
                                     pattern = repl.df$pattern,
                                     replacement = repl.df$replacement,
                                     vectorize_all = FALSE)

# после всех манипуляций еще раз "починим" имена
df1 %<>% repair_names(prefix = "repaired_", sep = "")

# выбираем только интересующие колонки
df2 <- df1 %>% select(angle_in, speed_diff_in, slot_in, pressure_in, concentration_in,
                      performance_out, weight_out, mark_out) %>%
  filter(row_number() > 6) %>% # удаляем весь верхний шлак
  filter(complete.cases(.)) %>% # удаляем строки, содержащие пустые данные
  distinct() %>% # уберем идентичные строчки
  mutate_each(funs(as.numeric), -mark_out)

Трюк №2. Оптимизация Shiny кода


Концепция reactive programming является ортогональной к классическому линейному исполнению кода и тем самым тяжела для полноценного понимания аналитиками. Половина Shiny кода является не кодом на исполнение, а декларацией реакции на что-либо. С учетом того, что Shiny очень активно развивается. крайне полезно периодически актуализировать свое понимание текущего состояния. Собственно говоря, материалы "2016 Shiny Developer Conference", в частности, доклад Effective Shiny Programming by после очередного просмотра дали основание для переработке кода Shiny приложений с одновременным сокращением кода ~ на 25% и повышением общей прозрачности.
Что интересного:


  • отказ от reactiveValues в пользу раздельных функций reactive и observe.
  • исключение всевозможных проверок на инициализацию значений путем использования функции req().

Из еще полезных трюков является способ частичной проблемы с utf-8 кодировкой под Windows. Функция sourse в app.R приводит к проблемам; букву Я принципиально нельзя исключить (комментарии можно переписать, но если она встречается на осях графиков или шапке таблиц...).
Эта задача легко решается путем следующей замены:


# source("common.R")
eval(parse("common.R", encoding="UTF-8"))

Трюк №3. Боремся с непредсказуемыми входными данными


Иногда встречается ситуация, когда внутри функции, осуществляющей достаточно сложную обработку посредством функций из других пакетов, возникает сбой. Точнее, может быть exception, да так, что функция не завершает свою работу, а просто прерывается. В результате структура пакетного потока данных нарушается, после чего рушится все. Пристальное вглядывание в код ничего не дает, потому что проблема за его пределами. Обкладывать все tryCatch, особенно на программах, исполняемых раз-другой, как правило никто не любит.


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


Пакет futile.logger построен по принципу классических логгеров и не требует глубокого изучения. Однако его использование позволяет существенно повысить свою эффективность или получить свободное время.


Естественно, это далеко не все полезные трюки, но самые популярные в контексте рассмотренного класса задач.


Предыдущий пост: «До чего дошел прогRесс»

Проголосовать:
Сохранить: