網上找了很久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; }