Browse Source

Merge remote-tracking branch 'origin/chart/14.9'

Roc 1 year ago
parent
commit
f2dd8db192
2 changed files with 358 additions and 14 deletions
  1. 24 0
      services/data/predict_edb_info.go
  2. 334 14
      services/data/predict_edb_info_rule.go

+ 24 - 0
services/data/predict_edb_info.go

@@ -341,8 +341,32 @@ func GetChartPredictEdbInfoDataListByConfList(predictEdbConfList []data_manage.P
 			if err != nil {
 				return
 			}
+
+		case 15: //15:N年均值:过去N年同期均值。过去N年可以连续或者不连续,指标数据均用线性插值补全为日度数据后计算;
+			predictEdbInfoData, tmpMinValue, tmpMaxValue, err = GetChartPredictEdbInfoDataListByRuleNAnnualAverage(predictEdbConf.PredictEdbInfoId, predictEdbConf.Value, dayList, realPredictEdbInfoData, predictEdbInfoData, existMap)
+			if err != nil {
+				return
+			}
+
+		case 16: //16:年度值倒推
+			predictEdbInfoData, tmpMinValue, tmpMaxValue, err = GetChartPredictEdbInfoDataListByRuleAnnualValueInversion(predictEdbConf.PredictEdbInfoId, predictEdbConf.Value, dayList, frequency, realPredictEdbInfoData, predictEdbInfoData, existMap)
+			if err != nil {
+				return
+			}
+		}
+
+		// 下一个规则的开始日期
+		{
+			lenPredictEdbInfoData := len(predictEdbInfoData)
+			if lenPredictEdbInfoData > 0 {
+				tmpDataEndTime, _ := time.ParseInLocation(utils.FormatDate, predictEdbInfoData[lenPredictEdbInfoData-1].DataTime, time.Local)
+				if startDate.Before(tmpDataEndTime) {
+					startDate = tmpDataEndTime
+				}
+			}
 		}
 		//startDate = dataEndTime.AddDate(0, 0, 1)
+
 		if startDate.Before(dataEndTime) {
 			startDate = dataEndTime
 		}

+ 334 - 14
services/data/predict_edb_info_rule.go

@@ -1,6 +1,7 @@
 package data
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 	"github.com/nosixtools/solarlunar"
@@ -139,24 +140,21 @@ func GetChartPredictEdbInfoDataListByRuleTb(edbInfoId int, tbValue float64, dayL
 // @params a float64 去年同期值
 // @params b float64 固定同比增速
 func TbzDivMul(a, b float64) (result float64) {
-	if b != 0 {
-		// 去年同期值
-		af := decimal.NewFromFloat(a)
+	// 去年同期值
+	af := decimal.NewFromFloat(a)
 
-		// 同比增速
-		bf := decimal.NewFromFloat(b)
+	// 同比增速
+	bf := decimal.NewFromFloat(b)
 
-		// 默认1
-		cf := decimal.NewFromFloat(1)
+	// 默认1
+	cf := decimal.NewFromFloat(1)
 
-		// 总增速
-		val := bf.Add(cf)
+	// 总增速
+	val := bf.Add(cf)
 
-		// 计算
-		result, _ = val.Mul(af).RoundCeil(4).Float64()
-	} else {
-		result = 0
-	}
+	// 计算
+	result, _ = val.Mul(af).RoundCeil(4).Float64()
+	
 	return
 }
 
@@ -1490,3 +1488,325 @@ func handleNhccData(dataList []*data_manage.EdbDataList, moveDay int) (newDataLi
 
 	return
 }
+
+// GetChartPredictEdbInfoDataListByRuleNAnnualAverage 根据 N年均值 规则获取预测数据
+// ETA预测规则:N年均值:过去N年同期均值。过去N年可以连续或者不连续,指标数据均用线性插值补全为日度数据后计算;
+func GetChartPredictEdbInfoDataListByRuleNAnnualAverage(edbInfoId int, configValue string, dayList []time.Time, realPredictEdbInfoData, predictEdbInfoData []*data_manage.EdbDataList, existMap map[string]float64) (newPredictEdbInfoData []*data_manage.EdbDataList, minValue, maxValue float64, err error) {
+	// 获取配置的年份列表
+	yearList, _, err := getYearListBySeasonConf(configValue)
+	if err != nil {
+		return
+	}
+
+	allDataList := make([]*data_manage.EdbDataList, 0)
+	allDataList = append(allDataList, realPredictEdbInfoData...)
+	allDataList = append(allDataList, predictEdbInfoData...)
+	newPredictEdbInfoData = predictEdbInfoData
+
+	// 插值法数据处理
+	handleDataMap := make(map[string]float64)
+	err = handleDataByLinearRegression(allDataList, handleDataMap)
+	if err != nil {
+		return
+	}
+
+	index := len(allDataList)
+	//获取后面的预测数据
+	predictEdbInfoData = make([]*data_manage.EdbDataList, 0)
+	for k, currentDate := range dayList {
+		// 如果遇到闰二月,如2.29,去掉该天数据
+		if strings.Contains(currentDate.Format(utils.FormatDate), "02-29") {
+			continue
+		}
+		tmpK := len(allDataList) - 1     //上1期数据的下标
+		lastDayData := allDataList[tmpK] // 上1期的数据
+
+		tmpHistoryVal := decimal.NewFromFloat(0) //往期的差值总和
+		tmpHistoryValNum := 0                    // 往期差值计算的数量
+		for _, year := range yearList {
+			//前几年当日的日期
+			tmpHistoryCurrentDate := currentDate.AddDate(year-currentDate.Year(), 0, 0)
+			if val, ok := handleDataMap[tmpHistoryCurrentDate.Format(utils.FormatDate)]; ok {
+				tmpHistoryVal = tmpHistoryVal.Add(decimal.NewFromFloat(val))
+				tmpHistoryValNum++
+			}
+		}
+
+		//计算的差值与选择的年份数量不一致,那么当前日期不计算
+		if tmpHistoryValNum != len(yearList) {
+			continue
+		}
+		val, _ := tmpHistoryVal.Div(decimal.NewFromInt(int64(tmpHistoryValNum))).RoundCeil(4).Float64()
+
+		currentDateStr := currentDate.Format(utils.FormatDate)
+		tmpData := &data_manage.EdbDataList{
+			EdbDataId:     edbInfoId + 100000 + index + k,
+			EdbInfoId:     edbInfoId,
+			DataTime:      currentDateStr,
+			Value:         val,
+			DataTimestamp: currentDate.UnixNano() / 1e6,
+		}
+		newPredictEdbInfoData = append(newPredictEdbInfoData, tmpData)
+		allDataList = append(allDataList, tmpData)
+		existMap[currentDateStr] = val
+
+		// 继续使用插值法补充新预测日期的数据之间的值
+		err = handleDataByLinearRegression([]*data_manage.EdbDataList{
+			lastDayData, tmpData,
+		}, handleDataMap)
+		if err != nil {
+			return
+		}
+
+		// 最大最小值
+		if val < minValue {
+			minValue = val
+		}
+		if val > maxValue {
+			maxValue = val
+		}
+	}
+	return
+}
+
+// AnnualValueInversionConf 年度值倒推规则
+type AnnualValueInversionConf struct {
+	Value float64 `description:"年度值"`
+	Type  int     `description:"分配方式,1:均值法;2:同比法"`
+	Year  int     `description:"同比年份"`
+}
+
+// GetChartPredictEdbInfoDataListByRuleAnnualValueInversion 根据 年度值倒推 规则获取预测数据
+// ETA预测规则:年度值倒推:设定年度值,余额=年度值-年初至今累计值(算法参考累计值),进行余额分配,均值法分配时保证每期数值相等(日度/周度:剩余期数=剩余自然日历天数/今年指标最新日期自然日历天数*今年至今指标数据期数;旬度/月度/季度/半年度:剩余期数=全年期数(36\12\4\2)-今年至今自然日历期数),同比法保证每期同比相等(同比增速=余额/同比年份相应日期的余额,预测值等于同比年份同期值*同比增速)
+// 举例:
+// 指标A 日度 最新日期 2023-05-19 年初至今累计值100
+// 设置年度值1000
+// 则余额=1000-100=900
+// 均值法分配:剩余期数=226/139*120=195.11
+// 今年之后的每一期预测值=900/195.11=4.6128
+// 同比法分配:同比增速=900/同比年份5.19的余额
+// 预测值=同比年份5-20的值*(1+同比增速)
+func GetChartPredictEdbInfoDataListByRuleAnnualValueInversion(edbInfoId int, configValue string, dayList []time.Time, frequency string, realPredictEdbInfoData, predictEdbInfoData []*data_manage.EdbDataList, existMap map[string]float64) (newPredictEdbInfoData []*data_manage.EdbDataList, minValue, maxValue float64, err error) {
+	if frequency == "年度" {
+		err = errors.New("当前指标频度是年度,不允许配置年度值倒推")
+		return
+	}
+
+	// 获取配置
+	var annualValueInversionConf AnnualValueInversionConf
+	err = json.Unmarshal([]byte(configValue), &annualValueInversionConf)
+	if err != nil {
+		err = errors.New("年度值倒推配置信息异常:" + err.Error())
+		return
+	}
+
+	allDataList := make([]*data_manage.EdbDataList, 0)
+	allDataList = append(allDataList, realPredictEdbInfoData...)
+	allDataList = append(allDataList, predictEdbInfoData...)
+	newPredictEdbInfoData = predictEdbInfoData
+	index := len(allDataList)
+
+	// 配置的年度值
+	yearValueConfig := annualValueInversionConf.Value
+
+	// 当前年的日期
+	currDayTime := dayList[0]
+	lastDayTime := dayList[len(dayList)-1]
+	if currDayTime.Year() != lastDayTime.Year() {
+		err = errors.New("年度值倒推不支持跨年预测")
+		return
+	}
+
+	// 均值法
+	if annualValueInversionConf.Type == 1 {
+		// 当前年的期数
+		currYearN := 0
+		var currYearVal float64
+		for _, v := range allDataList {
+			currTime, tmpErr := time.ParseInLocation(utils.FormatDate, v.DataTime, time.Local)
+			if tmpErr != nil {
+				err = tmpErr
+				return
+			}
+			// 只是计算今年的
+			if currTime.Year() != currDayTime.Year() {
+				continue
+			}
+			currYearN++
+			currYearVal = currYearVal + v.Value
+		}
+
+		var averageVal float64
+		switch frequency {
+		case "半年度":
+			averageVal, _ = (decimal.NewFromFloat(yearValueConfig).Sub(decimal.NewFromFloat(currYearVal))).Div(decimal.NewFromInt(int64(2 - currYearN))).Float64()
+		case "季度":
+			averageVal, _ = (decimal.NewFromFloat(yearValueConfig).Sub(decimal.NewFromFloat(currYearVal))).Div(decimal.NewFromInt(int64(4 - currYearN))).Float64()
+		case "月度":
+			averageVal, _ = (decimal.NewFromFloat(yearValueConfig).Sub(decimal.NewFromFloat(currYearVal))).Div(decimal.NewFromInt(int64(12 - currYearN))).Float64()
+		case "旬度":
+			averageVal, _ = (decimal.NewFromFloat(yearValueConfig).Sub(decimal.NewFromFloat(currYearVal))).Div(decimal.NewFromInt(int64(36 - currYearN))).Float64()
+		case "周度", "日度":
+			//剩余期数=剩余自然日历天数/今年指标最新日期自然日历天数*今年至今指标数据期数
+
+			// 当前年的第一天
+			yearFirstDay := time.Date(currDayTime.Year(), 1, 1, 0, 0, 0, 0, time.Local)
+			subDay := utils.GetTimeSubDay(yearFirstDay, currDayTime) + 1
+
+			// 当前年的最后一天
+			yearLastDay := time.Date(currDayTime.Year(), 12, 31, 0, 0, 0, 0, time.Local)
+			subDay2 := utils.GetTimeSubDay(yearFirstDay, yearLastDay) + 1
+
+			surplusN := decimal.NewFromInt(int64(subDay2 - subDay)).Div(decimal.NewFromInt(int64(subDay))).Mul(decimal.NewFromInt(int64(currYearN)))
+			averageVal, _ = (decimal.NewFromFloat(annualValueInversionConf.Value).Sub(decimal.NewFromFloat(currYearVal))).Div(surplusN).Round(4).Float64()
+
+		}
+
+		// 保留四位小数
+		averageVal, _ = decimal.NewFromFloat(averageVal).Round(4).Float64()
+
+		for k, currentDate := range dayList {
+			currentDateStr := currentDate.Format(utils.FormatDate)
+			tmpData := &data_manage.EdbDataList{
+				EdbDataId:     edbInfoId + 100000 + index + k,
+				EdbInfoId:     edbInfoId,
+				DataTime:      currentDateStr,
+				Value:         averageVal,
+				DataTimestamp: currentDate.UnixNano() / 1e6,
+			}
+			newPredictEdbInfoData = append(newPredictEdbInfoData, tmpData)
+			allDataList = append(allDataList, tmpData)
+			existMap[currentDateStr] = averageVal
+		}
+
+		// 最大最小值
+		if averageVal < minValue {
+			minValue = averageVal
+		}
+		if averageVal > maxValue {
+			maxValue = averageVal
+		}
+
+		return
+	}
+
+	// 同比法分配
+	// 同比法保证每期同比相等(同比增速=余额/同比年份相应日期的余额,预测值等于同比年份同期值*同比增速);
+	// 同比法分配:同比增速=900/同比年份5.19的余额
+
+	// 每年截止到当前日期的累计值
+	dateTotalMap := make(map[time.Time]float64)
+	// 每年的累计值(计算使用)
+	yearTotalMap := make(map[int]float64)
+	for _, v := range allDataList {
+		currTime, tmpErr := time.ParseInLocation(utils.FormatDate, v.DataTime, time.Local)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+		yearVal := yearTotalMap[currTime.Year()]
+		yearVal = yearVal + v.Value
+		yearTotalMap[currTime.Year()] = yearVal
+		dateTotalMap[currTime] = yearVal
+	}
+
+	//(同比增速=余额/同比年份相应日期的余额,预测值等于同比年份同期值*同比增速);
+	for k, currentDate := range dayList {
+		currYearBalance := yearValueConfig - yearTotalMap[currentDate.Year()] // 当年的余额
+		//同比年份相应日期
+		lastYear := annualValueInversionConf.Year + (currentDate.Year() - currDayTime.Year())
+
+		var lastDateTime time.Time
+
+		switch frequency {
+		case "半年度", "季度":
+			lastDateTime = time.Date(lastYear, currentDate.Month(), currentDate.Day(), 0, 0, 0, 0, currentDate.Location())
+		case "月度":
+			lastDateTime = time.Date(lastYear, currentDate.Month()+1, 1, 0, 0, 0, 0, currentDate.Location()).AddDate(0, 0, -1)
+		case "旬度":
+			if currentDate.Day() == 10 || currentDate.Day() == 20 {
+				lastDateTime = time.Date(lastYear, currentDate.Month(), currentDate.Day(), 0, 0, 0, 0, currentDate.Location())
+			} else {
+				lastDateTime = time.Date(lastYear, currentDate.Month()+1, 1, 0, 0, 0, 0, currentDate.Location()).AddDate(0, 0, -1)
+			}
+		case "周度", "日度":
+			lastDateTime = time.Date(lastYear, currentDate.Month(), currentDate.Day(), 0, 0, 0, 0, currentDate.Location())
+		}
+
+		// 同比年份相应日期的累计值
+		var dateTotal float64
+		dateTotal, ok := dateTotalMap[lastDateTime]
+		if !ok { //如果没有找到这个日期,那么就往前面找,一直到找到这个累计值,或者找完这一年
+			yearFirstDayTime := time.Date(lastDateTime.Year(), 1, 1, 0, 0, 0, 0, lastDateTime.Location())
+			for tmpDateTime := lastDateTime.AddDate(0, 0, -1); tmpDateTime.After(yearFirstDayTime) || tmpDateTime.Equal(yearFirstDayTime); tmpDateTime = tmpDateTime.AddDate(0, 0, -1) {
+				dateTotal, ok = dateTotalMap[tmpDateTime]
+				if ok {
+					break
+				}
+			}
+		}
+
+		//同比年份相应日期的余额
+		lastYearDateBalance := yearValueConfig - dateTotal
+
+		// 同比增速=余额/同比年份相应日期的余额
+		tbVal := decimal.NewFromFloat(currYearBalance).Div(decimal.NewFromFloat(lastYearDateBalance))
+
+		// 获取同比年份同期值,获取失败的话,就不处理
+		if lastDateVal, ok := existMap[lastDateTime.Format(utils.FormatDate)]; ok {
+			//预测值 = 同比年份同期值*同比增速
+			tmpVal, _ := decimal.NewFromFloat(lastDateVal).Mul(tbVal).Round(4).Float64()
+			currentDateStr := currentDate.Format(utils.FormatDate)
+			tmpData := &data_manage.EdbDataList{
+				EdbDataId:     edbInfoId + 100000 + index + k,
+				EdbInfoId:     edbInfoId,
+				DataTime:      currentDateStr,
+				Value:         tmpVal,
+				DataTimestamp: currentDate.UnixNano() / 1e6,
+			}
+			newPredictEdbInfoData = append(newPredictEdbInfoData, tmpData)
+			allDataList = append(allDataList, tmpData)
+			existMap[currentDateStr] = tmpVal
+
+			yearVal := yearTotalMap[currentDate.Year()]
+			yearVal = yearVal + tmpVal
+			yearTotalMap[currentDate.Year()] = yearVal
+			dateTotalMap[currentDate] = yearVal
+
+			// 最大最小值
+			if tmpVal < minValue {
+				minValue = tmpVal
+			}
+			if tmpVal > maxValue {
+				maxValue = tmpVal
+			}
+		}
+	}
+
+	return
+}
+
+// getYearListBySeasonConf 根据配置获取年份列表
+func getYearListBySeasonConf(configValue string) (yearList []int, seasonConf SeasonConf, err error) {
+	tmpErr := json.Unmarshal([]byte(configValue), &seasonConf)
+	if tmpErr != nil {
+		err = errors.New("年份配置信息异常:" + tmpErr.Error())
+		return
+	}
+	//选择方式,1:连续N年;2:指定年份
+	if seasonConf.YearType == 1 {
+		if seasonConf.NValue < 1 {
+			err = errors.New("连续N年不允许小于1")
+			return
+		}
+
+		currYear := time.Now().Year()
+		for i := 0; i < seasonConf.NValue; i++ {
+			yearList = append(yearList, currYear-i-1)
+		}
+	} else {
+		yearList = seasonConf.YearList
+	}
+
+	return
+}