本文目录导读:

在 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等成熟库
如果你需要针对特定场景(如:实时计算、数据库查询结果计算)的详细代码,可以告诉我具体需求,我可以进一步优化实现。