edb_data_calculate_stl.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  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. err = execStlPythonCode(loadFilePath, saveFilePath, stlConfig.Period, stlConfig.Seasonal, stlConfig.Trend, stlConfig.TrendDeg, stlConfig.SeasonalDeg, stlConfig.LowPassDeg, stlConfig.Fraction, stlConfig.Robust)
  156. if err != nil {
  157. msg = "执行Python代码失败"
  158. return
  159. }
  160. trendChart, seasonalChart, residualChart, nonTrendChartInfo, err := ParseStlExcel(saveFilePath)
  161. if err != nil {
  162. msg = "解析Excel失败"
  163. return
  164. }
  165. defer os.Remove(saveFilePath)
  166. edbInfo, err := models.GetEdbInfoById(edbInfoId)
  167. if err != nil {
  168. msg = "获取指标信息失败"
  169. return
  170. }
  171. err = SyncUpdateRelationEdbInfo(edbInfo, stlConfig, trendChart, seasonalChart, residualChart, nonTrendChartInfo)
  172. if err != nil {
  173. msg = "更新关联指标失败"
  174. return
  175. }
  176. return
  177. }
  178. func SyncUpdateRelationEdbInfo(edbInfo *models.EdbInfo, config EdbStlConfig, trendData, seasonalData, residualData, nonTrendChartInfo ChartEdbInfo) (err error) {
  179. configId, err := models.GetCalculateStlConfigMappingIdByEdbInfoId(edbInfo.EdbInfoId)
  180. if err != nil {
  181. return
  182. }
  183. mappingList, err := models.GetCalculateStlConfigMappingByConfigId(configId)
  184. if err != nil {
  185. return
  186. }
  187. for _, v := range mappingList {
  188. edbInfo, er := models.GetEdbInfoById(v.EdbInfoId)
  189. if er != nil {
  190. continue
  191. }
  192. switch v.StlEdbType {
  193. case utils.StlTypeTrend:
  194. // 趋势指标
  195. er = UpdateStlEdbData(edbInfo, config, edbInfo.EdbCode, trendData)
  196. case utils.StlTypeSeasonal:
  197. // 季节性指标
  198. er = UpdateStlEdbData(edbInfo, config, edbInfo.EdbCode, seasonalData)
  199. case utils.StlTypeResidual:
  200. // 残差指标
  201. er = UpdateStlEdbData(edbInfo, config, edbInfo.EdbCode, residualData)
  202. case utils.StlTypeNonTrend:
  203. // 季节+残差指标
  204. er = UpdateStlEdbData(edbInfo, config, edbInfo.EdbCode, nonTrendChartInfo)
  205. default:
  206. utils.FileLog.Info("未知的stlEdbType类型, mapping:%v", v)
  207. continue
  208. }
  209. if er != nil {
  210. utils.FileLog.Error("更新指标数据失败, edbInfoId:%v, err:%v", v.EdbInfoId, er)
  211. err = er
  212. continue
  213. }
  214. }
  215. // 同步更新计算配置
  216. newStlConf := &models.CalculateStlConfig{
  217. CalculateStlConfigId: configId,
  218. Config: edbInfo.CalculateFormula,
  219. ModifyTime: time.Now(),
  220. }
  221. err = newStlConf.Update([]string{"config", "modify_time"})
  222. return
  223. }
  224. func UpdateStlEdbData(edbInfo *models.EdbInfo, config EdbStlConfig, edbCode string, edbData ChartEdbInfo) (err error) {
  225. var dataList []*models.EdbDataCalculateStl
  226. for _, v := range edbData.DataList {
  227. dataTime, _ := time.Parse(utils.FormatDate, v.DataTime)
  228. dataList = append(dataList, &models.EdbDataCalculateStl{
  229. EdbInfoId: edbData.EdbInfoId,
  230. EdbCode: edbCode,
  231. DataTime: dataTime,
  232. Value: v.Value,
  233. CreateTime: time.Now(),
  234. ModifyTime: time.Now(),
  235. DataTimestamp: dataTime.UnixMilli(),
  236. })
  237. }
  238. err = models.DeleteAndInsertEdbDataCalculateStl(edbCode, dataList)
  239. if err != nil {
  240. return
  241. }
  242. models.ModifyEdbInfoDataStatus(int64(edbInfo.EdbInfoId), edbInfo.Source, edbInfo.SubSource, edbInfo.EdbCode)
  243. maxAndMinItem, _ := models.GetEdbInfoMaxAndMinInfo(edbInfo.Source, edbInfo.SubSource, edbInfo.EdbCode)
  244. if maxAndMinItem != nil {
  245. err = models.ModifyEdbInfoMaxAndMinInfo(edbInfo.EdbInfoId, maxAndMinItem)
  246. if err != nil {
  247. return
  248. }
  249. }
  250. bconfig, _ := json.Marshal(config)
  251. edbInfo.CalculateFormula = string(bconfig)
  252. edbInfo.ModifyTime = time.Now()
  253. err = edbInfo.Update([]string{"calculate_formula", "modify_time"})
  254. if err != nil {
  255. return
  256. }
  257. return
  258. }
  259. func CheckOsPathAndMake(path string) (err error) {
  260. if _, er := os.Stat(path); os.IsNotExist(er) {
  261. err = os.MkdirAll(path, os.ModePerm)
  262. }
  263. return
  264. }
  265. func SaveToExcel(data []*models.EdbInfoSearchData, filePath string) (err error) {
  266. xlsxFile := xlsx.NewFile()
  267. sheetNew, err := xlsxFile.AddSheet("Tmp")
  268. if err != nil {
  269. return
  270. }
  271. titleRow := sheetNew.AddRow()
  272. titleRow.AddCell().SetString("日期")
  273. titleRow.AddCell().SetString("值")
  274. for i, d := range data {
  275. row := sheetNew.Row(i + 1)
  276. row.AddCell().SetString(d.DataTime)
  277. row.AddCell().SetFloat(d.Value)
  278. }
  279. err = xlsxFile.Save(filePath)
  280. if err != nil {
  281. return
  282. }
  283. return
  284. }
  285. func ParseStlExcel(excelPath string) (TrendChart, SeasonalChart, ResidualChart, nonTrendChartInfo ChartEdbInfo, err error) {
  286. file, err := xlsx.OpenFile(excelPath)
  287. if err != nil {
  288. return
  289. }
  290. for _, sheet := range file.Sheets {
  291. switch sheet.Name {
  292. case "季节":
  293. var MinData, MaxData float64
  294. for i, row := range sheet.Rows {
  295. if i == 0 {
  296. continue
  297. }
  298. var date string
  299. var dataTimestamp int64
  300. if row.Cells[0].Type() == xlsx.CellTypeNumeric {
  301. dataNum, _ := strconv.ParseFloat(row.Cells[0].Value, 64)
  302. tmpTime := xlsx.TimeFromExcelTime(dataNum, false)
  303. date = tmpTime.Format(utils.FormatDate)
  304. dataTimestamp = tmpTime.UnixMilli()
  305. } else {
  306. timeDate, _ := time.Parse(utils.FormatDateTime, date)
  307. date = timeDate.Format(utils.FormatDate)
  308. dataTimestamp = timeDate.UnixMilli()
  309. }
  310. fv, _ := row.Cells[1].Float()
  311. if MinData == 0 || fv < MinData {
  312. MinData = fv
  313. }
  314. if MaxData == 0 || fv > MaxData {
  315. MaxData = fv
  316. }
  317. SeasonalChart.DataList = append(SeasonalChart.DataList, &EdbData{DataTime: date, Value: fv, DataTimestamp: dataTimestamp})
  318. }
  319. SeasonalChart.MinData = MinData
  320. SeasonalChart.MaxData = MaxData
  321. case "趋势":
  322. var MinData, MaxData float64
  323. for i, row := range sheet.Rows {
  324. if i == 0 {
  325. continue
  326. }
  327. var date string
  328. var dataTimestamp int64
  329. if row.Cells[0].Type() == xlsx.CellTypeNumeric {
  330. dataNum, _ := strconv.ParseFloat(row.Cells[0].Value, 64)
  331. tmpTime := xlsx.TimeFromExcelTime(dataNum, false)
  332. date = tmpTime.Format(utils.FormatDate)
  333. dataTimestamp = tmpTime.UnixMilli()
  334. } else {
  335. timeDate, _ := time.Parse(utils.FormatDateTime, date)
  336. date = timeDate.Format(utils.FormatDate)
  337. dataTimestamp = timeDate.UnixMilli()
  338. }
  339. fv, _ := row.Cells[1].Float()
  340. if MinData == 0 || fv < MinData {
  341. MinData = fv
  342. }
  343. if MaxData == 0 || fv > MaxData {
  344. MaxData = fv
  345. }
  346. TrendChart.DataList = append(TrendChart.DataList, &EdbData{DataTime: date, Value: fv, DataTimestamp: dataTimestamp})
  347. }
  348. TrendChart.MaxData = MaxData
  349. TrendChart.MinData = MinData
  350. case "残差":
  351. var MinData, MaxData float64
  352. for i, row := range sheet.Rows {
  353. if i == 0 {
  354. continue
  355. }
  356. var date string
  357. var dataTimestamp int64
  358. if row.Cells[0].Type() == xlsx.CellTypeNumeric {
  359. dataNum, _ := strconv.ParseFloat(row.Cells[0].Value, 64)
  360. tmpTime := xlsx.TimeFromExcelTime(dataNum, false)
  361. date = tmpTime.Format(utils.FormatDate)
  362. dataTimestamp = tmpTime.UnixMilli()
  363. } else {
  364. timeDate, _ := time.Parse(utils.FormatDateTime, date)
  365. date = timeDate.Format(utils.FormatDate)
  366. dataTimestamp = timeDate.UnixMilli()
  367. }
  368. fv, _ := row.Cells[1].Float()
  369. if MinData == 0 || fv < MinData {
  370. MinData = fv
  371. }
  372. if MaxData == 0 || fv > MaxData {
  373. MaxData = fv
  374. }
  375. ResidualChart.DataList = append(ResidualChart.DataList, &EdbData{DataTime: date, Value: fv, DataTimestamp: dataTimestamp})
  376. }
  377. ResidualChart.MaxData = MaxData
  378. ResidualChart.MinData = MinData
  379. }
  380. }
  381. // 数据处理
  382. dateList := make([]string, 0)
  383. residualDateMap := make(map[string]*EdbData)
  384. for _, item := range ResidualChart.DataList {
  385. if _, ok := residualDateMap[item.DataTime]; ok {
  386. continue
  387. }
  388. residualDateMap[item.DataTime] = item
  389. dateList = append(dateList, item.DataTime)
  390. }
  391. seasonalDateMap := make(map[string]*EdbData)
  392. for _, item := range SeasonalChart.DataList {
  393. if _, ok := seasonalDateMap[item.DataTime]; ok {
  394. continue
  395. }
  396. seasonalDateMap[item.DataTime] = item
  397. }
  398. // 季节性项+残差项
  399. {
  400. dataList := make([]*EdbData, 0)
  401. var minValue, maxValue sql.NullFloat64
  402. for _, date := range dateList {
  403. tmpResidual, ok := residualDateMap[date]
  404. if !ok {
  405. continue
  406. }
  407. tmpSeasonal, ok := seasonalDateMap[date]
  408. if !ok {
  409. continue
  410. }
  411. tmpValue := tmpResidual.Value + tmpSeasonal.Value
  412. tmpValue, _ = decimal.NewFromFloat(tmpValue).Round(4).Float64()
  413. dataList = append(dataList, &EdbData{
  414. DataTime: date,
  415. DataTimestamp: 0,
  416. Value: tmpValue,
  417. })
  418. // 如果没有设置最小值,或者设置的最小值比当前值还大,则需要更新最小值
  419. if !minValue.Valid || minValue.Float64 > tmpValue {
  420. err = minValue.Scan(tmpValue)
  421. if err != nil {
  422. return
  423. }
  424. }
  425. // 如果没有设置最大值,或者设置的最大值比当前值还小,则需要更新最大值
  426. if !maxValue.Valid || maxValue.Float64 < tmpValue {
  427. err = maxValue.Scan(tmpValue)
  428. if err != nil {
  429. return
  430. }
  431. }
  432. }
  433. nonTrendChartInfo.DataList = dataList
  434. nonTrendChartInfo.MinData = minValue.Float64
  435. nonTrendChartInfo.MaxData = maxValue.Float64
  436. }
  437. return
  438. }
  439. func execStlPythonCode(path, toPath string, period, seasonal, trend, trendDeg, seasonalDeg, lowPassDeg int, fraction float64, robust bool) (err error) {
  440. pythonCode := `
  441. import json
  442. import warnings
  443. warnings.filterwarnings('ignore')
  444. import pandas as pd
  445. from statsmodels.tsa.seasonal import STL
  446. from statsmodels.nonparametric.smoothers_lowess import lowess
  447. from statsmodels.tsa.stattools import adfuller
  448. from statsmodels.stats.diagnostic import acorr_ljungbox
  449. import numpy as np
  450. file_path = r"%s"
  451. df = pd.read_excel(file_path, parse_dates=['日期'], engine='openpyxl')
  452. df.set_index('日期', inplace=True)
  453. df = df[df.index.notna()]
  454. period = %d
  455. seasonal = %d
  456. trend = %d
  457. fraction = %g
  458. seasonal_deg = %d
  459. trend_deg = %d
  460. low_pass_deg = %d
  461. robust = %s
  462. stl = STL(
  463. df['值'],
  464. period=period,
  465. seasonal=seasonal,
  466. trend=trend,
  467. low_pass=None,
  468. seasonal_deg=seasonal_deg,
  469. trend_deg=trend_deg,
  470. low_pass_deg=low_pass_deg,
  471. seasonal_jump=1,
  472. trend_jump=1,
  473. low_pass_jump=1,
  474. robust=robust
  475. )
  476. result = stl.fit()
  477. smoothed = lowess(df['值'], np.arange(len(df)), frac=fraction)
  478. trend_lowess = smoothed[:, 1]
  479. # 季节图
  480. seasonal_component = result.seasonal
  481. # 趋势图
  482. trend_lowess_series = pd.Series(trend_lowess, index=df.index)
  483. # 残差图
  484. residual_component = df['值'] - trend_lowess - seasonal_component
  485. # 计算打印残差的均值
  486. residual_mean = np.mean(residual_component)
  487. # 计算打印残差的方差
  488. residual_var = np.std(residual_component)
  489. # 计算打印残差的ADF检验结果, 输出p-value
  490. adf_result = adfuller(residual_component)
  491. # 根据p-value判断是否平稳
  492. lb_test = acorr_ljungbox(residual_component, lags=period, return_df=True)
  493. output_file = r"%s"
  494. with pd.ExcelWriter(output_file) as writer:
  495. # 保存季节图
  496. pd.Series(seasonal_component, index=df.index, name='值').to_frame().reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='季节', index=False)
  497. # 保存趋势图
  498. trend_lowess_series.to_frame(name='值').reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='趋势', index=False)
  499. # 保存残差图
  500. pd.Series(residual_component, index=df.index, name='值').to_frame().reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='残差', index=False)
  501. output = json.dumps({
  502. 'residual_mean': residual_mean,
  503. 'residual_var': residual_var,
  504. 'adf_p_value': adf_result[1],
  505. 'lb_test_p_value': lb_test['lb_pvalue'].values[0],
  506. 'lb_test_stat': lb_test['lb_stat'].values[0]
  507. })
  508. print(output)
  509. `
  510. robustStr := "True"
  511. if !robust {
  512. robustStr = "False"
  513. }
  514. pythonCode = fmt.Sprintf(pythonCode, path, period, seasonal, trend, fraction, seasonalDeg, trendDeg, lowPassDeg, robustStr, toPath)
  515. cmd := exec.Command(utils.PYTHON_PATH, "-c", pythonCode)
  516. _, err = cmd.CombinedOutput()
  517. if err != nil {
  518. return
  519. }
  520. defer cmd.Process.Kill()
  521. return
  522. }