Skip to content

Commit c95c430

Browse files
Adding feature direction rules (#1358) (#1374)
* add feature direction * changed condition --------- (cherry picked from commit 1a3b8c9) Signed-off-by: Amit Galitzky <amgalitz@amazon.com> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 94acda1 commit c95c430

15 files changed

+387
-50
lines changed

src/main/java/org/opensearch/ad/ml/IgnoreSimilarExtractor.java

+16-10
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,18 @@ public static ThresholdArrays processDetectorRules(AnomalyDetector detector) {
5353
if (rules != null) {
5454
for (Rule rule : rules) {
5555
for (Condition condition : rule.getConditions()) {
56-
processCondition(
57-
condition,
58-
featureNames,
59-
baseDimension,
60-
ignoreSimilarFromAbove,
61-
ignoreSimilarFromBelow,
62-
ignoreSimilarFromAboveByRatio,
63-
ignoreSimilarFromBelowByRatio
64-
);
56+
if (condition.getThresholdType() != ThresholdType.ACTUAL_IS_BELOW_EXPECTED
57+
&& condition.getThresholdType() != ThresholdType.ACTUAL_IS_OVER_EXPECTED) {
58+
processCondition(
59+
condition,
60+
featureNames,
61+
baseDimension,
62+
ignoreSimilarFromAbove,
63+
ignoreSimilarFromBelow,
64+
ignoreSimilarFromAboveByRatio,
65+
ignoreSimilarFromBelowByRatio
66+
);
67+
}
6568
}
6669
}
6770
}
@@ -100,7 +103,10 @@ private static void processCondition(
100103
int featureIndex = featureNames.indexOf(featureName);
101104

102105
ThresholdType thresholdType = condition.getThresholdType();
103-
double value = condition.getValue();
106+
Double value = condition.getValue();
107+
if (value == null) {
108+
value = 0d;
109+
}
104110

105111
switch (thresholdType) {
106112
case ACTUAL_OVER_EXPECTED_MARGIN:

src/main/java/org/opensearch/ad/ml/ThresholdingResult.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@
1212
package org.opensearch.ad.ml;
1313

1414
import java.time.Instant;
15+
import java.util.ArrayList;
1516
import java.util.Arrays;
1617
import java.util.Collections;
1718
import java.util.List;
1819
import java.util.Objects;
1920
import java.util.Optional;
2021

2122
import org.apache.commons.lang.builder.ToStringBuilder;
23+
import org.opensearch.ad.model.AnomalyDetector;
2224
import org.opensearch.ad.model.AnomalyResult;
25+
import org.opensearch.ad.model.Rule;
2326
import org.opensearch.timeseries.ml.IntermediateResult;
2427
import org.opensearch.timeseries.model.Config;
2528
import org.opensearch.timeseries.model.Entity;
@@ -331,6 +334,12 @@ public List<AnomalyResult> toIndexableResults(
331334
String taskId,
332335
String error
333336
) {
337+
List<Rule> rules = new ArrayList<>();
338+
if (detector instanceof AnomalyDetector) {
339+
AnomalyDetector detectorConfig = (AnomalyDetector) detector;
340+
rules = detectorConfig.getRules();
341+
}
342+
334343
return Collections
335344
.singletonList(
336345
AnomalyResult
@@ -358,7 +367,8 @@ public List<AnomalyResult> toIndexableResults(
358367
likelihoodOfValues,
359368
threshold,
360369
currentData,
361-
featureImputed
370+
featureImputed,
371+
rules
362372
)
363373
);
364374
}

src/main/java/org/opensearch/ad/model/AnomalyDetector.java

+11
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,17 @@ private void validateRules(List<Feature> features, List<Rule> rules) {
835835
this.issueType = ValidationIssueType.RULE;
836836
return;
837837
}
838+
} else if (thresholdType == ThresholdType.ACTUAL_IS_BELOW_EXPECTED
839+
|| thresholdType == ThresholdType.ACTUAL_IS_OVER_EXPECTED) {
840+
// Check if both operator and value are null
841+
if (condition.getOperator() != null || condition.getValue() != null) {
842+
this.errorMessage = SUPPRESSION_RULE_ISSUE_PREFIX
843+
+ "For threshold type \""
844+
+ thresholdType
845+
+ "\", both operator and value must be empty or null, as this rule compares actual to expected values directly";
846+
this.issueType = ValidationIssueType.RULE;
847+
return;
848+
}
838849
}
839850
}
840851
}

src/main/java/org/opensearch/ad/model/AnomalyResult.java

+93-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.io.IOException;
1818
import java.time.Instant;
1919
import java.util.ArrayList;
20+
import java.util.Arrays;
2021
import java.util.List;
2122
import java.util.Optional;
2223

@@ -312,6 +313,7 @@ public AnomalyResult(
312313
* @param threshold Current threshold
313314
* @param currentData imputed data if any
314315
* @param featureImputed whether feature is imputed or not
316+
* @param rules rules we apply on anomaly grade based on condition
315317
* @return the converted AnomalyResult instance
316318
*/
317319
public static AnomalyResult fromRawTRCFResult(
@@ -338,15 +340,20 @@ public static AnomalyResult fromRawTRCFResult(
338340
double[] likelihoodOfValues,
339341
Double threshold,
340342
double[] currentData,
341-
boolean[] featureImputed
343+
boolean[] featureImputed,
344+
List<Rule> rules
342345
) {
343346
List<DataByFeatureId> convertedRelevantAttribution = null;
344347
List<DataByFeatureId> convertedPastValuesList = null;
345348
List<ExpectedValueList> convertedExpectedValues = null;
349+
List<FeatureData> featuresForComparison = null;
346350

347351
int featureSize = featureData == null ? 0 : featureData.size();
348352

349353
if (grade > 0) {
354+
// Get the top feature names based on the relevant attribution criteria
355+
featuresForComparison = getTopFeatureNames(featureData, relevantAttribution);
356+
350357
if (relevantAttribution != null) {
351358
if (relevantAttribution.length == featureSize) {
352359
convertedRelevantAttribution = new ArrayList<>(featureSize);
@@ -425,6 +432,28 @@ public static AnomalyResult fromRawTRCFResult(
425432
);
426433
}
427434
}
435+
436+
for (FeatureData feature : featuresForComparison) {
437+
Double valueToCompare = getValueToCompare(feature, convertedPastValuesList, featureData);
438+
Double expectedValue = getExpectedValue(feature, convertedExpectedValues);
439+
if (valueToCompare == null || expectedValue == null) {
440+
continue; // Skip if either valueToCompare or expectedValue is missing
441+
}
442+
for (Rule rule : rules) {
443+
for (Condition condition : rule.getConditions()) {
444+
if (condition.getFeatureName().equals(feature.getFeatureName())) {
445+
ThresholdType thresholdType = condition.getThresholdType();
446+
if (thresholdType == ThresholdType.ACTUAL_IS_BELOW_EXPECTED && valueToCompare < expectedValue) {
447+
LOG.info("changed anomaly grade from: " + grade + " to 0d for detector: " + detectorId);
448+
grade = 0d;
449+
} else if (thresholdType == ThresholdType.ACTUAL_IS_OVER_EXPECTED && valueToCompare > expectedValue) {
450+
LOG.info("changed anomaly grade from: " + grade + " to 0d for detector: " + detectorId);
451+
grade = 0d;
452+
}
453+
}
454+
}
455+
}
456+
}
428457
}
429458

430459
List<FeatureImputed> featureImputedList = new ArrayList<>();
@@ -468,6 +497,69 @@ public static AnomalyResult fromRawTRCFResult(
468497
);
469498
}
470499

500+
private static Double getValueToCompare(
501+
FeatureData feature,
502+
List<DataByFeatureId> convertedPastValuesList,
503+
List<FeatureData> featureData
504+
) {
505+
String featureId = feature.getFeatureId();
506+
if (convertedPastValuesList != null) {
507+
for (DataByFeatureId data : convertedPastValuesList) {
508+
if (data.getFeatureId().equals(featureId)) {
509+
return data.getData();
510+
}
511+
}
512+
} else {
513+
for (FeatureData data : featureData) {
514+
if (data.getFeatureId().equals(featureId)) {
515+
return data.getData();
516+
}
517+
}
518+
}
519+
return 0d;
520+
}
521+
522+
private static Double getExpectedValue(FeatureData feature, List<ExpectedValueList> convertedExpectedValues) {
523+
Double expectedValue = 0d;
524+
if (convertedExpectedValues != null) {
525+
for (ExpectedValueList expectedValueList : convertedExpectedValues) {
526+
if (expectedValueList != null && expectedValueList.getValueList() != null) {
527+
for (var data : expectedValueList.getValueList()) {
528+
if (data.getFeatureId().equals(feature.getFeatureId())) {
529+
expectedValue = data.getData();
530+
}
531+
}
532+
}
533+
}
534+
}
535+
return expectedValue;
536+
}
537+
538+
private static List<FeatureData> getTopFeatureNames(List<FeatureData> featureData, double[] relevantAttribution) {
539+
List<FeatureData> topFeatureNames = new ArrayList<>();
540+
541+
if (relevantAttribution == null || relevantAttribution.length == 0 || (relevantAttribution.length != featureData.size())) {
542+
topFeatureNames.addAll(featureData);
543+
return topFeatureNames;
544+
}
545+
546+
// Find the maximum rounded value in a single pass and add corresponding feature names
547+
double maxRoundedAttribution = Arrays
548+
.stream(relevantAttribution)
549+
.map(value -> Math.round(value * 100.0) / 100.0)
550+
.max()
551+
.orElse(Double.NaN);
552+
553+
// Collect feature names with values that match the max rounded value
554+
for (int i = 0; i < relevantAttribution.length; i++) {
555+
if (Math.round(relevantAttribution[i] * 100.0) / 100.0 == maxRoundedAttribution) {
556+
topFeatureNames.add(featureData.get(i));
557+
}
558+
}
559+
560+
return topFeatureNames;
561+
}
562+
471563
public AnomalyResult(StreamInput input) throws IOException {
472564
super(input);
473565
this.modelId = input.readOptionalString();

src/main/java/org/opensearch/ad/model/Condition.java

+23-10
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ public class Condition implements Writeable, ToXContentObject {
2929
private String featureName;
3030
private ThresholdType thresholdType;
3131
private Operator operator;
32-
private double value;
32+
private Double value;
3333

34-
public Condition(String featureName, ThresholdType thresholdType, Operator operator, double value) {
34+
public Condition(String featureName, ThresholdType thresholdType, Operator operator, Double value) {
3535
this.featureName = featureName;
3636
this.thresholdType = thresholdType;
3737
this.operator = operator;
@@ -42,7 +42,7 @@ public Condition(StreamInput input) throws IOException {
4242
this.featureName = input.readString();
4343
this.thresholdType = input.readEnum(ThresholdType.class);
4444
this.operator = input.readEnum(Operator.class);
45-
this.value = input.readDouble();
45+
this.value = input.readBoolean() ? input.readDouble() : null;
4646
}
4747

4848
/**
@@ -56,7 +56,7 @@ public static Condition parse(XContentParser parser) throws IOException {
5656
String featureName = null;
5757
ThresholdType thresholdType = null;
5858
Operator operator = null;
59-
Double value = 0d;
59+
Double value = null;
6060

6161
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser);
6262
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
@@ -71,10 +71,18 @@ public static Condition parse(XContentParser parser) throws IOException {
7171
thresholdType = ThresholdType.valueOf(parser.text().toUpperCase(Locale.ROOT));
7272
break;
7373
case OPERATOR_FIELD:
74-
operator = Operator.valueOf(parser.text().toUpperCase(Locale.ROOT));
74+
if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
75+
operator = null; // Set operator to null if the field is missing
76+
} else {
77+
operator = Operator.valueOf(parser.text().toUpperCase(Locale.ROOT));
78+
}
7579
break;
7680
case VALUE_FIELD:
77-
value = parser.doubleValue();
81+
if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
82+
value = null; // Set value to null if the field is missing
83+
} else {
84+
value = parser.doubleValue();
85+
}
7886
break;
7987
default:
8088
break;
@@ -89,8 +97,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
8997
.startObject()
9098
.field(FEATURE_NAME_FIELD, featureName)
9199
.field(THRESHOLD_TYPE_FIELD, thresholdType)
92-
.field(OPERATOR_FIELD, operator)
93-
.field(VALUE_FIELD, value);
100+
.field(OPERATOR_FIELD, operator);
101+
if (value != null) {
102+
builder.field("value", value);
103+
}
94104
return xContentBuilder.endObject();
95105
}
96106

@@ -99,7 +109,10 @@ public void writeTo(StreamOutput out) throws IOException {
99109
out.writeString(featureName);
100110
out.writeEnum(thresholdType);
101111
out.writeEnum(operator);
102-
out.writeDouble(value);
112+
out.writeBoolean(value != null);
113+
if (value != null) {
114+
out.writeDouble(value);
115+
}
103116
}
104117

105118
public String getFeatureName() {
@@ -114,7 +127,7 @@ public Operator getOperator() {
114127
return operator;
115128
}
116129

117-
public double getValue() {
130+
public Double getValue() {
118131
return value;
119132
}
120133

src/main/java/org/opensearch/ad/model/ThresholdType.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,19 @@ public enum ThresholdType {
5757
* should be ignored if the ratio of the deviation from the expected to the actual
5858
* (b-a)/|a| is less than or equal to ignoreNearExpectedFromBelowByRatio.
5959
*/
60-
EXPECTED_OVER_ACTUAL_RATIO("the ratio of the expected value over the actual value");
60+
EXPECTED_OVER_ACTUAL_RATIO("the ratio of the expected value over the actual value"),
61+
62+
/**
63+
* Specifies a threshold for ignoring anomalies based on whether the actual value
64+
* is over the expected value returned from the model.
65+
*/
66+
ACTUAL_IS_OVER_EXPECTED("the actual value is over the expected value"),
67+
68+
/**
69+
* Specifies a threshold for ignoring anomalies based on whether the actual value
70+
* is below the expected value returned from the model.
71+
* */
72+
ACTUAL_IS_BELOW_EXPECTED("the actual value is below the expected value");
6173

6274
private final String description;
6375

0 commit comments

Comments
 (0)