Files
trendyol-analiz/backend/analytics/metrics.py
furkanyigit34 ce1dc1e25f feat: tek birleştirilmiş JSON yapısına geçiş + sosyal kanıt fallback
Ne yaptık:
- data_consolidator.py: Tüm normalizasyon ve hesaplama mantığını main.py'den çıkardık
- Dashboard endpoint 1150 satırdan 25 satıra düştü (main.py -1730/+1880 net)
- Enrichment bitince otomatik konsolide dosya oluşturuluyor (report_{id}_data.json)
- Eski raporlar ilk dashboard isteğinde lazy migration ile konsolide ediliyor
- Trendyol API artık order-count döndürmediği için baskets fallback eklendi
- Inline socialProofs (scrape) > enrichment API öncelik sırası uygulandı
- Frontend KPI başlıkları orders/baskets durumuna göre dinamik değişiyor
- logging_config.py, category_seeder.py, alembic migration eklendi
- Playwright ile 9 tab test edildi, tüm veriler doğru

Neden yaptık:
- 3 farklı kaynaktan her istekte birleştirme yapılması veri tutarsızlığına ve yavaşlığa yol açıyordu
- Tek konsolide JSON dosyası ile dashboard anında yükleniyor
- Trendyol API değişikliği nedeniyle sipariş verisi kayboluyordu, baskets fallback ile çözüldü

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:25:25 +03:00

300 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Metrics Utilities - Genel metrik hesaplama fonksiyonları
Performance optimized with Counter and NumPy
"""
from typing import List, Dict
from collections import Counter, defaultdict
import numpy as np
def calculate_hhi_index(products: List[Dict]) -> float:
"""
HHI (Herfindahl-Hirschman Index) hesapla
Pazar konsantrasyonunu ölçer (0-10000 arası)
PERFORMANCE: Counter kullanarak optimize edildi
Args:
products: Ürün listesi
Returns:
HHI Index (0-10000)
"""
# Counter kullanarak marka sayılarını topla (O(n) complexity)
brand_names = [
p.get("brand", {}).get("name", "Unknown")
for p in products
if p.get("brand", {}).get("name")
]
if not brand_names:
return 0
# Counter ile hızlı sayım
brand_counts = Counter(brand_names)
total = len(products)
if total == 0:
return 0
# NumPy array ile hızlı hesaplama
counts_array = np.array(list(brand_counts.values()))
market_shares = counts_array / total
hhi = np.sum(market_shares ** 2) * 10000
return round(float(hhi), 2)
def calculate_market_concentration(brand_counts: Dict[str, int]) -> Dict[str, float]:
"""
Pazar konsantrasyonu hesapla (marka payları)
PERFORMANCE: NumPy ile optimize edildi
Args:
brand_counts: Marka bazlı ürün sayıları
Returns:
Marka payları (yüzde)
"""
if not brand_counts:
return {}
total = sum(brand_counts.values())
if total == 0:
return {}
# NumPy ile hızlı hesaplama
counts_array = np.array(list(brand_counts.values()))
shares = (counts_array / total) * 100
return {
brand: round(float(share), 2)
for brand, share in zip(brand_counts.keys(), shares)
}
def calculate_price_premium(category_avg_price: float, overall_avg_price: float) -> float:
"""
Fiyat primi hesapla
Kategori ortalamasının genel ortalamaya göre farkı
Args:
category_avg_price: Kategori ortalama fiyatı
overall_avg_price: Genel ortalama fiyat
Returns:
Fiyat primi (yüzde)
"""
if overall_avg_price == 0:
return 0
premium = ((category_avg_price / overall_avg_price) - 1) * 100
return round(premium, 2)
def calculate_conversion_rates(social_data: Dict) -> Dict[str, float]:
"""
Conversion rate'leri hesapla
Görüntülenme → Sepet → Sipariş dönüşüm oranları
Args:
social_data: Sosyal kanıt verileri
Returns:
Conversion rate'ler (yüzde)
"""
details = social_data.get("details", {})
total_views = sum(
data.get("page_views", 0)
for data in details.values()
)
total_baskets = sum(
data.get("baskets", 0)
for data in details.values()
)
total_orders = sum(
data.get("orders", 0)
for data in details.values()
)
return {
"view_to_basket": round((total_baskets / total_views * 100) if total_views > 0 else 0, 2),
"basket_to_order": round((total_orders / total_baskets * 100) if total_baskets > 0 else 0, 2),
"view_to_order": round((total_orders / total_views * 100) if total_views > 0 else 0, 2),
"total_views": total_views,
"total_baskets": total_baskets,
"total_orders": total_orders
}
def calculate_brand_strength(
brand_products: List[Dict],
total_products: int,
social_data: Dict = None
) -> float:
"""
Marka güç skoru hesapla
Formül: brand_share + (avg_rating * 5) - stockout_rate
Args:
brand_products: Markaya ait ürünler
total_products: Toplam ürün sayısı
social_data: Sosyal kanıt verileri (opsiyonel)
Returns:
Marka güç skoru
"""
brand_count = len(brand_products)
brand_share = (brand_count / total_products * 100) if total_products > 0 else 0
# Rating ortalaması
ratings = []
for p in brand_products:
rating = p.get("rating", 0)
if isinstance(rating, dict):
rating = rating.get("averageRating", 0)
if rating and rating > 0:
ratings.append(rating)
avg_rating = sum(ratings) / len(ratings) if ratings else 0
# Stockout rate
out_of_stock = sum(1 for p in brand_products if not p.get("inStock", False))
stockout_rate = (out_of_stock / brand_count * 100) if brand_count > 0 else 0
# Güç skoru
strength_score = brand_share + (avg_rating * 5) - stockout_rate
return round(strength_score, 2)
def calculate_potential_score(
page_views: int,
orders: int,
review_count: int,
conversion_rate: float,
competition_level: str
) -> float:
"""
Potential score hesapla (0-100)
Ürünün büyüme potansiyelini ölçer
Args:
page_views: Görüntülenme sayısı
orders: Sipariş sayısı
review_count: Yorum sayısı
conversion_rate: Dönüşüm oranı (yüzde)
competition_level: Rekabet seviyesi (low/medium/high)
Returns:
Potential score (0-100)
"""
score = 0
# 1. Görüntülenme skoru (30 puan)
if page_views >= 10000:
score += 30
elif page_views >= 5000:
score += 20
elif page_views >= 1000:
score += 10
# 2. Conversion rate skoru (25 puan)
if conversion_rate >= 5:
score += 25
elif conversion_rate >= 3:
score += 20
elif conversion_rate >= 2:
score += 15
else:
score += 10
# 3. Rekabet seviyesi skoru (25 puan)
if competition_level == "low":
score += 25
elif competition_level == "medium":
score += 15
else:
score += 5
# 4. Yorum sayısı skoru (20 puan) - Az yorum = henüz keşfedilmemiş
if 1 <= review_count <= 10:
score += 20 # Çok az yorum, büyüme potansiyeli
elif 11 <= review_count <= 20:
score += 15
elif 21 <= review_count <= 50:
score += 10
else:
score += 5
return min(100, round(score, 2))
def get_rating_value(product: Dict) -> float:
"""
Ürün rating'ini al (dict veya number olabilir)
Args:
product: Ürün dict'i
Returns:
Rating değeri (0-5)
"""
rating = product.get("rating", 0)
if isinstance(rating, dict):
return rating.get("averageRating", 0) or 0
if rating:
return float(rating)
# Fallback: ratingScore nested object
rating_score = product.get("ratingScore", {})
if isinstance(rating_score, dict):
return float(rating_score.get("averageRating", 0) or 0)
return 0
def get_review_count(product: Dict) -> int:
"""
Ürün yorum sayısını al
Args:
product: Ürün dict'i
Returns:
Yorum sayısı
"""
review_count = product.get("rating_count", 0)
if not review_count:
rating = product.get("rating", {})
if isinstance(rating, dict):
review_count = rating.get("totalComments", 0) or rating.get("totalCount", 0) or 0
if not review_count:
# Fallback: ratingScore nested object
rating_score = product.get("ratingScore", {})
if isinstance(rating_score, dict):
review_count = rating_score.get("totalCount", 0) or 0
return int(review_count) if review_count else 0
def calculate_competition_score_from_hhi(hhi: float) -> float:
"""
HHI Index'ten rekabet skoru hesapla (0-100)
Args:
hhi: HHI Index değeri
Returns:
Rekabet skoru (0-100)
"""
if hhi < 1500:
# Düşük konsantrasyon (rekabetçi pazar) → YÜKSEK SKOR
return round(100 - (hhi / 15), 2)
elif hhi < 2500:
# Orta konsantrasyon → ORTA SKOR
return round(70 - ((hhi - 1500) / 10), 2)
else:
# Yüksek konsantrasyon (tekelci pazar) → DÜŞÜK SKOR
return round(max(0, 55 - ((hhi - 2500) / 50)), 2)