Browse Source

feat:预测规则新增季节性预测规则和移动平均同比预测规则

Roc 2 years ago
parent
commit
9517fdc8cf
3 changed files with 393 additions and 0 deletions
  1. 42 0
      services/data/predict_edb_info.go
  2. 328 0
      services/data/predict_edb_info_rule.go
  3. 23 0
      utils/common.go

+ 42 - 0
services/data/predict_edb_info.go

@@ -1,6 +1,7 @@
 package data
 
 import (
+	"encoding/json"
 	"errors"
 	"github.com/shopspring/decimal"
 	"hongze/hongze_chart_lib/models"
@@ -319,6 +320,47 @@ func GetChartPredictEdbInfoDataListByConfList(predictEdbConfList []*data_manage.
 			}
 			finalValue, _ := tmpValDecimal.Float64()
 			predictEdbInfoData, tmpMinValue, tmpMaxValue = GetChartPredictEdbInfoDataListByRuleFinalValueHc(predictEdbConf.PredictEdbInfoId, finalValue, startDate, dataEndTime, frequency, realPredictEdbInfoData, predictEdbInfoData, existMap)
+		case 11: //11:根据 季节性 规则获取预测数据
+			var seasonConf SeasonConf
+			tmpErr := json.Unmarshal([]byte(predictEdbConf.Value), &seasonConf)
+			if tmpErr != nil {
+				err = errors.New("季节性配置信息异常:" + tmpErr.Error())
+				return
+			}
+			calendar := "公历"
+			if seasonConf.Calendar == "农历" {
+				calendar = "农历"
+			}
+			yearList := make([]int, 0)
+			//选择方式,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
+			}
+			predictEdbInfoData, tmpMinValue, tmpMaxValue, err = GetChartPredictEdbInfoDataListByRuleSeason(predictEdbConf.PredictEdbInfoId, yearList, calendar, startDate, dataEndTime, frequency, realPredictEdbInfoData, predictEdbInfoData, existMap)
+			if err != nil {
+				return
+			}
+		case 12: //12:根据 移动平均同比 规则获取预测数据
+			var moveAverageConf MoveAverageConf
+			tmpErr := json.Unmarshal([]byte(predictEdbConf.Value), &moveAverageConf)
+			if tmpErr != nil {
+				err = errors.New("季节性配置信息异常:" + tmpErr.Error())
+				return
+			}
+			predictEdbInfoData, tmpMinValue, tmpMaxValue, err = GetChartPredictEdbInfoDataListByRuleMoveAverageTb(predictEdbConf.PredictEdbInfoId, moveAverageConf.NValue, moveAverageConf.Year, startDate, dataEndTime, frequency, realPredictEdbInfoData, predictEdbInfoData, existMap)
+			if err != nil {
+				return
+			}
 		}
 		//startDate = dataEndTime.AddDate(0, 0, 1)
 		if startDate.Before(dataEndTime) {

+ 328 - 0
services/data/predict_edb_info_rule.go

@@ -1,6 +1,9 @@
 package data
 
 import (
+	"errors"
+	"fmt"
+	"github.com/nosixtools/solarlunar"
 	"github.com/shopspring/decimal"
 	"hongze/hongze_chart_lib/models"
 	"hongze/hongze_chart_lib/models/data_manage"
@@ -720,3 +723,328 @@ func GetChartPredictEdbInfoDataListByRuleFinalValueHc(edbInfoId int, finalValue
 	}
 	return
 }
+
+// SeasonConf 季节性规则的配置
+type SeasonConf struct {
+	Calendar string `description:"公历、农历"`
+	YearType int    `description:"选择方式,1:连续N年;2:指定年份"`
+	NValue   int    `description:"连续N年"`
+	YearList []int  `description:"指定年份列表"`
+}
+
+//	GetChartPredictEdbInfoDataListByRuleSeason 根据 季节性 规则获取预测数据
+// ETA预测规则:季节性
+//已知选定指标A最近更新日期: 2022-12-6  200
+//设置预测截止日期2023-01-06
+//1、选择过去N年,N=3
+//则过去N年为2021、2020、2019
+//指标A日期	实际值	指标A日期
+//2019/12/5	150	2019/12/6
+//2020/12/5	180	2020/12/6
+//2021/12/5	210	2021/12/6
+//2019/12/31	200	2020/1/1
+//2020/12/31	210	2021/1/1
+//2021/12/31	250	2022/1/1
+//
+//计算12.7预测值,求过去N年环差均值=[(100-150)+(160-180)+(250-210)]/3=-10
+//则12.7预测值=12.6值+过去N年环差均值=200-10=190
+//以此类推...
+//
+//计算2023.1.2预测值,求过去N年环差均值=[(300-200)+(220-210)+(260-250)]/3=40
+//则2023.1.2预测值=2023.1.1值+过去N年环差均值
+func GetChartPredictEdbInfoDataListByRuleSeason(edbInfoId int, yearsList []int, calendar string, startDate, endDate time.Time, frequency string, realPredictEdbInfoData, predictEdbInfoData []*models.EdbDataList, existMap map[string]float64) (newPredictEdbInfoData []*models.EdbDataList, minValue, maxValue float64, err error) {
+	allDataList := make([]*models.EdbDataList, 0)
+	allDataList = append(allDataList, realPredictEdbInfoData...)
+	allDataList = append(allDataList, predictEdbInfoData...)
+	newPredictEdbInfoData = predictEdbInfoData
+
+	// 获取每个年份的日期数据需要平移的天数
+	moveDayMap := make(map[int]int, 0) // 每个年份的春节公历
+	{
+		if calendar == "公历" {
+			for _, year := range yearsList {
+				moveDayMap[year] = 0 //公历就不平移了
+			}
+		} else {
+			currentDay := time.Now()
+			if currentDay.Month() >= 11 { //如果大于等于11月份,那么用的是下一年的春节
+				currentDay = currentDay.AddDate(1, 0, 0)
+			}
+
+			currentYear := currentDay.Year()
+			currentYearCjnl := fmt.Sprintf("%d-01-01", currentYear)            //当年的春节农历
+			currentYearCjgl := solarlunar.LunarToSolar(currentYearCjnl, false) //当年的春节公历
+			currentYearCjglTime, tmpErr := time.ParseInLocation(utils.FormatDate, currentYearCjgl, time.Local)
+			if tmpErr != nil {
+				err = errors.New("当前春节公历日期转换失败:" + tmpErr.Error())
+				return
+			}
+
+			// 指定的年份
+			for _, year := range yearsList {
+				tmpYearCjnl := fmt.Sprintf("%d-01-01", year)               //指定年的春节农历
+				tmpYearCjgl := solarlunar.LunarToSolar(tmpYearCjnl, false) //指定年的春节公历
+				//moveDayList = append(moveDayList, 0) //公历就不平移了
+
+				tmpYearCjglTime, tmpErr := time.ParseInLocation(utils.FormatDate, tmpYearCjgl, time.Local)
+				if tmpErr != nil {
+					err = errors.New(fmt.Sprintf("%d公历日期转换失败:%s", year, tmpErr.Error()))
+					return
+				}
+
+				tmpCurrentYearCjglTime := currentYearCjglTime.AddDate(year-currentYear, 0, 0)
+				moveDay := utils.GetTimeSubDay(tmpYearCjglTime, tmpCurrentYearCjglTime)
+				moveDayMap[year] = moveDay //公历平移
+			}
+		}
+	}
+
+	index := len(allDataList)
+	//获取后面的预测日期
+	dayList := getPredictEdbDayList(startDate, endDate, frequency)
+
+	//获取后面的预测数据
+	predictEdbInfoData = make([]*models.EdbDataList, 0)
+	for k, currentDate := range dayList {
+		tmpHistoryVal := decimal.NewFromFloat(0) //往期的差值总和
+		tmpHistoryValNum := 0                    // 往期差值计算的数量
+
+		tmpLenAllDataList := len(allDataList)
+		tmpK := tmpLenAllDataList - 1 //上1期数据的下标
+		lastDayStr := allDataList[tmpK].DataTime
+		lastDayVal := allDataList[tmpK].Value
+		lastDay, tmpErr := time.ParseInLocation(utils.FormatDate, lastDayStr, time.Local)
+		if tmpErr != nil {
+			err = errors.New("获取上期日期转换失败:" + tmpErr.Error())
+		}
+		for _, year := range yearsList {
+			moveDay := moveDayMap[year] //需要移动的天数
+			var tmpHistoryCurrentVal, tmpHistoryLastVal float64
+			var isFindHistoryCurrent, isFindHistoryLast bool //是否找到前几年的数据
+
+			//前几年当日的日期
+			tmpHistoryCurrentDate := currentDate.AddDate(year-currentDate.Year(), 0, moveDay)
+			for i := 0; i <= 35; i++ { // 前后35天找数据,找到最近的值,先向后面找,再往前面找
+				tmpDate := tmpHistoryCurrentDate.AddDate(0, 0, i)
+				if val, ok := existMap[tmpDate.Format(utils.FormatDate)]; ok {
+					tmpHistoryCurrentVal = val
+					isFindHistoryCurrent = true
+					break
+				} else {
+					tmpDate := tmpHistoryCurrentDate.AddDate(0, 0, -i)
+					if val, ok := existMap[tmpDate.Format(utils.FormatDate)]; ok {
+						tmpHistoryCurrentVal = val
+						isFindHistoryCurrent = true
+						break
+					}
+				}
+			}
+
+			//前几年上一期的日期
+			tmpHistoryLastDate := lastDay.AddDate(year-lastDay.Year(), 0, moveDay)
+			for i := 0; i <= 35; i++ { // 前后35天找数据,找到最近的值,先向后面找,再往前面找
+				tmpDate := tmpHistoryLastDate.AddDate(0, 0, i)
+				if val, ok := existMap[tmpDate.Format(utils.FormatDate)]; ok {
+					tmpHistoryLastVal = val
+					isFindHistoryLast = true
+					break
+				} else {
+					tmpDate := tmpHistoryLastDate.AddDate(0, 0, -i)
+					if val, ok := existMap[tmpDate.Format(utils.FormatDate)]; ok {
+						tmpHistoryLastVal = val
+						isFindHistoryLast = true
+						break
+					}
+				}
+			}
+
+			// 如果两个日期对应的数据都找到了,那么计算两期的差值
+			if isFindHistoryCurrent && isFindHistoryLast {
+				af := decimal.NewFromFloat(tmpHistoryCurrentVal)
+				bf := decimal.NewFromFloat(tmpHistoryLastVal)
+				tmpHistoryVal = tmpHistoryVal.Add(af.Sub(bf))
+				tmpHistoryValNum++
+			}
+		}
+
+		//计算的差值与选择的年份数量不一致,那么当前日期不计算
+		if tmpHistoryValNum != len(yearsList) {
+			continue
+		}
+		lastDayValDec := decimal.NewFromFloat(lastDayVal)
+		val, _ := tmpHistoryVal.Div(decimal.NewFromInt(int64(tmpHistoryValNum))).Add(lastDayValDec).RoundCeil(4).Float64()
+
+		currentDateStr := currentDate.Format(utils.FormatDate)
+		tmpData := &models.EdbDataList{
+			EdbDataId:     edbInfoId + 10000000000 + index + k,
+			EdbInfoId:     edbInfoId,
+			DataTime:      currentDateStr,
+			Value:         val,
+			DataTimestamp: (currentDate.UnixNano() / 1e6) + 1000, //前端需要让加1s,说是2022-09-01 00:00:00 这样的整点不合适
+		}
+		newPredictEdbInfoData = append(newPredictEdbInfoData, tmpData)
+		allDataList = append(allDataList, tmpData)
+		existMap[currentDateStr] = val
+
+		// 最大最小值
+		if val < minValue {
+			minValue = val
+		}
+		if val > maxValue {
+			maxValue = val
+		}
+	}
+	return
+}
+
+// MoveAverageConf 移动平均同比规则的配置
+type MoveAverageConf struct {
+	Year   int `description:"指定年份"`
+	NValue int `description:"N期的数据"`
+}
+
+//	GetChartPredictEdbInfoDataListByRuleMoveAverageTb 根据 移动平均同比 规则获取预测数据
+// ETA预测规则:季节性
+//2、选择指定N年,N=3
+//指定N年为2012、2015、2018
+//指标A日期	实际值	指标A日期	实际值
+//2012/12/5	150	2012/12/6	130
+//2015/12/5	180	2015/12/6	150
+//2018/12/5	210	2018/12/6	260
+//2012/12/31	200	2013/1/1	200
+//2015/12/31	210	2016/1/1	250
+//2018/12/31	250	2019/1/1	270
+//计算12.7预测值,求过去N年环差均值=[(130-150)+(150-180)+(290-210)]/3=10
+//则12.7预测值=12.6值+过去N年环差均值=200+10=210
+//以此类推...
+//计算2023.1.2预测值,求过去N年环差均值=[(200-200)+(250-210)+(270-250)]/3=16.67
+//则2023.1.2预测值=2023.1.1值+过去N年环差均值
+func GetChartPredictEdbInfoDataListByRuleMoveAverageTb(edbInfoId int, nValue, year int, startDate, endDate time.Time, frequency string, realPredictEdbInfoData, predictEdbInfoData []*models.EdbDataList, existMap map[string]float64) (newPredictEdbInfoData []*models.EdbDataList, minValue, maxValue float64, err error) {
+	allDataList := make([]*models.EdbDataList, 0)
+	allDataList = append(allDataList, realPredictEdbInfoData...)
+	allDataList = append(allDataList, predictEdbInfoData...)
+	newPredictEdbInfoData = predictEdbInfoData
+
+	lenAllData := len(allDataList)
+	if lenAllData < nValue || lenAllData <= 0 {
+		return
+	}
+	if nValue <= 0 {
+		return
+	}
+	// 分母
+	decimalN := decimal.NewFromInt(int64(nValue))
+
+	//获取后面的预测数据
+	dayList := getPredictEdbDayList(startDate, endDate, frequency)
+	for k, currentDate := range dayList {
+		tmpLenAllDataList := len(allDataList)
+		tmpIndex := tmpLenAllDataList - 1 //上1期数据的下标
+
+		averageDateList := make([]string, 0) //计算平均数的日期
+
+		// 数据集合中的最后一个数据
+		tmpDecimalVal := decimal.NewFromFloat(allDataList[tmpIndex].Value)
+		averageDateList = append(averageDateList, allDataList[tmpIndex].DataTime)
+		for tmpK := 2; tmpK <= nValue; tmpK++ {
+			tmpIndex2 := tmpIndex - tmpK //上N期的值
+			tmpDecimalVal2 := decimal.NewFromFloat(allDataList[tmpIndex2].Value)
+			tmpDecimalVal = tmpDecimalVal.Add(tmpDecimalVal2)
+			averageDateList = append(averageDateList, allDataList[tmpIndex2].DataTime)
+		}
+		// 最近的N期平均值
+		tmpAverageVal := tmpDecimalVal.Div(decimalN)
+
+		var tmpHistoryCurrentVal float64                 // 前几年当日的数据值
+		var isFindHistoryCurrent, isFindHistoryLast bool //是否找到前几年的数据
+		tmpHistoryDecimalVal := decimal.NewFromFloat(0)  //前几年N期数据总值
+
+		{
+			// 前几年N期汇总期数
+			tmpHistoryValNum := 0
+			{
+				//前几年当日的日期
+				tmpHistoryCurrentDate := currentDate.AddDate(year-currentDate.Year(), 0, 0)
+				for i := 0; i <= 35; i++ { // 前后35天找数据,找到最近的值,先向后面找,再往前面找
+					tmpDate := tmpHistoryCurrentDate.AddDate(0, 0, i)
+					if val, ok := existMap[tmpDate.Format(utils.FormatDate)]; ok {
+						tmpHistoryCurrentVal = val
+						isFindHistoryCurrent = true
+						break
+					} else {
+						tmpDate := tmpHistoryCurrentDate.AddDate(0, 0, -i)
+						if val, ok := existMap[tmpDate.Format(utils.FormatDate)]; ok {
+							tmpHistoryCurrentVal = val
+							isFindHistoryCurrent = true
+							break
+						}
+					}
+				}
+			}
+
+			for _, averageDate := range averageDateList {
+				lastDay, tmpErr := time.ParseInLocation(utils.FormatDate, averageDate, time.Local)
+				if tmpErr != nil {
+					err = tmpErr
+					return
+				}
+				//前几年上一期的日期
+				tmpHistoryLastDate := lastDay.AddDate(year-lastDay.Year(), 0, 0)
+				for i := 0; i <= 35; i++ { // 前后35天找数据,找到最近的值,先向后面找,再往前面找
+					tmpDate := tmpHistoryLastDate.AddDate(0, 0, i)
+					if val, ok := existMap[tmpDate.Format(utils.FormatDate)]; ok {
+						tmpDecimalVal2 := decimal.NewFromFloat(val)
+						tmpHistoryDecimalVal = tmpHistoryDecimalVal.Add(tmpDecimalVal2)
+						tmpHistoryValNum++
+						break
+					} else {
+						tmpDate := tmpHistoryLastDate.AddDate(0, 0, -i)
+						if val, ok := existMap[tmpDate.Format(utils.FormatDate)]; ok {
+							tmpDecimalVal2 := decimal.NewFromFloat(val)
+							tmpHistoryDecimalVal = tmpHistoryDecimalVal.Add(tmpDecimalVal2)
+							tmpHistoryValNum++
+							break
+						}
+					}
+				}
+			}
+
+			// 汇总期数与配置的N期数量一致
+			if tmpHistoryValNum == nValue {
+				isFindHistoryLast = true
+			}
+		}
+
+		// 如果没有找到前几年的汇总数据,或者没有找到前几年当日的数据,那么退出当前循环,进入下一循环
+		if !isFindHistoryLast || !isFindHistoryCurrent {
+			continue
+		}
+
+		// 计算最近N期同比值
+		tbVal := tmpAverageVal.Div(tmpHistoryDecimalVal)
+
+		// 预测值结果 = 同比年份同期值(tmpHistoryCurrentVal的值)* 同比值(tbVal的值)
+		val, _ := decimal.NewFromFloat(tmpHistoryCurrentVal).Mul(tbVal).RoundCeil(4).Float64()
+
+		currentDateStr := currentDate.Format(utils.FormatDate)
+		tmpData := &models.EdbDataList{
+			EdbDataId:     edbInfoId + 10000000000 + lenAllData + k,
+			EdbInfoId:     edbInfoId,
+			DataTime:      currentDateStr,
+			Value:         val,
+			DataTimestamp: (currentDate.UnixNano() / 1e6) + 1000, //前端需要让加1s,说是2022-09-01 00:00:00 这样的整点不合适
+		}
+		newPredictEdbInfoData = append(newPredictEdbInfoData, tmpData)
+		allDataList = append(allDataList, tmpData)
+		existMap[currentDateStr] = val
+
+		// 最大最小值
+		if val < minValue {
+			minValue = val
+		}
+		if val > maxValue {
+			maxValue = val
+		}
+	}
+	return
+}

+ 23 - 0
utils/common.go

@@ -640,3 +640,26 @@ func RevSlice(slice []int) []int {
 	}
 	return slice
 }
+
+// GetTimeSubDay 计算两个时间的自然日期差
+func GetTimeSubDay(t1, t2 time.Time) int {
+	var day int
+	swap := false
+	if t1.Unix() > t2.Unix() {
+		t1, t2 = t2, t1
+		swap = true
+	}
+
+	t1_ := t1.Add(time.Duration(t2.Sub(t1).Milliseconds()%86400000) * time.Millisecond)
+	day = int(t2.Sub(t1).Hours() / 24)
+	// 计算在t1+两个时间的余数之后天数是否有变化
+	if t1_.Day() != t1.Day() {
+		day += 1
+	}
+
+	if swap {
+		day = -day
+	}
+
+	return day
+}