Pārlūkot izejas kodu

add:stl趋势分解刷新

zqbao 4 mēneši atpakaļ
vecāks
revīzija
ea2d5faabf

+ 11 - 0
controllers/base_from_calculate.go

@@ -1680,6 +1680,17 @@ func (this *CalculateController) Refresh() {
 			errMsg = "RefreshAllCalculateRjz Err:" + err.Error()
 			break
 		}
+	case utils.DATA_SOURCE_CALCULATE_STL: // STL趋势分解
+		msg, err := services.RefreshStlData(edbInfoId)
+		if err != nil {
+			if msg == "" {
+				errMsg = "RefreshStlData Err:" + err.Error()
+			} else {
+				errMsg = msg + " Err:" + err.Error()
+			}
+			break
+		}
+
 	default:
 		// 获取通用的数据源处理服务
 		baseEdbInfoModel = models.GetBaseEdbInfoModel(source)

+ 1 - 0
go.mod

@@ -16,6 +16,7 @@ require (
 	github.com/qiniu/qmgo v1.1.8
 	github.com/rdlucklib/rdluck_tools v1.0.3
 	github.com/shopspring/decimal v1.4.0
+	github.com/tealeg/xlsx v1.0.5
 	go.mongodb.org/mongo-driver v1.16.0
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
 )

+ 2 - 0
go.sum

@@ -219,6 +219,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
 github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
+github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
+github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=

+ 2 - 0
models/db.go

@@ -172,6 +172,8 @@ func initBaseIndex() {
 		new(EdbDataTradeAnalysis),
 		new(BaseFromHisugarIndex),
 		new(BaseFromHisugarData),
+		new(EdbDataCalculateStl),
+		new(CalculateStlConfig),
 	)
 }
 

+ 97 - 0
models/edb_data_calculate_stl.go

@@ -0,0 +1,97 @@
+package models
+
+import (
+	"time"
+
+	"github.com/beego/beego/v2/client/orm"
+)
+
+type EdbDataCalculateStl struct {
+	EdbDataId     int       `orm:"pk"`
+	EdbInfoId     int       `description:"指标id"`
+	EdbCode       string    `description:"指标编码"`
+	DataTime      time.Time `description:"数据时间"`
+	Value         float64   `description:"数据值"`
+	CreateTime    time.Time `description:"创建时间"`
+	ModifyTime    time.Time `description:"修改时间"`
+	DataTimestamp int64     `description:"数据时间戳"`
+}
+
+type CalculateStlConfigMapping struct {
+	Id                   int       `orm:"pk" description:"主键"`
+	CalculateStlConfigId int       `description:"stl配置id"`
+	EdbInfoId            int       `description:"edb信息id"`
+	StlEdbType           int       `description:"stl指标类型: 1-Trend, 2-Seasonal, 3-Residual"`
+	CreateTime           time.Time `description:"创建时间"`
+	ModifyTime           time.Time `description:"修改时间"`
+}
+
+type CalculateStlConfig struct {
+	CalculateStlConfigId int       `orm:"column(calculate_stl_config_id);pk"`
+	Config               string    `description:"STL计算配置"`
+	SysUserId            int       `description:"系统用户ID"`
+	CreateTime           time.Time `description:"创建时间"`
+	ModifyTime           time.Time `description:"更新时间"`
+}
+
+func GetRelationCalculateStlConfigMappingByEdbInfoId(edbInfoId int) (items []*CalculateStlConfigMapping, err error) {
+	o := orm.NewOrm()
+	sql := `SELECT calculate_stl_config_id FROM calculate_stl_config_mapping WHERE edb_info_id =? LIMIT 1`
+	var confId int
+	err = o.Raw(sql, edbInfoId).QueryRow(&confId)
+	if err != nil {
+		return
+	}
+	sql = `SELECT * FROM calculate_stl_config_mapping WHERE calculate_stl_config_id =?`
+	_, err = o.Raw(sql, confId).QueryRows(&items)
+	return
+}
+
+// GetCalculateStlConfigMappingByConfigId 根据配置文件id获取配置文件映射信息
+func GetCalculateStlConfigMappingByConfigId(configId int) (items []*CalculateStlConfigMapping, err error) {
+	o := orm.NewOrm()
+	sql := `SELECT * FROM calculate_stl_config_mapping WHERE calculate_stl_config_id=?`
+	_, err = o.Raw(sql, configId).QueryRows(&items)
+	return
+}
+
+// GetCalculateStlConfigMappingIdByEdbInfoId 获取配置文件id
+func GetCalculateStlConfigMappingIdByEdbInfoId(edbInfoId int) (configId int, err error) {
+	o := orm.NewOrm()
+	sql := `SELECT calculate_stl_config_id FROM calculate_stl_config_mapping WHERE edb_info_id=? LIMIT 1`
+	err = o.Raw(sql, edbInfoId).QueryRow(&configId)
+	return
+}
+
+func DeleteAndInsertEdbDataCalculateStl(edbCode string, dataList []*EdbDataCalculateStl) (err error) {
+	tx, err := orm.NewOrm().Begin()
+	if err != nil {
+		return
+	}
+	defer func() {
+		if err != nil {
+			tx.Rollback()
+		} else {
+			tx.Commit()
+		}
+	}()
+	sql := `DELETE FROM edb_data_calculate_stl WHERE edb_code =?`
+	_, err = tx.Raw(sql, edbCode).Exec()
+	if err != nil {
+		return
+	}
+	_, err = tx.InsertMulti(500, dataList)
+	return
+}
+func (c *CalculateStlConfig) Update(cols []string) (err error) {
+	o := orm.NewOrm()
+	_, err = o.Update(c, cols...)
+	return
+}
+
+func GetCalculateStlConfigById(id int) (item *CalculateStlConfig, err error) {
+	o := orm.NewOrm()
+	sql := "SELECT * FROM calculate_stl_config WHERE calculate_stl_config_id =?"
+	err = o.Raw(sql, id).QueryRow(&item)
+	return
+}

+ 2 - 0
models/edb_data_table.go

@@ -172,6 +172,8 @@ func GetEdbDataTableName(source, subSource int) (tableName string) {
 		tableName = "edb_data_trade_analysis"
 	case utils.DATA_SOURCE_LY: // 粮油商务网->91
 		tableName = "edb_data_ly"
+	case utils.DATA_SOURCE_CALCULATE_STL: // STL趋势分解->98
+		tableName = "edb_data_calculate_stl"
 	default:
 		edbSource := EdbSourceIdMap[source]
 		if edbSource != nil {

+ 10 - 0
models/edb_info.go

@@ -1612,3 +1612,13 @@ func (m SortEdbDataList) Less(i, j int) bool {
 func (m SortEdbDataList) Swap(i, j int) {
 	m[i], m[j] = m[j], m[i]
 }
+
+func ModifyEdbInfoDataStatus(edbInfoId int64, source, subSource int, edbCode string) (err error) {
+	o := orm.NewOrm()
+	sql := ``
+	tableName := GetEdbDataTableName(source, subSource)
+	sql = ` UPDATE %s SET edb_info_id=?,modify_time=NOW() WHERE edb_code=? `
+	sql = fmt.Sprintf(sql, tableName)
+	_, err = o.Raw(sql, edbInfoId, edbCode).Exec()
+	return
+}

+ 490 - 0
services/edb_data_calculate_stl.go

@@ -0,0 +1,490 @@
+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            int     `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()
+		lastDate := time.Date(year-stlConfig.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 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
+import json
+import warnings
+warnings.filterwarnings('ignore')
+
+file_path = r"%s"
+df = pd.read_excel(file_path, parse_dates=['日期'])
+df.set_index('日期', inplace=True)
+
+
+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['值'], df.index, 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)
+	cmd := exec.Command(`D:\conda\envs\py311\python`, "-c", pythonCode)
+	_, err = cmd.CombinedOutput()
+	if err != nil {
+		return
+	}
+	defer cmd.Process.Kill()
+	return
+}

+ 1 - 0
utils/constants.go

@@ -116,6 +116,7 @@ const (
 	DATA_SOURCE_LY                                   = 91 // 粮油商务网
 	DATA_SOURCE_TRADE_ANALYSIS                       = 92 // 持仓分析
 	DATA_SOURCE_HISUGAR                              = 93 // 泛糖科技 -> 93
+	DATA_SOURCE_CALCULATE_STL                        = 98 // STL趋势分解 -> 98
 )
 
 // 指标来源的中文展示