一、写在前面
业务中遇到箱型图,本来打算偷个懒,直接用同事封装好的工具类的,结果到了测试阶段,测试提出上/下 四分位计算方法不止一种,希望我们和之前的项目保持一致!在开发中最怕听到的就是保持一致了!结果,我所用的工具类和之前的项目计算方式还真的不一样。项目已经到了测试阶段,改工具类是最省事的办法了。
改着改着,突然发现目前所用工具类有点麻烦,于是决定改造下。
最大/小值、中位数,直接计算就行,没啥难度,主要是上四分位数和下四分位数,查了一圈(https://baike.baidu.com/item/%E5%9B%9B%E5%88%86%E4%BD%8D%E6%95%B0/5040599?fr=aladdin)概念,信心满满以为这下子能够完全看懂大佬封装的代码了(从而改造成工具类),结果卡在了“下标不为整数即数据个数不是4的整数”相关处理上了,最后还是去问大佬,才半懂了。记录下,以后要么慢慢就看懂了,要么直接使用 。
和测试小姐姐验证上/下四分位计算是否正确,我只能对着代码计算,结果小姐姐说距离哪个数最近就乘3/4(0.75),哇!精髓了,对于不怎么理解算法的我来说简直了!
二、代码
package com.wisedu.hawkeye.domain.util;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 统计相关数据计算
*
* 最大值、最小值、平均值、中位数、上四分位、下四分位
*/
public class DataStatisticsCalculate {
/**
* 保留小数
*/
private static final int DEFAULT_SCALE = 2;
/**
* 数据最小计算长度:计算 上/下 四分位数 时,数据的长度<= 3
*/
private static final int QUARTILES_LIMIT_NUM = 3;
/**
* 上四分位
*/
private static final BigDecimal NUMBER_75 = BigDecimal.valueOf(0.75);
/**
* 下四分位
*/
private static final BigDecimal NUMBER_25 = BigDecimal.valueOf(0.25);
private List<BigDecimal> dataList;
public DataStatisticsCalculate(List<BigDecimal> dataList) {
// 排序
this.dataList = sort(dataList);
}
/**
* @return 最大值
*/
public BigDecimal getMax() {
if (ObjectUtils.isEmpty(dataList)) {
return BigDecimal.ZERO;
}
return dataList.get(dataList.size() - 1);
}
/**
* @return 最小值
*/
public BigDecimal getMin() {
if (ObjectUtils.isEmpty(dataList)) {
return BigDecimal.ZERO;
}
return dataList.get(0);
}
/**
*
* @return 平均数
*/
public BigDecimal getAverage() {
return twoNumDivide(sum(), BigDecimal.valueOf(dataList.size()));
}
/**
* 数据从小到大排列后,第25%的数字
* @return 下四分位
*/
public BigDecimal getFourDown(){
if(ObjectUtils.isEmpty(dataList)){
return BigDecimal.ZERO;
}
int len=dataList.size();
if(len<=QUARTILES_LIMIT_NUM){
// 数据小于3条,下四分位数=最小值
return dataList.get(0);
}
return calFourPositionNumber(NUMBER_25);
}
public BigDecimal getFourUp(){
if(ObjectUtils.isEmpty(dataList)){
return BigDecimal.ZERO;
}
int len=dataList.size();
if(len<=QUARTILES_LIMIT_NUM){
// 数据小于3条,上四分位数=最大值
return dataList.get(len-1);
}
return calFourPositionNumber(NUMBER_75);
}
/**
* 计算上/下四分位数对应的值
* @param factor 上四分位/下分位
* @return 对应的值
* 计算公式
* 下:Q1的位置= (n+1) × 0.25
* 上:Q3的位置= (n+1) × 0.75
* 例如:
*
* 数据总量: 7, 15, 36, 39, 40, 41 共6个数字
* 下四分位分(0.25) (6+1)/4=1.75 ,7与15之间,(1.75-1>0.5)更靠近于15,
* Q1 = 0.75*15+0.25*7 = 13
* ====》7*0.25+15*(1-0.25)
* 上四分位分(0.75) (6+1)/4*3= 5.25, 40与41之间,(5.25-5<0.5)更接近40
* Q3 = 0.25*41+0.75*40 = 40.25
* =====>
* 40*0.75+41*(1-0.75)
*/
private BigDecimal calFourPositionNumber(BigDecimal factor) {
int len=dataList.size();
BigDecimal value = BigDecimal.valueOf(len + 1).multiply(factor);
int startIndex = value.setScale(0, BigDecimal.ROUND_DOWN).intValue() - 1;
float pointNumber = value.floatValue() - (startIndex + 1);
if (pointNumber == 0.0f) {
return dataList.get(startIndex).setScale(2, BigDecimal.ROUND_HALF_UP);
}
if (pointNumber <= 0.5f) {
return dataList.get(startIndex).multiply(BigDecimal.valueOf(1 - pointNumber))
.add(dataList.get(startIndex + 1).multiply(BigDecimal.valueOf(pointNumber)))
.setScale(2, BigDecimal.ROUND_HALF_UP);
} else {
return dataList.get(startIndex + 1).multiply(BigDecimal.valueOf(pointNumber))
.add(dataList.get(startIndex).multiply(BigDecimal.valueOf(1 - pointNumber)))
.setScale(2, BigDecimal.ROUND_HALF_UP);
}
}
/**
* 总数
* @return
*/
private BigDecimal sum() {
BigDecimal result = BigDecimal.ZERO;
for (BigDecimal num : dataList) {
result = result.add(num);
}
return result;
}
/**
* @return 中位数
*/
public BigDecimal getMiddle() {
if (ObjectUtils.isEmpty(dataList)) {
return BigDecimal.ZERO;
}
int len = dataList.size();
if (len == 1) {
return dataList.get(0);
}
if (len % 2 == 1) {
// 奇数
return dataList.get(len / 2);
} else {
// 偶数
int endIndex = len / 2;
int startIndex = endIndex - 1;
return calAverage(dataList.get(startIndex), dataList.get(endIndex));
}
}
/**
* @param num1 第一个数
* @param num2 第二个数
* @return 两个数的平均数
*/
private BigDecimal calAverage(BigDecimal num1, BigDecimal num2) {
return twoNumDivide(num1.add(num2), BigDecimal.valueOf(2));
}
/**
* @param num1 被除数
* @param num2 除数
* @return nm1/num2 的结果,四舍五入,并保留小树
*/
private BigDecimal twoNumDivide(BigDecimal num1, BigDecimal num2) {
return num1.divide(num2, DEFAULT_SCALE, RoundingMode.HALF_UP);
}
/**
* @param dataList 数据源
* @return 升序:排序后的数据
*/
private List<BigDecimal> sort(List<BigDecimal> dataList) {
return dataList.stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList());
}
}