github.com/go-graphite/carbonapi@v0.17.0/expr/functions/tukey/function.go (about)

     1  package tukey
     2  
     3  import (
     4  	"container/heap"
     5  	"context"
     6  	"errors"
     7  	"math"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/go-graphite/carbonapi/expr/helper"
    12  	"github.com/go-graphite/carbonapi/expr/interfaces"
    13  	"github.com/go-graphite/carbonapi/expr/types"
    14  	"github.com/go-graphite/carbonapi/pkg/parser"
    15  )
    16  
    17  type tukey struct{}
    18  
    19  func GetOrder() interfaces.Order {
    20  	return interfaces.Any
    21  }
    22  
    23  func New(configFile string) []interfaces.FunctionMetadata {
    24  	res := make([]interfaces.FunctionMetadata, 0)
    25  	f := &tukey{}
    26  	functions := []string{"tukeyAbove", "tukeyBelow"}
    27  	for _, n := range functions {
    28  		res = append(res, interfaces.FunctionMetadata{Name: n, F: f})
    29  	}
    30  	return res
    31  }
    32  
    33  // tukeyAbove(seriesList,basis,n,interval=0) , tukeyBelow(seriesList,basis,n,interval=0)
    34  func (f *tukey) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) {
    35  	arg, err := helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  
    40  	basis, err := e.GetFloatArg(1)
    41  	if err != nil || basis <= 0 {
    42  		return nil, err
    43  	}
    44  
    45  	n, err := e.GetIntArg(2)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	if n < 1 {
    50  		return nil, errors.New("n must be larger or equal to 1")
    51  	}
    52  
    53  	var beginInterval int
    54  	endInterval := len(arg[0].Values)
    55  	if e.ArgsLen() >= 4 {
    56  		switch e.Arg(3).Type() {
    57  		case parser.EtConst:
    58  			beginInterval, err = e.GetIntArg(3)
    59  		case parser.EtString:
    60  			var i32 int32
    61  			i32, err = e.GetIntervalArg(3, 1)
    62  			beginInterval = int(i32)
    63  			beginInterval /= int(arg[0].StepTime)
    64  			// TODO(nnuss): make sure the arrays are all the same 'size'
    65  		default:
    66  			err = parser.ErrBadType
    67  		}
    68  		if err != nil {
    69  			return nil, err
    70  		}
    71  		if beginInterval < 0 && (-1*beginInterval) < endInterval {
    72  			// negative intervals are everything preceding the last 'interval' points
    73  			endInterval += beginInterval
    74  			beginInterval = 0
    75  		} else if beginInterval > 0 && beginInterval < endInterval {
    76  			// positive intervals are the last 'interval' points
    77  			beginInterval = endInterval - beginInterval
    78  			//endInterval = len(arg[0].Values)
    79  		} else {
    80  			// zero -or- beyond the len() of the series ; will revert to whole range
    81  			beginInterval = 0
    82  			//endInterval = len(arg[0].Values)
    83  		}
    84  	}
    85  
    86  	// gather all the valid points
    87  	var points []float64
    88  	for _, a := range arg {
    89  		for i, m := range a.Values[beginInterval:endInterval] {
    90  			if math.IsNaN(a.Values[beginInterval+i]) {
    91  				continue
    92  			}
    93  			points = append(points, m)
    94  		}
    95  	}
    96  
    97  	sort.Float64s(points)
    98  
    99  	first := int(0.25 * float64(len(points)))
   100  	third := int(0.75 * float64(len(points)))
   101  
   102  	iqr := points[third] - points[first]
   103  
   104  	max := points[third] + basis*iqr
   105  	min := points[first] - basis*iqr
   106  
   107  	isAbove := strings.HasSuffix(e.Target(), "Above")
   108  
   109  	var mh types.MetricHeap
   110  
   111  	// count how many points are above the threshold
   112  	for i, a := range arg {
   113  		var outlier int
   114  		for i, m := range a.Values[beginInterval:endInterval] {
   115  			if math.IsNaN(a.Values[beginInterval+i]) {
   116  				continue
   117  			}
   118  			if isAbove {
   119  				if m >= max {
   120  					outlier++
   121  				}
   122  			} else {
   123  				if m <= min {
   124  					outlier++
   125  				}
   126  			}
   127  		}
   128  
   129  		// not even a single anomalous point -- ignore this metric
   130  		if outlier == 0 {
   131  			continue
   132  		}
   133  
   134  		if len(mh) < n {
   135  			heap.Push(&mh, types.MetricHeapElement{Idx: i, Val: float64(outlier)})
   136  			continue
   137  		}
   138  		// current outlier count is is bigger than smallest max found so far
   139  		foutlier := float64(outlier)
   140  		if mh[0].Val < foutlier {
   141  			mh[0].Val = foutlier
   142  			mh[0].Idx = i
   143  			heap.Fix(&mh, 0)
   144  		}
   145  	}
   146  
   147  	if len(mh) < n {
   148  		n = len(mh)
   149  	}
   150  	results := make([]*types.MetricData, n)
   151  	// results should be ordered ascending
   152  	for len(mh) > 0 {
   153  		v := heap.Pop(&mh).(types.MetricHeapElement)
   154  		results[len(mh)] = arg[v.Idx]
   155  	}
   156  
   157  	return results, nil
   158  }
   159  
   160  // Description is auto-generated description, based on output of https://github.com/graphite-project/graphite-web
   161  func (f *tukey) Description() map[string]types.FunctionDescription {
   162  	return map[string]types.FunctionDescription{
   163  		"tukeyAbove": {
   164  			Description: "Tukey's range test, also known as the Tukey's test, Tukey method, Tukey's honest significance test, Tukey's HSD (honest significant difference) test,[1] or the Tukey–Kramer method, is a single-step multiple comparison procedure and statistical test. https://en.wikipedia.org/wiki/Tukey%27s_range_test",
   165  			Function:    "tukeyAbove(seriesList, basis, n, interval=0)",
   166  			Group:       "Transform",
   167  			Module:      "graphite.render.functions.custom",
   168  			Name:        "tukeyAbove",
   169  			Params: []types.FunctionParam{
   170  				{
   171  					Name:     "seriesList",
   172  					Required: true,
   173  					Type:     types.SeriesList,
   174  				},
   175  				{
   176  					Required: true,
   177  					Name:     "basis",
   178  					Type:     types.Float,
   179  				},
   180  				{
   181  					Required: true,
   182  					Name:     "n",
   183  					Type:     types.Integer,
   184  				},
   185  				{
   186  					Default: types.NewSuggestion(0),
   187  					Name:    "interval",
   188  					Type:    types.IntOrInterval,
   189  				},
   190  			},
   191  		},
   192  		"tukeyBelow": {
   193  			Description: "Tukey's range test, also known as the Tukey's test, Tukey method, Tukey's honest significance test, Tukey's HSD (honest significant difference) test,[1] or the Tukey–Kramer method, is a single-step multiple comparison procedure and statistical test. https://en.wikipedia.org/wiki/Tukey%27s_range_test",
   194  			Function:    "tukeyBelow(seriesList, basis, n, interval=0)",
   195  			Group:       "Transform",
   196  			Module:      "graphite.render.functions.custom",
   197  			Name:        "tukeyBelow",
   198  			Params: []types.FunctionParam{
   199  				{
   200  					Name:     "seriesList",
   201  					Required: true,
   202  					Type:     types.SeriesList,
   203  				},
   204  				{
   205  					Required: true,
   206  					Name:     "basis",
   207  					Type:     types.Float,
   208  				},
   209  				{
   210  					Required: true,
   211  					Name:     "n",
   212  					Type:     types.Integer,
   213  				},
   214  				{
   215  					Default: types.NewSuggestion(0),
   216  					Name:    "interval",
   217  					Type:    types.IntOrInterval,
   218  				},
   219  			},
   220  			SeriesChange: true, // function aggregate metrics or change series items count
   221  			NameChange:   true, // name changed
   222  			TagsChange:   true, // name tag changed
   223  			ValuesChange: true, // values changed
   224  		},
   225  	}
   226  }