edb_data_calculate_stl.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. package services
  2. import (
  3. "encoding/json"
  4. "eta/eta_index_lib/models"
  5. "eta/eta_index_lib/utils"
  6. "fmt"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "strconv"
  11. "time"
  12. "github.com/tealeg/xlsx"
  13. )
  14. const (
  15. ALL_DATE = iota + 1
  16. LAST_N_YEARS
  17. RANGE_DATE
  18. RANGE_DATE_TO_NOW
  19. )
  20. type EdbStlConfig struct {
  21. EdbInfoId int `description:"指标ID"`
  22. CalculateStlConfigId int `description:"计算的STL配置ID"`
  23. DataRangeType int `description:"数据时间类型:1-全部时间,2-最近N年,3-区间设置,4-区间设置(至今)"`
  24. StartDate string `description:"开始日期"`
  25. EndDate string `description:"结束日期"`
  26. LastNYear string `description:"最近N年"`
  27. Period int `description:"数据的周期,根据频率设置"`
  28. Seasonal int `description:"季节性成分窗口大小,一般为period+1,可以设置为大于period的正奇数"`
  29. Trend int `description:"趋势成分窗口大小,一般为period+1,可以设置为大于period的正奇数"`
  30. Fraction float64 `description:"趋势项的平滑系数,默认为0.2,区间为[0-1]"`
  31. Robust bool `description:"是否使用稳健方法: true(使用) false(不使用) "`
  32. TrendDeg int `description:"分解中趋势多项式次数,默认为1,不超过5的正整数"`
  33. SeasonalDeg int `description:"分解中季节性多项次数,默认为1,不超过5的正整数"`
  34. LowPassDeg int `description:"分解中低通滤波器次数,默认为1,不超过5的正整数"`
  35. }
  36. type ChartEdbInfo struct {
  37. EdbInfoId int
  38. Title string
  39. Unit string
  40. Frequency string
  41. MaxData float64
  42. MinData float64
  43. ClassifyId int
  44. ClassifyPath string
  45. DataList []*EdbData
  46. }
  47. type EdbData struct {
  48. Value float64
  49. DataTime string
  50. DataTimestamp int64
  51. }
  52. func RefreshStlData(edbInfoId int) (msg string, err error) {
  53. calculateStl, err := models.GetEdbInfoCalculateMappingDetail(edbInfoId)
  54. if err != nil {
  55. return
  56. }
  57. fromEdbInfo, err := models.GetEdbInfoById(calculateStl.FromEdbInfoId)
  58. if err != nil {
  59. return
  60. }
  61. var stlConfig EdbStlConfig
  62. if err = json.Unmarshal([]byte(calculateStl.CalculateFormula), &stlConfig); err != nil {
  63. return
  64. }
  65. var condition string
  66. var pars []interface{}
  67. switch stlConfig.DataRangeType {
  68. case ALL_DATE:
  69. case LAST_N_YEARS:
  70. condition += " AND data_time >=?"
  71. year := time.Now().Year()
  72. lastNyear, er := strconv.Atoi(stlConfig.LastNYear)
  73. if er != nil {
  74. msg = "最近N年输入不合法"
  75. err = er
  76. return
  77. }
  78. lastDate := time.Date(year-lastNyear, 1, 1, 0, 0, 0, 0, time.Local)
  79. pars = append(pars, lastDate)
  80. case RANGE_DATE:
  81. condition = " AND data_time >=? AND data_time <=?"
  82. pars = append(pars, stlConfig.StartDate, stlConfig.EndDate)
  83. case RANGE_DATE_TO_NOW:
  84. condition = " AND data_time >=?"
  85. pars = append(pars, stlConfig.StartDate)
  86. }
  87. condition += " AND edb_code =?"
  88. pars = append(pars, fromEdbInfo.EdbCode)
  89. edbData, err := models.GetEdbDataByCondition(fromEdbInfo.Source, fromEdbInfo.SubSource, condition, pars)
  90. if err != nil {
  91. return
  92. }
  93. var condMsg string
  94. if stlConfig.Period < 2 || stlConfig.Period > len(edbData) {
  95. condMsg += "period必须是一个大于等于2的正整数,且必须小于时间序列的长度"
  96. }
  97. if stlConfig.Seasonal < 3 || stlConfig.Seasonal%2 == 0 || stlConfig.Seasonal <= stlConfig.Period {
  98. if condMsg != "" {
  99. condMsg += "\n"
  100. }
  101. condMsg += "seasonal必须是一个大于等于3的奇整数,且必须大于period"
  102. }
  103. if stlConfig.Trend < 3 || stlConfig.Trend%2 == 0 || stlConfig.Trend <= stlConfig.Period {
  104. if condMsg != "" {
  105. condMsg += "\n"
  106. }
  107. condMsg += "trend必须是一个大于等于3的奇整数,且必须大于period"
  108. }
  109. if stlConfig.Fraction < 0 || stlConfig.Fraction > 1 {
  110. if condMsg != "" {
  111. condMsg += "\n"
  112. }
  113. condMsg += "fraction必须是一个介于[0-1]之间"
  114. }
  115. if 1 > stlConfig.TrendDeg || stlConfig.TrendDeg > 5 {
  116. if condMsg != "" {
  117. condMsg += "\n"
  118. }
  119. condMsg += "trend_deg请设置成1-5的整数"
  120. }
  121. if 1 > stlConfig.SeasonalDeg || stlConfig.SeasonalDeg > 5 {
  122. if condMsg != "" {
  123. condMsg += "\n"
  124. }
  125. condMsg += "seasonal_deg请设置成1-5的整数"
  126. }
  127. if 1 > stlConfig.LowPassDeg || stlConfig.LowPassDeg > 5 {
  128. if condMsg != "" {
  129. condMsg += "\n"
  130. }
  131. condMsg += "low_pass_deg请设置成1-5的整数"
  132. }
  133. if condMsg != "" {
  134. msg = condMsg
  135. err = fmt.Errorf("参数错误")
  136. return
  137. }
  138. dir, _ := os.Executable()
  139. exPath := filepath.Dir(dir) + "/static/stl_tmp"
  140. err = CheckOsPathAndMake(exPath)
  141. if err != nil {
  142. msg = "计算失败"
  143. return
  144. }
  145. loadFilePath := exPath + "/" + strconv.Itoa(fromEdbInfo.SysUserId) + "_" + time.Now().Format(utils.FormatDateTimeUnSpace) + ".xlsx"
  146. err = SaveToExcel(edbData, loadFilePath)
  147. if err != nil {
  148. msg = "保存数据到Excel失败"
  149. return
  150. }
  151. defer os.Remove(loadFilePath)
  152. saveFilePath := exPath + "/" + strconv.Itoa(fromEdbInfo.SysUserId) + "_" + time.Now().Format(utils.FormatDateTimeUnSpace) + "_res" + ".xlsx"
  153. err = execStlPythonCode(loadFilePath, saveFilePath, stlConfig.Period, stlConfig.Seasonal, stlConfig.Trend, stlConfig.TrendDeg, stlConfig.SeasonalDeg, stlConfig.LowPassDeg, stlConfig.Fraction, stlConfig.Robust)
  154. if err != nil {
  155. msg = "执行Python代码失败"
  156. return
  157. }
  158. trendChart, seasonalChart, residualChart, err := ParseStlExcel(saveFilePath)
  159. if err != nil {
  160. msg = "解析Excel失败"
  161. return
  162. }
  163. defer os.Remove(saveFilePath)
  164. edbInfo, err := models.GetEdbInfoById(edbInfoId)
  165. if err != nil {
  166. msg = "获取指标信息失败"
  167. return
  168. }
  169. err = SyncUpdateRelationEdbInfo(edbInfo, stlConfig, trendChart, seasonalChart, residualChart)
  170. if err != nil {
  171. msg = "更新关联指标失败"
  172. return
  173. }
  174. return
  175. }
  176. func SyncUpdateRelationEdbInfo(edbInfo *models.EdbInfo, config EdbStlConfig, trendData, seasonalData, residualData ChartEdbInfo) (err error) {
  177. configId, err := models.GetCalculateStlConfigMappingIdByEdbInfoId(edbInfo.EdbInfoId)
  178. if err != nil {
  179. return
  180. }
  181. mappingList, err := models.GetCalculateStlConfigMappingByConfigId(configId)
  182. if err != nil {
  183. return
  184. }
  185. for _, v := range mappingList {
  186. edbInfo, er := models.GetEdbInfoById(v.EdbInfoId)
  187. if er != nil {
  188. continue
  189. }
  190. switch v.StlEdbType {
  191. case 1:
  192. // 趋势指标
  193. er = UpdateStlEdbData(edbInfo, config, edbInfo.EdbCode, trendData)
  194. case 2:
  195. // 季节性指标
  196. er = UpdateStlEdbData(edbInfo, config, edbInfo.EdbCode, seasonalData)
  197. case 3:
  198. // 残差指标
  199. er = UpdateStlEdbData(edbInfo, config, edbInfo.EdbCode, residualData)
  200. default:
  201. utils.FileLog.Info("未知的stlEdbType类型, mapping:%v", v)
  202. continue
  203. }
  204. if er != nil {
  205. utils.FileLog.Error("更新指标数据失败, edbInfoId:%v, err:%v", v.EdbInfoId, er)
  206. err = er
  207. continue
  208. }
  209. }
  210. // 同步更新计算配置
  211. newStlConf := &models.CalculateStlConfig{
  212. CalculateStlConfigId: configId,
  213. Config: edbInfo.CalculateFormula,
  214. ModifyTime: time.Now(),
  215. }
  216. err = newStlConf.Update([]string{"config", "modify_time"})
  217. return
  218. }
  219. func UpdateStlEdbData(edbInfo *models.EdbInfo, config EdbStlConfig, edbCode string, edbData ChartEdbInfo) (err error) {
  220. var dataList []*models.EdbDataCalculateStl
  221. for _, v := range edbData.DataList {
  222. dataTime, _ := time.Parse(utils.FormatDate, v.DataTime)
  223. dataList = append(dataList, &models.EdbDataCalculateStl{
  224. EdbInfoId: edbData.EdbInfoId,
  225. EdbCode: edbCode,
  226. DataTime: dataTime,
  227. Value: v.Value,
  228. CreateTime: time.Now(),
  229. ModifyTime: time.Now(),
  230. DataTimestamp: dataTime.UnixMilli(),
  231. })
  232. }
  233. err = models.DeleteAndInsertEdbDataCalculateStl(edbCode, dataList)
  234. if err != nil {
  235. return
  236. }
  237. models.ModifyEdbInfoDataStatus(int64(edbInfo.EdbInfoId), edbInfo.Source, edbInfo.SubSource, edbInfo.EdbCode)
  238. maxAndMinItem, _ := models.GetEdbInfoMaxAndMinInfo(edbInfo.Source, edbInfo.SubSource, edbInfo.EdbCode)
  239. if maxAndMinItem != nil {
  240. err = models.ModifyEdbInfoMaxAndMinInfo(edbInfo.EdbInfoId, maxAndMinItem)
  241. if err != nil {
  242. return
  243. }
  244. }
  245. bconfig, _ := json.Marshal(config)
  246. edbInfo.CalculateFormula = string(bconfig)
  247. edbInfo.ModifyTime = time.Now()
  248. err = edbInfo.Update([]string{"calculate_formula", "modify_time"})
  249. if err != nil {
  250. return
  251. }
  252. return
  253. }
  254. func CheckOsPathAndMake(path string) (err error) {
  255. if _, er := os.Stat(path); os.IsNotExist(er) {
  256. err = os.MkdirAll(path, os.ModePerm)
  257. }
  258. return
  259. }
  260. func SaveToExcel(data []*models.EdbInfoSearchData, filePath string) (err error) {
  261. xlsxFile := xlsx.NewFile()
  262. sheetNew, err := xlsxFile.AddSheet("Tmp")
  263. if err != nil {
  264. return
  265. }
  266. titleRow := sheetNew.AddRow()
  267. titleRow.AddCell().SetString("日期")
  268. titleRow.AddCell().SetString("值")
  269. for i, d := range data {
  270. row := sheetNew.Row(i + 1)
  271. row.AddCell().SetString(d.DataTime)
  272. row.AddCell().SetFloat(d.Value)
  273. }
  274. err = xlsxFile.Save(filePath)
  275. if err != nil {
  276. return
  277. }
  278. return
  279. }
  280. func ParseStlExcel(excelPath string) (TrendChart, SeasonalChart, ResidualChart ChartEdbInfo, err error) {
  281. file, err := xlsx.OpenFile(excelPath)
  282. if err != nil {
  283. return
  284. }
  285. for _, sheet := range file.Sheets {
  286. switch sheet.Name {
  287. case "季节":
  288. var MinData, MaxData float64
  289. for i, row := range sheet.Rows {
  290. if i == 0 {
  291. continue
  292. }
  293. var date string
  294. var dataTimestamp int64
  295. if row.Cells[0].Type() == xlsx.CellTypeNumeric {
  296. dataNum, _ := strconv.ParseFloat(row.Cells[0].Value, 64)
  297. tmpTime := xlsx.TimeFromExcelTime(dataNum, false)
  298. date = tmpTime.Format(utils.FormatDate)
  299. dataTimestamp = tmpTime.UnixMilli()
  300. } else {
  301. timeDate, _ := time.Parse(utils.FormatDateTime, date)
  302. date = timeDate.Format(utils.FormatDate)
  303. dataTimestamp = timeDate.UnixMilli()
  304. }
  305. fv, _ := row.Cells[1].Float()
  306. if MinData == 0 || fv < MinData {
  307. MinData = fv
  308. }
  309. if MaxData == 0 || fv > MaxData {
  310. MaxData = fv
  311. }
  312. SeasonalChart.DataList = append(SeasonalChart.DataList, &EdbData{DataTime: date, Value: fv, DataTimestamp: dataTimestamp})
  313. }
  314. SeasonalChart.MinData = MinData
  315. SeasonalChart.MaxData = MaxData
  316. case "趋势":
  317. var MinData, MaxData float64
  318. for i, row := range sheet.Rows {
  319. if i == 0 {
  320. continue
  321. }
  322. var date string
  323. var dataTimestamp int64
  324. if row.Cells[0].Type() == xlsx.CellTypeNumeric {
  325. dataNum, _ := strconv.ParseFloat(row.Cells[0].Value, 64)
  326. tmpTime := xlsx.TimeFromExcelTime(dataNum, false)
  327. date = tmpTime.Format(utils.FormatDate)
  328. dataTimestamp = tmpTime.UnixMilli()
  329. } else {
  330. timeDate, _ := time.Parse(utils.FormatDateTime, date)
  331. date = timeDate.Format(utils.FormatDate)
  332. dataTimestamp = timeDate.UnixMilli()
  333. }
  334. fv, _ := row.Cells[1].Float()
  335. if MinData == 0 || fv < MinData {
  336. MinData = fv
  337. }
  338. if MaxData == 0 || fv > MaxData {
  339. MaxData = fv
  340. }
  341. TrendChart.DataList = append(TrendChart.DataList, &EdbData{DataTime: date, Value: fv, DataTimestamp: dataTimestamp})
  342. }
  343. TrendChart.MaxData = MaxData
  344. TrendChart.MinData = MinData
  345. case "残差":
  346. var MinData, MaxData float64
  347. for i, row := range sheet.Rows {
  348. if i == 0 {
  349. continue
  350. }
  351. var date string
  352. var dataTimestamp int64
  353. if row.Cells[0].Type() == xlsx.CellTypeNumeric {
  354. dataNum, _ := strconv.ParseFloat(row.Cells[0].Value, 64)
  355. tmpTime := xlsx.TimeFromExcelTime(dataNum, false)
  356. date = tmpTime.Format(utils.FormatDate)
  357. dataTimestamp = tmpTime.UnixMilli()
  358. } else {
  359. timeDate, _ := time.Parse(utils.FormatDateTime, date)
  360. date = timeDate.Format(utils.FormatDate)
  361. dataTimestamp = timeDate.UnixMilli()
  362. }
  363. fv, _ := row.Cells[1].Float()
  364. if MinData == 0 || fv < MinData {
  365. MinData = fv
  366. }
  367. if MaxData == 0 || fv > MaxData {
  368. MaxData = fv
  369. }
  370. ResidualChart.DataList = append(ResidualChart.DataList, &EdbData{DataTime: date, Value: fv, DataTimestamp: dataTimestamp})
  371. }
  372. ResidualChart.MaxData = MaxData
  373. ResidualChart.MinData = MinData
  374. }
  375. }
  376. return
  377. }
  378. func execStlPythonCode(path, toPath string, period, seasonal, trend, trendDeg, seasonalDeg, lowPassDeg int, fraction float64, robust bool) (err error) {
  379. pythonCode := `
  380. import json
  381. import warnings
  382. warnings.filterwarnings('ignore')
  383. import pandas as pd
  384. from statsmodels.tsa.seasonal import STL
  385. from statsmodels.nonparametric.smoothers_lowess import lowess
  386. from statsmodels.tsa.stattools import adfuller
  387. from statsmodels.stats.diagnostic import acorr_ljungbox
  388. import numpy as np
  389. file_path = r"%s"
  390. df = pd.read_excel(file_path, parse_dates=['日期'], engine='openpyxl')
  391. df.set_index('日期', inplace=True)
  392. df = df[df.index.notna()]
  393. period = %d
  394. seasonal = %d
  395. trend = %d
  396. fraction = %g
  397. seasonal_deg = %d
  398. trend_deg = %d
  399. low_pass_deg = %d
  400. robust = %s
  401. stl = STL(
  402. df['值'],
  403. period=period,
  404. seasonal=seasonal,
  405. trend=trend,
  406. low_pass=None,
  407. seasonal_deg=seasonal_deg,
  408. trend_deg=trend_deg,
  409. low_pass_deg=low_pass_deg,
  410. seasonal_jump=1,
  411. trend_jump=1,
  412. low_pass_jump=1,
  413. robust=robust
  414. )
  415. result = stl.fit()
  416. smoothed = lowess(df['值'], np.arange(len(df)), frac=fraction)
  417. trend_lowess = smoothed[:, 1]
  418. # 季节图
  419. seasonal_component = result.seasonal
  420. # 趋势图
  421. trend_lowess_series = pd.Series(trend_lowess, index=df.index)
  422. # 残差图
  423. residual_component = df['值'] - trend_lowess - seasonal_component
  424. # 计算打印残差的均值
  425. residual_mean = np.mean(residual_component)
  426. # 计算打印残差的方差
  427. residual_var = np.std(residual_component)
  428. # 计算打印残差的ADF检验结果, 输出p-value
  429. adf_result = adfuller(residual_component)
  430. # 根据p-value判断是否平稳
  431. lb_test = acorr_ljungbox(residual_component, lags=period, return_df=True)
  432. output_file = r"%s"
  433. with pd.ExcelWriter(output_file) as writer:
  434. # 保存季节图
  435. pd.Series(seasonal_component, index=df.index, name='值').to_frame().reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='季节', index=False)
  436. # 保存趋势图
  437. trend_lowess_series.to_frame(name='值').reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='趋势', index=False)
  438. # 保存残差图
  439. pd.Series(residual_component, index=df.index, name='值').to_frame().reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='残差', index=False)
  440. output = json.dumps({
  441. 'residual_mean': residual_mean,
  442. 'residual_var': residual_var,
  443. 'adf_p_value': adf_result[1],
  444. 'lb_test_p_value': lb_test['lb_pvalue'].values[0],
  445. 'lb_test_stat': lb_test['lb_stat'].values[0]
  446. })
  447. print(output)
  448. `
  449. robustStr := "True"
  450. if !robust {
  451. robustStr = "False"
  452. }
  453. pythonCode = fmt.Sprintf(pythonCode, path, period, seasonal, trend, fraction, seasonalDeg, trendDeg, lowPassDeg, robustStr, toPath)
  454. cmd := exec.Command(`python3`, "-c", pythonCode)
  455. _, err = cmd.CombinedOutput()
  456. if err != nil {
  457. return
  458. }
  459. defer cmd.Process.Kill()
  460. return
  461. }