File size: 5,056 Bytes
78185c4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | <?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
declare(strict_types=1);
namespace Piwik\Plugins\BotTracking\DataTable;
use InvalidArgumentException;
use Piwik\DataTable;
use Piwik\DataTable\DataTableInterface;
use Piwik\Plugins\BotTracking\Columns\Metrics\DiscrepancyScore;
use Piwik\Plugins\BotTracking\Metrics;
/**
* Materialises the bounded 0β100 Discrepancy Score as a real column on the merged favoured-pages
* table. Invoked during archiving by {@see \Piwik\Plugins\BotTracking\RecordBuilders\AIChatbotFavouredPages}
* β for the day record and, re-run, for each higher period's recomputed union.
*
* A stored column rather than a ProcessedMetric: the score is table-relative (the `volume` term is
* anchored to the busiest page), so a ProcessedMetric recomputed at display time would see a shrunken
* anchor once ExcludeLowPopulation has deleted rows (and be absent while that filter itself runs).
*
* @see DiscrepancyScore for the column metadata (label, documentation, formatting).
*/
class FavouredPagesScorer
{
/** @var DiscrepancyScore::VARIANT_HUMAN_FAVOURED|DiscrepancyScore::VARIANT_AI_FAVOURED */
private $variant;
public function __construct(string $variant)
{
if (
$variant !== DiscrepancyScore::VARIANT_HUMAN_FAVOURED
&& $variant !== DiscrepancyScore::VARIANT_AI_FAVOURED
) {
throw new InvalidArgumentException('Unknown DiscrepancyScore variant: ' . $variant);
}
$this->variant = $variant;
}
/**
* Adds the `discrepancy_score` column to every row of the table (recursing into
* `DataTable\Map` children so each period self-calibrates), and marks the column 'skip' so it is
* never summed into a summary row.
*
* @param DataTable|DataTable\Map $table
*/
public function addScores(DataTableInterface $table): void
{
if ($table instanceof DataTable\Map) {
foreach ($table->getDataTables() as $childTable) {
$this->addScores($childTable);
}
return;
}
if (!$table instanceof DataTable) {
return;
}
$strongColumn = $this->variant === DiscrepancyScore::VARIANT_HUMAN_FAVOURED
? Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS
: Metrics::COLUMN_AI_CHATBOT_REQUESTS;
// Volume anchor = the busiest page's strong-side value. Exclude the "Others" summary row,
// whose aggregate tail would otherwise hijack the anchor and deflate every real score.
$maxStrong = 0;
foreach ($table->getRows() as $row) {
if ($row->isSummaryRow()) {
continue;
}
$value = (int) $row->getColumn($strongColumn);
if ($value > $maxStrong) {
$maxStrong = $value;
}
}
foreach ($table->getRows() as $row) {
// Leave the "Others" summary row unscored β a score over an aggregate of URLs is meaningless.
if ($row->isSummaryRow()) {
continue;
}
$human = (int) $row->getColumn(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS);
$ai = (int) $row->getColumn(Metrics::COLUMN_AI_CHATBOT_REQUESTS);
if ($this->variant === DiscrepancyScore::VARIANT_HUMAN_FAVOURED) {
$strong = $human;
$weak = $ai;
} else {
$strong = $ai;
$weak = $human;
}
$row->setColumn(Metrics::COLUMN_DISCREPANCY_SCORE, self::score($strong, $weak, $maxStrong));
}
// Mark the score 'skip' so the Truncate filter leaves it out of the "Others" summary row it
// builds while archiving β a score over an aggregate of URLs is meaningless. (Not serialised
// with the blob, so readers re-apply 'skip' for the report totals row; see API.)
$ops = $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME);
if (!is_array($ops)) {
$ops = [];
}
$ops[Metrics::COLUMN_DISCREPANCY_SCORE] = 'skip';
$table->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $ops);
}
/**
* The bounded 0β100 score for one page:
* lean = max(0, (strong β weak) / (strong + weak)) // 0 = balanced or opposite, 1 = entirely one-sided
* volume = log10(strong + 1) / log10(maxStrong + 1) // 0..1, anchored to the busiest page
* score = round(100 Γ lean Γ volume, 1)
*/
public static function score(int $strong, int $weak, int $maxStrong): float
{
$total = $strong + $weak;
if ($total <= 0) {
return 0.0;
}
$lean = max(0, ($strong - $weak) / $total);
$anchor = log10($maxStrong + 1);
$volume = $anchor > 0 ? log10($strong + 1) / $anchor : 0.0;
return round(100 * $lean * $volume, 1);
}
}
|