Scoring
Postbox berechnet zwei unabhaengige Score-Typen: Den Postbox Score (Profil-Level) und den Video Performance Score (VPS) (Video-Level, nur YouTube).
Postbox Score
Score von 0-100, tier-normalisiert, damit schnell wachsende kleine Kanaele dieselben Scores erreichen koennen wie grosse etablierte Kanaele.
Gewichtung
| Komponente | Gewicht | Beschreibung |
|---|---|---|
| Growth | 40% | Relatives Follower-Wachstum vs. Tier-Erwartung |
| Momentum | 30% | Beschleunigung des Wachstums (letzte 7d vs. vorherige 7d) |
| Consistency | 20% | Datenabdeckung der letzten 14 Tage |
| Engagement | 10% | Plattformspezifische Aktivitaets-Metriken |
Tier-Definitionen
| Tier | Follower-Bereich | Erwartete taegliche Growth-Rate |
|---|---|---|
| micro | 0 - 10.000 | 0,5% |
| small | 10.000 - 100.000 | 0,3% |
| medium | 100.000 - 500.000 | 0,2% |
| large | 500.000 - 1.000.000 | 0,1% |
| mega | 1.000.000+ | 0,05% |
public const TIERS = [
'micro' => [0, 10_000],
'small' => [10_000, 100_000],
'medium' => [100_000, 500_000],
'large' => [500_000, 1_000_000],
'mega' => [1_000_000, PHP_INT_MAX],
];
Location: app/Services/Scoring/ProfileScoreCalculator.php
Status
| Status | Datenpunkte | Beschreibung |
|---|---|---|
no_data | 0 | Kein Datenpunkt innerhalb von 5 Tagen |
pending | < 7 | Zu wenig Daten fuer Berechnung |
preliminary | 7 - 9 | Vorlaeufiger Score |
stable | 10+ | Stabiler Score |
public const MIN_DATA_POINTS = 7;
public const STABLE_DATA_POINTS = 10;
public const MAX_DATA_GAP_DAYS = 5;
Growth Score (40%)
Vergleicht die durchschnittliche taegliche Growth-Rate mit der Tier-Erwartung:
$dailyGrowthRate = $totalGrowthRate / $days;
$score = min(1, ($dailyGrowthRate / ($expectation * 2)) + 0.25);
Tier-Erwartung erreichen = Score ~0.5. Doppelte Erwartung = Score ~1.0.
Momentum Score (30%)
Vergleicht das Wachstum der letzten 7 Tage mit den vorherigen 7 Tagen:
- Positiver Momentum (Wachstum beschleunigt sich) = Score > 0.5
- Negativer Momentum (Wachstum verlangsamt sich) = Score < 0.5
- Normalisierung: -0.05 bis +0.05 Acceleration mapped auf 0-1
Consistency Score (20%)
Lineare Skalierung basierend auf Datenabdeckung:
$score = ($daysWithData - MIN_DATA_POINTS) / (14 - MIN_DATA_POINTS);
7 Datenpunkte = 0, 14 Datenpunkte = 1.0.
Engagement Score (10%) — V2: Unified Upload Activity
Seit V2 verwenden beide Plattformen eine einheitliche Upload-Aktivitaets-Metrik fuer faire Vergleichbarkeit:
| Plattform | Metrik | Erwartung (Max Score) | Beschreibung |
|---|---|---|---|
| YouTube | video_count Growth | 0.15/Tag (≈ 1 Video/Woche) | Upload-Frequenz basierend auf video_count Delta |
post_count Growth | 0.3/Tag (≈ 2 Posts/Woche) | Upload-Frequenz basierend auf post_count Delta |
YouTube Bonus: Wenn view_count verfuegbar ist, wird der Engagement-Score aus 60% Upload-Aktivitaet + 40% View-Engagement (views/follower, max bei 10) gemischt.
Hintergrund: Die V1-Formel nutzte fuer YouTube views/follower (lifetime) und fuer Instagram posts/day. Das fuehrte zu Instagram-Dominanz in den Top Scores, da 2 Posts/Woche = max Score, waehrend YouTube unrealistische View/Follower-Ratios brauchte.
Location: app/Services/Scoring/ProfileScoreCalculator.php (Methode calculateEngagementScore())
Tier-Relevanz-Multiplier (V2)
Micro/Small-Profile erhalten einen leichten Score-Abzug, damit groessere, relevantere Profile in Rankings dominieren:
| Tier | Multiplier | Effekt |
|---|---|---|
| micro | 0.85 | -15% |
| small | 0.95 | -5% |
| medium | 1.0 | kein Abzug |
| large | 1.0 | kein Abzug |
| mega | 1.0 | kein Abzug |
Konfigurierbar via config('postbox.scoring.tier_relevance_multiplier').
Follower-Filter (V2)
Zwei separate Schwellenwerte:
| Config | Default | Bedeutung |
|---|---|---|
postbox.scoring.min_followers_for_scoring | 100 | Minimum fuer Score-Berechnung |
postbox.scoring.min_followers_for_rankings | 500 | Minimum fuer Explore-Rankings (Top Scores, Breakouts) |
Wichtig: V2 prueft social_profiles.followers_count (aktuell) statt social_profile_daily_metrics.followers_count (historisch). Profile die unter den Schwellenwert gefallen sind, werden nicht mehr gescored.
Plattform-Balance in Explore Top Scores (V2)
Statt die Top 14 Profile rein nach Score zu sortieren (was zu Instagram-Dominanz fuehrte), werden pro Plattform separate Top-N abgefragt und dann gemischt:
'top_scores_per_platform' => [
'youtube' => 7,
'instagram' => 7,
],
Konfigurierbar via config('postbox.scoring.top_scores_per_platform').
Manual Penalty
User und Admins koennen eine manuelle Score-Strafe (0-100%) setzen. Der Standard-Toggle setzt 25%:
if ($profile->manual_penalty > 0) {
$penaltyMultiplier = 1 - ($profile->manual_penalty / 100);
$rawScore = $rawScore * $penaltyMultiplier;
}
Die Penalty wird NACH dem Tier-Relevanz-Multiplier und Following-Penalty angewandt, d.h. der 25%-Buffer reduziert den bereits adjustierten Score.
Eine Penalty von 25% reduziert den Score proportional (z.B. 80 wird zu 60).
Location: app/Livewire/Watchers/Show.php (Methode toggleScorePenalty())
Video Performance Score (VPS)
Score von 0-100 pro YouTube-Video, ebenfalls tier-normalisiert basierend auf dem Follower-Tier des Kanals.
Gewichtung
| Komponente | Gewicht | Beschreibung |
|---|---|---|
| View Performance | 40% | Views relativ zu Tier-Erwartung |
| Engagement | 25% | Like-to-View + Comment-to-View Ratios |
| Growth Trajectory | 20% | View-Wachstums-Beschleunigung |
| Freshness | 15% | Recency-Bonus fuer neuere Videos |
View Performance (40%)
Vergleicht durchschnittliche taegliche Views mit Tier-Erwartungen:
| Tier | Erwartete Views/Tag |
|---|---|
| micro | 500 |
| small | 2.000 |
| medium | 10.000 |
| large | 50.000 |
| mega | 200.000 |
Sehr kurze Videos (< 60 Sekunden) erhalten einen milden View-Performance-Abzug (1.5x hoehere Erwartung), da sie oft durch Autoplay inflated Views akkumulieren.
private const SHORT_DURATION_VIEW_MULTIPLIER = 1.5;
Logarithmische Skalierung verhindert, dass Ausreisser dominieren:
$score = 0.5 + 0.5 * log10(max(0.01, $ratio));
Engagement (25%)
Gewichtet Like-to-View (70%) und Comment-to-View (30%) Ratios:
| Tier | Erwartete Like-Rate |
|---|---|
| micro | 5% |
| small | 4% |
| medium | 3,5% |
| large | 3% |
| mega | 2,5% |
Growth Trajectory (20%)
Vergleicht die View-Wachstumsrate der juengeren Haelfte mit der aelteren Haelfte der Datenpunkte. Beschleunigende Videos scoren hoeher.
Freshness (15%)
Linearer Decay ueber 30 Tage:
private const MAX_FRESHNESS_DAYS = 30;
// Heute veroeffentlicht = 1.0, 30+ Tage alt = 0.0
$score = 1.0 - ($ageInDays / self::MAX_FRESHNESS_DAYS);
VPS Status
| Status | Datenpunkte | Beschreibung |
|---|---|---|
no_data | 0 | Keine Metriken |
pending | < 3 | Zu wenig Daten |
preliminary | 3 - 6 | Vorlaeufiger Score |
stable | 7+ | Stabiler Score |
Location: app/Services/Scoring/VideoScoreCalculator.php
Schedule
Scores werden taeglich um 05:00 UTC berechnet:
php artisan scores:calculate
Der Command iteriert ueber alle aktiven Profile (nicht blocked, nicht sanitized, nicht archived, tracking enabled) und berechnet den Score fuer den aktuellen Tag.
Explore-Integration: Score-Fallback-Logik
Die Explore-Seiten (/explore, /explore/browse) zeigen den Postbox Score auf allen Profilkarten an. Da scores:calculate taeglich um 05:00 UTC laeuft, koennen zwischen Mitternacht und 05:00 UTC keine Scores fuer den Vortag existieren.
Fallback-Mechanismus
Statt strikt date = yesterday zu joinen, nutzt die Query den neuesten verfuegbaren Score innerhalb eines 7-Tage-Fensters:
$yesterday = now()->subDay()->toDateString();
$scoreFloor = now()->subDays(7)->toDateString();
// Schritt 1: Neuestes Score-Datum pro Profil finden
$latestScoreDates = DB::table('social_profile_scores')
->select('social_profile_id', DB::raw('MAX(date) as max_date'))
->where('date', '<=', $yesterday)
->where('date', '>=', $scoreFloor)
->groupBy('social_profile_id');
// Schritt 2: Score fuer dieses Datum joinen
->leftJoinSub($latestScoreDates, 'latest_score_dates', ...)
->leftJoin('social_profile_scores', fn ($join) =>
$join->on('social_profile_scores.date', '=', 'latest_score_dates.max_date')
)
Verhalten nach Tageszeit
| Zeitpunkt (UTC) | Abgefragtes Datum | Ergebnis |
|---|---|---|
| 14.02. 02:00 | Score bis max 13.02. | Zeigt 12.02. Score (13.02. existiert noch nicht) |
| 14.02. 05:15 | Score bis max 13.02. | Zeigt 13.02. Score (gerade berechnet) |
| 14.02. 23:00 | Score bis max 13.02. | Zeigt 13.02. Score |
| 15.02. 00:01 | Score bis max 14.02. | Zeigt 13.02. Score (14.02. noch nicht berechnet) |
Score-Anzeige auf Profilkarten
Zwei Varianten je nach Datenverfuegbarkeit:
- Postbox Score vorhanden → Blauer Gradient-Bar (
from-cyan-500 to-blue-600) + Zahl - Kein Postbox Score, aber Trending Score > 0 → Oranger Gradient-Bar (
from-orange-500 to-red-500) als Fallback
Location: app/Livewire/Explore/Index.php, app/Livewire/Explore/ProfileGrid.php, resources/views/components/explore/profile-card.blade.php
UI-Integration
Score Chart (Watchers\ScoreChart)
Zeigt den Score-Verlauf der letzten 30 Tage auf der Watcher-Detailseite. Nutzt ApexCharts (CDN) mit Alpine.js-Integration (siehe Technical Debt fuer Hintergrund).
Features:
- Zwei Serien: Durchgezogene Linie (echte Datenpunkte) + gestrichelte Linie (interpolierte Luecken)
- Gap-Interpolation: Fehlende Tage werden linear zwischen den Nachbarpunkten interpoliert. Boundary-Points ueberlappen, damit die gestrichelte Linie nahtlos anschliesst.
- Custom Tooltips: Datum (deutsch formatiert), Score-Wert, Delta zum Vortag mit Farb-Indikator
- Feste Y-Achse: 0-100 (Score-Bereich)
- Score-Box: Aktuelle Score-Zahl links, Delta vs. gestern, Status-Badge (Vorlaeufig/Stabil)
- "Aehnliche anzeigen"-Button: Link zu
/explore/browse?sort=score&minScore=X&maxScore=Y - Erklaerungsmodal: Aufschluesselung der Score-Gewichtung
Location: app/Livewire/Watchers/ScoreChart.php, resources/views/livewire/watchers/score-chart.blade.php
Video Performance Summary
Auf der Watcher-Detailseite wird eine Video Performance Summary angezeigt:
- Durchschnitts-Score
- Anzahl gescorter Videos
- Top 3 und Flop 3 Videos
// Auf Basis der juengsten YouTubeVideoScore-Eintraege
$scores = YouTubeVideoScore::where('social_profile_id', $profileId)
->where('date', $latestDate)
->whereNotNull('score')
->orderByDesc('score')
->get();
Location: app/Livewire/Watchers/Show.php (Methode buildVideoPerformanceSummary())
Video Score Chart (Watchers\VideoScoreChart)
Per-Video Score-Verlauf (30 Tage) auf der Watcher-Detailseite. Adaptiert vom ScoreChart-Pattern, mit Video-spezifischer Score-Erklaerung (View Performance 40%, Engagement 25%, Growth 20%, Freshness 15%).
Features:
- Gleiche Zwei-Serien-Logik (solid + dashed) wie ScoreChart
- Gap-Interpolation fuer fehlende Tage
- Wird nur gerendert wenn >= 2 Score-Datenpunkte vorhanden (
chartEligible) - Kein "Aehnliche anzeigen"-Button, kein Followers-Check
- Eingebettet unterhalb jedes Video-Cards
Location: app/Livewire/Watchers/VideoScoreChart.php, resources/views/livewire/watchers/video-score-chart.blade.php
Video Trends Page (/video-trends)
Eigene Top-Level-Seite mit Video-Leaderboards. Vier Sektionen:
- KPI-Boxen (Videos mit Score, Ø Score, Top Performer, Im Trend)
- Top/Flop Performer (2 Spalten, Score-sortiert)
- Groesstes Wachstum / Rueckgang (2 Spalten, View-Growth-sortiert)
- Aktuell im Trend (2-Spalten-Grid, Trending-Score-sortiert)
Filter: Duration, Kategorie, Favoriten, Zeitraum (7d/30d). Caching: 1h TTL (global), Favorites in-memory.
Location: app/Livewire/VideoTrends/Index.php, resources/views/livewire/video-trends/index.blade.php
Watcher Show: Video-Sektion fuer alle User
Die Video-Sektion auf der Watcher-Detailseite (YouTube Videos) ist fuer alle authentifizierten User sichtbar. Admin-only Bereiche:
- Score-Breakdown-Bars (Views/Engagement/Wachstum/Aktualitaet)
- "Weitere Details" (JSON-Payloads, Metadaten)
Nicht-Admin-User sehen: Video-Thumbnail, Titel, Score-Badge, Performance Score, und den VideoScoreChart (wenn eligible).
Location: resources/views/livewire/watchers/show.blade.php