github.com/go-graphite/carbonapi@v0.17.0/expr/functions/pearsonClosest/function.go (about) 1 package pearsonClosest 2 3 import ( 4 "container/heap" 5 "context" 6 "errors" 7 "math" 8 9 "github.com/dgryski/go-onlinestats" 10 "github.com/go-graphite/carbonapi/expr/helper" 11 "github.com/go-graphite/carbonapi/expr/interfaces" 12 "github.com/go-graphite/carbonapi/expr/types" 13 "github.com/go-graphite/carbonapi/pkg/parser" 14 ) 15 16 type pearsonClosest struct{} 17 18 func GetOrder() interfaces.Order { 19 return interfaces.Any 20 } 21 22 func New(configFile string) []interfaces.FunctionMetadata { 23 res := make([]interfaces.FunctionMetadata, 0) 24 f := &pearsonClosest{} 25 functions := []string{"pearsonClosest"} 26 for _, n := range functions { 27 res = append(res, interfaces.FunctionMetadata{Name: n, F: f}) 28 } 29 return res 30 } 31 32 // pearsonClosest(series, seriesList, n, direction=abs) 33 func (f *pearsonClosest) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) { 34 if e.ArgsLen() > 3 { 35 return nil, types.ErrTooManyArguments 36 } 37 38 ref, err := helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values) 39 if err != nil { 40 return nil, err 41 } 42 if len(ref) != 1 { 43 // TODO(nnuss) error("First argument must be single reference series") 44 return nil, types.ErrWildcardNotAllowed 45 } 46 47 compare, err := helper.GetSeriesArg(ctx, eval, e.Arg(1), from, until, values) 48 if err != nil { 49 return nil, err 50 } 51 52 n, err := e.GetIntArg(2) 53 if err != nil { 54 return nil, err 55 } 56 57 direction, err := e.GetStringNamedOrPosArgDefault("direction", 3, "abs") 58 if err != nil { 59 return nil, err 60 } 61 if direction != "pos" && direction != "neg" && direction != "abs" { 62 return nil, errors.New("direction must be one of: pos, neg, abs") 63 } 64 65 // NOTE: if direction == "abs" && len(compare) <= n : we'll still do the work to rank them 66 67 refValues := make([]float64, len(ref[0].Values)) 68 copy(refValues, ref[0].Values) 69 70 mh := make(types.MetricHeap, 0, len(compare)) 71 72 for index, a := range compare { 73 compareValues := make([]float64, len(a.Values)) 74 copy(compareValues, a.Values) 75 if len(refValues) != len(compareValues) { 76 // Pearson will panic if arrays are not equal length; skip 77 continue 78 } 79 80 value := onlinestats.Pearson(refValues, compareValues) 81 // Standardize the value so sort ASC will have strongest correlation first 82 switch { 83 case math.IsNaN(value): 84 // special case of at least one series containing all zeros which leads to div-by-zero in Pearson 85 continue 86 case direction == "abs": 87 value = math.Abs(value) * -1 88 case direction == "pos" && value >= 0: 89 value = value * -1 90 case direction == "neg" && value <= 0: 91 default: 92 continue 93 } 94 heap.Push(&mh, types.MetricHeapElement{Idx: index, Val: value}) 95 } 96 97 if n > len(mh) { 98 n = len(mh) 99 } 100 results := make([]*types.MetricData, n) 101 for i := range results { 102 v := heap.Pop(&mh).(types.MetricHeapElement) 103 results[i] = compare[v.Idx] 104 } 105 106 return results, nil 107 } 108 109 func (f *pearsonClosest) Description() map[string]types.FunctionDescription { 110 return map[string]types.FunctionDescription{ 111 "pearsonClosest": { 112 Description: ` 113 Implementation of Pearson product-moment correlation coefficient (PMCC) function(s) 114 115 .. code-block:: none 116 117 pearsonClosest( series, seriesList, n, direction="abs" ) 118 119 120 Return the n series in seriesList with closest Pearson score to the first series argument. 121 An optional direction parameter may also be given: 122 "abs" - (default) Series with any Pearson score + or - [-1 .. 1]. 123 "pos" - Only series with positive correlation score [0 .. 1] 124 "neg" - Only series with negative correlation score [1 .. 0] 125 126 127 The default is "abs" which is most correlated (in either direction) 128 129 Examples: 130 131 .. code-block:: none 132 133 # metrics from 'metric.forest.*'' that "look like" 'metric.abnormal'' (have closest correllation coeeficient) 134 pearsonClosest( metric.abnormal , metric.forest.* , 2, direction="pos" ) 135 136 137 .. code-block:: none 138 139 # 2 metrics from "metric.forest.*"" that are most negatively correlated to "metric.increasing" (ie. "metric.forest.decreasing" ) 140 pearsonClosest( metric.increasing , metric.forest.* , 2, direction="neg" ) 141 142 143 .. code-block:: none 144 145 # you'd get "metric.increasing", "metric.decreasing" 146 pearsonClosest( metric.increasing, group (metric.increasing, metric.decreasing, metric.flat, metric.sine), 2 ) 147 148 Note: 149 Pearson will discard epochs where either series has a missing value. 150 151 Additionally there is a special case where a series (or window) containing only zeros leads to a division-by-zero 152 and will manifest as if the entire window/series had missing values.`, 153 Function: "pearsonClosest(seriesList, seriesList, n, direction)", 154 Group: "Transform", 155 Module: "graphite.render.functions.custom", 156 Name: "pearsonClosest", 157 Params: []types.FunctionParam{ 158 { 159 Name: "seriesList", 160 Required: true, 161 Type: types.SeriesList, 162 }, 163 { 164 Name: "seriesList", 165 Required: true, 166 Type: types.SeriesList, 167 }, 168 { 169 Name: "n", 170 Required: true, 171 Type: types.Integer, 172 }, 173 { 174 Name: "direction", 175 Required: true, 176 Options: types.StringsToSuggestionList([]string{ 177 "abs", 178 "pos", 179 "neg", 180 }), 181 Type: types.String, 182 }, 183 }, 184 SeriesChange: true, // function aggregate metrics or change series items count 185 NameChange: true, // name changed 186 TagsChange: true, // name tag changed 187 ValuesChange: true, // values changed 188 }, 189 } 190 }