R Dersleri - Ali Onur Gitmez

Ana Sayfa Ders 1 Ders 2 Ders 3 Ders 4 Ders 5 Ders 6 Ders 7 Ders 8 Ders 9 Ders 10 Ders 11 Ders 12 Ders 13 Ders 14 Ders 15

Apply Family Functions

R’in en guclu noktalarindan birisi apply komutu ailesidir. Bunlari for ve while looplarina alternatif olarak gorebiliriz. Looplar kodu karmasik yapip ilk bakista okumayi zorlastirabilirken bu functionlar kodun okunmasini kolaylastirir ve daha basit bir akis saglar. Amaclari uygulandiklari data structurei ustunde degisiklik yapip uygun bir formatta sonucu cikarmaktir. Bizim ustune odaklanacagimiz 7 komut var. Bunlar:

  • apply()
  • sapply()
  • mapply()
  • vapply()
  • tapply()
  • lapply()
  • rapply()

apply() komutu matrixlerin veya data framelerin row ve columnlari ustunde calismamiza yarar.

mat <- matrix(1:9, nrow = 3)

row_sums <- apply(mat, 1, sum)
print(row_sums)
[1] 12 15 18
col_means <- apply(mat, 2, mean)
print(col_means)
[1] 2 5 8

Bu ornekte ilk olarak 3x3 bir matris yaratiyoruz. Sonrasinda apply fonskiyonunu kullanarak su argumanlari inceliyoruz:

ilk olarak functioni hangi veri ustunde uygulamak istiyorsak, hangi marjin ustunde uygulamak istiyorsak(1 row, 2 column) ve hangi islemi gerceklestirmek istiyorsak belirtiyoruz. Burada orneklerde ileride de gorecegimiz sum ve mean kullanildi.

Hem row hem de columnlar ustunde ayni anda islem yapabiliriz

apply(mat, c(1, 2), function(x) x^2)
##      [,1] [,2] [,3]
## [1,]    1   16   49
## [2,]    4   25   64
## [3,]    9   36   81

Bu islemi yapmak icin normalde for loopu yazip butun columnlara bakmamiz gerekiyordu. Ayni islemi apply kullanarak ne kadar basit bir sekilde yapabildigimizi goruyoruz.

mat <- matrix(1:9, nrow = 3)

row_sums <- numeric(nrow(mat))
col_means <- numeric(ncol(mat))

for (i in 1:nrow(mat)) {
  row_sums[i] <- sum(mat[i, ])
}

for (j in 1:ncol(mat)) {
  col_means[j] <- mean(mat[, j])
}
print(row_sums)
## [1] 12 15 18
print(col_means)
## [1] 2 5 8

Bu islemi data frameler ustunde de yapmak mumkun ancak apply() dataframelere matris gibi davranacagi icin bazen beklenmedik sorunlar olusturabilir. Matrisler tek veri tipini tuttugu icin eger dataframe icinde karakter varsa butun veri karakter tipine donusur ve islem yapamayiz. Benzer sekilde veride faktorler varsa faktoerler seviyelerine gore sayiya donusur ve istemedigimiz sonuclar dogurabilir. Ondan dolayi apply() komutlarini kullanirken dikkatli olmak veya sadece matrisler ile kullanmak gerekir.Dataframelerde ise lapply ve sapply komutlari daha yaygin olarak kullanilir.

lapply() yapi olarak apply ile ayni olmasina ragmen vektorlere veya listelere uygulanir ve sonuc olarak her zaman sonuc olarak liste verir. Biz input olarak vektor versek bile ciktiyi liste olarak aliriz. Bu icine konulan verilerin formatini korumasini saglarken, listeler cok rahat calisilan bir veri tipi olmadigi icin ek islem gerektirecektir.

list_data <- list(a = 1:5, b = 6:10)
lapply(list_data, sum)
$a
[1] 15

$b
[1] 40

Bu ornekte gordugumuz gibi lapply liste icindeki her element ustunde islem yapti. Bunu for loop ile yapmak icin bu kodu kullanmamiz gerekirdi. Basit dursa bile hem hiz hem de okunurluk olarak lapply daha iyi bir secenek.

result <- list()
for (name in names(list_data)) {
  result[[name]] <- sum(list_data[[name]])
}

lapply() ayni zamanda dataframe columnlari ustunde calisacaksak isimizi daha da kolaylastiriyor. Zaten colmunlar esit sayida veri iceren listeler gibi dusunulebilecegi icin listeler ile calisan bir komutun, columnlarda ise yaramasi gayet dogal.

df_lapply <- data.frame(x = 1:5, y = 6:10, z = 11:15)
column_means <- lapply(df_lapply, mean)
print(column_means)
$x
[1] 3

$y
[1] 8

$z
[1] 13

sapply() ise lapply() komutunun daha basitlestirilmis bir sonuc vermeyi amaclayan kardesidir. Mumkun oldugu durumlarda sonucu matris veya vektor olarak vermeye calisir. Ek olarak simplify argumani alir ve eger TRUE olarak secersek sonucu eger mumkunse matrise veya vektore donusturmeye calisir. Bu arguman icin default olan giris TRUE oldugu icin degistirmeye gerek yok.

numbers <- c(1, 2, 3, 4, 5)
squares_sapply <- sapply(numbers, function(x) x^2)
squares_lapply <- lapply(numbers, function(x) x^2)
print(squares_sapply)
[1]  1  4  9 16 25
print(squares_lapply)
[[1]]
[1] 1

[[2]]
[1] 4

[[3]]
[1] 9

[[4]]
[1] 16

[[5]]
[1] 25

Oncelikle ornekte dikkat cekecek bir nokta functionin ne oldugunu belirtmedik. Komutun icinde function deyip ne ise yaradigini yazdigimiz durumlara anonymous function denir.

Bu ornekte gordugumuz uzere sapply bize vektor sonucu verirken lapply bize liste verdi. Bu ornek gibi durumlarda sapply daha avatanjli oluyor. Ancak bazen sapply hata yapip istemedigimiz bir formata sokabilir ve eger consistent data almak bizim icin onemliyse lapply daha iyi bir secenek oluyor.

sapply komutunun bu gibi durumlarinin onune gecmek icin ise vapply() kullaniriz. Bu kod icin ciktimizi hangi tipte istedigimizi belirtmemiz gerekiyor. Bu da cikti tipinde beklenmedik sorunlarin onune gecmemizi sagliyor. Ancak cikti tipi belirtimiz ve girdigimiz veri arasinda uyumsuzluk olursa error aliriz. Bunu digerlerinin ustune ek koruma gerektiren bir komut olarak gorebiliriz. Eksi yani daha uzun kod yazmamis.

number_vapply <- list(1:5, 6:10, 11:15)
result_vapply <- vapply(number_vapply, mean, numeric(1))
print(result_vapply)
[1]  3  8 13

Ayrica vapply bizden kac tane numeric deger cikacagini bilmemizi istiyor. Yani biz meanin yanina baska bir deger daha ekleseydik bu sefer 2 sayi cikacagi icin numeric olarak da 2 yazmamiz gerekecek. Bu durumdaysa komut vektor yerine dataframe uretecek.

result_vapply2 <- vapply(number_vapply, 
                         function(x) c(mean = mean(x), 
                                       sd = sd(x)), 
                         numeric(2))
print(result_vapply2)

Kisacasi eger ustunde calistigimiz dataya hakimsek ve consistent sonuclar istiyorsak vapply kullanmalaiyiz ama bunun disinda sapply ve lapply yeterli olacaktir.

sapply verilen veriyi en basit formata donusturmeye calisiyordu. Bu ornekte gorulecegi uzere cift olan sayilari oldugu gibi tek olan sayilari ise karakter olarak print et dedik. sapply bize liste yerine vektor olarak verdi ancak vektorler tek veri tipinden olusabilecegi icin numeric elementleri de karaktere donusturdu.

sapply(1:3, function(x) if(x %% 2 == 0) x 
       else as.character(x))
[1] "1" "2" "3"

Bunu engellemek icin default olarak TRUE olan simplify FALSE yapilabilir ancak o zaman lapply ile ayni komutu vermis ve gereksizce kodu uzatmis oluruz.

sapply(1:3, function(x) if(x %% 2 == 0) x 
       else as.character(x), simplify = FALSE)
## [[1]]
## [1] "1"
## 
## [[2]]
## [1] 2
## 
## [[3]]
## [1] "3"

mapply ise sapply komutunun birden fazla degisken arasinda islem yapmaya yarar. Mesela basit bir ornek olarak vektorler arasinda toplama islemi yapmak icin kullanabiliriz.

vec1 <- c(1, 2, 3)
vec2 <- c(4, 5, 6)
mapply(sum, vec1, vec2)
[1] 5 7 9

Ancak ayni islemi bu komutlari kullanamdan cok daha basitce yapmamiz da mumkun.

result <- vec1 + vec2
print(result)
[1] 5 7 9

Ancak R’in kendi islem yetenekleri sinirli ve komplike islemleri yukarida gordugumuz gibi basitce yapamayiz. Bu noktada fonkisyonlara ve onlari kullanmak icin mapply kullanimina ihtiyacimiz var. Bu da bizim birden cok nested loop kullanmaktan kurtarir.

multiply_add <- function(x, y) {
  return((x * y) + x + y)
}
result <- mapply(multiply_add, vec1, vec2)
print(result)
[1]  9 17 27

rapply ozellikle nested list yapisi ile calisirken isimize yarar. Nested listler karmasik olduklari ve genellikle farkli veri tiplerini de icerdikleri icin onlar ustunde islem yapmak karmasik olabilir ama rapply ile bu sorunlarin ustesinden geliyoruz.

Burada diger tiplerde bulunmayan iki arguman var. Birisi classes. Bu rapply isleminin hangi veri tipine uygulanmasini istedigimizi soruyor. Ikinci olaraksa how var. Burada da ciktimizi nasil almak istedigimizi belirtiyoruz. Bunun uc argumani var. Default olarak unlist degerini alir Unlist kullandigimiz zaman elemanlari liste disina cikarir ve ayni tipte olanlari vektor olarak tutar.

mixed_list <- list(1, "a", 2, "b", 
                   list(3, "c", TRUE, 4.5))

result_rapply1 <- rapply(mixed_list, 
                         function(x) x + 1, 
                         classes = "numeric", 
                         how = "list")
print(result_rapply1)
## [[1]]
## [1] 2
## 
## [[2]]
## NULL
## 
## [[3]]
## [1] 3
## 
## [[4]]
## NULL
## 
## [[5]]
## [[5]][[1]]
## [1] 4
## 
## [[5]][[2]]
## NULL
## 
## [[5]][[3]]
## NULL
## 
## [[5]][[4]]
## [1] 5.5

List yaparsak sadece ustunde islem yaptigimiz elemanlari liste olarak cikarir ve digerleri null degerini alir. Replace yaparsak da orijinal listedeki elemanlari islem yaptiklarimiz ile degistirir ve formati korur.

Bunu kullaninca liste yapisini koruyarak islem yapariz ve cikti olarak liste aliriz.

result_rapply2 <- rapply(mixed_list, 
                         function(x) x + 1, 
                         classes = "numeric", 
                         how = "replace")
print(result_rapply2)
[[1]]
[1] 2

[[2]]
[1] "a"

[[3]]
[1] 3

[[4]]
[1] "b"

[[5]]
[[5]][[1]]
[1] 4

[[5]][[2]]
[1] "c"

[[5]][[3]]
[1] TRUE

[[5]][[4]]
[1] 5.5

Eger basit bir liste ile calisiyor olsaydik classes argumani kullanmamiza gerek olmayacakti

my_list <- list(1, 2, 3, 4, 5)
result <- rapply(my_list, function(x) x^2, 
                 how = "replace")
print(result)
[[1]]
[1] 1

[[2]]
[1] 4

[[3]]
[1] 9

[[4]]
[1] 16

[[5]]
[1] 25

tapply ise gruplara bolunmus bir veride fonksiyonu gruplari baz alarak her grup icin yapmaya yarar. Mesela sadece bir cinsiyet veya ulke icin bir degisiklik yapmak istiyorsak bu komut ile rahat bir sekilde yapabiliriz.

tapply_data <- data.frame(
  id = 1:10,
  score = c(88, 92, 85, 91, 78, 85, 
            95, 89, 77, 82),
  group = c("A", "B", "A", "B", "A", 
            "A", "B", "B", "A", "B") 
)
mean_scores <- tapply(tapply_data$score, tapply_data$group, mean)
print(mean_scores)
   A    B 
82.6 89.8 

Bu ornekte goruldugu uzere basit bir komutla hem A hem de B grubu icin ayri ayri ortalama hesapladik. Bu komut icinde ilk argumaninimiz hesaplamanin hangi degiskende yapilacagi, ikincisi gruplarin ne oldugu, ucuncusu de hangi islemin yapilacagi olacak.

Elimizde iki grup varsa bir list seklinde fonksiyonun icine koyabiliriz.

gender <- c("M", "F", "M", "F", "M", "F", 
            "F", "M", "M", "F")
tapply_data$gender <- gender
mean_scores_by_group_gender <- tapply(tapply_data$score, 
list(tapply_data$group, tapply_data$gender), mean)

print(mean_scores_by_group_gender)
   F  M
A 85 82
B 90 89

Purr

Bir tidyverse paketi olan purr bu tarz islemlere daha hizli ve tidyverse dunyasinin diger paketleri ile de uygun bir cozum getirmesi amaciyla ortaya cikti. Onemli bir kullanici kitlesine sahip ve gercekten cok guclu islemleri yapabiliyor. Temel olarak kodun okunurlugunu arttiriyor ve looplara gore daha basit bir kod yazmamizi sagliyor.

Eger tidyverse yuklediysek paketi dogrudan cagirabiliriz.

library(purrr)

map() komutu purr paketinin temelini olustutur. Cok temel olarak bu sekilde kullanabiliriz.

map(1:5, sqrt)
[[1]]
[1] 1

[[2]]
[1] 1.414214

[[3]]
[1] 1.732051

[[4]]
[1] 2

[[5]]
[1] 2.236068

Bu paket icinde anonymous functionlari kullanmak yerine ~ kullanarak da fonkisyonlari belirleyebiliriz.

map(1:5, function(x) x^2)
[[1]]
[1] 1

[[2]]
[1] 4

[[3]]
[1] 9

[[4]]
[1] 16

[[5]]
[1] 25
map(1:5, ~.x^2)
[[1]]
[1] 1

[[2]]
[1] 4

[[3]]
[1] 9

[[4]]
[1] 16

[[5]]
[1] 25

Gorukdugu uzere ~ kullanmak kodu basitlestiriyor. ANCAK sonrasinda .x veya .y gibi bir deger ile fonskiyonu tanimlamamiz gerekir yoksa error aliriz. Purr paketi kullanabilecegimiz placeholderlari sinirliyor. map komutlarinin temelini ~ ve sonrasinda belirtecegimiz .x gibi degiskenler olusturuyor.

map komutunun farkli varyantlari degisik veri tiplerini almak icin kullanilir. Bunlarin apply ailesine gore tercih edilme sebebi cikti ustunde kontrol sahibi olunmasi ve kodun amacinin ilk bakista daha rahat anlasilmasidir.

map_dbl(1:5, sqrt) #double almak icin
[1] 1.000000 1.414214 1.732051 2.000000 2.236068
map_int(1:5, ~.x * 2) #integer almak icin
[1]  2  4  6  8 10
map_chr(1:5, ~paste0("Number ", .x)) #karakter almak icin
[1] "Number 1" "Number 2" "Number 3" "Number 4" "Number 5"
map_lgl(1:5, ~.x %% 2 == 0) #boolean almak icin
[1] FALSE  TRUE FALSE  TRUE FALSE

map2() ayni uzunluga sahip iki vektor veya liste ustunde islem yapmak istedigimiz zaman kullanilir.

vec1 <- c(1, 2, 3)
vec2 <- c(4, 5, 6)

result <- map2(vec1, vec2, ~ .x + .y)
result
[[1]]
[1] 5

[[2]]
[1] 7

[[3]]
[1] 9

Burada gordugunuz gibi iki vektoru daha onceden yaptigimiz sekilde topladik. Elbette bu cok basit bir ornek ve temel R komnutlari ile cok daha kolayca yapabiliriz. Ancak su an ogrenmedigimiz daha komplike durumlara gelinince ise yarayacaktir.

pmap() ise liste veya vektor sayisi ikinin ustune ciktigi durumlarda kullanilir.

uzunluklar <- c(2, 3, 4)
genislikler <- c(3, 4, 5)
yukseklikler <- c(4, 5, 6)
hacim <- function(uzunluk, genislik, yukseklik) {
  uzunluk * genislik * yukseklik
}
hacimler <- pmap(list(uzunluklar, genislikler, 
                      yukseklikler), hacim)
hacimler
[[1]]
[1] 24

[[2]]
[1] 60

[[3]]
[1] 120

Her ne kadar pmap, map2 ile ayni isi yapabilse de hem hiz hem de kodun anlasilirligi acisindan iki elemanli durumlarda map2 kullanmak, ikinin ustune ciktigimiz zamansa pmap kullanmak daha mantiklidir.

imap() bir elementi ve onun liste veya vektor icindeki endeksini istedigimiz zaman kullanilir

imap(c("a", "b", "c"), ~paste0(.y, ": ", .x))
[[1]]
[1] "1: a"

[[2]]
[1] "2: b"

[[3]]
[1] "3: c"

flatten ise nested listler ustunde calismamiza yardimci olur. nested listerlerin nested yapisindan kurtararak duz bir liste haline gelmesini saglar.

nested_list <- list(
  list(1, 2, 3),
  list(4, 5, 6),
  list(7, 8, 9)
)
flattened_list <- flatten(nested_list)
print(flattened_list)
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

[[4]]
[1] 4

[[5]]
[1] 5

[[6]]
[1] 6

[[7]]
[1] 7

[[8]]
[1] 8

[[9]]
[1] 9

transpose komutu ise veriyi donusturmeye yarar. Mesela her kisinin ayri bir liste olarak alindigi nested listi incleyelim. Eger sadece yas veya isim ile calismamiz gereken bir durum varsa verinin bu sekilde olmasi pek isimize gelmez ve her degeri bir degisken altinda toplamak isteriz. transpose() bunu sagliyor.

kisiler <- list(
  list(isim = "Ece", yas = 28, cinsiyet = "Kadin"),
  list(isim = "Zeynep", yas = 27, cinsiyet = "Kadin"),
  list(isim = "Onur", yas = 30, cinsiyet = "Erkek")
)
transposed_kisiler <- transpose(kisiler)
transposed_kisiler
$isim
$isim[[1]]
[1] "Ece"

$isim[[2]]
[1] "Zeynep"

$isim[[3]]
[1] "Onur"


$yas
$yas[[1]]
[1] 28

$yas[[2]]
[1] 27

$yas[[3]]
[1] 30


$cinsiyet
$cinsiyet[[1]]
[1] "Kadin"

$cinsiyet[[2]]
[1] "Kadin"

$cinsiyet[[3]]
[1] "Erkek"

Boylece her deger kendine ait degiskenin altina girmis oldu ve veriyle daha rahat calisabiliriz.

keep() ve discard () komutlari ise onumuzdeki hafta isleyecegimiz veri manipulasyonu ve temizlemesi islerine yariyor. Verinin bir kismini elimizde tutmamiz veya bir kismindan kurtulmamiz gerekirse bunu kullaniriz.

sayilar <- list(1, 2, 3, 4, 5, 6)
keep(sayilar, ~.x %% 2 == 0)
[[1]]
[1] 2

[[2]]
[1] 4

[[3]]
[1] 6
discard(sayilar, ~.x %% 2 == 0)
[[1]]
[1] 1

[[2]]
[1] 3

[[3]]
[1] 5

compact komutu ise yine ileride isleyecegimiz konulardan olan bos degerlerden kurtulmaya yarar. ANCAK bizim asil kurtulmak istedigimiz NA degerlerden kurulmaya yaramaz cunku NULL yokluk anlamina gelirken NA eksik veya tanimsiz degerler icin de kullanilabilinir. Zaten ileride bu konuya deginiyoruz.

mixed_list <- list(a = 1, b = NULL, c = 3, 
                   d = NULL, e = NA)
compact(mixed_list)
$a
[1] 1

$c
[1] 3

$e
[1] NA

some(), every() ve none () ise kondisyonlarin listenin veya vektorun elemanlari tarafinan karsilanip karsilanmadigina bakar

some(1:10, ~.x > 5)
[1] TRUE
every(1:10, ~.x > 5)
[1] FALSE
none(1:10, ~.x > 5)
[1] FALSE

En az birinin karsilanmasi bizim icin tamamsa some, hepsinin karsilanmasi gerekiyorsa da every kullaniriz. Eger hicbirinin karsilamasini istemiyorsak da none.

detect ve detect_index istedgimiz kousulu saglayan ilk sayiyi alabilir veya o sayinin vektor veya liste icindeki endeksini alabiliriz.

set.seed(2024)
random_sayilar <- sample(1:100, 10)
detect(random_sayilar, ~.x %%5 == 0)
[1] 45
detect_index(random_sayilar, ~.x  %%5 == 0)
[1] 3

map_if() bir liste veya vektor ustunden bazi sartlara bagli olarak islem yapmak istedigimiz zaman kullanilir.

sayi_liste <- list(1, "a", 3, "b", 5, 8, 11)
result <- map_if(sayi_liste, is.numeric, ~ .x ^ 2)
print(result)
[[1]]
[1] 1

[[2]]
[1] "a"

[[3]]
[1] 9

[[4]]
[1] "b"

[[5]]
[1] 25

[[6]]
[1] 64

[[7]]
[1] 121

Burada map_if sonrasi verdigimiz argumanlardan ilki hangi veriseti ustune calisacagimiz, ikincisi islemin yapilacagi verileri saglayan sartlar, ucuncusu de hangi islemi yapacagimizdir.

map_at ise listelerde ya isme ya da endekse gore islem yapabilmemize yarar. Burada 6 ve 7 endeks olduklari icin o sekilde belirttim.

result2 <- map_at(sayi_liste, c(6,7), ~ .x ^ 2)
print(result2)
[[1]]
[1] 1

[[2]]
[1] "a"

[[3]]
[1] 3

[[4]]
[1] "b"

[[5]]
[1] 5

[[6]]
[1] 64

[[7]]
[1] 121

map komutlarina bir alternatif ise modify komutlaridir. Bunlar map komutlarinin aksine yeni veri olusturmak yerine elimizdeki ustunde calisirlar ve yeni liste olusturmadan verdigimiz liste ustunde degisiklik yaparlar. ANCAK bu degisiklik biz degiskene atamadiysak otomatik olarak kaydedilmez.

numbers_to_modify <- list(1, 2, 3, 4)
modify(numbers_to_modify, ~ .x * 2)
## [[1]]
## [1] 2
## 
## [[2]]
## [1] 4
## 
## [[3]]
## [1] 6
## 
## [[4]]
## [1] 8
numbers_to_modify
## [[1]]
## [1] 1
## 
## [[2]]
## [1] 2
## 
## [[3]]
## [1] 3
## 
## [[4]]
## [1] 4

Bu tipi map kadar kullanmayiz ancak map hep liste formatini dondurdugu icin modify verinin orijinal yapisini koruma konusunda yardimci olur.

test_numbers <- c(2,3,4,5)
map_deneme <- map(test_numbers, ~.x^2)
map_deneme
## [[1]]
## [1] 4
## 
## [[2]]
## [1] 9
## 
## [[3]]
## [1] 16
## 
## [[4]]
## [1] 25

bu sonuc vektor girmemize ragmen bize liste verirken

modify_deneme <- modify(test_numbers, ~.x^2)
modify_deneme
## [1]  4  9 16 25

bize istedigimiz gibi vektor veriyor. Boylece veri tipimizi koruyabiliyoruz.

compose birden cok fonksiyonu sirali olarak kullanmak istedigimiz zamanlarda ise yarar. Boylece birden cok islem yapmak yerine elimizde hazir bulunan functionlar ile isimizi yapabiliriz.

double <- function(x) x * 2
add_one <- function(x) x + 1
compose_function <- compose(add_one, double)
compose_function(random_sayilar)
 [1] 133  75  91 121  35  65  59  23  33 189

SON