Эх сурвалжийг харах

修改正态分布部分,解决精确度问题

littleblue55 4 долоо хоног өмнө
parent
commit
53e8f4451f

+ 162 - 177
src/views/dashboard/sandianscore.vue

@@ -49,8 +49,8 @@
         </el-form-item>
         <el-form-item label="当前行业:" style="margin-bottom: 0">
           <div style="display: inline; padding-left: 10px">
-          {{ industryMap.get(selectedCode) }}
-        </div>
+            {{ industryMap.get(selectedCode) }}
+          </div>
         </el-form-item>
         <!-- <el-form-item label="数据分布区间"  style="margin-bottom:0"
           ><el-input
@@ -59,10 +59,15 @@
             clearable
             style="width: 240px"
         /></el-form-item> -->
-        
+
         <el-form-item style="margin-bottom: 0">
-          <el-button type="primary" icon="el-icon-search" @click="updateChart"  size="mini"
-            >搜索</el-button>
+          <el-button
+            type="primary"
+            icon="el-icon-search"
+            @click="updateChart"
+            size="mini"
+            >搜索</el-button
+          >
         </el-form-item>
       </el-form>
     </div>
@@ -95,6 +100,7 @@ import { listScore } from "@/api/score/score"; // 导入得分数据的接口
 import { listIndustry } from "@/api/industry/industry"; // 导入行业数据的接口
 require("echarts/theme/macarons"); // echarts theme
 import resize from "./mixins/resize";
+import { Decimal } from "decimal.js";
 
 export default {
   mixins: [resize],
@@ -235,7 +241,6 @@ export default {
       listIndustry(this.industryQueryParams)
         .then((response) => {
           this.industryData = response.rows;
-          // console.log("industry_info" + this.industryData);
           this.industryMap = this.industryData.reduce((map, item) => {
             const key = `${item.code}`; // 键是行业代码
             map.set(key, item.industryName); // 将键和对应的行业名称存储到Map中
@@ -262,7 +267,6 @@ export default {
       if (flag == 2) {
         return;
       }
-      // console.log(flag);
       let filteredData = this.chartData;
       if (this.selectedYear) {
         filteredData = filteredData.filter(
@@ -271,18 +275,8 @@ export default {
         );
       }
       const values = filteredData.map((item) => item[this.selectedDataKey]);
-      console.log(filteredData, values, "filteredData")
       const minVal = Math.min(...values).toFixed(2);
       const maxVal = Math.max(...values).toFixed(2);
-      // console.log(minVal, maxVal)
-      // console.log(
-      //   filteredData,
-      //   "filteredData",
-      //   this.selectedDataKey,
-      //   values,
-      //   "values"
-      // );
-      // console.log(filteredData, "filteredData");
       if (
         this.selectedRange === null ||
         this.selectedRange === "" ||
@@ -306,7 +300,6 @@ export default {
 
         // 从最接近 x1 的更小的值开始
         let current = x1;
-        // console.log(current)
         while (current < x2) {
           let next = parseFloat((current + interval).toFixed(2));
 
@@ -330,36 +323,6 @@ export default {
 
         return result;
       }
-      // function generateRangeArray(x1, x2, interval) {
-      //   x1 = parseFloat(x1);
-      //   x2 = parseFloat(x2);
-      //   interval = parseFloat(interval);
-      //   const result = [];
-      //   let current = x1 - (x2 - x1);
-      //   while (current < x2) {
-      //     let next;
-      //     if (current == x1) {
-      //       current = Math.floor(current * 10) / 10;
-      //     }
-      //     next = parseFloat((current + interval).toFixed(2));
-      //     // 保留两位小数
-      //     result.push({
-      //       name: `${current}-${next}`,
-      //       min: current,
-      //       max: next,
-      //     });
-      //     current = next;
-      //   }
-      //   // 最后一个区间
-      //   if (current <= x2) {
-      //     result.push({
-      //       name: `${current}-${x2}`,
-      //       min: current,
-      //       max: x2,
-      //     });
-      //   }
-      //   return result;
-      // }
       // 生成区间标签和频次
       const histogramData = [];
       let result = generateRangeArray(minVal, maxVal, 0.1);
@@ -369,7 +332,6 @@ export default {
         count += values.filter(
           (value) => value >= item.min && value < item.max
         ).length;
-        // console.log(item.max, maxVal)
         if (item.max == maxVal) {
           count += values.filter((value) => value === item.max).length;
         }
@@ -383,83 +345,147 @@ export default {
         (sum, item) => sum + item.value,
         0
       );
+      //---------------------------修改后正态分布-----------------------------
+      // 获取基础数据:最大值,最小值,平均值,标准差
+      function getBebeQ(numbers, digit = 2) {
 
-      //--------------------------正态分布----------------------------------
-      // 计算均值
-      const mean = values.reduce((acc, val) => acc + val, 0) / values.length;
-      // console.log(mean.toFixed(2),"mean")
-      // 计算标准差
-      const stdDev = Math.sqrt(
-        values.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) /
-          values.length
-      );
+        let sum = numbers.reduce(
+          (acc, num) => acc.add(new Decimal(num)),
+          new Decimal(0)
+        );
+
+        let max = Math.max.apply(null, numbers);
+        let min = Math.min.apply(null, numbers);
+
+        // 平均值
+        let mean = sum.dividedBy(numbers.length);
+
+        // 计算每个数与均值的差的平方
+        const squaredDiffs = numbers.map((num) => {
+          const diff = new Decimal(num).minus(mean);
+          return diff.pow(2);
+        });
+
+        // 计算平方差的均值
+        const sumOfSquaredDiffs = squaredDiffs.reduce(
+          (acc, diff) => acc.add(diff),
+          new Decimal(0)
+        );
+
+        const variance = sumOfSquaredDiffs.dividedBy(numbers.length);
+
+        // 开平方得到标准差
+        const standardDeviation = variance.sqrt();
+
+        // 向上取整到最近的 0.1
+        const ceilingRoundedMax = Math.ceil(max * 10) / 10;
+
+        // 向下取整到最近的 0.1
+        const floorRoundedMin = Math.floor(min * 10) / 10;
+
+        return {
+          max: ceilingRoundedMax,
+          min: floorRoundedMin,
+          avg: parseFloat(mean.toNumber().toFixed(digit)) || 0,
+          stdDev: parseFloat(standardDeviation.toNumber().toFixed(digit)),
+        };
+      }
+      // 计算 z-score 的基础数据
+      const scoreBasic = getBebeQ(values);
 
-      // 创建间隔为0.1的区间
-      const center = mean.toFixed(2)
-      const interval = 0.1;
-      let minLimit = Math.floor(Math.min(...values) * 10) / 10; // 向下取整到最近的0.1
-      let maxLimit = Math.ceil(Math.max(...values) * 10) / 10; // 向上取整到最近的0.1
-      console.log("中心", center,minLimit,maxLimit) 
-      if(center-minLimit != maxLimit-center){
-        minLimit = Math.floor(minLimit - (maxLimit-center - (center-minLimit)))
+      const center = new Decimal(scoreBasic.avg); // 确保 center 是 Decimal 类型
+      const interval = new Decimal(0.1);
+
+      // 使用 Decimal 来处理 minLimit 和 maxLimit
+      let minLimit = new Decimal(scoreBasic.min); // 向下取整到最近的0.1
+      let maxLimit = new Decimal(scoreBasic.max); // 向上取整到最近的0.1
+
+      // 确保 minLimit 和 maxLimit 关于 center 对称
+      if (!center.minus(minLimit).equals(maxLimit.minus(center))) {
+        const distanceToCenter = center
+          .minus(minLimit)
+          .gt(maxLimit.minus(center))
+          ? center.minus(minLimit)
+          : maxLimit.minus(center);
+
+        maxLimit = center.plus(distanceToCenter); // 更新 maxLimit 保持对称
+        minLimit = center.minus(distanceToCenter); // 更新 minLimit 保持对称
       }
+
       const intervals = [];
-      console.log(minLimit,maxLimit, interval)
-      // 乘以1000 是将小数变成整数去计算,小数计算会有误差
-      for (let i = minLimit * 1000 ; i <= maxLimit*1000 ; i += (interval*1000)) {
-        intervals.push(parseFloat((i/1000).toFixed(2)))
+      // // 乘以1000 是将小数变成整数去计算,小数计算会有误差
+      for (
+        let i = minLimit * 1000;
+        i <= maxLimit * 1000;
+        i += interval * 1000
+      ) {
+        intervals.push(parseFloat((i / 1000).toFixed(2)));
       }
-      // 计算每个间隔的频次
+      // // 计算每个间隔的频次
       const frequency = new Array(intervals.length - 1).fill(0); // 初始化频次数组
 
-      let intervalArr = intervals.slice(0,-1)
+      let intervalArr = intervals.slice(0, -1);
       const intervalStrings = [];
       for (let i = 0; i < intervalArr.length; i++) {
-          const start = intervalArr[i].toFixed(1); // 保留一位小数
-          const end = ((intervalArr[i]*1000 + interval*1000)/1000).toFixed(1); // 保留一位小数
-          intervalStrings.push({
-            name: `${start}-${end}`,
-            max: end,
-            min: start
-          }); // 构建区间字符串
+        const start = intervalArr[i].toFixed(1); // 保留一位小数
+        const end = ((intervalArr[i] * 1000 + interval * 1000) / 1000).toFixed(
+          1
+        ); // 保留一位小数
+        intervalStrings.push({
+          name: `${start}-${end}`,
+          max: end,
+          min: start,
+        }); // 构建区间字符串
       }
-      console.log(intervals,intervalArr)
+
       values.forEach((value) => {
-        for (let j = 0; j < intervalStrings.length - 1; j++) {
-          // console.log(value)
+        for (let j = 0; j < intervalStrings.length; j++) {
           // 保证包含右边界
-          // console.log(intervals[j].min, intervals[j].max)
-          if (value >= intervalStrings[j].min && value < intervalStrings[j].max) {
-            // console.log(intervals[j],intervals[j + 1])
+          let v = parseFloat(value) * 1000;
+          if (
+            v >= parseFloat(intervalStrings[j].min) * 1000 &&
+            v < parseFloat(intervalStrings[j].max) * 1000
+          ) {
             frequency[j]++;
             break; // 找到对应区间后可以跳出循环
           }
         }
       });
 
-      // 确保处理最大值等于maxLimit的情况
-      if (values.some((value) => value == maxLimit)) {
+      // // 确保处理最大值等于maxLimit的情况
+      if (
+        values.some(
+          (value) => parseFloat(value) * 1000 == parseFloat(maxLimit) * 1000
+        )
+      ) {
         frequency[frequency.length - 1]++;
       }
-
-      // 计算正态分布值
+      // 1.计算正态分布值
       const normalDistributionValues = intervals
         .slice(0, -1)
         .map((intervalStart, index) => {
           const intervalEnd = intervals[index + 1];
-          const midpoint = (intervalStart + intervalEnd) / 2; // 每个区间的中点
-          return (
-            (1 / (stdDev * Math.sqrt(2 * Math.PI))) *
-            Math.exp(-0.5 * Math.pow((midpoint - mean) / stdDev, 2))
+          const midpoint = (intervalStart + intervalEnd) / 2;
+
+          // 使用 Decimal 进行更高精度的计算
+          const midPointDecimal = new Decimal(midpoint);
+          const avgDecimal = new Decimal(scoreBasic.avg);
+          const stdDevDecimal = new Decimal(scoreBasic.stdDev);
+
+          const exponent = midPointDecimal
+            .minus(avgDecimal)
+            .dividedBy(stdDevDecimal)
+            .pow(2)
+            .negated()
+            .dividedBy(new Decimal(2));
+          const exponentValue = new Decimal(Math.exp(exponent.toNumber())); // 取自然指数
+          const coefficient = new Decimal(1).dividedBy(
+            stdDevDecimal.times(new Decimal(Math.sqrt(2 * Math.PI)))
           );
+
+          return coefficient.times(exponentValue).toNumber(); // 返回计算结果
         });
-      // 输出结果
-      // console.log("Intervals:", intervals.slice(0, -1)); // 显示间隔
-      // console.log("Frequencies:", frequency);
-      // console.log("Normal Distribution Values:", normalDistributionValues);
-      //---------------------------------正态分布end---------------------------
-      // const quadraticData = this.generateQuadraticData(binCount, maxHistogramValue, totalHistogramValue);
-      // littlegreen - 去掉line和scatter,增加x轴拖动,根据检测区间修改柱状的颜色
+      //----------------------------修改后正态分布end-----------------------------
       const option = {
         color: ["rgba(245,0,0,0.6)"],
         dataZoom: [
@@ -491,7 +517,7 @@ export default {
             `${this.selectedYear}年 ${
               this.keyToChinese[this.selectedDataKey]
             }企业数统计`,
-            "正态分布"
+            "正态分布",
           ],
         },
         grid: {
@@ -504,23 +530,25 @@ export default {
           type: "category",
           name: "分布区间",
           // data: histogramData.map((item) => item.name),
-          data: intervalStrings.map(item=>item.name),
+          data: intervalStrings.map((item) => item.name),
           axisLabel: {
             fontSize: 12,
-            interval: 0,
-            rotate: 20,
+            // interval: 0,
+            rotate: 0,
             // padding: [0, 0, 0, -200]
           },
         },
-        yAxis: [{
-          name: "企业数",
-          type: "value",
-          minInterval: 1,
-        },{
-          name: '正态分布',
-          type: 'value'
-
-        }],
+        yAxis: [
+          {
+            name: "企业数",
+            type: "value",
+            minInterval: 1,
+          },
+          {
+            name: "正态分布",
+            type: "value",
+          },
+        ],
         series: [
           {
             name: `${this.selectedYear}年 ${
@@ -534,16 +562,13 @@ export default {
                 color: function (params) {
                   const value = params.dataIndex; // 获取当前柱子在数据中的索引
                   if (flag == 1) {
-                    let min = parseFloat(intervalStrings[value].min)*1000
-                    let max = parseFloat(intervalStrings[value].max)*1000
-                    const detectMin = parseFloat(that.detectMin)*1000
-                    const detectMax = parseFloat(that.detectMax)*1000
-                    if (
-                      min >= detectMin &&
-                      max <= detectMax
-                    ) {
+                    let min = parseFloat(intervalStrings[value].min) * 1000;
+                    let max = parseFloat(intervalStrings[value].max) * 1000;
+                    const detectMin = parseFloat(that.detectMin) * 1000;
+                    const detectMax = parseFloat(that.detectMax) * 1000;
+                    if (min >= detectMin && max <= detectMax) {
                       return "#ffcc00"; // 变更颜色为黄色
-                    }else if(min == detectMax){
+                    } else if (min == detectMax) {
                       return "#ffcc00";
                     }
                     return "rgba(245,0,0,0.6)";
@@ -560,7 +585,7 @@ export default {
             },
           },
           {
-            name:"正态分布",
+            name: "正态分布",
             yAxisIndex: 1,
             type: "line",
             data: normalDistributionValues,
@@ -574,21 +599,24 @@ export default {
       this.isChartShow = true;
       this.chart.setOption(option);
       this.chart.on("click", function (param) {
-        // console.log(param.dataIndex, intervalStrings)
-        if(typeof param.dataIndex === "number" &&
+        if (
+          typeof param.dataIndex === "number" &&
           param.dataIndex >= 0 &&
           param.dataIndex < intervalStrings.length
-        ){
+        ) {
           let dataIndexValue = intervalStrings[param.dataIndex];
-          const filteredArray = filteredData.filter((item)=>{
-            const isMaxValEqual = parseFloat(dataIndexValue.max) == parseFloat(maxVal);
-            const lowerBoundCheck = item[that.selectedDataKey] >= dataIndexValue.min;
-            const upperBoundCheck  = isMaxValEqual
-              ? item[that.selectedDataKey] <= dataIndexValue.max
-              : item[that.selectedDataKey] < dataIndexValue.max;
+          const filteredArray = filteredData.filter((item) => {
+            let dmax = new Decimal(dataIndexValue.max);
+            let dmin = new Decimal(dataIndexValue.min);
+            // maxLimit
+            let v = new Decimal(item[that.selectedDataKey]);
+            const isMaxValEqual = dmax.equals(maxLimit);
+            const lowerBoundCheck = v.greaterThan(dmin) || v.equals(dmin);
+            const upperBoundCheck = isMaxValEqual
+              ? v.lessThan(dmax) || v.equals(dmax)
+              : v.lessThan(dmax);
             return lowerBoundCheck && upperBoundCheck;
-          })
-          // console.log(filteredData,filteredArray,that.selectedDataKey)
+          });
           that.tableData = filteredArray.map((item) => {
             return {
               industryName: item.code,
@@ -599,49 +627,6 @@ export default {
             };
           });
         }
-        // if (
-        //   typeof param.dataIndex === "number" &&
-        //   param.dataIndex >= 0 &&
-        //   param.dataIndex < intervalStrings.length
-        // ) {
-        //   let dataIndexValue = intervalStrings[param.dataIndex];
-        //   const filteredArray = filteredData.filter((item) => {
-        //     const isMaxValEqual = intervalStrings[1] === maxVal;
-        //     const lowerBoundCheck =
-        //       item[that.selectedDataKey] >= dataIndexValue.min;
-        //     const upperBoundCheck = isMaxValEqual
-        //       ? item[that.selectedDataKey] <= dataIndexValue.max
-        //       : item[that.selectedDataKey] < dataIndexValue.max;
-
-        //     return lowerBoundCheck && upperBoundCheck;
-        //   });
-        //   that.tableData = filteredArray.map((item) => {
-        //     return {
-        //       industryName: item.code,
-        //       enterpriseId: item.enterpriseName,
-        //       year: item.year,
-        //       value: item[that.selectedDataKey],
-        //       level: "高",
-        //     };
-        //   });
-        // }
-        // const filteredArray = filteredData.filter((item) => {
-        //     const isMaxValEqual = result[1] === maxVal;
-        //     const lowerBoundCheck = item[that.selectedDataKey] >= dataIndexValue.min;
-        //     const upperBoundCheck = isMaxValEqual
-        //         ? item[that.selectedDataKey] <= dataIndexValue.max
-        //         : item[that.selectedDataKey] < dataIndexValue.max;
-
-        //     return lowerBoundCheck && upperBoundCheck;
-        // })
-        // that.tableData = filteredArray.map(item => {
-        //   return {
-        //     industryName: item.code,
-        //     enterpriseId: item.enterpriseName,
-        //     year: item.year,
-        //     value: item[that.selectedDataKey]
-        //   }
-        // })
       });
     },
     generateQuadraticData(binCount, maxHistogramValue, totalHistogramValue) {