github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/functions/temporal/linear_regression.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package temporal 22 23 import ( 24 "fmt" 25 "math" 26 "time" 27 28 "github.com/m3db/m3/src/query/executor/transform" 29 "github.com/m3db/m3/src/query/ts" 30 xtime "github.com/m3db/m3/src/x/time" 31 ) 32 33 const ( 34 // PredictLinearType predicts the value of time series t seconds from now, 35 // based on the input series, using simple linear regression. 36 // PredictLinearType should only be used with gauges. 37 PredictLinearType = "predict_linear" 38 39 // DerivType calculates the per-second derivative of the time series, 40 // using simple linear regression. 41 // DerivType should only be used with gauges. 42 DerivType = "deriv" 43 ) 44 45 type linearRegressionProcessor struct { 46 fn linearRegFn 47 isDeriv bool 48 } 49 50 func (l linearRegressionProcessor) initialize( 51 _ time.Duration, 52 opts transform.Options, 53 ) processor { 54 return &linearRegressionNode{ 55 timeSpec: opts.TimeSpec(), 56 fn: l.fn, 57 isDeriv: l.isDeriv, 58 } 59 } 60 61 type linearRegFn func(float64, float64) float64 62 63 // NewLinearRegressionOp creates a new base temporal transform 64 // for linear regression functions. 65 func NewLinearRegressionOp( 66 args []interface{}, 67 optype string, 68 ) (transform.Params, error) { 69 var ( 70 fn linearRegFn 71 isDeriv bool 72 ) 73 74 switch optype { 75 case PredictLinearType: 76 if len(args) != 2 { 77 return emptyOp, fmt.Errorf("invalid number of args for %s: %d", 78 PredictLinearType, len(args)) 79 } 80 81 duration, ok := args[1].(float64) 82 if !ok { 83 return emptyOp, fmt.Errorf("unable to cast to scalar argument: %v for %s", 84 args[1], PredictLinearType) 85 } 86 87 fn = func(slope, intercept float64) float64 { 88 return slope*duration + intercept 89 } 90 91 case DerivType: 92 if len(args) != 1 { 93 return emptyOp, fmt.Errorf("invalid number of args for %s: %d", 94 DerivType, len(args)) 95 } 96 97 fn = func(slope, _ float64) float64 { 98 return slope 99 } 100 101 isDeriv = true 102 103 default: 104 return nil, fmt.Errorf("unknown linear regression type: %s", optype) 105 } 106 107 duration, ok := args[0].(time.Duration) 108 if !ok { 109 return emptyOp, fmt.Errorf("unable to cast to scalar argument: %v for %s", 110 args[0], optype) 111 } 112 113 l := linearRegressionProcessor{ 114 fn: fn, 115 isDeriv: isDeriv, 116 } 117 118 return newBaseOp(duration, optype, l) 119 } 120 121 type linearRegressionNode struct { 122 timeSpec transform.TimeSpec 123 fn linearRegFn 124 isDeriv bool 125 } 126 127 func (l linearRegressionNode) process( 128 dps ts.Datapoints, 129 iterBounds iterationBounds, 130 ) float64 { 131 if dps.Len() < 2 { 132 return math.NaN() 133 } 134 135 evaluationTime := iterBounds.end 136 slope, intercept := linearRegression(dps, evaluationTime, l.isDeriv) 137 return l.fn(slope, intercept) 138 } 139 140 func subSeconds(from xtime.UnixNano, sub xtime.UnixNano) float64 { 141 return float64(from-sub) / float64(time.Second) 142 } 143 144 // linearRegression performs a least-square linear regression analysis on the 145 // provided datapoints. It returns the slope, and the intercept value at the 146 // provided time. 147 // Uses this algorithm: https://en.wikipedia.org/wiki/Simple_linear_regression. 148 func linearRegression( 149 dps ts.Datapoints, 150 interceptTime xtime.UnixNano, 151 isDeriv bool, 152 ) (float64, float64) { 153 var ( 154 n float64 155 sumTimeDiff, sumVals float64 156 sumTimeDiffVals, sumTimeDiffSquared float64 157 valueCount int 158 ) 159 160 for _, dp := range dps { 161 if math.IsNaN(dp.Value) { 162 continue 163 } 164 165 if valueCount == 0 && isDeriv { 166 // set interceptTime as timestamp of first non-NaN dp 167 interceptTime = dp.Timestamp 168 } 169 170 valueCount++ 171 timeDiff := subSeconds(dp.Timestamp, interceptTime) 172 n += 1.0 173 sumVals += dp.Value 174 sumTimeDiff += timeDiff 175 sumTimeDiffVals += timeDiff * dp.Value 176 sumTimeDiffSquared += timeDiff * timeDiff 177 } 178 179 // need at least 2 non-NaN values to calculate slope and intercept 180 if valueCount == 1 { 181 return math.NaN(), math.NaN() 182 } 183 184 covXY := sumTimeDiffVals - sumTimeDiff*sumVals/n 185 varX := sumTimeDiffSquared - sumTimeDiff*sumTimeDiff/n 186 187 slope := covXY / varX 188 intercept := sumVals/n - slope*sumTimeDiff/n 189 190 return slope, intercept 191 }