Sfoglia il codice sorgente

0917 超短期报表字段调整 - 取消4小时前预测准确率,新增准确率(acc)计算

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
taosj 1 mese fa
parent
commit
8468f1efc2

+ 191 - 330
forecast-backend-server/src/main/java/com/ufo/project/data/service/impl/UltraShortTermPowerForecastReportServiceImpl.java

@@ -2,12 +2,14 @@ package com.ufo.project.data.service.impl;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.YearMonth;
 import java.time.ZoneId;
-import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -17,14 +19,11 @@ import java.util.Map.Entry;
 import java.util.stream.Collectors;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import com.ufo.base.common.utils.StringUtils;
 import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportQuery;
 import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportVO;
 import com.ufo.project.data.service.IUltraShortTermPowerForecastReportService;
-import com.ufo.project.forecast.supershort.domain.DataSuperShortTermForecastIssued;
-import com.ufo.project.forecast.supershort.domain.DataUltraShortTermPowerForecasting;
-import com.ufo.project.forecast.supershort.service.IDataSuperShortTermForecastIssuedService;
-import com.ufo.project.forecast.supershort.service.IDataUltraShortTermPowerForecastingService;
+import com.ufo.project.forecast.shortterm.power.domain.ForecastPowerAverage;
+import com.ufo.project.forecast.supershort.service.IDataSuperShortTermForecastReportService;
 import com.ufo.project.model.domain.DataElectricityLocal;
 import com.ufo.project.model.service.IDataElectricityLocalService;
 
@@ -39,22 +38,17 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
 {
     private static final int SCALE = 3;
     private static final long SECONDS_PER_DAY = 86400L;
-    private static final long SECONDS_PER_HOUR = 3600L;
-    private static final long SECONDS_PER_4H = SECONDS_PER_HOUR * 4L;
 
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyy-MM");
     private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
-    private static final ZoneOffset ZONE_8 = ZoneOffset.ofHours(8);
+    private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
     @Autowired
     private IDataElectricityLocalService electricityLocalService;
 
     @Autowired
-    private IDataSuperShortTermForecastIssuedService superShortService;
-
-    @Autowired
-    private IDataUltraShortTermPowerForecastingService ultraForecastService;
+    private IDataSuperShortTermForecastReportService superShortService;
 
     /**
      * 超短期预测报表查询
@@ -78,52 +72,56 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
         }
 
         // 2. 获取超短期预测功率数据
-        Long startSec = toUnixSeconds(startStr);
-        Long endSec = toUnixSeconds(endStr);
-        List<DataSuperShortTermForecastIssued> forecastList = selectSuperShortByTimeRange(entityId, startSec, endSec);
-
-        // 3. 获取超短期预报数据(提前4h)
-        Long startSec4h = startSec - SECONDS_PER_4H;
-        List<DataUltraShortTermPowerForecasting> ultraForecastList = ultraForecastService
-                .selectByTimeRange(entityId, startSec4h, endSec);
+        long utcStartTime = parseLong(startStr);
+        long utcEndTime = parseLong(endStr);
+        List<ForecastPowerAverage> forecastList = superShortService.selectAveragePowerByTimeRange(entityId, utcStartTime, utcEndTime);
+        this.dataForecastList2Gmt8(forecastList);
 
         // 4. 按基准类型分组
         switch (type)
         {
             case "daily":
-                return buildDailyReport(actualList, forecastList, ultraForecastList);
+                return buildDailyReport(actualList, forecastList);
             case "monthly":
-                return buildMonthlyReport(actualList, forecastList, ultraForecastList);
+                return buildMonthlyReport(actualList, forecastList);
             case "yearly":
-                return buildYearlyReport(actualList, forecastList, ultraForecastList);
+                return buildYearlyReport(actualList, forecastList);
             default:
                 return Collections.emptyList();
         }
     }
 
+    private void dataForecastList2Gmt8(List<ForecastPowerAverage> forecastList)
+    {
+        for (ForecastPowerAverage record : forecastList)
+        {
+            ZonedDateTime forecastZdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(record.getForecastTime()),
+                    ZoneId.systemDefault());
+            ZonedDateTime forecastGmt8 = forecastZdt.withZoneSameInstant(ZONE_SHANGHAI);
+            record.setForecastTime(forecastGmt8.toEpochSecond());
+        }
+    }
+
     /**
      * 构建日报
      */
     private List<UltraShortTermPowerForecastReportVO> buildDailyReport(
             List<DataElectricityLocal> actualList,
-            List<DataSuperShortTermForecastIssued> forecastList,
-            List<DataUltraShortTermPowerForecasting> ultraForecastList)
+            List<ForecastPowerAverage> forecastList)
     {
         Map<String, List<DataElectricityLocal>> dailyMap = groupByDay(actualList);
-        Map<String, List<DataSuperShortTermForecastIssued>> forecastDailyMap = groupForecastByDay(forecastList);
-        Map<Long, List<DataUltraShortTermPowerForecasting>> ultraForecastMap = groupUltraForecastByStartSec(ultraForecastList);
+        Map<String, List<ForecastPowerAverage>> forecastDailyMap = groupForecastByDay(forecastList);
 
         List<UltraShortTermPowerForecastReportVO> result = new ArrayList<>();
         for (Entry<String, List<DataElectricityLocal>> entry : dailyMap.entrySet())
         {
             String day = entry.getKey();
             List<DataElectricityLocal> records = entry.getValue();
-            List<DataSuperShortTermForecastIssued> forecastRecords = forecastDailyMap.getOrDefault(day, Collections.emptyList());
-            List<DataUltraShortTermPowerForecasting> ultraRecords = collectUltraForDay(day, ultraForecastMap);
+            List<ForecastPowerAverage> forecastRecords = forecastDailyMap.getOrDefault(day, Collections.emptyList());
 
             UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
             vo.setTime(day);
-            computeMetrics(vo, records, forecastRecords, ultraRecords);
+            computeMetrics(vo, records, forecastRecords);
             result.add(vo);
         }
         return result;
@@ -134,25 +132,22 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
      */
     private List<UltraShortTermPowerForecastReportVO> buildMonthlyReport(
             List<DataElectricityLocal> actualList,
-            List<DataSuperShortTermForecastIssued> forecastList,
-            List<DataUltraShortTermPowerForecasting> ultraForecastList)
+            List<ForecastPowerAverage> forecastList)
     {
         Map<String, List<DataElectricityLocal>> monthlyMap = groupByMonth(actualList);
-        Map<String, List<DataSuperShortTermForecastIssued>> forecastMonthlyMap = groupForecastByMonth(forecastList);
-        Map<Long, List<DataUltraShortTermPowerForecasting>> ultraForecastMap = groupUltraForecastByStartSec(ultraForecastList);
+        Map<String, List<ForecastPowerAverage>> forecastMonthlyMap = groupForecastByMonth(forecastList);
 
         List<UltraShortTermPowerForecastReportVO> result = new ArrayList<>();
         for (Entry<String, List<DataElectricityLocal>> entry : monthlyMap.entrySet())
         {
             String month = entry.getKey();
             List<DataElectricityLocal> records = entry.getValue();
-            List<DataSuperShortTermForecastIssued> forecastRecords =
+            List<ForecastPowerAverage> forecastRecords =
                     forecastMonthlyMap.getOrDefault(month, Collections.emptyList());
-            List<DataUltraShortTermPowerForecasting> ultraRecords = collectUltraForMonth(month, ultraForecastMap);
 
             UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
             vo.setTime(month);
-            computeMetrics(vo, records, forecastRecords, ultraRecords);
+            computeMetrics(vo, records, forecastRecords);
             result.add(vo);
         }
         return result;
@@ -163,25 +158,22 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
      */
     private List<UltraShortTermPowerForecastReportVO> buildYearlyReport(
             List<DataElectricityLocal> actualList,
-            List<DataSuperShortTermForecastIssued> forecastList,
-            List<DataUltraShortTermPowerForecasting> ultraForecastList)
+            List<ForecastPowerAverage> forecastList)
     {
         Map<String, List<DataElectricityLocal>> yearlyMap = groupByYear(actualList);
-        Map<String, List<DataSuperShortTermForecastIssued>> forecastYearlyMap = groupForecastByYear(forecastList);
-        Map<Long, List<DataUltraShortTermPowerForecasting>> ultraForecastMap = groupUltraForecastByStartSec(ultraForecastList);
+        Map<String, List<ForecastPowerAverage>> forecastYearlyMap = groupForecastByYear(forecastList);
 
         List<UltraShortTermPowerForecastReportVO> result = new ArrayList<>();
         for (Entry<String, List<DataElectricityLocal>> entry : yearlyMap.entrySet())
         {
             String year = entry.getKey();
             List<DataElectricityLocal> records = entry.getValue();
-            List<DataSuperShortTermForecastIssued> forecastRecords =
+            List<ForecastPowerAverage> forecastRecords =
                     forecastYearlyMap.getOrDefault(year, Collections.emptyList());
-            List<DataUltraShortTermPowerForecasting> ultraRecords = collectUltraForYear(year, ultraForecastMap);
 
             UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
             vo.setTime(year);
-            computeMetrics(vo, records, forecastRecords, ultraRecords);
+            computeMetrics(vo, records, forecastRecords);
             result.add(vo);
         }
         return result;
@@ -192,8 +184,7 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
      */
     private void computeMetrics(UltraShortTermPowerForecastReportVO vo,
             List<DataElectricityLocal> actualRecords,
-            List<DataSuperShortTermForecastIssued> forecastRecords,
-            List<DataUltraShortTermPowerForecasting> ultraRecords)
+            List<ForecastPowerAverage> forecastRecords)
     {
         // 1. 开机容量
         BigDecimal operatingCapacity = computeOperatingCapacity(actualRecords);
@@ -207,21 +198,22 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
         BigDecimal forecastPower = computeForecastPower(forecastRecords);
         vo.setForecastPower(forecastPower);
 
+        Map<Long, BigDecimal> forecastMap = buildForecastMap(forecastRecords);
         // 4. RMSE
-        BigDecimal rmse = computeRmse(actualRecords, forecastRecords);
+        BigDecimal rmse = computeRmse(actualRecords, forecastRecords, forecastMap);
         vo.setRootMeanSquareError(rmse);
 
         // 5. MAE
-        BigDecimal mae = computeMae(actualRecords, forecastRecords);
+        BigDecimal mae = computeMae(actualRecords, forecastRecords, forecastMap);
         vo.setMeanAbsoluteError(mae);
 
         // 6. 相关性系数
-        BigDecimal corr = computeCorrelation(actualRecords, forecastRecords);
+        BigDecimal corr = computeCorrelation(actualRecords, forecastRecords, forecastMap);
         vo.setCorrelationCoefficientR(corr);
 
-        // 7. 4小时前预测准确率
-        BigDecimal accuracy = computeAheadForecastAccuracy4h(actualRecords, ultraRecords, operatingCapacity);
-        vo.setAheadForecastAccuracy4h(accuracy);
+        // 7. 准确率 (acc)
+        BigDecimal acc = computeAcc(actualRecords, forecastRecords, operatingCapacity);
+        vo.setAcc(acc);
     }
 
     // ==================== 基础计算 ====================
@@ -264,18 +256,18 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
         return BigDecimal.valueOf(sum / count / 1000.0).setScale(SCALE, RoundingMode.HALF_UP);
     }
 
-    private BigDecimal computeForecastPower(List<DataSuperShortTermForecastIssued> records)
+    private BigDecimal computeForecastPower(List<ForecastPowerAverage> records)
     {
-        double sum = 0.0;
+        BigDecimal sum = BigDecimal.ZERO;
         int count = 0;
-        for (DataSuperShortTermForecastIssued r : records)
+        for (ForecastPowerAverage r : records)
         {
-            String power = r.getPower();
-            if (StringUtils.isNotEmpty(power))
+            BigDecimal power = r.getAvgPower();
+            if (power != null)
             {
                 try
                 {
-                    sum += Double.parseDouble(power.trim());
+                    sum = sum.add(power);
                     count++;
                 }
                 catch (NumberFormatException e)
@@ -285,229 +277,178 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
             }
         }
         if (count == 0) return BigDecimal.ZERO;
-        return BigDecimal.valueOf(sum / count / 1000.0).setScale(SCALE, RoundingMode.HALF_UP);
+        BigDecimal avg = sum.divide(BigDecimal.valueOf(count), 10, RoundingMode.HALF_UP);
+        return avg.divide(new BigDecimal(1000), 3, RoundingMode.HALF_UP);
     }
 
     private BigDecimal computeRmse(List<DataElectricityLocal> actualRecords,
-            List<DataSuperShortTermForecastIssued> forecastRecords)
+            List<ForecastPowerAverage> forecastRecords,
+            Map<Long, BigDecimal> forecastMap)
     {
-        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
         if (forecastMap.isEmpty()) return BigDecimal.ZERO;
 
-        double sumSquaredError = 0.0;
+        BigDecimal sumSquaredError = BigDecimal.ZERO;
         int count = 0;
 
         for (DataElectricityLocal r : actualRecords)
         {
             if (r.getTm() == null) continue;
             long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
-            Double fp = forecastMap.get(ts);
+            BigDecimal fp = forecastMap.get(ts);
             if (fp == null) continue;
 
-            Double ap = r.getActualPower();
-            if (ap == null) continue;
+            BigDecimal ap = new BigDecimal(r.getActualPower());
 
-            double diffMW = (ap - fp) / 1000.0;
-            sumSquaredError += diffMW * diffMW;
+            BigDecimal diffMW = ap.subtract(fp).divide(BigDecimal.valueOf(1000), 10, RoundingMode.HALF_UP);
+            sumSquaredError = sumSquaredError.add(diffMW.multiply(diffMW));
             count++;
         }
 
         if (count == 0) return BigDecimal.ZERO;
-        return BigDecimal.valueOf(Math.sqrt(sumSquaredError / count)).setScale(SCALE, RoundingMode.HALF_UP);
+        return BigDecimal.valueOf(Math.sqrt(sumSquaredError.divide(BigDecimal.valueOf(count), 10, RoundingMode.HALF_UP).doubleValue()));
     }
 
     private BigDecimal computeMae(List<DataElectricityLocal> actualRecords,
-            List<DataSuperShortTermForecastIssued> forecastRecords)
+            List<ForecastPowerAverage> forecastRecords,
+            Map<Long, BigDecimal> forecastMap)
     {
-        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
         if (forecastMap.isEmpty()) return BigDecimal.ZERO;
 
-        double sumAbsError = 0.0;
+        BigDecimal sumAbsError = BigDecimal.ZERO;
         int count = 0;
 
         for (DataElectricityLocal r : actualRecords)
         {
             if (r.getTm() == null) continue;
             long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
-            Double fp = forecastMap.get(ts);
+            BigDecimal fp = forecastMap.get(ts);
             if (fp == null) continue;
 
-            Double ap = r.getActualPower();
+            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
             if (ap == null) continue;
 
-            double absErrorMW = Math.abs(ap - fp) / 1000.0;
-            sumAbsError += absErrorMW;
+            BigDecimal absErrorMW = ap.subtract(fp).divide(BigDecimal.valueOf(1000), 10, RoundingMode.HALF_UP);
+            sumAbsError = sumAbsError.add(absErrorMW);
             count++;
         }
 
         if (count == 0) return BigDecimal.ZERO;
-        return BigDecimal.valueOf(sumAbsError / count).setScale(SCALE, RoundingMode.HALF_UP);
+        return sumAbsError.divide(BigDecimal.valueOf(count), 10, RoundingMode.HALF_UP);
     }
 
     private BigDecimal computeCorrelation(List<DataElectricityLocal> actualRecords,
-            List<DataSuperShortTermForecastIssued> forecastRecords)
+            List<ForecastPowerAverage> forecastRecords,
+            Map<Long, BigDecimal> forecastMap)
     {
-        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
         if (forecastMap.isEmpty()) return BigDecimal.ZERO;
 
-        List<Double> xList = new ArrayList<>();
-        List<Double> yList = new ArrayList<>();
+        List<BigDecimal> xList = new ArrayList<>();
+        List<BigDecimal> yList = new ArrayList<>();
 
         for (DataElectricityLocal r : actualRecords)
         {
             if (r.getTm() == null) continue;
             long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
-            Double fp = forecastMap.get(ts);
+            BigDecimal fp = forecastMap.get(ts);
             if (fp == null) continue;
 
-            Double ap = r.getActualPower();
+            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
             if (ap == null) continue;
 
-            xList.add(ap / 1000.0);
-            yList.add(fp / 1000.0);
+            xList.add(ap.divide(BigDecimal.valueOf(1000), 10, RoundingMode.HALF_UP));
+            yList.add(fp.divide(BigDecimal.valueOf(1000), 10, RoundingMode.HALF_UP));
         }
 
         int n = xList.size();
         if (n < 2) return BigDecimal.ZERO;
 
-        double meanX = xList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
-        double meanY = yList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
+        BigDecimal meanX = xList.stream()
+            .reduce(BigDecimal.ZERO, BigDecimal::add)
+            .divide(BigDecimal.valueOf(xList.size()), 10, RoundingMode.HALF_UP);
+        BigDecimal meanY = yList.stream()
+            .reduce(BigDecimal.ZERO, BigDecimal::add)
+            .divide(BigDecimal.valueOf(yList.size()), 10, RoundingMode.HALF_UP);
 
-        double numerator = 0.0;
-        double sumSqX = 0.0;
-        double sumSqY = 0.0;
+        BigDecimal numerator = BigDecimal.ZERO;
+        BigDecimal sumSqX = BigDecimal.ZERO;
+        BigDecimal sumSqY = BigDecimal.ZERO;
 
         for (int i = 0; i < n; i++)
         {
-            double dx = xList.get(i) - meanX;
-            double dy = yList.get(i) - meanY;
-            numerator += dx * dy;
-            sumSqX += dx * dx;
-            sumSqY += dy * dy;
+            BigDecimal dx = xList.get(i).subtract(meanX);
+            BigDecimal dy = yList.get(i).subtract(meanY);
+            numerator = numerator.add(dx.multiply(dy));
+            sumSqX = sumSqX.add(dx.multiply(dx));
+            sumSqY = sumSqY.add(dy.multiply(dy));
         }
 
-        double denominator = Math.sqrt(sumSqX * sumSqY);
-        if (denominator == 0.0) return BigDecimal.ZERO;
+        BigDecimal denominator = BigDecimal.valueOf(Math.sqrt(sumSqX.multiply(sumSqY).doubleValue()));
+        if (denominator.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
 
-        double r = numerator / denominator;
-        return BigDecimal.valueOf(r).setScale(SCALE, RoundingMode.HALF_UP);
+        BigDecimal r = numerator.divide(denominator, 10, RoundingMode.HALF_UP);
+        return r;
     }
 
     /**
-     * 计算4小时前预测准确率
-     *
-     * 对每个实际数据点,找到start_time匹配的超短期预报记录
-     * 从该记录的power15~power240字段提取16个预测值,取算术平均得P_Pi
-     * error_i = (P_Mi - P_Pi) / max(P_Mi, 0.2 * Cap)
-     * ACC = 1 - sqrt((1/N) * sum(error_i^2))
+     * 计算准确率(acc)
+     * acc = (1 - 偏差值) × 100%
+     * 偏差值 = sqrt( sum(Si) / N )
+     * Si = ((Pmi - Ppi) / max(阈值, Pmi))^2
+     * 阈值 = 开机容量(MW) * 0.2
      */
-    private BigDecimal computeAheadForecastAccuracy4h(List<DataElectricityLocal> actualRecords,
-            List<DataUltraShortTermPowerForecasting> ultraRecords,
+    private BigDecimal computeAcc(List<DataElectricityLocal> actualRecords,
+            List<ForecastPowerAverage> forecastRecords,
             BigDecimal operatingCapacity)
     {
-        if (ultraRecords == null || ultraRecords.isEmpty()) return BigDecimal.ZERO;
-        if (operatingCapacity == null || operatingCapacity.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
+        if (forecastRecords == null || forecastRecords.isEmpty()) return BigDecimal.ZERO;
 
-        double capacityMW = operatingCapacity.doubleValue();
-        double thresholdMW = Math.max(0.2 * capacityMW, 0.001); // 防止分母过小
+        // 1. 阈值 = 开机容量(MW) * 0.2 (四舍五入保留3位小数)
+        BigDecimal threshold = operatingCapacity.multiply(new BigDecimal("0.2")).setScale(SCALE, RoundingMode.HALF_UP);
 
-        // 构建超短期预报数据映射: start_time(秒) -> 记录
-        Map<Long, DataUltraShortTermPowerForecasting> ultraMap = new LinkedHashMap<>();
-        for (DataUltraShortTermPowerForecasting r : ultraRecords)
-        {
-            if (r.getStartTime() != null)
-            {
-                ultraMap.put(r.getStartTime(), r);
-            }
-        }
+        // 2. 构建预测数据映射: unix秒(GMT+8) -> power(kW)
+        Map<Long, BigDecimal> forecastMap = buildForecastMap(forecastRecords);
+        if (forecastMap.isEmpty()) return BigDecimal.ZERO;
 
-        double sumSquaredError = 0.0;
-        int count = 0;
+        // 3. 遍历actualRecords计算Si
+        BigDecimal stotal = BigDecimal.ZERO;
+        int n = 0;
 
-        for (DataElectricityLocal actual : actualRecords)
+        for (DataElectricityLocal r : actualRecords)
         {
-            if (actual.getTm() == null || actual.getActualPower() == null) continue;
-
-            // 实际数据的unix秒时间戳
-            long actualTs = actual.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
-
-            // 查找start_time匹配的超短期预报记录
-            DataUltraShortTermPowerForecasting ultra = ultraMap.get(actualTs);
-            if (ultra == null) continue;
-
-            // 计算P_Pi = (power15 + power30 + ... + power240) / 16 的算术平均
-            double ppSum = 0.0;
-            int ppCount = 0;
+            if (r.getTm() == null) continue;
 
-            String[] powerFields = {
-                    "power15", "power30", "power45", "power60",
-                    "power75", "power90", "power105", "power120",
-                    "power135", "power150", "power165", "power180",
-                    "power195", "power210", "power225", "power240"
-            };
+            // 将actual的tm转换为unix秒(GMT+8)以匹配forecastMap的key
+            long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
+            BigDecimal fp = forecastMap.get(ts);
+            if (fp == null) continue;
 
-            for (String field : powerFields)
-            {
-                Double value = getPowerField(ultra, field);
-                if (value != null)
-                {
-                    ppSum += value;
-                    ppCount++;
-                }
-            }
+            BigDecimal pm = BigDecimal.valueOf(r.getActualPower()); // kW
+            BigDecimal pp = fp; // kW
 
-            if (ppCount == 0) continue;
-            double ppMean = ppSum / ppCount; // P_Pi (kW)
+            // Si = ((Pmi - Ppi) / max(阈值, Pmi))^2
+            BigDecimal denom = threshold.max(pm);
+            if (denom.compareTo(BigDecimal.ZERO) == 0) continue;
 
-            // P_Mi (kW)
-            double pm = actual.getActualPower();
+            BigDecimal diff = pm.subtract(pp).divide(denom, 10, RoundingMode.HALF_UP);
+            stotal = stotal.add(diff.multiply(diff));
+            n++;
+        }
 
-            // error_i = (P_Mi - P_Pi) / max(P_Mi, 0.2 * Cap)
-            double denominator = Math.max(Math.abs(pm), thresholdMW);
-            double error = (pm - ppMean) / denominator;
+        if (n == 0) return BigDecimal.ZERO;
 
-            sumSquaredError += error * error;
-            count++;
-        }
+        // 4. 偏差值 = sqrt(sum(Si) / N)
+        BigDecimal meanSq = stotal.divide(BigDecimal.valueOf(n), 10, RoundingMode.HALF_UP);
+        BigDecimal deviation = BigDecimal.valueOf(Math.sqrt(meanSq.doubleValue()));
 
-        if (count == 0) return BigDecimal.ZERO;
+        // 5. acc = (1 - 偏差值) × 100%
+        BigDecimal acc = new BigDecimal("1").subtract(deviation).multiply(new BigDecimal("100"))
+                .setScale(SCALE, RoundingMode.HALF_UP);
 
-        double acc = 1.0 - Math.sqrt(sumSquaredError / count);
-        return BigDecimal.valueOf(acc).setScale(SCALE, RoundingMode.HALF_UP);
-    }
+        // 6. 边界处理
+        if (acc.compareTo(BigDecimal.ZERO) < 0) acc = BigDecimal.ZERO;
+        if (acc.compareTo(new BigDecimal("100")) > 0) acc = new BigDecimal("100");
 
-    /**
-     * 安全获取Power字段值
-     */
-    private Double getPowerField(DataUltraShortTermPowerForecasting record, String fieldName)
-    {
-        try
-        {
-            switch (fieldName)
-            {
-                case "power15":  return parseDouble(record.getPower15());
-                case "power30":  return parseDouble(record.getPower30());
-                case "power45":  return parseDouble(record.getPower45());
-                case "power60":  return parseDouble(record.getPower60());
-                case "power75":  return parseDouble(record.getPower75());
-                case "power90":  return parseDouble(record.getPower90());
-                case "power105": return parseDouble(record.getPower105());
-                case "power120": return parseDouble(record.getPower120());
-                case "power135": return parseDouble(record.getPower135());
-                case "power150": return parseDouble(record.getPower150());
-                case "power165": return parseDouble(record.getPower165());
-                case "power180": return parseDouble(record.getPower180());
-                case "power195": return parseDouble(record.getPower195());
-                case "power210": return parseDouble(record.getPower210());
-                case "power225": return parseDouble(record.getPower225());
-                case "power240": return parseDouble(record.getPower240());
-                default: return null;
-            }
-        }
-        catch (Exception e)
-        {
-            return null;
-        }
+        return acc;
     }
 
     // ==================== 辅助方法 ====================
@@ -515,25 +456,20 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
     /**
      * 构建超短期预测数据映射: unix秒 -> power(kW)
      */
-    private Map<Long, Double> buildForecastMap(List<DataSuperShortTermForecastIssued> records)
+    private Map<Long, BigDecimal> buildForecastMap(List<ForecastPowerAverage> records)
     {
-        Map<Long, Double> map = new LinkedHashMap<>();
+        Map<Long, BigDecimal> map = new LinkedHashMap<>();
         if (records == null) return map;
 
-        for (DataSuperShortTermForecastIssued r : records)
+        for (ForecastPowerAverage r : records)
         {
-            String power = r.getPower();
-            if (StringUtils.isEmpty(power)) continue;
+            BigDecimal power = r.getAvgPower();
+            if (power == null) continue;
             try
             {
-                Double p = Double.parseDouble(power.trim());
                 if (r.getForecastTime() != null)
                 {
-                    map.put(r.getForecastTime(), p);
-                }
-                if (r.getStartTime() != null)
-                {
-                    map.putIfAbsent(r.getStartTime(), p);
+                    map.put(r.getForecastTime(), power);
                 }
             }
             catch (NumberFormatException e)
@@ -560,33 +496,6 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
         }
     }
 
-    /**
-     * 安全解析String为Long
-     */
-    private Long parseLong(String value)
-    {
-        if (value == null || value.isEmpty()) return 0L;
-        try
-        {
-            return Long.parseLong(value.trim());
-        }
-        catch (NumberFormatException e)
-        {
-            return 0L;
-        }
-    }
-
-    /**
-     * 将yyyy-MM-dd HH:mm:ss字符串转换为unix秒级时间戳
-     */
-    private Long toUnixSeconds(String dateTime)
-    {
-        return java.time.LocalDateTime.parse(dateTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
-                .atZone(ZONE_8)
-                .toInstant()
-                .getEpochSecond();
-    }
-
     /**
      * 按日分组
      */
@@ -638,8 +547,8 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
     /**
      * 按日分组超短期预测数据
      */
-    private Map<String, List<DataSuperShortTermForecastIssued>> groupForecastByDay(
-            List<DataSuperShortTermForecastIssued> records)
+    private Map<String, List<ForecastPowerAverage>> groupForecastByDay(
+            List<ForecastPowerAverage> records)
     {
         return records.stream()
                 .filter(r -> r.getForecastTime() != null)
@@ -652,8 +561,8 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
     /**
      * 按月分组超短期预测数据
      */
-    private Map<String, List<DataSuperShortTermForecastIssued>> groupForecastByMonth(
-            List<DataSuperShortTermForecastIssued> records)
+    private Map<String, List<ForecastPowerAverage>> groupForecastByMonth(
+            List<ForecastPowerAverage> records)
     {
         return records.stream()
                 .filter(r -> r.getForecastTime() != null)
@@ -670,8 +579,8 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
     /**
      * 按年分组超短期预测数据
      */
-    private Map<String, List<DataSuperShortTermForecastIssued>> groupForecastByYear(
-            List<DataSuperShortTermForecastIssued> records)
+    private Map<String, List<ForecastPowerAverage>> groupForecastByYear(
+            List<ForecastPowerAverage> records)
     {
         return records.stream()
                 .filter(r -> r.getForecastTime() != null)
@@ -681,101 +590,53 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
                         Collectors.toList()));
     }
 
-    /**
-     * 按start_time秒分组超短期预报数据
+        /**
+     * 安全解析Long,支持纯数字时间戳或日期字符串(yyyy-MM-dd、yyyy-MM-dd HH:mm:ss、yyyy-MM)。
+     * 输入的日期字符串均为 GMT+8 时间,解析为 UTC epoch seconds 供查询使用。
      */
-    private Map<Long, List<DataUltraShortTermPowerForecasting>> groupUltraForecastByStartSec(
-            List<DataUltraShortTermPowerForecasting> records)
+    private Long parseLong(String value)
     {
-        Map<Long, List<DataUltraShortTermPowerForecasting>> map = new LinkedHashMap<>();
-        if (records == null) return map;
-        for (DataUltraShortTermPowerForecasting r : records)
+        if (value == null || value.isEmpty()) return 0L;
+        String trimmed = value.trim();
+        // 先尝试直接解析为数字时间戳
+        try
         {
-            if (r.getStartTime() != null)
-            {
-                map.computeIfAbsent(r.getStartTime(), k -> new ArrayList<>()).add(r);
-            }
+            return Long.parseLong(trimmed);
         }
-        return map;
-    }
-
-    /**
-     * 收集某日对应的超短期预报记录
-     */
-    private List<DataUltraShortTermPowerForecasting> collectUltraForDay(
-            String day, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
-    {
-        LocalDate ld = LocalDate.parse(day, DATE_FMT);
-        long dayStartSec = ld.atStartOfDay(ZONE_SHANGHAI).toInstant().getEpochSecond();
-        return collectByStartSec(dayStartSec, SECONDS_PER_DAY, ultraMap);
-    }
-
-    /**
-     * 收集某月对应的超短期预报记录
-     */
-    private List<DataUltraShortTermPowerForecasting> collectUltraForMonth(
-            String month, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
-    {
-        YearMonth ym = YearMonth.parse(month, MONTH_FMT);
-        LocalDate startDate = ym.atDay(1);
-        LocalDate endDate = ym.atEndOfMonth();
-        long startSec = startDate.atStartOfDay(ZONE_SHANGHAI).toInstant().getEpochSecond();
-        long endSec = endDate.atTime(23, 59, 59).atZone(ZONE_SHANGHAI).toInstant().getEpochSecond();
-        long duration = endSec - startSec;
-        return collectByStartSecRange(startSec, duration, ultraMap);
-    }
-
-    /**
-     * 收集某年对应的超短期预报记录
-     */
-    private List<DataUltraShortTermPowerForecasting> collectUltraForYear(
-            String year, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
-    {
-        int y = Integer.parseInt(year);
-        LocalDate startDate = LocalDate.of(y, 1, 1);
-        LocalDate endDate = LocalDate.of(y, 12, 31);
-        long startSec = startDate.atStartOfDay(ZONE_SHANGHAI).toInstant().getEpochSecond();
-        long endSec = endDate.atTime(23, 59, 59).atZone(ZONE_SHANGHAI).toInstant().getEpochSecond();
-        long duration = endSec - startSec;
-        return collectByStartSecRange(startSec, duration, ultraMap);
-    }
-
-    /**
-     * 根据start_time范围收集超短期预报记录
-     */
-    private List<DataUltraShortTermPowerForecasting> collectByStartSec(
-            long startSec, long duration, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
-    {
-        long endSec = startSec + duration;
-        return ultraMap.keySet().stream()
-                .filter(k -> k >= startSec && k < endSec)
-                .flatMap(k -> ultraMap.get(k).stream())
-                .collect(Collectors.toList());
-    }
-
-    private List<DataUltraShortTermPowerForecasting> collectByStartSecRange(
-            long startSec, long duration, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
-    {
-        long endSec = startSec + duration;
-        List<DataUltraShortTermPowerForecasting> result = new ArrayList<>();
-        for (long k = startSec; k < endSec; k += SECONDS_PER_DAY)
+        catch (NumberFormatException e)
         {
-            List<DataUltraShortTermPowerForecasting> dayRecords = collectByStartSec(k, SECONDS_PER_DAY, ultraMap);
-            result.addAll(dayRecords);
+            // 尝试作为日期字符串解析为 UTC epoch seconds
+            try
+            {
+                // yyyy-MM-dd HH:mm:ss
+                ZonedDateTime zdt = LocalDateTime.parse(trimmed, DATE_TIME_FMT)
+                        .atZone(ZONE_SHANGHAI);
+                return zdt.toInstant().getEpochSecond();
+            }
+            catch (DateTimeParseException e2)
+            {
+                // yyyy-MM-dd
+                try
+                {
+                    ZonedDateTime zdt = LocalDate.parse(trimmed, DATE_FMT)
+                            .atStartOfDay(ZONE_SHANGHAI);
+                    return zdt.toInstant().getEpochSecond();
+                }
+                catch (DateTimeParseException e3)
+                {
+                    // yyyy-MM(月末)
+                    try
+                    {
+                        YearMonth ym = YearMonth.parse(trimmed, MONTH_FMT);
+                        ZonedDateTime zdt = ym.atDay(ym.lengthOfMonth()).atTime(23, 59, 59).atZone(ZONE_SHANGHAI);
+                        return zdt.toInstant().getEpochSecond();
+                    }
+                    catch (DateTimeParseException e4)
+                    {
+                        return 0L;
+                    }
+                }
+            }
         }
-        return result;
-    }
-
-    /**
-     * 查询超短期预测功率数据列表
-     */
-    private List<DataSuperShortTermForecastIssued> selectSuperShortByTimeRange(
-            String entityId, Long startTime, Long endTime)
-    {
-        DataSuperShortTermForecastIssued query = new DataSuperShortTermForecastIssued();
-        query.setEntityId(entityId);
-        query.setForecastStartTime(startTime);
-        query.setForecastEndTime(endTime);
-        return superShortService.selectDataSuperShortTermForecastIssuedList(query);
     }
 }