19 Fonksiyonlar

19.1 Giriş

Bir veri bilimci olarak menzilinizi geliştirmenizin en iyi yollarından biri fonksiyon yazmaktır. Fonksiyonlar genel geçer işleri kopyala-yapıştırdan daha güçlü ve daha genel bir yolla yapmanıza olanak verir. Fonksiyon yazmanın kopyala-yapıştıra göre üç büyük avantajı vardır:

  1. Fonksiyona çağırışım yapan bir isim vererek kodunuzun daha iyi anlaşılmasını sağlayabilirsiniz.

  2. Gereksinimleriniz değiştikçe kodu birçok yerde güncellemektense sadece bir yerde güncellemeniz yeterlidir.

  3. Kopyala-yapıştır yaptığınızda ortaya çıkabilecek hataları (örn. birdeğişkenin adını bir yerde değiştirip, başka bir yerde değiştirmeyi unutmak gibi) yapma ihtimalinizi azaltmış olursunuz.

İyi fonksiyon yazmak ömürlük bir yolculuktur. R kullanmaya başladıktan yıllar sonra bile hala eski sorulara farklı şekillerde yaklaşmamı sağlayan yeni teknikler ve daha iyi yollar öğreniyorum. Bu bölümün amacı size fonksiyonların bütün kıyıda köşede kalmış ayrıntılarını öğretmek değil hemen kullanabileceğiniz birkaç pragmatik tavsiye vermektir.

Pratik tavsiyeler vermenin yanı sıra bu bölüm kodunuzu nasıl tasarlayacağınız ile ilgili birtakım önerilerde de bulunmaktadır. İyi bir kod stili doğru imla gibidir. Onsuz da yapabilirsiniz ama kodu okumanız ona bağlı olarak zorlaşır ya da kolaylaşır. Burada kendi kodumuzda kullandığımız stili sunuyoruz ancak şunu unutmayın ki en önemlisi tutarlı olmaktır.

19.1.1 Ön koşullar

Bu bölümün amacı Temel R’da fonksiyon yazmaktır; o nedenle ilave paketlere ihtiyacınız olmayacak.

19.2 Ne zaman fonksiyon yazmalısınız?

Bir kod bloğunu ikiden fazla kez kopyalayıp yapıştırdıysanız (yani elinizde aynı kodun üç kopyası varsa) bir fonksiyon yazmayı düşünmeye başlamanız gerekiyor demektir. Örneğin şu kodun ne yaptığına bir bakalım:

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

df$a <- (df$a - min(df$a, na.rm = TRUE)) / 
  (max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE))
df$b <- (df$b - min(df$b, na.rm = TRUE)) / 
  (max(df$b, na.rm = TRUE) - min(df$a, na.rm = TRUE))
df$c <- (df$c - min(df$c, na.rm = TRUE)) / 
  (max(df$c, na.rm = TRUE) - min(df$c, na.rm = TRUE))
df$d <- (df$d - min(df$d, na.rm = TRUE)) / 
  (max(df$d, na.rm = TRUE) - min(df$d, na.rm = TRUE))

Bunun her sütunu yeniden ölçeklendirerek 0 ile 1 arasında bir değer almasını sağladığını görmüş olmalısınız. Peki hatayı da gördünüz mü? df$b kodunu kopyalayıp yapıştırırken bir hata yaptım: Bir a’yı b’ye değiştirmeyi unuttum. Tekrarlı bir kodu bir fonksiyona dönüştürmek iyi bir fikirdir çünkü böyle bir hata yapmanızı engeller.

Bir fonksiyon yazmak için öncelikle kodu analiz etmelisiniz. Kodda kaç tane girdi bulunuyor?

(df$a - min(df$a, na.rm = TRUE)) /
  (max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE))

Bu kodda sadece bir girdi bulunuyor: df$a .(Eğer TRUE’nun bir girdi olmadığına şaşırdıysanız, bunun nedenini aşağıdaki alıştırmada bulmaya çalışın.) Girdileri daha açık bir hale getirmek için genel isimlere sahip geçici değişkenler kullanarak kodu yeniden yazmak iyi bir fikirdir. Buradaki kod sadece tek bir nümerik vektör gerektiriyor, o yüzden buna x diyeceğim:

x <- df$a
(x - min(x, na.rm = TRUE)) / (max(x, na.rm = TRUE) - min(x, na.rm = TRUE))
#>  [1] 0.289 0.751 0.000 0.678 0.853 1.000 0.172 0.611 0.612 0.601

Bu kodda bir miktar tekrar bulunuyor. Verinin aralığını (range) üç kere hesaplıyoruz; bunu tek bir basamakta yapabiliriz:

rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
#>  [1] 0.289 0.751 0.000 0.678 0.853 1.000 0.172 0.611 0.612 0.601

Ara hesaplamaları isim verilmiş değişkenlere çekmek iyi bir egzersizdir çünkü bu kodun ne işe yaradığını daha anlaşılır bir hale getirir. Şimdi kodu basitleştirdiğime ve hala çalışır durumda olup olmadığını kontrol ettiğime göre artık onu bir fonksiyona çevirebilirim:

rescale01 <- function(x) {
  rng <- range(x, na.rm = TRUE)
  (x - rng[1]) / (rng[2] - rng[1])
}
rescale01(c(0, 5, 10))
#> [1] 0.0 0.5 1.0

Yeni bir fonksiyon yaratmanın üç temel basamağı vardır:

  1. Fonksiyon için bir isim seçmeniz gerekir. Ben burada rescale01 kullandım çünkü bu fonksiyon bir vektörü 0 ile 1 arasında olacak şekilde yeniden ölçeklendirmektedir.

  2. Girdileri ya da argümanları function içindeki fonksiyonda listelersiniz. Burada tek bir argümanımız bulunuyor. Eğer daha fazla argümanımız olsaydı çağrı şöyle bir şeye benzeyecekti: function(x, y, z)

  3. Geliştirdiğiniz kodu fonksiyonun gövdesine yerleştirirsiniz. Bu function(...)’nun hemen arkasından gelen bir bloktur {.

Tüm sürece dikkat edin: Fonksiyonu ancak basit bir girdi ile nasıl çalıştıracağımı anladıktan sonra yaptım. Kod üzerinde çalışarak başlamak daha kolaydır. Önce fonksiyonu oluşturup sonra onu çalışır hale getirmek daha zordur.

Geldiğimiz bu noktada fonksiyonunuzu birkaç farklı girdi ile denemek iyi bir fikirdir:

rescale01(c(-10, 0, 10))
#> [1] 0.0 0.5 1.0
rescale01(c(1, 2, 3, NA, 5))
#> [1] 0.00 0.25 0.50   NA 1.00

Fonksiyon yazdıkça bu gayrı resmi interaktif sınamaları resmi ve otomatik sınamalara dönüştürmek isteyeceksiniz. Bu sürece birim sınama denir. Bu ne yazık ki bu kitabın kapsamı dışında kalıyor ama bunun hakkında http://r-pkgs.had.co.nz/tests.html adresinde bir şeyler öğrenebilirsiniz.

Bir fonksiyonumuz olduğuna göre artık orijinal örneği basitleştirebiliriz:

df$a <- rescale01(df$a)
df$b <- rescale01(df$b)
df$c <- rescale01(df$c)
df$d <- rescale01(df$d)

Orijinale kıyasla bu kodun anlaşılması daha kolaydır ve burada kopyala-yapıştırdan kaynaklanabilecek bir hata sınıfını bertaraf etmiş oluyoruz. Ancak hala biraz tekrar bulunuyor çünkü sütunları çarpmak için aynı şeyi yapıyoruz. Vektörler kısmında R’ın veri yapısı hakkında daha fazla şey öğrendikten sonra [İterasyonlar] kısmında bu tekrardan nasıl kurtulacağımızı öğreneceğiz.

Fonksiyonların bir diğer avantajı da ihtiyacımız değiştiğinde sadece bir yerde değişiklik yapmamızın yeterli olmasıdır. Örneğin değişkenlerimizin bazılarının sonsuz değerler içerdiğini ve bu nedenle rescale01()’in çalışmadığını tespit edersek:

x <- c(1:10, Inf)
rescale01(x)
#>  [1]   0   0   0   0   0   0   0   0   0   0 NaN

Kodu bir fonksiyona dönüştürdüğümüzden, sadece bir yerde düzenleme yapmamız yeterli olacak:

rescale01 <- function(x) {
  rng <- range(x, na.rm = TRUE, finite = TRUE)
  (x - rng[1]) / (rng[2] - rng[1])
}
rescale01(x)
#>  [1] 0.000 0.111 0.222 0.333 0.444 0.556 0.667 0.778 0.889 1.000   Inf

Bu “kendini tekrar etme” (ya da KTE) kuralının önemli bir kısmıdır. Kodunuzda ne kadar çok tekrar olursa bir takım şeyler değiştiğinde (ki her zaman değişen bir şeyler olur) güncellemeyi hatırlamanız gereken o kadar çok yer olacaktır ve zaman içerisinde bir takım hatalar (bug) yaratma ihtimaliniz de artacaktır.

19.2.1 Alıştırmalar

  1. Neden TRUE, rescale01() için bir paramtere değildir? Eğer x’te eksik bir değer olsaydı ve na.rm, FALSE olsaydı ne olurdu?

  2. rescale01()’in ikinci halinde sonsuz değişkenler değiştirilmeden bırakılmıştır. rescale01()’i -Inf 0’a ve Inf ise 1’e eşlenecek şekilde yeniden yazınız.

  3. Aşağıdaki kod parçalarını kullanarak fonksiyonlar üretiniz. Her bir fonksiyonun ne işe yaradığı hakkında düşünün. Bunlara ne isim veririsiniz? Kaç tane argümana ihtiyaçları var? Daha iyi ifade edici ya da daha az tekrarlı olacak şekilde yeniden yazabilir misiniz?

    mean(is.na(x))
    
    x / sum(x, na.rm = TRUE)
    
    sd(x, na.rm = TRUE) / mean(x, na.rm = TRUE)
  4. http://nicercode.github.io/intro/writing-functions.html adresini inceleyerek nümerik bir vektörün varyans ve eğikliğini hesaplamak için kendi fonksiyonlarınızı yazınız.

  5. Aynı uzunluktaki iki vektörü alıp her iki vektörde NA içeren konum sayısını veren bir both_na() fonksiyonu yazınız.

  6. Aşağıdaki fonksiyonlar ne işe yarar? Çok kısa olsalar da neden kullanışlıdırlar?

    is_directory <- function(x) file.info(x)$isdir
    is_readable <- function(x) file.access(x, 4) == 0
  7. “Little Bunny Foo Foo” (18. Bölüm) şarkısının tüm sözlerini okuyunuz. Şarkıda çok fazla tekrar bulunmaktadır. Başlangıçtaki küme komut işleme (piping) örneğini genişleterek tüm şarkıyı yeniden oluşturunuz ve tekrarları azaltmak için fonksiyon kullanınız.

19.3 İnsanlar ve bilgisayarlar için fonksiyonlar

Fonksiyonların sadece bilgisayarlar için değil aynı zaman da insanlar için de olduğunu unutmamak gerekir. R fonksiyonunuzun adının ne olduğu ya da içerdiği yorumlar ile ilgilenmez ancak bunlar insan okurlar açısından önemlidir. Bu bölümde insanların anlayabileceği fonksiyonlar yazmak için aklınızda tutmanız gereken birkaç şeyi tartışmaktadır.

Bir fonksiyonun adı önemlidir. İdeal bir fonksiyon adı kısa olmalı ancak fonksiyonun tam olarak ne yaptığını açık bir şekilde ifade edebilmelidir. Bu kolay bir iş değildir! Ancak RStudio’daki otomatik tamamlama uzun isimlerin yazımını kolaylaştırdığından netlik kısa olmasından daha iyidir.

Genellikle fonksiyon isimleri fiil, argümanlar ise isim olmalıdır. Bunun bazı istisnaları vardır: Fonksiyon iyi bilinen bir ismi hesaplıyorsa (mean(),compute_mean()’den daha iyidir) ya da bir objenin bir özelliğine erişim sağlıyorsa (coef() get_coefficients()’ten daha iyidir) isimler kullanılabilir. Bir isim kullanmanın daha iyi olabileceğine ilişkin bir işaret olarak fiilin çok geniş bir kullanıma sahip bir fiil olması alınabilir. Örneğin “get”, “compute”, “calculate” ya da “determine” bu tip fiillerdir. Değerlendirmenizi iyi bir şekilde yapın ve eğer sonrada daha iyi bir isim bulursanız fonksiyonunuzun ismini değiştirmekten korkmayın.

# Fazla kısa
f()

# Fiil de değil, tanımlayıcı da...
my_awesome_function()

# Uzun ama anlaşılır
impute_missing()
collapse_years()

Fonksiyonunuzun ismi çok sayıda kelimeden oluşuyorsa “yılan_şekli”ni kullanmanızı tavsiye ederim. Bunda küçük harflerden oluşan her bir kelime bir alt çizgi ile birbirinden ayrılır. “deveŞekli” ise popüler bir alternatiftir. Hangisini kullandığınızın pek bir önemi yoktur; önemli olan tutarlı olmaktır. Birini seçin ve hep bunu kullanın. R’ın kendisi pek tutarlı değildir ama bu konuda yapabileceğiniz bir şey yok. Kodunuzu olabildiğince tutarlı yaparak aynı tuzağa düşmemeye çalışın.

# Bunu asla yapmayın!
col_mins <- function(x, y) {}
rowMaxes <- function(y, x) {}

Benzer şeyleri yapan bir fonksiyon aileniz varsa tutarlı isim ve argümanlarının olmasına dikkat edin. Bunların birbiriyle alakalı olduğunu belirtmek için ortak bir ön ek kullanın. Ön ek son ekten daha iyidir çünkü otomatik tamamlama ön eki yazdığınızda ailedeki tüm fonksiyonları görmenizi sağlar.

# İyi
input_select()
input_checkbox()
input_text()

# O kadar iyi değil
select_input()
checkbox_input()
text_input()

Bu tasarıma iyi bir örnek olarak stringr paketi verilebilir: Hangi fonksiyona ihtiyacınız olduğunu tam olarak hatırlamıyorsanız str_ yazarak hafızanızı tazeleyebilirsiniz. Mümkünse mevcut fonksiyon ve değişkenlerin üzerine kaydetmekten kaçının. Bunu genele olarak yapmak mümkün değildir çünkü birçok iyi isim diğer paketler tarafından alınmıştır ama temel R’daki en yaygın isimlerden kaçınmak kafa karışıklığının önüne geçecektir.

# Don't do this!
T <- FALSE
c <- 10
mean <- function(x) sum(x)

Kodunuzun ne işe yaradığını açıklamak için # ile başlayan satırlar açarak yorumlar yazın. “ne” ve “nasıl”dan ziyade “niçin”i açıklamalısınız. Okuduğunuzda kodun ne yaptığını anlamıyorsanız daha açık yazın. Belki kullanışlı isimleri olan bazı ara değişkenler eklemelisiniz. Acaba büyük fonksiyonu alt bileşenlerine ayırıp, bunlara isim vererek işleri biraz daha kolaylaştırabilir misiniz? Ancak, kodunuz kararlarınızın arkasındaki gerekçeyi asla yakalayamaz: Neden başka bir alternatifi değil de bu yaklaşımı seçtiniz? Başka neyi denediniz de çalışmadı? Bir yorumda düşüncenin akışını bu şekilde yansıtmak gerçekten de iyi bir fikirdir.

Yorumların diğer bir faydası dosyanızı okunabilir parçalara kolaylıkla bölebilmenizdir. – ve = kullanarak uzun çizgiler yapın ve böylece ayırmak istediğiniz yerleri belirtin.

# Veriyi yükle --------------------------------------

# Veriyi grafiğe dök --------------------------------------

Rstudio’da bulunan bir klavye kısa yolu ile bu başlıkları oluşturmak (Cmd/Ctrl + Shift + R) ve bunları editörün alt solunda bulunan kod navigasyon menüsünde görüntülemek mümkündür.

19.3.1 Alıştırmalar

  1. Aşağıdaki üç fonksiyonun her birinin kaynak kodunu okuyun, bunların ne yaptıklarını çözün ve daha sonlara bunlara daha iyi isimler bulmak için beyin fırtınası yapın.

    f1 <- function(string, prefix) {
      substr(string, 1, nchar(prefix)) == prefix
    }
    f2 <- function(x) {
      if (length(x) <= 1) return(NULL)
      x[-length(x)]
    }
    f3 <- function(x, y) {
      rep(y, length.out = length(x))
    }
  2. Yakınlarda yazmış olduğunuz bir fonksiyonu alın ve 5 dakika beyin fırtınası yaparak bu fonksiyona ve argümanlarına daha iyi bir isim düşünün.

  3. rnorm() ve MASS::mvrnorm() isimlerini karşılaştırın. Bunları nasıl daha tutarlı hale getirirsiniz?

  4. norm_r(), norm_d() vb.’nin rnorm(), dnorm()’dan daha iyi olduğı bir örnek verin. Bunun tersi için bir örnek verin.

19.4 Koşullu yürütme

if ifadesi bir kodu koşullu olarak yürütmenizi sağlar. Bu şöyle görünür:

if (kosul) {
  # kosul TRUE olduğunda kod yurutulur
} else {
  # kosul FALSE olduğunda kod yurutulur
}

if konusunda yardım almak için şöyle yazıyoruz: ? ?`if` Eğer deneyimli bir programlamacı değilseniz bu yardım pek de yardımcı olmayacak ama en azından temel düzeyde bilgi sahibi olabileceksiniz.

Şimdi if ifadesi kullanan basit bir fonksiyon yazalım. Bu fonksiyonun hedefi bir vektörün her bir elemanının isimlendirilip isimlendirilmediğini tanımlayan mantıksal bir vektör getirmektir.

has_name <- function(x) {
  nms <- names(x)
  if (is.null(nms)) {
    rep(FALSE, length(x))
  } else {
    !is.na(nms) & nms != ""
  }
}

Bu fonksiyon standart getirme kuralından yararlanır: Fonksiyon hesapladığı en son değeri getirir. Bu örnekte bu if ifadesinin iki dalından biridir.

19.4.1 Koşullar

condition ya TRUE ya da FALSE olarak bir değerlendirme yapmak zorundadır. Bu eğer bir vektörse bir uyarı mesajı alırsınız; eğer NA ise hata alırsınız. Kendi kodunuzda şu mesajlara dikkat edin:

if (c(TRUE, FALSE)) {}
#> Error in if (c(TRUE, FALSE)) {: the condition has length > 1

if (NA) {}
#> Error in if (NA) {: missing value where TRUE/FALSE needed

Birden fazla mantıksal ifadeyi birleştirmek için || (ya da) ve && (ve) kullanabilirsiniz. Bu operatörler “kısa devreli”dir: || ilk TRUE’yu görür görmez TRUE verir ve başka bir şey hesaplamaz. && ilk FALSE’u görür görmez FALSE verir. | ya da &’yi asla bir if ifadesinde kullanmamalısınız: Bunlar çok sayıda değere uygulanan vektörleştirilmiş işlemlerdir (bunları bu nedenle filter()’da kullanıyoruz). Mantıksal bir vektörünüz varsa, bunu tek bir değere düşürmek için any() ya da all() kullanabilirsiniz.

Eşitlik testi yaparken dikkatli olun. == vektörleştirilmiştir. Bunun anlamı birden fazla çıktı almanın kolay olmasıdır. Ya uzunluğun 1 olduğunu kontrol edin, değilse all() ya da any() kullanarak bire düşürün ya da vektörleştirilmiş olmayan identical()’ı kullanın. identical() çok katıdır: Her zaman ya tek bir TRUE ya da tek bir FALSE verir; ve tipleri dönüştürmez. Bu, tam sayıları ve gerçek sayıları karşılaştırırken dikkatli olmanız gerektiği anlamına gelir:

identical(0L, 0)
#> [1] FALSE

Ayrıca kayan noktalı sayılara karşı da dikkatli olmalısınız:

x <- sqrt(2) ^ 2
x
#> [1] 2
x == 2
#> [1] FALSE
x - 2
#> [1] 4.44e-16

Karşılaştırmalarda açıklandığı gibi, karşılaştırma için dplyr::near() kullanabilirsiniz.

Ve unutmayın ki x == NA işe yarar herhangi bir şey yapmaz!

19.4.2 Çoklu koşullar

Birden fazla if ifadesini birbirine bağlamak mümkündür:

if (this) {
  # bunu yap
} else if (that) {
  # baska bir sey daha yap
} else {
  # 
}

Ancak eğer çok uzun if serileri ortaya çıkarsa bunu yeniden yazmalısınız. switch() fonksiyonu iyi bir alternatiftir. Seçilen bir kodu konum ya da isim temelinde değerlendirmenizi sağlar.

#> function(x, y, op) {
#>   switch(op,
#>     plus = x + y,
#>     minus = x - y,
#>     times = x * y,
#>     divide = x / y,
#>     stop("Unknown op!")
#>   )
#> }

Uzun if ifadesi zincirlerini bertaraf etmek için kullanılan diğer bir fonksiyon ise cut()’tır. Sürekli değişkenleri kesikli hale getirmede kullanılır.

19.4.3 Kod stili

Hem if hem de function (neredeyse) her zaman küme parantezi {} ile gösterilir ve içeriği iki boşlukla satır başı yapılmalıdır. Bu sol kenar boşluğunu gözden geçirerek kodunuzdaki hiyerarşiyi daha kolay görmenizi sağlar.

Bir küme parantezi açıldığında içeriği parantezin açıldığı satırda devam etmemeli, yeni bir satıra geçilmelidir. Kapanış parantezi de else ile devam edilmiyorsa her zaman ayrı bir satırda olmalıdır. Küme parantezi içerisindeki kodu her zaman satır başı boşluğu vererek yazın.

# Iyi
if (y < 0 && debug) {
  message("Y negatiftir.")
}

if (y == 0) {
  log(x)
} else {
  y ^ x
}

# Kotu
if (y < 0 && debug)
message("Y negatiftir.")

if (y == 0) {
  log(x)
} 
else {
  y ^ x
}

Tek bir satıra sığabilecek çok kısa bir if ifadeniz varsa küme parantezini kullanmayabilirsiniz.

y <- 10
x <- if (y < 20) "Çok düşük." else "Çok yüksek."

Bunu sadece çok kısa if ifadeleri için öneririm. Yoksa tam formu okumak daha kolaydır:

if (y < 20) {
  x <- "Çok düşük." 
} else {
  x <- "Çok yüksek."
}

19.4.4 Alıştırmalar

  1. if ile ifelse() arasındaki fark nedir? Yardımı dikkatle okuyun ve temel farkları gösteren üç örnek verin.

  2. Günün hangi vaktinde olunduğuna bağlı olarak “günaydın”, “iyi günler” ya da “iyi akşamlar”
    diyen bir selamlama fonksiyonu yazın. (İpucu:lubridate::now()’a varsayılı olan bir zaman argümanı kullanın. Bu fonksiyonunuzu test etmenizi kolaylaştıracaktır.)

  3. Bir fizzbuzz fonksiyonu yapın. Bu fonksiyon girdi olarak tek bir sayı alsın. Eğer bu sayı üçe bölünebiliyorsa, fonksiyon “fizz” versin. Sayı beşe bölünebiliyorsa çıktı “buzz” olsun. Sayı üçe ve beşe bölünebiliyorsa, çıktı “fizzbuzz” olsun. B unların dışında fonksiyon sayının kendisini versin. Bu fonksiyon bir kod yazın ve kodun çalışıp çalışmadığını kontrol edin.

  4. cut()’ı aşağıdaki iç içe geçmiş if-else ifadelerini basitleştirmek için nasıl kullanabilirsiniz?

    if (temp <= 0) {
      "ayaz"
    } else if (temp <= 10) {
      "soğuk"
    } else if (temp <= 20) {
      "serin"
    } else if (temp <= 30) {
      "ılık"
    } else {
      "sıcak"
    }

    Eğer < yerine <= kullanmış olsaydım cut() çağrısını nasıl değiştirirdiniz? cut()’ın bu problem için diğer bir temel avantajı nedir? (İpucu: sicaklik’ta çok sayıda değeriniz olsaydı ne olurdu?)

  5. switch()’i nümerik değerlerle kullanırsanız ne olur?

  6. Aşağıdaki switch() çağrısı ne yapar? Eğer x “e” olursa ne olur?

    switch(x, 
      a = ,
      b = "ab",
      c = ,
      d = "cd"
    )

    Deneyin, sonra yeniden okuyun.

19.5 Fonksiyon argümanları

Fonksiyon argümanlarını kabaca iki grupta toplamak mümkündür: bir grup hesaplamanın yapılacağı veriyi sağlar, diğer grup ise hesaplamanın ayrıntılarını düzenler.

  • log()’da veri x’tir ve ayrıntı logaritmanın tabanıdır.

  • mean()’de veri x’tir ve ayrıntılar uçlardan ne kadar verinin kesileceği (trim) ve eksik verinin nasıl ele alınacağıdır (na.rm).

  • t.test()’de veri x ve y’dir, ve testin detayları alternative, mu, paired, var.equal, ve conf.level’dır.

  • t.test()’te veri x ve y’dir ve testin ayrıntıları alternative, mu, paired, var.equal ve conf.level’dir.

Genel olarak veri argümanları ilk sırada yer almalıdır. Ayrıntı argümanları sona gelmelidir ve de genelde varsayılan değerlere sahip olmalıdır. Varsayılan bir değeri, adlandırılmış değişkenli bir fonksiyonu çağırdığınız gibi belirlersiniz:

# Normal yaklasim kullanarak ortalama etrafında guven araligini hesapla
mean_ci <- function(x, conf = 0.95) {
  se <- sd(x) / sqrt(length(x))
  alpha <- 1 - conf
  mean(x) + se * qnorm(c(alpha / 2, 1 - alpha / 2))
}

x <- runif(100)
mean_ci(x)
#> [1] 0.498 0.610
mean_ci(x, conf = 0.99)
#> [1] 0.480 0.628

Varsayılan değer neredeyse her zaman en yaygın olan değer olmalıdır. Bu kuralın birkaç istisnası güvenli bir şekilde ele alınmalıdır. Örneğin na.rm için varsayılan değer olarak FALSE almak mantıklıdır çünkü eksik değerler önemlidir. Genellikle kodunuza koyacağınız şey na.rm = TRUE olsa bile, Varsayılan olarak, eksik değerleri sessizce göz ardı etmek kötü bir fikirdir.

Bir fonksiyon çağırdığınızda genellikle çok sık kullanıldığı için veri argümanlarının isimlerini çıkarırsınız. Bir ayrıntı argümanının varsayılan değerini geçersiz kılarsanız, tam ismi kullanmanız gerekir:

# Iyi
mean(1:10, na.rm = TRUE)

# Kotu
mean(x = 1:10, , FALSE)
mean(, TRUE, x = c(1:10, NA))

Bir argümana özgün ön eki ile (örn. mean(x, n = TRUE)) atıfta bulunabilirsiniz ama karışıklığa yol açabileceğinden genel olarak bundan kaçınmak iyidir.

Bir fonksiyonu çağırdığınızda =’in etrafında boşluk bırakmanız gerektiğine dikkat edin. Virgülden sonra da (önce değil, tıpkı normal yazımda olduğu gibi) her zaman bir boşluk koyun. Boşluk bırakmak fonksiyon önemli bileşenleri için gözden geçirirken kolaylık sağlar.

# Iyi
average <- mean(feet / 12 + inches, na.rm = TRUE)

# Kotu
average<-mean(feet/12+inches,na.rm=TRUE)

19.5.1 İsim seçme

Argümanların isimleri de önemlidir. R argümanlarınıza ne isim verdiğini önemsemez ama kodunuzun okuyucuları (ve gelecekteki siz!) önemseyecektir. Genelde uzun, tanımlaycı isimler seçmelisiniz ama çok yaygın bir şekilde kullanılan çok kısa isimler de bulunmaktadır. Bunları ezberlemekte fayda var:

  • x, y, z: vektörler.
  • w: ağırlık (weight) vektörü.
  • df: veri çerçevesi (data frame).
  • i, j: nümerik indisler (genellikle satır ve sütunlar).
  • n: uzunluk ya da satır sayısı.
  • p: sütun sayısı.

Bunun dışında mevcut R fonksiyonlarındaki argüman isimlerini de göz önünde bulundurun. Örneğin, eksik değerlerin atılması gerekiyorsa na.rm kullanın.

19.5.2 Değerlerin kontrol edilmesi

Daha çok fonksiyon yazdıkça fonksiyonunuzun nasıl çalıştığını tam olarak hatırlamayacağınız bir noktaya geleceksiniz. Bu noktada, fonksiyonunuzu geçersiz girdilerle aramak kolaydır. Bu sorunun önüne geçmek için kısıtlamaları açıkça belirtmek genellikle yararlıdır. Örneğin, ağırlıklı özet istatistiklerini hesaplamak için bazı işlevler yazdığınızı hayal edin:

wt_mean <- function(x, w) {
  sum(x * w) / sum(w)
}
wt_var <- function(x, w) {
  mu <- wt_mean(x, w)
  sum(w * (x - mu) ^ 2) / sum(w)
}
wt_sd <- function(x, w) {
  sqrt(wt_var(x, w))
}

Eğer x ve w aynı uzunlukta değilse ne olur?

wt_mean(1:6, 1:3)
#> [1] 7.67

Bu durumda, R’nin vektör geridönüşüm kuralından ötürü, bir hata almayız.

Önemli ön koşulları kontrol etmek ve doğru değillerse hatayı atmak işe yarayacaktır (stop() kullanarak):

wt_mean <- function(x, w) {
  if (length(x) != length(w)) {
    stop("`x` ve `w` aynı uzunlukta olmalıdır.", call. = FALSE)
  }
  sum(w * x) / sum(w)
}

Bunu çok abartmamaya çalışın. Fonksiyonunuzu sağlamlaştırmak için harcayacağınız zamanla onu yazmaya ayıracağınz zaman arasında bir ödünleşim bulunmaktadır. Örneğin na.rm argümanını eklediyseniz bunu çok da dikkatli kontrol etmenize gerek yok:

wt_mean <- function(x, w, na.rm = FALSE) {
  if (!is.logical(na.rm)) {
    stop("`na.rm` mantıksal olmalıdır.")
  }
  if (length(na.rm) != 1) {
    stop("`na.rm` uzunluğu 1 olmalıdır.")
  }
  if (length(x) != length(w)) {
    stop("x` ve `w` aynı uzunlukta olmalıdır.", call. = FALSE)
  }
  
  if (na.rm) {
    miss <- is.na(x) | is.na(w)
    x <- x[!miss]
    w <- w[!miss]
  }
  sum(w * x) / sum(w)
}

Bu çok küçük bir kazanç için çok fazla çaba sarfetmektir. Bunun yerine stopifnot() yerleştirebilirsiniz: Bu, her bir argümanın TRUE olup olmadığını kontrol eder ve değilse jenerik bir hata mesajı verir.

wt_mean <- function(x, w, na.rm = FALSE) {
  stopifnot(is.logical(na.rm), length(na.rm) == 1)
  stopifnot(length(x) == length(w))
  
  if (na.rm) {
    miss <- is.na(x) | is.na(w)
    x <- x[!miss]
    w <- w[!miss]
  }
  sum(w * x) / sum(w)
}
wt_mean(1:6, 6:1, na.rm = "foo")
#> Error in wt_mean(1:6, 6:1, na.rm = "foo"): is.logical(na.rm) is not TRUE

stopifnot() kullanırken, neyin yanlış olabileceğini kontrol etmek yerine neyin doğru olması gerektiğini öne sürdüğünüze dikkat edin.

19.5.3 Üç nokta (…)

R’daki çoğu fonksiyon rastgele sayıda girdi alır:

sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
#> [1] 55
stringr::str_c("a", "b", "c", "d", "e", "f")
#> [1] "abcdef"

Bu fonksiyonlar nasıl çalışır? Bunlar özel bir argüman kullanır: ... (üç nokta). Bu özel argüman, başka türlü eşleşmeyen herhangi bir sayıda argümanı yakalar.

Bu faydalı bir argümandır çünkü üç noktayı(...) daha sonra diğer fonksiyonlara gönderebilirsiniz. Fonksiyonunuz başka bir fonksiyon barındırıyorsa bu işe yaracak bir özelliktir. Örneğin, str_c() barındırından şöyle yardımcı fonksiyonları sıklıkla oluşturuyorum:

commas <- function(...) stringr::str_c(..., collapse = ", ")
commas(letters[1:10])
#> [1] "a, b, c, d, e, f, g, h, i, j"

rule <- function(..., pad = "-") {
  title <- paste0(...)
  width <- getOption("width") - nchar(title) - 5
  cat(title, " ", stringr::str_dup(pad, width), "\n", sep = "")
}
rule("Önemli çıktı")
#> Önemli çıktı ---------------------------------------------------------------

Burada ..., str_c () ile halletmek istemediğim argümanları iletmeme izin veriyor. Bu çok iyi bir teknik. Ama bir bedeli de yok değil: Yanlış yazılan hiçbir argüman hata vermiyor. Bu da yazım hatalarının fark edilmemesine yol açıyor:

x <- c(1, 2)
sum(x, na.mr = TRUE)
#> [1] 4

Eğer ...’nin değerlerini görmek istiyorsanız, list(...) kullanın.

19.5.4 Tembel değerlendirme

R’daki argümanlar tembel bir şekilde değerlendirilir. Yani ihtiyaç duyulana kadar hesaplanmazlar. Bunun anlamı şudur: Eğer hiç kullanılmazlarsa, hiç çağrılmazlar. Bu bir programlama dili olarak R’nin önemli bir özelliğidir ama veri analizi için kendi fonksiyonunuzu yazıyorsanız genellikle bunun peki bir önemi yoktur. Şu bağlantıda tembel değerlendirme hakkında daha fazla şey öğrenebilirsiniz: http://adv-r.had.co.nz/Functions.html#lazy-evaluation.

19.5.5 Alıştırmalar

  1. commas(letters, collapse = “-“) ne yapar? Neden?

  2. pad argümanı için çok sayıda karakter sağlayabilseydiniz iyi olurdu. Örn. rule(“Title”, pad = “-+”). Bu neden şu anda çalışmıyor? Bunu nasıl düzeltirsiniz?

  3. trim argümanı mean()’e ne yapar? Bunu ne zaman kullanabilirsiniz?

  4. cor(c) için method argümanının varsayılan değeri c(“person”, “kendall”, “spearman”)’dir. Bu ne anlama gelir? Varsayılan olarak hangi değer kullanılır?

19.6 Çıktı değerleri

Fonksiyonunuzun ne vereceğini bulmak genellikle kolay bir iştir: Zaten fonksiyonu yapma amacınız bu çıktıdır! Bir değer çıktı olarak alınırken dikkat edilmesi gereken iki husus vardır:

  1. Çıktının erken alınması fonksiyonunuzun daha kolay okunmasını sağlar mı?

  2. Fonksiyonunuzu pipeline şeklinde tasarlayabilir misiniz?

19.6.1 Açık çıktı ifadeleri

Bir fonksiyonun çıkardığı değer genellikle değerlendirdiği en son ifadedir ancak return() kullanarak daha erken çıktı almayı tercih edebilirsiniz. Daha basit bir çözümle erken çıktı alabileceğinizi bildirmek için return() yöntemini kullanmanın en iyisi olduğunu düşünüyorum. Bunun en yaygın nedeni girdilerin boş olmasıdır:

complicated_function <- function(x, y, z) {
  if (length(x) == 0 || length(y) == 0) {
    return(0)
  }
    
  # Karmasik kod burada
}

Diğer bir neden, bir karmaşık ve bir de basit blok içeren bir if ifadesine sahip olmanızdır. Örneğin, şöyle bir if ifadesi yazabilirsiniz:

f <- function() {
  if (x) {
    # Ifadesi 
    # cok
    # sayida
    # satir
    # gerektiren
    # bir sey
    # yapin
  } else {
    # kisa bir cikti alın
  }
}

Ama ilk blok çok uzunsa, zamanla else’e gelir ve condition’ı unutmuş olursunuz. Bunu yeniden yazmanın bir yolu basit bir durum için erken bir çıktı kullanmaktır:


f <- function() {
  if (!x) {
    return(kisa_bir_cikti)
  }

   # Ifadesi 
    # cok
    # sayida
    # satir
    # gerektiren
    # bir sey
    # yapin
  
}

Bu kodun daha kolay anlaşılmasını sağlar çünkü onu anlamak için fazla bir içeriğe ihtiyacınız olmaz.

19.6.2 Pipeline fonksiyonlarının yazımı

Kendi pipeline fonksiyonlarınızı yazmak istiyorsanız çıktı değeri hakkında düşünmeniz önemlidir. Çıktı değerinin nesne tipini bilmek boru hattınızın çalışacağı anlamına gelir. Örneğin, dplyr ve tidyr ile nesne tipi veri çerçevesidir. Pipeline yapılabilir fonksiyonların iki temel tipi vardır: dönüşüm ve yan etki. Dönüşümde bir obje fonksiyonun ilk argümanına geçirilir ve çıktı olarak modifiye bir nesne alınır. Yan-etkide aktarılan nesne dönüştürülmez. Bunun yerine fonksiyon nesne üzerinde bir işlem yapar (bir grafik çizmek ya da bir dosya kaydetmek gibi). Yan etki fonksiyonları görünmez bir şekilde ilk argümanı çıktı olarak verir; böylece yazdırılmasalar bile hala bir pipeline’da kullanılabilirler. Örneğin, aşağıdaki basit fonksiyon bir veri çerçevesindeki eksik değerlerin sayısını yazdırıyor:

show_missings <- function(df) {
  n <- sum(is.na(df))
  cat("Missing values: ", n, "\n", sep = "")
  
  invisible(df)
}

İnteraktif bir şekilde çağırırsak invisible(), df inputunun yazdırılmadığı anlamına gelir.

show_missings(mtcars)
#> Missing values: 0

Ama hala oradadır; sadece yazdırılması varsayılmaz:

x <- show_missings(mtcars) 
#> Missing values: 0
class(x)
#> [1] "data.frame"
dim(x)
#> [1] 32 11

Ve bunu hala bir boruda kullanabiliriz:

mtcars %>% 
  show_missings() %>% 
  mutate(mpg = ifelse(mpg < 20, NA, mpg)) %>% 
  show_missings() 
#> Missing values: 0
#> Missing values: 18

19.7 Çevre

Bir fonksiyonun son olarak ele alacağımız bileşeni çevresidir. Bu fonksiyon yazmaya ilk başladığınızda derinlemesine anlamanız gereken bir şey değildir. Ancak çevre fonksiyonların nasıl çalıştığı ile yakından ilgili olduğundan bu konuda biraz fikir sahibi olmakta fayda vardır. Bir fonksiyonun çevresi R’ın biri isimle ilişkili bir değeri nasıl bulacağını kontrol eder. Örnek olarak şu fonksiyonu alalım:

f <- function(x) {
  x + y
} 

Bir çok programlama dilinde bu bir hata olacaktır çünkü y fonksiyonun içinde tanımlanmamıştır. R’da bu geçerli bir koddur çünkü R bir isimle ilişkili bir değeri bulmak için sözlüksel kapsam denen kuralları kullanır. y fonksiyon içinde tanımlanmadığından, R fonksiyonun tanımlandığı çevreye bakacaktır:

y <- 100
f(10)
#> [1] 110

y <- 1000
f(10)
#> [1] 1010

Bu davranış hatalar (bug) için bir açık davet gibi görünüyor. Gerçekten de kasıtlı olarak bunun gibi fonksiyonlar oluşturmaktan kaçınmalısınız. Ancak çok fazla soruna da yol açtığı söylenemez (özellikle temiz bir slate elde etmek için R’ı düzenli bir şekilde yeniden başlatıyorsanız).

Bu davranışın bir dil açısından avantajı R’ın çok tutarlı olmasını sağlamasıdır. Her isim aynı kurallar kullanılarak aranır. Beklemeyebileceğiniz iki şeyin davranışını içeren “f()” için: “{” ve “+”. Bu, aşağıdakiler gibi dolambaçlı şeyler yapmanızı sağlar:

`+` <- function(x, y) {
  if (runif(1) < 0.1) {
    sum(x, y)
  } else {
    sum(x, y) * 1.1
  }
}
table(replicate(1000, 1 + 2))
#> 
#>   3 3.3 
#> 100 900
rm(`+`)

Bu R’da yaygın bir olgudur. R sizi fazla sınırlandırmaz. Başka programlama dillerinde yapamayacağınız pek çok şeyi yapabilirsiniz. Yapmanız kesinlikle tavsiye edilmeyen bir çok şeyi yapabilirsiniz. Ancak bu güç ve esneklik ggplot2 ya da dplyr gibi araçları mümkün kılar. Bu esnekliği en iyi şekilde nasıl kullanabileceğinizi öğrenmek bu kitabın amaçlarının dışındadır ancak bu konuda okumak için Advanced R’a başvurabilirsiniz Advanced R.