Skip to content

KART Sistemi — Geliştirme Planı

Oluşturulma: 2026-05-14 | Güncelleme: 2026-05-15 | Durum: Kararlar Netleşti — Geliştirmeye Hazır


1. Amaç ve Genel Çerçeve

Ne inşa ediyoruz?

Ham mobil sinyal verisini, her seferinde trilyonlarca satır işlemek yerine bir kez hesaplayıp cache'lenen, makine tarafından okunabilir, standardize edilmiş JSON paketlerine (KART) dönüştüren bir sistemdir.

İki kart tipi:

Kart Hedef Tetikleyici Soru
Kişi Kartı (Person Card) Bir device_id'nin davranışsal DNA'sı "Bu kişi kim? Nerede yaşıyor, ne zaman aktif, nereleri ziyaret ediyor?"
Mekan Kartı (Place Card) Bir poi_id'nin ziyaretçi profili ve rekabet durumu "Bu mekana kim geliyor, ne zaman dolu, rakip yoğunluğu ne?"

Neden KART?

  • Maliyet: Her sorguda Spark/DuckDB tetiklenmez; DB'den JSON çekilir — milisaniyeler.
  • Kesinlik: LLM (Profil aşaması) soyut yorum yapmak yerine sayısal gerçekler üzerine çalışır.
  • Karşılaştırılabilirlik: 30 günlük kart ile 90 günlük kartı matematiksel olarak trend analizi için yanyana koyabilmek.

Geliştirme Önceliği: Önce Kişi Kartları biriktirilecek, ardından Mekan Kartlarının visitor_category_affinity alanı bu kartlardan beslenecek. Mekan Kartları Kişi Kartı olmadan da üretilebilir (affinity = null).


2. Hedef Çıktı Formatları

Kişi Kartı

{
  "id": "aaaa-bbbb-cccc-dddd",
  "type": "person",
  "period": "30d",
  "data": {
    "activity_metrics": {
      "total_signals": 120,
      "unique_days_active": 18,
      "frequency_score": 0.6
    },
    "location_dna": {
      "home_neighborhood": "Akatlar",
      "work_neighborhood_inferred": "Levent",
      "top_visited_neighborhoods": ["Akatlar", "Etiler", "Bebek"],
      "mobility_radius_km": 4.2
    },
    "top_categories": {
      "Modern Pub & Bistro": 15,
      "Kebap": 8,
      "Kokteyl Bar": 3,
      "Geleneksel Meyhane": 5
    },
    "time_segments": {
      "morning_07_11": 0.1,
      "afternoon_11_15": 0.2,
      "late_day_15_19": 0.3,
      "prime_time_19_23": 0.35,
      "night_23_07": 0.05
    }
  }
}

Not: reliability_index kaldırıldı — formülü belirsiz, somut değer katmıyor. top_categories anahtarları master.poi.category değerlerini (Mapin standart isimleri) kullanır.

Mekan Kartı

{
  "id": "POI_9988",
  "type": "place",
  "period": "90d",
  "data": {
    "store_info": {
      "name": "Kaan Tekel",
      "category": "Off-Trade",
      "sub_category": "Tekel Bayii",
      "extra_data": {
        "package_home": "yes",
        "segment": "Diamond",
        "operation_hours": "09:00-01:00"
      }
    },
    "visitor_stats": {
      "total_unique_visitors": 450,
      "loyalty_rate": 0.12,
      "visitor_category_affinity": null
    },
    "neighborhood": {
      "name": "Beşiktaş/Sinanpaşa",
      "ses_score": null,
      "nearby_competitors": 4,
      "neighboring_attractions": ["University", "Ferry_Port"]
    },
    "density_analytics": {
      "signal_density_index": 0.56,
      "peak_hour_window": "19:00-21:00",
      "hourly_density_index": [0.1, 0.1, 0.2, 0.5, 0.9, 0.8]
    }
  }
}

Not: avg_occupancysignal_density_index olarak yeniden adlandırıldı (kapasite verisi yok). visitor_category_affinity ve ses_score şimdilik null; veri birikmesiyle doldurulacak.


3. Sinyal Verisi Şeması (S3 Parquet)

Teyit edildi — fetchDeviceRecords / read_parquet ile çekilen kolon yapısı:

Kolon Tip Açıklama
timestamp int64 (Unix epoch, saniye) Sinyal zamanı. datetime.utcfromtimestamp(ts) ile dönüştürülür
device_aid string Cihaz kimliği (UUID formatı)
latitude float64 Enlem (WGS-84)
longitude float64 Boylam (WGS-84)
horizontal_accuracy float64 Konum hassasiyeti (metre)
neighborhood string Mahalle (genellikle boş — cleaned.neighborhoods dolmadan güvenilmez)
h3_res9_id int64 H3 res=9 hücre ID (int formatı)

Timestamp dönüşümü:

import datetime
dt = datetime.datetime.utcfromtimestamp(1735499320)
# → datetime(2025, 12, 29, 13, 28, 40)


4. Mevcut SDK ile Ne Örtüşüyor?

Aşağıdakiler zaten var — KART sistemi bunları tüketecek, yeniden yazmayacak.

KART İhtiyacı Mevcut Bileşen Durum
Ziyaretçi sayımı (radius bazlı) FootfallEngine.getCountByRadius() ✅ Hazır
Ziyaretçi cihaz listesi FootfallEngine.getDeviceList() ✅ Hazır
Cihaz sinyal kayıtları FootfallEngine.fetchDeviceRecords() ✅ Hazır
Koordinat → POI etiketleme CoordinateIdentifier.identify() ✅ Hazır
PostgreSQL bağlantısı PgClient ✅ Hazır
DuckDB S3 erişimi DuckDBClient ✅ Hazır
Ortam / config yönetimi ConfigManager ✅ Hazır
H3 hücre hesaplama geo_utils.h3CentroidCell ✅ Hazır
Haversine mesafe geo_utils.haversineDistance ✅ Hazır
Rakip POI listesi (DB) PgClient + master.poi ✅ Hazır
Mahalle reverse geocode cleaned.neighborhoods.geom (ST_Contains) ⚠️ Tablo boş — önce doldurulmalı
SES skoru analytics.neighborhood_analytics.ses_distribution ⚠️ Tablo boş — null ile ilerlenir

5. Veritabanı Şeması — Yeni identity Schema

Tüm KART sistemi tabloları identity adlı yeni schema altında toplanacak. public.coordinate_cache da buraya taşınacak (user public'tekini siler, kod identity altında açar).

-- Yeni schema
CREATE SCHEMA IF NOT EXISTS identity;

-- Koordinat önbelleği (coordinate_cache public'ten taşındı)
CREATE TABLE IF NOT EXISTS identity.coordinate_cache (
    id              SERIAL PRIMARY KEY,
    geom            GEOMETRY(Point, 4326) NOT NULL,
    lat             NUMERIC(10, 7) NOT NULL,
    lon             NUMERIC(10, 7) NOT NULL,
    place_id        TEXT,
    name            TEXT,
    primary_type    TEXT,
    types           JSONB,
    google_maps_uri TEXT,
    cid             TEXT,
    address         TEXT,
    queried_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_coord_cache_geom ON identity.coordinate_cache USING GIST(geom);

-- Kişi ve Mekan Kartları
CREATE TABLE IF NOT EXISTS identity.cards (
    id           TEXT        NOT NULL,
    type         TEXT        NOT NULL CHECK (type IN ('person', 'place')),
    period       TEXT        NOT NULL,  -- '30d', '90d' vb.
    created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at   TIMESTAMPTZ NOT NULL,
    json_content JSONB       NOT NULL,
    PRIMARY KEY (id, period)
);
CREATE INDEX IF NOT EXISTS idx_cards_type_period ON identity.cards (id, type, period);

-- Mekan ↔ Kişi ilişki tablosu
CREATE TABLE IF NOT EXISTS identity.card_links (
    place_id   TEXT        NOT NULL,
    person_id  TEXT        NOT NULL,
    period     TEXT        NOT NULL,
    visit_date DATE        NOT NULL,
    PRIMARY KEY (place_id, person_id, period, visit_date)
);
CREATE INDEX IF NOT EXISTS idx_card_links_place ON identity.card_links (place_id, period);
CREATE INDEX IF NOT EXISTS idx_card_links_person ON identity.card_links (person_id, period);

poi.py güncellenmesi gerekiyor: _saveToCache ve _lookupCache metodları public.coordinate_cache yerine identity.coordinate_cache kullanacak şekilde değiştirilmeli.


6. Geliştirilecek Yeni Modüller

6.1 core.time_windows — Zaman Dilimi Sabitleri

Ne yapacak: Period parametresini datetime aralığına çeviren yardımcı + 5 zaman dilimi sabitleri.

TIME_SEGMENTS = {
    "morning":    (7, 11),
    "afternoon":  (11, 15),
    "late_day":   (15, 19),
    "prime_time": (19, 23),
    "night":      (23, 7),   # gece yarısı geçişi: 23'ten 7'ye
}

def periodToRange(period: str) -> tuple[datetime, datetime]:
    # "30d" → (now - 30gün, now)
    # "90d" → (now - 90gün, now)

Bağımlılık: Yok. İlk yazılacak.


6.2 mobility.home_detector — Ev/İş Tespiti

Ne yapacak: Cihazın gece/gündüz sinyal yoğunluğuna bakarak en olası ev ve iş mahallesini tahmin eder.

Giriş:  device_id, sinyal DataFrame (timestamp Unix epoch, latitude, longitude, h3_res9_id)
Mantık:
  - timestamp → datetime dönüşümü (utcfromtimestamp)
  - 00:00–06:00 sinyalleri → en yoğun H3 hücre centroid → ST_Contains ile mahalle
  - 09:00–18:00 sinyalleri → en yoğun H3 hücre centroid → ST_Contains ile mahalle
Çıkış:  {"home": "Akatlar", "work": "Levent", "top_neighborhoods": [...]}

⚠️ Ön koşul: cleaned.neighborhoods.geom kolonunun dolu olması şart. Tablo şu an boş — H3 centroid koordinatı için ST_Contains çalışmayacak. Bu modül implement edilebilir ama cleaned.neighborhoods doldurulmadan test edilemez.

Bağımlılık: core.time_windows, geo_utils.h3CentroidCell, PgClient.


6.3 mobility.time_segmenter — Zaman & Aktivite Metrikleri

Ne yapacak: Sinyal dağılımını 5 zaman dilimine normalize eder; aktivite metriklerini hesaplar.

Giriş:  sinyal DataFrame (timestamp Unix epoch), period_days
Mantık:
  - Her satır: timestamp → utcfromtimestamp → saat → hangi zaman dilimi?
  - Zaman dilimi başına sinyal sayısı → normalize (toplam = 1.0)
  - unique_days_active: date(utcfromtimestamp(ts)) distinct count
  - frequency_score = unique_days_active / period_days
Çıkış:  time_segments dict, activity_metrics dict (reliability_index yok)

Bağımlılık: core.time_windows.


6.4 analytics.visitor_flow — Sadakat & Yoğunluk Metrikleri

Ne yapacak: Mekan kartının sayısal analitik kısmını hesaplar.

Giriş:  poi koordinatı, radius (200m varsayılan), sinyal DataFrame/DuckDB, period
Mantık:
  - loyalty_rate:
      period içinde 3+ farklı günde görülen cihaz / total_unique_visitors
  - hourly_density_index (6 dilim, 4'er saat):
      her 4 saatlik dilime düşen sinyal sayısı → normalize → [0..1]
  - signal_density_index: period ortalamasına göre relatif yoğunluk
  - peak_hour_window: max yoğunluk dilimine karşılık gelen saat aralığı
Çıkış:  density_analytics dict (avg_occupancy YOK, signal_density_index var), loyalty_rate float

Bağımlılık: FootfallEngine, core.time_windows.


6.5 analytics.competitor_analysis — Rakip Sayımı

Ne yapacak: Mekanın 500m çevresindeki aynı kategoriden POI sayısını döndürür.

Giriş:  lat, lon, radius_m (500m), target_category
Mantık: master.poi tablosunu PostGIS ST_DWithin sorgusu
Çıkış:  nearby_competitors int, neighboring_attractions list[str]
        ses_score: analytics.neighborhood_analytics.ses_distribution'dan — boşsa null

Bağımlılık: PgClient.


6.6 data.identity.store — Card Store (CRUD)

Ne yapacak: Üretilen kartları identity.cards tablosuna yazar; TTL yönetimi yapar.

CardStore.get(id, period)  dict | None
CardStore.save(card: dict, ttlDays: int = 30)  None  # expires_at = now + ttlDays
CardStore.invalidate(id, period)  None
CardStore.linkVisitor(placeId, personId, period, visitDate)  None
CardStore.getVisitors(placeId, period)  list[str]  # person_id listesi

TTL: Parametre ile özelleştirilebilir, varsayılan 30 gün.

Bağımlılık: PgClient, identity schema'nın mevcut olması.


6.7 data.identity.transformers — Orkestrasyon Katmanı

Ne yapacak: Tüm modülleri koordine ederek ham girdiyi standart KART JSON'una dönüştürür.

PersonCardBuilder.build(device_id, period_days=30, ttlDays=30) → dict
  1. Sinyalleri DuckDB'den çek (period filtresi: now - period_days)
  2. time_segmenter çalıştır → activity_metrics, time_segments
  3. home_detector çalıştır → location_dna (neighborhoods boşsa null döner)
  4. CoordinateIdentifier ile her benzersiz koordinat → top_categories hesapla
  5. JSON şemasına serialize et
  6. CardStore.save(ttlDays) ile kaydet

PlaceCardBuilder.build(poi_id, period_days=90, ttlDays=30) → dict
  1. POI koordinatını PgClient'tan çek (master.poi)
  2. FootfallEngine ile visitor device_id listesi al
  3. visitor_flow çalıştır → density_analytics, loyalty_rate
  4. competitor_analysis çalıştır → nearby_competitors, ses_score (null olabilir)
  5. visitor_category_affinity:
       → visitor device_id'leri için identity.cards'ta Kişi Kartı var mı?
       → Varsa: top_categories'leri oranla
       → Yoksa: null bırak (tavuk-yumurta — Kişi Kartları birikmesi beklenir)
  6. JSON şemasına serialize et
  7. CardStore.save + linkVisitor ile kaydet

Bağımlılık: Hepsi.


7. Geliştirme Sırası

[DB Hazırlığı — Geliştirme öncesi]
  ├── identity schema oluştur
  ├── identity.cards, identity.card_links, identity.coordinate_cache tabloları
  ├── poi.py → coordinate_cache referansını public → identity'ye güncelle
  └── cleaned.neighborhoods.geom doldurma görevi (home_detector için — ayrı sprint)

Aşama 1 (Temel):
  └── core.time_windows

Aşama 2 (Kişi Kartı Motorları):
  ├── mobility.time_segmenter   (neighborhoods olmadan çalışır)
  └── mobility.home_detector    (⚠️ neighborhoods dolu değilse home/work null döner)

Aşama 3 (Mekan Kartı Motorları):
  ├── analytics.visitor_flow
  └── analytics.competitor_analysis

Aşama 4 (Saklama):
  └── data.identity.store

Aşama 5 (Orkestrasyon — ÖNCELİK: Kişi Kartı):
  ├── PersonCardBuilder  ← önce bu
  └── PlaceCardBuilder   ← Kişi Kartları birikmeden visitor_category_affinity null kalır

Her aşama kendi testleriyle birlikte teslim edilir; bir sonraki aşama önceki testler yeşil olmadan başlamaz.


8. Alınan Kararlar (Tüm Sorular Yanıtlandı)

# Soru Karar
S1 reliability_index formülü Kaldırıldı — belirsiz formül, katma değeri yok
S2 avg_occupancy / kapasite signal_density_index olarak yeniden adlandırıldı
S3 Affinity tavuk-yumurta İlk çalıştırmada null; Kişi Kartları birikmesiyle doldurulur
S4 Paket path mapindata.data.identityPgClient ile aynı pakette
S5 SES skoru analytics.neighborhood_analytics.ses_distribution var ama boş; null ile ilerlenir, doldurmak ayrı sprint
S6 KART TTL Parametre ile özelleştirilebilir, default 30 gün
S7 mobility_radius_km formülü Tüm sinyallerin ağırlık merkezine olan max mesafenin ortalaması
S8 Visitor_Logs şeması Ayrı identity.card_links(place_id, person_id, period, visit_date) tablosu

Açık Kalan Altyapı Görevleri

Görev Etkisi Sprint
cleaned.neighborhoods.geom doldurulması home_detector home/work tespiti Ayrı sprint
analytics.neighborhood_analytics.ses_distribution doldurulması Mekan Kartı ses_score Ayrı sprint
public.coordinate_cache silinmesi (user tarafından) poi.py migration tamamlanır DB Hazırlığı

9. Kabul Kriterleri (Definition of Done)

Her modül şu kriterleri sağladığında "tamamlandı" sayılır:

  • [ ] Modül implement edildi
  • [ ] Birim testleri yazıldı ve yeşil
  • [ ] docstring mevcut
  • [ ] pyproject.toml'da paket path kayıtlı
  • [ ] Bağımlı modüllerin testleri hâlâ yeşil (regresyon yok)

1. Amaç ve Genel Çerçeve

Ne inşa ediyoruz?

Ham mobil sinyal verisini, her seferinde trilyonlarca satır işlemek yerine bir kez hesaplayıp cache'lenen, makine tarafından okunabilir, standardize edilmiş JSON paketlerine (KART) dönüştüren bir sistemdir.

İki kart tipi:

Kart Hedef Tetikleyici Soru
Kişi Kartı (Person Card) Bir device_id'nin davranışsal DNA'sı "Bu kişi kim? Nerede yaşıyor, ne zaman aktif, nereleri ziyaret ediyor?"
Mekan Kartı (Place Card) Bir poi_id'nin ziyaretçi profili ve rekabet durumu "Bu mekana kim geliyor, ne zaman dolu, rakip yoğunluğu ne?"

Neden KART?

  • Maliyet: Her sorguda Spark/DuckDB tetiklenmez; DB'den JSON çekilir — milisaniyeler.
  • Kesinlik: LLM (Profil aşaması) soyut yorum yapmak yerine sayısal gerçekler üzerine çalışır.
  • Karşılaştırılabilirlik: 30 günlük kart ile 90 günlük kartı matematiksel olarak trend analizi için yanyana koyabilmek.

2. Hedef Çıktı Formatları

Kişi Kartı

{
  "id": "aaaa-bbbb-cccc-dddd",
  "type": "person",
  "period": "30d",
  "data": {
    "activity_metrics": {
      "total_signals": 120,
      "unique_days_active": 18,
      "frequency_score": 0.6,
      "reliability_index": 0.85
    },
    "location_dna": {
      "home_neighborhood": "Akatlar",
      "work_neighborhood_inferred": "Levent",
      "top_visited_neighborhoods": ["Akatlar", "Etiler", "Bebek"],
      "mobility_radius_km": 4.2
    },
    "top_categories": {
      "modern_retail": 15,
      "gym": 8,
      "tekel": 3,
      "pub_bar": 5
    },
    "time_segments": {
      "morning_07_11": 0.1,
      "afternoon_11_15": 0.2,
      "late_day_15_19": 0.3,
      "prime_time_19_23": 0.35,
      "night_23_07": 0.05
    }
  }
}

Mekan Kartı

{
  "id": "POI_9988",
  "type": "place",
  "period": "90d",
  "data": {
    "store_info": {
      "name": "Kaan Tekel",
      "category": "Off-Trade",
      "sub_category": "Tekel Bayii",
      "extra_data": {
        "package_home": "yes",
        "segment": "Diamond",
        "operation_hours": "09:00-01:00"
      }
    },
    "visitor_stats": {
      "total_unique_visitors": 450,
      "loyalty_rate": 0.12,
      "visitor_category_affinity": {
        "premium_horeca": 0.4,
        "fitness": 0.3,
        "fast_food": 0.3
      }
    },
    "neighborhood": {
      "name": "Beşiktaş/Sinanpaşa",
      "ses_score": "A-",
      "nearby_competitors": 4,
      "neighboring_attractions": ["University", "Ferry_Port"]
    },
    "density_analytics": {
      "avg_occupancy": 0.56,
      "peak_hour_window": "19:00-21:00",
      "hourly_density_index": [0.1, 0.1, 0.2, 0.5, 0.9, 0.8]
    }
  }
}

3. Mevcut SDK ile Ne Örtüşüyor?

Aşağıdakiler zaten var — KART sistemi bunları tüketecek, yeniden yazmayacak.

KART İhtiyacı Mevcut Bileşen Notlar
Ziyaretçi sayımı (radius bazlı) FootfallEngine.getCountByRadius() 200m radius doğrudan kullanılabilir
Ziyaretçi cihaz listesi FootfallEngine.getDeviceList() Mekan kartı "Visitor IDs" için
Koordinat → POI etiketleme CoordinateIdentifier.identify() top_categories için
PostgreSQL bağlantısı PgClient Card Store altyapısı
DuckDB S3 erişimi DuckDBClient Ham sinyal okuma
Ortam / config yönetimi ConfigManager Tüm credentiallar buradan
H3 hücre hesaplama geo_utils.h3CentroidCell home/work tespiti için temel
Haversine mesafe geo_utils.haversineDistance mobility_radius için

4. Geliştirilecek Yeni Modüller

4.1 core.time_windows — Zaman Dilimi Sabitleri

Ne yapacak: Period parametresini ("30d", "90d") datetime aralığına çeviren yardımcı + 5 zaman diliminin saat sabitlerini tutan modül.

TIME_SEGMENTS = {
  "morning":    (7, 11),
  "afternoon":  (11, 15),
  "late_day":   (15, 19),
  "prime_time": (19, 23),
  "night":      (23, 7),   # gece yarısı geçişi dikkat
}

period_to_range("30d") → (start_dt, end_dt)

Bağımlılık: Yok. En basit modül — ilk yazılacak.


4.2 mobility.home_detector — Ev/İş Tespiti

Ne yapacak: Cihazın gece/gündüz sinyal yoğunluğuna bakarak en olası ev ve iş mahallesini tahmin eder.

Giriş:  device_id, sinyal DataFrame (timestamp, lat, lon, h3_res9_id)
Mantık:
  - 00:00–06:00 sinyalleri → en yoğun H3 hücre → reverse geocode → mahalle
  - 09:00–18:00 sinyalleri → en yoğun H3 hücre → reverse geocode → mahalle
Çıkış:  {"home": "Akatlar", "work": "Levent", "top_neighborhoods": [...]}

Bağımlılık: core.time_windows, geo_utils.h3CentroidCell, DuckDB sorgu altyapısı.


4.3 mobility.time_segmenter — Zaman & Aktivite Metrikleri

Ne yapacak: Cihazın sinyal dağılımını 5 zaman dilimine normalize eder; aktivite metriklerini hesaplar.

Giriş:  device_id, sinyal DataFrame, period_days
Mantık:
  - Her sinyal → hangi zaman dilimi?
  - Zaman dilimi başına sinyal sayısı → normalize (toplam = 1.0)
  - unique_days_active say
  - frequency_score = unique_days_active / period_days
Çıkış:  time_segments dict, activity_metrics dict

Bağımlılık: core.time_windows.


4.4 analytics.visitor_flow — Sadakat & Yoğunluk Metrikleri

Ne yapacak: Mekan kartının sayısal analitik kısmını hesaplar.

Giriş:  poi koordinatı, radius (200m varsayılan), sinyal DataFrame/DuckDB, period
Mantık:
  - loyalty_rate:
      period içinde 3+ farklı günde görülen cihaz / total_unique_visitors
  - hourly_density_index (6 dilim, 4'er saat):
      her 4 saatlik dilime düşen sinyal sayısı → normalize
  - peak_hour_window:
      max yoğunluk dilimine karşılık gelen saat aralığı
Çıkış:  density_analytics dict, loyalty_rate float

Bağımlılık: FootfallEngine, core.time_windows.


4.5 analytics.competitor_analysis — Rakip Sayımı

Ne yapacak: Mekanın 500m çevresindeki aynı kategoriden POI sayısını döndürür.

Giriş:  lat, lon, radius_m (500m), target_category
Mantık: master.poi tablosunu PostGIS / haversine sorgusu
Çıkış:  nearby_competitors int, neighboring_attractions list[str]

Bağımlılık: PgClient, geo_utils.haversineDistance.


4.6 data.identity.store — Card Store (CRUD)

Ne yapacak: Üretilen kartları PostgreSQL'e yazar, cache kontrolü yapar.

Tablo şeması:
  cards(id TEXT, type TEXT, period TEXT, created_at TIMESTAMPTZ,
        expires_at TIMESTAMPTZ, json_content JSONB)
  PRIMARY KEY (id, period)
  INDEX ON (id, type, period)

API:
  CardStore.get(id, period) → dict | None
  CardStore.save(card: dict)  → None
  CardStore.invalidate(id, period) → None

Bağımlılık: PgClient.


4.7 data.identity.transformers — Orkestrasyon Katmanı

Ne yapacak: Yukarıdaki tüm modülleri koordine ederek ham girdiyi standart KART JSON'una dönüştürür.

PersonCardBuilder.build(device_id, period_days) → dict
  1. Sinyalleri DuckDB'den çek (period filtresi)
  2. home_detector çalıştır
  3. time_segmenter çalıştır
  4. CoordinateIdentifier ile top_categories hesapla
  5. JSON şemasına serialize et
  6. CardStore.save() ile kaydet

PlaceCardBuilder.build(poi_id, period_days) → dict
  1. POI koordinatını PgClient'tan çek
  2. FootfallEngine ile visitor listesi al
  3. visitor_flow çalıştır
  4. competitor_analysis çalıştır
  5. visitor_category_affinity: visitor Kişi Kartlarından topla
  6. JSON şemasına serialize et
  7. CardStore.save() ile kaydet

Bağımlılık: Hepsi.


5. Geliştirme Sırası

Bağımlılık zinciri gözetilerek aşağıdaki sırayla ilerlenecek:

Aşama 1 (Temel):
  └── core.time_windows

Aşama 2 (Kişi Kartı Motorları):
  ├── mobility.home_detector
  └── mobility.time_segmenter

Aşama 3 (Mekan Kartı Motorları):
  ├── analytics.visitor_flow
  └── analytics.competitor_analysis

Aşama 4 (Saklama):
  └── data.identity.store  (+  DB migration: cards tablosu)

Aşama 5 (Orkestrasyon):
  └── data.identity.transformers
      ├── PersonCardBuilder
      └── PlaceCardBuilder

Her aşama kendi testleriyle birlikte teslim edilir; bir sonraki aşama önceki testler yeşil olmadan başlamaz.


6. Geliştirmeye Başlamadan Önce Netleşmesi Gereken Sorular

Aşağıdaki 8 belirsizlik, doğrudan implement kararlarını etkiliyor. Yanıtlanmadan kod yazılmaya başlanmamalı.


S1 — reliability_index formülü nedir?

Etkilediği alan: activity_metrics.reliability_index

Dokümanda "Verinin ne kadar temiz olduğu" yazıyor ama somut formül yok. Üç kandidat var:

Opsiyon Formül Açıklama
A mean(horizontal_accuracy < eşik) Konum hassasiyeti bazlı
B 1 - (boş_timestamp_oranı) Veri bütünlüğü bazlı
C unique_days / toplam_aktif_saat Düzenlilik bazlı

→ Hangi formül kullanılacak? Yoksa kompozit mi?


S2 — avg_occupancy ve hourly_density_index kapasitesiz hesaplanabilir mi?

Etkilediği alan: density_analytics.avg_occupancy

"Doluluk" gerçek anlamıyla fiziksel kapasiteye göre oran demek. Ama kapasite verisi elimizde yok. İki seçenek:

Opsiyon Açıklama
A Relatif yoğunluk: sinyal sayısı / period ortalaması (kapasite yok, karşılaştırmalı)
B Alan adını signal_density_index olarak değiştir, avg_occupancy hesaplanmaz

→ Kapasite verisi nereden gelecek? Yoksa alan adı değiştirilmeli mi?


S3 — visitor_category_affinity tavuk-yumurta problemi nasıl çözülecek?

Etkilediği alan: visitor_stats.visitor_category_affinity

"Gelen ziyaretçilerin diğer ziyaret ettiği kategori ağırlıkları" → bu bilgi visitor'ların Kişi Kartlarından geliyor. Ama ilk çalıştırmada hiç Kişi Kartı yok.

Opsiyon Açıklama
A İlk çalıştırmada bu alan null / boş bırakılır, kartlar birikmesi beklenir
B Ham sinyal üzerinden CoordinateIdentifier ile anlık hesaplanır (yavaş)
C Mekan kartı önce affinity hesaplamadan oluşturulur, sonraki güncellemede doldurulur

→ Hangi strateji benimseniyor?


S4 — data.identity mi, identity.card mı? — Paket path çelişkisi

Etkilediği alan: Tüm modül dizin yapısı

Dokümanın farklı bölümlerinde iki farklı path geçiyor: - Bölüm 3: mapindata.data.identity - Bölüm 5: mapindata.identity.card.transformers

Bu iki farklı paket yapısı — hangisi seçilirse pyproject.toml ve tüm importlar buna göre kurgulanacak.

Opsiyon Yapı Neden?
A mapindata.data.identity Saklama odaklı, PgClient ile aynı pakette
B mapindata.identity (ayrı top-level) Gelecekte identity.profile (LLM) ile aynı namespace

→ Hangi path kullanılacak?


S5 — ses_score tablosu kimin sorumluluğunda ve ne zaman hazır olacak?

Etkilediği alan: neighborhood.ses_score, dolaylı olarak competitor_analysis

ses_score için şu an elimizde tablo yok. Bu alan: - Kim hazırlayacak? - Hangi metodoloji (TÜİK verisi, POI yoğunluğu, sinyal demografisi)? - Hangi DB'ye, hangi şemaya yazılacak? - competitor_analysis buna depend ediyor — tablo yoksa bu modül kısmi çalışır.

→ SES tablosu bu sprint'e dahil mi, yoksa ses_score: null ile ilerlenecek mi?


S6 — KART TTL (geçerlilik süresi) ne olacak?

Etkilediği alan: data.identity.storeexpires_at alanı

cards tablosuna expires_at ekleneceği net, ama süre belirsiz:

Opsiyon Kural
A Sabit: 30d kart 7 gün geçerli, 90d kart 30 gün geçerli
B Dinamik: kart periyodunun %25'i kadar geçerli
C Manuel: CardStore.invalidate() çağrılmadıkça süresiz geçerli

→ TTL politikası nedir?


S7 — mobility_radius_km formülü ne olacak?

Etkilediği alan: location_dna.mobility_radius_km

Dokümanda "H3 gridleri arası geçiş sayısı" yazıyor — ama bu km değil. İki kandidat:

Opsiyon Formül
A Günlük bounding box köşegen uzunluğunun ortalaması (haversine)
B Tüm sinyallerin ağırlık merkezine olan max mesafenin ortalaması

→ Hangi formül?


S8 — Visitor_Logs ilişki tablosunun şeması nedir?

Etkilediği alan: data.identity.store — Mekan ↔ Kişi ilişkisi

"Bu mekana gelen 500 kişinin kartını getir" akışı için ilişki haritası gerekiyor. İki mimari seçenek:

Opsiyon Şema Avantaj / Dezavantaj
A cards tablosuna visitor_ids JSONB[] ekle Basit, tek tablo
B Ayrı card_links(place_id, person_id, period, visit_date) tablosu İndekslenebilir, sorgulama esnek ama 2 tablo

→ Hangi şema?


7. Özet Karar Matrisi

# Soru Öncelik Beklenen Yanıtlayan
S1 reliability_index formülü Yüksek — Aşama 2 öncesi Data Science
S2 avg_occupancy / kapasite Yüksek — Aşama 3 öncesi Ürün + Data
S3 Affinity tavuk-yumurta Yüksek — Aşama 5 öncesi Mimari karar
S4 Paket path seçimi Kritik — Aşama 1 öncesi Teknik karar
S5 SES tablosu hazırlığı Orta — bu sprint'e dahil mi? Proje yönetimi
S6 KART TTL politikası Orta — Aşama 4 öncesi Ürün
S7 mobility_radius_km formülü Orta — Aşama 2 öncesi Data Science
S8 Visitor_Logs şeması Yüksek — Aşama 4 öncesi Teknik karar

8. Kabul Kriterleri (Definition of Done)

Her modül şu kriterleri sağladığında "tamamlandı" sayılır:

  • [ ] Modül implement edildi
  • [ ] Birim testleri yazıldı ve yeşil
  • [ ] docstring mevcut (tek kaynak olarak kartlarin_tanimi.md referanslı)
  • [ ] pyproject.toml'da paket path kayıtlı
  • [ ] Bağımlı modüllerin testleri hâlâ yeşil (regresyon yok)