Sonuç

Sonuçlar ve Bu Model Nerede Yetmez

Ne kadar iyileşti, hangi varsayımlar tutmadı, üretimde ne değişir.

Bu dizi bir sözle açılmıştı: sıfır yeni ürünle daha çok satış. Depodan tek koli çıkmadan, üretime tek kuruş verilmeden, yalnızca yanlış yerde duranı doğru yere taşıyarak. Şimdi hesabı verme vakti.

Referans senaryoda — coverMevcut stoğun satış hızına bölünmesiyle bulunan yeterlilik süresi; kaç haftalık satışı karşılayacağını söyler. eşiği 6 hafta, alıcıda asgari haftalık satış 1 adet — açgözlünün çıkardığı plan şu: 252 option, 4.785 adet, 4,98 milyon TL net etki. Aşağıdaki demoda kadranı çevirip bu sayıların eşiklerle nasıl değiştiğini kendiniz görebilirsiniz; yazıdaki referans senaryo kadranın ilk konumu. İkinci yazıda Ali’nin tahtaya yazdığı iki sayı burada iki ayrı kadrandır: vericide asgari cover, alıcıda azami cover. Aşağıda ikisini de çevireceğiz.

Gönderen mağazada asgari cover (hafta)
Alıcı mağazada azami cover (hafta)
Çözüm yöntemi
Taşınacak option
252
Taşınan adet
4.785
Açılan sevkiyat
196
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.978.978,84
Kayıp satış yakalama (%)
26
Çözüm süresi (sn)
0,008
Taşınacak option
252
Taşınan adet
4.785
Açılan sevkiyat
140
Stoğu boşalan mağaza
25
Net kazanç (TL)
5.006.978,84
Kayıp satış yakalama (%)
29,7
Çözüm süresi (sn)
0,385
Taşınacak option
270
Taşınan adet
5.092
Açılan sevkiyat
180
Stoğu boşalan mağaza
25
Net kazanç (TL)
5.194.324,39
Kayıp satış yakalama (%)
19,6
Çözüm süresi (sn)
0,013
Taşınacak option
270
Taşınan adet
5.092
Açılan sevkiyat
104
Stoğu boşalan mağaza
25
Net kazanç (TL)
5.232.324,39
Kayıp satış yakalama (%)
26,8
Çözüm süresi (sn)
0,757
Taşınacak option
275
Taşınan adet
5.203
Açılan sevkiyat
140
Stoğu boşalan mağaza
25
Net kazanç (TL)
5.288.750,25
Kayıp satış yakalama (%)
12,8
Çözüm süresi (sn)
0,01
Taşınacak option
275
Taşınan adet
5.203
Açılan sevkiyat
75
Stoğu boşalan mağaza
25
Net kazanç (TL)
5.321.250,25
Kayıp satış yakalama (%)
21,9
Çözüm süresi (sn)
1,756
Taşınacak option
275
Taşınan adet
5.203
Açılan sevkiyat
137
Stoğu boşalan mağaza
25
Net kazanç (TL)
5.290.250,25
Kayıp satış yakalama (%)
12
Çözüm süresi (sn)
0,013
Taşınacak option
275
Taşınan adet
5.203
Açılan sevkiyat
65
Stoğu boşalan mağaza
25
Net kazanç (TL)
5.326.250,25
Kayıp satış yakalama (%)
21,3
Çözüm süresi (sn)
1,603
Taşınacak option
212
Taşınan adet
4.033
Açılan sevkiyat
170
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.698.662,57
Kayıp satış yakalama (%)
25,4
Çözüm süresi (sn)
0,008
Taşınacak option
212
Taşınan adet
4.033
Açılan sevkiyat
127
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.720.162,57
Kayıp satış yakalama (%)
32
Çözüm süresi (sn)
0,286
Taşınacak option
228
Taşınan adet
4.303
Açılan sevkiyat
159
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.907.111,14
Kayıp satış yakalama (%)
19,3
Çözüm süresi (sn)
0,008
Taşınacak option
228
Taşınan adet
4.303
Açılan sevkiyat
95
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.939.111,14
Kayıp satış yakalama (%)
26,2
Çözüm süresi (sn)
0,636
Taşınacak option
233
Taşınan adet
4.414
Açılan sevkiyat
125
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.998.537
Kayıp satış yakalama (%)
12,2
Çözüm süresi (sn)
0,008
Taşınacak option
233
Taşınan adet
4.414
Açılan sevkiyat
68
Stoğu boşalan mağaza
25
Net kazanç (TL)
5.027.037
Kayıp satış yakalama (%)
16,7
Çözüm süresi (sn)
1,54
Taşınacak option
233
Taşınan adet
4.414
Açılan sevkiyat
123
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.999.537
Kayıp satış yakalama (%)
11,5
Çözüm süresi (sn)
0,01
Taşınacak option
233
Taşınan adet
4.414
Açılan sevkiyat
56
Stoğu boşalan mağaza
25
Net kazanç (TL)
5.033.037
Kayıp satış yakalama (%)
18,2
Çözüm süresi (sn)
1,532
Taşınacak option
197
Taşınan adet
3.756
Açılan sevkiyat
161
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.491.668,06
Kayıp satış yakalama (%)
24,7
Çözüm süresi (sn)
0,005
Taşınacak option
197
Taşınan adet
3.756
Açılan sevkiyat
122
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.511.168,06
Kayıp satış yakalama (%)
31,4
Çözüm süresi (sn)
0,248
Taşınacak option
211
Taşınan adet
3.988
Açılan sevkiyat
150
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.680.305,43
Kayıp satış yakalama (%)
18,7
Çözüm süresi (sn)
0,01
Taşınacak option
211
Taşınan adet
3.988
Açılan sevkiyat
93
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.708.805,43
Kayıp satış yakalama (%)
25,2
Çözüm süresi (sn)
0,548
Taşınacak option
216
Taşınan adet
4.099
Açılan sevkiyat
121
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.769.231,29
Kayıp satış yakalama (%)
12,2
Çözüm süresi (sn)
0,01
Taşınacak option
216
Taşınan adet
4.099
Açılan sevkiyat
66
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.796.731,29
Kayıp satış yakalama (%)
14,5
Çözüm süresi (sn)
1,307
Taşınacak option
216
Taşınan adet
4.099
Açılan sevkiyat
119
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.770.231,29
Kayıp satış yakalama (%)
11,5
Çözüm süresi (sn)
0,009
Taşınacak option
216
Taşınan adet
4.099
Açılan sevkiyat
55
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.802.231,29
Kayıp satış yakalama (%)
16,1
Çözüm süresi (sn)
1,113
Taşınacak option
183
Taşınan adet
3.501
Açılan sevkiyat
151
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.279.434,45
Kayıp satış yakalama (%)
23,3
Çözüm süresi (sn)
0,006
Taşınacak option
183
Taşınan adet
3.501
Açılan sevkiyat
115
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.297.434,45
Kayıp satış yakalama (%)
29,4
Çözüm süresi (sn)
0,232
Taşınacak option
196
Taşınan adet
3.719
Açılan sevkiyat
139
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.465.313,5
Kayıp satış yakalama (%)
17,5
Çözüm süresi (sn)
0,007
Taşınacak option
196
Taşınan adet
3.719
Açılan sevkiyat
85
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.492.313,5
Kayıp satış yakalama (%)
21,8
Çözüm süresi (sn)
0,486
Taşınacak option
201
Taşınan adet
3.830
Açılan sevkiyat
117
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.550.739,36
Kayıp satış yakalama (%)
12
Çözüm süresi (sn)
0,007
Taşınacak option
201
Taşınan adet
3.830
Açılan sevkiyat
62
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.578.239,36
Kayıp satış yakalama (%)
11,8
Çözüm süresi (sn)
0,985
Taşınacak option
201
Taşınan adet
3.830
Açılan sevkiyat
115
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.551.739,36
Kayıp satış yakalama (%)
11,3
Çözüm süresi (sn)
0,009
Taşınacak option
201
Taşınan adet
3.830
Açılan sevkiyat
53
Stoğu boşalan mağaza
25
Net kazanç (TL)
4.582.739,36
Kayıp satış yakalama (%)
12,2
Çözüm süresi (sn)
1,257

Bütün kombinasyonlar sayfayla birlikte geldi; sunucuya hiç gidilmiyor.

Kadıköy’ün telefonuna dönelim

Dizinin ilk yazısı bir telefonla açılmıştı: Kadıköy’de balıkçı yaka siyahın ortası tükenmiş, elde XS ve XL kalmıştı. Ve o pazartesi fotoğrafında böyle 528 kırıkBir option'ın beden setinin bozulması; ara bedenler tükenmiş, uçlar rafta kalmıştır. (mağaza, option) çifti vardı.

Dürüst cevap şu: algoritma bunların hepsini çözmüyor.

AçgözlüÇözücü
Alıcı olarak adreslenen kırık çift137170
Kırık seti bozan hareket00

Beş yüz yirmi sekizin yüz yetmişi. Geri kalanı neden değil? Üç sebep var ve üçü de matematiğin değil, gerçekliğin sınırı: o option’ın hiçbir mağazada verilebilecek fazlası yoktur, ya alıcının satış hızı eşiğin altındadır, ya da taşımanın maliyeti kazancını yemektedir. Bir algoritmanın yapabileceği en dürüst şey, çözemediği yeri çözebildiği yerden ayırmaktır.

İkinci satır belki daha önemli: hiçbir hareket kırık bir seti verici yapmıyor. Yani model derdi bir mağazadan alıp başka mağazaya taşımıyor. Bunu ayrıca kısıt olarak yazmadık — aday üretimi kırık rafı zaten verici adayı saymıyor. İkinci yazıdaki cümlenin kodda karşılığı budur.

İki yöntem yan yana

Dizinin dört ve beşinci yazıları aynı problemi iki ayrı yoldan çözdü. Bütün ölçütlerle yan yana:

ÖlçütAçgözlü (SQL + döngü)Çözücü (MIP + PuLP)
Taşınan option252252
Taşınan adet4.7854.785
Açılan sevkiyat196 (147’si tek option)140 (56’sı tek option)
Net kazanç4.978.979 TL5.006.979 TL
Çözüm süresi~8 ms~0,4 sn
Kayıp satışın yakalanan kısmı%26,0%29,7
Adreslenen kırık çift137170
KurulumyokPuLP + CBC
“Neden bu hareket?” cevabıtek satıryok

Paraya bakarsanız çözücü kazanıyor: 28.000 TL, yani yüzde 0,6. Bu farkın tamamının nereden geldiğini dördüncü yazıda görmüştük — ikisi birebir aynı malı taşıyor, çözücü sadece daha az kamyon kaldırıyor.

Beklemediğim sonuç

Bu sonuç tabloda değil, kadranda çıktı. İkinci yazıda Ali tahtaya tek bir çizgi yerine iki sayı yazmıştı: biri vericideki bolluğun eşiği, öbürü alıcıdaki kıtlığın tavanı. Aynı stok fotoğrafını farklı eşik takımlarıyla çalıştırınca çözücünün planı şöyle değişiyor:

Eşik takımıTaşınan optionNet kazançKayıp satış yakalama
verici ≥ 6 hafta · alıcı tavanı kapalı2525.006.979 TL%29,7
verici ≥ 6 hafta · alıcı ≤ 6 hafta2755.321.250 TL%21,9
verici ≥ 14 hafta · alıcı ≤ 6 hafta2335.027.037 TL%16,7
verici ≥ 18 hafta · alıcı ≤ 14 hafta2164.802.231 TL%16,1

İlk iki satıra bakın: aralarında tek bir kadran dönüyor, verici eşiği yerinde duruyor. Alıcı tavanını açmak plana 23 hareket daha ekliyor ve 314.271 TL, yani yüzde 6,3 daha kazandırıyor. Aynı hamlede boş çıkan müşterinin yakalanan kısmı %29,7’den %21,9’a iniyor.

Kayıp satış, rafta bedenini bulamayıp boş çıkan müşteridir; hiçbir kasada kaydı yoktur, veri setinde ayrı bir tablo olarak durur ve hiçbir çözücüye girdi olarak verilmez — çünkü gerçek hayatta o veri yoktur. Yalnızca sonradan, “önerilen transferler geçmişte boş çıkan müşterinin ne kadarını adresliyor?” diye ölçmek için kullanılır.

Yani daha çok para kazandıran ayar, dizinin ilk sayfasındaki telefonun derdinde geri düşüyor. Pelin tabloyu görünce ikinci yazıdaki sorusuna dönüyor:

— Tavanı açınca daha çok mal kımıldıyor, para da artıyor. Nasıl oluyor da müşteriyi daha az kurtarıyorum?

Ali’nin cevabı kısa:

— Çünkü tavan açılınca alıcı listesine giren mağazaların seti kırık değil, rafı da boş değil; yalnızca malı azalmış. Oraya yolladığın koli iyi satar, ciroyu da büyütür. Ama o rafın önünde bedenini bulamayıp eli boş çıkmış bir müşteri yoktu ki kurtarasın.

Çözücü yanlış bir şey yapmadı. Ona ne dediysek onu yaptı: net kazancı büyüt. Kayıp satışı hiç duymadı bile; o ölçütü ona hiç söylemedik.

Bu dizinin en pahalı cümlesi bu olabilir: amaç fonksiyonuna yazmadığınız şey, sizin için var değildir. Optimizasyonun tehlikesi yanlış cevap vermesi değil, sorduğunuz soruya kusursuz cevap verirken sormadığınız soruyu sessizce çöpe atmasıdır. Önceliğiniz müşterinin aradığını bulmasıysa, bunu amaç fonksiyonuna yazmanız gerekir — yoksa modeliniz onu umursamaz.

Tablonun son iki satırı aynı dersin başka bir yüzü ve Ali’nin ikinci yazıda sayıyla söyleyemediği sezginin karşılığı: verici eşiğini yükseltirken alıcı tavanını da gevşetmek kâğıtta temkinli görünür. Sonuçta hem daha az mal kımıldıyor, hem de yakalanan kayıp satış artmıyor.

Peki hangisini kurmalı?

Kesin bir cevabı yok, ama karar birkaç soruya indirgenebilir.

Açgözlü yöntem yeter — sabit maliyetler (kamyon, koli, mal kabul) toplamın yanında küçükse; kararlar birbirini az kısıtlıyorsa; ekipte optimizasyon bakacak kimse yoksa; ve verilen kararın tek satırla savunulabilmesi sizin için “en iyi”den daha değerliyse.

Çözücü gerekir — rota veya parti maliyetleri ağırsa; kapasiteler gerçekten kilitliyorsa; kararlar birbirine bağımlıysa; ve kaybınızın büyüklüğünü bilmek istiyorsanız.

Son madde en önemlisi ve dördüncü yazının kapanışıydı: yüzde 99,4’ü biz biliyoruz, çünkü ikisini de kurup ölçtük. Yalnızca birini çalıştıran bir zincir, sonucunun yüzde 99,4 mü yoksa yüzde 70 mi olduğunu bilemez. Bu yüzden üçüncü bir seçenek daha var ve çoğu zaman en akıllısı odur: ikisini birden kurun, farkı ölçün, sonra hangisini yaşatacağınıza karar verin. Fark küçükse basit olanı üretimde tutarsınız — ama artık bilerek tutuyorsunuzdur.

Varsayımlar ve bu model nerede yetmez

Dizi boyunca dağıttığım dürüstlük maddelerini tek yerde topluyorum. Hiçbiri gizli değil; hepsi bilinçli sadeleştirme.

Kazanç kâr değil, ciro etkisidir. Terazi liste fiyatıyla kuruldu, alış maliyeti düşülmedi. Tablodaki TL rakamları “yer değiştiren ciro”dur.

Blok bölünmez, tek hedefe gider. Bir mağazadan alınan stok ikiye ayrılmıyor. Bölünebilir dağıtım ayrı bir problem ve kendi yazısını hak ediyor.

Ufuk sabit sekiz hafta. Veri setinde sezon kodu ve indirim takvimi yok. İkinci yazıdaki “raf ömrü kadranı” — sezon sonu yaklaştıkça terazinin taşımaya yatması — bu yüzden modelde yok. Gerçek kurulumda ufuk option başına değişir.

Soğuma yalnız yarım görülüyor. Depo sevkiyatını izleyebiliyoruz ama mağazalar arası geçmiş transferler veride tutulmuyor; “geçen hafta zaten oradan almıştık” kuralı tam sınanamıyor.

Talep sansürlü ölçülüyor. Kırık setli mağazanın gerçek talebi ölçtüğümüzden yüksektir. En çok yardıma muhtaç mağaza, modelin gözünde olduğundan sönük görünür. Düzeltmedik; düzeltmek tahmin üstüne tahmin kurmak olurdu.

Kapasite adet bazlı. Metrekare, reyon ve askı ayrı ayrı modellenmiyor.

Determinizm ücretsiz gelmiyor. Aynı veriyle iki koşunun aynı planı vermesi kendiliğinden olmuyor: hem açgözlü sıralamada, hem çözücünün değişken adlandırmasında, hem de aday tablosunun satır sırasında ayrı ayrı sağlanması gerekti. Üçünden birini kaçırdığınızda bu size hata olarak değil, “geçen hafta başka söylüyordu” olarak geri döner.

Üretimde ne değişir

Gerçek bir zincirde bu modelin şekli aynı kalır, ayrıntısı değişir. Ürün hiyerarşisi daha derindir; mağazalar grup ve bölge taşır; “şu iki mağaza arasında transfer olmaz” gibi şirkete özel kurallar vardır; sevkiyat takvimi haftanın belirli günlerine bağlıdır. Bunların hepsi ya aday kümesinin tanımına, ya da kısıt listesine eklenir — üçüncü yazının cümlesi burada da geçerlidir: modelin en önemli kısmı çoğu zaman yazmadığınız kısıtlardır.

Bir de ölçek meselesi var. Lumoda 25 mağaza ve 240 option’lı küçük bir zincir; gerçek bir zincirde bu sayılar bir büyüklük mertebesi yukarıdadır ve o zaman çözücünün saniyenin altındaki süresi dakikalara çıkar. Zaman limiti, limit bayrağı ve “kanıtlanmamış ama iyi” çözümle çalışmak orada gerçek bir mesele hâline gelir.

Gerçek kurulumda eklenen kısıtlar

Yukarıdaki modelde dört kısıt var. Gerçek bir zincirde bu liste hep uzar ve uzayan kısımlar genelde aynı yerlerden gelir. Sık istenenleri, bizim modelde karşılığı olup olmadığıyla birlikte topladım:

KısıtNe işe yararBizde
Alıcı kapasitesiMağazanın deposu ve rafı sonsuz değilvar
Minimum koliİki adet için kamyon kaldırılmazvar
Gönderici alt sınırıVericiyi soymamak: elinde en az şu kadar hafta kalsınyok
Stok şişme tavanıAlıcıya taşıdıktan sonra cover’ı şu haftayı geçmesinyok
Kargo/rota sayısıBir mağaza en çok kaç mağazaya gönderir, kaç mağazadan alıryok
Maksimum koli / araç hacmiBir sevkiyata sığacak adet, ağırlık, hacimyok
Sevkiyat takvimiTransfer yalnız belirli günlerde, belirli hatlar üzerindenyok
Bölge ve grup kuralları“Şu iki mağaza arasında transfer olmaz”, franchise sınırlarıkısmen (line kuralı)
Tekrar transfer soğumasıAynı malı iki hafta üst üste oradan oraya taşımamakkısmen (yalnız depo sevkiyatı)

Üçü özellikle sık atlanır ve atlandığında plan sahada uygulanamaz hâle gelir.

Gönderici alt sınırı. Bizim modelde blok, vericinin stoğunun tamamıdır; verici o option’da sıfıra iner. Lumoda’da bu kabul edilebilir çünkü verici zaten cover eşiğini aşmış, yani satmayan bir raf. Gerçek zincirde mağaza müdürü “benim de müşterim var” der ve haklıdır: pratikte vericide belirli bir hafta karşılığı bırakılır. Bu, bloğu bölmek demektir ve modeli sürekli miktar kararına yaklaştırır.

Stok şişme tavanı. Model alıcıya taşımayı sever, çünkü kazanç orada görünür. Ama alıcıda da bir doyma noktası vardır: sekiz haftalık talebi karşılayan mağazaya on altı haftalık mal göndermek bir sonraki haftanın ölü stoğunu üretir. Kapasite kısıtı bunu ancak kabaca engeller; asıl koruma alıcının transfer sonrası cover’ına konan üst sınırdır.

Kargo/rota sayısı. Dördüncü ve beşinci yazının bütün hikâyesi buydu: çözücü 140, açgözlü 196 sevkiyat açıyordu. Gerçek lojistikte bu sayının bir tavanı vardır (“bir mağaza haftada en çok üç yere gönderir”). Tavanı modele yazdığınızda çözücü rotaları daha da birleştirmek zorunda kalır ve iki yöntem arasındaki fark büyür — çünkü açgözlü yöntem bu kısıtı ancak sonradan eleyerek uygulayabilir, çözücü ise plan kurarken hesaba katar.

Son bir uyarı: her yeni kısıt çözüm uzayını daraltır, yani beklenen kazancı düşürür. Bu bir kayıp değil, dürüstlüktür — kısıtsız modelin vaat ettiği para zaten uygulanamayacak bir planın parasıdır. Asıl soru “kısıt eklersem ne kaybederim” değil, “bu kısıt gerçek mi, yoksa alışkanlık mı” sorusudur.

Baştaki soru burada kapanıyor

Altı yazı önce bir mağaza müdürünün telefonuyla başladık, bir Excel dosyası ve akşam dokuzda gelen “neden bu kırkı seçtik, bilmiyorum” itirafıyla devam ettik. Sonra teraziyi kurduk, sembollere çevirdik, iki ayrı yoldan çözdük ve sonunda ikisini tarttık.

Geriye kalan tek cümle şu: bu işin zor kısmı algoritma değildi. Zor kısım, kararın nasıl verildiğini yavaş çekimde izleyip yazabilmekti. Onu yazabildiğinizde hangi yöntemi seçtiğiniz neredeyse bir detay — ve seçiminizi artık savunabiliyorsunuz 🙂

Bu listenin ilk maddesini bir sonraki yazı devralıyor: buradaki rakamlar hâlâ ciro cinsinden ve onları paraya çevirmek o yazının işi.

Yazan: Sabri Suyunu