123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- package services
- import (
- "encoding/json"
- "eta/eta_index_lib/models"
- "eta/eta_index_lib/utils"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strconv"
- "time"
- "github.com/tealeg/xlsx"
- )
- const (
- ALL_DATE = iota + 1
- LAST_N_YEARS
- RANGE_DATE
- RANGE_DATE_TO_NOW
- )
- type EdbStlConfig struct {
- EdbInfoId int `description:"指标ID"`
- CalculateStlConfigId int `description:"计算的STL配置ID"`
- DataRangeType int `description:"数据时间类型:1-全部时间,2-最近N年,3-区间设置,4-区间设置(至今)"`
- StartDate string `description:"开始日期"`
- EndDate string `description:"结束日期"`
- LastNYear string `description:"最近N年"`
- Period int `description:"数据的周期,根据频率设置"`
- Seasonal int `description:"季节性成分窗口大小,一般为period+1,可以设置为大于period的正奇数"`
- Trend int `description:"趋势成分窗口大小,一般为period+1,可以设置为大于period的正奇数"`
- Fraction float64 `description:"趋势项的平滑系数,默认为0.2,区间为[0-1]"`
- Robust bool `description:"是否使用稳健方法: true(使用) false(不使用) "`
- TrendDeg int `description:"分解中趋势多项式次数,默认为1,不超过5的正整数"`
- SeasonalDeg int `description:"分解中季节性多项次数,默认为1,不超过5的正整数"`
- LowPassDeg int `description:"分解中低通滤波器次数,默认为1,不超过5的正整数"`
- }
- type ChartEdbInfo struct {
- EdbInfoId int
- Title string
- Unit string
- Frequency string
- MaxData float64
- MinData float64
- ClassifyId int
- ClassifyPath string
- DataList []*EdbData
- }
- type EdbData struct {
- Value float64
- DataTime string
- DataTimestamp int64
- }
- func RefreshStlData(edbInfoId int) (msg string, err error) {
- calculateStl, err := models.GetEdbInfoCalculateMappingDetail(edbInfoId)
- if err != nil {
- return
- }
- fromEdbInfo, err := models.GetEdbInfoById(calculateStl.FromEdbInfoId)
- if err != nil {
- return
- }
- var stlConfig EdbStlConfig
- if err = json.Unmarshal([]byte(calculateStl.CalculateFormula), &stlConfig); err != nil {
- return
- }
- var condition string
- var pars []interface{}
- switch stlConfig.DataRangeType {
- case ALL_DATE:
- case LAST_N_YEARS:
- condition += " AND data_time >=?"
- year := time.Now().Year()
- lastNyear, er := strconv.Atoi(stlConfig.LastNYear)
- if er != nil {
- msg = "最近N年输入不合法"
- err = er
- return
- }
- lastDate := time.Date(year-lastNyear, 1, 1, 0, 0, 0, 0, time.Local)
- pars = append(pars, lastDate)
- case RANGE_DATE:
- condition = " AND data_time >=? AND data_time <=?"
- pars = append(pars, stlConfig.StartDate, stlConfig.EndDate)
- case RANGE_DATE_TO_NOW:
- condition = " AND data_time >=?"
- pars = append(pars, stlConfig.StartDate)
- }
- condition += " AND edb_code =?"
- pars = append(pars, fromEdbInfo.EdbCode)
- edbData, err := models.GetEdbDataByCondition(fromEdbInfo.Source, fromEdbInfo.SubSource, condition, pars)
- if err != nil {
- return
- }
- var condMsg string
- if stlConfig.Period < 2 || stlConfig.Period > len(edbData) {
- condMsg += "period必须是一个大于等于2的正整数,且必须小于时间序列的长度"
- }
- if stlConfig.Seasonal < 3 || stlConfig.Seasonal%2 == 0 || stlConfig.Seasonal <= stlConfig.Period {
- if condMsg != "" {
- condMsg += "\n"
- }
- condMsg += "seasonal必须是一个大于等于3的奇整数,且必须大于period"
- }
- if stlConfig.Trend < 3 || stlConfig.Trend%2 == 0 || stlConfig.Trend <= stlConfig.Period {
- if condMsg != "" {
- condMsg += "\n"
- }
- condMsg += "trend必须是一个大于等于3的奇整数,且必须大于period"
- }
- if stlConfig.Fraction < 0 || stlConfig.Fraction > 1 {
- if condMsg != "" {
- condMsg += "\n"
- }
- condMsg += "fraction必须是一个介于[0-1]之间"
- }
- if 1 > stlConfig.TrendDeg || stlConfig.TrendDeg > 5 {
- if condMsg != "" {
- condMsg += "\n"
- }
- condMsg += "trend_deg请设置成1-5的整数"
- }
- if 1 > stlConfig.SeasonalDeg || stlConfig.SeasonalDeg > 5 {
- if condMsg != "" {
- condMsg += "\n"
- }
- condMsg += "seasonal_deg请设置成1-5的整数"
- }
- if 1 > stlConfig.LowPassDeg || stlConfig.LowPassDeg > 5 {
- if condMsg != "" {
- condMsg += "\n"
- }
- condMsg += "low_pass_deg请设置成1-5的整数"
- }
- if condMsg != "" {
- msg = condMsg
- err = fmt.Errorf("参数错误")
- return
- }
- dir, _ := os.Executable()
- exPath := filepath.Dir(dir) + "/static/stl_tmp"
- err = CheckOsPathAndMake(exPath)
- if err != nil {
- msg = "计算失败"
- return
- }
- loadFilePath := exPath + "/" + strconv.Itoa(fromEdbInfo.SysUserId) + "_" + time.Now().Format(utils.FormatDateTimeUnSpace) + ".xlsx"
- err = SaveToExcel(edbData, loadFilePath)
- if err != nil {
- msg = "保存数据到Excel失败"
- return
- }
- defer os.Remove(loadFilePath)
- saveFilePath := exPath + "/" + strconv.Itoa(fromEdbInfo.SysUserId) + "_" + time.Now().Format(utils.FormatDateTimeUnSpace) + "_res" + ".xlsx"
- err = execStlPythonCode(loadFilePath, saveFilePath, stlConfig.Period, stlConfig.Seasonal, stlConfig.Trend, stlConfig.TrendDeg, stlConfig.SeasonalDeg, stlConfig.LowPassDeg, stlConfig.Fraction, stlConfig.Robust)
- if err != nil {
- msg = "执行Python代码失败"
- return
- }
- trendChart, seasonalChart, residualChart, err := ParseStlExcel(saveFilePath)
- if err != nil {
- msg = "解析Excel失败"
- return
- }
- defer os.Remove(saveFilePath)
- edbInfo, err := models.GetEdbInfoById(edbInfoId)
- if err != nil {
- msg = "获取指标信息失败"
- return
- }
- err = SyncUpdateRelationEdbInfo(edbInfo, stlConfig, trendChart, seasonalChart, residualChart)
- if err != nil {
- msg = "更新关联指标失败"
- return
- }
- return
- }
- func SyncUpdateRelationEdbInfo(edbInfo *models.EdbInfo, config EdbStlConfig, trendData, seasonalData, residualData ChartEdbInfo) (err error) {
- configId, err := models.GetCalculateStlConfigMappingIdByEdbInfoId(edbInfo.EdbInfoId)
- if err != nil {
- return
- }
- mappingList, err := models.GetCalculateStlConfigMappingByConfigId(configId)
- if err != nil {
- return
- }
- for _, v := range mappingList {
- edbInfo, er := models.GetEdbInfoById(v.EdbInfoId)
- if er != nil {
- continue
- }
- switch v.StlEdbType {
- case 1:
- // 趋势指标
- er = UpdateStlEdbData(edbInfo, config, edbInfo.EdbCode, trendData)
- case 2:
- // 季节性指标
- er = UpdateStlEdbData(edbInfo, config, edbInfo.EdbCode, seasonalData)
- case 3:
- // 残差指标
- er = UpdateStlEdbData(edbInfo, config, edbInfo.EdbCode, residualData)
- default:
- utils.FileLog.Info("未知的stlEdbType类型, mapping:%v", v)
- continue
- }
- if er != nil {
- utils.FileLog.Error("更新指标数据失败, edbInfoId:%v, err:%v", v.EdbInfoId, er)
- err = er
- continue
- }
- }
- // 同步更新计算配置
- newStlConf := &models.CalculateStlConfig{
- CalculateStlConfigId: configId,
- Config: edbInfo.CalculateFormula,
- ModifyTime: time.Now(),
- }
- err = newStlConf.Update([]string{"config", "modify_time"})
- return
- }
- func UpdateStlEdbData(edbInfo *models.EdbInfo, config EdbStlConfig, edbCode string, edbData ChartEdbInfo) (err error) {
- var dataList []*models.EdbDataCalculateStl
- for _, v := range edbData.DataList {
- dataTime, _ := time.Parse(utils.FormatDate, v.DataTime)
- dataList = append(dataList, &models.EdbDataCalculateStl{
- EdbInfoId: edbData.EdbInfoId,
- EdbCode: edbCode,
- DataTime: dataTime,
- Value: v.Value,
- CreateTime: time.Now(),
- ModifyTime: time.Now(),
- DataTimestamp: dataTime.UnixMilli(),
- })
- }
- err = models.DeleteAndInsertEdbDataCalculateStl(edbCode, dataList)
- if err != nil {
- return
- }
- models.ModifyEdbInfoDataStatus(int64(edbInfo.EdbInfoId), edbInfo.Source, edbInfo.SubSource, edbInfo.EdbCode)
- maxAndMinItem, _ := models.GetEdbInfoMaxAndMinInfo(edbInfo.Source, edbInfo.SubSource, edbInfo.EdbCode)
- if maxAndMinItem != nil {
- err = models.ModifyEdbInfoMaxAndMinInfo(edbInfo.EdbInfoId, maxAndMinItem)
- if err != nil {
- return
- }
- }
- bconfig, _ := json.Marshal(config)
- edbInfo.CalculateFormula = string(bconfig)
- edbInfo.ModifyTime = time.Now()
- err = edbInfo.Update([]string{"calculate_formula", "modify_time"})
- if err != nil {
- return
- }
- return
- }
- func CheckOsPathAndMake(path string) (err error) {
- if _, er := os.Stat(path); os.IsNotExist(er) {
- err = os.MkdirAll(path, os.ModePerm)
- }
- return
- }
- func SaveToExcel(data []*models.EdbInfoSearchData, filePath string) (err error) {
- xlsxFile := xlsx.NewFile()
- sheetNew, err := xlsxFile.AddSheet("Tmp")
- if err != nil {
- return
- }
- titleRow := sheetNew.AddRow()
- titleRow.AddCell().SetString("日期")
- titleRow.AddCell().SetString("值")
- for i, d := range data {
- row := sheetNew.Row(i + 1)
- row.AddCell().SetString(d.DataTime)
- row.AddCell().SetFloat(d.Value)
- }
- err = xlsxFile.Save(filePath)
- if err != nil {
- return
- }
- return
- }
- func ParseStlExcel(excelPath string) (TrendChart, SeasonalChart, ResidualChart ChartEdbInfo, err error) {
- file, err := xlsx.OpenFile(excelPath)
- if err != nil {
- return
- }
- for _, sheet := range file.Sheets {
- switch sheet.Name {
- case "季节":
- var MinData, MaxData float64
- for i, row := range sheet.Rows {
- if i == 0 {
- continue
- }
- var date string
- var dataTimestamp int64
- if row.Cells[0].Type() == xlsx.CellTypeNumeric {
- dataNum, _ := strconv.ParseFloat(row.Cells[0].Value, 64)
- tmpTime := xlsx.TimeFromExcelTime(dataNum, false)
- date = tmpTime.Format(utils.FormatDate)
- dataTimestamp = tmpTime.UnixMilli()
- } else {
- timeDate, _ := time.Parse(utils.FormatDateTime, date)
- date = timeDate.Format(utils.FormatDate)
- dataTimestamp = timeDate.UnixMilli()
- }
- fv, _ := row.Cells[1].Float()
- if MinData == 0 || fv < MinData {
- MinData = fv
- }
- if MaxData == 0 || fv > MaxData {
- MaxData = fv
- }
- SeasonalChart.DataList = append(SeasonalChart.DataList, &EdbData{DataTime: date, Value: fv, DataTimestamp: dataTimestamp})
- }
- SeasonalChart.MinData = MinData
- SeasonalChart.MaxData = MaxData
- case "趋势":
- var MinData, MaxData float64
- for i, row := range sheet.Rows {
- if i == 0 {
- continue
- }
- var date string
- var dataTimestamp int64
- if row.Cells[0].Type() == xlsx.CellTypeNumeric {
- dataNum, _ := strconv.ParseFloat(row.Cells[0].Value, 64)
- tmpTime := xlsx.TimeFromExcelTime(dataNum, false)
- date = tmpTime.Format(utils.FormatDate)
- dataTimestamp = tmpTime.UnixMilli()
- } else {
- timeDate, _ := time.Parse(utils.FormatDateTime, date)
- date = timeDate.Format(utils.FormatDate)
- dataTimestamp = timeDate.UnixMilli()
- }
- fv, _ := row.Cells[1].Float()
- if MinData == 0 || fv < MinData {
- MinData = fv
- }
- if MaxData == 0 || fv > MaxData {
- MaxData = fv
- }
- TrendChart.DataList = append(TrendChart.DataList, &EdbData{DataTime: date, Value: fv, DataTimestamp: dataTimestamp})
- }
- TrendChart.MaxData = MaxData
- TrendChart.MinData = MinData
- case "残差":
- var MinData, MaxData float64
- for i, row := range sheet.Rows {
- if i == 0 {
- continue
- }
- var date string
- var dataTimestamp int64
- if row.Cells[0].Type() == xlsx.CellTypeNumeric {
- dataNum, _ := strconv.ParseFloat(row.Cells[0].Value, 64)
- tmpTime := xlsx.TimeFromExcelTime(dataNum, false)
- date = tmpTime.Format(utils.FormatDate)
- dataTimestamp = tmpTime.UnixMilli()
- } else {
- timeDate, _ := time.Parse(utils.FormatDateTime, date)
- date = timeDate.Format(utils.FormatDate)
- dataTimestamp = timeDate.UnixMilli()
- }
- fv, _ := row.Cells[1].Float()
- if MinData == 0 || fv < MinData {
- MinData = fv
- }
- if MaxData == 0 || fv > MaxData {
- MaxData = fv
- }
- ResidualChart.DataList = append(ResidualChart.DataList, &EdbData{DataTime: date, Value: fv, DataTimestamp: dataTimestamp})
- }
- ResidualChart.MaxData = MaxData
- ResidualChart.MinData = MinData
- }
- }
- return
- }
- func execStlPythonCode(path, toPath string, period, seasonal, trend, trendDeg, seasonalDeg, lowPassDeg int, fraction float64, robust bool) (err error) {
- pythonCode := `
- import json
- import warnings
- warnings.filterwarnings('ignore')
- import pandas as pd
- from statsmodels.tsa.seasonal import STL
- from statsmodels.nonparametric.smoothers_lowess import lowess
- from statsmodels.tsa.stattools import adfuller
- from statsmodels.stats.diagnostic import acorr_ljungbox
- import numpy as np
- file_path = r"%s"
- df = pd.read_excel(file_path, parse_dates=['日期'], engine='openpyxl')
- df.set_index('日期', inplace=True)
- df = df[df.index.notna()]
- period = %d
- seasonal = %d
- trend = %d
- fraction = %g
- seasonal_deg = %d
- trend_deg = %d
- low_pass_deg = %d
- robust = %s
- stl = STL(
- df['值'],
- period=period,
- seasonal=seasonal,
- trend=trend,
- low_pass=None,
- seasonal_deg=seasonal_deg,
- trend_deg=trend_deg,
- low_pass_deg=low_pass_deg,
- seasonal_jump=1,
- trend_jump=1,
- low_pass_jump=1,
- robust=robust
- )
- result = stl.fit()
- smoothed = lowess(df['值'], np.arange(len(df)), frac=fraction)
- trend_lowess = smoothed[:, 1]
- # 季节图
- seasonal_component = result.seasonal
- # 趋势图
- trend_lowess_series = pd.Series(trend_lowess, index=df.index)
- # 残差图
- residual_component = df['值'] - trend_lowess - seasonal_component
- # 计算打印残差的均值
- residual_mean = np.mean(residual_component)
- # 计算打印残差的方差
- residual_var = np.std(residual_component)
- # 计算打印残差的ADF检验结果, 输出p-value
- adf_result = adfuller(residual_component)
- # 根据p-value判断是否平稳
- lb_test = acorr_ljungbox(residual_component, lags=period, return_df=True)
- output_file = r"%s"
- with pd.ExcelWriter(output_file) as writer:
- # 保存季节图
- pd.Series(seasonal_component, index=df.index, name='值').to_frame().reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='季节', index=False)
- # 保存趋势图
- trend_lowess_series.to_frame(name='值').reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='趋势', index=False)
- # 保存残差图
- pd.Series(residual_component, index=df.index, name='值').to_frame().reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='残差', index=False)
- output = json.dumps({
- 'residual_mean': residual_mean,
- 'residual_var': residual_var,
- 'adf_p_value': adf_result[1],
- 'lb_test_p_value': lb_test['lb_pvalue'].values[0],
- 'lb_test_stat': lb_test['lb_stat'].values[0]
- })
- print(output)
- `
- robustStr := "True"
- if !robust {
- robustStr = "False"
- }
- pythonCode = fmt.Sprintf(pythonCode, path, period, seasonal, trend, fraction, seasonalDeg, trendDeg, lowPassDeg, robustStr, toPath)
- cmd := exec.Command(`python3`, "-c", pythonCode)
- _, err = cmd.CombinedOutput()
- if err != nil {
- return
- }
- defer cmd.Process.Kill()
- return
- }
|