Python ile Mağaza-Ürün Talep Tahmin Modeli (Store-Item Demand Forecasting)🛒

Hilal Gözütok Şakar
12 min readMay 11, 2021

Herkese merhaba!

Bu yazımda, Python ile Makine Öğrenimi tekniğini kullanarak uçtan uca bir talep tahmin modeli gerçekleştireceğim.

Keyifli okumalar dilerim…

Photo by Alexandru Tugui on Unsplash

1) Veri Setini Tanıyalım

Elimizde 10 farklı mağaza ve 50 farklı ürün çeşidine ait 5 yıllık satış verilerini içeren bir retail veri seti var.

Amacımız store-item kırılımında 3 aylık ürün satışlarını tahmin edebilmek.

Veri setine buradan erişebilirsiniz.

Hazırsanız başlayalım!

📚 Öncelikle gerekli düzenlemeleri yapıp, kütüphaneleri ve veri setini import ettim.

Kütüphaneler ve eklentiler
Veri setlerinin okutulması, train ve test veri setinin birleştirilmesi
Veri setine ilk bakış

Değişkenler

  • date — Satış verilerinin tarihi
  • store — Mağaza ID
  • item — Ürün ID
  • sales — Belirli bir tarihte belirli bir mağazada satılan ürün sayısı

→ id değişkeninde NaN değerlerin olması train veri setinde değişkenin bulunmamasından dolayıdır.

2) Keşifçi Veri Analizi (EDA)

Veri setindeki minimum ve maximum tarihi gözlemleyecek olursak, buradan da elimizdeki veri setinin toplamda 5 yıllık bir datayı içerdiğini doğrulayabiliriz.

  • Train Veri Seti

◾ Toplamda 913000 gözleme sahip

◾ date, store, item, sales olmak üzere toplamda 4 adet değişkene sahip

◾ Hiç NA değer yok

  • Test Veri Seti

◾ Toplamda 45000 gözleme sahip

◾ id, date, store, item olmak üzere toplamda 4 adet değişkene sahip

◾ Hiç NA değer yok

  • Sample Submission Veri Seti

◾ Toplamda 45000 gözleme sahip

◾ id ve sales olmak üzere toplamda 2 adet değişkene sahip

◾ Hiç NA değer yok

✉ Kaggle’a göndereceğimiz submission dosyası bu formatta olacak.

Veri setinde aykırı (outlier) değer var mı?

Outlier değer yok

Veri setinde eksik (missing) değer var mı?

id değişkeninde 913000 adet eksik değer var. Bunun sebebi train veri setinde id bulunmamasıdır. Aynı şekilde sales değişkenindeki 45000 adet gözlemin eksikliği de, test veri setinde sales değişkeninin bulunmamasından kaynaklanıyor.

Başlarken train ve test veri setlerini birleştirdiğimiz için böyle bir durumla karşılaştık. Dolayısıyla şuanda veri setinde herhangi bir eskiklik ya da aykırılık söz konusu değildir.

Satış dağılımı nasıl ?

Sales değişkeninin betimsel istatistiği

Kaç store var ?

Eşsiz mağaza sayısı

Kaç item var ?

Eşsiz ürün sayısı

Her store’da eşit sayıda mı eşsiz item var ?

Mağazalardaki eşsiz ürün sayıları

Store - item kırılımında satışların betimsel istatistikleri nedir ?

Her store’daki toplam ürün satış sayıları farklılık gösteriyor

EDA kısmında da, veri seti hakkında az çok fikir edindiysek 3. bölüme geçebiliriz.

3) Değişken Mühendisliği (Feature Engineering)🔍

Geçmişe ilişkin level, trend, mevsimsellik ve model bilgilerini, üreteceğimiz değişkenlerle yeni modele yansıtmamız gerekiyor.

Yeni değişkenler üretmeden önceki değişken sayımız?

Mevcut gözlem sayısı

◾ Date Features (Tarih Değişkenleri) 📅

Tarihle ilgili değişkenler
  • month — Veri setindeki ay bilgisini verir. Mevsimselliği yakalamak için önemli bir değişken.
  • day_of_month — Ayın günleri
  • day_of_year — Yılın günleri
  • week_of_year — Yılın haftası
  • day_of_week — Haftanın gün bilgisini verir. Pazartesi günü 0 olarak kabul edildiği için +1 eklemesi yapıldı.
  • year — Yıl
  • is_wknd — “Hafta sonu mu, değil mi?” bilgisini bize veren değişken. Kullandığımız weekday metodunda haftanın başlangıcı yani Pazartesi günü 0 olarak kabul edilir. Veri setindeki ilk gün Salı günüdür. Yani 1 değeri ile başlıyor. Bundan yararlanarak, alışverişlerin sıklıkla gerçekleştiği hafta sonu günlerini yakalamak için 4'e bölümünden kalan sorgusunu gerçekleştirdik. Sonuç olarak Cuma, Cumartesi ve Pazar günleri 1 olarak dönerken diğer günler 0 oldu.
  • is_month_start — Ayın başlangıç bilgisini verir.
  • is_month_end — Ay sonu bilgisini verir.

0. indexe bakarsak, veri seti 2013–01–01 tarihi ile başlıyor. Bu tarih yeni oluşturduğumuz yılın ilk günü, ilk haftası ve ilk ayı değişkenlerinde 1 değerini alırken, haftanın day_of_week değişkeninde 2 değerine sahip. Yani bu da Salı gününü ifade ediyor.

📊 Aylara göre ürün satışların özet istatistikleri ?

store-item-month kırılımında sales’a ait count, sum, mean, median ve standard deviation değerleri

◾ Random Noise (Rastgele Gürültü)📢

Zaman serisi ve makine öğrenimi modellerinde overfitting (aşırı öğrenme) kavramının önüne geçmek için, veri setindeki değişkenler üzerinden yeni değişkenler türetirken, var olan değişkenlerin özelliklerine random noise (rastgele gürültü) eklenir.

Random noise fonksiyonu
Pattern’ı bozmak adına veriye rastgele değerler(gürültü) eklendi

◾ Lag/Shifted Features (Geçmiş Verilere İlişkin Değişkenler) 💼

Öncelikle elimizdeki verileri daha düzenli bir formata getirelim.

check_df fonksiyonuyla df’in son haline bir bakalım:

check_df(df)

head bölümünü dikkate alırsak;

Elimizdeki sales verisi ile bir önceki sales verisini aynı satıra getirip karşılaştırma yapmaya çalışalım.

Lag hesaplama

sales kolonunda dataframedeki orijinal sales değerlerimiz bulunuyor.

lag1, lag2, lag3 ve lag4 ile ifade edilen değerler ise gecikmeleri yani bir önceki değerleri bulunduruyor.

Yukarıdaki çıktıya göre yorum yapalım; solda veri setine ait sales değerleri, sağda ise bu sales değerlerine ek olarak lag1, lag2, lag3 ve lag4 değerlerini gözlemlemekteyiz.

2. indeksteki 14 değerini incelediğimizde; sol tarafta 14'ten önce 11 ve 13 değerleri bulunuyor. Bu nedenle sağ tarafta lag1 = 11, lag2 =13 , 13'ten önce başka geçmiş değer bulunmadığı için lag3 ve lag4 NaN olarak ifade edilmektedir.

Diğer indeksleri de bu şekilde inceleyebilirsiniz.

Bunu yapmamızın temel amacı “Zaman Serisi ve Tahmini” yazımda da bahsettiğim üzere, zaman serisindeki modellerin (örneğin SES modeli) kendinden bir önceki değerden daha çok etkileniyor olmasıdır.

İlgili yazıya buradan ulaşabilirsiniz.

Lag/shifted işleminin fonksiyonlaştırılması
Hesaplamada kullanılacak periyotların belirlenmesi

Neden bu değerleri seçtik ?

Problemin başına dönecek olursak bizden 3 aylık bir tahmin yapmamız isteniyor. Bu nedenle periyotları belirlerken 3 aylık tahminle başlayıp, 1 haftalık süreyi üzerine ekleyerek devam ettik.

Şimdi veri setinin son haline bir bakalım.

Değişkenlerimiz çoğaldığı için çıktıyı 3 parçada paylaşacağım.

Çıktı-I
Çıktı-II
Çıktı-III

◾ Rolling Mean Features (Hareketli Ortalamala Değişkenleri)🎠

Rolling mean features

Hareketli ortalama rolling fonksiyonu ile hesaplanır. Yukarıdaki kod parçasında ifade edilen window ise, hareketli ortalamada kendinden önce kaç gecikme (lag) dikkate alınacağımızı göstermektedir.

Yine 2. indekse bakarak yorum yapmaya çalışalım; sales değeri 14 olan değerden önce yanlızca 2 adet sales değeri bulunuyor. Bunlar 13 ve 11 değerleridir.

Kendisi de dahil olmak üzere roll2 (window=2) hareketli ortalamasını hesaplayacak olursak:

roll2 değerinin hesaplanması

Kendisi de dahil olmak üzere roll3 (window=3) hareketli ortalamasını hesaplayacak olursak:

roll3 değerinin hesaplanması

Bu hesaplamayı yaparken gözlem değerinin kendi değerini de hesaba kattık. Ama biz geçmiş değerlere ilişkin trende ulaşmaya çalışıyoruz. Bunun için de mevcut değerden bağımsız yeni bir feature türetmeye çalışmalıyız ki ancak o zaman geçmişteki trendi ifade edebilelim.

Peki bunu nasıl yapabiliriz?

Sales değerinin kendisini çıkarmak için hareketli ortalama hesabını yapmadan önce 1 adım shift/lag (gecikme) yaparsak problem çözülecektir. ✔

Shift işlemi uygulanmış rolling mean hesabı
Yukarıdaki işlemin fonksiyonlaştırılmış hali
Veri seti genel görünüm ➡ df.head(5)

En sondaki yeni oluşturduğumuz 2 değişkene odaklandığımızda ilk 5 indeksteki değerlerin hepsinin NaN olduğunu görüyoruz.

Bunun sebebi nedir ❓

date değişkeninde göreceğimiz üzere bu çıktı veri setinin 2013 yılınının ilk ayına ait verileri içermektedir. Train veri seti ise 2017 tarihi ile sonlanmaktadır. Bizim tahmin etmek istediğimiz değerler zaten 2018'in ilk 3 ayına ait olacak. SES modelinden de hatırlayacağımız üzere tahmin edeceğimiz bu değerler yakın geçmişten daha fazla etkileneceğinden, 2017'nin son aylarına ait hareketli ortalama değişkenleri NaN olmayacaktır.

Veri seti genel görünüm ➡ df.head(100)

◾ Exponentially Weighted Mean Features (Üssel Ağırlıklı Ortalama Değişkenleri)

Üssel ağırlıklı ortalama hesabı

Üssel ağırlıklı ortalama ewm fonksiyonu ile hesaplanır. shift(1) yani “1 adım kaydır” ifadesini bir önceki örnekte de hesaplama yaparken, değişkenin kendi değerinin alınmaması için kullanmıştık. Farklı olarak burada alpha değeri eklenmiş oldu.

Alpha, geçmiş değerlere ne kadar önem vereceğimizi belirttiğimiz bir parametredir. Yakın geçmişe ağırlık vermek istersek alpha değerini 1'e yakın seçeriz, uzak değerlere önem vermek istiyorsak da 0'a yakın bir değer belirleyebiliriz.

Şimdi çıktıyı yorumlamaya geçebiliriz.

Yine 2. indeks üzerinden gidelim. sales değerimiz 14, roll2 değerimiz 12, ewm099 değişken hesabına ait alpha değeri ise 0.99 olarak alınmış. Yakın geçmişe daha fazla ağırlık verilmesini istediğimiz için alpha değeri 1'e daha yakın. Bu nedenle üssel ağırlıklı ortalama sonucumuz en yakın geçmiş olan 11 değerine daha yakın olarak 11.09 çıktı.

ewm01 değişken hesabına ait i alpha değeri ise 0.1 olarak alındı. Bir önceki durumun tam tersi olarak bu sefer 11 değerine daha uzak olmasını bekleriz. Nitekim sonucumuz 11.94 olarak elde edilmiştir. Aynı şekilde diğer gözlemleri de yorumlayabilirsiniz.

EWM işleminin fonksiyonlaştırılmış hali

ewm_features fonksiyonu df dışında iki tane daha parametre almaktadır. Biri alpha değerlerini diğeri ise gecikmeleri (lags) temsil etmektedir.

alpha ve lags değerleri

En başta 4 değişkenle yola çıkmıştık. Şu anda elimizde 71 adet yeni değişkenimiz var!

Gözlem sayısı
Kolon isimleri

◾ One-Hot Encoding Yöntemi

store, item, day_of_week ve month değişkenleri veri setinden çıkarıldı

One-Hot Encoding işleminden sonra 146 adet yeni değişkenimiz oldu.

Logaritmik Dönüşüm

Gradient Descent kullandığımız yöntemlerde değişkenin orijinal halinde kalması iterasyon sayısını artırabilir. Bunun önüne geçmek için bağımlı değişken (sales) üzerinde logaritmik dönüşüm uygulayacağız.

log1p dönüşümü

Tekrar kullanacak olduğumuz zaman geri dönüştürmeyi unutmayalım❗

4) Custom Cost Function

Cost Function’ların Custom olması ne anlama gelir?

Bir iterasyonda train hatasıyla validasyon hatasının birbirinden çok farklılaştığı noktadaki durumu overfitting olarak değerlendiririz. Hata metriğine göre gözlem yaparak (manuel) bu duruma karar veririz. Dolayısıyla bu hatayı ilgili problem için ölçecek olan fonksiyon öyle bir fonksiyon olmalı ki, bana nerede duracağımı farklı alternatiflere bakarak göstermeli.

Regresyon problemleri için genel olarak en sık kullandığımız cost functionlar şu şekildeydi; MSE, RMSE, MAE.

Bu problem için aslında MAE fonksiyonu kullanılabilir. Ama biz burada farklı olarak MAPE ve SMAPE fonksiyonlarını ele alacağız.

MAPE — Ortalama Mutlak Yüzde Hata (Mean Absolute Percentage Error)

MAPE formülü

Regresyon ve zaman serileri modellerinde tahminlerin doğruluğunu ölçmek için ortalama mutlak yüzde hata sıkça kullanılmaktadır.Gerçek değerler arasında sıfır içerenler varsa, sıfır ile bölünme olacağı için MAPE hesaplanamaz. Çok düşük tahmin değerleri için yüzde hatası %100’ü geçemez, ancak çok yüksek tahmin değerleri olduğunda yüzde hatasının üst sınırı yoktur.

SMAPE — Simetrik Ortalama Mutlak Yüzde Hata (Symmetric Mean Absolute Percentage Error

SMAPE formülü

“Adjusted MAPE”

Öğeler için sıfır veya sıfıra yakın talep olduğunda Ortalama Mutlak Yüzde Hata (MAPE) alternatifidir. SMAPE, bu düşük hacimli öğelerin etkisini azaltarak,% 200’lük bir hata oranına kendini sınırlar. Düşük hacimli öğeler sorunludur, aksi halde genel hata oranını çarpıtan sonsuz yüksek hata oranlarına sahip olabilirler.

SMAPE, tahmini eksi gerçek değerleri, bu formülde ifade edilen tahminlere ve fiili toplamlara bölünür.

SMAPE formülünün fonksiyonlaştırılması

Artık model kurabiliriz 🦾

5) Model Validation

Validasyon setini nasıl belirleyeceğiz?

Klasik olarak train ve test setini split edip modele sokabiliriz. Fakat sonuçlar yanıltıcı olacaktır. Bu nedenle validasyonu zaman ayarlı yapmamız gerekiyor.

◾ Time - Based Validation Sets

Bizden 2018 yılının ilk 3 ayına ait tahminler bekleniyordu.

test seti min-max date
train seti min-max date

Train ve validasyon setini neye göre seçeceğiz ?

2017'nin başına kadar (2016'nın sonuna kadar) olan verileri train seti olarak belirleyelim.

Train setinin belirlenmesi

2017'nin ilk 3'ayına ait verileri de validasyon seti olarak belirleyelim.

Validasyon setinin belirlenmesi

Train ve validasyon setlerimiz hazır. Şimdi bağımlı ve bağımsız değişkenleri tespit etmemiz gerekiyor.

Bağımsız değişkenlerin seçilmesi
Train seti bağımlı ve bağımsız değişkenlerin belirlenmesi
Validasyon seti bağımlı ve bağımsız değişkenlerin belirlenmesi

🔸 LightGBM Model

— LightGBM Parameters

metric : {mae} ➡ Düzenlileştirme terimidir. Mutlak normu ifade eder.

num_leaves ➡ Max yaprak sayısı.

learning_rate ➡ Modelin öğrenirken atacağı adımı sembolize eder.

feature_fraction ➡ Her iterasyonda LightGBM için göz önünde bulundurulacak değişken sayısını ifade eder.

max_depth ➡ Ağacın max derinliği.

verbose ➡ Ekrana verilecek bilgiyi ifade eder.

num_boost_round ➡ İterasyon sayısı.

early_stopping_rounds ➡ Burada belirlediğimiz parametreye kadar validasyon setinde artık hata değeri düşmüyorsa, modelin num_boost_round’da belirlediğimiz iterasyona kadar çalışmasına gerek kalmaz, durur. Overfitting’in önüne geçmek için önemli bir parametredir.

nthread : -1 ➡ Tüm işlemcilerin ful performans kullanılmasını sağlar. İşlemci sayısı yazılarak da kullanılabilir.

İterasyon sonuçları

Kırmızıyla işaretlediğim verbose_eval parametresi ile her 100 iterasyonda raporlama istediğimizi belirttik.

Turuncuyla işaretlediğim kısımdaki “Daha önce early stopping ile karşılaşılmadı” mesajı ise bize iterasyon sayısını artırmamız konusunda ipucu vermektedir. Bu nedenle en iyi iterasyonu 1000. iterasyon olarak seçti.

Daha önce logaritmik dönüşüm uyguladığmız sales değişkenini tekrar eski haline çevirip yüzdelik olarak hata değerine (smape) bakalım.

🥇 Değişken Önem Düzeyleri

plot_importance-I

split ➡ İlgili feature’ın bütün modelleme süresinde kaç defa split edilmek üzere kullanıldığını gösterir.

gain ➡ Bölmeden önce ve böldükten sonraki entropi değişimini ifade eder.

Bu tabloya göre mevsimsellik hakkında yorum yapabiliriz. Mesela sales_lag_364 değişkeninin geçmiş değerlerdeki mevsimsellik ile ilgili bilgi taşıdığını söyleyebiliriz. Split ve gain değerlerini de göz önünde bulundurarak sezon sonlarında bir pattern yakalandığı çıkarımını rahatlıkla yapabiliriz.

plot_importance-II

◾◾ Final Model ◾◾

Tüm verilerle final modelini kuralım.

Test ve train seti ayrıldı
LightGBM parameters
LightGBM’in kendi kütüphanesi kullanıldı
Tahmin değerleri

Adım 1: Daha önce logaritmik dönüşüm yaptığımız tahmin değerlerini tekrar eski haline çevirip sales değişkenine attık.

Adım 2: Çıktıda ondalık ifade görmek istemediğimiz için astype ile değerleri integer’a çevirdik.

Adım 3: Kaggle’a gönderebilmek için submission_df dosyasını csv olarak kaydettik.

◼ Artık submission.csv dosyasını Kaggle’a yükleyebiliriz…

📣Kaggle Sonuçları

Bir sonraki yazımda görüşmek üzere…🎈

👀 İlgili proje kodlarının tamamına buradan ulaşabilirsiniz.

📃 Zaman Serisi ve Tahmini ile ilgili yazıma buradan ulaşabilirsiniz.

.

.

.

.

Kaynaklar :

--

--