Skip to content

Commit a95ab17

Browse files
committed
add feature direction
Signed-off-by: Amit Galitzky <amgalitz@amazon.com>
1 parent 4c545ab commit a95ab17

12 files changed

+204
-51
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

+10-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,11 @@ 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+
}
334342
return Collections
335343
.singletonList(
336344
AnomalyResult
@@ -358,7 +366,8 @@ public List<AnomalyResult> toIndexableResults(
358366
likelihoodOfValues,
359367
threshold,
360368
currentData,
361-
featureImputed
369+
featureImputed,
370+
rules
362371
)
363372
);
364373
}

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.";
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<String> featureNamesForComparison = 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+
featureNamesForComparison = getTopFeatureNames(featureData, relevantAttribution);
356+
350357
if (relevantAttribution != null) {
351358
if (relevantAttribution.length == featureSize) {
352359
convertedRelevantAttribution = new ArrayList<>(featureSize);
@@ -425,6 +432,66 @@ public static AnomalyResult fromRawTRCFResult(
425432
);
426433
}
427434
}
435+
for (String featureName : featureNamesForComparison) {
436+
Double valueToCompare = null;
437+
if (convertedPastValuesList != null) {
438+
Double pastValue = convertedPastValuesList
439+
.stream()
440+
.filter(data -> data.getFeatureId().equals(featureName))
441+
.map(DataByFeatureId::getData)
442+
.findFirst()
443+
.orElse(null);
444+
valueToCompare = pastValue != null ? pastValue : 0d;
445+
} else {
446+
int featureIndex = featureData
447+
.stream()
448+
.filter(data -> data.getFeatureId().equals(featureName))
449+
.map(featureData::indexOf)
450+
.findFirst()
451+
.orElse(-1);
452+
453+
valueToCompare = (featureIndex != -1 && currentData != null) ? currentData[featureIndex] : 0d;
454+
}
455+
456+
Double expectedValue = convertedExpectedValues
457+
.stream()
458+
.flatMap(evList -> evList.getValueList().stream())
459+
.filter(data -> data.getFeatureId().equals(featureName))
460+
.map(DataByFeatureId::getData)
461+
.findFirst()
462+
.orElse(null);
463+
464+
int featureIndex = featureData
465+
.stream()
466+
.filter(data -> data.getFeatureId().equals(featureName))
467+
.map(featureData::indexOf)
468+
.findFirst()
469+
.orElse(-1);
470+
471+
if (valueToCompare == null || expectedValue == null) {
472+
continue; // Skip if either valueToCompare or expectedValue is missing
473+
}
474+
475+
for (Rule rule : rules) {
476+
for (Condition condition : rule.getConditions()) {
477+
if (condition.getFeatureName().equals(featureName)) {
478+
ThresholdType thresholdType = condition.getThresholdType();
479+
480+
if (thresholdType == ThresholdType.ACTUAL_IS_BELOW_EXPECTED && valueToCompare < expectedValue) {
481+
grade = 0d;
482+
break;
483+
} else if (thresholdType == ThresholdType.ACTUAL_IS_OVER_EXPECTED && valueToCompare > expectedValue) {
484+
grade = 0d;
485+
break;
486+
}
487+
}
488+
}
489+
if (grade == 0)
490+
break;
491+
}
492+
if (grade == 0)
493+
break;
494+
}
428495
}
429496

430497
List<FeatureImputed> featureImputedList = new ArrayList<>();
@@ -468,6 +535,31 @@ public static AnomalyResult fromRawTRCFResult(
468535
);
469536
}
470537

538+
private static List<String> getTopFeatureNames(List<FeatureData> featureData, double[] relevantAttribution) {
539+
List<String> topFeatureNames = new ArrayList<>();
540+
541+
if (relevantAttribution == null || relevantAttribution.length == 0 || (relevantAttribution.length != featureData.size())) {
542+
featureData.forEach(feature -> topFeatureNames.add(feature.getFeatureId()));
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).getFeatureId());
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

+24-11
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) {
@@ -70,11 +70,19 @@ public static Condition parse(XContentParser parser) throws IOException {
7070
case THRESHOLD_TYPE_FIELD:
7171
thresholdType = ThresholdType.valueOf(parser.text().toUpperCase(Locale.ROOT));
7272
break;
73-
case OPERATOR_FIELD:
74-
operator = Operator.valueOf(parser.text().toUpperCase(Locale.ROOT));
73+
case "operator":
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;
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)