R İle Temel Veri Düzenleme İşlemleri

Güncelleme Yayın

Veri kaynağımız bir web sitesi, bir servis veya bir uygulama olabilir. Hatta, amacımız doğrultusunda günlük alışkanlıklarımızı ve/ya eylemlerimizi de kayıt altına alabiliriz. Ara ara scrapping işlemi ile ilgili de yazılar yayınlamaya çalışacağım. Şimdiye değin temel araçlara kısaca değinmeye çalıştım. Aşağıdaki kısa listeye göz atabilirsiniz.

Evet, amacımız doğrultusunda bir XML/HTML dökümanından veya REST API aracılığıyla bir kaynaktan verilere ulaşabiliyoruz. Bunların ikisinin de mümkün olmadığı ya da sınırlı olduğu durumlar için de alternatif çözümler üretebiliyoruz. O halde, bir sonraki aşamaya geçelim ve elde ettiğimiz verileri birleştirelim, temizleyelim (data tidying) ve görselleştirelim (data visualization).

R İle Verilerin İşlenmesi

Elimizde neler var bir bakalım. Web sitesindeki içeriklere sahibiz. Aynı şekilde Google Analytics üzerinden de sayfa görüntüleme verilerini kullanabiliyoruz. Dolayısıyla farklı yapılarda 2 farklı tablomuz var. Her iki tablonun ortak noktası sayfa adresleri (uri, page path veya slug olarak ifade edebiliriz). O halde yapacağımız ilk işlem bu iki tabloyu bir araya getirmek. Bu amaçla left join fonksiyonundan faydalanabiliriz. Daha önce SQL bağlamında JOIN türlerine kısaca değinmeye çalışmıştım. Ön bir bilgilendirme için JOIN Tipleri ve SQL Tablo Birleştirme İşlemleri başlıklı yazıya göz atabilirsiniz.

İlk olarak içeriklerimizin yer aldığı tablo yapısına bakalım.

'data.frame': 411 obs. of  8 variables:
 $ postId     : int  16743 16668 16361 16303 16300 16287 16248 16173 16220 16204 ...
 $ pubDate    : Date, format: "2020-04-11" "2020-03-29" "2020-03-20" "2020-03-13" ...
 $ postStatus : Factor w/ 1 level "publish": 1 1 1 1 1 1 1 1 1 1 ...
 $ postType   : Factor w/ 1 level "post": 1 1 1 1 1 1 1 1 1 1 ...
 $ slug       : chr  "r-ile-wordpress-rest-api-erisimi" "anomali-ve-anomali-tespiti-nedir" "gtm-ads-conversions" "gpg-nedir-nasil-kullanilir" ...
 $ postTitle  : chr  "R İle WordPress REST API Erişimi" "Anomali ve Anomali Tespiti Nedir?" "Google Tag Manager İle Google Ads Dönüşüm Takibi" "GPG (GnuPG – GNU Privacy Guard) Nedir? Nasıl Kullanılır?" ...
 $ postContent: chr  "<p class=\"ui text\"><strong>R programlama dili</strong> üzerine uzun zamandır yazı yayınlayamadım. Son yayınla"| __truncated__ "<p class=\"ui text\">Verinin raporlanması sürecine gelene değin pek çok farklı aşamada veri setine müdahalelerd"| __truncated__ "<p class=\"ui text\">Web sitemiz ve/ya uygulamamızdaki kullanıcı davranışlarının takibi ve bu takip işlemleri k"| __truncated__ "<p class=\"ui text\">Veri hakkında bu kadar yazı yayınlayıp veri güvenliği -en azından kişisel verilerimiz- kon"| __truncated__ ...
 $ postCats   : chr  "293,450,351,516,401,714" "293,350,450,1073,444,394,3134" "293,349,444,443,516,401" "293,1755,516,394,401,2244" ...
>

Sütunlarımız (columns) veya diğer bir ifadeyle değişkenlerimiz (variables); postId, pubDate, postStatus, postType, slug, postTitle, postContent ve postCats.

Şimdi de Google Analytics verilerimize bakalım.

'data.frame': 3908 obs. of  7 variables:
 $ pagePath  : chr  "/cea-logo-dark.png" "/cea-logo.png" "/ceaksan" "/channel/UClgihdkPzNDtuoQy4xDw5mA" ...
 $ pageTitle : chr  "Nothing found for Tr Cea Logo Dark Png" "Nothing found for Tr Cea Logo Png" "Ceyhun Enki Aksan - YouTube" "(100) How to Build a Scroll + Timer Trigger with Google Tag Manager - YouTube" ...
 $ segment   : Factor w/ 1 level "Organic Traffic": 1 1 1 1 1 1 1 1 1 1 ...
 $ sessions  : num  0 0 1 0 1 0 3 1 1 1 ...
 $ bounceRate: num  0 0 100 0 0 0 67 100 100 0 ...
 $ pageviews : num  1 1 1 1 1 1 3 1 1 1 ...
 $ timeOnPage: num  60 6 0 5 960 0 16 0 0 519 ...
 - attr(*, "totals")=List of 1
  ..$ :List of 4
  .. ..$ sessions  : chr "356529"
  .. ..$ bounceRate: chr "88.73331482151522"
  .. ..$ pageviews : chr "438834"
  .. ..$ timeOnPage: chr "1.7744958E7"
 - attr(*, "minimums")=List of 1
  ..$ :List of 4
  .. ..$ sessions  : chr "0"
  .. ..$ bounceRate: chr "0.0"
  .. ..$ pageviews : chr "1"
  .. ..$ timeOnPage: chr "0.0"
 - attr(*, "maximums")=List of 1
  ..$ :List of 4
  .. ..$ sessions  : chr "13233"
  .. ..$ bounceRate: chr "100.0"
  .. ..$ pageviews : chr "14246"
  .. ..$ timeOnPage: chr "473096.0"
 - attr(*, "isDataGolden")= logi TRUE
 - attr(*, "rowCount")= int 3908

Sütunlarımız (columns) veya diğer bir ifadeyle değişkenlerimiz (variables); pagePath, pageTitle, segment, sessions, bounceRate, pageviews, timeOnPage.

Siteden çekilen verilere göz atıldığında görseller URL’lerinin, eski ve/ya hatalı URL yazımlarının, preview modunda görüntülenen sayfaların, YouTube video URL’lerinin de listelendiğini görebiliriz. O halde önce verilerimizi daha derli (tidying) bir hale getirmemiz gerekiyor. Bu süreçte dplyr ve stringr paketlerini kullanabiliriz. Var olan veri tablosunu olduğu gibi bırakıp (raw) yeni bir tablo üzerinden ilerleyebiliriz.

cln_gaPageData <- gaPageData %>%
  filter(pageTitle != "(not set)") %>%
  mutate(pageLang = ifelse(str_detect(pagePath, "/en/") == 0, "tr", "en"),
         pageType = sapply(pagePath, getPageType),
         pageTitle = str_trim(str_replace_all(pageTitle,
                                          paste(c("- ceaksan", "- Ceyhun Enki Aksan"),
                                                collapse = '|'),
                                          replacement = ""),
                              side = "both"),
         pagePath = str_trim(str_replace_all(pagePath,
                                         paste(c("/tr/", "/"),
                                               collapse = "|"),
                                         replacement = ""),
                             side = "both")) %>%
  filter(pageLang == "tr", pageType == "post", pagePath != '') %>%
  select(pagePath, pageTitle, sessions, bounceRate, pageviews, timeOnPage) %>%
  group_by(pagePath, pageTitle) %>%
  mutate(pageviews = round(sum(pageviews)),
         sessions = round(mean(sessions)),
         bounceRate = round(mean(bounceRate)),
         timeOnPage = round(sum(timeOnPage))) %>%
  # sample_n(., 20) %>%
  arrange(desc(pageviews)) %>%
  unique()

Örnek kodda da görüldüğü üzere ilk filtreleme işlemi (not set) olarak tanımlı başlıklara sahip yazılar için gerçekleştirildi. Kullandığım qTranslate XT eklentisi shortcode aracılığıyla içerikleri farklı dil tanımlarını ayrıştırıyor. Bu aşamada bazı içerikler Türkçe olsalar dahi İngilizce içerik olarak da kayıt edilebiliyor. Bu durumda da İngilizce versiyon başlıksız oluyor. 844 içeriği bu şekilde filtrelemiş oluyoruz.

getPageType <- function(x){
  if(str_detect(x, "/cat/")){
    postType <- "cat"
  } else if(str_detect(x,
                       paste(c("/search/query:", "/search"),
                                collapse = '|'))) {
    postType <- "search"
  } else if(str_detect(x, "/channel/")) {
    postType <- "youtube"
  } else if(str_detect(x, "/course/")) {
    postType <- "course"
  } else if(str_detect(x, "/project/")) {
    postType <- "project"
  } else {
    postType <- "post"
  }
  postType
}

Ardından, mutate ile bir dizi işlem gerçekleştiriyoruz. İlk aşamada URI kontrolü ile yazının dilini ve yazının türünü belirliyoruz. Ardından, sayfa başlığındaki yer alan son ekleri kaldırıyor ve Google Analyticss Page URL ile WordPress slug değerlerinin aynı yapıya çeviriyoruz.

İlerleyeceğimiz aşama Türkçe (tr) içerikler (post) olacağı için oluşturduğumuz buna uygun olarak oluşturduğumuz değişkenler üzerinden filtreleme işlemini gerçekleştirebiliriz.

Sadece ilgili sütunlar üzerinden devam etmek için select ile ilgili sütunları belirtiyor ve satır veya diğer bir iade ile gözlemleri (observations) path ve title üzerinden gruplandırıyorum. Son aşamada artık elimde benzersiz gözlemlerim var. Artık bu gözlemler için ortalama ve toplam değerleri hesaplayıp sıralayabilirim.

Yaklaşık 4.000 satırla başladığımız işlemlerin sonucunda elimizde 1.200 satır bulunuyor. Ancak, işlemlerimiz sonlanmış durumda değil. Şimdiye kadar Google Analytics üzerinden edindiğimiz veriler üzerinde işlemler yapmıştık. Şimdi, WordPress içerikleri üzerinden ilerleyeceğiz. Post içeriklerinde ve başlıklardaki istenmeyen karakterleri temizleyip, draft, planlı vb. yazıları filtreleyelim. İçeriklerdeki kelime yoğunluğu ve benzeri süreçlerde de ihtiyaç duyacağım için qdap paketini kullanacağım.

cleanContent <- function(x) {
  cleanedText <- bracketX(x, "all", scrub = TRUE)
  cleanedText
}

cln_wordPressPosts <- wordPressPosts %>%
  filter(postStatus == "publish",
         postType == "post",
         pubDate > start_date || pubDate < end_date) %>%
  select(postId, slug, pubDate, postTitle, postContent, postCats) %>%
  mutate(postContent = cleanContent(postContent),
         postTitle = str_replace_all(postTitle,
                                     paste(c("&#[1-9]*;"),
                                           sep=""),
                                     replacement = ""))

Son durumda, JOIN için uygun 2 adet düzenli tablomuz var.

cln_data <- cln_wordPressPosts %>%
  left_join(cln_gaPageData, by = c("slug" = "pagePath"))

slug ve pagePath aynı yapıda ve tablo içerisinde benzersiz değerler oldukları için cln_wordPressPosts tablosuna cln_gaPageData tablosunu iliştiriyorum. Elimde artık sayfalarla ilgili ihtiyaç duyabileceğim tüm değerler 11 sütuna ayrılmış biçimde mevcut.

R ile Veri Düzenleme

Elbette birleşme sonrasında eşleşmeyen kayıtlar olacaktır. Örneğin, GA verilerini çekerken belirlediğim tarihten eski veya yeni içerikler tablolardan birinde olup diğerinde karşılık bulamayabilir ya da bir yazının URL'i değişmişse ya da yazı silinmişse yine ilgili sütuna ait hücre NA değerini alacaklardır.

(hasNA <- cln_data[unique(unlist(lapply(cln_data, function (x) which (is.na (x))))), ])

Elde ettiğimiz tabloyu dosya olarak kayıt ederek (excel, csv, rds vb.) süreci sonlandırabiliriz.

cln_data <- cln_data %>% select(-pageTitle)
saveRDS(cln_data, file = paste(path, "/datasets/tidyData.rds", sep=""))

Bir sonraki aşamada oluşturduğumuz bu tabloyu kullanarak ggplot aracılığıyla kolaylıkla grafikler oluşturabiliriz.


Kaynakça