Talep Tahminine (Demand Forecasting) Giriş

Giray Hakan
19 min readNov 18, 2023

--

Bu yazıyı Demand Forecasting’e giriş yapmak ve burada ne tür problemler, veriler ve çözümler olduğunu görmek için yazıyorum. Aslında kendime yazıyorum bu yazıyı, çünkü benim en iyi öğrenme yöntemim bu.

İçindekiler:

  1. Giriş
  2. Veri Analizi ve Ön İşleme
    a) Tek Değişkenli Analiz
    b) Çok Değişkenli Analiz
  3. Feature Engineering
  4. Ölçeklendirme (scaling)
  5. Model Seçimi ve Hiperparametre Ayarlama

1) Giriş

Öncelikle, düşünelim ki benim bir şirketim var ve bu şirket piyasada rekabet ediyor. Rekabetin yoğun olduğu bu ortamda, müşterilerimin ne zaman, ne kadar ve hangi ürünleri isteyeceğini doğru bir şekilde tahmin etmem gerekiyor. Neden mi? Çünkü yanlış tahminler, ya çok fazla stok yapmama ve bu stokların ziyan olmasına, yani para kaybına yol açar, ya da yetersiz stokla müşterilerimi kaybetmeme ve onları rakiplerime kaptırmama sebep olur.

Diyelim ki şirketimde çeşitli departmanlar var ve her birinin bu tahminlere ihtiyacı var. Mali departman, gelecekteki giderleri, gelirleri ve gereken sermayeyi hesaplamak için bu tahminleri kullanıyor. Pazarlama ekibi, hangi stratejinin satışları nasıl etkileyeceğini anlamak için bu verilere bakıyor. Satın alma departmanı, kısa ve uzun vadeli yatırımlarını planlarken bu tahminlere dayanıyor. Operasyonlar departmanı da, gerekli malzemeleri, ekipmanları ve iş gücünü önceden ayarlamak için bu tahminlere güveniyor.

Yani aslında, bu talep tahmini meselesi, sadece stok yönetimi veya satış tahmini değil, şirketin tüm kollarını ilgilendiriyor. Ayrıca, bu tahminlerin doğruluğu şirketin karlılığını doğrudan etkiliyor. Doğru tahminler, israfı azaltıyor, müşteri memnuniyetini artırıyor ve tabii ki kârı maksimize ediyor.

Hedefim, bu tahminlerin nasıl yapıldığını, neden bu kadar önemli olduğunu ve şirketlerin bu tahminleri nasıl kullanarak daha başarılı olabileceklerini görmeye çalışmak.

Amacım ise önümüzdeki 10 hafta için sipariş sayısını tahmin etmek.

Bu problem için Kaggle’da bulduğum şu veri setini kullanacağım. Bunları bilgisayarıma indirdim, train, meal_info ve fulfilment_center_info isminde .csv dosyaları mevcut.

Probleme geri dönecek olursam, belki de bu tarz işlerle uğraşan şirketlerin en zorlandığı challenge’lardan biri, özellikle hızla bozulabilen hammaddelerin ziyanını en aza indirebilmek. Bu durum, talep tahmininin doğruluğunu son derece önemli kılıyor.

İyi tahminde bulunabilmek için çeşitli Machine Learning (Random Forest, çeşitli boosting algoritmaları — LightGBM, XGBoost) algoritmaları kullanacağım. Ve bunların başarılarını da RMSE, MAPE, MAE gibi metriklerle ölçmeye çalışacağım.

Literatüre baktığımda Demand Forecasting problemi hakkında birçok çalışma olduğunu gördüm. Sadece yiyecek ve yemek talep tahmini değil, elektrik talep tahmini, moda talep tahmini gibi daha pek çok farklı alanda da benzer problemler üzerinde çalışmalar varmış.

Bu kadar açıklamanın yeterli olduğunu düşünüyorum ve artık elimi biraz kirletip veriyle oynamak istiyorum.

2) Veri Analizi ve Ön İşleme (Data Analysis — Preprocessing)

Şimdi bu veriyi anlayabilmem için sırayla bunları incelemem gerekiyor.

Bunun için önce train.csv dosyasını inceleyeceğim.

import pandas as pd

# veriyi yükle
train_df = pd.read_csv('foodDemand_train/train.csv')

# bu dosyanın ilk beş satırı göster
print(train_df.head())

Şimdi buradaki sütunları kısaca açıklıyım:

  • id : Her satır için benzersiz bir tanımlayıcı.
  • week : Hangi haftaya ait olduğunu gösteren sayısal bir değer (1 ile 145 arasında değişiyor)
  • center_id : Hangi dağıtım merkezine ait olduğunu gösteren bir tanımlayıcı.
  • meal_id : Hangi yiyeceğe ait olduğunu gösteren bir tanımlayıcı.
  • base_price : yemeğin taban fiyatı.
  • emailer_for_promotion : Promosyon için e-posta gönderilip gönderilmediğini belirten ikili (0 veya 1) bir değer.
  • homepage_featured : Yiyeceğin websitenin ana sayfasında öne çıkarılıp çıkarılmadığını belirten, ve 0 veya 1'den bir değer.
  • num_orders : Verilen sipariş sayısı.

“İyi bir stok yönetimi yapabilmem için bunlardan hangisi veya hangileri en önemli feature olabilir?” diye düşünüyorum.

num_orders sütununa dikkat etmem gerekiyor, çünkü buradaki sayılar ve bunları etkileyen faktörleri iyi modelleyebilirsem soruna iyi bir yaklaşımla çözüm önerisinde bulunabilirim. Bu sayede yiyeceklerin bozulma riski azalacak ve müşteri talebini karşılayacak şekilde dağıtım merkezlerindeki stokları optimize edebilirim.

num_orders sütunundaki her bir sayı, belirli bir meal_id ve center_id kombinasyonu için, belirtilen week (hafta) içerisinde alınan toplam sipariş sayısını gösteriyor dedim. Örneğin, train.csv dosyasının ilk satırında num_orders değeri 177 olarak görünüyor. Bu, meal_id 1885 olan yemeğin, center_id 55 olan dağıtım merkezinden, 1. haftada toplam 177 sipariş alındığını gösterir.

ayrıca checkout_price ve base_price gibi sütunlarda dikkatimi çekiyor. Bu iki sütun, her bir yemeğin satış ve temel fiyatını gösteriyor. Fiyat farklılıkları, talebi etkileyebilir. Örneğin, indirimler veya promosyonlar daha fazla sipariş alınmasına yol açabilir diye düşünüyorum.

veya emailer_for_promotion ve homepage_featured gibi sütunlar belirli yemeklerin tanıtımlarının yapılıp yapılmadığını gösteriyor. Tanıtım yapılan yemeklerin daha fazla sipariş alması muhtemeldir.

center_id ve meal_id sütunları da, hangi dağıtım merkezinin hangi yemekleri dağıttığını gösteriyor. Bazı dağıtım merkezlerinin belirli yemekler için daha yüksek talep gördüğünü fark edebiliriz.

Kısaca bu tablo bize, hangi yiyeceklerin popüler olduğunu, hangi dağıtım merkezlerinde daha fazla talep olduğunu ve zaman içinde bu talebin nasıl değiştiğini anlamama yardımcı olacak.

Şimdi de elimdeki bu verinin daha detaylı istatistiklerine bakmak istiyorum. Yani ortalama sipariş sayısı kaçmış, eksik veri veya aykırı değer var mı vb. gibi sorular sorup cevaplamalıyım.

# veri setinin temel istatistikleri
train_df_description = train_df.describe()
print(train_df_description)

Şimdi sıra sıra buradaki satırların ne anlama geldiğine bakmalıyım:

count: Her bir sütunun kayıt sayısı 456548 olarak gözüküyor, bu da veri setinde herhangi bir eksik veri olmadığı anlamına gelir. Bu, veri temizliği açısından iyi bir işaret.

mean (Ortalama): num_orders sütununun ortalaması yaklaşık ~262. Bu, ortalama olarak her yiyecek ve merkez kombinasyonu için 261 sipariş alındığını gösteriyor. Bu değer, talebin yoğunluğu ve dağılımı hakkında bana iyi bir bilgi veriyor.

std (Standart Sapma): num_orders için standart sapma yaklaşık 396. Bu da sipariş sayısında büyük varyasyonlar olduğunu gösteriyor. Yani bazı yiyecek-merkez kombinasyonları çok daha yüksek veya çok daha düşük sipariş sayılarına sahip olabilir.

min (Minimum): En az sipariş alınan yiyecek-merkez kombinasyonu için sipariş sayısı 13. Bu, bazı kombinasyonların çok düşük talep gördüğünü gösteriyor.

%25, %50 (Medyan), ve %75: Bu değerler, num_orders dağılımının çeyrekliklerini gösteriyor. Medyan ise 136, bu da sipariş sayılarının yarısının 136'nın altında, diğer yarısının ise 136'nın üzerinde olduğu anlamına geliyor.

max (Maksimum): En yüksek sipariş sayısı 24299. Bu, bazı yiyecek-merkez kombinasyonlarının çok yüksek talep gördüğünü gösterir ve bu kombinasyonların stok ve lojistik planlamasında öncelikli olarak ele alınması gerekebilir.

Bu istatistikler, talebin dağılımını ve yoğunluğunu anlamak için kritik bilgiler sağlıyor. Örneğin, ortalama sipariş sayısından çok daha yüksek veya düşük sipariş alan yiyecek-merkez kombinasyonlarına odaklanmak, stok yönetimi ve müşteri taleplerini karşılama açısından önemli olabilir. Aykırı değerleri (yani çok yüksek veya çok düşük sipariş sayıları) tespit etmek, hangi kombinasyonların daha fazla dikkat gerektirdiğini belirlememe yardımcı olacaktır diye düşünüyorum.

Şimdi eksik veri ve aykırı değerlere bakalım.

#hangi sütunlarda eksik veri var
missing_values = train_df.isnull().sum()
print(missing_values)

Eksik veri çıktısına göre, train_df veri setinde hiç eksik veri bulunmuyor. Bu, veri temizleme sürecinde eksik verilerle ilgili ekstra bir işlem yapmam gerekmeyeceği anlamına geliyor. Eksik veriler genellikle, veri toplama aşamasında oluşan kayıplardan veya hatalardan kaynaklanır ve çeşitli imputation (doldurma) teknikleriyle ele alınabilir. Ancak bu durumda, eksik veri olmadığından bu adımı atlayabilirim.

Şimdi fullfilment_center_info.csv’ye bakacağım:

import pandas as pd

# Dosya yükleme
meal_info_df = pd.read_csv('foodDemand_train/meal_info.csv')

# İlk beş satırı göstererek veri setinin bir örneğini incele
meal_info_df.head()

Buradaki sütunlar:

  • meal_id: Yiyeceğin benzersiz kimliği.
  • category: Yiyeceğin kategorisi.
  • cuisine: Yiyeceğin mutfak türü.

Şimdi de fulfulment_center_info.csv dosyasına bakalım:

# dosyayı yükle
fulfilment_center_info_df = pd.read_csv('foodDemand_train/fulfilment_center_info.csv')

# ilk beş satırı göstererek veri setinin bir örneğini incele
fulfilment_center_info_df.head()

Buradaki sütunlar,

  • center_id: Deponun benzersiz kimliği.
  • city_code: Deponun bulunduğu şehrin kodu.
  • region_code: Deponun bulunduğu bölgenin kodu.
  • center_type: Deponun tipi.
  • op_area: Deponun işletme alanı.

Veri Setlerini Analiz

Bu üç veriyi de inceledim ve eksik veri olmadığını gördüm. Şimdi bunları birleştirmeye çalışacağım.

Yukarıda verileri tek tek incelerken atamaları şu şekilde yapmıştım:

import pandas as pd

train_df = pd.read_csv('yol/train.csv')
meal_info_df = pd.read_csv('yol/meal_info.csv')
fulfilment_center_info_df = pd.read_csv('yol/fulfilment_center_info.csv')

Birleştirme işlemi için Pandas’ın merge fonksiyonunu kullanabilirim. Bu fonksiyon, iki DataFrame’i belirli bir sütuna (veya sütunlara) göre birleştirmek için kullanılıyor. Bunu yaparken de meal_info ve fulfilment_center_info veri setlerini, sırasıyla meal_id ve center_id üzerinden train veri seti ile birleştireceğim. Önce train_df ile meal_info’yu birleştireceğim:

merged_df = pd.merge(train_df, meal_info_df, on='meal_id', how='left')

Burada bu birleştirme işlemini “left” ile yaptım. Yani soldaki değeri baz alarak birleştirme yapıyor. Eğer soldaki DataFrame’de bir değer var ama karşılığında yoksa, NaN ile dolduruyor. Bu birleştirme işlemlerini daha iyi anlamak için şuradaki yazıya göz atılabilir. İlk birleştirme işleminden şu geldi:

Şimdi de bu DataFrame’i fullfilment_center_info_df ile birleştireceğim.

final_df = pd.merge(merged_df, fulfilment_center_info_df, on='center_id', how='left')

Şimdi de son olarak bu DataFrame’in boyutlarına bakmak istiyorum.

final_df.shape

Yani son durumda elde ettiğim df, 456 bin 548 satır ve 15 sütundan oluşuyormuş.

Daha önce train_csv de yaptığım gibi burada da anomali kontrolü yapmalıyım.

# temel istatistiksel özetleri almak için describe() fonksiyonu
statistical_summary = final_df.describe()

statistical_summary

Örneğin, num_orders sütunu için mean değeri yaklaşık 261.8 iken, max değeri 24299'dur. Bu büyük fark, potansiyel bir aykırı değer olabileceğine işaret eder. Benzer şekilde, checkout_price ve base_price için de min ve max değerleri arasında önemli bir fark var, bu da fiyatlar arasında büyük dalgalanmalar olduğunu gösteriyor.

Şimdi de her bir değişkenin dağılımına bakmak istiyorum.

import pandas as pd
import matplotlib.pyplot as plt

fig, axes = plt.subplots(3, 4, figsize=(20, 15))
axes = axes.ravel()
# değişkenlerin listesi (id ve diğer kategorik sütunlar hariç)
columns_to_plot = ['week', 'center_id', 'meal_id', 'checkout_price', 'base_price',
'emailer_for_promotion', 'homepage_featured', 'num_orders',
'city_code', 'region_code', 'op_area']
# histogramları çizdir
for i, col in enumerate(columns_to_plot):
axes[i].hist(final_df[col], bins=20, color='skyblue', edgecolor='black')
axes[i].set_title(col)
# göster
plt.tight_layout()
plt.show()

Histogram grafiklerinden görebildiğim kadarıyla num_order yani sipariş sayısının 0 ile 1000 arasında yoğunlaşmış. Ama dağılımı fazla olduğu için muhtemelen outliers yani aykırı değerlere sahip veriler var.

Aykırı değerlerin varlığı, iş modeli veya veri girişi hataları gibi faktörlere bağlı olarak normal veya olumsuz olabilir. Eğer bir iş modeli büyük sipariş miktarlarına veya büyük fiyat dalgalanmalarına izin veriyorsa, bu aykırı değerler işin doğası gereği normal olabilir. Ancak eğer bu tip büyük siparişler veya fiyat değişimleri beklenmiyorsa, bu durumda bu aykırı değerler veri girişi hatalarından veya diğer anomali durumlarından kaynaklanıyor olabilir.

Aykırı değerlerle ilgili genellikle kullanılan yöntem şu: önce bunları tespit et. Bunun için IQR yani Interguartile Range yöntemi kullanılıyor.

Bunun için genelde kullanılan boxplot modeline kısaca değiniyim.

cc

Yukarıdaki kutu grafiğinin (boxplot) temel bileşenleri şunlar:

  • Kutu (Box): Bu, verinin alt çeyreğini (Q1) ve üst çeyreğini (Q3) temsil ediyor. Kutunun içi, orta %50'lik kısmı (IQR) temsil eder ve medyanı da içerir.
  • Çizgiler (Whiskers): Kutunun dışına çıkan çizgiler, genellikle verinin geri kalan %25'lik kısımlarını gösterir. Ancak bazı durumlarda bu çizgiler, Q1'den 1.5IQR daha düşük ve Q3'ten 1.5IQR daha yüksek değerleri temsil eder.
  • Noktalar: Bunlar aykırı değerleri gösterir ve genellikle kutunun dışındaki tekil veri noktalarıdır.

Şimdi değişkenlerin box modeline bakıp, outliers var mı bakayım:

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# sayısal değişkenler için box model oluştur
plt.figure(figsize=(16, 10)) # Grafiğin boyutunu ayarlayın
sns.boxplot(data=final_df.select_dtypes(include=['float64', 'int64']), showfliers=True)
plt.yscale('log')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

Görebildiğim kadarıyla num_orders da birçok outliers var. Buna daha yakından bakmam gerekiyor.

num_orders için kutu grafiğini çizeyim.

plt.figure(figsize=(10, 8))
sns.boxplot(data=final_df, x=final_df['num_orders'], color='lightgreen')
plt.title('Num Orders Boxplot')
plt.xlabel('Number of Orders')
plt.show()

Tuhaf bir grafik çıktı. Çünkü çok fazla aykırı değer var. Bu, num_orders sütununda, genel eğilimin dışında kalan, beklenenden çok daha yüksek veya düşük sipariş sayıları olan veri noktaları olduğunu gösteriyor. Bu aykırı değerler, çok popüler veya hiç ilgi görmeyen yiyecekleri temsil ediyor olabilir. Bu durum, dağıtım planlamasında ve stok optimizasyonunda dikkate alınması gereken önemli bir faktör olmuş oldu bu haliyle.

Bu grafik fazla anlaşılır olmadığı için üzerinde çizimler yaparak neyin ne olduğunu göstermeye çalıştım:

Aykırı değerlerle ilgilenirken iki yol izleyebilirim: aykırı değerleri veri setinden çıkarabilir ya da bu değerleri başka bir yöntemle düzeltebilirim.

Ama aykırı değerlerin çıkarılması genellikle daha güvenli bir yaklaşım olarak kabul edilir. Çünkü bu değerlerin model üzerinde yaratabileceği potansiyel gürültüyü ve yanıltıcı etkileri ortadan kaldırır. (Tabii bu değerler tüm değerlerinizin çok küçük bir bölümünü oluşturuyorsa). Aykırı değerler, modelin genelleyebilirliğini ve tahmin performansını olumsuz etkiler yani. Özellikle stok yönetimi ve tahminlerde, gerçek dışı yüksek veya düşük sipariş sayıları, gelecekteki talebi tahmin ederken yanıltıcı olabilir.

Bu aykırı değerleri birazdan veri setinden çıkaracağım, ama öncesinde verileri analiz etmeye devam edeyim ve mesela siparişin depo tipine göre kırılımlarına bakayım.

orders_per_center_type = final_df.groupby('center_type')['num_orders'].sum()

# bar grafiği
plt.figure(figsize=(10, 6))
orders_per_center_type.plot(kind='bar', color=['blue', 'orange', 'green'])
plt.title('Total No. of Orders for Each Center type')
plt.xlabel('Center Type')
plt.ylabel('No. of Orders')
plt.yscale('log') # yeksenini logaritmik olsun
plt.show()

Graifkten anladığım kadarıyla, A tipindeki depo (veya merkez) en fazla sipariş alınan merkez olmuş. C tipindeki merkez ise en az sipariş almış.

Şimdi de bu yiyecek merkezlerini en fazla sipariş alanlarına göre görselleştirmek istiyorum.

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# top_centers dataframe
top_centers = final_df.groupby('center_id')['num_orders'].sum().nlargest(15).sort_values(ascending=False)
# bar grafiği
plt.figure(figsize=(14, 8))
sns.barplot(x=top_centers.index, y=top_centers.values, order=top_centers.index, palette="viridis", legend=False )
plt.yscale('log') # logaritmik
plt.title('Top Centers with Highest Orders')
plt.xlabel('Center Id')
plt.ylabel('Number of Orders')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

Burada önemli olabilecek bir başka şey ise bu merkezlerin hangi tipte olduğu.

Buna ek olarak hangi tipte merkezden kaçar tane olduğuna bakmak istiyorum.

import matplotlib.pyplot as plt
import seaborn as sns

# merkez tiplerine göre sayma
center_type_counts = final_df['center_type'].value_counts()
# grafik
plt.figure(figsize=(10, 6))
sns.barplot(x=center_type_counts.index, y=center_type_counts.values, palette='viridis')
plt.title('Number of Unique Centers by Center Type')
plt.xlabel('Center Type')
plt.ylabel('Count')
plt.show()

Type_A tipinden yaklaşık 43, type_c’den 19 ve type_b’den 15 tane varmış.

Bir başka merak ettiğim şey ise acaba en çok hangi yemek sipariş edilmiş?

import matplotlib.pyplot as plt
import seaborn as sns

# 'category' sütununa göre gruplama yaparak her bir kategori için toplam sipariş sayısını hesaplama
category_orders = final_df.groupby('category')['num_orders'].sum().sort_values(ascending=False)
# grafik
plt.figure(figsize=(14, 7))
sns.barplot(x=category_orders.index, y=category_orders.values, palette='viridis')
plt.title('Number of Orders for Each Category')
plt.xlabel('Category')
plt.ylabel('Number of Orders')
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()

Tek tek değişken analizinde bakmak istediğim son değişken ise hangi haftalarda ne kadar sipariş alındığı.

import matplotlib.pyplot as plt

# haftaya göre gruplandırma ve her hafta için toplam sipariş sayısını hesaplama
weekly_orders = final_df.groupby('week')['num_orders'].sum()

# zaman serisi grafiğini çizme
plt.figure(figsize=(14, 7))
plt.plot(weekly_orders.index, weekly_orders.values)
plt.title('Total Number of Orders Received Every Week')
plt.xlabel('Week')
plt.ylabel('Number of Orders')
plt.grid(True)
plt.tight_layout()
plt.show()

2.2) Çok Değişkenli Analiz

Bu bölümde ise iki ve daha fazla değişkeni analiz edip, birbiri ile ne kadar bağımlı olduklarını ve aralarındaki ilişkiye bakacağım.

import numpy as np
# sayısal sütunları seç
numeric_df = final_df.select_dtypes(include=[np.number])

# korelasyon matrisi
corr = numeric_df.corr()
# çiz
plt.figure(figsize=(14, 12))
sns.heatmap(corr, annot=True, fmt=".2f", cmap='coolwarm')
plt.title('Heatmap')
plt.show()

Yukarıdaki heatmap’i biraz incelemek istiyorum. Heatmap, veri setindeki değişkenler arasındaki ilişkileri gösteren bir korelasyon matrisi ve korelasyon katsayıları -1 ile 1 arasında değişiyor. 1 veya -1'e yakın değerler güçlü bir ilişkiyi, 0'a yakın değerler ise zayıf veya hiçbir ilişki olmadığını gösteriyor. Pozitif değerler doğru orantılı bir ilişkiyi, negatif değerler ise ters orantılı bir ilişkiyi ifade ediyor. Örneğin:

  • checkout_price ve base_price arasında güçlü pozitif bir korelasyon var (0.95), bu da beklenen bir durum çünkü genellikle base price ile ödeme sırasındaki fiyat birbiriyle yakından ilişkilidir.
  • emailer_for_promotion ve homepage_featured arasında orta şiddette pozitif bir korelasyon var (0.39), bu da bu iki promosyon taktiğinin birlikte kullanılma eğiliminde olduğunu gösteriyor.

Buna ek olarak şunu da göz önünde bulundurmak gerekiyor, eğer iki özellik arasında çok yüksek bir korelasyon varsa, bunlardan birini çıkarmak modelin performansını iyileştirebilir. Çünkü yüksek korelasyonlu özellikler modelin aşırı öğrenmesine (overfitting) neden olabilir.

Bu heatmap grafiği, yapılan indirim ile sipariş sayısını da bir kontrol etmem gerektiğini hatırlattı. Ona da bakayım:

import matplotlib.pyplot as plt

# indirim miktarı: base_price - checkout_price
final_df['discount'] = final_df['base_price'] - final_df['checkout_price']

# scatter plot
plt.figure(figsize=(10, 6))
plt.scatter(final_df['num_orders'], final_df['discount'], alpha=0.6)
plt.title('Scatter plot of discount versus num_orders')
plt.xlabel('Number of Orders')
plt.ylabel('Discount (Base Price - Checkout Price)')
plt.show()

Grafik şunu gösteriyor, aslında hiç indirim yokken de yani discount=0 olduğunda bile çok satış oluyormuş. Ama en fazla satış discount 100 ile 200 arasındayken olmuş.

Bununla birlikte negatif indirim varken de aslında satışlar olmaya devam etmiş. Bu da aciliyet veya özet etkinlikler durumlarında olmuş olabilir.

Sonuç olarak bu grafik, fiyatlandırma stratejisi, müşteri talebi ve promosyon etkinliklerinin etkinliği hakkında içgörüler sağlayabilir. İndirim miktarının artması ile sipariş sayısının nasıl değiştiğini anlamak, gelecekteki pazarlama ve fiyatlandırma kararları için önemli olabilir. Örneğin, çok yüksek indirimlerin sadece sınırlı bir etkisi varsa, bu, indirim stratejilerinin daha verimli bir şekilde ayarlanması gerektiğini gösterebilir.

3) Feature Engineering

Feature Engineering yani özellik mühendisliği kısaca, veri setindeki ham verileri makine öğrenimi modelleri tarafından daha iyi kullanılabilecek hale getirme sürecidir.

Bu işlem, modelin verilerden öğrenmesini ve tahminlerini geliştirmesini sağlar. Feature Engineering, modellerin karmaşık ilişkileri ve desenleri tanımasına yardımcı olur ve genellikle model performansında önemli iyileştirmeler sağlar.

İyi de neden Feature Engineering?

  • Ham verilerin doğrudan modeller tarafından kullanılması her zaman mümkün veya yeterli olmayabilir.
  • Feature Engineering, verilerdeki bilgiyi daha açık hale getirir ve modellerin bu bilgiyi öğrenmesine yardımcı olur.
  • Modellerin daha doğru ve güvenilir tahminler yapmasını sağlar.

Şimdi hızlıca birkaç feature çıkaracağım ve bunları en son aykırı değerlerden temizlediğim DataFrame’e ekleyeceğim.

Feature Engineerin yaparken genellikle elimizdeki verilerden, anlamlı olabilecek çeşitli değişkenleri kullanarak modelin genelleme yeteneğini artırmaya yardımcı olabileceğini düşündüğümüz feature’lar çıkarırız. Örneğin:

Zamanla ilgili özellikler (year_week )

Yıl içindeki hafta numarası ve mevsimsel bilgiler gibi zamanla ilgili özellikleri ekleyerek, modelin zamanın etkilerini anlamasına ve mevsimsel etkileri tahminlerine dahil etmesine yardımcı olması için iki yeni feature ekleyeceğim. Bu sayede yılın hangi haftasında verildiğini anlayabilir, mevsimsel etkileri ve yıl içindeki sipariş trendlerini görebilirim. Daha doğrusu model görebilir. Bu sayede bazı yemeklerin belirli haftalarda √eya mevsimlerde daha popüler olduğunu anlayabilir. Örneğin depoda dondurma varsa, muhtemelen yaz mevsiminde satışları daha fazla olacaktır. Veya çorba varsa muhtemelen kışın daha fazla talep olacaktır.

Şimdi bu feature’ları sıraya ekleyeceğim. İlki yılın hangi haftası olduğu:

# bu arada SettingWithCopyWarning uyarısı almamak için kopyalama işlemi yapıyorum.
# muhtemelen kopyalama işlemi yapmadan en aşağıdaki satırı çalıştırırsanız siz de alırsınız
# https://www.analyticsvidhya.com/blog/2021/11/3-ways-to-deal-with-settingwithcopywarning-in-pandas/

feature_df = final_df_cleaned.copy()
feature_df.loc[:, 'year_week'] = feature_df['week'].mod(52)

Sonraki de yılın hangi mevsimi olduğu bilgisi (season ):

feature_df.loc[:, 'season'] = feature_df['week'].apply(lambda x: (x // 13) % 4)

Fiyat değişikliği (price_change )

Fiyat indirimleri veya artışları gibi fiyat değişikliklerini gösteren bir özellik de eklemek istiyorum. Bunu da promosyonların sipariş miktarları üzerindeki etkisini anlamak için kullanabilirim. Eğer, checkout_price ile base_price arasında bir fark varsa bu bir promosyon veya indirimin olup olduğunu gösterir. Fiyat düşüşleri genellikle sipariş sayısında artışa yol açar.

feature_df.loc[:, 'price_change'] = feature_df['base_price'] - feature_df['checkout_price']

Promosyon etkisi (promotion_interaction )

Eğer bir siparişin e-mail promosyonu ve ana sayfada gösterildiği gibi bilgilere sahipsem, bunu mutlaka yeni bir feature olarak eklemem gerek diye düşünüyorum. Her iki tür promosyonun da varlığı, sipariş sayısını önemli ölçüde artırabilir.

# Eğer hem e-mail promosyonu hem de ana sayfa özelliği varsa, etkileşim özelliği
feature_df.loc[:, 'promotion_interaction'] = feature_df['emailer_for_promotion'] & feature_df['homepage_featured']

Merkez Türüne Göre Fiyatlandırma (avg_price_per_center_type)

Farklı merkez türlerinin ortalama fiyatları, müşteri talebi üzerinde bir etkiye sahip olabilir o yüzden bunu farklı merkez türlerinin fiyatlandırma stratejilerini anlamak için kullanabilirim.

feature_df.loc[:, 'avg_price_per_center_type'] = feature_df.groupby('center_type')['checkout_price'].transform('mean')

Güzel. Birçok yeni feature ekledim. Ama bu kadarı yetmez diye düşünüyorum ve hızlıca birkaç özellik daha eklemek istiyorum.

base_price_max: Bir yemeğin tüm zamanlar ve lokasyonlar için maksimum baz fiyatını belirler. Bu, bir yemeğin fiyat tavanını ve potansiyel kar marjını anlamak için önemlidir.

base_price_mean: Belirli bir yemeğin ortalama baz fiyatını verir. Fiyatlandırma stratejilerini planlarken kullanılabilir.

base_price_min: Bir yemeğin mümkün olan en düşük satış fiyatını gösterir, bu da promosyonlar ve indirimler için bir temel oluşturabilir.

center_cat_count: Belirli bir merkezdeki belirli bir kategoriye ait siparişlerin sayısını hesaplar. Hangi kategorinin popüler olduğunu anlamak için kullanılır.

center_price_rank: Bir merkezdeki yemeklerin fiyatına göre sıralamasını verir ve rekabetçi fiyatlandırma analizi için yararlıdır.

meal_count: Bir yemeğin tüm merkezlerdeki toplam satış sayısını verir ve popülerlik açısından önemlidir.

meal_price_max: Bir yemeğin tüm lokasyonlar için maksimum satış fiyatını belirler, bu da en yüksek fiyat noktasını gösterir.

meal_price_mean: Ortalama satış fiyatı, genel fiyat seviyesi hakkında bilgi verir ve müşteri talebinin fiyat duyarlılığını anlamada kullanılabilir.

meal_week_count: Bir yemeğin haftalık toplam satış sayısını verir ve zaman içindeki talep dalgalanmalarını gösterir.

region_meal_count: Belirli bir yemeğin bir bölgedeki toplam satışlarını gösterir ve bölgesel popülerlik veya pazar doygunluğunu değerlendirmek için kullanılır.

Hızlıca bunları yaptığım kodu yazayım:

import pandas as pd
import numpy as np

# base_price_max, base_price_mean, base_price_min hesaplamaları
feature_df['base_price_max'] = feature_df.groupby('meal_id')['base_price'].transform('max')
feature_df['base_price_mean'] = feature_df.groupby('meal_id')['base_price'].transform('mean')
feature_df['base_price_min'] = feature_df.groupby('meal_id')['base_price'].transform('min')
# center_cat_count hesaplaması
feature_df['center_cat_count'] = feature_df.groupby(['center_id', 'category'])['id'].transform('count')
# center_price_rank hesaplaması
feature_df['center_price_rank'] = feature_df.groupby(['center_id'])['checkout_price'].rank("dense", ascending=False)
# meal_count hesaplaması
feature_df['meal_count'] = feature_df.groupby('meal_id')['id'].transform('count')
# meal_price_max, meal_price_mean, meal_price_min hesaplamaları
feature_df['meal_price_max'] = feature_df.groupby('meal_id')['checkout_price'].transform('max')
feature_df['meal_price_mean'] = feature_df.groupby('meal_id')['checkout_price'].transform('mean')
feature_df['meal_price_min'] = feature_df.groupby('meal_id')['checkout_price'].transform('min')
# meal_week_count hesaplaması
feature_df['meal_week_count'] = feature_df.groupby(['meal_id', 'week'])['id'].transform('count')
# region_meal_count hesaplaması
feature_df['region_meal_count'] = feature_df.groupby(['region_code', 'meal_id'])['id'].transform('count')

Bu kadar feature ekledik.

Kategorik Verilerin Dönüştürülmesi

Machine learning modelleri sayısal veriyle çalışır. cuisine , category , center_type gibi değişkenler kategorik olduğu için bunları one-hot encoding ile sayısal değişkenlere dönüştürmeliyim.

Bu sayede örneğin cuisin için yani her mutfak türü için ayrı sütunlar oluşturulur ve ilgili mutfak türü için sipariş varsa 1, yoksa 0 değeri atanır.

# kategorik değişkenlerin listesini oluştur.
categorical_vars = ['cuisine', 'category', 'center_type']
# her bir kategorik değişken için one-hot encoding uygula
for var in categorical_vars:
dummies = pd.get_dummies(feature_df[var], prefix=var)
feature_df = pd.concat([feature_df, dummies], axis=1)

Şimdi de sıra daha önce bahsettiğim aykırı değerleri çıkarmaya geldi. Bunun için Q1, 3 ve IQR değerlerini önce bulmalıyım ve bunlar dışında kalan kısımları DataFrame’den şutlamalıyım.

# öncelikle IQR değerini hesaplama.
Q1 = feature_df['num_orders'].quantile(0.25)
Q3 = feature_df['num_orders'].quantile(0.75)
IQR = Q3 - Q1

# aykırı değerler için alt ve üst sınırları belirleme
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# aykırı değerleri göstermek için bir filtre oluşturma
outliers = feature_df[(feature_df['num_orders'] < lower_bound) | (feature_df['num_orders'] > upper_bound)]
# aykırı değerlerin sayısını yazdır
print("Aykırı değerlerin sayısı:", outliers.shape[0])
# aykırı değerleri çıkarmak için veri setinden çıkarma kısmı
final_df_cleaned = feature_df[(final_df['num_orders'] >= lower_bound) & (feature_df['num_orders'] <= upper_bound)]

Toplam çıkarılan aykırı değer sayısı 32937. Peki son durumda yeni oluşan DataFrame’in boyutları ne oldu?

final_df_cleaned.shape

Güzel. Elimizde aykırı olmayan, yani alt sınır ve üst sınır arasında kalan tam tamına 423 bin 611 değer var. Böylece sadece daha gerçekçi sipariş sayılarını içeren bir veri setim oldu.

4) Ölçeklendirme (scaling)

Scaling, farklı özelliklerin (features) aynı ölçeğe getirilmesi olarak geçiyor. Farklı özelliklerin farklı ölçeklerde olması, bazı makine öğrenimi algoritmalarının yanıltıcı sonuçlar vermesine neden olabiliyormuş.

Örneğin, bir özellik 129813489 ve benzeri kadar büyük değerlere sahio, ama diğerleri ondalık birimlerse, büyük ölçekli özellik modelin karar sürecinde aşırı ağırlık kazanabilir. Bu, modelin daha küçük ölçekli özelliklerin önemini göz ardı etmesine ve yanlış genellemeler yapmasına neden olabiliyormuş.

Özellik ölçeklendirme yapmak için 3 yaygın yöntem vardır:

  1. Standardization: Her bir özelliğin ortalaması 0 ve standart sapması 1 olur.
  2. Normalization: Her bir özelliğin değerleri 0 ile 1 arasındadır.
  3. Min-Max Scaling: Her bir özelliğin minimum değeri 0 ve maksimum değeri 1 olur.

Ben burada standardization’ı kullanacağım. Bunun için şu kodu yazıyorum:

import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

# Sayısal özellikleri seç
numerical_features = final_df_encoded.select_dtypes(include=['int64', 'float64']).columns.tolist()

numerical_features.remove('id')
numerical_features.remove('num_orders')

# ölçeklendirme
final_df_scaled = final_df_cleaned.copy()

final_df_scaled[numerical_features] = scaler.fit_transform(final_df_cleaned[numerical_features])

5) Model Seçimi ve Hiperparametre Ayarlama

Bu aşamada, XGBoost gibi bir gradient boosting modelini kullanarak tahmin modelimizi eğitmek istiyorum. Hatta sadece XGBoost değil, LightGBM ve Random Forest modellerini de kullanacağım ve sonuçlarını karşılaştıracağım. Hangisi daha iyi sonuç verirse onu kullanırım.

XGBoost genellikle karmaşık veri setleri üzerinde iyi performans gösteren ve genellikle yüksek doğruluk sağlayan güçlü bir modeldir.

Modeli eğitmeden önce veriyi eğitim ve test setlerine ayırmam gerekiyor tabii ki.

Ayrıca kullanacağım Evaluation metrikleri de RMSE, MAE ve MAPE olacak. Her birini burada uzun uzun anlatmayacağım, matematiksel ifadeleri aşağıdaki gibi.

Öncelikle eğitim seti hazırlayacağım için, veriden hedef değişkenim olan num_orders değişkenini ve değerlerini kaldırmam gerekiyor. Daha sonrasında ise scikit-learn kütüphanesinden train_test_split fonksiyonunu kullanarak tüm verinin yüzde 80'ini train, kalan %20'sini ise test için ayıracağım.

Veriyi test ve train olarak ayırmadan önce hedef değişkeni train setinden ayırdığımdan emin olmalıyım.

from sklearn.model_selection import train_test_split

X = final_df_scaled.drop('num_orders', axis=1) # hedef değişken dışındaki tüm sütunlar
y = final_df_scaled['num_orders'] # hedef değişken

# train test setlerine bölme
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

Veriyi %20 test, %80 eğitim olacak şekilde ayırdım. Şimdi sıra sıra modelleri eğitmeye başlayacağım. Önce Random Forest, sonra LightGBM ve en son XGBoost.

Random Forest

from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error

# random forest modeli
rf = RandomForestRegressor(n_estimators=100, random_state=42, verbose=1)
# modeli eğitim verileriyle eğit
rf.fit(X_train, y_train)
# tahminleri hesapla
y_train_pred = rf.predict(X_train)
y_test_pred = rf.predict(X_test)
# RMSE hesapla
train_rmse = mean_squared_error(y_train, y_train_pred, squared=False)
test_rmse = mean_squared_error(y_test, y_test_pred, squared=False)
# MAE hesapla
train_mae = mean_absolute_error(y_train, y_train_pred)
test_mae = mean_absolute_error(y_test, y_test_pred)
# MAPE hesapla
train_mape = mean_absolute_percentage_error(y_train, y_train_pred)
test_mape = mean_absolute_percentage_error(y_test, y_test_pred)

Eğitim için --> RMSE: 166.4105 MAE :90.2215 MAPE: 0.7160
Test içim d --> RMSE: 180.8169 MAE: 92.7225 MAPE: 0.7262

LigthGBM

import lightgbm as lgb
from sklearn.metrics import mean_squared_error, mean_absolute_error, mean_absolute_percentage_error

# LightGBM modelini başlat
lgbm = lgb.LGBMRegressor(random_state=42, n_jobs=-1, verbose=1)
# Modeli eğit
lgbm.fit(X_train, y_train)
# Tahminleri hesapla
y_train_pred = lgbm.predict(X_train)
y_test_pred = lgbm.predict(X_test)
# Performans metriklerini hesapla
train_rmse = mean_squared_error(y_train, y_train_pred, squared=False)
test_rmse = mean_squared_error(y_test, y_test_pred, squared=False)
train_mae = mean_absolute_error(y_train, y_train_pred)
test_mae = mean_absolute_error(y_test, y_test_pred)
train_mape = mean_absolute_percentage_error(y_train, y_train_pred)
test_mape = mean_absolute_percentage_error(y_test, y_test_pred)

Eğitim --> RMSE: 155.6238 MAE: 84.3433 MAPE: 0.6553
Test --> RMSE: 162.0462 MAE: 85.3488 MAPE: 0.6626

XGBoost

import xgboost as xgb
from sklearn.metrics import mean_squared_error, mean_absolute_error, mean_absolute_percentage_error

# XGBoost modelini başlat
xgb_model = xgb.XGBRegressor(random_state=42, n_jobs=-1, verbosity=1)
# Modeli eğit
xgb_model.fit(X_train, y_train)
# Tahminleri hesapla
y_train_pred = xgb_model.predict(X_train)
y_test_pred = xgb_model.predict(X_test)
# Performans metriklerini hesapla
train_rmse = mean_squared_error(y_train, y_train_pred, squared=False)
test_rmse = mean_squared_error(y_test, y_test_pred, squared=False)
train_mae = mean_absolute_error(y_train, y_train_pred)
test_mae = mean_absolute_error(y_test, y_test_pred)
train_mape = mean_absolute_percentage_error(y_train, y_train_pred)
test_mape = mean_absolute_percentage_error(y_test, y_test_pred)

Eğitim --> RMSE: 125.6392 MAE: 70.5424 MAPE: 0.5456
Test --> RMSE: 142.7359 MAE: 73.7876 MAPE: 0.5602

Tüm sonunçları bir görmek istiyorum. O yüzden şöyle bir tablo hazırladım:

Sonuçları değerlendirmeye geçiyorum.

Random Forest modelim eğitim verilerinde 166.41'lik bir RMSE değeri verirken, test setinde 180.81'e çıkıyor. Bu, modelimin genellemeyi iyi yapmadığını ve eğitim verilerine biraz fazla uyduğunu (overfitting) gösteriyor. MAE ve MAPE değerleri de bu durumu destekliyor; her ikisi de eğitim ve test setleri arasında benzer ve nispeten yüksek. Bu durum, bazı ayarlamaların yapılmasını gerektiriyor gibi görünüyor.

LightGBM modelim ise biraz daha iyi performans gösteriyor. Eğitim setinde 155.62 RMSE değerine sahipken, test setinde bu değer 162.04'e yükseliyor. Bu, Random Forest’a göre daha iyi bir genelleme yapabildiğini gösteriyor. MAE ve MAPE değerleri de buna paralel bir iyileşme sergiliyor.

XGBoost modelimde ise en iyi sonuçları gözlemliyorum. Eğitim setinde 125.63 RMSE değeri elde ederken, test setinde bu değer sadece biraz artarak 142.73'e çıkıyor. Bu, modelimin hem eğitim verilerine iyi uyduğunu hem de yeni verilere iyi genelleme yaptığını gösteriyor. MAE ve MAPE metrikleri de bu iyileşmeyi onaylıyor; her iki sette de diğer modellere göre daha düşük hata oranlarına işaret ediyor.

Bu sonuçlar ışığında, XGBoost modelimin mevcut durumda en iyi performans gösteren model olduğunu düşünüyorum. Ancak, modelin hala iyileştirilmesi gerektiğini ve özellikle Random Forest modelinin overfitting sorununu çözmem gerektiğini anlıyorum. Sonraki yazıda bunları nasıl daha da iyileştirebileceğim üzerine kafa yormayı planlıyorum.

--

--