PHP项目百分位与中位数

wen PHP项目 9

本文目录导读:

PHP项目百分位与中位数

  1. 基础实现(纯 PHP)
  2. 实际项目场景示例
  3. 大数据量优化
  4. 推荐的类库
  5. 常见问题与注意事项

在 PHP 项目中处理百分位中位数(中位数本质上是第 50 百分位)时,通常需要先对数据进行排序,然后根据数据量计算位置。

下面我会从基础实现、常见场景到性能优化,给出完整的 PHP 解决方案。

基础实现(纯 PHP)

中位数

中位数是指将一组数据排序后位于中间位置的数:

  • 数据个数为奇数:取中间那个数
  • 数据个数为偶数:取中间两个数的平均值
function median(array $data): float {
    if (empty($data)) {
        throw new InvalidArgumentException('数据不能为空');
    }
    sort($data);
    $count = count($data);
    $middle = floor($count / 2);
    if ($count % 2 === 0) {
        // 偶数个数:取中间两个的平均值
        return ($data[$middle - 1] + $data[$middle]) / 2;
    } else {
        // 奇数个数:取中间那个数
        return $data[$middle];
    }
}
// 示例
$salaries = [3500, 4800, 6200, 8900, 11000, 15000];
echo median($salaries); // 输出: 7550 ( (6200+8900)/2 )

百分位(Percentile)

百分位表示在排序后的数据中,有多少百分比的数据小于或等于该值。

常用算法:最近秩法(Nearest Rank)线性插值法(Linear Interpolation)

最近秩法

function percentileNearestRank(array $data, float $percentile): float {
    if (empty($data) || $percentile < 0 || $percentile > 100) {
        throw new InvalidArgumentException('参数不合法');
    }
    sort($data);
    $count = count($data);
    // 计算秩(位置),从1开始
    $rank = ceil($percentile / 100 * $count);
    // 转为数组索引(从0开始)
    return $data[$rank - 1];
}
// 示例:计算第90百分位
$responseTimes = [12, 15, 23, 29, 35, 42, 58, 67, 89, 120];
echo percentileNearestRank($responseTimes, 90); // 输出: 120(第90%的数据小于等于120ms)

线性插值法(更精确,Excel 也使用此方法)

function percentileLinearInterp(array $data, float $percentile): float {
    if (empty($data) || $percentile < 0 || $percentile > 100) {
        throw new InvalidArgumentException('参数不合法');
    }
    sort($data);
    $count = count($data);
    // 计算位置索引(从1开始的实数)
    $position = 1 + ($percentile / 100) * ($count - 1);
    $index = floor($position) - 1; // 转为从0开始的整数索引
    $fraction = $position - floor($position);
    // 如果正好落在边界上
    if ($index < 0) return $data[0];
    if ($index >= $count - 1) return $data[$count - 1];
    // 线性插值
    return $data[$index] + $fraction * ($data[$index + 1] - $data[$index]);
}
// 示例:计算中位数(第50百分位)
$data = [1, 2, 3, 4, 5];
echo percentileLinearInterp($data, 50); // 输出: 3 (与median一致)

实际项目场景示例

API 响应时间统计

class ResponseTimeAnalyzer {
    private array $times = [];
    public function addTime(float $milliseconds): void {
        $this->times[] = $milliseconds;
    }
    public function analyze(): array {
        if (empty($this->times)) {
            return ['error' => '无数据'];
        }
        sort($this->times);
        return [
            'count'     => count($this->times),
            'min'       => $this->times[0],
            'max'       => $this->times[count($this->times) - 1],
            'median'    => $this->calcMedian(),
            'p50'       => $this->calcPercentile(50),
            'p90'       => $this->calcPercentile(90),
            'p95'       => $this->calcPercentile(95),
            'p99'       => $this->calcPercentile(99),
            'average'   => array_sum($this->times) / count($this->times),
        ];
    }
    private function calcMedian(): float {
        $count = count($this->times);
        $middle = floor($count / 2);
        return ($count % 2 === 0) 
            ? ($this->times[$middle - 1] + $this->times[$middle]) / 2
            : $this->times[$middle];
    }
    private function calcPercentile(float $percentile): float {
        $count = count($this->times);
        $position = 1 + ($percentile / 100) * ($count - 1);
        $index = floor($position) - 1;
        $frac = $position - floor($position);
        if ($index < 0) return $this->times[0];
        if ($index >= $count - 1) return $this->times[$count - 1];
        return $this->times[$index] + $frac * ($this->times[$index + 1] - $this->times[$index]);
    }
}
// 使用
$analyzer = new ResponseTimeAnalyzer();
foreach ($responseLog as $entry) {
    $analyzer->addTime($entry['duration_ms']);
}
$stats = $analyzer->analyze();
// $stats['p99'] 表示99%的请求在此时间内完成

考试成绩排名

function gradeDistribution(array $scores): array {
    sort($scores);
    $total = count($scores);
    // 计算各百分位的分数
    return [
        'highest'   => max($scores),
        'quartiles' => [
            'Q1' => percentileNearestRank($scores, 25), // 第25百分位
            'Q2' => percentileNearestRank($scores, 50), // 中位数
            'Q3' => percentileNearestRank($scores, 75), // 第75百分位
        ],
        'percentiles' => [
            'p10' => percentileNearestRank($scores, 10),
            'p90' => percentileNearestRank($scores, 90),
        ],
        'average'   => array_sum($scores) / $total,
    ];
}

大数据量优化

当数据量很大(几十万以上)时,每次排序会非常慢。

只维护所需百分位(不使用完整排序)

// 使用 SplMinHeap 和 SplMaxHeap 维护 top N
class StreamingPercentile {
    private SplMinHeap $lowerHalf; // 存放较小的一半,用最大堆模拟(取最大值)
    private SplMaxHeap $upperHalf; // 存放较大的一半,用最小堆模拟(取最小值)
    private int $count = 0;
    public function __construct() {
        // PHP原生SplMinHeap和SplMaxHeap已经帮我们处理了堆排序
        // 但我们用反向思路:
        // 要获取中位数,维护两个堆,保证lowerHalf大小 >= upperHalf
        // SplMaxHeap可以取出lowerHalf的最大值
        // SplMinHeap可以取出upperHalf的最小值
        // 这里用SplMaxHeap模拟lower(取最大值)
        // 用SplMinHeap模拟upper(取最小值)
        $this->lowerHalf = new SplMaxHeap(); // 存较小的数,取最大
        $this->upperHalf = new SplMinHeap(); // 存较大的数,取最小
    }
    public function add(float $value): void {
        if ($this->count % 2 === 0) {
            // 偶数时插入lower
            $this->lowerHalf->insert($value);
        } else {
            $this->upperHalf->insert($value);
        }
        $this->count++;
        // 平衡两个堆,保证lower中所有元素 <= upper中所有元素
        if ($this->lowerHalf->count() > 0 && $this->upperHalf->count() > 0) {
            if ($this->lowerHalf->top() > $this->upperHalf->top()) {
                $maxLower = $this->lowerHalf->extract();
                $minUpper = $this->upperHalf->extract();
                $this->lowerHalf->insert($minUpper);
                $this->upperHalf->insert($maxLower);
            }
        }
    }
    public function getMedian(): float {
        if ($this->count === 0) {
            throw new RuntimeException('无数据');
        }
        if ($this->count % 2 === 1) {
            return $this->lowerHalf->top();
        } else {
            return ($this->lowerHalf->top() + $this->upperHalf->top()) / 2;
        }
    }
    // 获取任意百分位(需要完整数据,但可以只存top N个)
    // 这种流式方法只适合中位数,其他百分位需要更复杂的结构
}

抽样 + 估算(适合极大数据)

function approximatePercentile(array $data, float $percentile, int $sampleSize = 10000): float {
    if (count($data) <= $sampleSize) {
        return percentileLinearInterp($data, $percentile);
    }
    // 随机抽样
    $sample = [];
    $keys = array_rand($data, $sampleSize);
    foreach ($keys as $key) {
        $sample[] = $data[$key];
    }
    return percentileLinearInterp($sample, $percentile);
}

推荐的类库

如果需要更完善的功能,可以用以下 Composer 包:

composer require tecnickcom/tc-lib-statistics
# 或者
composer require markrogoyski/math-php

示例:

use MathPHP\Statistics\Average;
use MathPHP\Statistics\Descriptive;
$data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
echo Average::median($data);                 // 5.5
echo Descriptive::percentile($data, 90);      // 9.1(线性插值)

常见问题与注意事项

问题 解决方案
数据为空 检查并抛出异常或返回默认值
数据包含null 过滤掉 array_filter($data, 'is_numeric')
百分位计算标准不统一 明确使用最近秩法还是线性插值法(Excel是线性插值)
排序影响原数组 sort() 会改变原数组,先复制 $sorted = $data; sort($sorted)
大数据量排序慢 使用流式算法(只适合中位数)或抽样估算
浮点数精度问题 使用 round($result, 2) 限制小数位
  • 中小数据量:直接用 sort() + 线性插值法,代码简单准确
  • 大数据量:使用堆维护中位数(流式),或者抽样估算百分位
  • 生产项目:建议封装成 Statistics 类复用,或者直接用 math-php 等成熟库

如果你需要针对特定场景(如:实时计算、数据库查询结果计算)的详细代码,可以告诉我具体需求,我可以进一步优化实现。

抱歉,评论功能暂时关闭!