Zum Hauptinhalt springen

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​

KomponenteGewichtBeschreibung
Growth40%Relatives Follower-Wachstum vs. Tier-Erwartung
Momentum30%Beschleunigung des Wachstums (letzte 7d vs. vorherige 7d)
Consistency20%Datenabdeckung der letzten 14 Tage
Engagement10%Plattformspezifische Aktivitaets-Metriken

Tier-Definitionen​

TierFollower-BereichErwartete taegliche Growth-Rate
micro0 - 10.0000,5%
small10.000 - 100.0000,3%
medium100.000 - 500.0000,2%
large500.000 - 1.000.0000,1%
mega1.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​

StatusDatenpunkteBeschreibung
no_data0Kein Datenpunkt innerhalb von 5 Tagen
pending< 7Zu wenig Daten fuer Berechnung
preliminary7 - 9Vorlaeufiger Score
stable10+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:

PlattformMetrikErwartung (Max Score)Beschreibung
YouTubevideo_count Growth0.15/Tag (≈ 1 Video/Woche)Upload-Frequenz basierend auf video_count Delta
Instagrampost_count Growth0.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:

TierMultiplierEffekt
micro0.85-15%
small0.95-5%
medium1.0kein Abzug
large1.0kein Abzug
mega1.0kein Abzug

Konfigurierbar via config('postbox.scoring.tier_relevance_multiplier').

Follower-Filter (V2)​

Zwei separate Schwellenwerte:

ConfigDefaultBedeutung
postbox.scoring.min_followers_for_scoring100Minimum fuer Score-Berechnung
postbox.scoring.min_followers_for_rankings500Minimum 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​

KomponenteGewichtBeschreibung
View Performance40%Views relativ zu Tier-Erwartung
Engagement25%Like-to-View + Comment-to-View Ratios
Growth Trajectory20%View-Wachstums-Beschleunigung
Freshness15%Recency-Bonus fuer neuere Videos

View Performance (40%)​

Vergleicht durchschnittliche taegliche Views mit Tier-Erwartungen:

TierErwartete Views/Tag
micro500
small2.000
medium10.000
large50.000
mega200.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:

TierErwartete Like-Rate
micro5%
small4%
medium3,5%
large3%
mega2,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​

StatusDatenpunkteBeschreibung
no_data0Keine Metriken
pending< 3Zu wenig Daten
preliminary3 - 6Vorlaeufiger Score
stable7+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 (auf followers_count, flaggt die ganze Tageszeile).
  • YouTube-Videos: metrics:detect-video-anomalies (auf view_count pro 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 DatumErgebnis
14.02. 02:00Score bis max 13.02.Zeigt 12.02. Score (13.02. existiert noch nicht)
14.02. 05:15Score bis max 13.02.Zeigt 13.02. Score (gerade berechnet)
14.02. 23:00Score bis max 13.02.Zeigt 13.02. Score
15.02. 00:01Score bis max 14.02.Zeigt 13.02. Score (14.02. noch nicht berechnet)

Score-Anzeige auf Profilkarten​

Zwei Varianten je nach Datenverfuegbarkeit:

  1. Postbox Score vorhanden → Blauer Gradient-Bar (from-cyan-500 to-blue-600) + Zahl
  2. 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

Eigene Top-Level-Seite mit Video-Leaderboards. Vier Sektionen:

  1. KPI-Boxen (Videos mit Score, Ø Score, Top Performer, Im Trend)
  2. Top/Flop Performer (2 Spalten, Score-sortiert)
  3. Groesstes Wachstum / Rueckgang (2 Spalten, View-Growth-sortiert)
  4. 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