Поглавје 4 Конверзија во повторлив код

4.1 Податоци

За да може да ги видиме следните чекори во акција неопходни ни се податоци. Табелата за трошоци има три колони (варијабли) vraboten, tip_na_trosok и cena. Еве ги првите неколку редови:

trosoci
## # A tibble: 30 x 3
##    vraboten    tip_na_trosok            cena
##    <chr>       <chr>                   <dbl>
##  1 Ана И.      печатење                   75
##  2 Ристе Н.    транспорт                  81
##  3 Ана И.      печатење                   13
##  4 Благоја В.  канцелариски материјали    40
##  5 Антонија А. транспорт                  89
##  6 Љупчо В.    печатење                   48
##  7 Благоја В.  канцелариски материјали    96
##  8 Љупчо В.    канцелариски материјали    23
##  9 Љупчо В.    транспорт                  84
## 10 Ана И.      печатење                   29
## # … with 20 more rows

Трансформацијата која ја прави нашиот, сѐ уште неповторлив, код можеме исто така да ја видиме во акција:

trosoci %>% 
  group_by(vraboten, tip_na_trosok) %>% 
  summarise_at("cena", "sum") %>% 
  arrange(vraboten, tip_na_trosok)
## # A tibble: 12 x 3
## # Groups:   vraboten [5]
##    vraboten    tip_na_trosok            cena
##    <chr>       <chr>                   <dbl>
##  1 Ана И.      канцелариски материјали    31
##  2 Ана И.      печатење                  177
##  3 Ана И.      транспорт                  51
##  4 Антонија А. печатење                  324
##  5 Антонија А. транспорт                 111
##  6 Благоја В.  канцелариски материјали   196
##  7 Благоја В.  печатење                  218
##  8 Благоја В.  транспорт                  54
##  9 Љупчо В.    канцелариски материјали    23
## 10 Љупчо В.    печатење                   48
## 11 Љупчо В.    транспорт                 127
## 12 Ристе Н.    транспорт                 116

4.2 Први чекори кон повторливост

Откако сме ги согледале проблемите што доведуваат до тоа резултатот од нашиот код да не може да се повтори (без колегата да почне да чепка и кодира од почеток), можеме да интервенираме. За нашиот код да биде повторлив, минимално треба:

  1. Да ги повикува R пакетите кои се неопходни за функциите што ги користиме

  2. Да не користи апсолутни патеки за вчитување и зачувување на патоци коишто се надвор од фолдерот во кој што е сместена самата скрипта

  3. Да биде детално документиран

Што се однесува до првиот проблем, кодот едноставно нема да работи без пакетите readr (Wickham, Hester, and Francois 2018) и dplyr (Wickham, François, et al. 2020) да се вчитани па дури и нашиот колега да ги има истите фајлови на истите локации како ние. Оваа корекција е едноставна:

# Вчитај ги ти пакетите кои се користат подолу 
library(dplyr)
library(readr)

# Доколку не се достапни, инсталирај од CRAN со:
# install.packages("dplyr")
# install.packages("readr")

trosoci <- read_csv("data/trosoci-moja-firma.csv")

trosoci_sumirani <- trosoci %>% 
  group_by(vraboten, tip_na_trosok) %>% 
  summarise_at("cena", "sum") %>% 
  arrange(vraboten, tip_na_trosok)

write_csv(trosoci_sumirani, 
          path = "data/trosoci-moja-firma-sumirani.csv")

Што се однесува до втората задача, имаме повеќе можности. Можеме да побараме корисникот да ја зачува патеката во варијабла која ќе биде користена од понатамошниот код. Ова се уште дозволува патеки во друг директориум, и очигледно бара соодветна интервенција од корисникот, но ако ништо друго, корисникот на овој код, доколку ја прочита документацијата (значи снаоѓањето зависи од нашата инвестиција кон добра документација), може барем да ја повтори анализата без да добие грешки:

# Вчитај ги ти пакетите кои се користат подолу 
library(dplyr)
library(readr)

# Доколку не се достапни, инсталирај со:
# install.packages("dplyr")
# install.packages("readr")

# ВНИМАНИЕ:
# Скриптата нема да работи доколку не внесете валидни дестинации
# за фајлови за вчитување и зачувување
pateka_do_input <- NULL # "data/trosoci-moja-firma.csv"
pateka_za_output <- NULL # "data/trosoci-moja-firma-sumirani.csv"

# На пример:
# pateka_do_input <- "~/Downloads/trosoci-moja-firma.csv" 
# pateka_do_output <- "~/Downloads/trosoci-moja-firma-sumirani.csv" 

# или:
# pateka_do_input <- "C:\rabota\podatoci\trosoci\trosoci-moja-firma.csv"
# pateka_do_output <- "C:\rabota\podatoci\trosoci\trosoci-moja-firma-sumirani.csv"

# Вчитај ги податоците
trosoci <- read_csv(pateka_do_input)

# Групирај и пресметај суми
trosoci_sumirani <- trosoci %>% 
  group_by(vraboten, tip_na_trosok) %>% 
  summarise_at("cena", "sum") %>% 
  arrange(vraboten, tip_na_trosok)

# Зачувај
write_csv(trosoci_sumirani, 
          path = pateka_za_output)

4.3 Функција

Алтернатива која бара малку поголема подготовка е да го напишеме функција којашто ќе работи на ист начин, земајќи ги патеките како аргументи:

sumiraj_trosoci <- function(trosoci, destinacija) {
  
  # Вчитај ги податоците
  trosoci <- read_csv(trosoci)
  
  # Групирај и пресметај суми
  trosoci_sumirani <- trosoci %>%
    group_by(vraboten, tip_na_trosok) %>%
    summarise_at("cena", "sum") %>% 
    arrange(vraboten, tip_na_trosok)
  
  # Зачувај
  write_csv(trosoci_sumirani,
            path = destinacija)
}

Често, сакаме табелата којашто ја снимаме да го има истото основно име како табелата што ја трансформираме (на пример moja-tabela.csv и moja-tabela-medijani.csv) и да се наоѓа во истиот фолдер. Со малку манипулација на текст во R (ова е уште полесно во Python) добиваме:

# Функција за групирање и сумирање трошоци
# Аргументот `trosoci` е патека до табелата што треба да се трансформира

sumiraj_trosoci <- function(trosoci_tabela) {
  
  # Вчитај ги податоците
  trosoci <- read_csv(trosoci_tabela)
  
  # Групирај и пресметај суми
  trosoci_sumirani <- trosoci %>%
    group_by(vraboten, tip_na_trosok) %>%
    summarise_at("cena", "sum") %>% 
    arrange(vraboten, tip_na_trosok)
  
  # Направи патека за дестинација
  folder_name <- dirname(trosoci_tabela)
  base_name <- tools::file_path_sans_ext(basename(trosoci_tabela))
  new_name <- paste(base_name, "sumirani.csv", sep="-")
  destinacija <- file.path(folder_name, new_name)
  
  # Зачувај
  write_csv(trosoci_sumirani, path = destinacija)
}

Целата скрипта по овие промени би изгледала вака:

# Употреба:
# Вчитај ја оваа скипта во R за да ја користиш функцијата `sumiraj_trosoci`  

# Вчитај ги ти пакетите кои се користат подолу 
library(dplyr)
library(readr)

# Доколку не се достапни, инсталирај со:
# install.packages("dplyr")
# install.packages("readr")

# Функција за групирање и сумирање трошоци
# Аргументот `trosoci` е патека до табелата што треба да се трансформира

sumiraj_trosoci <- function(trosoci_tabela) {
  
  # Вчитај ги податоците
  trosoci <- read_csv(trosoci_tabela)
  
  # Групирај и пресметај суми
  trosoci_sumirani <- trosoci %>%
    group_by(vraboten, tip_na_trosok) %>%
    summarise_at("cena", "sum") %>% 
  arrange(vraboten, tip_na_trosok)
  
  # Направи патека за дестинација
  folder_name <- dirname(trosoci_tabela)
  base_name <- tools::file_path_sans_ext(basename(trosoci_tabela))
  new_name <- paste(base_name, "sumirani.csv", sep="-")
  destinacija <- file.path(folder_name, new_name)
  
  # Зачувај
  write_csv(trosoci_sumirani, path = destinacija)
}

Што сме постигнале до сега? Зависностите на кодот се решени. Документација имаме, впрочем, пишување на коментари во кодот треба да стане навика. И имаме функција на која може да и дадеме табела која што се наоѓа било каде и да зачуваме сумирана табела во истата папка.

4.4 Rscript што може да го користиме без да отвараме R

Нашиот код е далеку подобар и има повеќе шанси да работи на други компјутери, но се уште може да се каже дека има некои недостатоци. На пример, корисникот мора да отвори R, да ја вчита скриптата, и да ја изврши функцијата. Тоа е можеби во ред доколку нашиот колега има доволно познавање од R, но можеби нашиот шеф не знае R или едноставно нема време за чепкање во R терминал. Можеби сака решение од една линија налик на следното:

Rscript.exe sumiraj_trosoci.R trosoci_dekemvri_2020.csv

Да го конвертираме нашиот код во ваква скрипта е лесно. Доколку кодот го процесираме со Rscript, треба само да го земеме името на датотеката даден по името на скриптата (тоа е табелата со трошоци), и да го предадеме на нашата функција (внатре во скриптата):

# (data/sumiraj-trosoci-1.R)

# Употреба:
# Вчитај ја оваа скипта во R за да ја користиш функцијата `sumiraj_trosoci`  

# Закачи ги ти пакетите кои се користат подолу 
library(dplyr)
library(readr)

# Доколку не се достапни, инсталирај со:
# install.packages("dplyr")
# install.packages("readr")

# Функција за групирање и сумирање трошоци
# Аргументот `trosoci` е патека до табелата што треба да се трансформира

sumiraj_trosoci <- function(trosoci_tabela) {
  
  # Вчитај ги податоците
  trosoci <- read_csv(trosoci_tabela)
  
  # Групирај и пресметај суми
  trosoci_sumirani <- trosoci %>%
    group_by(vraboten, tip_na_trosok) %>%
    summarise_at("cena", "sum") %>% 
  arrange(vraboten, tip_na_trosok)
  
  # Направи патека за дестинација
  folder_name <- dirname(trosoci_tabela)
  base_name <- tools::file_path_sans_ext(basename(trosoci_tabela))
  new_name <- paste(base_name, "sumirani.csv", sep="-")
  destinacija <- file.path(folder_name, new_name)
  
  # Зачувај
  write_csv(trosoci_sumirani, path = destinacija)
}

# Земи го првиот аргумент
dadeni_trosoci <- commandArgs(trailingOnly=TRUE)[[1]]

# Изврши ја функцијата
sumiraj_trosoci(trosoci = dadeni_trosoci)

Доколку го тестирате кодот додека читате го имате преземено директориумот за овој текст, оваа скрипта и податоците за трошоци се наоѓаат како фолдерот data под името sumiraj-trosoci-1.R и trosoci-moja-firma.csv.

Доколку сакаме навистина да се потрудиме, како поради безбедност така и поради лесно користење на ваквата скрипта, можеме да додадеме кратко упатство за користење, и код за проверка на аргументот.

За да направиме упатство за користење, ќе го користиме пакетот docopt кој што користи таканаречен docstring, односно текст којшто следи некои правила за форматирање со цел да биде лесно парсиран како прирачник за употреба на нашата скрипта. docopt/docstring имаат еквиваленти во сите други програмски јазици, така да доколку програмирате во Python или Perl (Wall, Christiansen, and Orwant 2000) веројатно ви се веќе познати овие концепти.

За да провериме дека се е во ред со табелата што и е дадена на скриптата, ќе го користиме пакетот assertthat, што ни овозможува лесни проверки и информативни пораки за грешката. Стриктно гледано, пакетите docopt и assertthat не се неопходни, можеме да користиме функции како commandArgs() и stopifnot() од основната дистрибуција на R. Но во некои случаи користење на додатни пакети навистина ја олеснува работата.

# (data/sumiraj-trosoci-2.R)

'Сумирај трошоци групирани по вработен и тип на трошок. 
 Табелаta со трошоци мора да ги содржи колоните: `vraboten`, `tip_na_trosok`, и `cena`.
 
 Usage:
    sumiraj-trosoci-2.R <tabela_so_trosoci>
    sumiraj-trosoci-2.R --help
    sumiraj-trosoci-2.R --version

 Options:
    --help      Прикажи помош
    --version   Прикажи верзија
    
' -> doc

# Логика за аргументи
library(docopt)
arguments <- docopt(doc, version = "Сумирај трошоци 2.0\n")

# Провери дали табелата е csv формат
assertthat::assert_that(
  assertthat::has_extension(arguments$tabela_so_trosoci, ext = "csv"))

# Вчитај ги ти пакетите кои се користат подолу
suppressPackageStartupMessages({
  library(dplyr)
  library(readr)
  library(assertthat)
})

# Доколку не се достапни, инсталирај со:
# install.packages("dplyr")
# install.packages("readr")
# install.packages(assertthat)

# Функција за групирање и сумирање трошоци
# Аргументот `trosoci_tabela` е патека до табелата што треба да се трансформира

sumiraj_trosoci <- function(trosoci_tabela) {

  # Вчитај ги податоците
  trosoci <- read_csv(trosoci_tabela)
  
  assertthat::assert_that(inherits(trosoci, "data.frame"), msg = "Табелата не беше вчитана како `data.frame`.")
  assertthat::assert_that(all(c("vraboten", "tip_na_trosok", "cena") %in% names(trosoci)), 
                          msg = "Табелата мора да содржи колони со имињата: 'vraboten', 'tip_na_trosok', 'cena'.")
  assertthat::assert_that(is.numeric(trosoci$cena), msg = "Колоната `cena` мора да биде нумеричка.")
  
  
  # Групирај и пресметај суми
  trosoci_sumirani <- trosoci %>%
    group_by(vraboten, tip_na_trosok) %>%
    summarise_at("cena", "sum") %>%
    arrange(vraboten, tip_na_trosok)

  # Направи патека за дестинација
  folder_name <- dirname(trosoci_tabela)
  base_name <- tools::file_path_sans_ext(basename(trosoci_tabela))
  new_name <- paste(base_name, "sumirani.csv", sep="-")
  destinacija <- file.path(folder_name, new_name)

  # Зачувај
  write_csv(trosoci_sumirani, path = destinacija)
}

# Земи го првиот аргумент (табелата)
dadeni_trosoci <- arguments$tabela_so_trosoci

# Изврши ја функцијата
sumiraj_trosoci(trosoci = dadeni_trosoci)

Со овие додатоци, нашата скрипта сега ќе може дури и да им помогне на корисниците доколку наидат на грешка. Сѐ уште не сме комплетно безбедни од не-повторливост, но стигнавме далеку имајќи во предвид каде почнавме. На пример, ако ја извршиме скриптата без аргументи:

$ Rscript sumiraj-trosoci-2.R 
Error: Сумирај трошоци групирани по вработен и тип на трошок. 
 Табелаta со трошоци мора да ги содржи колоните: `vraboten`, `tip_na_trosok`, и `cena`.
 
 Usage:
    sumiraj-trosoci-2.R <tabela_so_trosoci>
    
Execution halted

Со коректен инпут:

$ Rscript sumiraj-trosoci-2.R trosoci-moja-firma.csv 
[1] TRUE
Parsed with column specification:
cols(
  vraboten = col_character(),
  tip_na_trosok = col_character(),
  cena = col_double()
)

Со погрешен фајл, праќаме ексел наместо текстуална табела со запирки:

Rscript sumiraj-trosoci-2.R trosoci-moja-firma.xls 
Error: File 'trosoci-moja-firma.xls' does not have extension csv
Execution halted

Ако колоната за cena е крстена eur:

Rscript sumiraj-trosoci-2.R trosoci-moja-firma-eur.csv
[1] TRUE
Parsed with column specification:
cols(
  vraboten = col_character(),
  tip_na_trosok = col_character(),
  eur = col_double()
)
Error: Табелата мора да содржи колони со имињата: 'vraboten', 'tip_na_trosok', 'cena'.
Execution halted

4.5 Резиме

Во ова поглавје видовме како нашите едноставни три линии код напишани набрзина можеме да ги претвориме во постабилна скрипта која веќе ги има следните карактеристики значајни за повторливост:

  • Сите зависности на кодот се експлицитно наведени и пакетите се вчитани
  • Скриптата не зависи од нашата работна средина
  • Имаме далеку подобра документација, како за корисници кои ќе го отворат фајлот, така и за тие кои само ќе ја вчитаат скриптата
  • Имаме неколку проверки/валидации на табелата што се трансформира – мора да осигураме дека табелата ги исполнува потребите пред да почнеме да сумираме
  • Имаме автоматско составување на името за зачувување од коренот на името на табелата што ја праќаме во скриптата

Сите овие чекори придонесуваат до побезбедно и одбранбено (дефенсивно) програмирање, односно пракса која ги зголемува шансите дека некој код ќе работи како што се очекува надвор од контекстот во кој бил креиран. Во овој случај, контекстот на креирање беше нашата R сесија со 10 вчитани пакети и датотека зачувана во ~/Downloads на Linux оперативен систем. Видовме дека и само еден од овие аспекти на работната средина да варира кај нашите колеги, нашата скрипта нема да работи без тие да почнат да го менуваат изворниот код. Но со наведените подобрувања ги предвидуваме и надминуваме голем дел од овие проблеми.

Независно, иако направиме голем напредок кон повторлива обработка на податоци, сѐ уште правиме некакви претпоставки за контекстот во кој скриптата ќе биде користена. На пример, претпоставуваме дека сите потенцијални корисници ќе имаат табелата со трошoци, дека сите корисници ќе имаат R и Rscript инсталирано за да можат да ја извршат скриптата, и дека сите корисници ќе ги имаат инсталирано библиотеките од кои зависи нашата скрипта. Во следните поглавја ќе разгледаме начини на кои може да составиме повторливи проекти кои вклучуваат многу скрипти и податоци и прават минимални претпоставки за средината во која некој ќе се обиде да ја повтори нашата анализа.

Литература

Wall, Larry, Tom Christiansen, and Jon Orwant. 2000. Programming Perl. " O’Reilly Media, Inc.".

Wickham, Hadley, Romain François, Lionel Henry, and Kirill Müller. 2020. Dplyr: A Grammar of Data Manipulation. https://CRAN.R-project.org/package=dplyr.

Wickham, Hadley, Jim Hester, and Romain Francois. 2018. Readr: Read Rectangular Text Data. https://CRAN.R-project.org/package=readr.