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.
Ausnahme — Watched/Favorited-Bypass: Profile, die von mindestens einem User beobachtet (Watcher) oder favorisiert werden, umgehen beide Schwellenwerte. Sie werden immer gescored und erscheinen immer in Explore-Rankings, unabhaengig von der Follower-Zahl. Grund: Ein realer User hat aktives Interesse am Profil bekundet. Gleiches gilt fuer die Low-Priority-Klassifizierung — watched/favorited/PRO Profile werden nie als "low priority" eingestuft und bleiben in der regulaeren Scrape-Rotation.
Location: SocialProfile::isWatchedOrFavorited(), Bypass in CalculateProfileScores, CacheExploreSections, Explore\Index, RunNightlyPipeline.
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
Zählweise-Wechsel: Wachstum ist zeitweise nicht messbar
YouTube zählt seit dem 24.08.2026 einen Aufruf ab dem ersten Frame (Details: YouTube-Pipeline). Die Historie wurde dabei nicht neu berechnet — der kumulative Bestand springt also nicht, wohl aber die Tages-Rate.
Für das Scoring hat das eine konkrete Folge: Die Wachstums-Trajektorie ist über den Stichtag hinweg nicht berechenbar. Sie teilt die Messreihe eines Videos in eine jüngere und eine ältere Hälfte und bildet den Quotienten der beiden Wachstumsraten. Liegen die Hälften auf verschiedenen Seiten des Stichtags, misst dieser Quotient den Methodenwechsel und nicht das Video: Dieselbe Zuschauerschaft erzeugt danach mehr gezählte Aufrufe, was sich exakt wie Beschleunigung liest.
calculateGrowthTrajectory() liefert deshalb den Neutralwert 0,5, solange das
30-Tage-Rückschaufenster den Stichtag enthält — denselben Wert wie bei zu dünner Datenlage.
Die ehrliche Aussage lautet in beiden Fällen „keine Aussage möglich", nicht „mittelmässig".
Der Zustand endet von selbst, sobald das Fenster vollständig hinter dem Stichtag liegt.
Ohne diesen Ausstieg sähen ab dem Stichtag alle Videos gleichzeitig beschleunigt aus, ohne dass eines davon besser liefe.
Was NICHT betroffen ist
Der 1,5-fache Aufschlag auf die Aufruf-Erwartung bei Videos unter 60 Sekunden
(SHORT_DURATION_VIEW_MULTIPLIER) bleibt unverändert. Er kompensiert wiederholtes
Abspielen — die Shorts-Oberfläche läuft in Schleife, und die API zählt dort ausdrücklich
jedes „start to play or replay". Für Langformat gibt es kein vergleichbares automatisches
Wiederholen. Diese Asymmetrie besteht nach dem 24.08.2026 unverändert fort; der Aufschlag
hatte nie mit der Mindestwiedergabezeit zu tun.
Offene Nacharbeit
Die absoluten Erwartungswerte (TIER_VIEW_EXPECTATIONS, „10 Aufrufe pro Follower") sind
ab dem Stichtag zu locker kalibriert, weil dieselbe Zuschauerschaft mehr gezählte Aufrufe
erzeugt. Der Umfang ist unbekannt und wird gemessen, nicht geschätzt — vier Wochen nach
dem Stichtag über den gesamten Bestand.
Schedule
Scores werden täglich im Rahmen der Nightly Pipeline (pipeline:run, 03:00 UTC, Batch 1A
CalculateProfileMetricsBatch) berechnet. Manuell/für Backfills:
php artisan scores:calculate # heute
php artisan scores:calculate --from=2026-06-01 --to=2026-06-16 --force # Range, überschreibt
Der Lauf iteriert über alle aktiven Profile (nicht blocked, nicht sanitized, nicht archived, tracking enabled) und berechnet den Score für den jeweiligen Tag.
Daten-Qualität: Anomalie-Ausschluss (Glitch-Tage)
Der Collector/Sync liefert gelegentlich an einzelnen Tagen Müll-Werte (z. B. Follower stürzen
einen Tag von 5 Mio. auf 1.912 und sind am Folgetag wieder real = „V-Dip"). Solche Tage werden erkannt
und additiv markiert (anomaly_state = pending/confirmed), ohne die Rohwerte zu verändern:
- Profile:
metrics:detect-anomalies(auffollowers_count, flaggt die ganze Tageszeile). - YouTube-Videos:
metrics:detect-video-anomalies(aufview_countpro Video).
Erkennungs-Logik (geteilte DailyMetricAnomalyDetector-State-Machine): Ein Tag, der um mehr als
tolerance_pct (Default 25 %) von der letzten guten Baseline abweicht, gilt als Glitch-Verdacht. Er
wird bestätigt, sobald ein Folgewert wieder bis nahe an die Baseline zurückkehrt — die Prüfung
ist richtungsabhängig: Ein Abwärts-Dip ist erholt, wenn der Wert wieder >= Baseline·(1−Toleranz)
erreicht, wobei ein Überschießen über die Baseline (typisch bei wachsenden Metriken wie
view_count) weiterhin als Recovery zählt (Aufwärts-Spikes spiegelbildlich). Bis zu max_glitch_days
(Default 5, ANOMALY_MAX_GLITCH_DAYS) aufeinanderfolgende, gleichsinnig abweichende Soft-Dips
werden als ein Glitch-Run gemeinsam bestätigt (z. B. ein mehrtägiger Sync-Ausfall). Collapse-Ausnahme:
Bricht ein Wert auf ≤ collapse_pct (Default 10 %, ANOMALY_COLLAPSE_PCT) der letzten guten Baseline
ein (Einbruch Richtung 0, z. B. 1,66 Mio. → 4), gilt das als eindeutiger Scrape-Ausfall und wird
unabhängig von max_glitch_days erkannt (beliebig lange Runs); ein solcher Garbage-Wert wird zudem
nie als Vergleichs-Baseline übernommen — sonst maskiert ein langer Ausfall alle Folgetage. Eine
gehaltene, echte Niveau-Änderung kehrt nicht zurück und wird daher nicht geflaggt.
min_base/min_abs_change verhindern Fehlalarme bei kleinen Accounts. Der jüngste, noch nicht aufgelöste Tag bleibt pending
(1-Tag-Latenz), bis der Folgetag ihn auflöst.
Beide laufen täglich um 02:15 UTC — also VOR der Score-Berechnung (Pipeline 03:00), damit ein
Glitch des Vortags bereits geflaggt ist, bevor der Score rechnet. Der Postbox Score und der
Video Performance Score schließen geflaggte Tage konsequent aus (withoutAnomalies()-Scope an
allen Score-Datenquellen) — ein Glitch-Tag wird nie als jüngster Datenpunkt gewählt und verfälscht
weder Wachstum noch Score. Dieselbe Bereinigung gilt für die Leaderboards und die Video-Charts. Die Aggregierte
Video-Performance schreibt zusätzlich pro Video den letzten bekannten kumulativen Wert über
nicht-gesyncte/Glitch-Tage fort (Carry-Forward) und summiert dann — so bleibt die Gesamtlinie
glatt und monoton, statt an Tagen mit wenig Sync-Abdeckung auf einen falschen 0-Wert einzubrechen
(ein Per-Video-Collapse-Guard überspringt zudem noch ungeflaggte, physikalisch unmögliche Einbrüche).
Rückwirkend (Backfill): Historische Glitches werden erst nach einem Backfill geflaggt; danach müssen die betroffenen Scores neu gerechnet werden:
php artisan metrics:detect-anomalies --all # Profile, ganze Historie (chunked, 128-MB-safe)
php artisan metrics:detect-video-anomalies --all # Videos, ganze Historie
php artisan scores:calculate --from=<startdatum> --to=<heute> --force # Scores ohne Glitches neu
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