İçeriğe geç
Tüm yazılar

Stocky'yi Geliştirmek: mutfağınızda neyin biteceğini tahmin etmek — hem de cihazın üzerinde

Stocky bir barkod taramasını ve biraz kullanım geçmişini, siz daha bitirmeden önünüze düşen bir alışveriş listesine nasıl dönüştürüyor — tüm matematik ve tüm veriler telefonunuzda kalarak.

MFKAPPS 3 dk okuma

Çoğu kiler uygulaması, süslenmiş bir yapılacaklar listesinden ibarettir. Süt eklersiniz, sütü işaretlersiniz, sonra tekrar süt eklersiniz. Uygulama hiçbir şey öğrenmez. Stocky farklı bir soruyla yola çıktı: ya uygulama, şeyleri gerçekte ne kadar hızlı tükettiğinizi fark etseydi ve siz daha bitirmeden alışveriş listenizi yeniden kursaydı?

Bu tek fikir — küçük, dürüst bir tahmin — mühendisliğin büyük kısmını şekillendirdi. İşte nasıl çalıştığı ve neden hepsinin yerel olarak çalıştığı.

Bir tarama, bir satır değil, zaman damgalı bir olaydır

Naif model, artırıp azalttığınız bir quantity sütununa sahip bir ürün tablosudur. Basittir ve onu düzenlediğiniz anda her şeyi unutur.

Stocky bunun yerine olayları saklar. Her tarama, her “biraz kullanıldı”, her yeniden stoklama, yalnızca ekleme yapılan bir kayıttır:

@Entity(tableName = "pantry_events")
data class PantryEvent(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val productId: String,     // barcode / Open Food Facts id
    val type: EventType,       // ADDED, CONSUMED, DISCARDED
    val amount: Double,        // in the product's base unit (ml, g, pcs)
    val at: Long,              // epoch millis
)

Mevcut miktar, sadece olayların bir toplamıdır (fold). Kulağa savurgan geliyor ama bütün numara bu: geçmişi sakladığım için, çalışan bir toplamın yanıtlayamayacağı soruları sorabiliyorum — örneğin “bu evde bundan günde kaç mililitre tüketiliyor?”

Tahmin bilinçli olarak sıradan

Burada sinir ağı yok. Bir evin zeytinyağı tüketimi, bir transformer gerektiren bir problem değildir — büyük bir hafta sonuna aşırı tepki vermeyen bir ortalamaya ihtiyaç duyar.

Her ürün için, geriye dönük bir pencere üzerinde, yakın zamandaki olaylara biraz daha ağırlık vererek bir tüketim oranı hesaplıyorum:

fun dailyRate(events: List<PantryEvent>, now: Long): Double {
    val consumed = events.filter { it.type == EventType.CONSUMED }
    if (consumed.size < 2) return 0.0

    val windowStart = now - 30.days.inWholeMilliseconds
    val recent = consumed.filter { it.at >= windowStart }
    if (recent.isEmpty()) return 0.0

    val total = recent.sumOf { it.amount }
    val spanDays = ((now - recent.first().at) / 1.days.inWholeMilliseconds)
        .coerceAtLeast(1)
    return total / spanDays
}

daysLeft = currentQuantity / dailyRate. Bu değer bir eşiğin altına düştüğünde, ürün sessizce alışveriş listenize düşüyor. Henüz oran yok mu? Tahmin de yok — Stocky tahmin yürütmek yerine hiçbir şey söylemez. Sakin bir uygulama, yanlış olmaktansa sessiz kalmayı tercih eder.

Kısmi kullanım, çünkü mutfaklar analogdur

En çok uğraştıran özellik matematik değildi — “bunun aşağı yukarı üçte birini içtim” diyebilmenizi sağlamaktı. İnsanlar tam sayılarla azaltmaz. Bir kutu meyve suyu 200 ml’lik yudumlarla tüketilir ve uygulama sizi ya “dolu” tutmaya ya da “bitti” işaretlemeye zorlarsa, tahmin çöp olur.

Bu yüzden tüketim ekranı gerçek birimlerle konuşuyor ve ürünün ambalaj boyutunu hatırlıyor; böylece “bir bardak” ya da “yarısı”, oran fonksiyonunun kullanabileceği bir şeye karşılık geliyor. Bütün modeli sessizce güvenilir kılan minik bir arayüz parçası.

Neden bunların hiçbiri telefondan çıkmıyor

Yukarıdaki her sayı, olaylarınızdan türetiliyor ve olaylarınız hayatınızı rahatsız edici bir ayrıntıyla anlatıyor — ne zaman yediğinizi, ne kadar, ne sıklıkla alışveriş yaptığınızı. İşte tam da elimde tutmak istemediğim veri bu.

Bu yüzden ne bir hesap var ne de kilerinizi saklayan bir sunucu. Her şey yerel bir Room veritabanında yaşıyor. Ürün aramaları Open Food Facts’e (ve topluluk kaynaklı bir Türk ürünleri setine) gidiyor, ama o istek yalnızca bir barkod içeriyor — asla geçmişinizi değil. Tahmin cihaz üzerinde çalışıyor çünkü veri zaten orada ve orada kalması gerekiyor.

Yerel öncelikli olmak burada bir pazarlama kutucuğu değil. Bu uygulamanın dürüst sürümünü mümkün kılan şeyin ta kendisi: Hiç toplamadığım bir veritabanını kaybedemem, satamam ya da sızdıramam.

Neyi farklı yapardım

Oran modelini bugün yeniden kursaydım, nazik bir mevsimsel terim eklerdim — bazı şeyler yazın artar — ve kategori başına bir öncül (prior) eklerdim; böylece yepyeni bir ürün, bir hafta boyunca sessiz kalmak yerine makul bir başlangıç oranı ödünç alırdı. İkisi de küçük. İkisinin de buluta ihtiyacı yok.

Stocky Google Play’de yayında. Denerseniz, kendiliğinden beliren o alışveriş listesi en gurur duyduğum kısım — ve umarım akıllıca olduğunu bile unutacağınız kısım.