本文目录导读:

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