Mert Özen Her Satırda Daha İleri
Proje Yönetimi Kanban — #04

Flow Metric'leri: WIP, Throughput, Work Item Age ve Cycle Time

Mert Özen 8 Kas 2026 13 dk 4 görüntülenme
Flow Metric'leri: WIP, Throughput, Work Item Age ve Cycle Time

Kanban Guide'ın dört zorunlu metric'i olan WIP, Throughput, Work Item Age ve Cycle Time'ı Bookstore ekibinin altı aylık board verisinden bir .NET 10 console uygulamasıyla hesaplıyoruz. Ortalama yerine percentile kullanmayı, cycle time scatterplot ve aging chart okumayı, Work Item Age'in neden en erken uyarı olduğunu ve Created tarihinden hesaplamanın p85'i 14 günden 79 güne nasıl çıkardığını görüyoruz.

Sürüm notu: Bu yazı The Kanban Guide'ın 2025.5 sürümünü esas alır; metric kodu .NET 10 ile derlenip Bookstore ekibinin örnek board verisi üzerinde çalıştırılmıştır.

Önce Tahmin Et

Bookstore ekibinin Mayıs–Ekim 2026 arasında bitirdiği 193 item'ın cycle time ortalaması 11,4 gün. Ürün sahibi yeni bir destek talebi için müşteriye bir süre söyleyecek. "Ortalama 11 gün" derse, müşterilerin kabaca yüzde kaçına verdiği sözü tutar? Yarısına mı, dörtte üçüne mi, neredeyse hepsine mi?

İkinci soru: 30 Ekim cuma sabahı board'da 11 açık item var. Bunlardan hangisi en riskli: en büyük feature mı, Code Review'da bekleyen bug mı, yoksa 19 gündür In Dev kolonunda duran ve kimsenin konuşmadığı item mı? Bunu bir metric'le nasıl görürsün?

Guide'ın Dört Flow Metric'i

The Kanban Guide dört flow metric'i minimum kabul eder ve tanımları kısadır. WIP (Work in Progress), başlamış ama bitmemiş item'ların sayısıdır. Throughput, belirli bir zaman biriminde biten item'ların sayısıdır; Guide bunun tahmini değil, kesin bir sayım olduğunu vurgular. Work Item Age, başlamış ama bitmemiş bir item'ın başlangıcından bugüne geçen süredir. Cycle Time ise biten bir item'ın başlangıcından bitişine geçen süredir.

Dikkat edersen ikisi biten işe, ikisi bitmemiş işe bakar. Cycle Time ve Throughput geçmişi anlatır: ne kadar sürdü, ne kadar bitti. WIP ve Work Item Age ise şu anı anlatır: şu an ne kadar iş açık, açık işler ne kadar yaşlı. Geçmişe bakan iki metric'le ne olduğunu, bugüne bakan ikisiyle ne olmak üzere olduğunu görürsün.

Guide süre birimini ve yuvarlamayı tarif etmez. Pratikte yaygın konvansiyon günü birim almak ve aynı gün başlayıp biten işi 0 değil 1 gün saymaktır: cycle time = bitiş tarihi - başlangıç tarihi + 1. ActionableAgile (Vacanti'nin flow analizi için kullanılan aracı) dokümantasyonu da bu hesabı böyle tarif eder; +1, işin üzerinde geçirilen her takvim gününü saymak ve sıfır günlük cycle time'ları önlemek içindir. Biz de 02. yazıdaki Bookstore DoW'unda (Definition of Workflow) bu konvansiyonu seçtik. Hangisini seçtiğin, ekip içinde aynı konvansiyonu kullandığın sürece o kadar önemli değil.

Veri: Board Geçmişinden CSV'ye

Dört metric'in hepsi tek bir veriye ihtiyaç duyar: her item'ın her state'e hangi tarihte girdiği. Jira ve GitHub Projects bu geçmişi tutar, ama standart CSV (Comma-Separated Values) export'ları genellikle item'ın güncel durumunu ve created/resolved tarihlerini verir. State geçiş tarihleri için Jira'da issue changelog'unu, örneğin REST (Representational State Transfer) API'sinin (Application Programming Interface) expand=changelog parametresiyle, çekip düz bir tabloya çevirmen gerekir. Bu yazıda bu adımı geçiyor ve elimizde şu biçimde bir dosya olduğunu varsayıyoruz:

Id,Title,Type,Created,Ready,InDev,CodeReview,Test,ReadyToDeploy,Done,BlockedDays
BS-1001,Fix VAT rounding on invoice PDF,Bug,2026-04-07,2026-05-04,2026-05-04,2026-05-05,2026-05-07,2026-05-08,2026-05-12,0
BS-1002,Add wishlist sharing,Feature,2026-04-25,2026-05-04,2026-05-04,2026-05-15,2026-05-21,2026-05-22,2026-05-26,7
BS-1003,Fix duplicate order e-mail,Bug,2026-03-11,2026-05-04,2026-05-04,2026-05-04,2026-05-05,2026-05-06,2026-05-07,0
BS-1004,Fix stock count after cancelled order,Bug,2026-03-17,2026-05-04,2026-05-04,2026-05-05,2026-05-07,2026-05-08,2026-05-12,0

Her satır bir item; her state kolonu o state'e giriş tarihi, boşsa item o state'e henüz girmemiş. Created kaydın açıldığı gün, BlockedDays item'ın blocked etiketiyle geçirdiği gün sayısı. Bu yazıdaki veri seti, serinin anlatısına uyan, Bookstore ekibinin altı aylık board'unu taklit eden bir simülasyonla üretildi: 205 item, Mayıs–Haziran'da limit'siz bir board ve Code Review'da biriken işler, Eylül'de sıkılaşan WIP limit'leri. Rakamlar uydurma değil, bu verinin üzerinde koşan kodun gerçek çıktısı; veri setinin kendisi ise gerçek bir ekibin değil.

Kod: CSV'den Metric'e

Bir .NET 10 console projesi açıyoruz (dotnet new console -n FlowMetrics); dış bir paket yok. İlk dosya veri modelini, CSV okuyucuyu ve percentile hesabını içeriyor:

namespace Bookstore.Flow;

public sealed record WorkItem(
    string Id,
    string Title,
    string Type,
    DateOnly Created,
    IReadOnlyDictionary<string, DateOnly> Entered,
    int BlockedDays)
{
    public const string StartState = "InDev";
    public const string FinishState = "Done";

    public DateOnly? Started => Entered.TryGetValue(StartState, out DateOnly d) ? d : null;
    public DateOnly? Finished => Entered.TryGetValue(FinishState, out DateOnly d) ? d : null;

    public bool IsInProgress => Started is not null && Finished is null;

    // Aynı gün başlayıp biten iş 1 gün sayılır (bitiş - başlangıç + 1)
    public int? CycleTimeDays => Started is { } s && Finished is { } f ? f.DayNumber - s.DayNumber + 1 : null;

    public int AgeOn(DateOnly today) => today.DayNumber - Started!.Value.DayNumber + 1;

    // Board'daki son state: girilen en geç tarih, eşitlikte workflow sırası
    public string CurrentState => Entered.OrderBy(e => e.Value).ThenBy(e => BoardCsv.States.IndexOf(e.Key)).Last().Key;
}

public static class BoardCsv
{
    public static readonly List<string> States = ["Ready", "InDev", "CodeReview", "Test", "ReadyToDeploy", "Done"];

    public static List<WorkItem> Load(string path)
    {
        string[] lines = File.ReadAllLines(path);
        string[] header = lines[0].Split(',');
        List<WorkItem> items = [];

        foreach (string line in lines.Skip(1))
        {
            string[] cells = line.Split(',');
            Dictionary<string, DateOnly> entered = [];
            foreach (string state in States)
            {
                string value = cells[Array.IndexOf(header, state)];
                if (value.Length > 0)
                    entered[state] = DateOnly.Parse(value);
            }

            items.Add(new WorkItem(
                Id: cells[Array.IndexOf(header, "Id")],
                Title: cells[Array.IndexOf(header, "Title")],
                Type: cells[Array.IndexOf(header, "Type")],
                Created: DateOnly.Parse(cells[Array.IndexOf(header, "Created")]),
                Entered: entered,
                BlockedDays: int.Parse(cells[Array.IndexOf(header, "BlockedDays")])));
        }

        return items;
    }
}

public static class Stats
{
    // Nearest-rank percentile: sıralı listede ceil(p * n). eleman
    public static int Percentile(IReadOnlyList<int> sorted, double p) =>
        sorted[(int)Math.Ceiling(p / 100 * sorted.Count) - 1];
}

Birkaç tasarım kararı var. Started ve finished noktaları StartState ve FinishState sabitlerinde, yani DoW'daki karar kodda tek bir yerde duruyor; ekip finished noktasını değiştirirse yalnız burayı değiştirir. Tarihler DateOnly; saat bilgisi tutmuyoruz çünkü metric'leri gün biriminde konuşuyoruz. CSV okuyucu bilinçli olarak basit: başlıklarda virgül olmadığını biliyoruz. Gerçek bir export'ta tırnaklı alanlar varsa CsvHelper gibi bir library kullanmak daha güvenli.

Percentile için nearest-rank yöntemini kullanıyoruz: 193 değerlik sıralı listede 85. percentile, ceil(0,85 × 193) = 165. eleman. Bu yöntem interpolasyon yapmaz, her zaman listede gerçekten bulunan bir değeri döner; "item'ların %85'i 15 gün ya da daha kısa sürede bitti" cümlesini doğrudan kurmanı sağlar. Excel'in PERCENTILE.INC fonksiyonu interpolasyon yapar ve küçük farklı sonuçlar üretebilir; hangisini kullandığını yazmak yeter.

Program dört soruyu sırayla cevaplıyor:

using System.Globalization;
using Bookstore.Flow;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

DateOnly today = new(2026, 10, 30);
List<WorkItem> items = BoardCsv.Load("board-history.csv");

// 1) Cycle time: yalnız bitmiş item'lar
PrintCycleTimes("All finished (May-Oct)", items.Where(i => i.Finished is not null));
PrintCycleTimes("Finished in last 8 weeks", items.Where(i => i.Finished >= today.AddDays(-55)));

// 2) Throughput: haftalık bitirilen item sayısı (boş haftalar da 0 olarak görünmeli)
Console.WriteLine();
Console.WriteLine("Weekly throughput (last 8 weeks, Mon-Sun):");
DateOnly monday = today.AddDays(-(((int)today.DayOfWeek + 6) % 7));
for (int w = 7; w >= 0; w--)
{
    DateOnly from = monday.AddDays(-7 * w);
    int count = items.Count(i => i.Finished >= from && i.Finished < from.AddDays(7));
    Console.WriteLine($"  {from:yyyy-MM-dd}  {count,2} {new string('#', count)}");
}

// 3) WIP ve Work Item Age: bugün board'da olan işler, en yaşlıdan başla
List<int> recent = [.. items.Where(i => i.Finished >= today.AddDays(-55)).Select(i => i.CycleTimeDays!.Value).Order()];
int p50 = Stats.Percentile(recent, 50), p85 = Stats.Percentile(recent, 85);
List<WorkItem> wip = [.. items.Where(i => i.IsInProgress).OrderByDescending(i => i.AgeOn(today))];

Console.WriteLine();
Console.WriteLine($"WIP on {today:yyyy-MM-dd}: {wip.Count} items (p50 = {p50} d, p85 = {p85} d)");
foreach (WorkItem i in wip)
{
    int age = i.AgeOn(today);
    string flag = age > p85 ? "  <-- older than p85" : age > p50 ? "  <-- older than p50" : "";
    Console.WriteLine($"  {i.Id}  {i.CurrentState,-13} age {age,2} d  {i.Title}{flag}");
}

// 4) Tuzak: başlangıç olarak Created kullanmak
List<int> fromCreated = [.. items.Where(i => i.Finished >= today.AddDays(-55))
    .Select(i => i.Finished!.Value.DayNumber - i.Created.DayNumber + 1).Order()];
Console.WriteLine();
Console.WriteLine($"Same items measured from Created: p50 = {Stats.Percentile(fromCreated, 50)} d, p85 = {Stats.Percentile(fromCreated, 85)} d");

static void PrintCycleTimes(string label, IEnumerable<WorkItem> finished)
{
    List<int> ct = [.. finished.Select(i => i.CycleTimeDays!.Value).Order()];
    Console.WriteLine($"{label}: n = {ct.Count}, mean = {ct.Average():F1} d, " +
        $"p50 = {Stats.Percentile(ct, 50)} d, p85 = {Stats.Percentile(ct, 85)} d, " +
        $"p95 = {Stats.Percentile(ct, 95)} d, max = {ct[^1]} d");
}

Throughput döngüsü haftaları LINQ (Language Integrated Query) ile gruplamak yerine pazartesiden başlayan sekiz haftayı tek tek dolaşıyor. Sebebi basit: GroupBy hiç item bitmeyen haftayı hiç üretmez, oysa sıfır throughput'lu bir hafta en önemli bilgilerden biridir. Gerçek çıktı:

All finished (May-Oct): n = 193, mean = 11.4 d, p50 = 10 d, p85 = 15 d, p95 = 21 d, max = 24 d
Finished in last 8 weeks: n = 66, mean = 10.1 d, p50 = 9 d, p85 = 14 d, p95 = 18 d, max = 24 d

Weekly throughput (last 8 weeks, Mon-Sun):
  2026-09-07  11 ###########
  2026-09-14   5 #####
  2026-09-21  10 ##########
  2026-09-28   9 #########
  2026-10-05   6 ######
  2026-10-12   8 ########
  2026-10-19  10 ##########
  2026-10-26   7 #######

WIP on 2026-10-30: 11 items (p50 = 9 d, p85 = 14 d)
  BS-1205  InDev         age 19 d  Add 3-D Secure fallback for card payments  <-- older than p85
  BS-1193  ReadyToDeploy age  8 d  Add customer reviews moderation
  BS-1194  ReadyToDeploy age  8 d  Fix VAT rounding on invoice PDF
  BS-1196  Test          age  5 d  Fix duplicate order e-mail
  BS-1197  ReadyToDeploy age  5 d  Fix stock count after cancelled order
  BS-1198  InDev         age  4 d  Add gift card balance page
  BS-1199  CodeReview    age  3 d  Fix cart total when coupon expires
  BS-1200  CodeReview    age  2 d  Change delivery address for order BK-2026-006660
  BS-1204  Test          age  2 d  Urgent: refund request for order BK-2026-004401
  BS-1201  CodeReview    age  1 d  Export sales report for publisher
  BS-1202  InDev         age  1 d  Fix author search with apostrophes

Same items measured from Created: p50 = 46 d, p85 = 79 d

Çıktıyı Okumak: Neden Ortalama Değil Percentile

Baştaki soruya dönelim. Tüm dönemde ortalama 11,4 gün, ama medyan (p50) 10 gün, p85 15 gün, p95 21 gün ve en uzun item 24 gün. Ortalama medyandan büyük, çünkü dağılımın sağ kuyruğu uzun: çoğu iş 7–12 günde bitiyor, ama arada 20 günü geçen büyük feature'lar ve blocked kalmış işler ortalamayı yukarı çekiyor. Cycle time dağılımları neredeyse her zaman böyle sağa çarpıktır, çünkü bir iş birkaç günden daha kısa süremez ama çok uzun sürebilir.

Ürün sahibi müşteriye "ortalama 11 gün" derse, bu veride item'ların kabaca %40'ı bu sözü geçer. "Ortalama" kelimesi kulağa güvenli gelir, ama aslında "yaklaşık yarı yarıya" demektir. Percentile ise doğrudan olasılık söyler: "item'ların %85'i 15 gün içinde bitti." Bir SLE (Service Level Expectation) tam olarak bu cümledir ve 05. yazıda bunu tahmine çevireceğiz.

Son 8 haftaya bakınca sayılar iyileşiyor: p50 9 gün, p85 14 gün. Bu, Eylül'deki WIP değişikliğinin etkisi; ama dikkat, bu pencerede Eylül öncesi başlamış işler de var. Değişikliğin etkisini temiz ölçmek için başlangıç tarihine göre ayırmak gerekir; bunu 06. yazıda yapacağız.

Throughput haftadan haftaya 5 ile 11 arasında oynuyor. Bunun bir kısmı deploy günlerinden: ekip yalnız salı ve perşembe deploy ettiği için item'lar Done'a toplu giriyor. Haftalık throughput'un bu kadar oynaması normaldir ve tek bir haftaya bakarak "yavaşladık" demenin neden yanlış olduğunu gösterir. Ortalama haftada 8,3 item; ama tahmin yaparken ortalamayı değil bu dağılımın kendisini kullanacağız.

Cycle Time Scatterplot

Aynı veriyi görselleştirmenin en faydalı yolu cycle time scatterplot'u: x ekseninde item'ın bittiği gün, y ekseninde cycle time'ı, yatay çizgiler percentile'lar.

04-cycle-time-scatter-tr.png

Bu grafikte üç şey okunur. Birincisi yoğunluk: noktaların büyük kısmı p50 ile p85 çizgileri arasında, yani "tipik" iş 10–15 gün sürüyor. İkincisi trend: Haziran–Ağustos arasında noktalar yukarı doğru dağılıyor, Eylül'deki mor çizgiden sonra aşağı iniyor ve sıkışıyor; WIP limit'lerinin etkisi tabloya bakmadan görülüyor. Üçüncüsü aykırı noktalar: p95 çizgisinin üstündeki her nokta bir hikâyedir. Ekim'deki 24 günlük feature mesela; ekip retro'da onu konuşmak isteyecektir.

Renkler item türünü gösteriyor ve destek işlerinin (sarı) genellikle alt tarafta kaldığını, büyük feature'ların (mavi) üst tarafı doldurduğunu görüyorsun. Bu, türe göre ayrı percentile hesaplamanın faydalı olabileceğini söyler; ama her tür için yeterli veri olduğundan emin olmadan sayıyı bölme. 30 item'ın altında percentile'lar oynak olur.

Work Item Age ve Aging Chart

Cycle time yalnız biten işi görür. 19 gündür In Dev'de duran BS-1205 henüz bitmediği için scatterplot'ta yok; bittiğinde ise geç kalmış olacak. Work Item Age bu yüzden dört metric arasında en erken uyarıyı veren metric'tir: bir işin geç kalacağını, geç kaldıktan sonra değil, geç kalmaya başladığı gün gösterir.

04-aging-chart-tr.png

Aging chart'ta x ekseni board'un kolonları, her nokta açık bir item ve yüksekliği yaşı. Arka plandaki bantlar son 8 haftanın percentile'larından geliyor: yeşil bölge p50'nin altı, sarı bölge p50 ile p85 arası, kırmızı bölge p85'in üstü. Ready to Deploy kolonundaki 8 günlük iki item p50'ye yaklaşıyor ama hâlâ yeşilde; salı deploy'uyla çıkacaklar. BS-1205 ise kırmızı bölgede tek başına: 3-D Secure fallback'i için ödeme sağlayıcısının test ortamını bekleyen bir feature. 14 günlük SLE'yi zaten kaçırdı.

Bu grafiğin asıl değeri sağdan sola okunmasıdır. Sağdaki kolonlardaki yaşlı bir item bitmeye yakındır, küçük bir itkiyle çıkar. Soldaki kolonlarda yaşlı bir item ise hem yaşlıdır hem de daha gidecek çok yolu vardır. Daily toplantısında ekibin ilk sorusu "dün ne yaptın?" değil, "en yaşlı item'ımızı bugün nasıl ilerletiriz?" olmalı; bunu 06. yazıda açacağız.

Cumulative Flow Diagram'a Kısa Bakış

Dört metric'in üçünü tek grafikte gösteren bir araç daha var: CFD (Cumulative Flow Diagram). Her gün için her state'e o güne kadar girmiş toplam item sayısı çizilir; ortaya üst üste binmiş bantlar çıkar. İki çizgi arasındaki dikey mesafe o gün o state'teki item sayısı, yani o kolonun WIP'idir. En alt çizginin eğimi throughput'u gösterir. Bantlar arasındaki yatay mesafe ise yaklaşık bir bekleme süresi verir.

CFD'nin en çok işe yaradığı yer, bir bandın zamanla genişlemesini görmektir: bir state'e giren iş, çıkan işten hızlıysa o bant kalınlaşır ve orada bir bottleneck oluşuyordur. Bookstore'un Code Review bandı Haziran–Ağustos arasında tam olarak böyle genişledi. CFD'yi aynı veriden hesaplayıp bu bandı 06. yazıda inceleyeceğiz.

Üretimde Dikkat

Pencereyi bilinçli seç. Tüm geçmişi kullanmak daha çok veri demek, ama sistem değiştiyse eski veri yanıltır. Bookstore'da Eylül'de WIP limit'leri değişti; Mayıs verisi bugünkü sistemi anlatmıyor. Genelde son 6–12 hafta, en az 30 biten item ile çalışmak iyi bir başlangıç; sistemde büyük bir değişiklik olduysa pencereyi o tarihten başlat.

Takvim günü mü iş günü mü, baştan karar ver. Biz takvim günü kullandık, çünkü müşteri takvimle bekler ve hafta sonu da bekleme süresidir. İş günü kullanmak istersen hesabı her yerde aynı yap; Little's Law'u ya da Monte Carlo'yu karışık birimlerle besleme.

Hesabı otomatikleştir. Bu console uygulamasını her pazartesi export'u indirip çalıştıran bir scheduled job'a çevirmek bir saatlik iş; sonucu ekip kanalına göndermek, metric'leri "retro'da bir kere bakılan" bir şey olmaktan çıkarır. Hesabın kodda olması, "bu sayı nereden geldi?" sorusunun cevabını da her zaman hazır tutar.

Sık Düşülen Tuzaklar

En pahalı tuzak, başlangıç tarihini state değişiminden değil Created alanından almak. Programın son satırı aynı 66 item için bunu yapıyor: p50 9 günden 46 güne, p85 14 günden 79 güne çıkıyor. Created tarihi, kaydın backlog'a girdiği gündür; arada geçen haftalar ekibin flow'unu değil backlog'un büyüklüğünü ölçer. Created'tan Done'a geçen süre de faydalı bir sayıdır (müşteri açısından talep ile çözüm arası), ama onu cycle time diye raporlamak yanlıştır; adını ayrı koy.

İkinci tuzak sıfır haftaları kaybetmek. Throughput'u GroupBy ile hesaplarsan hiçbir şeyin bitmediği hafta listede hiç görünmez ve ortalama throughput olduğundan yüksek çıkar. Tahminde bu hata doğrudan iyimser bir tarihe dönüşür.

Üçüncüsü yalnız biten işe bakmak. Cycle time hesabı tanım gereği bitmemiş işi dışarıda bırakır. Board'da üç ay önce başlayıp unutulmuş beş item varsa, cycle time'ın harika görünür; ta ki o işler bitene kadar. Work Item Age'e bakmadan cycle time'a güvenme.

Dördüncüsü metric'i kişiye bağlamak. "Ahmet'in cycle time'ı" diye bir sayı hesaplamak teknik olarak mümkün ama Kanban'ın amacına ters: flow bir sistem özelliğidir ve kişiye bağlandığı anda insanlar sayıyı iyileştirmek için işi parçalar, zor işten kaçar. Metric'ler board'u, kişiyi değil ölçer.

Küçük Bir Deneme

★☆☆ Programa item türüne göre ayrı cycle time percentile'ları ekle (Bug, Support, Feature). Hangi türün p85'i SLE'nin üstünde kalıyor? Her türde kaç item var ve percentile için yeterli mi?

★★☆ Throughput'u hafta yerine gün bazında hesapla ve sıfır günlerin oranını bul. Salı ve perşembe deploy'larının etkisini görebiliyor musun? Ekip her gün deploy etseydi bu dağılım nasıl değişirdi?

★★★ Kendi ekibinin Jira ya da GitHub Projects verisinden bu CSV biçiminde bir dosya üret (changelog'dan state geçiş tarihlerini çekerek) ve programı onun üzerinde çalıştır. Little's Law'u kontrol et: ortalama WIP / günlük throughput, ölçtüğün ortalama cycle time'a ne kadar yakın?

Geçmişi ölçmeyi öğrendik. Bir sonraki yazıda bu geçmişi geleceğe çevireceğiz: aynı veriden bir SLE çıkaracak ve 10.000 çalıştırmalı bir Monte Carlo simülasyonuyla "27 Kasım'a kadar kaç item biter?" ve "gift card epic'i ne zaman biter?" sorularına olasılıkla cevap vereceğiz.