Beide Werte müssen gemessen sein, bevor die erste Maßnahme aus Block 1 live geht — ohne Vergleichspunkt wäre jede spätere Bewertung eine Vermutung. press-releases:view-distribution gibt die Verteilung der Aufrufzahlen je Portal aus: Median, Quartile, P10/P90 und eine Häufigkeitsverteilung. Ein Mittelwert allein trägt hier nicht — eine Handvoll Ausreißer zieht ihn nach oben, während der typische Absender darunter liegt. Genau davor soll E2 schützen, deshalb wird die Schiefe (Mittelwert gegen Median) ausdrücklich mit ausgewiesen. Altbestand und seit dem Relaunch eingegangene Meldungen stehen getrennt: Ihre Zähler haben unterschiedliche Vorgeschichten, und eine Einordnung, die beide vermischt, vergleicht Äpfel mit Birnen. Mit 410 entfernte Meldungen zählen nicht mit, eine Meldung auf beiden Portalen bei beiden. press-releases:rejection-rate gibt die Ablehnungsquote je Einreichungsquelle aus. Gezählt wird je EINREICHUNG, nicht je Meldung: Ein Übergang nach `review` eröffnet eine Einreichung, die nächste Entscheidung schließt sie. Eine überarbeitete und erneut eingereichte Meldung ist damit ein zweiter Vorgang mit eigenem Ausgang — und genau das ist die Größe, die die Infoseite (1d) später verändern soll. Gruppiert wird nach dem Einreichenden, nicht nach dem Entscheider; letzterer ist immer die KI-Prüfung und als Gruppierung wertlos. Jeder API-Client bekommt eine eigene Zeile, Einreichungen aus dem Kundenbereich fallen zu "direkt" zusammen. Noch offene Prüfungen zählen nicht in den Nenner, sonst sänke die Quote allein dadurch, dass der Zeitraum bis heute reicht. Entscheidungen zu Einreichungen von vor dem Zeitraum werden gemeldet statt verschluckt — ein zu kurz gewählter Zeitraum sähe sonst wie eine niedrige Quote aus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
105 lines
3.7 KiB
PHP
105 lines
3.7 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Services\PressRelease\ViewDistributionReport;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Carbon;
|
||
|
||
/**
|
||
* Basiswert der Aufrufzahlen (Roadmap 0.1).
|
||
*
|
||
* Muss gemessen sein, BEVOR die erste Maßnahme aus Block 1 live geht — ohne
|
||
* Vergleichspunkt wird jede spätere Bewertung eine Vermutung. Die Verteilung
|
||
* entscheidet zugleich E2: ob die Absenderansicht Rohzahlen zeigt oder eine
|
||
* Einordnung gegen den Portalschnitt.
|
||
*/
|
||
class ReportPressReleaseViews extends Command
|
||
{
|
||
protected $signature = 'press-releases:view-distribution
|
||
{--months=3 : Zeitraum in Monaten rückwärts}
|
||
{--from= : Startdatum, überschreibt --months}
|
||
{--to= : Enddatum (Standard: heute)}
|
||
{--json : Ausgabe als JSON statt als Tabelle}';
|
||
|
||
protected $description = 'Basiswert: Verteilung der Aufrufzahlen je Portal (Median, Quartile, Häufigkeiten).';
|
||
|
||
public function handle(ViewDistributionReport $report): int
|
||
{
|
||
$to = $this->option('to') !== null ? Carbon::parse((string) $this->option('to')) : Carbon::now();
|
||
$from = $this->option('from') !== null
|
||
? Carbon::parse((string) $this->option('from'))
|
||
: $to->copy()->subMonths(max(1, (int) $this->option('months')));
|
||
|
||
$data = $report->build($from, $to);
|
||
|
||
if ($this->option('json')) {
|
||
$this->line((string) json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$this->info("Aufrufzahlen je Portal — veröffentlicht {$data['from']} bis {$data['to']}");
|
||
|
||
foreach ($data['portals'] as $portal) {
|
||
$this->newLine();
|
||
$this->line("<comment>{$portal['label']}</comment>");
|
||
|
||
$rows = [];
|
||
|
||
foreach ($portal['segments'] as $segment) {
|
||
$s = $segment['stats'];
|
||
|
||
if ($s['anzahl'] === 0) {
|
||
$rows[] = [$segment['segment'], 0, '–', '–', '–', '–', '–', '–', '–'];
|
||
|
||
continue;
|
||
}
|
||
|
||
$rows[] = [
|
||
$segment['segment'],
|
||
$s['anzahl'],
|
||
$s['min'],
|
||
$s['p10'],
|
||
$s['p25'],
|
||
$s['median'],
|
||
$s['p75'],
|
||
$s['p90'],
|
||
$s['max'],
|
||
];
|
||
}
|
||
|
||
$this->table(['Bestand', 'PMs', 'Min', 'P10', 'P25', 'Median', 'P75', 'P90', 'Max'], $rows);
|
||
|
||
$gesamt = $portal['segments'][0]['stats'];
|
||
|
||
if ($gesamt['anzahl'] === 0) {
|
||
continue;
|
||
}
|
||
|
||
$this->line(sprintf(
|
||
' Mittelwert %s gegenüber Median %d%s',
|
||
$gesamt['mittelwert'],
|
||
$gesamt['median'],
|
||
// Die Schiefe ist die eigentliche Antwort auf E2: Zieht eine
|
||
// Handvoll Ausreißer den Schnitt nach oben, liegt der typische
|
||
// Absender darunter und eine Rohzahl liest sich für ihn als
|
||
// Misserfolg.
|
||
$gesamt['mittelwert'] > $gesamt['median'] * 1.25
|
||
? ' ← rechtsschief, Rohzahlen wirken für die Mehrheit enttäuschend'
|
||
: '',
|
||
));
|
||
|
||
$verteilung = [];
|
||
|
||
foreach ($gesamt['verteilung'] as $klasse => $anzahl) {
|
||
$anteil = $gesamt['anzahl'] > 0 ? round($anzahl / $gesamt['anzahl'] * 100) : 0;
|
||
$verteilung[] = [$klasse.' Aufrufe', $anzahl, $anteil.' %'];
|
||
}
|
||
|
||
$this->table(['Klasse', 'PMs', 'Anteil'], $verteilung);
|
||
}
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
}
|