Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding feature direction rules #1358

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions src/main/java/org/opensearch/ad/ml/IgnoreSimilarExtractor.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,18 @@ public static ThresholdArrays processDetectorRules(AnomalyDetector detector) {
if (rules != null) {
for (Rule rule : rules) {
for (Condition condition : rule.getConditions()) {
processCondition(
condition,
featureNames,
baseDimension,
ignoreSimilarFromAbove,
ignoreSimilarFromBelow,
ignoreSimilarFromAboveByRatio,
ignoreSimilarFromBelowByRatio
);
if (condition.getThresholdType() != ThresholdType.ACTUAL_IS_BELOW_EXPECTED
|| condition.getThresholdType() != ThresholdType.ACTUAL_IS_OVER_EXPECTED) {
ohltyler marked this conversation as resolved.
Show resolved Hide resolved
processCondition(
condition,
featureNames,
baseDimension,
ignoreSimilarFromAbove,
ignoreSimilarFromBelow,
ignoreSimilarFromAboveByRatio,
ignoreSimilarFromBelowByRatio
);
}
}
}
}
Expand Down Expand Up @@ -100,7 +103,10 @@ private static void processCondition(
int featureIndex = featureNames.indexOf(featureName);

ThresholdType thresholdType = condition.getThresholdType();
double value = condition.getValue();
Double value = condition.getValue();
if (value == null) {
value = 0d;
}

switch (thresholdType) {
case ACTUAL_OVER_EXPECTED_MARGIN:
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/org/opensearch/ad/ml/ThresholdingResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@
package org.opensearch.ad.ml;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.opensearch.ad.model.AnomalyDetector;
import org.opensearch.ad.model.AnomalyResult;
import org.opensearch.ad.model.Rule;
import org.opensearch.timeseries.ml.IntermediateResult;
import org.opensearch.timeseries.model.Config;
import org.opensearch.timeseries.model.Entity;
Expand Down Expand Up @@ -331,6 +334,12 @@ public List<AnomalyResult> toIndexableResults(
String taskId,
String error
) {
List<Rule> rules = new ArrayList<>();
if (detector instanceof AnomalyDetector) {
AnomalyDetector detectorConfig = (AnomalyDetector) detector;
rules = detectorConfig.getRules();
}

return Collections
.singletonList(
AnomalyResult
Expand Down Expand Up @@ -358,7 +367,8 @@ public List<AnomalyResult> toIndexableResults(
likelihoodOfValues,
threshold,
currentData,
featureImputed
featureImputed,
rules
)
);
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/org/opensearch/ad/model/AnomalyDetector.java
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,17 @@ private void validateRules(List<Feature> features, List<Rule> rules) {
this.issueType = ValidationIssueType.RULE;
return;
}
} else if (thresholdType == ThresholdType.ACTUAL_IS_BELOW_EXPECTED
|| thresholdType == ThresholdType.ACTUAL_IS_OVER_EXPECTED) {
// Check if both operator and value are null
if (condition.getOperator() != null || condition.getValue() != null) {
this.errorMessage = SUPPRESSION_RULE_ISSUE_PREFIX
+ "For threshold type \""
+ thresholdType
+ "\", both operator and value must be empty or null, as this rule compares actual to expected values directly";
this.issueType = ValidationIssueType.RULE;
return;
}
}
}
}
Expand Down
94 changes: 93 additions & 1 deletion src/main/java/org/opensearch/ad/model/AnomalyResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -312,6 +313,7 @@ public AnomalyResult(
* @param threshold Current threshold
* @param currentData imputed data if any
* @param featureImputed whether feature is imputed or not
* @param rules rules we apply on anomaly grade based on condition
* @return the converted AnomalyResult instance
*/
public static AnomalyResult fromRawTRCFResult(
Expand All @@ -338,15 +340,20 @@ public static AnomalyResult fromRawTRCFResult(
double[] likelihoodOfValues,
Double threshold,
double[] currentData,
boolean[] featureImputed
boolean[] featureImputed,
List<Rule> rules
) {
List<DataByFeatureId> convertedRelevantAttribution = null;
List<DataByFeatureId> convertedPastValuesList = null;
List<ExpectedValueList> convertedExpectedValues = null;
List<FeatureData> featuresForComparison = null;

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

if (grade > 0) {
// Get the top feature names based on the relevant attribution criteria
featuresForComparison = getTopFeatureNames(featureData, relevantAttribution);

if (relevantAttribution != null) {
if (relevantAttribution.length == featureSize) {
convertedRelevantAttribution = new ArrayList<>(featureSize);
Expand Down Expand Up @@ -425,6 +432,28 @@ public static AnomalyResult fromRawTRCFResult(
);
}
}

for (FeatureData feature : featuresForComparison) {
Double valueToCompare = getValueToCompare(feature, convertedPastValuesList, featureData);
Double expectedValue = getExpectedValue(feature, convertedExpectedValues);
if (valueToCompare == null || expectedValue == null) {
continue; // Skip if either valueToCompare or expectedValue is missing
}
for (Rule rule : rules) {
for (Condition condition : rule.getConditions()) {
if (condition.getFeatureName().equals(feature.getFeatureName())) {
ThresholdType thresholdType = condition.getThresholdType();
if (thresholdType == ThresholdType.ACTUAL_IS_BELOW_EXPECTED && valueToCompare < expectedValue) {
LOG.info("changed anomaly grade from: " + grade + " to 0d for detector: " + detectorId);
grade = 0d;
} else if (thresholdType == ThresholdType.ACTUAL_IS_OVER_EXPECTED && valueToCompare > expectedValue) {
LOG.info("changed anomaly grade from: " + grade + " to 0d for detector: " + detectorId);
grade = 0d;
}
}
}
}
}
}

List<FeatureImputed> featureImputedList = new ArrayList<>();
Expand Down Expand Up @@ -468,6 +497,69 @@ public static AnomalyResult fromRawTRCFResult(
);
}

private static Double getValueToCompare(
FeatureData feature,
List<DataByFeatureId> convertedPastValuesList,
List<FeatureData> featureData
) {
String featureId = feature.getFeatureId();
if (convertedPastValuesList != null) {
for (DataByFeatureId data : convertedPastValuesList) {
if (data.getFeatureId().equals(featureId)) {
return data.getData();
}
}
} else {
for (FeatureData data : featureData) {
if (data.getFeatureId().equals(featureId)) {
return data.getData();
}
}
}
return 0d;
}

private static Double getExpectedValue(FeatureData feature, List<ExpectedValueList> convertedExpectedValues) {
Double expectedValue = 0d;
if (convertedExpectedValues != null) {
for (ExpectedValueList expectedValueList : convertedExpectedValues) {
if (expectedValueList != null && expectedValueList.getValueList() != null) {
for (var data : expectedValueList.getValueList()) {
if (data.getFeatureId().equals(feature.getFeatureId())) {
expectedValue = data.getData();
}
}
}
}
}
return expectedValue;
}

private static List<FeatureData> getTopFeatureNames(List<FeatureData> featureData, double[] relevantAttribution) {
List<FeatureData> topFeatureNames = new ArrayList<>();

if (relevantAttribution == null || relevantAttribution.length == 0 || (relevantAttribution.length != featureData.size())) {
topFeatureNames.addAll(featureData);
return topFeatureNames;
}

// Find the maximum rounded value in a single pass and add corresponding feature names
double maxRoundedAttribution = Arrays
.stream(relevantAttribution)
.map(value -> Math.round(value * 100.0) / 100.0)
.max()
.orElse(Double.NaN);

// Collect feature names with values that match the max rounded value
for (int i = 0; i < relevantAttribution.length; i++) {
if (Math.round(relevantAttribution[i] * 100.0) / 100.0 == maxRoundedAttribution) {
topFeatureNames.add(featureData.get(i));
}
}

return topFeatureNames;
}

public AnomalyResult(StreamInput input) throws IOException {
super(input);
this.modelId = input.readOptionalString();
Expand Down
35 changes: 24 additions & 11 deletions src/main/java/org/opensearch/ad/model/Condition.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ public class Condition implements Writeable, ToXContentObject {
private String featureName;
private ThresholdType thresholdType;
private Operator operator;
private double value;
private Double value;

public Condition(String featureName, ThresholdType thresholdType, Operator operator, double value) {
public Condition(String featureName, ThresholdType thresholdType, Operator operator, Double value) {
this.featureName = featureName;
this.thresholdType = thresholdType;
this.operator = operator;
Expand All @@ -42,7 +42,7 @@ public Condition(StreamInput input) throws IOException {
this.featureName = input.readString();
this.thresholdType = input.readEnum(ThresholdType.class);
this.operator = input.readEnum(Operator.class);
this.value = input.readDouble();
this.value = input.readBoolean() ? input.readDouble() : null;
}

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

ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser);
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
Expand All @@ -70,11 +70,19 @@ public static Condition parse(XContentParser parser) throws IOException {
case THRESHOLD_TYPE_FIELD:
thresholdType = ThresholdType.valueOf(parser.text().toUpperCase(Locale.ROOT));
break;
case OPERATOR_FIELD:
operator = Operator.valueOf(parser.text().toUpperCase(Locale.ROOT));
case "operator":
amitgalitz marked this conversation as resolved.
Show resolved Hide resolved
if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
operator = null; // Set operator to null if the field is missing
} else {
operator = Operator.valueOf(parser.text().toUpperCase(Locale.ROOT));
}
break;
case VALUE_FIELD:
value = parser.doubleValue();
if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
ohltyler marked this conversation as resolved.
Show resolved Hide resolved
value = null;
} else {
value = parser.doubleValue();
}
break;
default:
break;
Expand All @@ -89,8 +97,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
.startObject()
.field(FEATURE_NAME_FIELD, featureName)
.field(THRESHOLD_TYPE_FIELD, thresholdType)
.field(OPERATOR_FIELD, operator)
.field(VALUE_FIELD, value);
.field(OPERATOR_FIELD, operator);
if (value != null) {
builder.field("value", value);
}
return xContentBuilder.endObject();
}

Expand All @@ -99,7 +109,10 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeString(featureName);
out.writeEnum(thresholdType);
out.writeEnum(operator);
out.writeDouble(value);
out.writeBoolean(value != null);
if (value != null) {
out.writeDouble(value);
}
}

public String getFeatureName() {
Expand All @@ -114,7 +127,7 @@ public Operator getOperator() {
return operator;
}

public double getValue() {
public Double getValue() {
return value;
}

Expand Down
14 changes: 13 additions & 1 deletion src/main/java/org/opensearch/ad/model/ThresholdType.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,19 @@ public enum ThresholdType {
* should be ignored if the ratio of the deviation from the expected to the actual
* (b-a)/|a| is less than or equal to ignoreNearExpectedFromBelowByRatio.
*/
EXPECTED_OVER_ACTUAL_RATIO("the ratio of the expected value over the actual value");
EXPECTED_OVER_ACTUAL_RATIO("the ratio of the expected value over the actual value"),

/**
* Specifies a threshold for ignoring anomalies based on whether the actual value
* is over the expected value returned from the model.
*/
ACTUAL_IS_OVER_EXPECTED("the actual value is over the expected value"),

/**
* Specifies a threshold for ignoring anomalies based on whether the actual value
* is below the expected value returned from the model.
* */
ACTUAL_IS_BELOW_EXPECTED("the actual value is below the expected value");

private final String description;

Expand Down
Loading
Loading