stl.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. package stl
  2. import (
  3. "encoding/json"
  4. "eta/eta_api/models/data_manage"
  5. "eta/eta_api/models/data_manage/stl"
  6. "eta/eta_api/models/data_manage/stl/request"
  7. "eta/eta_api/models/data_manage/stl/response"
  8. "eta/eta_api/services/data/data_manage_permission"
  9. "eta/eta_api/services/elastic"
  10. "eta/eta_api/utils"
  11. "fmt"
  12. "os"
  13. "os/exec"
  14. "path/filepath"
  15. "strconv"
  16. "strings"
  17. "time"
  18. "github.com/rdlucklib/rdluck_tools/paging"
  19. "github.com/tealeg/xlsx"
  20. )
  21. const (
  22. ALL_DATE = iota + 1
  23. LAST_N_YEARS
  24. RANGE_DATE
  25. RANGE_DATE_TO_NOW
  26. )
  27. var EDB_DATA_CALCULATE_STL_TREND_CACHE = `eta:stl_decompose:trend:config_id:`
  28. var EDB_DATA_CALCULATE_STL_SEASONAL_CACHE = `eta:stl_decompose:seasonal:config_id:`
  29. var EDB_DATA_CALCULATE_STL_RESIDUAL_CACHE = `eta:stl_decompose:residual:config_id:`
  30. func GenerateStlEdbData(req *request.StlConfigReq, adminId int) (resp *response.StlPreviewResp, msg string, err error) {
  31. edbInfo, err := data_manage.GetEdbInfoById(req.EdbInfoId)
  32. if err != nil {
  33. if err.Error() == utils.ErrNoRow() {
  34. msg = "指标不存在"
  35. return
  36. }
  37. msg = "获取指标信息失败"
  38. return
  39. }
  40. var condition string
  41. var pars []interface{}
  42. switch req.DataRangeType {
  43. case ALL_DATE:
  44. case LAST_N_YEARS:
  45. condition += " AND data_time >=?"
  46. year := time.Now().Year()
  47. lastDate := time.Date(year-req.LastNYear, 1, 1, 0, 0, 0, 0, time.Local)
  48. pars = append(pars, lastDate)
  49. case RANGE_DATE:
  50. condition = " AND data_time >=? AND data_time <=?"
  51. pars = append(pars, req.StartDate, req.EndDate)
  52. case RANGE_DATE_TO_NOW:
  53. condition = " AND data_time >=?"
  54. pars = append(pars, req.StartDate)
  55. }
  56. condition += " AND edb_code =?"
  57. pars = append(pars, edbInfo.EdbCode)
  58. edbData, err := data_manage.GetAllEdbDataListByCondition(condition, pars, edbInfo.Source, edbInfo.SubSource)
  59. if err != nil {
  60. msg = "获取指标数据失败"
  61. return
  62. }
  63. var condMsg string
  64. if req.Period < 2 || req.Period > len(edbData) {
  65. condMsg += "period必须是一个大于等于2的正整数,且必须小于时间序列的长度"
  66. }
  67. if req.Seasonal < 3 || req.Seasonal%2 == 0 || req.Seasonal <= req.Period {
  68. if condMsg != "" {
  69. condMsg += "\n"
  70. }
  71. condMsg += "seasonal必须是一个大于等于3的奇整数,且必须大于period"
  72. }
  73. if req.Trend < 3 || req.Trend%2 == 0 || req.Trend <= req.Period {
  74. if condMsg != "" {
  75. condMsg += "\n"
  76. }
  77. condMsg += "trend必须是一个大于等于3的奇整数,且必须大于period"
  78. }
  79. if req.Fraction < 0 || req.Fraction > 1 {
  80. if condMsg != "" {
  81. condMsg += "\n"
  82. }
  83. condMsg += "fraction必须是一个介于[0-1]之间"
  84. }
  85. if 1 > req.TrendDeg || req.TrendDeg > 5 {
  86. if condMsg != "" {
  87. condMsg += "\n"
  88. }
  89. condMsg += "trend_deg请设置成1-5的整数"
  90. }
  91. if 1 > req.SeasonalDeg || req.SeasonalDeg > 5 {
  92. if condMsg != "" {
  93. condMsg += "\n"
  94. }
  95. condMsg += "seasonal_deg请设置成1-5的整数"
  96. }
  97. if 1 > req.LowPassDeg || req.LowPassDeg > 5 {
  98. if condMsg != "" {
  99. condMsg += "\n"
  100. }
  101. condMsg += "low_pass_deg请设置成1-5的整数"
  102. }
  103. if condMsg != "" {
  104. msg = condMsg
  105. err = fmt.Errorf("参数错误")
  106. return
  107. }
  108. dir, _ := os.Executable()
  109. exPath := filepath.Dir(dir) + "/static/stl_tmp"
  110. err = CheckOsPathAndMake(exPath)
  111. if err != nil {
  112. msg = "计算失败"
  113. return
  114. }
  115. loadFilePath := exPath + "/" + strconv.Itoa(adminId) + "_" + time.Now().Format(utils.FormatDateTimeUnSpace) + ".xlsx"
  116. err = SaveToExcel(edbData, loadFilePath)
  117. if err != nil {
  118. msg = "保存数据到Excel失败"
  119. return
  120. }
  121. defer os.Remove(loadFilePath)
  122. saveFilePath := exPath + "/" + strconv.Itoa(adminId) + "_" + time.Now().Format(utils.FormatDateTimeUnSpace) + "_res" + ".xlsx"
  123. result, err := execStlPythonCode(loadFilePath, saveFilePath, req.Period, req.Seasonal, req.Trend, req.TrendDeg, req.SeasonalDeg, req.LowPassDeg, req.Fraction, req.Robust)
  124. if err != nil {
  125. msg = "计算失败,请重新选择指标和参数后计算"
  126. }
  127. trendChart, seasonalChart, residualChart, err := ParseStlExcel(saveFilePath)
  128. if err != nil {
  129. msg = "解析Excel失败"
  130. return
  131. }
  132. defer os.Remove(saveFilePath)
  133. resp = new(response.StlPreviewResp)
  134. resp.OriginEdbInfo.Title = edbInfo.EdbName
  135. resp.OriginEdbInfo.Frequency = edbInfo.Frequency
  136. resp.OriginEdbInfo.Unit = edbInfo.Unit
  137. resp.OriginEdbInfo.DataList = formatEdbData(edbData)
  138. resp.TrendChartInfo = trendChart
  139. resp.TrendChartInfo.Title = edbInfo.EdbName + "Trend"
  140. resp.TrendChartInfo.Frequency = edbInfo.Frequency
  141. resp.TrendChartInfo.Unit = edbInfo.Unit
  142. resp.SeasonalChartInfo = seasonalChart
  143. resp.SeasonalChartInfo.Title = edbInfo.EdbName + "Seasonal"
  144. resp.SeasonalChartInfo.Frequency = edbInfo.Frequency
  145. resp.SeasonalChartInfo.Unit = edbInfo.Unit
  146. resp.ResidualChartInfo = residualChart
  147. resp.ResidualChartInfo.Title = edbInfo.EdbName + "Residual"
  148. resp.ResidualChartInfo.Frequency = edbInfo.Frequency
  149. resp.ResidualChartInfo.Unit = edbInfo.Unit
  150. resp.EvaluationResult.Mean = strconv.FormatFloat(result.ResidualMean, 'f', 4, 64)
  151. resp.EvaluationResult.Std = strconv.FormatFloat(result.ResidualVar, 'f', 4, 64)
  152. resp.EvaluationResult.AdfPValue = strconv.FormatFloat(result.AdfPValue, 'f', -1, 64)
  153. resp.EvaluationResult.LjungBoxPValue = strconv.FormatFloat(result.LbTestPValue, 'f', -1, 64)
  154. utils.Rc.Put(EDB_DATA_CALCULATE_STL_TREND_CACHE+strconv.Itoa(req.CalculateStlConfigId)+":"+resp.TrendChartInfo.Title, trendChart, time.Hour)
  155. utils.Rc.Put(EDB_DATA_CALCULATE_STL_TREND_CACHE+strconv.Itoa(req.CalculateStlConfigId)+":"+resp.SeasonalChartInfo.Title, seasonalChart, time.Hour)
  156. utils.Rc.Put(EDB_DATA_CALCULATE_STL_TREND_CACHE+strconv.Itoa(req.CalculateStlConfigId)+":"+resp.ResidualChartInfo.Title, residualChart, time.Hour)
  157. return
  158. }
  159. func formatEdbData(items []*data_manage.EdbData) []*response.EdbData {
  160. res := make([]*response.EdbData, 0, len(items))
  161. for _, item := range items {
  162. res = append(res, &response.EdbData{
  163. DataTime: item.DataTime,
  164. Value: strconv.FormatFloat(item.Value, 'f', -1, 64),
  165. })
  166. }
  167. return res
  168. }
  169. func CheckOsPathAndMake(path string) (err error) {
  170. if _, er := os.Stat(path); os.IsNotExist(er) {
  171. err = os.MkdirAll(path, os.ModePerm)
  172. }
  173. return
  174. }
  175. func ParseStlExcel(excelPath string) (TrendChart, SeasonalChart, ResidualChart response.ChartEdbInfo, err error) {
  176. file, err := xlsx.OpenFile(excelPath)
  177. if err != nil {
  178. return
  179. }
  180. for _, sheet := range file.Sheets {
  181. switch sheet.Name {
  182. case "季节":
  183. for i, row := range sheet.Rows {
  184. if i == 0 {
  185. continue
  186. }
  187. date := row.Cells[0].String()
  188. date = strings.Split(date, " ")[0]
  189. fv, _ := row.Cells[1].Float()
  190. value := strconv.FormatFloat(fv, 'f', 4, 64)
  191. SeasonalChart.DataList = append(SeasonalChart.DataList, &response.EdbData{DataTime: date, Value: value})
  192. }
  193. case "趋势":
  194. for i, row := range sheet.Rows {
  195. if i == 0 {
  196. continue
  197. }
  198. date := row.Cells[0].String()
  199. date = strings.Split(date, " ")[0]
  200. fv, _ := row.Cells[1].Float()
  201. value := strconv.FormatFloat(fv, 'f', 4, 64)
  202. TrendChart.DataList = append(TrendChart.DataList, &response.EdbData{DataTime: date, Value: value})
  203. }
  204. case "残差":
  205. for i, row := range sheet.Rows {
  206. if i == 0 {
  207. continue
  208. }
  209. date := row.Cells[0].String()
  210. date = strings.Split(date, " ")[0]
  211. fv, _ := row.Cells[1].Float()
  212. value := strconv.FormatFloat(fv, 'f', 4, 64)
  213. ResidualChart.DataList = append(ResidualChart.DataList, &response.EdbData{DataTime: date, Value: value})
  214. }
  215. }
  216. }
  217. return
  218. }
  219. func SaveToExcel(data []*data_manage.EdbData, filePath string) (err error) {
  220. xlsxFile := xlsx.NewFile()
  221. sheetNew, err := xlsxFile.AddSheet("Tmp")
  222. if err != nil {
  223. return
  224. }
  225. titleRow := sheetNew.AddRow()
  226. titleRow.AddCell().SetString("日期")
  227. titleRow.AddCell().SetString("值")
  228. for i, d := range data {
  229. row := sheetNew.Row(i + 1)
  230. row.AddCell().SetString(d.DataTime)
  231. row.AddCell().SetFloat(d.Value)
  232. }
  233. err = xlsxFile.Save(filePath)
  234. if err != nil {
  235. return
  236. }
  237. return
  238. }
  239. type STLResult struct {
  240. ResidualMean float64 `json:"residual_mean"`
  241. ResidualVar float64 `json:"residual_var"`
  242. AdfPValue float64 `json:"adf_p_value"`
  243. LbTestPValue float64 `json:"lb_test_p_value"`
  244. LbTestStat float64 `json:"lb_test_stat"`
  245. }
  246. func execStlPythonCode(path, toPath string, period, seasonal, trend, trendDeg, seasonalDeg, lowPassDeg int, fraction float64, robust bool) (stlResult *STLResult, err error) {
  247. pythonCode := `
  248. import pandas as pd
  249. from statsmodels.tsa.seasonal import STL
  250. from statsmodels.nonparametric.smoothers_lowess import lowess
  251. from statsmodels.tsa.stattools import adfuller
  252. from statsmodels.stats.diagnostic import acorr_ljungbox
  253. import numpy as np
  254. import json
  255. import warnings
  256. warnings.filterwarnings('ignore')
  257. file_path = r"%s"
  258. df = pd.read_excel(file_path, parse_dates=['日期'])
  259. df.set_index('日期', inplace=True)
  260. period = %d
  261. seasonal = %d
  262. trend = %d
  263. fraction = %g
  264. seasonal_deg = %d
  265. trend_deg = %d
  266. low_pass_deg = %d
  267. robust = %s
  268. stl = STL(
  269. df['值'],
  270. period=period,
  271. seasonal=seasonal,
  272. trend=trend,
  273. low_pass=None,
  274. seasonal_deg=seasonal_deg,
  275. trend_deg=trend_deg,
  276. low_pass_deg=low_pass_deg,
  277. seasonal_jump=1,
  278. trend_jump=1,
  279. low_pass_jump=1,
  280. robust=robust
  281. )
  282. result = stl.fit()
  283. smoothed = lowess(df['值'], df.index, frac=fraction)
  284. trend_lowess = smoothed[:, 1]
  285. # 季节图
  286. seasonal_component = result.seasonal
  287. # 趋势图
  288. trend_lowess_series = pd.Series(trend_lowess, index=df.index)
  289. # 残差图
  290. residual_component = df['值'] - trend_lowess - seasonal_component
  291. # 计算打印残差的均值
  292. residual_mean = np.mean(residual_component)
  293. # 计算打印残差的方差
  294. residual_var = np.std(residual_component)
  295. # 计算打印残差的ADF检验结果, 输出p-value
  296. adf_result = adfuller(residual_component)
  297. # 根据p-value判断是否平稳
  298. lb_test = acorr_ljungbox(residual_component, lags=period, return_df=True)
  299. output_file = r"%s"
  300. with pd.ExcelWriter(output_file) as writer:
  301. # 保存季节图
  302. pd.Series(seasonal_component, index=df.index, name='值').to_frame().reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='季节', index=False)
  303. # 保存趋势图
  304. trend_lowess_series.to_frame(name='值').reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='趋势', index=False)
  305. # 保存残差图
  306. pd.Series(residual_component, index=df.index, name='值').to_frame().reset_index().rename(columns={'index': '日期'}).to_excel(writer, sheet_name='残差', index=False)
  307. output = json.dumps({
  308. 'residual_mean': residual_mean,
  309. 'residual_var': residual_var,
  310. 'adf_p_value': adf_result[1],
  311. 'lb_test_p_value': lb_test['lb_pvalue'].values[0],
  312. 'lb_test_stat': lb_test['lb_stat'].values[0]
  313. })
  314. print(output)
  315. `
  316. robustStr := "True"
  317. if !robust {
  318. robustStr = "False"
  319. }
  320. pythonCode = fmt.Sprintf(pythonCode, path, period, seasonal, trend, fraction, seasonalDeg, trendDeg, lowPassDeg, robustStr, toPath)
  321. cmd := exec.Command(`D:\conda\envs\py311\python`, "-c", pythonCode)
  322. output, err := cmd.CombinedOutput()
  323. if err != nil {
  324. return
  325. }
  326. defer cmd.Process.Kill()
  327. if err = json.Unmarshal(output, &stlResult); err != nil {
  328. return
  329. }
  330. return
  331. }
  332. func SaveStlConfig(req *request.StlConfigReq, adminId int) (insertId int64, err error) {
  333. conf := new(stl.CalculateStlConfig)
  334. b, _ := json.Marshal(req)
  335. conf.Config = string(b)
  336. conf.SysUserId = adminId
  337. conf.CreateTime = time.Now()
  338. conf.ModifyTime = time.Now()
  339. insertId, err = conf.Insert()
  340. return
  341. }
  342. func SearchEdbInfoWithStl(adminId int, keyWord string, currentIndex, pageSize int) (resp data_manage.EdbInfoFilterDataResp, msg string, err error) {
  343. var edbInfoList []*data_manage.EdbInfoList
  344. noPermissionEdbInfoIdList := make([]int, 0) //无权限指标
  345. // 获取当前账号的不可见指标
  346. {
  347. obj := data_manage.EdbInfoNoPermissionAdmin{}
  348. confList, er := obj.GetAllListByAdminId(adminId)
  349. if er != nil && er.Error() != utils.ErrNoRow() {
  350. msg = "获取失败"
  351. err = fmt.Errorf("获取不可见指标配置数据失败,Err:" + er.Error())
  352. return
  353. }
  354. for _, v := range confList {
  355. noPermissionEdbInfoIdList = append(noPermissionEdbInfoIdList, v.EdbInfoId)
  356. }
  357. }
  358. if currentIndex <= 0 {
  359. currentIndex = 1
  360. }
  361. startSize := utils.StartIndex(currentIndex, pageSize)
  362. // 是否走ES
  363. isEs := false
  364. var total int64
  365. if keyWord != "" {
  366. var keyWordArr []string
  367. keyWordArr = append(keyWordArr, keyWord)
  368. newKeyWord := strings.Split(keyWord, " ")
  369. keyWordArr = append(keyWordArr, newKeyWord...)
  370. frequencyList := []string{"日度", "周度", "旬度", "月度", "季度"}
  371. // 普通的搜索
  372. total, edbInfoList, err = elastic.SearchEdbInfoDataByfrequency(utils.DATA_INDEX_NAME, keyWord, startSize, pageSize, 0, frequencyList, noPermissionEdbInfoIdList)
  373. isEs = true
  374. } else {
  375. var condition string
  376. var pars []interface{}
  377. // 普通指标
  378. condition += ` AND edb_info_type = ? `
  379. pars = append(pars, 0)
  380. // 无权限指标id
  381. lenNoPermissionEdbInfoIdList := len(noPermissionEdbInfoIdList)
  382. if lenNoPermissionEdbInfoIdList > 0 {
  383. condition += ` AND edb_info_id not in (` + utils.GetOrmInReplace(lenNoPermissionEdbInfoIdList) + `) `
  384. pars = append(pars, noPermissionEdbInfoIdList)
  385. }
  386. //频度
  387. condition += ` AND frequency IN ('日度', '周度', '旬度', '月度', '季度') `
  388. total, edbInfoList, err = data_manage.GetEdbInfoFilterList(condition, pars, startSize, pageSize)
  389. }
  390. if err != nil {
  391. edbInfoList = make([]*data_manage.EdbInfoList, 0)
  392. }
  393. page := paging.GetPaging(currentIndex, pageSize, int(total))
  394. edbInfoListLen := len(edbInfoList)
  395. classifyIdList := make([]int, 0)
  396. for i := 0; i < edbInfoListLen; i++ {
  397. edbInfoList[i].EdbNameAlias = edbInfoList[i].EdbName
  398. classifyIdList = append(classifyIdList, edbInfoList[i].ClassifyId)
  399. }
  400. // 当前列表中的分类map
  401. classifyMap := make(map[int]*data_manage.EdbClassify)
  402. if edbInfoListLen > 0 {
  403. classifyList, er := data_manage.GetEdbClassifyByIdList(classifyIdList)
  404. if er != nil {
  405. msg = "获取失败"
  406. err = fmt.Errorf("获取分类列表失败,Err:" + er.Error())
  407. return
  408. }
  409. for _, v := range classifyList {
  410. classifyMap[v.ClassifyId] = v
  411. }
  412. // 获取所有有权限的指标和分类
  413. permissionEdbIdList, permissionClassifyIdList, er := data_manage_permission.GetUserEdbAndClassifyPermissionList(adminId, 0, 0)
  414. if er != nil {
  415. msg = "获取失败"
  416. err = fmt.Errorf("获取所有有权限的指标和分类失败,Err:" + er.Error())
  417. return
  418. }
  419. // 如果是ES的话,需要重新查一下指标的信息,主要是为了把是否授权字段找出来
  420. if isEs {
  421. edbInfoIdList := make([]int, 0)
  422. for i := 0; i < edbInfoListLen; i++ {
  423. edbInfoIdList = append(edbInfoIdList, edbInfoList[i].EdbInfoId)
  424. tmpEdbInfo := edbInfoList[i]
  425. if currClassify, ok := classifyMap[tmpEdbInfo.ClassifyId]; ok {
  426. edbInfoList[i].HaveOperaAuth = data_manage_permission.CheckEdbPermissionByPermissionIdList(tmpEdbInfo.IsJoinPermission, currClassify.IsJoinPermission, tmpEdbInfo.EdbInfoId, tmpEdbInfo.ClassifyId, permissionEdbIdList, permissionClassifyIdList)
  427. }
  428. }
  429. tmpEdbList, er := data_manage.GetEdbInfoByIdList(edbInfoIdList)
  430. if er != nil {
  431. msg = "获取失败"
  432. err = fmt.Errorf("获取所有有权限的指标失败,Err:" + er.Error())
  433. return
  434. }
  435. edbInfoMap := make(map[int]*data_manage.EdbInfo)
  436. for _, v := range tmpEdbList {
  437. edbInfoMap[v.EdbInfoId] = v
  438. }
  439. for i := 0; i < edbInfoListLen; i++ {
  440. tmpEdbInfo, ok := edbInfoMap[edbInfoList[i].EdbInfoId]
  441. if !ok {
  442. continue
  443. }
  444. edbInfoList[i].IsJoinPermission = tmpEdbInfo.IsJoinPermission
  445. }
  446. }
  447. // 权限校验
  448. for i := 0; i < edbInfoListLen; i++ {
  449. tmpEdbInfoItem := edbInfoList[i]
  450. if currClassify, ok := classifyMap[tmpEdbInfoItem.ClassifyId]; ok {
  451. edbInfoList[i].HaveOperaAuth = data_manage_permission.CheckEdbPermissionByPermissionIdList(tmpEdbInfoItem.IsJoinPermission, currClassify.IsJoinPermission, tmpEdbInfoItem.EdbInfoId, tmpEdbInfoItem.ClassifyId, permissionEdbIdList, permissionClassifyIdList)
  452. }
  453. }
  454. }
  455. for i := 0; i < edbInfoListLen; i++ {
  456. for j := 0; j < edbInfoListLen; j++ {
  457. if (edbInfoList[i].EdbNameAlias == edbInfoList[j].EdbNameAlias) &&
  458. (edbInfoList[i].EdbInfoId != edbInfoList[j].EdbInfoId) &&
  459. !(strings.Contains(edbInfoList[i].EdbName, edbInfoList[i].SourceName)) {
  460. edbInfoList[i].EdbName = edbInfoList[i].EdbName + "(" + edbInfoList[i].SourceName + ")"
  461. }
  462. }
  463. }
  464. //新增搜索词记录
  465. {
  466. searchKeyword := new(data_manage.SearchKeyword)
  467. searchKeyword.KeyWord = keyWord
  468. searchKeyword.CreateTime = time.Now()
  469. go data_manage.AddSearchKeyword(searchKeyword)
  470. }
  471. resp = data_manage.EdbInfoFilterDataResp{
  472. Paging: page,
  473. List: edbInfoList,
  474. }
  475. return
  476. }