-
Notifications
You must be signed in to change notification settings - Fork 1
/
04.Rmd
424 lines (302 loc) · 22.9 KB
/
04.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# Конверзија во повторлив код {#conversion}
## Податоци
```{r, echo=FALSE, eval=FALSE}
suppressPackageStartupMessages({
library(dplyr)
library(readr)
})
set.seed(1)
person <- c("Ана И.", "Благоја В.", "Љупчо В.", "Ристе Н.", "Антонија А.")
type <- c("канцелариски материјали", "печатење", "транспорт")
persons <- sample(x = person, size = 30, replace = TRUE)
types <- sample(x = type, size = 30, replace = TRUE)
trosoci <- dplyr::tibble(
vraboten = persons,
tip_na_trosok = types,
cena = sample.int(n = 100:1000, size = 30))
write_csv(trosoci, "data/trosoci-moja-firma.csv")
```
```{r, echo =FALSE}
suppressPackageStartupMessages({
library(dplyr)
library(readr)
})
trosoci <- readr::read_csv(
file = "data/trosoci-moja-firma.csv",
col_types = "ccn")
```
За да може да ги видиме следните чекори во акција неопходни ни се податоци. Табелата за трошоци има три колони (варијабли) `vraboten`, `tip_na_trosok` и `cena`. Еве ги првите неколку редови:
```{r}
trosoci
```
Трансформацијата која ја прави нашиот, сѐ уште неповторлив, код можеме исто така да ја видиме во акција:
```{r}
trosoci %>%
group_by(vraboten, tip_na_trosok) %>%
summarise_at("cena", "sum") %>%
arrange(vraboten, tip_na_trosok)
```
## Први чекори кон повторливост
Откако сме ги согледале проблемите што доведуваат до тоа резултатот од нашиот код да не може да се повтори (без колегата да почне да чепка и кодира од почеток), можеме да интервенираме. За нашиот код да биде повторлив, минимално треба:
1. Да ги повикува `R` пакетите кои се неопходни за функциите што ги користиме
2. Да не користи апсолутни патеки за вчитување и зачувување на патоци коишто се надвор од фолдерот во кој што е сместена самата скрипта
3. Да биде детално документиран
Што се однесува до првиот проблем, кодот едноставно нема да работи без пакетите [`readr`](https://www.rdocumentation.org/packages/readr/versions/1.3.1) [@R-readr] и [`dplyr`](https://www.rdocumentation.org/packages/dplyr/versions/0.7.8) [@R-dplyr] да се вчитани па дури и нашиот колега да ги има истите фајлови на истите локации како ние. Оваа корекција е едноставна:
```{r, eval=FALSE}
# Вчитај ги ти пакетите кои се користат подолу
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")
```
Што се однесува до втората задача, имаме повеќе можности. Можеме да побараме корисникот да ја зачува патеката во варијабла која ќе биде користена од понатамошниот код. Ова се уште дозволува патеки во друг директориум, и очигледно бара соодветна интервенција од корисникот, но ако ништо друго, корисникот на овој код, доколку ја прочита документацијата (значи снаоѓањето _зависи_ од нашата инвестиција кон добра документација), може барем да ја повтори анализата без да добие грешки:
```{r, eval=FALSE}
# Вчитај ги ти пакетите кои се користат подолу
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)
```
## Функција
Алтернатива која бара малку поголема подготовка е да го напишеме функција којашто ќе работи на ист начин, земајќи ги патеките како аргументи:
```{r}
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`) добиваме:
```{r}
# Функција за групирање и сумирање трошоци
# Аргументот `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, eval=FALSE}
# Употреба:
# Вчитај ја оваа скипта во 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)
}
```
Што сме постигнале до сега? Зависностите на кодот се решени. Документација имаме, впрочем, пишување на коментари во кодот треба да стане навика. И имаме функција на која може да и дадеме табела која што се наоѓа било каде и да зачуваме сумирана табела во истата папка.
## Rscript што може да го користиме без да отвараме `R`
Нашиот код е далеку подобар и има повеќе шанси да работи на други компјутери, но се уште може да се каже дека има некои недостатоци. На пример, корисникот _мора_ да отвори `R`, да ја вчита скриптата, и да ја изврши функцијата. Тоа е можеби во ред доколку нашиот колега има доволно познавање од `R`, но можеби нашиот шеф не знае `R` или едноставно нема време за чепкање во `R` терминал. Можеби сака решение од една линија налик на следното:
```{bash, eval = FALSE}
Rscript.exe sumiraj_trosoci.R trosoci_dekemvri_2020.csv
```
Да го конвертираме нашиот код во ваква скрипта е лесно. Доколку кодот го процесираме со [`Rscript`](https://www.rdocumentation.org/packages/utils/versions/3.6.2/topics/Rscript), треба само да го земеме името на датотеката даден по името на скриптата (тоа е табелата со трошоци), и да го предадеме на нашата функција (внатре во скриптата):
```{r, eval=FALSE}
# (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`](https://www.rdocumentation.org/packages/docopt/versions/0.7.1) кој што користи таканаречен [`docstring`](https://en.wikipedia.org/wiki/Docstring), односно текст којшто следи некои правила за форматирање со цел да биде лесно парсиран како прирачник за употреба на нашата скрипта. `docopt/docstring` имаат еквиваленти во сите други програмски јазици, така да доколку програмирате во `Python` или `Perl` [@wall2000programming] веројатно ви се веќе познати овие концепти.
За да провериме дека се е во ред со табелата што и е дадена на скриптата, ќе го користиме пакетот [assertthat](https://www.rdocumentation.org/packages/assertthat/versions/0.2.1), што ни овозможува лесни проверки и информативни пораки за грешката. Стриктно гледано, пакетите `docopt` и `assertthat` не се неопходни, можеме да користиме функции како [`commandArgs()`](https://www.rdocumentation.org/packages/base/versions/3.6.2/topics/commandArgs) и [`stopifnot()`](https://www.rdocumentation.org/packages/base/versions/3.6.2/topics/stopifnot) од основната дистрибуција на `R`. Но во некои случаи користење на додатни пакети навистина ја олеснува работата.
```{r, eval=FALSE}
# (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)
```
Со овие додатоци, нашата скрипта сега ќе може дури и да им помогне на корисниците доколку наидат на грешка. Сѐ уште не сме комплетно безбедни од не-повторливост, но стигнавме далеку имајќи во предвид каде почнавме. На пример, ако ја извршиме скриптата без аргументи:
```{bash, eval = FALSE}
$ Rscript sumiraj-trosoci-2.R
Error: Сумирај трошоци групирани по вработен и тип на трошок.
Табелаta со трошоци мора да ги содржи колоните: `vraboten`, `tip_na_trosok`, и `cena`.
Usage:
sumiraj-trosoci-2.R <tabela_so_trosoci>
Execution halted
```
Со коректен инпут:
```{bash, eval = FALSE}
$ 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()
)
```
Со погрешен фајл, праќаме ексел наместо текстуална табела со запирки:
```{bash, eval = FALSE}
Rscript sumiraj-trosoci-2.R trosoci-moja-firma.xls
Error: File 'trosoci-moja-firma.xls' does not have extension csv
Execution halted
```
Ако колоната за `cena` е крстена `eur`:
```{bash, eval = FALSE}
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
```
## Резиме
Во ова поглавје видовме како нашите едноставни три линии код напишани набрзина можеме да ги претвориме во постабилна скрипта која веќе ги има следните карактеристики значајни за повторливост:
- Сите зависности на кодот се експлицитно наведени и пакетите се вчитани
- Скриптата _не_ зависи од нашата работна средина
- Имаме далеку подобра документација, како за корисници кои ќе го отворат фајлот, така и за тие кои само ќе ја вчитаат скриптата
- Имаме неколку проверки/валидации на табелата што се трансформира -- мора да осигураме дека табелата ги исполнува потребите пред да почнеме да сумираме
- Имаме автоматско составување на името за зачувување од коренот на името на табелата што ја праќаме во скриптата
Сите овие чекори придонесуваат до побезбедно и одбранбено (дефенсивно) програмирање, односно пракса која ги зголемува шансите дека некој код ќе работи како што се очекува надвор од контекстот во кој бил креиран. Во овој случај, контекстот на креирање беше нашата `R` сесија со 10 вчитани пакети и датотека зачувана во `~/Downloads` на `Linux` оперативен систем. Видовме дека и само еден од овие аспекти на работната средина да варира кај нашите колеги, нашата скрипта нема да работи без тие да почнат да го менуваат изворниот код. Но со наведените подобрувања ги предвидуваме и надминуваме голем дел од овие проблеми.
Независно, иако направиме голем напредок кон повторлива обработка на податоци, сѐ уште правиме некакви претпоставки за контекстот во кој скриптата ќе биде користена. На пример, претпоставуваме дека сите потенцијални корисници ќе имаат табелата со трошoци, дека сите корисници ќе имаат `R` и `Rscript` инсталирано за да можат да ја извршат скриптата, и дека сите корисници ќе ги имаат инсталирано библиотеките од кои зависи нашата скрипта. Во следните поглавја ќе разгледаме начини на кои може да составиме повторливи проекти кои вклучуваат многу скрипти и податоци и прават минимални претпоставки за средината во која некој ќе се обиде да ја повтори нашата анализа.