Python İle Temel Veri İşlemleri

Bir içerik sitesi, hedeflediği kitle ile (ilgili aramaları yapan yeni kullanıcılar) ilk tanıştığı ilk an itibariyle 2 aşamalı bir alt hedeflemeye sahip olur; yeni ziyaretçilerin artışını sağlamak ve yeni ziyaretçilerin yeniden ziyaret etmesini (geri gelen) sağlamak. Bu alt hedefler de yeni içerik üretimi ve var olan içeriklerin düzenli olarak geliştirilmesini zorunlu kılar.

AA

İçeriklerin düzenli olarak takibi ve gerekli optimizasyon - geliştirme süreçleri sistemli bir şekilde ele alınamazsa zaman içerisinde içerikler 3 gruba ayrılmaya başlar; öne çıkan içerikler, ziyaretçisi olmayan içerikler, aradakiler. Özellikle teknik problemler ve/veya algoritma güncellemelerinde öne çıkan içeriklerin etkilenmesi istenen bir durum değildir. Çünkü, değişim sitenin ortalama sayfa görüntüleme sayısını da önemli düzeyde etkiler. Ziyaretçisi olmayan içerikler ise sadece yük oluşturur. Aradaki içerikler ise rekabet içerisinde (site içi ve site dışı) pozitif ve negatif küçük değişimler gösterseler de çoğu zaman uzun kuyruklu arama terimleri (long-tail keywords) ile ilişkilidir.

Bir içerik yayıncı, içerikleri düzenli olarak içerik-ziyaretçi ve içerik-arama motoru bağlamlarında değerlendirmeli ve gerekli ise optimize etmelidir. Yazı içerisindeki bağlantılar zaman içerisinde işlevsiz hale gelmiş olabilir, görseller ve sayfa içerisindeki JavaScript kodları ile ilgili problemler oraya çıkmış olabilir, vb.

Bu yazıda, Python İle Google Analytics ve Search Console API İşlemleri başlıklı bir önceki yazının devamı olarak Python programlama dili çerçevesinde sunulan Pandas kütüphanesini ve birkaç string metodunu kullanarak içerikler hakkında nasıl fikir edinebileceğimize değinmeye çalışacağım. Mümkün olduğu kadar pratik yapabilmek adına Pandas metodlarını kullanmaya öze göstereceğim. Zaman içerisinde bazı bölümlerde daha işlevsel ve uygun çözümler bulabilirsel kod değişikliğine gidebilirim. Bu durumda, eski çözümleri yorum olarak tutmaya özen göstereceğim.

Python İle Google Analytics ve Search Console API İşlemleri başlıklı yazıda Google Analytics Reporting API1 aracılığı ile belirli bir tarih arasında görünülenmiş olan sayfalar ile ilgili bazı bilgiler edinmiştik. Burada görüntülenmiş ifadesi Google Analytics izleme kodunun çalıştığı ve Google Analytics sunucusuna sayfa ile ilgili verilerin iletildiği süreci ifade eder. Sayfalar ön tanımlı olarak URL temelinde ele alınır2.

document.title
document.location

Elbette JavaScript ve/veya measurement protocol ile farklı veya değiştirilmiş bilgiler de Google sunucularına iletilebilir.

Google Collect POST Query

Bu bilgilerin yanı sıra sosyal medya platformları, reklam mecraları ve e-posta bültenler gibi farklı kaynaklar URL'e çeşitli sorgu parametreleri (query) ekleyeceklerdir veya farklı ülkelerden erişim sağlayan ziyaretçiler Google Translate ve benzeri araçlarla sayfa başlığı ve içeriğini farklı dillere çevirebilirler. Bu ve benzeri pek çok neden sayfalara dair aykırı değerler oluşturacağı için bir ön işlem yapılması gerekliliğini doğurur.

Google Analytics - Bounce Rate

Öncelikle, veriler ile ilgili ön bir işlem olarak ilgili metrik ve boyutların yer aldığı ve istediğimizde hızlıca erişebileceğimi özelleştirilmiş bir Google Analyics raporu oluşuralım.

Google Analytics - Custom Report

Pandas ve String Metodları İle Verilerin Düzenlenmesi

Aşağıdaki işlemlerde pratik yapmak adına mümkün olduğunda Python Pandas metodlarını kullanmaya özen göstereceğim. Kimi durumlarda da string metotları yazısı ile ilişkili olarak ek örneklere yer vereceğim. O halde, adım adım başlayalım.

Kullanacağımız veri seti Python İle Google Analytics ve Search Console API İşlemleri başlıklı yazıda oluşturduğumuz Google Analytics sayfa görüntüleme verileri. Ana kütle boyutumuz 5939. Ancak, sitemap üzerinden bakıldığında beklenen sayının 560 civarında olması gerekirdi. Peki, bu farklılığa dair neler söyleyebiliriz?

import pandas as pd

df = pd.read_csv('GA-2020-05-30.csv')
# df.dtypes
# df.isna().any()
df.info()
Column Type
pagePath object
pageTitle object
bounceRate float64
pageViews int64
avgTimeOnPage float64
avgSessionDuration float64
avgPageLoadTime float64
dtype: object

Bir sonraki adımda da yığına dair istatistiklere bir bakalım.

df.describe()
bounceRate pageViews avgTimeOnPage avgSessionDuration avgPageLoadTime
count 5939.000000 5939.000000 5939.000000 5939.000000 5939.000000
mean 47.068327 67.850985 133.914575 56.241569 2.157740
std 42.735453 287.962893 225.226011 251.591516 4.802585
min 0.000000 0.000000 0.000000 0.000000 0.000000
25% 0.000000 1.000000 0.000000 0.000000 0.000000
50% 47.500000 3.000000 17.000000 0.000000 0.000000
75% 94.682411 25.000000 194.254808 44.715433 3.656950
max 105.555556 7511.000000 2541.000000 10531.000000 113.786000

Sahip olduğumuz ana kütlenin üst, alt ve yığın içerisinden çekilen rastlanısal gözlemlerine bakalım.

df.head(10)
df.tail(10)
df.sample(10)

Bu işlemler neticesinde bounceRate içerisinde 0 ve 100'e çok yakın değerler olduğunu gördüm. Bu iki değer yakınlığı normal şartlarda pek olası değil. Dolayısıyla, ilgili alan için bir sonraki aşamada süreci biraz daha detaylandırmak gerekecek. Aynı şekilde, pageViews ve diğer alanlarda da benzer durumların olduğu görülebilmekte. Ek olaak, içeriklerde belirli bir konu akışı takip ediliyorsa değerlerin dağınık olmaması, zamanla belirli bir aralıkta toplanması beklenir.

Sayısal değerler ile ilişkili olarak pagePath alanına bakıldığında problem olarak nitelendirilebilecek sayısal tutarsızlıkların nedenine dair bazı ipuçları görmek mümkün. Örneğin, /tr/api-nedir/ sayfası gibi bir sayfa seçip sorguladığımda aynı sayfaya ait pek çok ilgili değer olduğu görülebilmekte.

df[df.apply(lambda r: r.str.contains('api-nedir', case=False).any(), axis=1)] 
pagePath
...
/tr/api-nedir
/tr/api-nedir/
/tr/posts/api-nedir/
/tr/posts/api-nedir
/tr/posts/2017/07/api-nedir
/tr/posts/2017/07/api-nedir/
...

Ayrıca, path farklılıklarının yanı sıra, çeşitli sorguların (query) da farklı sayfalar olarak değerlendirilmesi nedeniyle tutarsızlık oluşurduğu görülebilmekte. Bu durumda, ilgili alanlar kaldırılabilir ya da sorguların ve ardından path sonunda yer alan / işareti temizlenebilir.

# df[df.apply(lambda p: p.str.endswith('/').any(), axis=1)] # Tüm alanları aramaya dahil etmek için
# df.loc[[p.endswith('/') for p in df.pagePath]] # Belirli bir sütunda işlem yapmak için
df.loc[df.PagePath.str.contains('/api-nedir', case=False, regex=False)]

Kaldırma işlemini tercih etmek çok doğru olmayacaktır. Çünkü, path ile ilgili değişiklikler zaman içerisinde kasıtlı olarak gerçekleştirilmiş olabilir. Böyle durum mevcut ise, geçerli verileri de silmiş oluruz. Bu nedenle 2. yol üzerinden ilerleyelim.

# df.pagePath = df.pagePath.apply(lambda x: x.split('?')[0].rstrip('/'))
df.pagePath = [p.split('?')[0].rstrip('/') for p in df.pagePath]

Bu aşamadan sonra duplicate veriler netlik kazanmış olacak. Gerekli olmadığı düşünülen satırlar silinebilir ya da toplamlar ve ortalamalar ile tek bir satır haline getirilebilir ya da öncesinde belirli bir değer (örneğin, median ya da mean gibi) altındaki veriler de analiz sürecinin dışında tutulabilir. Duplicate kayıtların çoğu bu vesile ile hariç tutulmuş olacaktır.

Matematiksel işlemler yapacak isek, numeric verileri hesaplaması kolay hale getirelim.

#df.bounceRate = [round(p) for p in df.bounceRate]
#df.avgTimeOnPage = [round(p) for p in df.avgTimeOnPage]
#df.avgSessionDuration = [round(p) for p in df.avgSessionDuration]
#df.avgPageLoadTime = [round(p) for p in df.avgPageLoadTime]

df.bounceRate = df.bounceRate.map(round) # percentage
df.avgTimeOnPage = df.avgTimeOnPage.map(round) # seconds
df.avgSessionDuration = df.avgSessionDuration.map(round) # seconds
df.avgPageLoadTime = df.avgPageLoadTime.map(round) # seconds

Bu aşamadan sonrası opsiyonel olarak geçekleştirilebilir. Şayet sadece Google Analytics verileri üzerinde çalışılacaksa elbette başlıklar ve sayfa yolu ile ilgili filtreleme işlemleri yapılabilir. Ancak, Seach Console sayfa verileri ve site haritası üzerinden edinilen güncel veriler de sürece dahil edilecekse (tavsiyem bu şekilde olur) bu durumda gereksiz işlem tekrarlarından kaçınmak adına sayfa başlığı ve filtreleme işlemlerinin tabloların birleştirilmesinden sonra yapılmasını öneririm.

Google Analytics bir sayfa 404 durum koduna sahip olsa dahi path ve başlık değeri iletir. Eğer bir 404 sayfası var ise bu sayfa tarafından üretilen başlık değeri gönderilir. Bu durumda, silinmiş ve/veya taşınmış sayfaların başlıklarını Page Not Found gibi değerler üzerinden ayırt edebilir ve bu kayıtları indeks değerlerini kullanarak veri tablosundan kaldırabiliriz.

df.drop(df.index[df.pageTitle.str.contains('found|error|not set', case=False, regex=True)], axis=0, inplace=True)

Elbette bu listeye sabit, form, varış ve listeleme sayfalarını da dahil edebiliriz. Tekrar, değerlerdeki olası değişime bir göz atalım.

df.describe()

Verilerin Yorumlanması

Yığındaki gözlem sayısında epey bir azalma olmuş olmalı. Artık aşama aşama belirli durumlara odaklanabiliriz. İlk olarak histogram grafiği ile dağılımları inceleyelim.

df.hist(figsize=(20, 10), grid = True, bins=25, color='#007bff', zorder=2, rwidth=0.9);
Histogram Grafiği

Ardından, verinin merkez etrafındaki yayılımını görmek adına boxplot grafiğini oluşturalım.

color = {
    "boxes": "DarkGreen",
    "whiskers": "DarkOrange",
    "medians": "DarkBlue",
    "caps": "Gray",
}
df.plot.box(vert=False, figsize=(20, 10), showmeans=True, color=color);
Kutu Grafiği

Görüldüğü üzere, neredeyse tüm alanlarda aykırı değerler mevcut. Elbette her biri için grafiği detayladırıp verilere daha yakından bakabilir ve daha net fikir edinebiliriz.

df.avgPageLoadTime.plot.box(vert=False, figsize=(20,10), showmeans=True, color=color);

Önemli olan gösterim almış ancak yüklenme süresi ve hemen çıkma oranı yüksek olan sayfaları mümkün olduğu kadar ortalamaya yakın hale getirebilmek. Bunun için de ilk hedef aykırı değerleri en azından 1.5*(Q3-Q1) aralığına dahil etmek.

df[
    (df.bounceRate <= df.bounceRate.median()) &
    (df.avgTimeOnPage <= df.avgTimeOnPage.median()) &
    (df.avgSessionDuration <= df.avgSessionDuration.median()) &
    (df.avgPageLoadTime <= df.avgPageLoadTime.median())
]
pagePath pageTitle bounceRate pageViews avgTimeOnPage avgSessionDuration avgPageLoadTime
925 /tr/grav-markdown-icerik-yazimi Grav Makdown İçerik Yazımı 50 66 109 26 0
1260 /tr/html5-api-kullanimlari-1 HTML5 ve API Kullanımları 48 35 130 17 0
1263 /tr/mermaid-js Mermaid JS Nedir? 33 35 109 19 0
1392 /tr/parse-urls-url-yapisi-cozumlemesi URL Yapısı Çözümlemesi 31 29 0 20 0
1401 /tr/wodpress-grav-cms-gecisi WordPress - GRAV CMS Geçişi 0 29 37 0 0
... ... ... ... ... ... ... ...
5879 /tr/wordpress-posts-markdown WordPress İçeriklerin Markdown Olarak Dönüştür... 0 1 3 0 0
5886 /tr/wp-ads-conversion-tracking WordPress Eklentisi: 'WP-Ads Conversion Tracki... 0 1 51 0 1
5888 /tr/wp-cli-ile-post-regex-img-search WP-Cli İle Post İçeriklerinde Arama İşlemi 0 1 11 0 0
5891 /tr/wp-cli-post-meta-thumbnail WP-CLI Post ve Meta İle Tüm Postlara Aynı Görs... 0 1 1 0 0
5894 /tr/wp-cron-nedir-nasil-kullanilir WP-Cron Nedir? Nasıl Kullanılır? 0 1 6 0 0
345 rows × 7 columns

Elimizde tüm durumlarda median altında kalan içeriklerin listesi var. Bu içerikler ile alakalı olarak şu soruları sorabiliriz;

  • Bu içerikler ne sıklıkla güncelleniyor?
  • İçerikler arama motorları tarafından erişilebiliyor mu?
  • İçeriklerin görüntülenmesini engelleyen teknik bir problem mi var?
  • İçerik ilgi çekici değil mi? İçeriğe dair ilgili arama hacmi nedir?
  • İçerik yeterli derecede açıklayıcı değil mi?
  • İçeriğin sıralama aralığı nedir? Rekabette çok mu geride?

İçeriklerden bir kaçına bakıldığında arama hacimlerinin oldukça düşük olduğu ve içeriğin zayıf olduğu görülebilmekte. O halde başlangıç noktamız artık belirgin. Önceliğimiz bu içerikleri zenginleştirmek ve odaklanılan konuyu olası yakın alakalı sorgularla eşleşecek şekilde zenginleştirmek. Sadece bu içerikleri ayrı bir tablo olarak kayı altına alıp zamana bağlı değişimi takip edilebilir.

Sayfalarla ilgili işlemleri bölümlendirirken quantile dilimleri üzerinden de ilerleyebiliriz. Neticede amacımız düşük gösterime sahip, hemen çıkma oranı yüksek ve bir nedenle ziyaretçilerin vakit harcamak istemedikleri içerikleri ilgi çekici kılmak ya da bu içerikleri kaldırmak.

df.loc[df.avgPageLoadTime > df.avgPageLoadTime.quantile(.75)].sort_values(by=['avgPageLoadTime', 'bounceRate'], ascending=[False, False], axis=0)

Evet, bir sonraki aşamada ortalama sayfa yükleme süresi bağlamında sayfaların performansına ve performans değerlerine bağlı olarak neler yapılabileceğine bakılabilir.

Saçılım Grafiği

Gerekli ise teknik geliştirmeler yapldıktan sonra yakın bir zaman aralığı için değişimler takip edilebilir ve yukarıdaki işlemler -birkaçı zaman içerisinde gereksiz hale gelecektir- döngüsel olarak tekrarlanabilir.

Kelime Bulutu

Kelime tekrarlarını değerlendirmek için genel ya da sadece belirli bir metrik (örneğin; pageView) bağlamında da kelime bulutu oluşturabilirsiniz. Elbette bu tür durumlarda, web sitesinde son ek (suffix) kullanılıyor ise ilgili eklerin önceden temizlenmesi gerekir.

from wordcloud import WordCloud, STOPWORDS, ImageColorGenerator

text = ' '.join(k for k in list(df.pageTitle))

stopwords = set(STOPWORDS)
stopwords.update(['ve','ile','user','behavior'])

wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white").generate(text)

plt.figure(figsize=(20,14), dpi=200)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.tight_layout(pad=0)
plt.savefig('myfigure_200.png')
plt.show()

Evet, işlemlerimiz bu kadar. Unutmamak gerekir ki genel değerlendirme içerisinde metriklerin ortalamaları esas kılınacak iken belirli bir metrik önceliğinde elde edilecek sonuç daha doğru bir yorum yapmayı mümkün kılabilir. Bu nedenle, metrikler temelinde neden-sonuç ilişkisi çerçevesinde önem göserilmelidir.