網上找了很久SPC八大判異標准的代碼實現,沒找到JAVA的,自己寫了一套,做個備忘。
最終輸出異常數據的坐標集,判異標准K值可編輯。效果如下:

/**
* Xbar判異標准
* Created by GaoYuman on 2021/12/29 15:57
*/
public class XbarAbnormalJudgeEnums {
public enum type {
ONE(1, "一個點,距離中心線大於K個標准差"),
TWO(2, "連續K個點在中心線同一側"),
THREE(3, "連續K個點, 全部遞增或全部遞減"),
FOUR(4, "連續K個點,上下交接"),
FIVE(5, "K+1個點中有K個點,距離中心線(同側)大於2個標准差"),
SIX(6, "K+1個點中有K個點,距離中心線(同側)大於1個標准差"),
SEVEN(7, "連續K個點,距離中心線(任一側)1個標准差以內"),
EIGHT(8, "連續K個點,距離中心線(任一側)大於1個標准差"),
;
private final Integer code;
private final String desc;
type(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
public static type getByCode(Integer code) {
if (code == null) {
return null;
}
return Arrays.stream(type.values()).filter(o -> code.equals(o.getCode())).findFirst().get();
}
}
/**
* 是否選中
*/
public enum selectFlag {
YES(10, "是"),
NO(20, "否") ;
private final Integer code;
private final String desc;
selectFlag(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
}
@Data
public class SpcXbarAbnormalJudgment implements Serializable {
/** | 表字段 id */
private String id;
/** | 表字段 spc_id */
private String spcId;
/** 標准序號 | 表字段 standard_code */
private String standardCode;
/** 標准描述 | 表字段 standard_desc */
private String standardDesc;
/** K值 | 表字段 k_value */
private Integer kValue;
/** 是否選中 10是 20否 | 表字段 select_flag */
private Integer selectFlag;
/** | 表字段 create_time */
private Date createTime;
}
/**
* Xbar-R計算工具類
*/
public class XbarRUtils {
/**
* 計算平均值
*
* @param datas
* @return
*/
public static BigDecimal average(List<BigDecimal> datas) {
BigDecimal sum = datas.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal average = divide(sum, BigDecimal.valueOf(datas.size()));
return average;
}
/**
* 計算極差
*
* @param datas
* @return
*/
public static BigDecimal extremeDifference(List<BigDecimal> datas) {
BigDecimal max = datas.stream().max((x1, x2) -> x1.compareTo(x2)).get();
BigDecimal min = datas.stream().min((x1, x2) -> x1.compareTo(x2)).get();
return max.subtract(min);
}
/**
* 計算中位數
*
* @param datas
* @return
*/
public static BigDecimal median(List<BigDecimal> datas) {
BigDecimal median;
Collections.sort(datas);
int size = datas.size();
if (size % 2 == 1) {
median = datas.get((size - 1) / 2);
} else {
median = divide(datas.get(size / 2 - 1).add(datas.get(size / 2)), new BigDecimal(2));
}
return median;
}
public static BigDecimal divide(BigDecimal a, BigDecimal b) {
if (b.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
return a.divide(b, 6, BigDecimal.ROUND_HALF_UP);
}
/**
* 計算標准差σ=sqrt(s^2)
*
* @return
*/
public static BigDecimal standardDiviation(List<BigDecimal> datas) {
int n = datas.size();
BigDecimal sum = datas.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal avg = sum.divide(new BigDecimal(n), 6, BigDecimal.ROUND_HALF_UP);
//方差s^2=[(x1-x)^2 +...(xn-x)^2]/n 或者s^2=[(x1-x)^2 +...(xn-x)^2]/(n-1)
BigDecimal dVar = BigDecimal.ZERO;
for (int i = 0; i < n; i++) {
dVar = dVar.add((datas.get(i).subtract(avg)).multiply(datas.get(i).subtract(avg)));
}
BigDecimal v = dVar.divide(new BigDecimal(n - 1), 6, BigDecimal.ROUND_HALF_UP);
if (v.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
return sqrt(v);
}
// BigDecimal的開方
private static BigDecimal sqrt(BigDecimal num) {
if (num.compareTo(BigDecimal.ZERO) < 0) {
return BigDecimal.ZERO;
}
BigDecimal x = num.divide(new BigDecimal("2"), MathContext.DECIMAL128);
while (x.subtract(x = sqrtIteration(x, num)).abs().compareTo(new BigDecimal("0.0000000000000000000001")) > 0) ;
return x;
}
private static BigDecimal sqrtIteration(BigDecimal x, BigDecimal n) {
return x.add(n.divide(x, MathContext.DECIMAL128)).divide(new BigDecimal("2"), MathContext.DECIMAL128);
}
}
/**
* 判異
* @param datas
* @param spcXbarAbnormalJudgments
* @return
*/
public List<Integer> abnormalJudgment(List<BigDecimal> datas, List<SpcXbarAbnormalJudgment> spcXbarAbnormalJudgments) {
List<Integer> abnormalDataIndexs = Lists.newArrayList();// 用於存儲異常數據的下標
// 基礎數據
BigDecimal avgX = XbarRUtils.average(datas); //中心線-總均值
BigDecimal standardDiviation = XbarRUtils.standardDiviation(datas); //標准差
spcXbarAbnormalJudgments = spcXbarAbnormalJudgments.stream().filter(
spcXbarAbnormalJudgment -> XbarAbnormalJudgeEnums.selectFlag.YES.getCode().equals(spcXbarAbnormalJudgment.getSelectFlag())
).collect(Collectors.toList());
// 數據計算容器
int twoDirection = 1; // 1:中心線上方。-1:中心線下方 (標准2)
List<Integer> twoAbnormalIndex = Lists.newArrayList(); // 累計異常數據下標(標准2)
int threeDirection = 1; // 1:上升 -1:下降 (標准3)
List<Integer> threeAbnormalIndex = Lists.newArrayList(); // 累計異常數據下標(標准3)
int fourDirection = 1; // 1:上升 -1:下降 (標准4)
List<Integer> fourAbnormalIndex = Lists.newArrayList(); // 累計異常數據下標(標准4)
List<Integer> sevenAbnormalIndex = Lists.newArrayList(); // 累計異常數據下標(標准7)
List<Integer> eightAbnormalIndex = Lists.newArrayList(); // 累計異常數據下標(標准8)
for(int i=0; i<datas.size(); i++){
BigDecimal data = datas.get(i);
for (SpcXbarAbnormalJudgment abnormalJudgment : spcXbarAbnormalJudgments) {
BigDecimal k = new BigDecimal(abnormalJudgment.getkValue());
switch (XbarAbnormalJudgeEnums.type.getByCode(Integer.parseInt(abnormalJudgment.getStandardCode()))) {
// 一個點,距離中心線大於K個標准差
case ONE:
if (data.compareTo(avgX.add(k.multiply(standardDiviation))) > 0 || data.compareTo(avgX.subtract(k.multiply(standardDiviation))) < 0) {
abnormalDataIndexs.add(i);
}
break;
// 連續K個點在中心線同一側
case TWO:
if(k.intValue() > 1){
if(i==0){
twoDirection = data.compareTo(avgX) > 0 ? 1 : -1;
twoAbnormalIndex.add(i);
} else {
// 跟上個數據在同一側,累加
if ((data.compareTo(avgX) > 0 && twoDirection > 0) || (data.compareTo(avgX) < 0 && twoDirection < 0)) {
twoAbnormalIndex.add(i);
if(twoAbnormalIndex.size() == k.intValue()) { //第一次到達閾值,把前面符合條件的數據都存入警告集合;
abnormalDataIndexs.addAll(twoAbnormalIndex);
} else if(twoAbnormalIndex.size() > k.intValue()) {
abnormalDataIndexs.add(i);
}
}
// 不符合報警條件,歸零重新計算
else {
twoDirection = data.compareTo(avgX) > 0 ? 1 : -1;
twoAbnormalIndex.clear();
twoAbnormalIndex.add(i);
}
}
}
break;
// 連續K個點, 全部遞增或全部遞減
case THREE:
if(k.intValue() > 1){
if(i<2){
threeAbnormalIndex.add(i); // 判斷方向至少需要兩個點,初始值默認要存兩個,從第三個開始判斷是否遞增遞減;
} else {
// 跟上個數據同個趨勢,累加
if ((data.compareTo(datas.get(i-1)) > 0 && threeDirection > 0) || (data.compareTo(datas.get(i-1)) < 0 && threeDirection < 0)) {
threeAbnormalIndex.add(i);
if(threeAbnormalIndex.size() == k.intValue()) { //第一次到達閾值,把前面符合條件的數據都存入警告集合;
abnormalDataIndexs.addAll(threeAbnormalIndex);
} else if(threeAbnormalIndex.size() > k.intValue()) {
abnormalDataIndexs.add(i);
}
}
// 不符合報警條件,歸零重新計算
else {
threeDirection = data.compareTo(datas.get(i-1)) > 0 ? 1 : -1;
threeAbnormalIndex.clear();
threeAbnormalIndex.add(i-1);
threeAbnormalIndex.add(i);
}
}
}
break;
// 連續K個點,上下交接
case FOUR:
if(k.intValue() > 1){
if(i<2){
fourAbnormalIndex.add(i); // 判斷方向至少需要兩個點,初始值默認要存兩個,從第三個開始判斷是否遞增遞減;
} else {
// 跟上個數據不同趨勢,累加
if ((data.compareTo(datas.get(i-1)) > 0 && fourDirection < 0) || (data.compareTo(datas.get(i-1)) < 0 && fourDirection > 0)) {
fourDirection = data.compareTo(datas.get(i-1)) > 0 ? 1 : -1; // 方向重置
fourAbnormalIndex.add(i);
if(fourAbnormalIndex.size() == k.intValue()) { //第一次到達閾值,把前面符合條件的數據都存入警告集合;
abnormalDataIndexs.addAll(fourAbnormalIndex);
} else if(fourAbnormalIndex.size() > k.intValue()) {
abnormalDataIndexs.add(i);
}
}
// 不符合報警條件,歸零重新計算
else {
fourDirection = data.compareTo(datas.get(i-1)) > 0 ? 1 : -1;
fourAbnormalIndex.clear();
fourAbnormalIndex.add(i-1);
fourAbnormalIndex.add(i);
}
}
}
break;
// K+1個點中有K個點,距離中心線(同側)大於2個標准差
case FIVE:
List<Integer> fiveUpAbnormalIndex = Lists.newArrayList(); // 中心線上方累計異常數據下標(標准5)
List<Integer> fiveDownAbnormalIndex = Lists.newArrayList(); // 中心線下方累計異常數據下標(標准5)
// 從第K個值開始統計,往前遍歷是否有K-1個值符合條件
if (i >= k.intValue() - 1) {
for (int j = 0; j < k.intValue(); j++) {
if (datas.get(i-j).compareTo(avgX.add(new BigDecimal(2).multiply(standardDiviation))) > 0) {
fiveUpAbnormalIndex.add(i);
}
if (datas.get(i-j).compareTo(avgX.subtract(new BigDecimal(2).multiply(standardDiviation))) < 0) {
fiveDownAbnormalIndex.add(i);
}
}
if (fiveUpAbnormalIndex.size() >= k.intValue()-1) {
abnormalDataIndexs.addAll(fiveUpAbnormalIndex);
}
if (fiveDownAbnormalIndex.size() >= k.intValue()-1) {
abnormalDataIndexs.addAll(fiveDownAbnormalIndex);
}
}
break;
// K+1個點中有K個點,距離中心線(同側)大於1個標准差
case SIX:
List<Integer> sixUpAbnormalIndex = Lists.newArrayList(); // 中心線上方累計異常數據下標(標准6)
List<Integer> sixDownAbnormalIndex = Lists.newArrayList(); // 中心線下方累計異常數據下標(標准6)
// 從第K個值開始統計,往前遍歷是否有K-1個值符合條件
if (i >= k.intValue() - 1) {
for (int j = 0; j < k.intValue(); j++) {
if (datas.get(i-j).compareTo(avgX.add(standardDiviation)) > 0) {
sixUpAbnormalIndex.add(i);
}
if (datas.get(i-j).compareTo(avgX.subtract(standardDiviation)) < 0) {
sixDownAbnormalIndex.add(i);
}
}
if (sixUpAbnormalIndex.size() >= k.intValue()-1) {
abnormalDataIndexs.addAll(sixUpAbnormalIndex);
}
if (sixDownAbnormalIndex.size() >= k.intValue()-1) {
abnormalDataIndexs.addAll(sixDownAbnormalIndex);
}
}
break;
// 連續K個點,距離中心線(任一側)1個標准差以內
case SEVEN:
if (data.compareTo(avgX.add(standardDiviation)) < 0 || data.compareTo(avgX.subtract(standardDiviation)) > 0) {
sevenAbnormalIndex.add(i);
if(sevenAbnormalIndex.size() == k.intValue()) { //第一次到達閾值,把前面符合條件的數據都存入警告集合;
abnormalDataIndexs.addAll(sevenAbnormalIndex);
} else if(sevenAbnormalIndex.size() > k.intValue()) {
abnormalDataIndexs.add(i);
}
}
// 不符合報警條件,歸零重新計算
else {
sevenAbnormalIndex.clear();
}
break;
// 連續K個點,距離中心線(任一側)大於1個標准差
case EIGHT:
if (data.compareTo(avgX.add(standardDiviation)) > 0 || data.compareTo(avgX.subtract(standardDiviation)) < 0) {
eightAbnormalIndex.add(i);
if(eightAbnormalIndex.size() == k.intValue()) { //第一次到達閾值,把前面符合條件的數據都存入警告集合;
abnormalDataIndexs.addAll(eightAbnormalIndex);
} else if(eightAbnormalIndex.size() > k.intValue()) {
abnormalDataIndexs.add(i);
}
}
// 不符合報警條件,歸零重新計算
else {
eightAbnormalIndex.clear();
}
break;
}
}
}
abnormalDataIndexs = abnormalDataIndexs.stream().distinct().collect(Collectors.toList());
Collections.sort(abnormalDataIndexs);
return abnormalDataIndexs;
}