edb_data_calculate_stl.go 17 KB

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