26  Επανάληψη

26.1 Εισαγωγή

Σε αυτό το κεφάλαιο, θα μάθετε εργαλεία σχετικά με την επανάληψη, εκτελώντας επανειλημμένα την ίδια ενέργεια σε διαφορετικά αντικείμενα. Γενικά, η επανάληψη στην R φαίνεται διαφορετική από άλλες γλώσσες προγραμματισμού, επειδή μεγάλο μέρος της εκτελείται έμμεσα οπότε, κάθε φορά το έχουμε έτοιμο. Για παράδειγμα, εάν θέλετε να διπλασιάσετε ένα αριθμητικό διάνυσμα x στην R, μπορείτε απλώς να γράψετε 2 * x. Στην πλειοψηφία των άλλων γλωσσών, θα χρειαστεί να γράψετε και το κομμάτι του κώδικα για να διπλασιάσετε κάθε στοιχείο του x χρησιμοποιώντας κάποιου είδους βρόγχο for.

Το βιβλίο αυτό σας έχει ήδη δώσει έναν μικρό αλλά ισχυρό αριθμό εργαλείων που εκτελούν την ίδια ενέργεια για πολλά “πράγματα”:

  • Την facet_wrap() και την facet_grid() που σχεδιάζουν ένα διάγραμμα για κάθε υποσύνολο.
  • Την group_by() μαζί με την summarize() που υπολογίζουν συνοπτικά στατιστικά στοιχεία για κάθε υποσύνολο.
  • Την unnest_wider() και την unnest_longer() που δημιουργούν νέες γραμμές και στήλες για κάθε στοιχείο μιας στήλης ή λίστας.

Τώρα, ήρθε η ώρα να μάθετε μερικά πιο γενικά εργαλεία που συχνά ονομάζονται εργαλεία συναρτησιακού προγραμματισμού επειδή είναι χτισμένα γύρω από συναρτήσεις που λαμβάνουν ως είσοδο άλλες συναρτήσεις. Η εκμάθηση συναρτησιακού προγραμματισμού μπορεί εύκολα να εκτραπεί σε γενικές και ασαφής έννοιες, αλλά σε αυτό το κεφάλαιο θα διατηρήσουμε μία συγκεκριμένη κατεύθυνση εστιάζοντας σε τρεις κοινές εργασίες: τροποποίηση πολλαπλών στηλών, ανάγνωση πολλαπλών αρχείων και αποθήκευση πολλαπλών αντικειμένων.

26.1.1 Προαπαιτούμενα

Σε αυτό το κεφάλαιο, θα επικεντρωθούμε στα εργαλεία που παρέχονται από το πακέτο dplyr και το πακέτο purrr, δύο βασικά μέλη του tidyverse. Έχετε δει το πακέτο dplyr στο παρελθόν, αλλά όχι το purrr. Θα χρησιμοποιήσουμε απλώς μερικές συναρτήσεις του purrr σε αυτό το κεφάλαιο, αλλά γενικά είναι ένα εξαιρετικό πακέτο για να εξερευνήσετε καθώς βελτιώνετε τις προγραμματιστικές σας ικανότητες.

26.2 Τροποποίηση πολλαπλών στηλών

Φανταστείτε ότι έχετε αυτό το απλό tibble και θέλετε να μετρήσετε τον αριθμό των παρατηρήσεων και να υπολογίσετε τη διάμεσο κάθε στήλης.

df <- tibble(
  a = rnorm(10),
  b = rnorm(10),
  c = rnorm(10),
  d = rnorm(10)
)

Θα μπορούσατε να το κάνετε με αντιγραφή-επικόλληση:

df |> summarize(
  n = n(),
  a = median(a),
  b = median(b),
  c = median(c),
  d = median(d),
)
#> # A tibble: 1 × 5
#>       n      a      b       c     d
#>   <int>  <dbl>  <dbl>   <dbl> <dbl>
#> 1    10 -0.246 -0.287 -0.0567 0.144

Αυτό παραβιάζει τον εμπειρικό μας κανόνα που λέει να μην εφαρμόζεται αντιγραφή και επικόλληση περισσότερες από δύο φορές. Μπορείτε, ακόμα, να φανταστείτε ότι αυτό γίνεται πολύ κουραστικό αν έχετε δεκάδες ή και εκατοντάδες στήλες. Αντίθετα, μπορείτε να χρησιμοποιήσετε την across():

df |> summarize(
  n = n(),
  across(a:d, median),
)
#> # A tibble: 1 × 5
#>       n      a      b       c     d
#>   <int>  <dbl>  <dbl>   <dbl> <dbl>
#> 1    10 -0.246 -0.287 -0.0567 0.144

Η across() έχει τρία ιδιαίτερα σημαντικά ορίσματα τα οποία θα συζητήσουμε λεπτομερώς στις επόμενες ενότητες. Θα χρησιμοποιείτε τα δύο πρώτα κάθε φορά που χρησιμοποιείτε την across(): το πρώτο όρισμα, .cols, καθορίζει ποιες στήλες θέλετε να επαναλάβετε και το δεύτερο όρισμα, .fns, καθορίζει το τι θα εφαρμόσετε σε κάθε στήλη. Μπορείτε να χρησιμοποιήσετε το όρισμα .names όταν χρειάζεστε έναν επιπλέον έλεγχο στα ονόματα των στηλών εξόδου, κάτι που είναι ιδιαίτερα σημαντικό όταν χρησιμοποιείτε την across() με την mutate(). Θα συζητήσουμε επίσης δύο σημαντικές παραλλαγές, τις if_any() και if_all(), οι οποίες λειτουργούν μέσα στη filter().

26.2.1 Επιλογή στηλών με .cols

Το πρώτο όρισμα στην across(), το .cols, επιλέγει τις στήλες προς επεξεργασία. Χρησιμοποιεί τις ίδιες προδιαγραφές με την select(), Ενότητα 3.3.2, ώστε να μπορείτε να χρησιμοποιήσετε συναρτήσεις όπως την starts_with() και την ends_with() για να επιλέξετε στήλες με βάση το όνομά τους.

Υπάρχουν δύο πρόσθετες τεχνικές επιλογής που είναι ιδιαίτερα χρήσιμες για την across(): οι everything() και where(). Η everything() είναι απλή: επιλέγει κάθε στήλη (που δεν χρησιμοποιείται για ομαδοποίηση):

df <- tibble(
  grp = sample(2, 10, replace = TRUE),
  a = rnorm(10),
  b = rnorm(10),
  c = rnorm(10),
  d = rnorm(10)
)

df |> 
  group_by(grp) |> 
  summarize(across(everything(), median))
#> # A tibble: 2 × 5
#>     grp       a       b     c     d
#>   <int>   <dbl>   <dbl> <dbl> <dbl>
#> 1     1 -0.0935 -0.0163 0.363 0.364
#> 2     2  0.312  -0.0576 0.208 0.565

Παρατηρήστε ότι οι στήλες που χρησιμοποιούνται για ομαδοποίηση (εδώ, η grp) δεν περιλαμβάνονται στην across() επειδή διατηρούνται αυτόματα από την summarize().

Η where() σας επιτρέπει να επιλέξετε στήλες με βάση τον τύπο τους:

  • Η where(is.numeric) επιλέγει όλες τις αριθμητικές στήλες.
  • Η where(is.character) επιλέγει όλες τις στήλες συμβολοσειρών.
  • Η where(is.Date) επιλέγει όλες τις στήλες ημερομηνίας.
  • Η where(is.POSIXct) επιλέγει όλες τις στήλες ημερομηνίας-ώρας.
  • Η where(is.logical) επιλέγει όλες τις λογικές στήλες.

Όπως και σε άλλες συναρτήσεις επιλογής στηλών, μπορείτε να συνδυάσετε τα παραπάνω με άλγεβρα Boole. Για παράδειγμα, η !where(is.numeric) επιλέγει όλες τις μη αριθμητικές στήλες και ο συνδυασμός starts_with("a") & where(is.logical) επιλέγει όλες τις λογικές στήλες των οποίων το όνομα αρχίζει με “a”.

26.2.2 Κλήση μιας μεμονωμένης συνάρτησης

Το δεύτερο όρισμα της across() ορίζει πώς θα μετασχηματιστεί κάθε στήλη. Σε απλές περιπτώσεις, όπως παραπάνω, το όρισμα θα είναι μία υπάρχουσα συνάρτηση. Αυτό αποκαλύπτει ένα ιδιαίτερο χαρακτηριστικό της R: μπορούμε να τοποθετήσουμε μία συνάρτηση (median, mean, str_flatten, …) μέσα σε μία άλλη συνάρτηση (across). Λειτουργίες σαν και αυτή καθιστούν την R μια συναρτησιακή γλώσσα προγραμματισμού.

Είναι σημαντικό να σημειωθεί ότι τοποθετούμε τη συνάρτηση ως όρισμα στην across(), έτσι ώστε η across() να μπορεί να την καλέσει. Δεν την καλούμε εμείς. Αυτό σημαίνει ότι το όνομα της συνάρτησης δεν πρέπει ποτέ να ακολουθείται από (). Εάν ξεχαστείτε, θα λάβετε ένα σφάλμα:

df |> 
  group_by(grp) |> 
  summarize(across(everything(), median()))
#> Error in `summarize()`:
#> ℹ In argument: `across(everything(), median())`.
#> Caused by error in `median.default()`:
#> ! argument "x" is missing, with no default

Αυτό το σφάλμα προκύπτει επειδή καλείτε τη συνάρτηση χωρίς κάποιο όρισμα ως είσοδο, π.χ.

median()
#> Error in median.default(): argument "x" is missing, with no default

26.2.3 Κλήση πολλαπλών συναρτήσεων

Σε πιο περίπλοκες περιπτώσεις, μπορεί να θέλετε να προσθέσετε ορίσματα ή να εφαρμόσετε πολλαπλούς μετασχηματισμούς. Ας εξηγήσουμε το παρασκήνιο αυτού του προβλήματος με ένα απλό παράδειγμα: τι θα συμβεί εάν λείπουν κάποιες τιμές στα δεδομένα μας; Η median() επηρεάζεται από τις κενές τιμές, δίνοντάς μας ένα όχι και τόσο καλό αποτέλεσμα:

rnorm_na <- function(n, n_na, mean = 0, sd = 1) {
  sample(c(rnorm(n - n_na, mean = mean, sd = sd), rep(NA, n_na)))
}

df_miss <- tibble(
  a = rnorm_na(5, 1),
  b = rnorm_na(5, 1),
  c = rnorm_na(5, 2),
  d = rnorm(5)
)
df_miss |> 
  summarize(
    across(a:d, median),
    n = n()
  )
#> # A tibble: 1 × 5
#>       a     b     c     d     n
#>   <dbl> <dbl> <dbl> <dbl> <int>
#> 1    NA    NA    NA  1.15     5

Θα ήταν ωραίο εάν μπορούσαμε να προσθέσουμε το όρισμα na.rm = TRUE στη median() για να αφαιρέσουμε τις κενές τιμές. Για να το κάνουμε, αντί να καλέσουμε απευθείας τη median(), πρέπει να δημιουργήσουμε μία νέα συνάρτηση που καλεί τη median() με τα ορίσματα που θέλουμε:

df_miss |> 
  summarize(
    across(a:d, function(x) median(x, na.rm = TRUE)),
    n = n()
  )
#> # A tibble: 1 × 5
#>       a     b      c     d     n
#>   <dbl> <dbl>  <dbl> <dbl> <int>
#> 1 0.139 -1.11 -0.387  1.15     5

Ο νέος τρόπος παραπάνω είναι κάπως μακροσκελής και για αυτό η R περιέχει μία εύχρηστη συντόμευση: σε περιπτώσεις κώδικα με εικονικές, ή ανώνυμες συναρτήσεις1, μπορείτε να αντικαταστήσετε το κομμάτι function με το \2:

df_miss |> 
  summarize(
    across(a:d, \(x) median(x, na.rm = TRUE)),
    n = n()
  )

Σε κάθε περίπτωση, η across() αναπτύσσεται αποτελεσματικά στον ακόλουθο κώδικα:

df_miss |> 
  summarize(
    a = median(a, na.rm = TRUE),
    b = median(b, na.rm = TRUE),
    c = median(c, na.rm = TRUE),
    d = median(d, na.rm = TRUE),
    n = n()
  )

Όταν αφαιρούμε τις κενές τιμές από τη median(), θα ήταν ωραίο να γνωρίζουμε και πόσες τιμές αφαιρέθηκαν. Μπορούμε να το ανακαλύψουμε παρέχοντας δύο συναρτήσεις στην across(): μία για τον υπολογισμό της διάμεσης τιμής και άλλη μία για την καταμέτρηση των κενών τιμών. H παροχή πολλαπλών συναρτήσεων γίνεται χρησιμοποιώντας μία λίστα με ονόματα στα στοιχεία της στο όρισμα .fns:

df_miss |> 
  summarize(
    across(a:d, list(
      median = \(x) median(x, na.rm = TRUE),
      n_miss = \(x) sum(is.na(x))
    )),
    n = n()
  )
#> # A tibble: 1 × 9
#>   a_median a_n_miss b_median b_n_miss c_median c_n_miss d_median d_n_miss
#>      <dbl>    <int>    <dbl>    <int>    <dbl>    <int>    <dbl>    <int>
#> 1    0.139        1    -1.11        1   -0.387        2     1.15        0
#> # ℹ 1 more variable: n <int>

Αν παρατηρήσετε προσεκτικά, ίσως καταλάβετε ότι οι στήλες ονομάζονται χρησιμοποιώντας μία προδιαγραφή ένωσης (Ενότητα 14.3.2) όπως η {.col}_{.fn}. Όπου .col είναι το όνομα της αρχικής στήλης και . fn είναι το όνομα της συνάρτησης. Δεν είναι τυχαίο! Όπως θα μάθετε στην επόμενη ενότητα, μπορείτε να χρησιμοποιήσετε το όρισμα .names για να παρέχετε μία ένωση με βάση τις δικές σας προδιαγραφές.

26.2.4 Ονόματα στηλών

Το αποτέλεσμα της across() ονομάζεται σύμφωνα με τις προδιαγραφές που παρέχονται στο όρισμα .names. Θα μπορούσαμε να καθορίσουμε το δικό μας, αν θέλαμε το όνομα της συνάρτησης να εμφανίζεται πρώτο3:

df_miss |> 
  summarize(
    across(
      a:d,
      list(
        median = \(x) median(x, na.rm = TRUE),
        n_miss = \(x) sum(is.na(x))
      ),
      .names = "{.fn}_{.col}"
    ),
    n = n(),
  )
#> # A tibble: 1 × 9
#>   median_a n_miss_a median_b n_miss_b median_c n_miss_c median_d n_miss_d
#>      <dbl>    <int>    <dbl>    <int>    <dbl>    <int>    <dbl>    <int>
#> 1    0.139        1    -1.11        1   -0.387        2     1.15        0
#> # ℹ 1 more variable: n <int>

Το όρισμα .names είναι ιδιαίτερα σημαντικό όταν χρησιμοποιείτε την across() με την mutate(). Από προεπιλογή, η έξοδος της across() έχει τα ίδια ονόματα με τις εισόδους. Αυτό σημαίνει ότι η across() μέσα στη mutate() θα αντικαταστήσει τις υπάρχουσες στήλες. Για παράδειγμα, εδώ χρησιμοποιούμε την coalesce() για να αντικαταστήσουμε τα NA με 0:

df_miss |> 
  mutate(
    across(a:d, \(x) coalesce(x, 0))
  )
#> # A tibble: 5 × 4
#>        a      b      c     d
#>    <dbl>  <dbl>  <dbl> <dbl>
#> 1  0.434 -1.25   0     1.60 
#> 2  0     -1.43  -0.297 0.776
#> 3 -0.156 -0.980  0     1.15 
#> 4 -2.61  -0.683 -0.785 2.13 
#> 5  1.11   0     -0.387 0.704

Σε περίπτωση που θέλετε να δημιουργήσετε νέες στήλες, μπορείτε να χρησιμοποιήσετε το όρισμα .names για να δώσετε στην έξοδο νέα ονόματα:

df_miss |> 
  mutate(
    across(a:d, \(x) coalesce(x, 0), .names = "{.col}_na_zero")
  )
#> # A tibble: 5 × 8
#>        a      b      c     d a_na_zero b_na_zero c_na_zero d_na_zero
#>    <dbl>  <dbl>  <dbl> <dbl>     <dbl>     <dbl>     <dbl>     <dbl>
#> 1  0.434 -1.25  NA     1.60      0.434    -1.25      0         1.60 
#> 2 NA     -1.43  -0.297 0.776     0        -1.43     -0.297     0.776
#> 3 -0.156 -0.980 NA     1.15     -0.156    -0.980     0         1.15 
#> 4 -2.61  -0.683 -0.785 2.13     -2.61     -0.683    -0.785     2.13 
#> 5  1.11  NA     -0.387 0.704     1.11      0        -0.387     0.704

26.2.5 Φιλτράρισμα

Η across() ταιριάζει αρκετά μέσα στην summarize() και στην mutate(). Η χρήση της όμως με την filter() είναι πιο άβολη, επειδή συνήθως πρέπει να συνδυάσετε πολλαπλές συνθήκες με τους τελεστές | και &. Είναι σαφές ότι η across() μπορεί να βοηθήσει στη δημιουργία πολλαπλών στηλών λογικού τύπου, αλλά μετά τι; Για αυτό, το πακέτο dplyr προσφέρει δύο παραλλαγές της across() που ονομάζονται if_any() και if_all():

# ίδιο με το df_miss |> filter(is.na(a) | is.na(b) | is.na(c) | is.na(d))
df_miss |> filter(if_any(a:d, is.na))
#> # A tibble: 4 × 4
#>        a      b      c     d
#>    <dbl>  <dbl>  <dbl> <dbl>
#> 1  0.434 -1.25  NA     1.60 
#> 2 NA     -1.43  -0.297 0.776
#> 3 -0.156 -0.980 NA     1.15 
#> 4  1.11  NA     -0.387 0.704

# ίδιο με το df_miss |> filter(is.na(a) & is.na(b) & is.na(c) & is.na(d))
df_miss |> filter(if_all(a:d, is.na))
#> # A tibble: 0 × 4
#> # ℹ 4 variables: a <dbl>, b <dbl>, c <dbl>, d <dbl>

26.2.6 Η across() σε συναρτήσεις

Η across() είναι ιδιαίτερα χρήσιμη γιατί σας επιτρέπει να λειτουργείτε σε πολλαπλές στήλες. Για παράδειγμα, ο Jacob Scott χρησιμοποιεί τη μικρή βοηθητική συνάρτηση η οποία περιλαμβάνει μία ομάδα συναρτήσεων του πακέτου lubridate για να επεκτείνει όλες τις στήλες ημερομηνίας σε ξεχωριστές στήλες έτους, μήνα και ημέρας:

expand_dates <- function(df) {
  df |> 
    mutate(
      across(where(is.Date), list(year = year, month = month, day = mday))
    )
}

df_date <- tibble(
  name = c("Amy", "Bob"),
  date = ymd(c("2009-08-03", "2010-01-16"))
)

df_date |> 
  expand_dates()
#> # A tibble: 2 × 5
#>   name  date       date_year date_month date_day
#>   <chr> <date>         <dbl>      <dbl>    <int>
#> 1 Amy   2009-08-03      2009          8        3
#> 2 Bob   2010-01-16      2010          1       16

Η across() διευκολύνει επίσης την παροχή πολλών στηλών σε ένα μόνο όρισμα επειδή το πρώτο όρισμα χρησιμοποιεί tidy-select. Απλά πρέπει να θυμάστε να ενθυλακώσετε αυτό το όρισμα, όπως συζητήσαμε στην Ενότητα 25.3.2. Για παράδειγμα, αυτή η συνάρτηση θα υπολογίσει τους μέσους όρους των αριθμητικών στηλών από προεπιλογή. Ωστόσο, παρέχοντας το δεύτερο όρισμα, μπορείτε να επιλέξετε να συνοψίσετε μόνο στήλες που θα επιλέξετε:

summarize_means <- function(df, summary_vars = where(is.numeric)) {
  df |> 
    summarize(
      across({{ summary_vars }}, \(x) mean(x, na.rm = TRUE)),
      n = n(),
      .groups = "drop"
    )
}
diamonds |> 
  group_by(cut) |> 
  summarize_means()
#> # A tibble: 5 × 9
#>   cut       carat depth table price     x     y     z     n
#>   <ord>     <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 Fair      1.05   64.0  59.1 4359.  6.25  6.18  3.98  1610
#> 2 Good      0.849  62.4  58.7 3929.  5.84  5.85  3.64  4906
#> 3 Very Good 0.806  61.8  58.0 3982.  5.74  5.77  3.56 12082
#> 4 Premium   0.892  61.3  58.7 4584.  5.97  5.94  3.65 13791
#> 5 Ideal     0.703  61.7  56.0 3458.  5.51  5.52  3.40 21551

diamonds |> 
  group_by(cut) |> 
  summarize_means(c(carat, x:z))
#> # A tibble: 5 × 6
#>   cut       carat     x     y     z     n
#>   <ord>     <dbl> <dbl> <dbl> <dbl> <int>
#> 1 Fair      1.05   6.25  6.18  3.98  1610
#> 2 Good      0.849  5.84  5.85  3.64  4906
#> 3 Very Good 0.806  5.74  5.77  3.56 12082
#> 4 Premium   0.892  5.97  5.94  3.65 13791
#> 5 Ideal     0.703  5.51  5.52  3.40 21551

26.2.7 Περί pivot_longer()

Πριν συνεχίσουμε, αξίζει να επισημάνουμε μία ενδιαφέρουσα σύνδεση μεταξύ της across() και της pivot_longer() (Ενότητα 5.3). Σε πολλές περιπτώσεις, εκτελείτε τους ίδιους υπολογισμούς συγκεντρώνοντας πρώτα τα δεδομένα και στη συνέχεια εκτελώντας τους υπολογισμούς ανά ομάδα και όχι ανά στήλη. Για παράδειγμα, δείτε αυτήν τη σύνοψη ως αποτέλεσμα πολλαπλών συναρτήσεων:

df |> 
  summarize(across(a:d, list(median = median, mean = mean)))
#> # A tibble: 1 × 8
#>   a_median a_mean b_median b_mean c_median c_mean d_median d_mean
#>      <dbl>  <dbl>    <dbl>  <dbl>    <dbl>  <dbl>    <dbl>  <dbl>
#> 1   0.0380  0.205  -0.0163 0.0910    0.260 0.0716    0.540  0.508

Θα μπορούσαμε να υπολογίσουμε τις ίδιες τιμές συγκεντρώνοντας τα δεδομένα σε ύψος (pivot longer) και στη συνέχεια να συνοψίσουμε:

long <- df |> 
  pivot_longer(a:d) |> 
  group_by(name) |> 
  summarize(
    median = median(value),
    mean = mean(value)
  )
long
#> # A tibble: 4 × 3
#>   name   median   mean
#>   <chr>   <dbl>  <dbl>
#> 1 a      0.0380 0.205 
#> 2 b     -0.0163 0.0910
#> 3 c      0.260  0.0716
#> 4 d      0.540  0.508

Και αν θέλατε την ίδια δομή με την across(), θα μπορούσατε να συγκεντρώσετε ξανά σε πλάτος:

long |> 
  pivot_wider(
    names_from = name,
    values_from = c(median, mean),
    names_vary = "slowest",
    names_glue = "{name}_{.value}"
  )
#> # A tibble: 1 × 8
#>   a_median a_mean b_median b_mean c_median c_mean d_median d_mean
#>      <dbl>  <dbl>    <dbl>  <dbl>    <dbl>  <dbl>    <dbl>  <dbl>
#> 1   0.0380  0.205  -0.0163 0.0910    0.260 0.0716    0.540  0.508

Είναι καλό να γνωρίζετε την παραπάνω τεχνική καθώς μερικές φορές μπορεί να αντιμετωπίσετε ένα πρόβλημα το οποίο δεν μπορεί, για την ώρα, να λυθεί χρησιμοποιώντας την across(): όταν έχετε ομάδες στηλών τις οποίες θέλετε να χρησιμοποιήσετε ταυτόχρονα σε υπολογισμούς. Για παράδειγμα, φανταστείτε ότι το πλαίσιο δεδομένων μας περιέχει και τιμές και βάρη και θέλουμε να υπολογίσουμε έναν σταθμισμένο μέσο όρο:

df_paired <- tibble(
  a_val = rnorm(10),
  a_wts = runif(10),
  b_val = rnorm(10),
  b_wts = runif(10),
  c_val = rnorm(10),
  c_wts = runif(10),
  d_val = rnorm(10),
  d_wts = runif(10)
)

Δεν υπάρχει τρόπος, προς το παρόν, αυτό να γίνει με την across()4, είναι όμως σχετικά απλό με την pivot_longer():

df_long <- df_paired |> 
  pivot_longer(
    everything(), 
    names_to = c("group", ".value"), 
    names_sep = "_"
  )
df_long
#> # A tibble: 40 × 3
#>   group    val   wts
#>   <chr>  <dbl> <dbl>
#> 1 a      0.715 0.518
#> 2 b     -0.709 0.691
#> 3 c      0.718 0.216
#> 4 d     -0.217 0.733
#> 5 a     -1.09  0.979
#> 6 b     -0.209 0.675
#> # ℹ 34 more rows

df_long |> 
  group_by(group) |> 
  summarize(mean = weighted.mean(val, wts))
#> # A tibble: 4 × 2
#>   group    mean
#>   <chr>   <dbl>
#> 1 a      0.126 
#> 2 b     -0.0704
#> 3 c     -0.360 
#> 4 d     -0.248

Εάν χρειαστεί, μπορείτε να χρησιμοποιήσετε την pivot_wider() για να επιστρέψετε στην αρχική μορφή των δεδομένων.

26.2.8 Ασκήσεις

  1. Εξασκήστε τις δεξιότητές σας στην across():

    1. Υπολογίζοντας του αριθμού των μοναδικών τιμών σε κάθε στήλη του συνόλου δεδομένων palmerpenguins::penguins.

    2. Υπολογίζοντας τον μέσο όρο κάθε στήλης στο σύνολο δεδομένων mtcars.

    3. Ομαδοποιώντας, στο σύνολο δεδομένων diamonds, ανά cut, clarity, και color και στη συνέχεια μετρώντας τον αριθμό των παρατηρήσεων και υπολογίζοντας τον μέσο όρο κάθε αριθμητικής στήλης.

  2. Τι συμβαίνει εάν χρησιμοποιήσετε μία λίστα συναρτήσεων στην across(), αλλά δεν τις ονομάσετε; Τι όνομα εμφανίζεται στην έξοδο;

  3. Προσαρμόστε την expand_dates() για να αφαιρέσετε αυτόματα τις στήλες ημερομηνίας μετά την επέκτασή τους. Χρειάζεται να ενθυλακώσετε κάποια επιχειρήματα;

  4. Εξηγήστε τι κάνει κάθε βήμα σε αυτή τη συνάρτηση. Ποιο ιδιαίτερο χαρακτηριστικό της where() εκμεταλλευόμαστε εδώ;

    show_missing <- function(df, group_vars, summary_vars = everything()) {
      df |> 
        group_by(pick({{ group_vars }})) |> 
        summarize(
          across({{ summary_vars }}, \(x) sum(is.na(x))),
          .groups = "drop"
        ) |>
        select(where(\(x) any(x > 0)))
    }
    nycflights13::flights |> show_missing(c(year, month, day))

26.3 Ανάγνωση πολλαπλών αρχείων

Στην προηγούμενη ενότητα μάθατε πώς να χρησιμοποιείτε την dplyr::across() για να επαναλάβετε έναν μετασχηματισμό σε πολλαπλές στήλες. Σε αυτήν την ενότητα θα μάθετε πώς να χρησιμοποιείτε την purrr::map() για να εφαρμόσετε οτιδήποτε σε κάθε αρχείο ενός καταλόγου. Ας ξεκινήσουμε με ένα παράδειγμα: φανταστείτε ότι έχετε έναν φάκελο γεμάτο αρχεία excel5 που θέλετε να διαβάσετε. Μπορείτε να το κάνετε με αντιγραφή και επικόλληση για κάθε ένα αρχείο:

data2019 <- readxl::read_excel("data/y2019.xlsx")
data2020 <- readxl::read_excel("data/y2020.xlsx")
data2021 <- readxl::read_excel("data/y2021.xlsx")
data2022 <- readxl::read_excel("data/y2022.xlsx")

Και μετά να χρησιμοποιήσετε την dplyr::bind_rows() για να τα συνδυάσετε όλα μαζί:

data <- bind_rows(data2019, data2020, data2021, data2022)

Μπορείτε να φανταστείτε ότι αυτό θα γινόταν κουραστικό αρκετά γρήγορα, ειδικά αν είχατε εκατοντάδες αρχεία, όχι μόνο τέσσερα. Οι επόμενες ενότητες σας δείχνουν πώς να αυτοματοποιήσετε αυτού του είδους την εργασία. Υπάρχουν τρία βασικά βήματα: χρησιμοποιήστε την list.files() για να απαριθμήσετε όλα τα αρχεία σε έναν κατάλογο, στη συνέχεια χρησιμοποιήστε την purrr::map() για να διαβάσετε το καθένα από αυτά σε μία λίστα, και στη συνέχεια χρησιμοποιήστε την purrr::list_rbind() για να τα συνδυάσετε σε ένα ενιαίο πλαίσιο δεδομένων. Θα συζητήσουμε στη συνέχεια πώς μπορείτε να χειριστείτε καταστάσεις όπου η ετερογένεια αυξάνεται, όπου δεν μπορείτε να κάνετε ακριβώς το ίδιο πράγμα σε κάθε αρχείο.

26.3.1 Καταγραφή αρχείων σε έναν κατάλογο

Όπως προτείνει και το όνομα, η list.files() καταγράφει (lists) τα αρχεία σε έναν κατάλογο. Σχεδόν πάντα θα χρησιμοποιείτε τρία ορίσματα:

  • Το πρώτο όρισμα, path, είναι ο κατάλογος που θα αναζητήσετε τα αρχεία.

  • Το pattern είναι μία κανονική έκφραση που χρησιμοποιείται για να φιλτράρει τα ονόματα των αρχείων. Το πιο κοινό μοτίβο είναι κάτι σαν [.]xlsx$ ή [.]csv$ για να βρείτε όλα τα αρχεία με μία συγκεκριμένη κατάληξη.

  • Το full.names καθορίζει εάν το όνομα του καταλόγου πρέπει να συμπεριληφθεί στην έξοδο ή όχι. Σχεδόν πάντα θέλετε να είναι TRUE.

Για να κάνουμε το παράδειγμά μας συγκεκριμένο, αυτό το βιβλίο περιέχει έναν φάκελο με 12 αρχεία Excel που περιέχουν δεδομένα από το πακέτο gapminder. Κάθε αρχείο περιέχει δεδομένα ενός έτους για 142 χώρες. Μπορούμε να τα καταγράψουμε όλα με την κατάλληλη κλήση στην list.files():

paths <- list.files("data/gapminder", pattern = "[.]xlsx$", full.names = TRUE)
paths
#>  [1] "data/gapminder/1952.xlsx" "data/gapminder/1957.xlsx"
#>  [3] "data/gapminder/1962.xlsx" "data/gapminder/1967.xlsx"
#>  [5] "data/gapminder/1972.xlsx" "data/gapminder/1977.xlsx"
#>  [7] "data/gapminder/1982.xlsx" "data/gapminder/1987.xlsx"
#>  [9] "data/gapminder/1992.xlsx" "data/gapminder/1997.xlsx"
#> [11] "data/gapminder/2002.xlsx" "data/gapminder/2007.xlsx"

26.3.2 Λίστες

Τώρα που έχουμε αυτές τα 12 μονοπάτια, θα μπορούσαμε να καλέσουμε τη read_excel() 12 φορές για να λάβουμε 12 πλαίσια δεδομένων:

gapminder_1952 <- readxl::read_excel("data/gapminder/1952.xlsx")
gapminder_1957 <- readxl::read_excel("data/gapminder/1957.xlsx")
gapminder_1962 <- readxl::read_excel("data/gapminder/1962.xlsx")
 ...,
gapminder_2007 <- readxl::read_excel("data/gapminder/2007.xlsx")

Η τοποθέτηση κάθε αρχείου Excel στη δική του μεταβλητή θα κάνει τη διαχείρισή τους δύσκολη σε λίγα βήματα πιο κάτω. Αντίθετα, θα είναι πιο εύκολο εάν τα τοποθετήσουμε σε ένα μόνο αντικείμενο. Μία λίστα είναι το τέλειο εργαλείο για αυτή τη δουλειά:

files <- list(
  readxl::read_excel("data/gapminder/1952.xlsx"),
  readxl::read_excel("data/gapminder/1957.xlsx"),
  readxl::read_excel("data/gapminder/1962.xlsx"),
  ...,
  readxl::read_excel("data/gapminder/2007.xlsx")
)

Τώρα που έχετε αυτά τα πλαίσια δεδομένων σε μία λίστα, πώς μπορείτε να βγάλετε ένα από αυτή; Μπορείτε να χρησιμοποιήσετε την files[[i]] για να εξάγετε το iοστο στοιχείο:

files[[3]]
#> # A tibble: 142 × 5
#>   country     continent lifeExp      pop gdpPercap
#>   <chr>       <chr>       <dbl>    <dbl>     <dbl>
#> 1 Afghanistan Asia         32.0 10267083      853.
#> 2 Albania     Europe       64.8  1728137     2313.
#> 3 Algeria     Africa       48.3 11000948     2551.
#> 4 Angola      Africa       34    4826015     4269.
#> 5 Argentina   Americas     65.1 21283783     7133.
#> 6 Australia   Oceania      70.9 10794968    12217.
#> # ℹ 136 more rows

Θα επανέλθουμε στο [[ με περισσότερες λεπτομέρειες στην Ενότητα 27.3.

26.3.3 purrr::map() και list_rbind()

Ο κώδικας για να συλλέξετε αυτά τα πλαίσια δεδομένων σε μία λίστα “με το χέρι” είναι εξίσου κουραστικός στην πληκτρολόγηση όσο με τον κώδικα που διαβάζει τα αρχεία ένα ένα. Ευτυχώς, μπορούμε να χρησιμοποιήσουμε τη purrr::map() για να χρησιμοποιήσουμε με ακόμα καλύτερο τρόπο το διάνυσμα paths. Η map() είναι παρόμοια με την across(), αλλά αντί να εφαρμόζει κάτι σε κάθε στήλη μέσα σε ένα πλαίσιο δεδομένων, εφαρμόζει κάτι σε κάθε στοιχείο ενός διανύσματος. Η map(x, f) είναι συντομογραφία για:

list(
  f(x[[1]]),
  f(x[[2]]),
  ...,
  f(x[[n]])
)

Μπορούμε λοιπόν να χρησιμοποιήσουμε τη map() για να πάρουμε μία λίστα με 12 πλαίσια δεδομένων:

files <- map(paths, readxl::read_excel)
length(files)
#> [1] 12

files[[1]]
#> # A tibble: 142 × 5
#>   country     continent lifeExp      pop gdpPercap
#>   <chr>       <chr>       <dbl>    <dbl>     <dbl>
#> 1 Afghanistan Asia         28.8  8425333      779.
#> 2 Albania     Europe       55.2  1282697     1601.
#> 3 Algeria     Africa       43.1  9279525     2449.
#> 4 Angola      Africa       30.0  4232095     3521.
#> 5 Argentina   Americas     62.5 17876956     5911.
#> 6 Australia   Oceania      69.1  8691212    10040.
#> # ℹ 136 more rows

(Αυτή είναι μία άλλη δομή δεδομένων που δεν εμφανίζεται ιδιαίτερα συμπαγής χρησιμοποιώντας την str(), επομένως ίσως θέλετε να τη φορτώσετε στο RStudio και να την επιθεωρήσετε με την View()).

Τώρα μπορούμε να χρησιμοποιήσουμε την purrr::list_rbind() για να συνδυάσουμε αυτήν τη λίστα πλαισίων δεδομένων σε ένα ενιαίο πλαίσιο δεδομένων:

list_rbind(files)
#> # A tibble: 1,704 × 5
#>   country     continent lifeExp      pop gdpPercap
#>   <chr>       <chr>       <dbl>    <dbl>     <dbl>
#> 1 Afghanistan Asia         28.8  8425333      779.
#> 2 Albania     Europe       55.2  1282697     1601.
#> 3 Algeria     Africa       43.1  9279525     2449.
#> 4 Angola      Africa       30.0  4232095     3521.
#> 5 Argentina   Americas     62.5 17876956     5911.
#> 6 Australia   Oceania      69.1  8691212    10040.
#> # ℹ 1,698 more rows

Ή θα μπορούσαμε να κάνουμε και τα δύο βήματα ταυτόχρονα σε μία διαδικασία με τον τελεστή pipe:

paths |> 
  map(readxl::read_excel) |> 
  list_rbind()

Τι γίνεται αν θέλουμε να προσθέσουμε ορίσματα στη read_excel(); Χρησιμοποιούμε την ίδια τεχνική που χρησιμοποιήσαμε με την across(). Για παράδειγμα, είναι συχνά χρήσιμο να βλέπουμε τις πρώτες γραμμές των δεδομένων με το όρισμα n_max = 1:

paths |> 
  map(\(path) readxl::read_excel(path, n_max = 1)) |> 
  list_rbind()
#> # A tibble: 12 × 5
#>   country     continent lifeExp      pop gdpPercap
#>   <chr>       <chr>       <dbl>    <dbl>     <dbl>
#> 1 Afghanistan Asia         28.8  8425333      779.
#> 2 Afghanistan Asia         30.3  9240934      821.
#> 3 Afghanistan Asia         32.0 10267083      853.
#> 4 Afghanistan Asia         34.0 11537966      836.
#> 5 Afghanistan Asia         36.1 13079460      740.
#> 6 Afghanistan Asia         38.4 14880372      786.
#> # ℹ 6 more rows

Αυτό καθιστά σαφές ότι κάτι λείπει: δεν υπάρχει στήλη year, επειδή αυτή η τιμή καταγράφεται στο μονοπάτι και όχι στα μεμονωμένα αρχεία. Θα αντιμετωπίσουμε αυτό το πρόβλημα στη συνέχεια.

26.3.4 Δεδομένα στο μονοπάτι

Μερικές φορές το όνομα του αρχείου περιέχει και αυτό πληροφορία. Σε αυτό το παράδειγμα, το όνομα αρχείου περιέχει το έτος, το οποίο δεν καταγράφεται με άλλον τρόπο στα ξεχωριστά αρχεία. Για να μπει αυτή η στήλη στο τελικό πλαίσιο δεδομένων, πρέπει να κάνουμε δύο πράγματα:

Αρχικά, δίνουμε ένα όνομα στο διάνυσμα των μονοπατιών. Ο πιο εύκολος τρόπος για να γίνει αυτό είναι με τη συνάρτηση set_names(), η οποία μπορεί να λάβει μία συνάρτηση. Εδώ χρησιμοποιούμε τη basename() για να εξαγάγουμε μόνο το όνομα του αρχείου από το πλήρες μονοπάτι:

paths |> set_names(basename) 
#>                  1952.xlsx                  1957.xlsx 
#> "data/gapminder/1952.xlsx" "data/gapminder/1957.xlsx" 
#>                  1962.xlsx                  1967.xlsx 
#> "data/gapminder/1962.xlsx" "data/gapminder/1967.xlsx" 
#>                  1972.xlsx                  1977.xlsx 
#> "data/gapminder/1972.xlsx" "data/gapminder/1977.xlsx" 
#>                  1982.xlsx                  1987.xlsx 
#> "data/gapminder/1982.xlsx" "data/gapminder/1987.xlsx" 
#>                  1992.xlsx                  1997.xlsx 
#> "data/gapminder/1992.xlsx" "data/gapminder/1997.xlsx" 
#>                  2002.xlsx                  2007.xlsx 
#> "data/gapminder/2002.xlsx" "data/gapminder/2007.xlsx"

Αυτά τα ονόματα μεταφέρονται αυτόματα από όλες τις λειτουργίες της map, επομένως η λίστα των πλαισίων δεδομένων θα έχει τα ίδια ονόματα:

files <- paths |> 
  set_names(basename) |> 
  map(readxl::read_excel)

Ο παραπάνω κώδικας με τη κλήση της map(), είναι συντομογραφία για:

files <- list(
  "1952.xlsx" = readxl::read_excel("data/gapminder/1952.xlsx"),
  "1957.xlsx" = readxl::read_excel("data/gapminder/1957.xlsx"),
  "1962.xlsx" = readxl::read_excel("data/gapminder/1962.xlsx"),
  ...,
  "2007.xlsx" = readxl::read_excel("data/gapminder/2007.xlsx")
)

Μπορείτε επίσης να χρησιμοποιήσετε το [[ για να εξάγετε στοιχεία ανά όνομα:

files[["1962.xlsx"]]
#> # A tibble: 142 × 5
#>   country     continent lifeExp      pop gdpPercap
#>   <chr>       <chr>       <dbl>    <dbl>     <dbl>
#> 1 Afghanistan Asia         32.0 10267083      853.
#> 2 Albania     Europe       64.8  1728137     2313.
#> 3 Algeria     Africa       48.3 11000948     2551.
#> 4 Angola      Africa       34    4826015     4269.
#> 5 Argentina   Americas     65.1 21283783     7133.
#> 6 Australia   Oceania      70.9 10794968    12217.
#> # ℹ 136 more rows

Στη συνέχεια, χρησιμοποιούμε το όρισμα names_to στη list_rbind() για να της πούμε να αποθηκεύσει τα ονόματα σε μία νέα στήλη που ονομάζεται year και στη συνέχεια χρησιμοποιούμε τη readr::parse_number() για να εξαγάγουμε τον αριθμό από το κείμενο.

paths |> 
  set_names(basename) |> 
  map(readxl::read_excel) |> 
  list_rbind(names_to = "year") |> 
  mutate(year = parse_number(year))
#> # A tibble: 1,704 × 6
#>    year country     continent lifeExp      pop gdpPercap
#>   <dbl> <chr>       <chr>       <dbl>    <dbl>     <dbl>
#> 1  1952 Afghanistan Asia         28.8  8425333      779.
#> 2  1952 Albania     Europe       55.2  1282697     1601.
#> 3  1952 Algeria     Africa       43.1  9279525     2449.
#> 4  1952 Angola      Africa       30.0  4232095     3521.
#> 5  1952 Argentina   Americas     62.5 17876956     5911.
#> 6  1952 Australia   Oceania      69.1  8691212    10040.
#> # ℹ 1,698 more rows

Σε πιο περίπλοκες περιπτώσεις, μπορεί να υπάρχουν άλλες ήδη αποθηκευμένες μεταβλητές στο όνομα του καταλόγου ή ίσως το όνομα του αρχείου να περιέχει πολλά bit δεδομένων. Σε αυτήν την περίπτωση, χρησιμοποιήστε τη set_names() (χωρίς ορίσματα) για να καταγράψετε το πλήρες μονοπάτι και, στη συνέχεια, χρησιμοποιήστε τη tidyr::separate_wider_delim() και την αντίστοιχη οικογένεια συναρτήσεων για να τα μετατρέψετε σε χρήσιμες στήλες.

paths |> 
  set_names() |> 
  map(readxl::read_excel) |> 
  list_rbind(names_to = "year") |> 
  separate_wider_delim(year, delim = "/", names = c(NA, "dir", "file")) |> 
  separate_wider_delim(file, delim = ".", names = c("file", "ext"))
#> # A tibble: 1,704 × 8
#>   dir       file  ext   country     continent lifeExp      pop gdpPercap
#>   <chr>     <chr> <chr> <chr>       <chr>       <dbl>    <dbl>     <dbl>
#> 1 gapminder 1952  xlsx  Afghanistan Asia         28.8  8425333      779.
#> 2 gapminder 1952  xlsx  Albania     Europe       55.2  1282697     1601.
#> 3 gapminder 1952  xlsx  Algeria     Africa       43.1  9279525     2449.
#> 4 gapminder 1952  xlsx  Angola      Africa       30.0  4232095     3521.
#> 5 gapminder 1952  xlsx  Argentina   Americas     62.5 17876956     5911.
#> 6 gapminder 1952  xlsx  Australia   Oceania      69.1  8691212    10040.
#> # ℹ 1,698 more rows

26.3.5 Αποθηκεύστε την δουλειά σας

Τώρα που έχετε κάνει όλη αυτή τη σκληρή δουλειά για να φτάσετε σε ένα ωραίο τακτοποιημένο πλαίσιο δεδομένων, είναι η κατάλληλη στιγμή να την αποθηκεύσετε:

gapminder <- paths |> 
  set_names(basename) |> 
  map(readxl::read_excel) |> 
  list_rbind(names_to = "year") |> 
  mutate(year = parse_number(year))

write_csv(gapminder, "gapminder.csv")

Όταν επανέλθετε σε αυτό το πρόβλημα στο μέλλον, μπορείτε να διαβάσετε μόνο ένα αρχείο csv. Για μεγάλα και πιο περιεκτικά σύνολα δεδομένων, η χρήση αρχείων parquet μπορεί να είναι καλύτερη επιλογή από τα .csv, όπως συζητήθηκε στην Ενότητα 22.4.

Εάν εργάζεστε σε ένα project, προτείνουμε να ονομάσετε το αρχείο που κάνει αυτού του είδους προετοιμασίας δεδομένων κάτι παρόμοιο με το 0-cleanup.R. Το 0 στο όνομα του αρχείου υποδηλώνει ότι πρέπει να εκτελεστεί πριν από οτιδήποτε άλλο.

Εάν τα αρχεία δεδομένων εισόδου σας αλλάξουν με την πάροδο του χρόνου, μπορεί να θελήσετε να μάθετε ένα εργαλείο όπως το targets για να ρυθμίσετε τον αντίστοιχο κώδικα καθαρισμού δεδομένων έτσι ώστε να εκτελείται αυτόματα κάθε φορά που ένα από τα αρχεία εισόδου τροποποιείται.

26.3.6 Πολλαπλές απλές επαναλήψεις

Εδώ μόλις φορτώσαμε τα δεδομένα απευθείας από το δίσκο και ήμασταν τυχεροί με το να έχουμε πλέον ένα τακτοποιημένο σύνολο δεδομένων. Στις περισσότερες περιπτώσεις, θα χρειαστεί να κάνετε κάποια επιπλέον τακτοποίηση. Έχετε δύο βασικές επιλογές: μπορείτε να κάνετε έναν γύρο επανάληψης με μία σύνθετη συνάρτηση ή να κάνετε πολλαπλούς γύρους επανάληψης με απλές συναρτήσεις. Σύμφωνα με την εμπειρία μας, οι περισσότεροι προσεγγίζουν πρώτα μία σύνθετη επανάληψη, αλλά συχνά είστε καλύτερα κάνοντας πολλές απλές επαναλήψεις.

Για παράδειγμα, φανταστείτε ότι θέλετε να διαβάσετε σε μία ομάδα αρχείων, να φιλτράρετε τις κενές τιμές, να συγκεντρώσετε πληροφορία και στη συνέχεια να τη συνδυάσετε. Ένας τρόπος για να προσεγγίσετε το πρόβλημα είναι να γράψετε μία συνάρτηση που παίρνει ένα αρχείο ως είσοδο και κάνει όλα τα βήματα και στη συνέχεια να καλέσετε την map() μία φορά:

process_file <- function(path) {
  df <- read_csv(path)
  
  df |> 
    filter(!is.na(id)) |> 
    mutate(id = tolower(id)) |> 
    pivot_longer(jan:dec, names_to = "month")
}

paths |> 
  map(process_file) |> 
  list_rbind()

Εναλλακτικά, θα μπορούσατε να εκτελέσετε κάθε βήμα της process_file() σε κάθε αρχείο:

paths |> 
  map(read_csv) |> 
  map(\(df) df |> filter(!is.na(id))) |> 
  map(\(df) df |> mutate(id = tolower(id))) |> 
  map(\(df) df |> pivot_longer(jan:dec, names_to = "month")) |> 
  list_rbind()

Συνιστούμε αυτήν την προσέγγιση επειδή σας εμποδίζει από το να εστιάζετε στο να λαμβάνετε το πρώτο αρχείο αμέσως πριν προχωρήσετε στα υπόλοιπα. Λαμβάνοντας υπόψη όλα τα δεδομένα όταν κάνετε τακτοποίηση και καθαρισμό, είναι πιο πιθανό να σκεφτείτε ολιστικά και να καταλήξετε σε ένα αποτέλεσμα καλύτερης ποιότητας.

Σε αυτό το συγκεκριμένο παράδειγμα, υπάρχει άλλη μία βελτιστοποίηση που θα μπορούσατε να κάνετε με το να συνδέσετε όλα τα πλαίσια δεδομένων μαζί σε κάποιο πρότερο βήμα. Στη συνέχεια, μπορείτε να βασιστείτε στην κανονική συμπεριφορά της dplyr:

paths |> 
  map(read_csv) |> 
  list_rbind() |> 
  filter(!is.na(id)) |> 
  mutate(id = tolower(id)) |> 
  pivot_longer(jan:dec, names_to = "month")

26.3.7 Ετερογενή δεδομένα

Δυστυχώς, μερικές φορές δεν είναι δυνατό να μεταβείτε απευθείας από τη map() στη list_rbind() επειδή τα πλαίσια δεδομένων είναι τόσο ετερογενή που η list_rbind() είτε αποτυγχάνει είτε παράγει ένα πλαίσιο δεδομένων που δεν είναι χρήσιμο. Σε αυτήν την περίπτωση, εξακολουθεί να είναι χρήσιμο να ξεκινήσετε με τη φόρτωση όλων των αρχείων:

files <- paths |> 
  map(readxl::read_excel) 

Στη συνέχεια, μία πολύ χρήσιμη στρατηγική είναι να αποθηκεύσετε τη δομή των πλαισίων δεδομένων, ώστε να μπορείτε να την εξερευνήσετε χρησιμοποιώντας τις δεξιότητές σας στην επιστήμη δεδομένων. Ένας τρόπος για να το κάνετε αυτό είναι με τη εύχρηστη συνάρτηση df_types6 που επιστρέφει ένα tibble με μία σειρά για κάθε στήλη:

df_types <- function(df) {
  tibble(
    col_name = names(df), 
    col_type = map_chr(df, vctrs::vec_ptype_full),
    n_miss = map_int(df, \(x) sum(is.na(x)))
  )
}

df_types(gapminder)
#> # A tibble: 6 × 3
#>   col_name  col_type  n_miss
#>   <chr>     <chr>      <int>
#> 1 year      double         0
#> 2 country   character      0
#> 3 continent character      0
#> 4 lifeExp   double         0
#> 5 pop       double         0
#> 6 gdpPercap double         0

Στη συνέχεια, μπορείτε να εφαρμόσετε αυτήν τη συνάρτηση σε όλα τα αρχεία και ίσως με τη χρήση κάποιον συγκεντρωτικών πινάκων οι διαφορές να γίνουν πιο εύκολα αντιληπτές. Για παράδειγμα, αυτό διευκολύνει την επαλήθευση ότι τα υπολογιστικά φύλλα του gapminder με τα οποία έχουμε εργαστεί είναι όλα αρκετά ομοιογενή:

files |> 
  map(df_types) |> 
  list_rbind(names_to = "file_name") |> 
  select(-n_miss) |> 
  pivot_wider(names_from = col_name, values_from = col_type)
#> # A tibble: 12 × 6
#>   file_name country   continent lifeExp pop    gdpPercap
#>   <chr>     <chr>     <chr>     <chr>   <chr>  <chr>    
#> 1 1952.xlsx character character double  double double   
#> 2 1957.xlsx character character double  double double   
#> 3 1962.xlsx character character double  double double   
#> 4 1967.xlsx character character double  double double   
#> 5 1972.xlsx character character double  double double   
#> 6 1977.xlsx character character double  double double   
#> # ℹ 6 more rows

Εάν τα αρχεία έχουν ετερογενείς δομές, ίσως χρειαστεί να τα επεξεργαστείτε περισσότερο για να μπορέσετε να τα ενώσετε με επιτυχία. Δυστυχώς, θα σας αφήσουμε να ανακαλύψετε το πως μόνοι σας, ίσως όμως θέλετε να διαβάσετε περισσότερα για την map_if() και την map_at(). Η map_if() σας επιτρέπει να τροποποιείτε επιλεκτικά στοιχεία μιας λίστας βάσει των τιμών τους. Η map_at() σας επιτρέπει να τροποποιείτε επιλεκτικά στοιχεία βάσει των ονομάτων τους.

26.3.8 Διαχείριση αποτυχιών

Μερικές φορές η δομή των δεδομένων σας μπορεί να είναι αρκετά δυσανάγνωστη με αποτέλεσμα να μην μπορείτε να διαβάσετε όλα τα αρχεία με μία μόνο εντολή. Και τότε θα συναντήσετε ένα από τα μειονεκτήματα της map(): πετυχαίνει ή αποτυγχάνει συνολικά. Η map() είτε θα διαβάσει με επιτυχία όλα τα αρχεία σε έναν κατάλογο, είτε θα αποτύχει με ένα σφάλμα, διαβάζοντας μηδέν αρχεία. Αυτό είναι ενοχλητικό: γιατί μία αποτυχία σας εμποδίζει να αποκτήσετε πρόσβαση σε όλες τις άλλες επιτυχίες;

Ευτυχώς, το πακέτο purrr συνοδεύεται από μία βοηθητική συνάρτηση για την αντιμετώπιση αυτού του προβλήματος: possibly(). Η possibly() είναι αυτό που είναι γνωστό ως τελεστής συνάρτησης: δέχεται ως είσοδο μία συνάρτηση και επιστρέφει μία συνάρτηση με τροποποιημένη συμπεριφορά. Συγκεκριμένα, η possibly() αλλάζει μία συνάρτηση από το να επιστρέψει κάποιο σφάλμα στην επιστροφή μιας τιμής που εσείς καθορίζετε:

files <- paths |> 
  map(possibly(\(path) readxl::read_excel(path), NULL))

data <- files |> list_rbind()

Αυτό λειτουργεί ιδιαίτερα καλά εδώ επειδή η list_rbind(), όπως πολλές συναρτήσεις του tidyverse, αγνοεί αυτόματα τις NULL τιμές.

Έχετε πλέον όλα τα δεδομένα που μπορούν να διαβαστούν εύκολα και ήρθε η ώρα να αντιμετωπίσετε το δύσκολο κομμάτι του να καταλάβετε γιατί απέτυχε η φόρτωση ορισμένων αρχείων και τι να κάνετε για αυτό. Ξεκινήστε βρίσκοντας τα μονοπάτια που απέτυχαν:

failed <- map_vec(files, is.null)
paths[failed]
#> character(0)

Στη συνέχεια, καλέστε ξανά τη συνάρτηση εισαγωγής για κάθε αποτυχία και ανακαλύψτε τι πήγε στραβά.

26.4 Αποθήκευση πολλαπλών εξόδων

Στην τελευταία ενότητα, μάθατε για την map(), η οποία είναι χρήσιμη για την ανάγνωση πολλαπλών αρχείων σε ένα ενιαίο αντικείμενο. Σε αυτή την ενότητα, θα εξερευνήσουμε το αντίθετο πρόβλημα: πώς μπορείτε να πάρετε ένα ή περισσότερα αντικείμενα της R και να τα αποθηκεύσετε σε ένα ή περισσότερα αρχεία; Θα εξερευνήσουμε αυτή την πρόκληση χρησιμοποιώντας τρία παραδείγματα:

  • Αποθήκευση πολλαπλών πλαισίων δεδομένων σε μία βάση δεδομένων.
  • Αποθήκευση πολλαπλών πλαισίων δεδομένων σε πολλαπλά αρχεία .csv.
  • Αποθήκευση πολλαπλών διαγραμμάτων σε πολλαπλά αρχεία .png.

26.4.1 Εγγραφή σε βάση δεδομένων

Μερικές φορές, όταν εργάζεστε με πολλά αρχεία ταυτόχρονα, δεν είναι δυνατό να χωρέσετε όλα τα δεδομένα σας στη μνήμη ταυτόχρονα, και δεν μπορείτε να τρέξετε την map(files, read_csv). Μία προσέγγιση για να αντιμετωπίσετε αυτό το πρόβλημα είναι να φορτώσετε τα δεδομένα σας σε μία βάση δεδομένων, ώστε να μπορείτε να αποκτήσετε πρόσβαση μόνο στα τμήματα που χρειάζεστε με την dbplyr.

Αν είστε τυχεροί, το πακέτο βάσης δεδομένων που χρησιμοποιείτε θα παρέχει μία εύκολη στη χρήση συνάρτηση που λαμβάνει ένα διάνυσμα μονοπατιών και φορτώνει όλα τα δεδομένα στη βάση δεδομένων. Αυτό συμβαίνει με τη συνάρτηση duckdb_read_csv() της duckdb:

con <- DBI::dbConnect(duckdb::duckdb())
duckdb::duckdb_read_csv(con, "gapminder", paths)

Αυτό θα λειτουργούσε καλά εδώ, αλλά δεν έχουμε αρχεία csv, αντίθετα έχουμε αρχεία Excel. Έτσι, θα πρέπει να το κάνουμε “με το χέρι”. Το να μάθετε να το κάνετε με το χέρι θα σας βοηθήσει επίσης σε περιπτώσεις που έχετε μία σειρά από αρχεία csv και ταυτόχρονα η βάση δεδομένων με την οποία εργάζεστε δεν έχει κάποια λειτουργία για να τα φορτώσει όλα.

Πρέπει να ξεκινήσουμε δημιουργώντας έναν πίνακα που θα γεμίσουμε με δεδομένα. Ο πιο εύκολος τρόπος για να το κάνουμε αυτό είναι δημιουργώντας το αντικείμενο template, ένα εικονικό πλαίσιο δεδομένων που περιέχει όλες τις στήλες που θέλουμε, αλλά μόνο ένα δείγμα των δεδομένων. Για τα δεδομένα από την gapminder, μπορούμε να δημιουργήσουμε το template διαβάζοντας ένα μόνο αρχείο και προσθέτοντας το έτος (year) σε αυτό:

template <- readxl::read_excel(paths[[1]])
template$year <- 1952
template
#> # A tibble: 142 × 6
#>   country     continent lifeExp      pop gdpPercap  year
#>   <chr>       <chr>       <dbl>    <dbl>     <dbl> <dbl>
#> 1 Afghanistan Asia         28.8  8425333      779.  1952
#> 2 Albania     Europe       55.2  1282697     1601.  1952
#> 3 Algeria     Africa       43.1  9279525     2449.  1952
#> 4 Angola      Africa       30.0  4232095     3521.  1952
#> 5 Argentina   Americas     62.5 17876956     5911.  1952
#> 6 Australia   Oceania      69.1  8691212    10040.  1952
#> # ℹ 136 more rows

Τώρα μπορούμε να συνδεθούμε στη βάση δεδομένων και να χρησιμοποιήσουμε την DBI::dbCreateTable() για να μετατρέψουμε το πρότυπό μας σε έναν πίνακα βάσης δεδομένων:

con <- DBI::dbConnect(duckdb::duckdb())
DBI::dbCreateTable(con, "gapminder", template)

Η dbCreateTable() δεν χρησιμοποιεί τα δεδομένα του template, μόνο τα ονόματα και τους τύπους μεταβλητών. Έτσι, αν ελέγξουμε τον πίνακα gapminder θα δείτε ότι είναι κενός αλλά έχει τις μεταβλητές που χρειαζόμαστε με τους τύπους που αναμένουμε:

con |> tbl("gapminder")
#> # Source:   table<gapminder> [0 x 6]
#> # Database: DuckDB v0.10.1 [unknown@Linux 6.5.0-1018-azure:R 4.3.3/:memory:]
#> # ℹ 6 variables: country <chr>, continent <chr>, lifeExp <dbl>, pop <dbl>,
#> #   gdpPercap <dbl>, year <dbl>

Στη συνέχεια, χρειαζόμαστε μία συνάρτηση που παίρνει ως είσοδο ένα μονοπάτι αρχείου, το διαβάζει στην R και προσθέτει το αποτέλεσμα στον πίνακα gapminder. Μπορούμε να το κάνουμε συνδυάζοντας την read_excel() με την DBI::dbAppendTable():

append_file <- function(path) {
  df <- readxl::read_excel(path)
  df$year <- parse_number(basename(path))
  
  DBI::dbAppendTable(con, "gapminder", df)
}

Τώρα πρέπει να καλέσουμε την append_file() μία φορά για κάθε στοιχείο του path. Αυτό μπορεί να γίνει σίγουρα με την map():

paths |> map(append_file)

Δεν μας ενδιαφέρει η έξοδος της append_file(), επομένως αντί για την map() είναι λίγο πιο ωραίο να χρησιμοποιείτε την walk(). Η walk() κάνει ακριβώς το ίδιο με την map(), αλλά χωρίς να δίνει έξοδο:

paths |> walk(append_file)

Τώρα μπορούμε να ελέγξουμε αν έχουμε όλα τα δεδομένα στον πίνακά μας:

con |> 
  tbl("gapminder") |> 
  count(year)
#> # Source:   SQL [?? x 2]
#> # Database: DuckDB v0.10.1 [unknown@Linux 6.5.0-1018-azure:R 4.3.3/:memory:]
#>    year     n
#>   <dbl> <dbl>
#> 1  1967   142
#> 2  1977   142
#> 3  1987   142
#> 4  2007   142
#> 5  1952   142
#> 6  1957   142
#> # ℹ more rows

26.4.2 Εγγραφή αρχείων csv

Η ίδια βασική αρχή ισχύει αν θέλουμε να γράψουμε πολλαπλά αρχεία csv, ένα για κάθε ομάδα. Ας φανταστούμε ότι θέλουμε να πάρουμε το σύνολο δεδομένων ggplot2::diamonds και να αποθηκεύσουμε ένα αρχείο csv για κάθε τιμής της στήλης clarity. Πρώτα πρέπει να δημιουργήσουμε τα ξεχωριστά σύνολα δεδομένων. Υπάρχουν πολλοί τρόποι που θα μπορούσατε να το κάνετε αυτό, αλλά υπάρχει ένας τρόπος που μας αρέσει ιδιαίτερα: η group_nest().

by_clarity <- diamonds |> 
  group_nest(clarity)

by_clarity
#> # A tibble: 8 × 2
#>   clarity               data
#>   <ord>   <list<tibble[,9]>>
#> 1 I1               [741 × 9]
#> 2 SI2            [9,194 × 9]
#> 3 SI1           [13,065 × 9]
#> 4 VS2           [12,258 × 9]
#> 5 VS1            [8,171 × 9]
#> 6 VVS2           [5,066 × 9]
#> # ℹ 2 more rows

Αυτό μας δίνει ένα νέο tibble με οκτώ γραμμές και δύο στήλες. H clarity είναι η μεταβλητή ομαδοποίησης μας και η data είναι μία λίστα/στήλη που περιέχει ένα tibble για κάθε μοναδική τιμή της clarity:

by_clarity$data[[1]]
#> # A tibble: 741 × 9
#>   carat cut       color depth table price     x     y     z
#>   <dbl> <ord>     <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
#> 1  0.32 Premium   E      60.9    58   345  4.38  4.42  2.68
#> 2  1.17 Very Good J      60.2    61  2774  6.83  6.9   4.13
#> 3  1.01 Premium   F      61.8    60  2781  6.39  6.36  3.94
#> 4  1.01 Fair      E      64.5    58  2788  6.29  6.21  4.03
#> 5  0.96 Ideal     F      60.7    55  2801  6.37  6.41  3.88
#> 6  1.04 Premium   G      62.2    58  2801  6.46  6.41  4   
#> # ℹ 735 more rows

Όσο είμαστε σε αυτό το στάδιο, ας δημιουργήσουμε μία στήλη που δίνει το όνομα του αρχείου εξόδου, χρησιμοποιώντας την mutate() και την str_glue():

by_clarity <- by_clarity |> 
  mutate(path = str_glue("diamonds-{clarity}.csv"))

by_clarity
#> # A tibble: 8 × 3
#>   clarity               data path             
#>   <ord>   <list<tibble[,9]>> <glue>           
#> 1 I1               [741 × 9] diamonds-I1.csv  
#> 2 SI2            [9,194 × 9] diamonds-SI2.csv 
#> 3 SI1           [13,065 × 9] diamonds-SI1.csv 
#> 4 VS2           [12,258 × 9] diamonds-VS2.csv 
#> 5 VS1            [8,171 × 9] diamonds-VS1.csv 
#> 6 VVS2           [5,066 × 9] diamonds-VVS2.csv
#> # ℹ 2 more rows

Έτσι, εάν επρόκειτο να αποθηκεύσουμε αυτά τα πλαίσια δεδομένων με το χέρι, θα μπορούσαμε να γράψουμε κάτι σαν:

write_csv(by_clarity$data[[1]], by_clarity$path[[1]])
write_csv(by_clarity$data[[2]], by_clarity$path[[2]])
write_csv(by_clarity$data[[3]], by_clarity$path[[3]])
...
write_csv(by_clarity$by_clarity[[8]], by_clarity$path[[8]])

Αυτό είναι λίγο διαφορετικό από τις προηγούμενες χρήσεις της map(), επειδή υπάρχουν δύο ορίσματα που αλλάζουν, όχι μόνο ένα. Αυτό σημαίνει ότι χρειαζόμαστε μία νέα συνάρτηση: την map2(), η οποία διαφοροποιεί και το πρώτο και το δεύτερο όρισμα. Και επειδή και πάλι δεν μας ενδιαφέρει η έξοδος, θέλουμε την walk2() αντί για την map2(). Αυτό μας δίνει:

walk2(by_clarity$data, by_clarity$path, write_csv)

26.4.3 Αποθήκευση διαγραμμάτων

Μπορούμε να ακολουθήσουμε την ίδια βασική προσέγγιση για να δημιουργήσουμε πολλαπλά διαγράμματα. Ας φτιάξουμε πρώτα μία συνάρτηση που σχεδιάζει το διάγραμμα που θέλουμε:

carat_histogram <- function(df) {
  ggplot(df, aes(x = carat)) + geom_histogram(binwidth = 0.1)  
}

carat_histogram(by_clarity$data[[1]])

Ιστόγραμμα καρατιών διαμαντιών από το σύνολο δεδομένων by_clarity, που κυμαίνεται από 0 έως 5 καράτια. Η κατανομή έχει μία κορυφή και είναι λοξή προς τα δεξιά με μια κορυφή γύρω από το 1 καράτι.

Τώρα μπορούμε να χρησιμοποιήσουμε την map() για να δημιουργήσουμε μία λίστα από πολλαπλά διαγράμματα7 και τα τελικά μονοπάτια τους:

by_clarity <- by_clarity |> 
  mutate(
    plot = map(data, carat_histogram),
    path = str_glue("clarity-{clarity}.png")
  )

Στη συνέχεια, χρησιμοποιήστε την walk2() με την ggsave() για να αποθηκεύσετε κάθε διάγραμμα:

walk2(
  by_clarity$path,
  by_clarity$plot,
  \(path, plot) ggsave(path, plot, width = 6, height = 6)
)

Ο παραπάνω κώδικας είναι συντομογραφία για:

ggsave(by_clarity$path[[1]], by_clarity$plot[[1]], width = 6, height = 6)
ggsave(by_clarity$path[[2]], by_clarity$plot[[2]], width = 6, height = 6)
ggsave(by_clarity$path[[3]], by_clarity$plot[[3]], width = 6, height = 6)
...
ggsave(by_clarity$path[[8]], by_clarity$plot[[8]], width = 6, height = 6)

26.5 Σύνοψη

Σε αυτό το κεφάλαιο, είδατε πώς να χρησιμοποιείτε επανάληψη για να λύσετε τρία συνηθισμένα προβλήματα που εμφανίζονται συχνά όταν ασχολείστε με την επιστήμη δεδομένων: την διαχείριση πολλαπλών στηλών, την ανάγνωση πολλαπλών αρχείων και την αποθήκευση πολλαπλών εξόδων. Γενικά όμως, η επανάληψη είναι μία υπερδύναμη: αν γνωρίζετε τη σωστή τεχνική επανάληψης, μπορείτε εύκολα να περάσετε από το να διορθώσετε ένα πρόβλημα στο να διορθώσετε όλα τα προβλήματα. Αφού εξοικειωθείτε με τις τεχνικές σε αυτό το κεφάλαιο, σας προτείνουμε να μάθετε περισσότερα διαβάζοντας το κεφάλαιο Functionals του βιβλίου Advanced R και να συμβουλευτείτε την ιστοσελίδα purrr.

Αν γνωρίζετε ήδη αρκετά για την επανάληψη σε άλλες γλώσσες, μπορεί να εκπλαγείτε που δεν συζητήσαμε τον βρόγχο for. Αυτό οφείλεται στο ότι η προσανατολισμένη προς την ανάλυση δεδομένων φύση της R αλλάζει τον τρόπο που επαναλαμβάνουμε: στις περισσότερες περιπτώσεις μπορείτε να βασιστείτε σε ένα ιδίωμα για να κάνετε κάτι σε κάθε στήλη ή σε κάθε ομάδα. Και όταν δεν μπορείτε, συχνά μπορείτε να χρησιμοποιήσετε ένα εργαλείο λειτουργικού προγραμματισμού όπως η map() που εφαρμόζει κάτι σε κάθε στοιχείο μιας λίστας. Ωστόσο, θα δείτε βρόγχους for σε κώδικες που θα συναντήσετε εκεί έξω, έτσι θα μάθετε για αυτούς στο επόμενο κεφάλαιο, όπου θα συζητήσουμε κάποια σημαντικά εργαλεία της βασικής έκδοσης της R.


  1. Ανώνυμες, γιατί ποτέ δεν ορίσαμε ρητά το όνομα με τον τελεστή <-. Ένας άλλος όρος που χρησιμοποιούν οι προγραμματιστές σε αυτή τη περίπτωση είναι “συνάρτηση λάμδα (lambda)”.↩︎

  2. Σε κάποια παλαιότερη έκδοση κώδικα μπορεί να δείτε σύνταξη παρόμοια με ~ .x + 1. Αυτός είναι ένας άλλος τρόπος για να γράψετε ανώνυμες συναρτήσεις, αλλά λειτουργεί μόνο μέσα σε συναρτήσεις του tidyverse και χρησιμοποιεί πάντα το όνομα της μεταβλητής .x. Τώρα, συνιστούμε τη σύνταξη όπως αυτή ορίζεται στη βασική έκδοση της R, \(x) x + 1.↩︎

  3. Προς το παρόν, δεν μπορείτε να αλλάξετε τη σειρά των στηλών, αλλά θα μπορούσατε να τις αναδιατάξετε εκ των υστέρων χρησιμοποιώντας την relocate() ή κάτι παρόμοιο.↩︎

  4. Ίσως μπορέσει να γίνει κάποια μέρα, αλλά αυτή τη στιγμή δεν βλέπουμε το πως.↩︎

  5. Δεν θα εξηγήσουμε πώς λειτουργεί, αλλά αν κοιτάξετε τις οδηγίες για τις συναρτήσεις που χρησιμοποιούνται, θα πρέπει να μπορείτε να το ξεκαθαρίσετε.↩︎

  6. Δεν θα εξηγήσουμε πώς λειτουργεί, αλλά αν κοιτάξετε τις οδηγίες για τις συναρτήσεις που χρησιμοποιούνται, θα πρέπει να μπορείτε να το ξεκαθαρίσετε.↩︎

  7. Μπορείτε να εκτυπώσετε το by_clarity$plot για να λάβετε μία πρόχειρη απεικόνηση — θα πάρετε ένα διάγραμμα για κάθε στοιχείο του plots. ΣΗΜΕΙΩΣΗ: αυτό δεν συνέβη σε εμένα.↩︎