From 83ab98b382450773ae67a2fb4a2c5e9353569d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Mon, 30 Jun 2025 16:48:42 +0100 Subject: [PATCH 01/15] Create topics with retention --- .../kafka/data/generator/DataGenerator.java | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java index d05065e..1d30ef7 100644 --- a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java +++ b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java @@ -1,23 +1,38 @@ package com.github.streamshub.kafka.data.generator; +import org.apache.kafka.clients.admin.*; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; -import java.util.List; +import java.util.*; import java.util.function.Supplier; +import static org.apache.kafka.clients.CommonClientConfigs.*; +import static org.apache.kafka.common.config.TopicConfig.RETENTION_MS_CONFIG; + public class DataGenerator implements Runnable { + final static Map RETENTION_CONFIG = Collections.singletonMap(RETENTION_MS_CONFIG, String.valueOf(60 * 60 * 1000)); // 1 hour + final static int KAFKA_ADMIN_CLIENT_REQUEST_TIMEOUT_MS_CONFIG = 5000; + final static String KAFKA_ADMIN_CLIENT_ID_CONFIG = "data-generator-admin-client"; final String bootstrapServers; final List dataTypes; + final Properties kafkaAdminProps; public DataGenerator(String bootstrapServers, List dataTypes) { this.bootstrapServers = bootstrapServers; this.dataTypes = dataTypes; + + kafkaAdminProps = new Properties(); + kafkaAdminProps.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + kafkaAdminProps.put(CLIENT_ID_CONFIG, KAFKA_ADMIN_CLIENT_ID_CONFIG); + kafkaAdminProps.put(REQUEST_TIMEOUT_MS_CONFIG, String.valueOf(KAFKA_ADMIN_CLIENT_REQUEST_TIMEOUT_MS_CONFIG)); } @Override public void run() { + createTopics(dataTypes.stream().map(Data::topic).toList()); + if (Boolean.parseBoolean(System.getenv("USE_APICURIO_REGISTRY"))) { String registryUrl = System.getenv("REGISTRY_URL"); Producer producer = new KafkaProducer<>(KafkaClientProps.avro(bootstrapServers, registryUrl)); @@ -28,6 +43,28 @@ public void run() { } } + private void createTopics(List topicNames) { + try (AdminClient adminClient = AdminClient.create(kafkaAdminProps)) { + Set existingTopicNames = adminClient.listTopics().names().get(); + List newTopics = topicNames.stream() + .filter(topicName -> !existingTopicNames.contains(topicName)) // createTopics() will throw if topic already exists + .map( + topicName -> new NewTopic(topicName, Optional.empty(), Optional.empty()).configs(RETENTION_CONFIG) + ) + .toList(); + + if (newTopics.isEmpty()) { + return; + } + + adminClient.createTopics(newTopics).all().get(); + System.out.println("Kafka Data Generator : Successfully created topics: " + newTopics); + } catch (Exception e) { + System.out.println("Kafka Data Generator : Failed to create topics."); + throw new RuntimeException(e); + } + } + private ProducerRecord generateAvroRecord(Data data) { return getKafkaProducerRecord(data, data.generate()); } From 9f6d7ab8f0f12522001c243643849506db78831b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Mon, 30 Jun 2025 16:49:26 +0100 Subject: [PATCH 02/15] Increase emission rate to 1 event per second --- .../github/streamshub/kafka/data/generator/DataGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java index 1d30ef7..59448d1 100644 --- a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java +++ b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java @@ -98,7 +98,7 @@ private void send(Producer producer, Supplier Date: Wed, 9 Jul 2025 17:27:47 +0100 Subject: [PATCH 03/15] Add `batchSize()` to `Data` class --- .../java/com/github/streamshub/kafka/data/generator/Data.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/Data.java b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/Data.java index 01a68d0..559578c 100644 --- a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/Data.java +++ b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/Data.java @@ -5,6 +5,9 @@ public interface Data { String topic(); + default int batchSize() { + return 1; + } SpecificRecord generate(); String generateCsv(); Schema schema(); From 2770cd1367d1fcebd6d6bc65df5b1e0ab68554d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Wed, 9 Jul 2025 17:40:30 +0100 Subject: [PATCH 04/15] Add `batchSize()` and quantity multiplication to `SalesData` --- .../kafka/data/generator/examples/SalesData.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/examples/SalesData.java b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/examples/SalesData.java index 00a9d80..ce7e3d3 100644 --- a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/examples/SalesData.java +++ b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/examples/SalesData.java @@ -9,6 +9,7 @@ public class SalesData implements Data { private final Random random = new Random(); + private final static int USER_COUNT = 100; public String topic() { return "flink.sales.records"; @@ -16,6 +17,9 @@ public String topic() { public Schema schema() { return Sales.SCHEMA$; } + public int batchSize() { + return USER_COUNT; + } public SpecificRecord generate() { return Sales.newBuilder() @@ -40,7 +44,7 @@ private String generateInvoiceId() { } private String generateUserId() { - return "user-" + Math.abs(random.nextInt(100)); + return "user-" + Math.abs(random.nextInt(USER_COUNT)); } private String generateProductId() { @@ -48,7 +52,13 @@ private String generateProductId() { } private String generateQuantity() { - return String.valueOf(Math.abs(random.nextInt(3) + 1)); + int multiplier = 1; + + if (Math.abs(random.nextInt(batchSize() * 10)) < 2) { + multiplier = 10; + } + + return String.valueOf(Math.abs((1 + random.nextInt(3)) * multiplier )); } private String generateUnitCost() { From 646b82953cddccda48e982f3651c718d1616d0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Wed, 9 Jul 2025 17:41:16 +0100 Subject: [PATCH 05/15] Use `batchSize()` in `DataGenerator` --- .../kafka/data/generator/DataGenerator.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java index 59448d1..7d85fbc 100644 --- a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java +++ b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java @@ -6,6 +6,7 @@ import org.apache.kafka.clients.producer.ProducerRecord; import java.util.*; +import java.util.function.Function; import java.util.function.Supplier; import static org.apache.kafka.clients.CommonClientConfigs.*; @@ -36,13 +37,25 @@ public void run() { if (Boolean.parseBoolean(System.getenv("USE_APICURIO_REGISTRY"))) { String registryUrl = System.getenv("REGISTRY_URL"); Producer producer = new KafkaProducer<>(KafkaClientProps.avro(bootstrapServers, registryUrl)); - send(producer, () -> dataTypes.stream().map(this::generateAvroRecord).toList()); + send(producer, () -> generateTopicRecords(this::generateAvroRecord)); } else { Producer producer = new KafkaProducer<>(KafkaClientProps.csv(bootstrapServers)); - send(producer, () -> dataTypes.stream().map(this::generateCsvRecord).toList()); + send(producer, () -> generateTopicRecords(this::generateCsvRecord)); } } + private List> generateTopicRecords(Function> recordsGenerator) { + List> records = new ArrayList<>(); + + for (Data dataType : this.dataTypes) { + for (int i = 0; i < dataType.batchSize(); i++) { + records.add(recordsGenerator.apply(dataType)); + } + } + + return records; + } + private void createTopics(List topicNames) { try (AdminClient adminClient = AdminClient.create(kafkaAdminProps)) { Set existingTopicNames = adminClient.listTopics().names().get(); From bef1a966189c413ce322fbfc00d9b147528c5a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Wed, 9 Jul 2025 17:41:36 +0100 Subject: [PATCH 06/15] Add `flink-session-anomaly.yaml` --- .../flink-session-anomaly.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tutorials/anomaly-detection/flink-session-anomaly.yaml diff --git a/tutorials/anomaly-detection/flink-session-anomaly.yaml b/tutorials/anomaly-detection/flink-session-anomaly.yaml new file mode 100644 index 0000000..72d5ec3 --- /dev/null +++ b/tutorials/anomaly-detection/flink-session-anomaly.yaml @@ -0,0 +1,18 @@ +apiVersion: flink.apache.org/v1beta1 +kind: FlinkDeployment +metadata: + name: session-cluster-anomaly +spec: + image: quay.io/streamshub/flink-sql-runner:0.2.0 + flinkVersion: v2_0 + flinkConfiguration: + taskmanager.numberOfTaskSlots: "2" + serviceAccount: flink + jobManager: + resource: + memory: "2048m" + cpu: 1 + taskManager: + resource: + memory: "2048m" + cpu: 2 From b4b4abc4374575893cdfdd0f6d14a591f753b664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Wed, 9 Jul 2025 18:05:34 +0100 Subject: [PATCH 07/15] Add anomaly detection tutorial unfinished draft --- .../assets/avg_range.excalidraw.svg | 5 + .../assets/first_last.excalidraw.svg | 5 + .../assets/scenario.excalidraw.svg | 4 + .../assets/useful_match.excalidraw.svg | 5 + .../assets/useful_query.excalidraw.svg | 5 + docs/anomaly-detection/index.md | 322 ++++++++++++++++++ 6 files changed, 346 insertions(+) create mode 100644 docs/anomaly-detection/assets/avg_range.excalidraw.svg create mode 100644 docs/anomaly-detection/assets/first_last.excalidraw.svg create mode 100644 docs/anomaly-detection/assets/scenario.excalidraw.svg create mode 100644 docs/anomaly-detection/assets/useful_match.excalidraw.svg create mode 100644 docs/anomaly-detection/assets/useful_query.excalidraw.svg create mode 100644 docs/anomaly-detection/index.md diff --git a/docs/anomaly-detection/assets/avg_range.excalidraw.svg b/docs/anomaly-detection/assets/avg_range.excalidraw.svg new file mode 100644 index 0000000..967347b --- /dev/null +++ b/docs/anomaly-detection/assets/avg_range.excalidraw.svg @@ -0,0 +1,5 @@ + + +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWW/qSlx1MDAxMn6/v1wiyrzMSFx1MDAwN0/vy33LXHUwMDFlspCF7KNR5IBcdTAwMDNOXGJcdTAwMTAwXHUwMDEwcnX/+1STgI23XHUwMDE4cM7hjFx1MDAwMClK2kva3fXVV1VdXf7rj7W1dW/Ydtb/XFxbd94qdsOtduzB+lx1MDAwZtPedzpdt9WEQ2T0d7fV61RGZ9Y9r93989//9q+wKq2Xj6uchvPiNL0unPdcdTAwMWb4e23tr9FPOOJWzbVX/H27fsVqJ/T8QVx1MDAxNoZHqvTSPlx1MDAxOV06OmncmY5T8exmreH4h96gXHUwMDFkMyUmXHJDaJCaWohTNPlOjlx1MDAwZdyqV1x1MDAxZp1h6anP5Iy649bqXuopXHUwMDFmXfhzXHJNWrpep/XsbLVcdTAwMWGtjunnP7Bjvn4vXHUwMDFm7MpzrdPqNauTc7yO3ey27Vx1MDAwZVxmi3/eo9tolL3h6O4wtDCM66H/cf35XHUwMDAwONSedFx1MDAxNfzTWr3pdLtT/W217YrrmaHCyG81PWxcdTAwMTero1n6r9+njv3iXHUwMDE0zTQ1e43GpNltVlx1MDAxZDP46/bu1H9rVj//23iK/fmjny1/+313XHUwMDFjc2OCiSRSws/JXHUwMDExX86kJuHWUqs5kjlJXHUwMDExoUhI7vequ1xysuaN7vpoN7qOP1x1MDAwNaZrO2E5XGbKYkBcdTAwMWWpfVO8JPbZ4KC6dWA/vO+0XHUwMDBmbtHkMadk0nPevPXJgb8/f/OHr9eu2lx1MDAxZv3Bklx1MDAxMyQpo1hTNjnecJvP4bFttCrP/iP8XHUwMDExXHUwMDE4s1x1MDAxMG7S+zndx2nIcMwtbKRaaKZcYiNkXG5AXG4ji6dcdTAwMDKIYEsg+FxijCjcS1x1MDAxMFx1MDAxZUVcdTAwMTDjS1x1MDAwNVx1MDAxOTJcdTAwMDdk8PdAZi9cdTAwMWUyU2d/YkNJxamORYZWPFx0XHUwMDE5WFGYXHUwMDFizVx0nVx1MDAwN1x1MDAxYWnSq+C+s0ivL4xGXGLh4Ulg7lpNr+y+j3SDmGrdtV/chlx1MDAxOWwxdf1Gw62ZJ1+vQFedznrw8T1cdTAwMTd4Z3LCi1utXHUwMDA2eaJcdTAwMDI3td2m0ylmIZxWx625TbtxXHUwMDEx7bPd81rnTvej116n51x1MDAwNMfC2Vx1MDAxZss+tlxiT1x1MDAwMa79JFx1MDAwN/ThlW+4myf9/qDzdufUitlcdFx1MDAwZlx1MDAxM19cdTAwMDTHhKeJpphihbGgKoJXhSxcdTAwMTT8+PKUSHj+0C9cdTAwMDN6l4jw9lx1MDAxNyY8jJnmlCupg1x1MDAxMjyGNY+AfVx1MDAwMmusMcOE0dxcdTAwMTnv8fZ+6+V6r/eyefy4f8N7133lqZxcdTAwMTiPMqmxXHUwMDBl4mR+xkvvZ1x1MDAxYePBkKcyXHUwMDFltlgqglaMN2qdXHUwMDEzM8XsjIeJYFxcI8LiOFx1MDAwZmzEXGJmxuggXHUwMDE05lbKwHU5cZ7EKkCkvyvnfcE53855pc3LSqH5uK+6e+StfMP3z6v6MFxuXafRcNvdMONcdOzPajZcdTAwMTcvzHj+qE7gqnyxXlx1MDAwNriyUPsvpLiDrFx1MDAxNEeSKFx1MDAwZXxyXCI4ojgor58gpjxcdTAwMTHDWGLEtUb+ZTlcdTAwMTFcXLNE601W2Hxnh1eV56Orw9tK5T5cdTAwMTPB/Ui7bVx1MDAwNVx1MDAxM+9cdTAwMTjfVXaPN57r28XL472dgFBP3dbudFqD2YhTXHUwMDFiXHUwMDA3N1x1MDAxZuJMf/404lx1MDAxNFxmWVxmXGJPMCGA9thcdTAwMTRcdTAwMGVcdTAwMTX4kVIoXHRcbptzLlx1MDAxNIvgkFLDm5goLKmCn3G8SX9/3vwmIFx1MDAxZWbnTa5cdTAwMDRcdTAwMDO7RkecQvNELNFVXHUwMDA0i5JgXHUwMDA11+XNmogyimZcdTAwMTDeXGJrUlx1MDAxNEubRE21fjdtfkFbYdpcZnY6XHUwMDFm3uxf7Z/d3nrd90LPXHUwMDFintnvg53B8DGK3Fx1MDAwZvUyXHJdxKb9RC38v8f49Ft8OKLlXCLGJcJjaWFixFx1MDAxYYxJQZmgQZFcdTAwMWNcdTAwMDc7VbhxXGZTJpRGUoi8jVvKiVxu+EIzwLTdcsO86/+25k/A6I/J7//98fXZXHUwMDA1XHUwMDEwQP+CXGJPNuyut9V6eXE9eJJT04uIRvTsjrdcdLPmNmvhY06zmnBkdNWGgVHdsSMyXHUwMDAw11x1MDAwNY+BTLuh0IzTeGhccjKRcVk/6v2b+1x1MDAwMWEnO+dnz1x1MDAxNy8v5P0lP0grXHUwMDEyXHUwMDAz6lx1MDAxNaRcdTAwMTMgfbIwpME5UVhoXHUwMDEx57CmxGglkZwqLfNkXqwlU1x1MDAxOKyyuZg3V0hcdTAwMDekMHT+/yGi9UG/OXjeXHUwMDE4XHUwMDFjV19cdTAwMGJcdTAwMDdcck3sm+OnmFx1MDAxNcyoea0wtVTg41x1MDAwYsMwhOQxusFes0jw41x1MDAxM4lcdTAwMWaIWi60J1x1MDAwNm+rLTPjP1x1MDAxNe7nM0SiXHUwMDE4ZYRcdTAwMDJIY3CNk31YKrBGVKK8LWoqXGJcIlx1MDAwYq29XFyWd85cdTAwMGKUx0ejyFTrwma112on2dRTXVx1MDAwZVx1MDAxYtCRPk5Z0dPPNYtcdTAwMTl9WG877lx1MDAxNrmjr9t6r3d4VGu1ytUoQs0t87GiyXJhcIlcdTAwMTj3anEjmnKGMImulKxPXHUwMDA1XHUwMDEzossnXFxiIXGeq6LAuIQj6Fx1MDAxMFx1MDAwZorlrzeiydJcdTAwMTPu1LF2qzGsjebvK6pcdTAwMTWPpWJn7/6oWmrveE87YvP2oHeWKZKFQsumWkXDxizOXHUwMDFmXoWnXHUwMDEykHydnUxcdMZEIVx1MDAxMbRwXHUwMDAya56JOT6YSUCtZjpPx3eEWVx1MDAxM2r2yW1cdTAwMGU2/TnhqXl5NClcdTAwMGU1P4NeOifVXHUwMDA3WbeLJ/ZuwS2WOjf4uJ2JQYlaMWiuuLtZnEFcdTAwMTXWXHUwMDE0S4nijNvo0utkjVx1MDAxNWNcblx1MDAxZcc3wHFFoYGrvplCL1x1MDAxZVx1MDAxYW6/fvAgj/ZcdTAwMWYujm6qg8P+kZeJQolYUWi+UL6dgUI1XHUwMDAzl5Sg2MRcYozCrVx1MDAxM1xupZhoZFh06ShcdTAwMTQvNYXi3Cn0/r506Vx1MDAxMVxccmyYnP3eY6Hmle8yUShcdTAwMGbbritcbv1snVx1MDAxM3d3i1Oo5oJrXHUwMDE1VIE+XHUwMDFlebJcdTAwMTeKkJJcdTAwMWEhvqLQ35hCT3v81Xb6p7a6LTbbLfV69FwiMlx1MDAwNXwxRytcbs1cdTAwMTfK9lxmIV0wXzVFwbSaIIcmLr9KwCtcdTAwMTFSzJWX9K1cdTAwMTRcdTAwMWGfW7gsXHUwMDE0mpRFOD+FXG782jnau6+dtThF4vT6YFx1MDAxM6OYtdNcdTAwMThcbpVh23VFoZ+tc+LuIVx1MDAwNy+UXHUwMDBiQVx1MDAxOItccuSKSO7ghEI5wZIoKVdcdTAwMTT6XHUwMDFiU+h9837Y1s3ife/S6Vx1MDAwZa82SkeaXGYzUahcZq/IrCh0QShXZqBQITl8kY7Pz2fh1jFoXHUwMDE105IrleuWtHwolC41hdLcKXSISuS5gd5cdTAwMGbbV6/9g+M3Uie960zAXHUwMDBiL4ViJH0lPUk/wqGdZFEgrvJcdTAwMTNcdTAwMTKR+DhcdTAwMDNcdTAwMTKpkMpkKMQhkeBEJMKsgeuqKc3TmiUgXG5aXGJB50omXHUwMDFjQ7Hd61Tqdte599xcdTAwMTdn7Z9dXHUwMDA3gFLt/mupM1x1MDAxNr7sc16Wb+flfafzVL/c37e32W7ZO78psChsXHUwMDEzNo2GrV+uhKWVQsLE8ymVUVx1MDAwNsU4jkO5pFx1MDAxNsJYg+xJmO7AzuSlRvKMlLowjOuzXHUwMDEwquKIxadcdTAwMGZcdTAwMTKa6JJSzjhCKleX1KRcdTAwMTlJjSmdhU9TRNap17ebXHUwMDFla16x/it/uNSl82tnN7PIYlx1MDAxNFo3lPKrXV+Cfsk9eMlcdTAwMWO6vESWLyyz7kwyK2BcdTAwMDJiSlx1MDAxMKyP1uxcdTAwMTOpXHUwMDA3OItcYjSf4/Z5IHYjlrJvSseOfDgrn9+hd3AoXHUwMDFhyPPjQlPSNvtGLMFcYlJyUUjEhyaC4SecmFMoXHUwMDE4XHUwMDA1j5fpLEs4XHUwMDFmXHUwMDAzUnR6pHCGN7cvXHUwMDBihN/tXHUwMDBmvYdcdTAwMWJ0tSTyOYZcdTAwMWS1MMNcXFx1MDAxM2E2w2upfTzHXHUwMDA2bEyubDz8w6j92PI5O2on1IdimC+gjPxFdFx1MDAwNIZcdTAwMTRcdTAwMGW4QvHQmynNc9bt8oFRmMfVz1x1MDAxOEKYM1x1MDAxOPBdUVx1MDAxMfbl6WLm86c3XGbjXHUwMDFmaVx1MDAwN7+6W0CCRn+Gq0nR2bpfiNxAzHZcdTAwMDNqcU7kyGNQXGJxhJ1cdTAwMDLmM414Wlx1MDAxOMrnkKeAionnm0RqyVx1MDAxZTl6un5jZ+6pwNVB43r36apztrlcdTAwMTGTy5tYPlx1MDAwNVGLmUJcdTAwMGafX5/DRkaGolx1MDAxNlxyflwi2oHBXHUwMDE5hDI1/sYk4OMl20KX7OHa3frP9XBcdTAwMWZcdTAwMTCewc5QXHUwMDEy5sfMVVx1MDAxY2GK5LRBzjRVcs4k/DRDY//Ee7R3Onv9cu2pe35XO6lcZlx1MDAwZu+yXHUwMDE5XHUwMDFhP9Luy1x1MDAxYq/E3bBcdTAwMGJ3W7tcdTAwMDUlNp7ey689msN9j/mw/LihN1x1MDAwZTaOS51cdTAwMGJvcOcy18nLMOJcdTAwMDJcXIWFXHKjj45cdTAwMGU29l45euaXut+5Prvtk/P+rsyYlEQsTYhcdTAwMTSESa5oIHA3XG5cdTAwMDdQYTE8+aJoaVx1MDAxN6yYXHUwMDA1SlExQlRSaZclXHUwMDBiXHUwMDFkL1x1MDAxNZ5JdjxTMPe05NHsiJGBKJLgTFx1MDAxMFx1MDAwNkWrg9vYfz2cv4JcdTAwMDeIlGR4sT3wXHUwMDE3t6fFrY2j+/LG0c5MUWo1davvXHUwMDBmgsV3NJ9cdTAwMWTw6XOUul2WguGGOYBagZnOyfTuOqnIXHUwMDE000eLV5iLkZBcdTAwMTJcXDNJTVx1MDAwMVx1MDAwYuaPa4DrlaVNRFx1MDAxNVRcYuhcdTAwMTcu/HNWqmJtWlWweFUxU9ZcdTAwMTVcdTAwMWHNh4ovXHUwMDE2mpy5jLBEiKhcXDf/TECOg4L9a9aMk0XVfFxuUSlNsednXlZcdTAwMGVM4Gex32JcdTAwMDZLfaS0Kr2RwFlIYI6BfaXWWFHpXHUwMDEzrlx1MDAxMTe7bVx1MDAwNjxcIlx1MDAxM1NebVJcdTAwMWbSXHKLQFx1MDAxZlxu0Fx0zlx1MDAxONGMKVxyM8s1ljG9sDTVgpo+SqNVdEyZv7DDXHUwMDFjt7c4xW3Ovu/4jujHYa2+f+iy1s2Wulx1MDAxOHbo3lZ2T4iGXHUwMDAzrOOyWlx1MDAxMU24qpico1x1MDAxNtzJqlx1MDAwNZMrSCqKXHUwMDE5wzw2j01FvaVJtVx1MDAxZlx1MDAxM7tcdTAwMDCk5V5fq9wvblx1MDAxM3X5VmS7yD5W7snBacmLt6NmL5lcZpbfTFk5KYBJ72eal8FQelx1MDAwMcnPkslcdTAwMTHkrFxuR45a58VKQnnx2FxuWJggXHUwMDA0XHUwMDE2X2xuJ9B0XHUwMDEyKFx1MDAwMEZMMkpw7jWwpFxilm7OLTXlJ1eO/IJksiaszF8tuehuXHUwMDBmrlrt59qBuznYeD+66F3tZyc5wsbExE3d82ncXHUwMDFhxvuw/j+WXHUwMDE0Vsz3rcyXUPl8XHUwMDE25iNUgYmLcFxcRDCmUPpcdTAwMDTkgksw7VT+1Ff1tlx1MDAwZoqbb/3ns5rWt31v7/32OltpyVxm1Fx1MDAwN6yBZnEuUmCU3s/UXHUwMDAwm1x1MDAxY1NcdTAwMWZcdTAwMTOUalx1MDAxNqU+lVxuoVx1MDAxNVx1MDAwNY5a51x1MDAwNU1CwfH4+Fx1MDAxYWUwXHUwMDE3OHZZPmV9XHUwMDE5XFxcdTAwMWRcImCOcn9fwKzyXHUwMDFiYcD4LYI/u3ZyOlx1MDAwM2XdODg3XHUwMDAzpi9cdTAwMGWkRsDwaLFKSoFcZlx1MDAxMWI1Xb/V0Fx1MDAxZifm9StcdTAwMWbfaFxijCvwzFx041xivFxyXGZcdTAwMGVxIErm54QxXHUwMDBizCcwvkxFM47UckF5qVx1MDAwMmBcdIXQZ1x0gCEtXHUwMDE1w/FcdTAwMTCPJpFNXG5IQu9cdGZ0nsybqYeJJoyZvM+5rNxcXFx1MDAwM2DJgmo+hYiM+veL0PDPXHUwMDBmf1x1MDAxNZBcdTAwMDWuXHUwMDAylzCijIP6Rr6ZujZcdDwhXHUwMDBiK00/vzCfKlwiXCKZ4mHdt7NcdTAwMDP8/lTZPe/gN2/3tfq0vTdM6Fx1MDAxNFFcbnNTV59zXHUwMDEzUpTRTlnQV212xmEhlFx1MDAxMFwiKrY/LVx1MDAxOJa+1JmqJUFiLEFcdTAwMTVYN0hQTcT0XHUwMDEyotTYolxuZMYsd5qc66iWhFx1MDAxM1x1MDAwNJbC2KVcZum4ZVx1MDAwMk6NXHLFTFxyN1x1MDAwNf9cIlx1MDAxMKNZKcm1aSW5ePl5MspQNnHuXHUwMDE4LUlY4kIjZzA1ZmdZ/lqSXGKKf32h3UKipH5cdTAwMWNcclx1MDAwYulSaUlkSSA+ZlxuyWNK4Fx1MDAwMVx1MDAwMjlan/pcYkiAwYBTMGUpXHUwMDAzVM+nXCIve/VXu4xcdTAwMWba9+fNnfvdi1rXOVx1MDAxOcT3iIBLI7RcIlx1MDAwNDxcdTAwMWKFZdyCwS9Uiem6PvV9OZpbjJgtkZQrXHUwMDFljpogYnHFONdUcYlkTE5cdTAwMDWSJqeCmWRymFxmXHUwMDE5UHernIpcZipwhsL/WIK4M4l0bI5U8lx1MDAwNlx1MDAwMsGxXHUwMDA0/0zM5fNNhDzfVKaIkonqUfNeu7m26I49yt3iefnin/F7ipYlnVwi0sd8/Mh0vZa6+oGlZdZhJEiNpjz0Np6xOlBGXHUwMDA3Ulx1MDAxMY1cdTAwMDBpYjHQXHUwMDA2QMtcZqxcXFx1MDAxNSDClTbIoFxyjmbQXHUwMDA2WCAhXGKOfb9cdTAwMDCO6lxiPz+CXHUwMDAyuJSaL1x1MDAwNJSmXHUwMDBlvjVcdTAwMDNRSErJQlx1MDAwMaajjaXXXHUwMDA24S7mo1xm0rfMpLpLQnDw+1xmqzMwfoKbgD/cJWSZMK9UXHUwMDEym3dcdTAwMDP51nbAXeLmkGBIcFx1MDAxNtwz6mdVYWxJ8HONOc5cdTAwMTjCK+2QqFx1MDAxZI5cdTAwMTd2l6hS4Fx0yNicKlx1MDAxY6035y+cUlx1MDAwNDDUc71yL81dMllVWlx1MDAwNsrx/LKgUpKgmk8hXCKj/u1cIporN28pfU/ltLdcdTAwMDRcblx1MDAxMituumdcdTAwMTLfdOCkT9dcdTAwMDRIXVx1MDAxMMFcdTAwMTRcdTAwMDM7kXGqouGbTP7S9tOwUVx1MDAxN+2b21NB3041abbOXHUwMDBivaRcdTAwMTQrpcArgDnWXFxzpnXUh5OWXHUwMDExK9AsIJRcXIODXHUwMDExXHUwMDE125/mQKU/Wep704i0uNZcdTAwMWNcdTAwMDRcdTAwMDRjwvj0O+onWelIMoowi9l+vcpK/5CJObViwquaYlx1MDAwYmtKZWxaXHUwMDE0V5o6OVbEXHUwMDE0XHUwMDAxd1/p3Fx1MDAxN5S/cS+reX23pmwh/+mydFm+/Fx1MDAxZHLS4zs6s/H0x+d4rtvtdtmD0ZwowvW+61xmNqMw+sfj6GPmZaRVXGaEnFx1MDAxMSX//cff/1x1MDAwM4GMNGUifQ==2230USER-350123purchase_time (seconds)TYPICAL_SALE31FIRST()LAST()UNUSUAL_SALE \ No newline at end of file diff --git a/docs/anomaly-detection/assets/first_last.excalidraw.svg b/docs/anomaly-detection/assets/first_last.excalidraw.svg new file mode 100644 index 0000000..37107be --- /dev/null +++ b/docs/anomaly-detection/assets/first_last.excalidraw.svg @@ -0,0 +1,5 @@ + + +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWVPqSFx1MDAxYr4/v8JybuarXHUwMDFhmN6XuXNBRVx1MDAxMVx1MDAxNVxc+WqKXG5cdTAwMTAghy1AXHUwMDAwcer89+mgkFx1MDAxMDpcdTAwMTggKk5cdTAwMDGWJZ1OeNP9Pnnerdt/fuzt7Ttj29z/a2/ffC5cdTAwMWJNq9IzRvt/uO1Ds9e3Om11XGJNPvc7g1550rPuOHb/rz//9M5Iljut17PMptky205f9fu/+ry398/ktzpiVSbndlx1MDAwNz3wclxuK6KWfipcdTAwMTVKjZ/9enNy6qTTVJieWXaMdq1peoeeVTuEXHUwMDAwzFx1MDAxYcaqQVx1MDAwMJAkWEBcdTAwMDJcdJaAMTE7OrIqTn3SI1x0/C8+61E3rVrdUV24TEr/i826vIrw1573pX2n12mYR51mp+fK+Vx1MDAxYjTdtydlySg3ar3OoF2Z9XF6RrtvXHUwMDFiPTUsXr+q1WzmnfHk6mpo1TDuXHUwMDA3vuPh7Vx1MDAwNmCgPews9aW1etvs9+fk7dhG2XLcofJcdTAwMGadK6Gdrkxm6W9Ppp7RMtPuNLVcdTAwMDfN5qzZaldMd/D3XHI0923tytu3TafYmz/81vLLk9003Vx1MDAwYkOCJEaAcTk74ulcdTAwMTlcdTAwMDRcdTAwMDRcdTAwMDebs532ROmgkJghgXw9rP6x0jZnct2q0eyb3iS4wqWCmujXRp9Gti5cdTAwMWVOX06rJ4fO4VV7PDwqXFyeP2RmNzqnlY757OzPXHUwMDBl/Hr7y1x1MDAxYsCBXTFe5YGcXCLAIWdcZlHvVptWu1x1MDAxMVx1MDAxY91mp9zwbuGHb9RcdTAwMDLIWS7nvIxcdTAwMDHQIJmEXHUwMDEzxZZEjSBC81x1MDAxMII8KZdCXGLBJHOhwyDAXHUwMDE0UnVLi1x1MDAxOFwidKtAg9ZcdTAwMDBccvxcdTAwMTjQYD1o5nq/oUPNXHUwMDBlXHUwMDE2lHGoXHUwMDAzXHUwMDA3ZiRcdTAwMWNcdTAwMWOYSOQ+XHUwMDA311x1MDAwMUec+uupo6uG6vahb/Y6bSdvvUyeXHUwMDBmbK71xGhZTXe42dz5XHUwMDA3Tavm3vp+WYlq9vb9t+9YintmXHUwMDFkWlal4ueKsrqoYbXNXjpcbul0elbNalx1MDAxYs3bRZmNgdPJmf1XqZ3ewPSPhXk21X6YRHRcdHSH92c3T09O/yUxcMY3xssoNVx1MDAxYVdcdTAwMTeha/R6nVFcdTAwMDC7gMxcdTAwMTOeZN7nKT69llx1MDAxOVx1MDAxY5HwXHUwMDFhvytcdTAwMWU/iMSyUUlcZoWSmFQ4ZZiwXHUwMDA1tnJVmFx1MDAwN1x1MDAxYqcopYwySFx0XovBwkFcbpjkSqB1QGp3rCBBen/teVx1MDAxMzD5MPv77z/e751QXG7onbBAlE2j71x1MDAxY3VaLctRd3LtSrHwRHSMnnOoZs1q14LHzHYl5MjkrFx1MDAwM1x1MDAxN0Z101jQXHUwMDAxdZ7/mNJpK2Bjms1SZ1x1MDAxNImN87Iqz1x1MDAxZYsjRK5SuZvGbauFXlrxQVogXHKod5BcdTAwMGWB9NXGkGaQXHUwMDBiyORcInjduaDBxlx1MDAxOe9yxClcdTAwMTbSZ81uXHUwMDBlaSg5XHUwMDExUEDqXHLLV0Hap4WB/v9BRMvzYXvUOFx1MDAxOF1WuonzpkTG4+XPqyj2tYA4KXwvT1x1MDAxOcZcdTAwMDEkT9GtvKAk8r88SvBcZurtQnuoXHUwMDE3Wum4M/6pcM9Ft6iVT0NcdTAwMTBWINVZ1D6EXHUwMDA1gK1McI6VTb199vRdPpVLQKa3qtFcXOvGVrXTscNM6jmRg/bzgoxzVvT8fa1iRjeer8f3w9t6ySaVq0xcblVPjMFT9NhcdTAwMTGD3nRGi1x1MDAxZFx1MDAwNVx1MDAwM0PezIXGjrwuW43az+fo281jR1xicuBcdTAwMTKjzj3mWFx1MDAwNFtndjdcdTAwMTFcdTAwMDJyQtZcIumZeJrIXHUwMDExsVx1MDAxMsfX8tKw8vzgyjxcdTAwMWGwg1T2Pp7I0epG/Vx1MDAxMtwsl3NZ5IhcdTAwMTGwi1x1MDAxY31Z5OguOs9cdDU/TI2xLqwqcWhUXHUwMDE1S1x1MDAwNVxmJEGc9ut/KG70XHUwMDBl4Xx43OiibpvWXHUwMDExKuDusTxcdTAwMWRcXGRqnU6+slxiXFz3kjGFjbbL6NxcIlx1MDAxN/M+Kn2FR40wJVx1MDAwMFwiqnMxcbiLXHQl5VDZo3FGjZSLiShQXHUwMDAyUb9afn3UXGJtvYc5d8zuNMe1yfy9x8Csmk33TouZStZOOT9T7PDpfHBcdTAwMTOJgVx1MDAwMVxuXHUwMDAwWXiKMFx1MDAwNTLRIFx1MDAxOX9/Vv0gJD9EZ1VcdTAwMDQhXHUwMDEygPldelx1MDAxZq2iYONcZrOEK9RKXCI9qMeEWcgh8LhuXHJaXHUwMDA1WlpFYq71y1x1MDAxY0dcdTAwMTBCoOu7jHfmVaXE60b6yjhJWOls71x1MDAxMV7akVx1MDAxONSfQdkxaFxmuHvcnEFcdTAwMDWUXHUwMDE4clx1MDAwZXTRXHUwMDFjXHUwMDEymndRKMZcYuFcdTAwMGaA445CfWd9MIXelprWsH5e4pmz0m3msTK6XHUwMDE4ZpxIXHUwMDE0itiOQuOF8tNcblx1MDAxNCrd6Fx1MDAwMFx1MDAwMjqrXHUwMDE3wtBcdTAwMDAsxFC5pS6Lblx1MDAxZIXqPdNtodAwXHUwMDFmdH1cbi1cdTAwMTazd1x1MDAwZYJZ01CTczaoJmpOvlx1MDAxMIlCadB23VHoW+uauCtsTqGSMiqF/1x1MDAxMejhkYZ7oVx1MDAwMFxiLlx1MDAwMaA7XG79xlx1MDAxNHo9oF3DXHUwMDFjXlx1MDAxYuIp3bY7optpsUhcdTAwMTlOSMGOQuOFsrFCXHUwMDBlU5mvXHUwMDEyXHUwMDAzgLRJTFx1MDAxOJr34Fxur4hxXHUwMDE2Z1x1MDAxMjNcdTAwMWVcbkVbTaEodlxuZbDby5xcdTAwMTZrN1x1MDAxZIpcdTAwMDG7fjg/hEBTLKShUFx1MDAxZbRdd1x1MDAxNPrWuibuSjF4oZQxRIg2kMtcdTAwMTZykzNcbqVcYnIkON9R6Dem0GK7OLZlO11cdTAwMWPcmf3x/UE2I9E4XHUwMDEyhfJgRmZHoVx1MDAxYkK5vFx1MDAwMoUyTtVcdTAwMWJIrVx1MDAxYopCXHUwMDBi61x1MDAwNZGcXG6BYk++bEyheKspXHUwMDE0x06hY5BFjSZ4ubDvu8Pzy2dUR4OHSMBcdTAwMGKmQiHwra+Y1dvCQFx1MDAxZM9cIlx1MDAxMHdcdTAwMDV5oUisroBEzLhwS/J0SEQgfImLgrBUrCtiLlXYoHp+XG5Fe9Ar142+WXSslrn3e99UQKn0/7fVJXrvylx1MDAxY5fl22u9pHo/63dnZ8YxOck7ucdcdTAwMDSJXFyyXHUwMDE3tH5cdTAwMTlYtH4h1LEmZd9cdTAwMDStK9LmxlCtr0Kaglx1MDAwMqKviVembChSXHUwMDExXHUwMDEyXHUwMDA0Sya/vKhomVx1MDAxZDdcdTAwMWHbsJk4bdG6nc6c30nDXHUwMDFlXFxELyVcdTAwMDWB5CCflYFcblx1MDAwMFxiYXJBTZlOSyGaLk6GUlKKIflPKi3dWGtcdTAwMWIraK3AXHUwMDE0Yom5NlqyJGnPJEFIOXexLzBcdTAwMWXeXHUwMDE3ioeVWreTPc7d0Juj49tL0teXib4uXHUwMDE3mlx1MDAxZPm0XHUwMDE1xvpcYoRv3IhP4Vx1MDAwM1x1MDAwMychXHUwMDA2SEpcdTAwMTTFr31cdTAwMWSQ5+IxPeuL5nHGXHUwMDFjpU6GLW7XWndboqBT3OEkJJBKxCim7lx1MDAwMlx1MDAxZk8rpnFcdTAwMTlvW1x1MDAwMcJcdTAwMDOF5XDuacBcdTAwMDNSelx1MDAxMH49fXVcYs+4XHUwMDBmaFx1MDAxZSq+mZpCXHUwMDAyIC5cdTAwMTBcIj6bS4/Dz7Gb1vPuI0Zcctb0/z8qXHUwMDEwQt7tzlbuP7+fXHUwMDA1/GPZwfeu5tOgycfgalx1MDAwNrya+ImFXHUwMDBisNUugJOUXCI+cVx1MDAxMlx1MDAxNHYogGZcdTAwMDLSlUZ8WeTJI1x1MDAxNN/q71x1MDAxMPJcdOWZ6MFcInCZRz3nslR4qCVOXHUwMDFi7Xaims1fRjcyXHUwMDEwXGaU2+/Wq0R9oG/u05ZcdTAwMDDU21x1MDAxY6ssWFGTpZ6Wi5uauPMgWLB1Vq9cdTAwMDQ4Jlx1MDAxMHHuRf5iskQu+oWWZWVHNXztnD31ReGwUijrLZFcdTAwMTVcdTAwMTesKDYgkJGYXHUwMDE2rCyXc2mtXHUwMDBmgbtcdTAwMDUrX7VgpVx1MDAwNEI2XGLS2+lcdTAwMTJhjLhcdTAwMTZcdTAwMWRShOdRMFx1MDAwNFxcKJMzzjxKPGtW9DHZT16z8lx1MDAwZelEjdSuvWZcdTAwMDU9XHUwMDFk3bZcdTAwMWGZRDd7cCjYRTbRKLCT6KSHfeiaeNaahCfS+tLfJeJTMfr1z43Pllx1MDAwMF2hYFx1MDAwZlCBXHUwMDA0W1x1MDAwNKCryqFcdTAwMGLJiMCQYyDWoqxPivn0riBcdTAwMTdcdTAwMDOc+4mG193roZGp1kxNIZtuy1x1MDAwZeIrOZ5opW+PuGXJO//c7ZQyoJR881Q8RIBIyoQ2XHUwMDE1XHUwMDFmXHUwMDFh6kFAOVx1MDAxOIJgXHUwMDFje4RS/cC1KCRWXHUwMDBmNEFcdTAwMDJOXHUwMDE52Ip0vG53jiVOefSdO5ZcdTAwMDfYllx1MDAwMptcIlx1MDAwZsivwPZUaVx1MDAwN+w1gX24MbCZRJIpO1670j+8aFx1MDAxY1x1MDAxMiqVY/X1LPQxuP5YWPum521cdTAwMTPXdIRUycSqLVx1MDAwZibqlMSQIa5cdTAwMWMrKVxi44xcdTAwMTPm61Uz7MmAXHUwMDA2XHUwMDEyXHUwMDFliyqwXHJPXHUwMDE02S3gRFx1MDAxMeds26Hj0e3904to1VwiOaA4UCnLNParcmCSbmRYQoxVf+6ztba17Gerni9HK1izakKIxFx1MDAxOOpWcKLQXHUwMDEwXGZVRrCb84h5q75cdTAwMTh8zJN0Ln/7u764IKz6R8xd5eOLXHUwMDBiXHUwMDE2ZIzHs0zlnluUPybyxZvDo2zq5mxET0+jla/7RiZcZpRcdTAwMTIlicIkUiSCqIKkZz3uMFx1MDAxOVx1MDAwMZPH0TEp1ENPKDzoILmkfpZcdTAwMTDCXHUwMDE5pGD79uTKXHUwMDFjbD0kg1wixlx1MDAxNOtcdTAwMTlcdTAwMTHy5LSrve7h0DZcdTAwMGVLqYfuw3kkRFx1MDAxMlx1MDAxY7C7NbvksUksXHUwMDE2XHSgpp1DwTWh2Fx1MDAxZFwiw1x1MDAxMZlaIVx1MDAxNsuxuzFcdNauXG5joavCKFx1MDAwNdK1uLdcdTAwMGaR+YNMaqvxOC9gPGjMZrKJoVlr1Fx1MDAxMoXK3dnltXPXXHUwMDFlXHUwMDFhi2g0m03L7i/EXZlMulx0V8wwZdy/S81r6lx1MDAxMb2mXHUwMDFlXHUwMDAxo+pHXHUwMDEz/eI8SSVQbyhcdTAwMDRHXHUwMDEyakxa7lxy8jaAlVx1MDAwNNq/Mtd4slxuVlx1MDAxMVx1MDAxMJhqNvVyjZjFLalnYHVcclx1MDAxYugvTds4qah9vvtcdTAwMTSyL1x1MDAxZnPZetXA/W72ppzI5ZDTuV6pukkwiFxi8aNh/fCvXpooZIWl8lpcdTAwMTmXVKk2I8S3XFzgNakokoJcdTAwMDGpWEoojJDFZVx1MDAxZN8wqbhN4DiNXHUwMDBlXHUwMDBlXHUwMDA0XFzPXHIhXTyYslx1MDAwNYPT28dcdTAwMTlwTihcdTAwMDHrXHUwMDA0jubk2Fxcg1x1MDAxN7hMv1jyk7OK73BL1CWUa3NbrZu7KqWh9VxcXHUwMDFj3nXNWq1gnV9p/vmJntuIUFdcdTAwMDdqfqmycqTkXHUwMDAx/M64TUJcdJlYrN3dcdveXHUwMDA28D1bgdtcYiaAz1x1MDAxNSr6/ztcdTAwMTBcYlx1MDAwNbBiNkDcXHUwMDA37OeRW/m49LNvwfuXPOpcXMGH9HnCejxYjdw4WW3h5lx1MDAxMnzopYlCbsqGT1wiN1arqI1yXHUwMDE4xm2EXCIhfe7WjttiXHUwMDAxRzo6OCjCUkJK9GGT0FAm54yqh1asO3espb3bSWzvXHUwMDEwS5zE9uPtabBv2HbeUaO5P0267Fx1MDAwZi1zdLio6L9VJy/3qTKBvavk5iSV9uvHr39cdTAwMDFoZqBcdTAwMGYifQ==1USER-1610123purchase_time (seconds)3FIRST()LAST()SALE22 \ No newline at end of file diff --git a/docs/anomaly-detection/assets/scenario.excalidraw.svg b/docs/anomaly-detection/assets/scenario.excalidraw.svg new file mode 100644 index 0000000..6dbca04 --- /dev/null +++ b/docs/anomaly-detection/assets/scenario.excalidraw.svg @@ -0,0 +1,4 @@ + + +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWW/iyFx1MDAxNn7vX1x1MDAxMWVe7pVcdTAwMDam9mXeQlx1MDAxNlx1MDAwMiFkhSxXo8iAXHUwMDAxJ8ZsXHUwMDA2XHUwMDAyo/7vt0zS2HghXHUwMDA2nI67XHUwMDA1kVwiKC9cdTAwMWNXna++s1Xx77e9vX172tP3/97b11/rmmk0XHUwMDA22mT/T6d9rFx1MDAwZoZG11KH0PzzsDtcdTAwMWHU52e2bbs3/Puvv9wrsvVu5+0q3dQ7umVcdTAwMGbVef9Tn/f2/p3/V0eMhnOtfYBsfJ7XKqBRyedzXHUwMDBmXHUwMDA3/Vq1NL90ftJcdTAwMGZhXHUwMDA2et3WrJapu4deVTvEhC9cdTAwMWGmqkFcdTAwMDCQXHUwMDE1nCGBXHUwMDA0XHUwMDE3XHUwMDEwY7Y4OjFcdTAwMWF2W53BZVYuvVx1MDAxNme0daPVtlee8ibC33tg0TK0XHUwMDA33Vx1MDAxN/2wa3ZcdTAwMDeOnH9A3flzpaxp9ZfWoDuyXHUwMDFhi3PsgWZccnvaQHWLe17TMM1cdTAwMWJ7Or+76lrVjfu+77h7f1x1MDAwMOhrj7pKfWmrbenD4ZK83Z5WN2ynqyBwW1x1MDAxZFx0e4XGfJT+cWVcdTAwMWFoXHUwMDFkveBcZpM1Ms1Fs2E1dKfz9zWw9G1W4/3bflxmsTt++L3luyu7rjfmQmBOoKDMXHUwMDFkRlfPOKP+1nLXmutcdTAwMWNcdTAwMTaQQ4mI21x1MDAxOcbwSOmaPb9rUzOHujtcdTAwMDSOaMd+PfTqokdcdTAwMWZH7OjopDPWuVnMNauvxs0znvVcdTAwMTePuaSTtv5q7y9cdTAwMGV8f3/ndt+o19De5IGcXCLAXHUwMDAxRVx1MDAxOFx1MDAxMbI4blx1MDAxYdaLv2/Nbv3FfYRvnj7z4Wa1nMsy+iDDWVx1MDAxNjpazSRcdTAwMTGIILRcZiAosnglgFx1MDAxMMwyoF5MjVx1MDAxZIWUIVx1MDAxYURcdTAwMTChqYJcZtpcdTAwMDAy8HMgXHUwMDAzwyGzdPY7NiSVXHUwMDFjSUJkXGI0JFxyXHUwMDAw5lx1MDAwNzSoukyNK99cYlx1MDAxYUlqr6uMjlx1MDAxMqqHR56x61r2jTGbQ5kttZ5oXHUwMDFkw3Q6my1df2BcdTAwMWEt58H360pUfbDvfXrbULyzOKFjNFx1MDAxYV6eqKubaoalXHUwMDBmXG5xXGKnOzBahqWZt0GZtZHdvdaHb1Lbg5Hu7Vx1MDAwYv30h+7DLKIrgNvuj1x1MDAwNmCWh1xy0So81Fx1MDAxZWsvz8O2XHUwMDE5n/AgcVUwXHUwMDFl4Vx0kFx1MDAwNd6XqziRhOfeJFxy6E1cdTAwMTHhoe1cdI8giVx1MDAxMWA8XGbVXHUwMDEweEbXXHUwMDA3a8hcdTAwMTDAUlDgTq1cdFFe5+wuP8s3T3J27sKajlx1MDAwZlx1MDAxZs+Ld6VcdTAwMTRS3mo5V1FcdTAwMWVkckd5X0Z5OD7lqdHBjjFcYsPAgVkkOFxiVaDCnKaP82BcdTAwMWE471x1MDAwM87xc1x1MDAxZUya86p0dtSuktZcdTAwMDW+rvHMtCTKnd7FXHUwMDFhTp5w+8XpJepRhJ1b9zaESVx1MDAwMvZka5ZDXHUwMDEwccS5+lx1MDAxZurWMX/rguSwVIOLKHPHJiGSw9p9oYK0q0mxcVjUarPjXvFcdTAwMDGkkORWy7mK5FxipCtIjnKepVx1MDAwMdTsiG3euiFO8vGJTVlcdTAwMTZcdTAwMTTLUDhIKlwiXTmm/DxA8EZo+P1p7Vx1MDAwM1r5dFrTnvlcdTAwMDTX+vTAyF2Mx5PB66PeKqzhyqFlV07NfDtXblx1MDAwMafPJrnT7V05SCTFVIS6cpIgf6vryVx1MDAxMaFGXHUwMDA2cZw0yTVcdTAwMWaeXHUwMDBlO3f5USd33jy9p6O7sbBFXG5JbrWcKz05ssqTo3znyX0m4Vx1MDAxNeJcdTAwMTNcdTAwMWVETDllXHUwMDAwkTDKU1x1MDAwN3FcdTAwMTQ6XHUwMDEwpFx1MDAwMFx1MDAwMvU/dZyH08B5XHUwMDFmcI6f83DSnFfOVepcdTAwMTmreSqGefR6c09Pr1x1MDAxYvIsXGJd3TSN3tDPeFT48Fx1MDAxYeLI+TnO7cdcdTAwMDVAhXtVXHUwMDFhXHUwMDAwSnztX0hqxbikhqJIjVxihlx1MDAxOFx1MDAwNTgsXHUwMDAyg6JcdTAwMWQ3XHUwMDA0IYdAmTSJO25WXHUwMDE5ty2Syc3IWbX+UqqePdTrT7E47c9Vt61DZJ/Dx/rJ+cFL+6hQOc9cdTAwMWZ79Hjpttpg0J18XHUwMDE5V65+/lVcXMlcdTAwMDDNKqeQMcKYYjqyXGY9gbJcdTAwMWNSrpiSXCKKXHUwMDA1XGZcdTAwMDBcdTAwMTFjhyohXHUwMDEykGNFqDCMKvGvT5WfhMSz+FRJXHUwMDA1I8qUkYFktyNcdFlcdTAwMTErYVxcOYg4fUSJQChTXCKx1PrZTPlcdTAwMDFTXHUwMDA1XHUwMDEyfSBpqlx1MDAxY1dPr1x1MDAxZVx1MDAxZezhLDOyp1fabHI8mTaDyH2bXpahXHUwMDBifFk+XHUwMDE5QpRuy1x1MDAwMo5cdTAwMTSki1x1MDAxOVOEx/LWzFxipUCYYVx1MDAxMjRdXHUwMDFkXHUwMDFkjmZG5ehRgjfy9T5cdTAwMDWlva7hp1333Z7b//NcdTAwMGaL9//8+fHZXHUwMDE5pX/uXHUwMDA1XHUwMDAxmjS1oX3Y7XRcZls9yaUjRWBCtLWBnVODZlgt/zHdakRcdTAwMWOZX3XgoKita1x1MDAwMVx1MDAxNVDXeY8plTZ8wVx1MDAxON2sdSexuPhGNuXp/dNcdTAwMDSRi+Prq5fbTlx1MDAwN806ySFaoFx1MDAxMEzvXHUwMDEwXHUwMDFkgeiLrVx1MDAxMc0gXHUwMDE3kMnQ2jNcdTAwMTFZelx1MDAwNrlcdTAwMDI0XHUwMDE2kidcdTAwMTmVhZJcdTAwMTNcdTAwMDGF8n2/XHUwMDFj0lx1MDAxZS30nf9cdTAwMWJcItqoTO+Nl3zlVnHzXf2uXnyYTWNFolx1MDAwNPTb00E8Q4KyyPtyScBccjalXHUwMDBi35FcdTAwMDHaRtdcdTAwMTnjn1xu8Ks1ok1cdTAwMThcbqqm2PDCgehgXHUwMDEzVD6roJKmL8FSuTm+zsCIijm01Lq1IW13e1FW9JLIfpM5IOOS3bz8XFzrXHUwMDE4zrI4tiYvXHUwMDA3k/NGP1M0JdLuz59Dylx1MDAwNcJAibPC83KHdeqj11x1MDAxZERcdTAwMTOB6PVcdTAwMWFcdTAwMTAlmDglOmHWM/RgJZAsUVxmrY5vVun9+Vx1MDAxMMVcdTAwMTGZ0DRBXHUwMDE0R2U+N4foy+vltDq+bdd6pHFROkbNXHUwMDEzbfRcdTAwMTA/9cmgO5y7ZVx1MDAxYj/bcr7dPvWJIFx1MDAwN465XHUwMDFhxrdcdTAwMWNH1qZDiSHiinFcdTAwMTNcdTAwMGZcdTAwMTNcdTAwMTMjc3QpzzXjhlx1MDAxZlxc6IcjdnBcXK6mMPW5Ws6V4VxcXHUwMDAydkWsX5b6rKxR66PGh6k+XHUwMDBlLVx1MDAwYsCRNaxqzFx1MDAxOJBcdTAwMDCkj+hSUezzXHUwMDAx43x6sc9Zu6dcdTAwMWKH6Fx1MDAxMfePZH50Vmp1uzeNIHCdWyZcdTAwMTPMRekyO1NcdTAwMTT5qcblr+hYLqZcdTAwMDRAXHUwMDE0XFxEtb+U01xueIuScsg4TDKYXHUwMDBiJUdcdTAwMTQogahXLb8+mItSXHUwMDFm+Fk61uua09Z8/D5iYNYsXHUwMDE3XHUwMDA2+adSo9w7tp+PWe6hOLqKxcDAV7AnhatcYj+ATEKQvMuSRiH5Lj6rXCJcYpFcdTAwMDDM69R7aDW62I5whVpJpFx1MDAwYvWEMOtcdTAwMTQ8uFxct1x1MDAwMa3+nCzppp5jVDp0c5+xol80arytXHUwMDE1LrSTjFEoXHUwMDBm7uF5L1x1MDAxNoNcIrFj0ERxd789g1xux5fjXHUwMDFjhMVzgkV/njohjFx1MDAxMP5cdTAwMDQ47ijUc9UnU+htzTTG7WKNl05rt6X7xuRsXFyyY1EoYjtcbk1cdTAwMTbKXHUwMDBma1CoJJBgXHUwMDA0QktyYfTKY1xmkVx1MDAwNDDZLEkyXHUwMDE0XHUwMDFh7pmmhUKTXHUwMDBmuz49lSs2gmVdU4NzOmpmWvbNYyxcbqV+23VHoe+tXHUwMDFi4u5xe1xulZRRKbxToItHXHUwMDFh7YVcdTAwMDIguFx1MDAwNIDuKPRcdTAwMTem0MtcdTAwMTHta/r4Ulx1MDAxM1x1MDAwZlx1MDAwNavXXHUwMDE1/VKHxcpxQm+R345Ck4CytkZcdTAwMTZTma9cdTAwMTJcdTAwMDOAQtOYMHIlJ1d4RYyzJIO7yVBoeIlBWig0+eJcdTAwMDJcdTAwMDb7g1L+qXXVpVx1MDAxOLDLu2JcdTAwMGWCkFx1MDAxYb5cdTAwMTBcbuV+23VHoe+tXHUwMDFi4q6WgFx1MDAxN0pcdTAwMTlDhIRcdTAwMDZyWSA5uaBQiqCz8oHvKPRcdTAwMTem0CfradqTVuFpVNGH0+pBuSTRNFx1MDAxNoVyf0ZmR6FbQrm+XHUwMDA2hTJO1Vx1MDAxZpDhK0MjK4FcdTAwMDSRnFxugVx1MDAxMk++bE2h4Vx1MDAwYkPTQqFRS0A3p9ApKKNcdTAwMTdcdTAwMTPMznrV/rh4/oraaHRcdTAwMTdcdTAwMGJ4/lQo9FZsLsrgoa+QJ1xixF1JXiRcdTAwMTKbayBcdTAwMTEzLpyivDAkomBkd7EtiYRcdTAwMDI4i0RcdTAwMTNEYjKVXG620dH3/jPUXHUwMDE1PFx1MDAxYcP/proyL0rUpMzcQWd2PHhuV05PtSNycmNf32dI7Fx1MDAwMj2/qYtDTN2lrSg9a7N/XHUwMDExaK7JkVvjsr1cdTAwMGVDOsXs4etSYLB1YdVcIkZcdTAwMTBcdTAwMDJcZiZp1SZd/6a320eWTawqXHUwMDE592mtXCLL13f6SWy9hMCXXHSkJKiYXGZ/yFx1MDAxZjBlTllSWkq3VlNjLTVlXHUwMDAwXHUwMDAzXHUwMDE5XFwqNe/hyFhcYqSAci5l4nt333ZcdTAwMWKHh3TSfLYz+UrvcnBjWa9umHZJv75qTX94eGEpXHJcdTAwMTNtXHUwMDAwM1xmKFTixOi3t1x1MDAwZSnoI5S5grmjSlx1MDAwNtHH06ldu1x1MDAwN9WU6OdcdTAwMGbY4ayzc51EzNlKSXLpXHUwMDFhXHUwMDE1oUFcdTAwMTfvOis/Tt82XGZZXHUwMDFmp1x1MDAwYkZcdTAwMDOhhFx1MDAxNtB7XHUwMDAxXHUwMDAxZVx1MDAxMnpcdTAwMWOYcLCtNcdiwNBGXshmXHUwMDBlekzHf0NcdTAwMTf+s2JcdTAwMTnkw9PZ2ucvbzdcdTAwMDP/XFx18KO7eTRo/tG/XCJcdTAwMDGvJ34mcFx1MDAwM7beXHJwllLE53a+MswogHpcdTAwMDbStXp8VfDIZY1nz6RcdTAwMTLOMJFkska8ZzLtQTOT79B2r1AqVqTWXHUwMDFihWw9XHUwMDE013Tgi/VcIqprXGLxbDm6MCTCXGZcXGU6vCtcdTAwMDWUklLsWVx1MDAwN7qzJLw68bKGJSEwhVhiXHUwMDFlmlVcdTAwMTGRllx1MDAwNFOaTSkkX+6Ibsb1JLhz4GIxgIKtcrBjPdlcdTAwMWI6Xp+O6OlQmEclfXJ8Mu7wXqtTSYlcdTAwMDbF53p3vvX+gMXbXHUwMDAylyW48kiM/SwrXHUwMDAwIC5cdTAwMTBcIjxZK4BcdM9WITsrIPTsnVx1MDAxNZBOK8D8XHUwMDE5VkD/Tr/nonGUXHUwMDAz08PrXHUwMDAzWOTlfqFcdTAwMTK0XHUwMDAygvlb4aub4GHLwUPB/4vEXG7WXHI2b0/xnTUoXHUwMDFlXG5cdJ1NXHUwMDAyw4JcdTAwMDU0cvckiVx1MDAwMJPCu4voV4eaXHUwMDEzndagb6r4XVO1qyMzKzdOgv6In2e79FW52t81xJdAimhcdTAwMTJcdTAwMGXbdeoulO0jXHUwMDExpjjMYMeRVq2gTrSQJlp2kVwiNFx1MDAxM1x1MDAxZm0nW3nhXHUwMDE5nvdcdTAwMWbVLMSIqDv60K2PnEfMgCxz8ufUqVx1MDAwM1fGNnHdLUeXtN7c2VxuXGb5XHUwMDFhM0fYVkwr5o/42zSdTvpNu4lzV3f1We2RnbzWyqOYXHUwMDBiXHUwMDBlwPJcdTAwMGaEYk/UbsH6jM8tJIBcdTAwMDXlXHUwMDA0eVx1MDAxZM60ln6kaDaZrmFcdTAwMDQgJqmz31LIrFx1MDAxMb1cdTAwMDWMXHUwMDFhXHUwMDE0ypw8dWpmjVx1MDAxZunmXHUwMDFizdT3+iPNsp3edLssfUUgXHUwMDExkiazNH71Xsor2Z0p34ZcdTAwMTLmbKdcdTAwMDdcYobMt6lcdTAwMWFFWcU0XHUwMDE4K0ebirAqLf/lYZtacN9N3Fx1MDAwMoBcdTAwMWSSl5A8294uXHUwMDAwXHUwMDEwYcfKXHUwMDBmXHUwMDAxuIysJ+FcdTAwMTJQwrlIz07GiZpcdTAwMDWZaCWdXHUwMDFmXHUwMDBl6GeSVkO0XHUwMDEzXHUwMDEwZU/kL2XbalxicPZU1Eon1WthWGfPYfZcdTAwMDTIOtvnOT9mKZXskkomPGe92Vx1MDAxMzSoKT/NaFj9ICt/SsuXdCQhVVx1MDAwNlx1MDAxOKAsJFx1MDAxMkkopTJcdTAwMTl4+ndHT89MU1x1MDAwM1x1MDAxMb9cdTAwMWNcdTAwMWVqNFBcdImzYVGY1Vx1MDAxML2dXHUwMDBlXHUwMDE0kipLbkOzYaHHqf/pgIBRUrFGw5FmmtO9ttLDX8M8+VDmtVxylW/vvbyv9Xo3turjxYy3Pzb0SS6Irj+a85czWvP5xEGWPue/79++/1x1MDAxZolNuaoifQ==211320USER-12USER-3110123time (seconds)Sale quantityUnusually high quantity \ No newline at end of file diff --git a/docs/anomaly-detection/assets/useful_match.excalidraw.svg b/docs/anomaly-detection/assets/useful_match.excalidraw.svg new file mode 100644 index 0000000..b376ba9 --- /dev/null +++ b/docs/anomaly-detection/assets/useful_match.excalidraw.svg @@ -0,0 +1,5 @@ + + +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW/ySLb+3r9cIsp86blqPLUvrTsjhSxcdTAwMDSyL4QkV6PIgFx1MDAwM07YXHUwMDAyXHUwMDA2Qkb9329cdTAwMTVJ8O5gMFx071xikFr92sYpV53nPM85dar8n9+2tradSc/a/nNr23qtmS273jfH23/o4yOrP7C7XHUwMDFkdVxuTf896Fx1MDAwZfu16ZVNx+lccv78xz/cX1x1MDAxOLVu+/1XVstqW1x1MDAxZGegrvs/9e+trf9M/6vO2HX9W2dcdTAwMDc5+KRglkG9XFwo5O92Xqo3x9OfTi/6bEzfqjlmp9Gy3FOv6jjEhM9cdTAwMGVM1Fx1MDAwMVx1MDAwMYAhOENcdTAwMDJcdC4gxmx2dmzXnaa6gktD+j6zK5qW3Wg6iZe8N+HPLTA7MnD63Wdrt9vq9nU7/1x1MDAwNi39dVtZNWvPjX532KnPrnH6ZmfQM/uqW9zrXHUwMDFl7VbryplM7666VnXjduBvVD5cdTAwMWVcdTAwMDBcdTAwMDaOx/1K/dFGs2Ncclx1MDAwNr72dntmzXZ0V0HgXHUwMDFl1S3sXHUwMDE169NR+rfbpr7Ztop6mDrDVmt22O7ULd352ybw/bVO/eOvfVx1MDAwZbE7fvjjyF9u2y2rPm1cdTAwMDTmXHUwMDA0XG7K3GF07YxzXHUwMDEwPHra7UxtTjWeQ6y+1G3WYE9cdTAwMTmbM73to9lcdTAwMWFY7lx1MDAxOOi27Vx1MDAwN1xy0WuMXHUwMDFlg1x1MDAxY7K9vYP2yOKtUv7x5tW+esJvL7Pn9Fx1MDAxOaVjvTrbs1x1MDAxM399/J/bf8Ne3XxvXHUwMDBm5Fx1MDAxNKlcdTAwMDZLLIjnmVp25znYua1u7dl9hN88nVx1MDAxNlx1MDAwME5yO/1tXGZghjNcdTAwMDNqs2aSXGJEXHUwMDEw8iNcYlxuXHUwMDAzJ1wiXGJBg1x1MDAwMfVhavAopFxm0TCEXGJdK8ygXHUwMDA1MFx1MDAwM1eDXHUwMDE5XHUwMDE4jVx1MDAxOd/VXHUwMDFm4JBUciRcdJFcdTAwMTHYkIzEYYNcdTAwMDIghPpcdTAwMWRcXFx1MDAwNFx1MDAxYVlar2uM2lxi1cMjz9h1O86V/TZ1XHUwMDBlzHf0wGzbLd3ZzPf7nZbd0Fx1MDAwZr5dU021+tvep3dsRTyzXHUwMDBi2na97iWKmrqpaXesfnFcdTAwMWXG6fbtht0xW9fhNptDp3tpXHLeW+30h5a3L6zDT9uHXHUwMDA2olx0wG2+XGb74K1cdTAwMDDrolG8q95Xn59cdTAwMDbN1vyMXHUwMDA3iTtcdTAwMDTzMZ5cdTAwMDBcdTAwMDbwflxcV1x1MDAxYst47k3WXHUwMDAxvWvEeGh5xiNIYlx1MDAwNFx1MDAxOI9CNVx1MDAwNCRcdTAwMTbWkFx1MDAxM0kxXHUwMDE1yFx1MDAxZL+MKK99VCm8XHUwMDE1XHUwMDFlXHUwMDBm8k7+rDNcdTAwMTnt3p+UKsdrSHnJ7UyiPMjkhvJ+jPLw/JSnRlx1MDAwN2s1XGKjwIFcdTAwMTmNXHUwMDAzXHUwMDA3XHUwMDAyRFx1MDAxMSWncCFwrJT04DqQ3lx1MDAxN6RcdTAwMTMkPZg16d3Qt73mXHJpnOHLKs9NjsVpu3eWXCLMXHUwMDEzbr/oXqLMXHUwMDFkkk1g9z6EWVwi9mBpmkNcdTAwMTBxxDlEkYFcdTAwMWRjwaMzlsNSXHIuosxcdTAwMWSbjFhcdTAwMGWbt8UyMi/Gpfpuyay+7fdKdyBcdTAwMWKWXHUwMDAzXHUwMDE0YeRh7qVYLrmdSSxHIE1gOcq5QUOo2TDb9OiCOCnMz2xKWlAsI+EgqYiN5ZikXHUwMDFjXHUwMDEwvFx1MDAxMFx1MDAxYbI02PWktS9oZeW0Zj7xMa6+0Fx1MDAxZDt/Nlx1MDAxYY37r/dWo5hcIpZD/lhOeb5NLDeD06pJ7nD5WFx1MDAwZb6HZJGxnCQoeHRGcoxcYjUyiOOsSe7x7mG3XSlcZtv5k8fDWzqsjIQj1pDkktuZXHUwMDE4ypGkUI7yTSi3SsIrzk94XHUwMDEwMUIlQCSK8tRJXHUwMDFjh1x1MDAwZVx1MDAwNCmAXHUwMDAwLZbaXynn4XXgvC84J8h5OGvOO82Xa7nO46FcdTAwMThcdTAwMTTQ69UtPbysy6MwdK1Wy+5ccoKMR0VcdTAwMDCvXHUwMDExgVxckOPcfpxcdTAwMDFUuL9aXHUwMDA3gJLA8Vx1MDAxZiS10rykhuJIjVxihlx1MDAxOFx1MDAwNTgqXHUwMDA1g+JcdTAwMDM3XHUwMDA0IYdASZrMXHUwMDAzt84pbnZILv9Gjm5qz8c3R3e12sNcXJz2R9Jta1x1MDAxMDkn8L52cLLz3Nwrlk9cbvtcdTAwMWU79t3W7Pe741x1MDAxZuPK5OdP4kpcdTAwMDaooYJCxlxiY4rpiFx1MDAxZnpcdTAwMDJcdTAwMTlcdTAwMWNSrpiSXCKKXHUwMDA1XGZcdTAwMDFcdTAwMTFjTZVcdTAwMTBcdMixXCJUXHUwMDE4RZX416fKXHUwMDE1IfFofqqkglx1MDAxMSVlZCi9qVtCXHUwMDEyciWMq1x1MDAwMFx1MDAxMa9cdTAwMWZRXCJcdTAwMTDJlEj4jq6aKb9gqtBMXHUwMDFmyJoqRzeHXHUwMDE3d3fO4C03dCZcdTAwMTfm23h/PHlcZiP33b34oVx1MDAwYlx1MDAwMtN8MoIo3SMzOFKwXsy4Rng8XZpcdTAwMTmhXHUwMDE0XGIzTMLSVdtwbFx1MDAwZYdTKFx1MDAwNGByfebje107yLvu/225XHUwMDAzMP3H7P///cfXV+eUXHUwMDAxuj9cYvFky1x1MDAxYzi73XbbdtSTnOtWhDyiY/advFx1MDAxYTW701xinrM69Zgz01/taFx1MDAxODUtM2RcdTAwMDPqd95zyqbtQDbGalW747nI+Eo+ysPbhzFcImf7l1x1MDAxN8/X7TZ6a2dcdTAwMDdpgVwiQL2BdFxmpM+WhjSDXFxAJiPLz0TsbCPkiCvFJHmWaVkoOVx1MDAxMVCo4PfHIe2xwsD1/4WItsuTW/u5UL5W5FxcqVVqpbu3yVxcqShcdTAwMDGDgjqMZ0iQgbxcdTAwMWaXOtxs03rhOzZDW+/qMf5WgF+kSDdhKKhysdGlXHUwMDAz8ZVcdTAwMDOMS6J+Jlx1MDAxN0rFJrGzUtHUvelcdTAwMDJcIrp8tX+ZwzE1c8h3dGkl7XR7cTLa1+SgZlx1MDAwZbXRJ5z9z5VGOcvSqDN+3lx1MDAxOZ/UX3KllkTm7clTRL1AXHUwMDE0KLEhPFx1MDAxZtdDT1x1MDAwMvS6gWgmXHUwMDEwvUxcdTAwMDFRglx0wpxGyWdI49lcdTAwMTZcdTAwMTE4zUNkXHUwMDFj5yqIcuRxXGaLQpTETIWuXHUwMDEzREnc1OfiXHUwMDEwfX49n9yMrpvVXHUwMDFlqZ9cdTAwMWTvo8dcdTAwMDNzeDf/3CeDrlx1MDAxNW9Wbny3cr5efu5cdTAwMTNBXHUwMDBltFxcjeJb7nGYXHUwMDAxNEtcdTAwMTW3qmA4+6lPYuf2zuWJaV/xnTNrd8h29k9vlk9cdTAwMTNcdTAwMGZcdTAwMDd8WO83xp1divIntebD86R1XHUwMDE4fdvUaWLElT+EyFx1MDAwYr/F08TJz5+YJiZgU1x1MDAxZPtjU6rlXHUwMDE0NURqfJjq48hyXHUwMDAzXHUwMDFjXzmOKEVMQpJxXHUwMDE10XtcdTAwMDZcbqYw31x1MDAxMIGuRVx1MDAxNdFcdTAwMTdMtvIqoqNmz7J30T1+2ZOF4dFxo9u9qoeRq2+ZTZZcdTAwMTitl5xdo4zSzby8XHUwMDE4nyTGlGitXHUwMDFhlVHC8Vx1MDAxYVeLUcg4zDJcZoWSI1xuVINcdTAwMTbSuKtLXHUwMDEyo7VPKPnO9bqtSWM6fl9RMHs8LfZcdTAwMGJcdTAwMGbH9dPevvO0z/J3peHFXFxcdTAwMTRcZlx1MDAwMpWAUriG8Fx0ZFx1MDAxMoHkzfRrXHUwMDFjkivz0yqCXHUwMDEwXHTAvMlcdTAwMDJcdTAwMGavxlfxXHUwMDExXHUwMDFkQkpcIrNcXHMyxayupHC5blx1MDAwMVr9nunXRSPSuHnWxWPRsnVWr/KmWTwzXHUwMDBmcnbxtH9cdTAwMGJPenMxKFx1MDAxMlx1MDAxYlx1MDAwNs1cdTAwMTR3t8szqFDCXHUwMDEyclx1MDAwZaLyROFqQk9cdTAwMDFcdTAwMTJGXGKvXHUwMDAwjlx1MDAxYlxu9fxqxVx1MDAxNHpdbdmjZqnKj1x1MDAwZqvXx7f18dHo2JmLQlx1MDAxMdtQaLZQvktBoZJAglx1MDAxMYis9YXx23hgiCTQLLp2XHUwMDE0XHUwMDFhXHUwMDFkma5cdTAwMGKFZp/OfXg4LTtcYp5aplx1MDAxYZzD4WOu4Vxc3c9FoTSoXTdcdTAwMTT6cXRB3N0vT6GSMiqF11x1MDAwNbp4TJhpXHUwMDAxQHBcdFx1MDAwMN1Q6C9MoedD+mJao3NT3Fx1MDAxNTu9rng5brO55k6ht3pwQ6FZQNlMMTuq5KvEXHUwMDAwoMjpUVx1MDAxOF9eqPCKXHUwMDE4Z1mWXHUwMDE3ZkOh0aVcdTAwMGLrQqHZXHUwMDE3LTD40j8uPDQuulx1MDAxNFx1MDAwM3ZeKeUhiKhccoygUFx1MDAxZdSuXHUwMDFiXG79OLog7qpcdTAwMTlEoZQxREhkXCKXhSY9Z1x1MDAxNEpcdTAwMTHUSyr4hkJ/YVxufeg8THqyU3xcdTAwMTiWrcHkZuf0WKLJXFxcdTAwMTTKgzMyXHUwMDFiXG5dXHUwMDEyyrVcdTAwMTRcdTAwMTTKOFVfIKOXnMZOkFxuXCI5XHUwMDE1XHUwMDAyZT75sjSFRq84XVx1MDAxN1xujVtbujiFTsApem6Bt6PezcuodPKKmmhYmVx1MDAwYnjBqVDorVx1MDAwNJ2V18NAgVBcdTAwMTiIm1K/WCQ+pkBcImZcXOhivygkXCJcdTAwMThfqlx1MDAwMLjUS9pw5otlXHUwMDE4YzhNpU1cYoq9Yb/WNFx1MDAwN9aDY7etrd9cdTAwMDeWXHUwMDAySn3w97Wu/fuyzVkp3377bb//1CxcdTAwMWZcdTAwMWWae+Tgyrm8zZG5a1x1MDAwMYPqXHUwMDE3R6hf376XnnXgv1xiWlPS5tJQbaYhTV03XHUwMDFmvVx1MDAwNFx1MDAwNvLYXVx1MDAxYSTBXHUwMDAyXHUwMDEymn1Rbtqaolx1MDAwNLO0ms29jkM6N2T0QqtleXpZsVx1MDAwZeY2S1xiXHUwMDAyc4OUhO2S4S9cdTAwMTlcdTAwMDWuWZiWlZHSpa3UTmWlXGZgIMOLsqZsXHUwMDEzO8PAOUJcdTAwMDLjxTKaXHUwMDFmJ1wi60Kf7ur8UciXYY0+XHUwMDFm7Ndcbk8l1uxkVVx1MDAxN6rX45M0wV4kXGKiXHUwMDEzXHUwMDBlvomZeEnMMKBQNWeOfnvvkKI1RLlcdTAwMGKY3yvnXHUwMDEwvT+cONVbcLMm9vlcdDts6E3yJGJ61ybJPYt+XCLTMN5cdTAwMTVdQZy+702SXHUwMDFlpzNCXHUwMDAzkXxcdTAwMTaye1x1MDAwMYFcdTAwMTJE0Fx1MDAxM9JEgy3VXHUwMDAyf1xmXHUwMDE4WiguWSxknzNcdTAwMTWwYFC/quxcdTAwMDb58nKW+nr/zjbwj6STX93NY0HTf1x1MDAwNtc+4HTNz4VuwNLdXHUwMDAwXHUwMDFilFwiPlX+SpdRXHUwMDAwrVx1MDAxY6SpejwpneSyxpPHqUQzTCyZpMhcdTAwMDCNJz3Yylx1MDAxNdq02StcdTAwMWWXytLsXHIjdjmaVzrw2cpcdTAwMTTVNYR4djedXHSJKH2rpMOHUUApKcWewGmjJLw28ZxCSVxiTCGWOCxt9UlcdTAwMTnLiFx1MDAxYyvfiZVhZ60kVrvCXHUwMDA0XHUwMDEzwdMsQ11MSVx1MDAxMFx1MDAxMZtcdTAwMTFHeu1cdTAwMWVcdTAwMDFkfiXx+rBHXHUwMDBmXHUwMDA3orV3bI33XHUwMDBmRm3ea7TLa2Kf8ytcdNebe+OZ91x1MDAxNTU+Z8BcdTAwMDOt/H6NXHUwMDAxXHUwMDEwXHUwMDE3XGJcdTAwMTGeocZYYm3IRmNsNEbcXHK+S2O0vkNjvFSsWy7qe3kw2b3cgSV++lIshzVGeL5YXHUwMDA06jR41LL2SPD/XCKZiLTJ7eVcdTAwMDVEO4WAgEJCvdthVCqCxu7WJlx1MDAxMWBSeLdD/end2jJ1azDgKv5bp4afKq/kwj5nsD5uVVx1MDAwZZ5u+lx1MDAxN/mdiMV6sVt+XHUwMDAzbFx1MDAxMFxmxefXtaGpXHUwMDFkU2AgoVTmxzeEaiyYwTxcdTAwMWbuXHUwMDBlvFx1MDAxYjT8Kknxujlofu9cdTAwMTRWXHUwMDE1pHj9XHUwMDFhXHUwMDE0XFyNj1x1MDAxZasoxVx1MDAwYuPXXHUwMDA1IYox4CvIOVx1MDAxZZ45j+Z+vzC6ajxcci7vXHUwMDFiZ7XJ0X1WkVx1MDAwMiRYgozWoo93XG4vXHUwMDE0PNOyXHUwMDFj9StcdTAwMTd3I3Q5OuDzTeNKblxiXHUwMDAyXHUwMDAxkZRLRj26WltcdTAwMDBcdTAwMTHS4MqFzr7hxehcdTAwMTBcdTAwMGK9XHUwMDFhnWHloFx1MDAxNVo4XG5cdTAwMDNkzWot1lxuXHUwMDFmMS84i8JcdTAwMDdW8Y/kNFxcTjzt4th4kDDCkeSL7Z32c+ig1DtFtsD88c5N4fe/b/1zK11VpPDdZ/Uzxlx1MDAxMa3MZpF78ugkbpyIJDIgXCJAWVxyJYRxv0+gRFx1MDAxOIBcbvD5dVx1MDAwN2lWb8VcZsJUsC0xXHUwMDA1XHUwMDA0MSFcInZcdTAwMTTHxMBcXEKiX7ai/op0+33jXCK2/C6CRLuIVMtcdTAwMTNcdTAwMDDnSkKLyDnn+EkpXHUwMDE192EgXHUwMDA0XFzEdfieJkN4Zyqic/GmOj1cdTAwMWSy0ixFtmdcdTAwMDA/3tpdnEPxTlx1MDAxZFZtODU4XHUwMDAzXHUwMDExqVx1MDAxYaZaTfRcdTAwMTRcdTAwMTN231Grzc3saZvw77UmKFx0XHUwMDE5iU/fxzUqWWF4XHUwMDFhlVx1MDAwM4ZcdTAwMTJcbrotqlVcdTAwMTJhXHUwMDAxWKhR0kBcdTAwMTBhqqI6oFPDJNymUGRcdTAwMTG15WRCfDH/dpSTdl1UJq/1Yv/4+vztNF86P3pK83pYxKfbaH18/a6SS394XHUwMDExseU7xFx1MDAwNvZ8UISrhMA/iJ53Lm585ZbfV9I0aVx1MDAwNYJcdTAwMTmkOPJlQvGvhVx1MDAxNVx1MDAxOHGgXHUwMDA2M/NgXHUwMDAzP1Rp52Jncun0Ok4jf3Rn35/lM5NTgkqR0ftcdTAwMTGe8i1Uca53x0VTVFx1MDAxZu9f6H7ljqVcZjaY9GRK37GCmUGRXHUwMDA0RJNcdTAwMGXzvnp0XHUwMDE2aTBpXHUwMDEwoJhcZlx1MDAwMUn0ToJRsmJcdTAwMTNqxGKDzY9ccslcdTAwMDHT7jtyR4Dwa1x1MDAxM9xddVx1MDAwMCVcdTAwMWNcdTAwMTK4UDXpz4FDb0wpUoAjNtaAXHUwMDA2/SWiXHJfO7OJN5JHKDHewIhcdTAwMThcXHAoXHUwMDA1RVxcUODP0XEhXHKoK3LevyhMokrEKX+iR1x1MDAxMivFXHUwMDAxKI7YXHUwMDBmXHUwMDBmQ4OpSFhcYl3rXHUwMDA1f5WNKn/CTfDlw1xyXHUwMDAxVdygVGB07WCIb93X9Fx0XHR1Jm9cdTAwMDXxhuQ4VbJtdfFGjKlOT1x1MDAwN63UvV/Ii2VcdTAwMTZuJFx1MDAwYmBfuKGgiT1zicJz0buwh1x1MDAwNvR8gKBcImQjc0VcdTAwMWLJXHUwMDEyw1x1MDAxZm1cYiWIlZBTyoAy4imgdlx1MDAxYqWuoVS5XHUwMDEwXG70TjeA8rDhflu4kVxcxproJlx1MDAxOZI6LSNcdTAwMDVcdTAwMTYqtsLB10tRZmDB8Oc34tXDwuBcXG9SwFU3XHUwMDAx4l0p53mfjUHVKH98idjIqVg/KZb2k0xKrlx1MDAxY17kvCZcdTAwMDaxXHUwMDBi0PXEXHUwMDAxVjEnWGT5XFyym8RQyfOFNtHP1E3GW6r+5EJG+lx1MDAxZG4yeW2D301yJlxiYnptlYpyXHUwMDE48lxc9OGRXHUwMDAyUIVhXHUwMDEzmctNPjhv48n5+fXOM6zYyH47KFx1MDAxZKNSjJuEXHUwMDE4Y1x1MDAwMVx1MDAwNFb9qfdAZmE/XHUwMDE52MhcdTAwMWb+ZFYm+clcdTAwMTL31Jh6SVx1MDAwMFx0VaGI5/Xf0+ksxlxmSZj8/PJcYiepM4L6tYpcdTAwMThcdTAwMTAmeJSS3PjEWJ8oU6RfOFZCjyirjFx1MDAxMonx+Vx1MDAxN1xikOJAXG5Y5vNZK11hovy5JEvNZyGw9a+t398juP/ZwtFcdTAwMGIh1yXQjG9sNtFmclxyb7KMoureXHUwMDEwSkL0/oUs8IJcXC6RQVx1MDAxOGDKe+hkhnA7bram2VA+XHUwMDE0a9fOXHUwMDE1d2NcdTAwMTDhXCJcdTAwMTA01EnOXHUwMDE4VZGHkvNkk7GNdVx1MDAxOTtLyyghMeZUsKg8rrdgJ+hI9KxcdTAwMGZTsWrGS5+Rli6erd9/7KVicXaqP7mwiX6HiEre+d0vWLCSuVRpXariXHUwMDFliUR4alx1MDAwYkJDKp6XhKrQXHUwMDBmc8FoyELmUlEnN0/te+mwQaNqsfrZ69vryePeXFwqimFcdTAwMWVqlU9FSV20/oMyKvnRXHUwMDEyZVx1MDAxNGYqslx1MDAwNlCoroWKqV0/+J6sV17yPVx0QPQ37CVV71x1MDAxOEr8XHUwMDAyioRcbstcdI2INTdCKt4r5lNcYimit29cdTAwMDCSRJXNeV9cdTAwMWZcdTAwMTKa3NdvQ5Fgsb1Ak4TUalx1MDAxN9go74pcdTAwMTda5zDbaXTrf39cdTAwMTlcdTAwMWRcdTAwMTXb1tQy6rePXt02e70rR/XpzCNuj2xrnFx1MDAwZuPpb4/Tj1x1MDAxZZ2pd9FYsqbc/Ndvf/0/6P1SfSJ9211320USER-32USER-4110123purchase_time (seconds)AVG() = 2AVG() = 1.520 > (AVG() * 3)1 < (AVG() * 3) \ No newline at end of file diff --git a/docs/anomaly-detection/assets/useful_query.excalidraw.svg b/docs/anomaly-detection/assets/useful_query.excalidraw.svg new file mode 100644 index 0000000..5622fa2 --- /dev/null +++ b/docs/anomaly-detection/assets/useful_query.excalidraw.svg @@ -0,0 +1,5 @@ + + +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1da0/jyFx1MDAxMv2+v1x1MDAwMrFfdq823n4/VrpXguFcdTAwMTVIeFxmMFxmXFytkJM4iSEvXHUwMDEyh1x1MDAxMFbz3291ILHjR3BcdTAwMTJcdTAwMDOZq1x1MDAwNGlcdTAwMDTttlPurlOnqrq6559fNjY2vWHH2fxrY9N5KttccrfStVx1MDAwN5t/mPZHp9tz2y24REZ/99r9bnnUs+55nd5ff/7p32GV282Xu5yG03RaXlx1MDAwZvr9XHUwMDE3/t7Y+Gf0L1xccSuje1x1MDAxZvpd9LyPK6qWvy7dlO7vevXG6NZRp7EwXafs2a1aw/EvPUE7xlxiTVx1MDAxYYbQoFx1MDAwMn9cdTAwMGbcildcdTAwMWa1WSj4kZNcdTAwMWV1x63VPehcIrWlg1x1MDAxZjHp8vKlf234j+153fa986XdaHeNZL9ix/z4cpXs8n2t2+63KpM+Xtdu9Tp2XHUwMDE3XHUwMDA2wu9XdVx1MDAxYo1zbzh6OlxmJlxm3GboO65eX1x1MDAwMIfak+6CL63VW06vNyVvu2OXXc9cZk5wsIyEnXxlNC9/+zJ17aaTN1x1MDAxM9PqN1x1MDAxYZNmt1VxzHBv2mTq21qV129cdTAwMWJPqj9j9LXlhy+745hcdTAwMDdjRjQlSEg9ueJrXHUwMDE2RoyEm4/brZGaYUE0wYxcdTAwMTLqy9XbXHUwMDAx/fJGz63ajZ7jT4JcdTAwMTFuN6x7Qf1cdTAwMGLoYPPoav95v7q37W2ftIaPX26Kh1eFyYtO6aHnPHmbk1x1MDAwYj9ef/NcdTAwMDew36nYL/JgyVx0kjDkVGo2ud5wW/fh0W20y/f+K/xcdTAwMTJcdTAwMTi1XHUwMDEwVmbLOS1jXGImRFt4pNiaKcJcYplcdTAwMDZccpZcdTAwMTaPwIZgS1x1MDAxOLhcYowox1xcXHUwMDEwXHUwMDFlxVxy4ytcdTAwMDVcdTAwMTSyXHUwMDAwUPD7XHUwMDAwhcZcdTAwMDNlqvcrXCJgRqjiQuI4QFBBXHUwMDEzXHUwMDAxQaSmVFMuXHUwMDE3XHUwMDAxxEydlTSAw7d11ldBo3rw+lx1MDAwMTNRbbe8c/d5ZFx1MDAxM8RU657ddFx1MDAxYma4xdT9W1xyt2ZefbNcZqI63c3g63suMMykQ9OtVIKMUIaH2m7L6ebTUEu769bclt24iMps9732V6f3XCK11+07wbFwXHUwMDBlxtqPLcJnwPXx28HZ9bXXe871veGZ/TzYXHUwMDFkXGarUbja3W57XHUwMDEwwiti07SmRZTW/JZcdFx1MDAxY4nyXHUwMDFif1Y8vlx1MDAxM3FcdTAwMWSnJS6SSFxcXHUwMDFhcCooi1x1MDAwMtKosFxmN45Rylx1MDAwNVx1MDAxN5gzulx1MDAxMGslg1x1MDAxNFx0LUGgRUDaabthUvR/2/AnYPTH5Pe//3i7d1x1MDAwZVx1MDAxNNC/IUKOXHK7531pN5uuXHUwMDA3b3JqpIhYRM/uetswa26rXHUwMDE2vua0Klx0V0Z3bVx1MDAxOVx1MDAxONVcdTAwMWQ7olx1MDAwM3Bf8Fx1MDAxYei0XHUwMDFi8iSdRqk9SMXA57qqXHUwMDBmvt9cdTAwMGVcYjvZ/Xp2f9FskudmdpBWJFx1MDAwNtRrSCdA+mRpSFx1MDAwYixcdTAwMTVcdTAwMTY6XG5eM1x1MDAxNzzcOOFdSSSnSlx1MDAwNzzY5SGNtWRcbivM/WH5LEhcdTAwMDe0MNT//1x1MDAxMNH68LE1uN9cdTAwMWFcdTAwMTQrXHUwMDBmucOGJvb34t1JXHUwMDFhn1phaqnAx1eGYVxiyWN0Q+RjkeDHp1x1MDAwNN+hXi20J0aelbaZ8Vx1MDAwZoX71/RcdTAwMWU1RImMUFx1MDAwMGmcR81cIoHnXHUwMDA02Fx1MDAxY8hcdTAwMWFxXHUwMDE1gH5WXHUwMDBlNVx1MDAwNF9cdTAwMGJcdTAwMDF77FBfnu9+zZFcdTAwMDS3mky1Lu1We+1Okk89JXLYgY7IOOVGT7/XPH70/dPp8NvjRb3UYZWTwi6p7tn96/QpXCKBfS1+TVx1MDAxMVmMKsxASzRcdTAwMTJCRYBcdTAwMWHOXHUwMDA2+Vxuk5gw8rusNGw/nqQvlk9cdTAwMThcdTAwMTEskWHGuPhYUlx1MDAxNW6dON5MKSxcdTAwMTlbiKUn4sWki5ib2znVRds9l1snzpe+2No9/pZNumh+r35cdTAwMDZuZss5K10kXHUwMDE4eiNdpGdcdTAwMDJonTpcdTAwMWG1Llximcv0RKdgflx1MDAwNIxxXFwuVdPEVCpBWiEtXHUwMDExwYtgY1x1MDAxNtExglx1MDAxMZ5DfSNEt1x1MDAxMpmjN1x1MDAxOOfdM0dH9Y7jfiE39GFH7/ePXG61dvu8XHUwMDEyRa55ZEaJo9VyO1coyPyWlr+S80aUM4RcdI9cdTAwMGIyaXKQiTWXWEicZd5cYoJMwlx1MDAxMVxixINq+fl5I7LyMebUtU67MayN5u8tXG5cdTAwMTbV43x3/7ZQOe7sene7Yvv6sH+WioJcdTAwMTFcdFx1MDAwMVn5ijBcdTAwMDYyi0Ey/flp9Z2QfJWeVlx0xkQhXHUwMDExXGbqXHUwMDAzvJq8QskkoFYzneWCzFxis1x1MDAxOKjV57pcdTAwMDVoXHUwMDE1xdIqUVOtn1x1MDAxNjmiXHUwMDA0XHUwMDAyXTxmvHROKiVZt/Mn9l7OzVx1MDAxZne/42InXHUwMDE1g1x1MDAwNtdQ1lxmmlx1MDAwMe6+L8+gXG5riqVEcflcdTAwMWOWuPJcdTAwMDIopoTQd4DjmkJcdTAwMDN3vTOFXpRcdTAwMWHuY/2wJFx1MDAwYlx1MDAwN6WLwvfK4Oix4KWiUFwi1lx1MDAxNJotlK/noFBt0lx1MDAwM1x1MDAwNMV5vVx1MDAxOKNw64RCKSZcdTAwMWFcdTAwMTlcdTAwMTZdOVxuxStNoThzXG69vT2+9FxiPnZsmJyDfjVX885vUlEoXHUwMDBm+65rXG59bV1cdTAwMTB3N8tTqOaCa1x1MDAxNTSBPlx1MDAxZXlyXHUwMDE0ipCSXHUwMDFhoUxLjNZcdTAwMTT6wVx1MDAxNHra51x1MDAwZrbzeGqr63yr01ZcdTAwMGaFpki1xok5WlNotlC251jFXHUwMDA091VThEjsMiZOXFz4kIBXXCKkyDK5m1xyhcYnd1eFQrNfuVx1MDAxNPihW9i/rZ21OUXi9OpwXHUwMDFio5hyoVx1MDAxOFxulWHfdU2hr61cdTAwMGLirpRBXHUwMDE0yoUgjMUmckVkcdIvKiBYXHUwMDEyJeWaQn9iXG69bd1cdTAwMGU7upW/7V86veG3reOCJsNUXHUwMDE0KsMrMmtcbl1cdTAwMTLK5TkoVEhcdTAwMGU/SMeGoYSFW8egVUxLrtRiW03elULpSlMozZxCh+iY3DfQ81Hn28PjYfGJ1En/Klx1MDAxNfDCS6FcdTAwMThcdTAwMDV2WEwqbnGokCdcbsR1SV5cIlx1MDAxMqtzIJFcbqlMUV5cdTAwMWNcdTAwMTJcdEpEXCJcdTAwMDZcYmtgXZVlQmi5+vkxXHUwMDE0O/1uuW73nFvPbTpcdTAwMWK/9Vx1MDAxY1x1MDAwMEql9/tK1+i9KXNWnm+3+bzbvatfXHUwMDFlXHUwMDFj2Dts79z7+j3HUtfsSUEspqjAhGhcdTAwMDV+l2/dRmNcdTAwMTazx1x1MDAxM+M4XHUwMDBl5eInwe6cJLo0cOvzUKjiiMXXyOPonjW/xFxiXHUwMDAxyJDiWZdcdTAwMThJJLnIqELudjDs4EZuv8nrnXzh8FLbnf5R+spSXHUwMDE0WiqUk6pQhVx1MDAxMGNCR9RUxGkpJuNccspYa84pZv+XSsuX1tr7ObRWUY6ppjI2d6JcdTAwMTPVllx1MDAxMsqZwHQhrX29XHUwMDEwWzS69/Bl//TwXHUwMDE2i8smpi3Jhzl6uVx1MDAxNV80+rJ7aHIlxSZjKsFYLo2J+HREsHI+oO+hcdOYXCKiNUlcdTAwMTPkvlxmyNPtXHUwMDBlP+ipxk7BXHUwMDE57O49NmWn1rxcXFx1MDAxMf1cdTAwMWPDjlqYYa6J4JSb/T6+Mo2TNP7JXHUwMDAyTIbKzPGUMZAhKX1cdTAwMDS/3D4/gifUh2JsSmCmxohARCpCWMBcdTAwMDGLh+HHOFGLhfopU1xiXHUwMDBiJlx1MDAwM94rK8Le7C7m7j99pFx1MDAwNf5j1sW3nlx1MDAxNtCg0Z/hvVxydD7xc5FcdTAwMDeI+Vx1MDAxZUAtzolcdTAwMWNFXGaAXHUwMDFkjrCTw3yuXHUwMDExn5WG8vkksFx1MDAxOTyBe1x1MDAxMmkmfeZcYlx1MDAxNc9J1yuWbq5quf37VitXPT4vpvcxXGJcdTAwMGVcdTAwMTXfr3evpDXoy1x1MDAwN7glhONdjnm2r8BkgbVkcY6IVFwi3Or7z5IyTKRcXGgv2ixP5Kh303Td40GNnnpcdTAwMDfXPXWzXbkpx3tcInNuX1x1MDAwMTZgWMx1csRcZuDMlnNm4Vx1MDAwZsPr7SuftX2lhFx1MDAxMs5cYop30zWhlMhYdOhkd1x1MDAxMzNcdTAwMDKQ0lx1MDAxNGWcXHUwMDE1wshcdTAwMTRez3NcXI+vjzPLhD54XHUwMDAzy1x1MDAxYqSTtnho4VxyLK3a8N6uN/H2IJ/fuTugj832Q8xJRUmkR1x1MDAwM9mGNel9OOklnF00XHUwMDBm6UFsoJHULFJZZCZCJpYuXHUwMDEwXHTzyznKPPy+O3i4u9u57HRFe49fX1x1MDAxZLhcZnVcdTAwMWKZkFx1MDAxZdhcZlwiZOAsiKVIb7acs0iParEmvc8jPTZcdTAwMDfpSYUpzFx1MDAwMY0lveSUKlx1MDAwM3ZiXHUwMDFhZX86XHUwMDAxpWixQoKV2rT5XHUwMDA257z7ps3ZXHUwMDE5vplnXHUwMDAzXHUwMDExriz4XHUwMDA3McRcdMXK9z9esspcdTAwMTSQSSBcItZcdTAwMThmSvnUOIYu5ZYkimCBXHUwMDAx4lx1MDAxMjwq/1x1MDAwMX45XHUwMDAxtjQzi8/GvGopfeysXHUwMDAykpNcdTAwMTc17V79Y1x1MDAxNzVLaH/pUiFcYoQkNmVcdTAwMDcxXHUwMDAwZzhxw1xu5VxuK7TQjs+pd4kgnIFTXHUwMDFiqLn/rFKhXFyyor5cXFx1MDAwZeuo/8BcYlx1MDAwYs9dTFx1MDAxNJi+1zNp8ynWgEa2q9w3I5BDXHUwMDE2lkwoRJlcdTAwMDRcdFx1MDAxNVx1MDAwMJZcdTAwMDS61eyOmXmLQlx1MDAxMCyxUtJcdTAwMDT+XG5HVGQquZkkVPnk6pJcdTAwMTWc4e1Nv7vbe6KFh25LxVx0ZfxiI5M5XHUwMDE5lXNNuK89XHUwMDEzmZRFXHUwMDE0XHUwMDE4XHUwMDE2RYDfXHUwMDA1xTrGa1x1MDAwYidN445UmpE6TX/c0uxcdTAwMTebfYQpsjhcdTAwMTVcdTAwMDSGXHUwMDFm9Fx1MDAxOdNp/0ZoZVHJlKZcXIOXI6P+XHJcdTAwMTbaYlxigVYhUDJcIrhPQ6tac7VSVvEgvYOjXHUwMDA0ZUqAXHUwMDEzXHUwMDE5Y/5w8n49XGLqlX5cdTAwMDff/52X3jhfzLSOnaetb/u//b7x71xyabFYNyqptktNPen9S0di5czGdVKdut3Yblx1MDAxNYroZoiva/Vujlbz6dNcdTAwMDVMgExcdTAwMDBoaeqEXHUwMDE0nj6JTUqwXHUwMDFiSENESlx1MDAwNVx1MDAwYuzd9itHpGWMXHUwMDBillx1MDAwMu6WMsYsYMYsXCKUyTlRylx1MDAxOVx1MDAwYtSjrM3ExrSZyM9jJlx1MDAxNFx1MDAwM1wi1XFmQidcdTAwMWbSplx1MDAwNaJKg6uctZ2gxWqluHPq3jXdIzbc/YpK5cv7zOxcdTAwMDSWwc2QSyVcdGZcdTAwMGI6M9LgWFtcdTAwMTBIcHDBmKJcbk3n2aTgXHUwMDE2ReDkXHUwMDAwiUqhXHUwMDAzXHUwMDAx3CTUkFx1MDAxNlx1MDAwMedGUVxyvlxyXHUwMDBlVlx1MDAwNVx1MDAwNZJcdTAwMDSWxEJcdTAwMTBpjpFcIkz8JGfpf1x1MDAwNlhcdTAwMGWXjjRcYlx1MDAxOC5cctNcdTAwMTZ3Nlx1MDAxYYlcdTAwMWWx71dVmkBcdTAwMTLs3TvEXHUwMDFhlJJcdTAwMTU4lzhRUc0nXHUwMDE30VH/cVx1MDAxMShnXHUwMDE2acxmuelIg5tcYpIgZmZXMVx1MDAxYVjff3XquVx1MDAwNTMvXHUwMDE4+Fx1MDAxZpJcdTAwMTDB+IKBxl7+ol53Lr7e7Fx1MDAxNo8wq+3Rc7dXTJDJXHUwMDFjiFwiMNKcgWBcdTAwMTT5dntcIlx1MDAxNLVM4IPAP+cm2YhcdTAwMTiP6u2HhVx1MDAxYbNfbVaowVx1MDAxOLKElJxcdTAwMDE/wVx1MDAxMPtcdTAwMDZsXHUwMDE0aShcZnFcdTAwMDSBgYdIXlx1MDAwMoxcIjZcdTAwMTL0XHR6gE9BXHUwMDExhGYyJo+6jjOSbeJRelx1MDAwN1x1MDAwMlxcN61cdTAwMDWmsXmWYOFEZFezNodcZqDs19bf1YVY3LJONo+gjf9s/PbiyP9rg8aXqq9KvJEsbDZBx8mgr1x1MDAxYlx1MDAwNbTPz65b9fsnsi++92O2dDmNhtvpRUJcdTAwMGUlLWGWU8DSXHShXHUwMDAzJ9O8rFfil/VKXHUwMDA0Pq5cZlx1MDAxZUAzWa9cdTAwMTRwOzJcdTAwMDfDUZP4jcvYytWKMlio/TNcdTAwMTcoXHUwMDBic9hcYiGASKdcdTAwMGVrXHUwMDBmXHUwMDFjv4eSN1GDeYe5yfL02FjGXHQoZJWcXdj8/KxW4/KiYp+f7Fxc5EpzWVxiaf5vJVx1MDAxNUTD4kFGvDRp6JMjZlHzP3NBpEaUJKG1fIi3lUDgtFx1MDAxYc8rWPkx2VxySS2ze1x1MDAwMFx1MDAxM1NhauBcdTAwMTFcdTAwMTdjrFx1MDAxNDZWikCL6cHBJYJZkCp2T1byQiRcdTAwMTaUgGMkXHUwMDE3OlhrdvRg9neQpVx1MDAwZZCl8UfdffBi5Fx1MDAxYuRcdTAwMTLluiXY7ZdXm7BpdzrnXHUwMDFljOfE3998dJ3BdlTdf62OPsa2jMBvVN1cdTAwMTlFnj9++fE/gPiHXHUwMDE5In0=2USER-2220123purchase_time (seconds)12AVG() = 7.430 > (AVG() * 3)30 \ No newline at end of file diff --git a/docs/anomaly-detection/index.md b/docs/anomaly-detection/index.md new file mode 100644 index 0000000..43344c9 --- /dev/null +++ b/docs/anomaly-detection/index.md @@ -0,0 +1,322 @@ ++++ +title = 'Anomaly Detection' ++++ + +> Note: This tutorial is mainly focused on anomaly detection. For detailed information on working with [Flink ETL Jobs](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/learn-flink/etl/) and [Session Clusters](https://nightlies.apache.org/flink/flink-kubernetes-operator-docs-main/docs/custom-resource/overview/#session-cluster-deployments), look at the [Interactive ETL example](../interactive-etl/index.md). + +[FlinkCEP](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/libs/cep/) (Complex Event Processing) is a Flink library made for finding patterns in data streams e.g. for detecting suspicious bank transactions. It can be accessed in [Flink SQL](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/overview/) using the [`MATCH_RECOGNIZE` clause](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/). In this tutorial, we will use Flink SQL and its `MATCH_RECOGNIZE` clause to detect and report suspicious sales across many users in real-time. + +The tutorial is based on the StreamsHub [Flink SQL Examples](https://github.com/streamshub/flink-sql-examples) repository and the code can be found under the `tutorials/anomaly-detection` directory. + +## Setup + +> Note: If you want more information on what the steps below are doing, look at the [Interactive ETL example](../interactive-etl/index.md) setup which is almost identical. + +1. Spin up a [minikube](https://minikube.sigs.k8s.io/docs/) cluster: + + ```shell + minikube start --cpus 4 --memory 16G + ``` + +2. From the main `tutorials` directory, run the data generator setup script: + + ```shell + ./scripts/data-gen-setup.sh + ``` + +3. (Optional) Verify that the test data is flowing correctly (wait a few seconds for messages to start flowing): + + ```shell + kubectl exec -it my-cluster-dual-role-0 -n flink -- /bin/bash \ + ./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic flink.sales.records + ``` + +4. Deploy a [Flink session cluster](https://nightlies.apache.org/flink/flink-kubernetes-operator-docs-main/docs/custom-resource/overview/#session-cluster-deployments): + + ```shell + kubectl -n flink apply -f anomaly-detection/flink-session-anomaly.yaml + ``` + +## Scenario + +### Source Data Table + +The data generator application creates a topic (`flink.sales.records`) containing sales records. +The schema for this topic can be seen in the `data-generator/src/main/resources/sales.avsc`: + +```json +{ + "namespace": "com.github.streamshub.kafka.data.generator.schema", + "type": "record", + "name": "Sales", + "fields": [ + {"name": "user_id", "type": "string"}, + {"name": "product_id", "type": "string"}, + {"name": "invoice_id", "type": "string"}, + {"name": "quantity", "type": "string"}, + {"name": "unit_cost", "type": "string"} + ] +} +``` + +> Note: All of the fields are of type `string` for simplicity. We'll `CAST()` `quantity` to an `INT` later on. + +*(Assuming you have the data generator up and running as per the instructions in the [Setup](#setup) section, you can verify this by running the following command):* + +```shell +$ kubectl exec -it my-cluster-dual-role-0 -n flink -- /bin/bash \ + ./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \ + --topic flink.sales.records + +user-11188&30299767430689348882 +£838 +user-9467&63188787247555258843 +£971 +... +``` + +| user_id | product_id | invoice_id | quantity | unit_cost | +|---------|------------|---------------------|----------|-----------| +| user-11 | 188 | 3029976743068934888 | 2 | £838 | +| user-94 | 67 | 6318878724755525884 | 3 | £971 | + +### High Sale Quantities + +We want to detect if a user ordered a higher quantity than they usually do. This could be a sign that they made a mistake or their account has been hijacked, which could cause troubles for all parties involved if not dealt with promptly. + +![](assets/scenario.excalidraw.svg) + +## Data analysis + +### Setting up the Flink SQL CLI + +We're going to use the Flink SQL CLI to interactively try various queries against our data. + +First, let's port forward the Flink Job Manager pod so the Flink SQL CLI can access it: + +```shell +kubectl -n flink port-forward 8081:8081 +``` + +The job manager pod will have the name format `session-cluster-anomaly-`, your `kubectl` should tab-complete the name. +If it doesn’t, then you can find the job manager name by running `kubectl -n flink get pods`. + +Next, let's get the Flink SQL CLI up and running: + +```shell +podman run -it --rm --net=host \ + quay.io/streamshub/flink-sql-runner:0.2.0 \ + /opt/flink/bin/sql-client.sh embedded +``` + +Once we're in, we can create a table for the sales records: + +```sql +CREATE TABLE SalesRecordTable ( + invoice_id STRING, + user_id STRING, + product_id STRING, + quantity STRING, + unit_cost STRING, + `purchase_time` TIMESTAMP(3) METADATA FROM 'timestamp', + WATERMARK FOR purchase_time AS purchase_time - INTERVAL '1' SECOND +) WITH ( + 'connector' = 'kafka', + 'topic' = 'flink.sales.records', + 'properties.bootstrap.servers' = 'my-cluster-kafka-bootstrap.flink.svc:9092', + 'properties.group.id' = 'sales-record-group', + 'value.format' = 'avro-confluent', + 'value.avro-confluent.url' = 'http://apicurio-registry-service.flink.svc:8080/apis/ccompat/v6', + 'scan.startup.mode' = 'latest-offset' +); +``` + +We can do a simple query to verify that the table was created correctly and that the data is flowing (give it a few seconds to start receiving data): + +```sql +SELECT * FROM SalesRecordTable; +``` + +### Classifying "unusual" sales + +There are many arbitrary ways we could use to define an "unusual" or "suspicious" sale quantity. + +By looking at the data in the `SalesRecordTable`, we can observe that users typically order quantities between `1` and `3` (inclusive): + +1. Fetch all the sales and group them by user: + + ```sql + SELECT + user_id, + COUNT(user_id) AS total_sales_count + FROM SalesRecordTable + GROUP BY user_id; + ``` + +2. For each user, take all of their sales and calculate the average quantity: + + ```sql + SELECT + user_id, + COUNT(user_id) AS total_sales_count, + AVG(CAST(quantity AS INT)) AS avg_quantity + FROM SalesRecordTable + GROUP BY user_id; + ``` + +However, we can occasionally see sales with much higher quantities. Usually, between `10` and `30`. + +If we wanted to, we could simply classify any quantity above `3` as "unusual": + +```sql +SELECT * +FROM SalesRecordTable +WHERE quantity > 3; +``` + +> Note: This query might take a couple of seconds to return results, since the sales with high quantities are unusual. + +Of course, this wouldn't be a particularly good measure. Specific users might always order higher quantities than the average user. + +A more useful measure would involve calculating the average sale quantity of each user, and considering a quantity "unusual" if it is, for example, 3 times higher than that user's average. + +The following query will return sales that match that condition: + +```sql +SELECT sales.*, user_average.avg_quantity +FROM SalesRecordTable sales +JOIN ( + SELECT + user_id, + AVG(CAST(quantity AS INT)) AS avg_quantity + FROM SalesRecordTable + GROUP BY user_id + ) user_average +ON sales.user_id = user_average.user_id +WHERE sales.quantity > (user_average.avg_quantity * 3); +``` + +![](assets/useful_query.excalidraw.svg) + +While useful, the query above has several flaws: + +- The "unusual" quantities are included in the `AVG()` calculation and skew it. + - Ideally, we would not only exclude these sales from the `AVG()`, but maybe even reset the `AVG()` after an "unusual" sale. + +- The `AVG()` calculation uses all of a user's sales, which might not be ideal. + - This calculation could use a lot of computing resources, depending on the volume and frequency of sales. + + - Limiting the `AVG()` to sales made, for example, in the past week, might be more useful. + +- The query would become much more complex if, for example, we only wanted to return a match if two "unusual" sales occurred one after another. + - We would likely have to use some elaborate combination of [`WITH_TIES`](https://learn.microsoft.com/en-us/sql/t-sql/queries/top-transact-sql?view=sql-server-ver17#with-ties), [`OVER`](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql?view=sql-server-ver17), and [`PARTITION BY`](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql?view=sql-server-ver17). Assuming they're even supported. + +`MATCH_RECOGNIZE` lets us easily and concisely solve these problems. + +## Using `MATCH_RECOGNIZE` + +### Simple pattern + +We can use `MATCH_RECOGNIZE` to easily look for both simple and complex patterns. + +For example, you can match any sale with a quantity higher than `3` like this: + +```sql +SELECT * +FROM SalesRecordTable +MATCH_RECOGNIZE ( + ORDER BY purchase_time + MEASURES + SALE.quantity AS unusual_quantity, + SALE.purchase_time AS unusual_tstamp + PATTERN (SALE) + DEFINE + SALE AS + CAST(SALE.quantity AS INT) > 3 +); +``` + +> Note: [The `ORDER BY` clause is required](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#order-of-events). With streaming, this ensures the output of the query will be correct even if some events arrive late. + +We `DEFINE` a single `SALE` ["pattern variable"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#defining-a-pattern) for our condition, then include it in our pattern. We then output both the quantity and timestamp of the unusual sale. + +### Pattern navigation + +Maybe instead, we want to look for two sales with the same quantity occurring one after another. This could be a sign that the user accidentally made the same order twice. + +For now, let's simply match two sales occurring one after another, that both have a quantity of `2`. The [`PATTERN` syntax](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#defining-a-pattern) is similar to [regular expression syntax](https://en.wikipedia.org/wiki/Regular_expression), so we can easily extend our pattern from above using ["quantifiers"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#defining-a-pattern) to accomplish that: + +```sql +SELECT * +FROM SalesRecordTable +MATCH_RECOGNIZE ( + ORDER BY purchase_time + MEASURES + FIRST(SALE.quantity) AS first_unusual_quantity, + FIRST(SALE.purchase_time) AS first_unusual_tstamp, + LAST(SALE.quantity) AS last_unusual_quantity, + LAST(SALE.purchase_time) AS last_unusual_tstamp + PATTERN (SALE{2}) + DEFINE + SALE AS + CAST(SALE.quantity AS INT) = 2 +); +``` + +![](assets/first_last.excalidraw.svg) + +Notice how the `SALE` pattern variable doesn't simply hold one value, it maps to multiple rows/sales/events. We're able to pass it to the `FIRST()` and `LAST()` functions to output the quantities from both the first and second matching sale respectively. These functions are specifically referred to as ["offset functions"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#logical-offsets), since we use "logical offsets" to navigate the events mapped to a particular pattern variable. + +### Useful pattern + +Finally, let's use some techniques from the useful measure/query in the ["Classifying "unusual" sales"](#classifying-unusual-sales) section in a `MATCH_RECOGNIZE`: + +```sql +SELECT * +FROM SalesRecordTable +MATCH_RECOGNIZE ( + PARTITION BY user_id + ORDER BY purchase_time + MEASURES + UNUSUAL_SALE.quantity AS unusual_quantity, + UNUSUAL_SALE.purchase_time AS unusual_tstamp, + AVG(CAST(TYPICAL_SALE.quantity AS INT)) AS avg_quantity, + FIRST(TYPICAL_SALE.purchase_time) AS avg_first_sale_tstamp, + LAST(TYPICAL_SALE.purchase_time) AS avg_last_sale_tstamp + ONE ROW PER MATCH + AFTER MATCH SKIP PAST LAST ROW + PATTERN (TYPICAL_SALE+? UNUSUAL_SALE) WITHIN INTERVAL '10' SECOND + DEFINE + UNUSUAL_SALE AS + UNUSUAL_SALE.quantity > AVG(CAST(TYPICAL_SALE.quantity AS INT)) * 3 +); +``` + +This query might look intimidating at first, but becomes easier to understand once we break it down. + +`PARTITION BY user_id` is similar to `GROUP BY user_id` in the original query, it lets us calculate `AVG()` values and find matches for each user separately. Unlike with a global user average, we won't receive false positives because of users who order in large quantities often. + +![](assets/useful_match.excalidraw.svg) + +We use two "pattern variables" in `PATTERN (TYPICAL_SALE+? UNUSUAL_SALE)`: + +- `TYPICAL_SALE`: We use this to match all the sales made before an "unusual" sale. + - By not specifying a condition for this variable in `DEFINE`, the [default condition](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#define--measures) is used, which evaluates to `true` for every row/sale. + + - Notice how we use `TYPICAL_SALE+?` instead of just `TYPICAL_SALE`. + - `+` and `?` are both "quantifiers", just like the [`{ n }` in `SALE{2}` from one of our previous queries](#classifying-unusual-sales). + - We use them to match "one or more" typical sales, since an `AVG()` of zero sales isn't very useful. + + - By combining both of those "quantifiers" into `+?`, we make a ["reluctant quantifier"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#greedy--reluctant-quantifiers). + - This [reduces memory consumption](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#controlling-memory-consumption) by specifying to match as few typical sales as possible. + + - Since `TYPICAL_SALE` matches every row/sale by default, we need to use a "reluctant quantifier", or the query might match all rows and never finish. + +- `UNUSUAL_SALE`: We use this to match an "unusual" sale. + - Unlike in our original query, we're able to easily calculate an `AVG()` of just typical sales by using `AVG(TYPICAL_SALE.quantity)`. + + - This prevents "unusual" sales from skewing our `AVG()` and creating false positives. + +Unlike in our original query, we can easily use `FIRST()` and `LAST()` to output the timestamps for the first and last sales that we use to calculate our `AVG()` respectively. This information lets us know exactly which previous sales were used to determine an "unusual" sale. + +![](assets/avg_range.excalidraw.svg) From 4ac19bc43e9446e27a92ff7ce66e576075b695b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Thu, 10 Jul 2025 13:50:08 +0100 Subject: [PATCH 08/15] Add to "Useful pattern" section --- .../assets/moving_average.excalidraw.svg | 5 ++ .../reluctant_quantifier.excalidraw.svg | 5 ++ .../assets/skip_past_last_row.excalidraw.svg | 5 ++ docs/anomaly-detection/index.md | 68 +++++++++++++++++-- 4 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 docs/anomaly-detection/assets/moving_average.excalidraw.svg create mode 100644 docs/anomaly-detection/assets/reluctant_quantifier.excalidraw.svg create mode 100644 docs/anomaly-detection/assets/skip_past_last_row.excalidraw.svg diff --git a/docs/anomaly-detection/assets/moving_average.excalidraw.svg b/docs/anomaly-detection/assets/moving_average.excalidraw.svg new file mode 100644 index 0000000..6bd6523 --- /dev/null +++ b/docs/anomaly-detection/assets/moving_average.excalidraw.svg @@ -0,0 +1,5 @@ + + +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daVNcIslcdTAwMTb93r/CcD7Me1x1MDAxMU1N7st8c1x1MDAwN1x1MDAxYkVcdTAwMDVb7Vx1MDAxN1x1MDAxM0YhJZSySVx1MDAxNVwiTvR/f7dQqVx1MDAxZFx1MDAwMcu2nIGO6JCshazMe/KcezNv1t9f1tbW3XHfWv9zbd16uDLbdmNgjta/euX31sCxe104RCbfnd5wcDU5s+W6fefPP/7wrzCuep2nq6y21bG6rlx1MDAwM+f9XHUwMDBmvq+t/T35XHUwMDFmjtiNybV3w1x1MDAwMXrcw1xy1Sxd1H/Ub2+cVnty6eSkl8pcZqwr1+w225Z/6Fx1MDAwMcoxUmpaMIZcdTAwMDKF0PT7yG64rUmZgYJcdTAwMWY5PaNl2c2WXHUwMDBip0ht6OBHTE95+tE/1/zbOu6gd2tt9dq9gVez37Dl/fPrVTevbpuD3rDbmJ7jXHUwMDBlzK7TN1x1MDAwN9BcdTAwMTD+edd2u111x5O7Q2NCw61HfuPs+Vx1MDAwMXCkPO0q+NFmq2s5Tqi+vb55Zbte43C/0Ktgv9SYdMtffpVcdTAwMDZmxyp5/dJcdTAwMWS229Niu9uwvNZeN0nox7qN51x1MDAxZnvpU7/D6HPJT7/qluXdXHUwMDE4M6IpQULq6Vx1MDAxMd+wMEY8WnzY606sjFx1MDAxMaxcYlxc69fK2Vx1MDAwNuNyJ3e9NtuO5feAV7WdqOFcdTAwMDWNL2CAnW9ne49717ub7malO77f+nGwf1aePmbIXGJd68Fdn1x1MDAxZfj5/JfffMN+w3yqXHUwMDBmlpxgpqnQXHUwMDE0senxtt29jbZtu3d16z/Cl0CbRYAyu57hOoYxgrE08MSqNVOEXHUwMDExXHUwMDEyRlxmXHUwMDFj5THMXHUwMDEwbFxiXHUwMDBmK1x1MDAwMiPKMVx1MDAxN4THQcN4rlBClkBcdH5cdTAwMTeU0GSUhM5+hlx1MDAwM3RcYlVcXEichFx1MDAwNkZEXHUwMDFhXHUwMDFhqFx1MDAxMFpIpPUycMjSYn1cdTAwMDP0XGZcdTAwMGaePvDw172uW7VcdTAwMWaf6lx1MDAxYirdNTt222tsXHUwMDExun6jbTe9J1+/gqpag/Xg07s2kMv0hI7daFx1MDAwNMngXG5uatpda1Cah1V6XHUwMDAzu2l3zXYtXmdz6PZOLOep1u5gaFx1MDAwNdvCKr7YPjZcYp9cdTAwMDHW++/F44tcdTAwMGLXeSxcZt3xsfk42lx1MDAxOY2v42A1XHUwMDA3g94oymjMtz+vhbSIM5pfMlx1MDAwNSNRfuFnRWNcdTAwMTJnYfRmOFx1MDAxZc5LWiSVtDTAVFAmaNBcIp9hXHUwMDFhaPhcYkgxZ0JKzOVSpJWOUq5cdTAwMTTRXFxcdTAwMDctc16U9nt2lFx1MDAxM/2/1vxcdTAwMWWYfJn+/dfX188ugFx1MDAwNfpcdTAwMTfEuLFtOu5Wr9OxXXiSI69cdTAwMTaxXHUwMDEx0TVcdTAwMDfuJnSb3W1Gj1ndRsqRyVVcdTAwMWJcdTAwMWWOWpZcdTAwMTkzXHUwMDAyuC54XGaM2o6oSKtd743mXCLgqr7WxfPLXHUwMDExYZWdk+PbWqdDXHUwMDFlO9lhWpFcdTAwMDRUrzCdgunKmzEtsFTYI9EkSKeqUCyJ5FRpmSXxYi2ZwlxuXHUwMDA3lMdHQTpghZHz/4GI1vv33dHtxuigcVfYb2tinlx1MDAxZtxU5pHUXG5TQ1x1MDAwNT6+MYwjSH5BN2g5g1x1MDAwND8+Jfh6Ol9oT/U6XHUwMDFiPa/HfyncT+ZX1JhRRiiANFx1MDAwMddMxbzOKbC9i5DEXHUwMDAxRycvivq0unNSoDxZV5NQ6Zt1tdvrp4nqUJWjXG46VseQjlx1MDAwZT/XXCJC+lurb9lb5Fx1MDAwN73b1nvDb+Vmr1dtxCHq3TIjXHUwMDFknS9cdTAwMTDmiHK/v11GU85cdTAwMTAgLIlyaTrlYs0lXHUwMDA2XHUwMDE3OUtcdTAwMTVccpRLOIJcbvGgWX68iia5Z9zQsX6vPW5O+u81rlx1MDAxNdeHpcHeZblx2N9xb3bE5sX+8Hiu8Fx1MDAxNVwiXHUwMDExICvfXHUwMDEwXoDMXHUwMDEykEw/f3jqnZB8Nj+bXHUwMDEyjMGvXHUwMDEzKk6cXk+QaKHPplx1MDAxMlCrmfahnlx1MDAxMWaxxMgntyXYXHUwMDE0JfIoUaHSXHUwMDBm41GUOYOeWpVGXbbMUsXcLdilw8E5PujPxaDBkNKKQTPA3fnbXHUwMDE5VGFNsZQoUd3GePVcdTAwMDWOgGJcbi7HO8BxRaGBq96ZQmv1tn3f2q/LcrFeK583Rt/uy+5cXFx1MDAxNErEikKzhfLFXHUwMDAyXHUwMDE0qlx1MDAxOfikXHUwMDA0JalejGNA9jHLXGJcdTAwMDP2JVliNlx1MDAxYoc02Vx1MDAxNc1cdTAwMGKFZu+EXl5cdTAwMWWeulx1MDAwNFx1MDAxZlomdE5xeF1outVcdTAwMWZzUSiPatdcdTAwMTWFPpcuibtcdTAwMWZvp1DNXHUwMDA11yo4XHUwMDA0+njk6V4oQkpqhPiKQj8xhVx1MDAxZVxy+Z1p3Vx1MDAxZpnqotTt99RduSPmivhijlZcdTAwMTSaLZTNXHUwMDA1YrogX4G0XHUwMDEwScIsJqluKMFIS8VcdTAwMTnJ3zJcdJxvP1x1MDAxNGfviFxufDco7102j3ucXCJxdLa/iVHC/GlcdTAwMDKLyqh8XbHoc+mS0Ktn4IhyIVxiY4mxXFxcdTAwMTFbzeSviCBYXHUwMDEyJeWKRT8xi152L8d93S1dXHUwMDBlTy1n/H3jsKzJeC5cdTAwMTaV0UmZXHUwMDE1i75cdTAwMTHKV1x1MDAwYrCokFx1MDAxY/4hneiJzpiAod68XHLDPH9TozjfrijO3lx1MDAxN1x1MDAxZKNDcttGj9/63+/u91x1MDAwZlx1MDAxZUiLXGbP5sJedEJcdTAwMTSjwKrT6SokXHUwMDFjWVx0XHUwMDFmx+JqmUIqXHUwMDE4r1x1MDAxN1x1MDAwMCNcdTAwMTVSeWtcdTAwMGWSwEiwipa+gFEhzlx1MDAxMZWZRnLftqTwXHUwMDA1i/3h4KplOtala3estf84XHUwMDE2XHUwMDAwpeH8N9fLXHUwMDE2Xq1zVtp30HncXHUwMDE53LROi0Vzm+1W3ZPzXHUwMDAyi6M2JctFXG5iMEVcdTAwMDV4OlqB8lxup7xwqlxmzZSkXHUwMDE0acFoYHZsuvhcYidxqiDYQFx1MDAxY1x1MDAxMyE0olxcaH8gzjWsXHUwMDE3pNg3Y7q1XGLBKo5Y8pJCkj5ZKlx1MDAxNUEgX7OGtERcdTAwMDL+WGSudJbkXHUwMDFijfu4Xdjr8Fa/VN4/1WZ/+G1uXHUwMDAzXHUwMDA2qiGG0FJSyZjAKNBCXlx1MDAxZslp+pVCXGJO0DFcdTAwMGJcdTAwMTZcXFx1MDAxYZIyXHUwMDA0Plx1MDAwN1wiXHUwMDE0XHUwMDEzXHUwMDFjN2dMXtK8sNacU/zPtOe3Z6fcLmDQinIwTCqT4i5cdTAwMDSnZqdg6CpGXHUwMDA1ydTNXHUwMDAzk1x1MDAxNuBw4lx1MDAwMHcsadLJoYZg4k1cXFwi+yElXHUwMDAyXHUwMDAyWlx1MDAwN004/dGesPNwuc2Ljmpvl63Rzu59R/abndOc2NBcdTAwMGI0qIFB4GtcIjjl3upmv8Of/DbJ/Fx1MDAxY0pcdTAwMTbQjeOQZpzAV6ai7OnyxVE25TGUQGOBRVxuL1aLXGJcZqmEXHUwMDA1rC9cdTAwMTkqi1hcdTAwMWXmijC6gOVcdTAwMDVaYVx1MDAxOT9+zvjAkp7+e4U82Kuni4XPXHUwMDBmJ+/ir7NcdTAwMGW+dreAXHUwMDA1Tb5GnFx1MDAxZE1cdTAwMTerfiF2XHUwMDAzsdhccqjBOZFcdTAwMTNfXHUwMDAwsMNcdTAwMTG2XG6YL9Tis2JM/phcdTAwMWbIfUvhh1QqmD8s1G2Ob81WXHUwMDA3b45Kpe2bXCK97/TuXHUwMDEyMlTTNFx1MDAwMqXyWeWGR1x1MDAxN/TSyYzCsJsgXHUwMDBlXHUwMDE0iXRCfJCI5nf7p/yTtMHbPdg6SsldXSTDXHUwMDFiSaSR1CxcdTAwMTZQmvRcdTAwMTWLlk6ZXHUwMDE15CGiWi01VzOtXkKG903x7uZm+7Q/XHUwMDEwvV1+cVa0XHUwMDE5XHUwMDFh+IBcYtnmh2Z4z67nrNBcdTAwMGVcdTAwMTXKXHUwMDAwrSaQSs3wnomgf3G2d1x1MDAxNphhXHUwMDBiKGqpMLjogibOZFwikjpzwsDeJEc0hzHY5Fxiz69N+H6Fe2KB2ZRcYs/SXHTfldFQt8toj1x1MDAxZl90W7dcdTAwMGZkT5xcdTAwMGZcdTAwMTOmRKx22+47UeZcdTAwMTOEXHUwMDE4UkjOXGLXXjQmToDP8looXHUwMDE5dE1e4CuFIVx1MDAxNPJyK8A1XHUwMDA2XHUwMDFi8bvTJ0A/ZpRcdTAwMDf0skj5RzJeeVx1MDAwMfRcbiExXHRlf1x1MDAwNjJY0pPLXHUwMDE4XHUwMDA1h5hoX7y+ndpcdTAwMTLZIGCQ1+S4ZvLqcbPJZa1hVivbtUL9w6gtuTbzUJvgyMBaYsZcdTAwMDRnXCJYoVx1MDAxN2pcdTAwMDO6U95cdTAwMDRcdTAwMTU4fCw+m/hcdKktfUbDdFq/dkajjlx1MDAwZeZHXHUwMDA3l0gwJFVyeldqRIVLymlcdTAwMGXnXHUwMDE2U1x1MDAxMi5/La+9wivzrn1dmtdcdTAwMWFDk5pWofdYxVx1MDAwN2WL3ur7ve3TOHDTfDqm0LP0xNib7vJ7OezaIeUtdPa7a+XaPXVvplCee1x1MDAxZpR0105cIimF0jhcdOJcdTAwMWGlx4O1pMzbzihDXHUwMDAyfLLP5mbF3t7Z2KpcdTAwMWbhzf37WuPghN43wnyYXHUwMDBi3252PWcuPMX4afcuXHUwMDE4XcHBY+GpXHUwMDEz37dLg9AnJMA8+XYpXHUwMDFijSSqQ86kolqTpMBcdTAwMDfGKDXyIbzpP1Av+cv0yIVv91xuXHUwMDA3vbtvt7F93DkxizvOzcHGZqN+craxv03j2E1auMqBnMBnUEhpKmWE/rTCXHUwMDA2o4JL71x1MDAwNK2R76JNXHUwMDE3tVx1MDAxYYhSgjVG4FwiSk5Z0sQnTb1NXHUwMDFlsJwrMXs8P5jBk0Y8pEtcdTAwMDNiVqRvI+LNnklvNj9jLDOMcVx1MDAxZbb8SrdI71OIXHUwMDE5o3/DXHUwMDE44X7mJbC2PFx1MDAxY1bMan/3XFxvtMrO8dXp3sica0yIZVVcdTAwMDe+z1jMTnO2v1+ucL3I9kBCXG7BhaRJO27KuLCdcjRDIMVY1itcdTAwMWG83FDBXHUwMDE2WdHwTriOoPifunR9VHQ63Zoql3G51m+gy+uzb51cdTAwMDW2m6ZIeFx1MDAxMy2UXHUwMDEwrCRnPLzxl1TSIFx1MDAwNIq54oRqmlx1MDAxMI3S2EBMwlxiKmFs1ChhXHUwMDE3MIy44U2CYqFcdTAwMTicgD/LXtRcdTAwMWaB++r8uFx1MDAxN1x1MDAwMrxPnFx1MDAxOLidsYGn0FxcwJXL5Y9NIZTgt244Q3mpzKvK5rZ1cDJGtZNd6ue2hmzxaUPJ6ZFf7Ljap5ejwtWd6lx1MDAwZa1iv3Y+uulcdTAwMWPtj+ZcdTAwMDdcZlZcdTAwMDJcZp5ihYTk4OREwrf+3D5cdTAwMDe4oIRcdTAwMDDQau/2jNb91VHt7fFcdTAwMWZv9Vx1MDAxZNUqnuA1XHUwMDAxtEpcdTAwMGZcdTAwMDBcdE604Mup4lk4umnume2tStusOOet3sWZY93ukmRcdTAwMWN97Nz+zHrO3LtcdTAwMDNzgzBv01x1MDAxN0EoXHUwMDA18Vx1MDAxMkaQP7efgqB/cfwnXHUwMDAzzJwuoiyJpGA5iTtPYjYjOoqJt/+rZPnLUs7FZu6vUNC7b+buXHUwMDFj1pz+XomNXG5H55VcdTAwMWZ4XFyv2CxhXHUwMDBlJGldXHUwMDBlXGJCqsBP1lxie6+vXGLHf1x1MDAwNIhJRbVcdTAwMDBUe8jlvtlPsStBUMKlXHUwMDAy/Gwpg+yZ18zIXFzJw5T9tpLAq1x1MDAxNFx1MDAwMVx1MDAxNIp4YMdr4vTILVbgT1wixZfL3ZhFbLJVPqnbR6XbxvlQumrvsFF/XHUwMDE4zydcdTAwMTC/zrrvsVRddGY+fP/ecZB8rNVcdTAwMWL3upqV8GREc/am3fnOSrVi6XDtd4x+X6vubFVcdTAwMGW3XHUwMDEzR6C0XHUwMDA0T1x1MDAxNbrh++ePzapuRsHnmY7CzF3nwU1cdTAwMDVnlWmulFx1MDAwMKc0LL2l4oaSinIqXHSiMlx1MDAxZXsmwpBcdTAwMWFcdTAwMGKmXHUwMDA0okSDeE+Q3kRcdTAwMWLK2zNZqImbXHUwMDA0XG5lNVx1MDAxOKVcckYpO4YtsueC18jQXHUwMDFk8XckPanv1EFKe9Iw+9dQvEFfZFx1MDAxYbtKt1TvU4hcdTAwMWKpf8PYkLZwbCvQf8+vOivNXHUwMDExaJqMW1dDr1x1MDAwNVxuXGJwiKSQRCtcdTAwMTCJivmJa561mf3JXHUwMDE4QZn0VodSUFx1MDAwM1x1MDAxY1x1MDAxYT5uI6GwWlqlWj82RfesZo+v0ZDu3dnFXHUwMDA350AnVVxuXHUwMDE50E5cbtw971xyJlx1MDAxYUvkq9dppZiBNfHeocA4Z4SpXHUwMDA0u41cdTAwMDb0knbrn1x1MDAxMdabfyf/2Vx1MDAwZjZTomFhICSJt+ySXHUwMDExLsO5tVx1MDAxMo7CKMlAnFx1MDAwMdVLXHUwMDEyX3tcdFx1MDAwMyyYXHUwMDE3Ut4qtDT3aqXR0ofFlFxy3ZJ3gcJcdTAwMWFkMovvVOH5eunDXHUwMDFmXHUwMDA3iYaVxEstMZulpfJcdTAwMTXEi2mp2sVRaWujfFndKO/kWkUlVzRcdTAwMWL9NFtHz9RPQnJDXHUwMDEwgVx1MDAxOKJaS6nDXHUwMDAySlx1MDAxMGZMVkNxjKDHRPxcdTAwMWR6XHUwMDEy5Fx1MDAxN8eaMoGJXHUwMDBl5tfn9PU+uVx1MDAxYVx1MDAxN1J2h1vobVx1MDAwM1pcdTAwMGKGk9ejxlx1MDAwYl+GXHUwMDBiXHJcdTAwMWW5pHgpsVx1MDAxNHqYuHckXGJccuxcXPZRcinNLCNcdTAwMTe/nzSaXHUwMDFkT1x0SyNcdTAwMDTt5k2EXHUwMDEx8GFAa1x1MDAwNN5cdTAwMGbqiyNvSlxydJFcdTAwMDSZq4B/XHUwMDExj1lE1i9cdTAwMWK67Fxy3fDU/vxSZbZcdTAwMDM+26GDXHUwMDAxScJAJDmahFxmw1rl9Vx1MDAwMSlcdTAwMTCfX1xyQa9cdTAwMGZBXHUwMDE57JInqbcgUCe+NzCeXHUwMDFm6b9kjHpcdTAwMDPXcttWvjJcdTAwMDYtXHUwMDFkocl0XGYqKJFyflx1MDAwZYadeUdcdTAwMWRQXHUwMDA2k/WemlEqXHSOXHUwMDE5wVx1MDAwN406X55bb93s96sudP+0TdbvbWu0XHUwMDE5XHUwMDA37W/Xk48nTye191x1MDAwMGtNTP3nl5//XHUwMDA3XHUwMDBmhn91In0=3USER-35051015purchase_time (seconds)1513WITHIN '10' SECONDTYPICAL_SALE \ No newline at end of file diff --git a/docs/anomaly-detection/assets/reluctant_quantifier.excalidraw.svg b/docs/anomaly-detection/assets/reluctant_quantifier.excalidraw.svg new file mode 100644 index 0000000..542cb62 --- /dev/null +++ b/docs/anomaly-detection/assets/reluctant_quantifier.excalidraw.svg @@ -0,0 +1,5 @@ + + +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1d2XLiyrJ931/R0efl3ohGp+Zhv9l4nm1sPNw44cAgjGxcZpjR+MT+95sl22hcdTAwMTZcdTAwMDJEN91cdTAwMDGO3btbXHUwMDEycklVK9fKrKys//717dv3/rhjf//723f7rVppOrVuZfT9hzk+tLs9p92CU8T9d6896FbdK1x1MDAxYv1+p/f3v//tfcOqtl8+vmU37Vx1MDAxN7vV78F1/1x1MDAwN//+9u2/7p9wxqmZ71ae5Ig+vPJccmfzdDhcdTAwMWN13+7sx333q+5FX43p2tV+pfXYtL1Tb3BcdTAwMWNjpCzlffTk7Fx1MDAxOM5ygS1sPkgrTVx1MDAxOFx1MDAxM5OzI6fWb8BcdTAwMTVcblnI/5GTK1x1MDAxYbbz2OjDJVJb2v/xbvLRnr+/ocmRXr/bfraL7Wa7a1x1MDAxYf0vbJtcdTAwMWavyVx1MDAwZpXq82O3PWjVJtf0u5VWr1Ppwjvyrqs7zWapP3bvXHUwMDBl71x1MDAxOd7p99DvuP58XHUwMDAwXHUwMDFjOp70Lfilj42W3etcdTAwMDXa2+5Uqk7fvCqMvKOmhZ39mttl//Ha1K282Pumz1qDZnNy2GnV7De3I/dcdTAwMDK/rVX7/G1f/e11Jv088o/Xdtuuub3JNKdcXEmvXHUwMDFivUFcdTAwMDc9jcKHT9otd1x1MDAwNGJBONKU+vrP6W3B0Ou7961Xmj3b61x1MDAwNNO47fCw9Fx1MDAwZk3f8Kzf3lx1MDAxN1+ud1x1MDAwNy+bx/W9XHUwMDFiPrhcdTAwMWWqvpo8aGCI9u23/vfJiX8+/+a9wEGnVvloXHUwMDBmlpxgxjTTXHUwMDAye4/adFrP4bfbbFefvUf4y/fWQjBKb2ewjSFcdTAwMDRRZSGDXHUwMDFkrqjEXHUwMDE0XHUwMDA3XHUwMDExJJUlUlx1MDAxMUSwJVxmclx1MDAwNEaUY1x1MDAwZVx1MDAxZFx1MDAxMYVcdTAwMTDjK4VcdTAwMTkyXHUwMDA3ZvByMLNcdTAwMWaPmcDVX+AggnGNXGKTceBgiCaCg2tBiUKczlx1MDAwM448x683XHUwMDFjzTCE58e+7mu3+iXn3bVcdTAwMGZcInB0p/LiNM37XHUwMDE2ge9vNJ1H8+jfq9BUu/vd//h9XHUwMDA3iGhywYtTq/mJo1xuN604Lbu7n4WB2l3n0WlVmpfRNldcdTAwMDb99oXd+2h1vzuw/e/C3vtcdTAwMWH+2FwiPFx1MDAwNbonm1fVQqu+p3q75K10w/cuavowXG5du9l0Or0w/1H2yX9JzCfdj5rKfN7LncDWZ29XXHUwMDAxtmx1qO4gK9WRJKpjSlx1MDAxMMGRz9x6YObRo19YXHUwMDE2SFxujlx1MDAxOZ5cdTAwMGLKaTzXOqGNXHUwMDE2K2y+s8Ny9fmofHhbrd5n4rlcdTAwMWZpt8Xdy532ZWeXYHL0tiNGnVFcdTAwMTG3429b6Xbbo1n4kzMuQS3kw5/pz5/Gn1RcdTAwMTKLXCLGOVx1MDAxNVx1MDAwMuiPXHUwMDA1caiopYhQXHUwMDE0dFxydDbXXHUwMDExXHUwMDFjUmr4XHUwMDEzXHUwMDEzhSVV8GdcdTAwMWN/0t+fP5dcdTAwMDTEw+z8yZVghFx1MDAxMc1jXHUwMDEwxzBLQlx1MDAxY5VcdTAwMWHcXHUwMDAy5pMwq0KeXHUwMDA0xbInUYGjy2bPKexcdTAwMTVmT3+j86HPYXnv/Pa233svXGb64/PK+2h7NK5HkfthXoLQRcxcdTAwMWKA5lx1MDAxNWnh/ftcdTAwMGKf3pFcdFx1MDAxYzlaLWJcXCE8nixMjFgrQlx1MDAwNWVcIqJnv1x1MDAxYl88UeRcIlBCYGhR3jhcdTAwMDXLLORcXDjttJ0w8Xp/++b1gPuPyd//82P61Vx1MDAwNVx1MDAxOIHeXHUwMDE3XCJE2az0+sX2y4vThyc5M62ImMR+pdvfhG5zWo/hc3arlnDG/daGwVHDrkRcdTAwMDZcdTAwMDF8z39cdTAwMGVcdTAwMDa1XHUwMDEzXG7Y2M2H9ihcdTAwMTNcdTAwMWKXdF3v3dyPXGI73b44f758eSHvL/lhWpFcdTAwMThUrzGdgOnThTEtsFRYaFx1MDAxMee5qlxiXHUwMDFmTyAtXHToO6V96FtcdTAwMWPSWEumsMLcey2/XG7SvlFcdTAwMTi6/lx1MDAwZkS0czW+cZ53ry6Bna+r19WD2/dxpviU1NiihDKfXHUwMDA2/lx1MDAxMNZRWGMlLdclwIJcdTAwMDGBIO2ByFx1MDAwYkWtXHUwMDE2zFx1MDAxM8O3tbbp6p+K8/NcdTAwMTliUVx1MDAxNCtcdTAwMGWWVsa5r4SS8NEvSIOUxpxcdTAwMTDufS1cdTAwMTeShv/gx1x1MDAxM7hziOmL7aOr4uXGyWV8RIpcdTAwMDSOLqyp++1OkqBcdTAwMGU0OqyeY1pcdTAwMTlcdTAwMTDRwWebRUXrg2Fr9LwxOq69XHUwMDE2XHUwMDBlmppUbo6fTjPhU2lcdTAwMGK8Xq11TFx1MDAxOMpPtFx1MDAxM5RqZGlJJlx1MDAxZuxccoY1Sqej9GJcdTAwMDaUglx1MDAxNSRU8jglzdKkNMFSc6Xz5F2DUsO9ZCGXd/die3vrdqUhXHUwMDFhbmJO+DxsdGynSO7o65beXHUwMDFkXHUwMDFjXHUwMDFlPbbbpVpcdTAwMTSf5pb5OLlktSC4Qnq4vLiPSzlDmPA4PUyT9TDWXFxiIedcdTAwMGL+Juthwlx1MDAxMTRoljjqcvRw4Gqy8nI4cK7Tbo5cdTAwMWbd/psmhEX9ZL+7e39UO+ls95+2xebtweA8XHUwMDBi0YJZXHUwMDBlXHUwMDAxWXlcdTAwMDPhXHUwMDBiyCxcdTAwMDbJ6+hxXHUwMDEykq+zcynIXHUwMDE0opDwXHUwMDBiXHUwMDFjXHUwMDBmszpR8GImXHUwMDAxtZrpuVx1MDAxMlx1MDAxM9IwiyVGXHUwMDBiKd6fXHUwMDEzPZ6XRpPCxPMz6JV9WnuQjcr+aWWn4OyfdG/wcSdcdTAwMTODXHUwMDEytWbQXFxxd7M4gyqsKZYymvXgatskOFx1MDAwMoopIXRcdHBcXFOo71tLptDLh6YzbFx1MDAxYzzIo72Hy6Ob2uhweNTPRKFErCk0XyjfzkChmoFHSlBs/lx1MDAxMk7O7aOYaGRYdOUoND59aVUoNClRaX5cbr2/P7nqXHUwMDEzfGJXoHP2XHUwMDA29cJjv3SXiUJ5WLuuKfTz6Jy4u1ucQjVcdTAwMTdcXCu/XHT0pSAle6FcYimTXHUwMDEywddcdTAwMTT6XHUwMDFiU+jZgL9W7OFZRd3utzpt9Xr0XCIyhXuxP/lhTaF5QLkyQ0RcdTAwMTfkq6ZcYpE4zGKswke/QCtcdTAwMDGvREiR57xLPlx1MDAxNEpWmkJJ7lx1MDAxNCrwa/do9/7xvM0pXHUwMDEyZ9dcdTAwMDebXHUwMDE4xWQ2xFCoXGZr1zWFflx1MDAxZZ1cdTAwMTN3XHUwMDBmOXihXFxcYsJYbCBXJCbxglx1MDAxYcKSKCnXXHUwMDE0+lx1MDAxYlPofet+3NGt/fvBld1cdTAwMWKXN06ONFx1MDAxOWeiUFx1MDAxOZ6RWVPoglCuzkChQnL4QTrWXHIliXnAimnJlVwiuU++LEyhdKUplOZOoWN0Qp6b6P2wU35cdTAwMWRcdTAwMWVcdTAwMWO/kVx1MDAwNlx1MDAxOVxcZ1x1MDAwMl54Klx1MDAxNPtcdTAwMTNYJsmBOLTgM1xuxHV6QlwiXHUwMDEy6zMgkVxuqUyCQlx1MDAxY1x1MDAxMoEhXHUwMDEz6Vx1MDAxM2mCKFnCejautC86NVx1MDAwN1x1MDAxNDuDbrVR6dn3fefF/vY/PVx1MDAxYoBS6/3vSicsTG1zXsq3+/K+3X1qXFzt7VW22E6pf3FTYFHYJiz0XHUwMDBlq19cdTAwMWGjfjGOY021Ytn6ea3YXlx1MDAxOKqNWUjTpPvFJ/BcdTAwMTKenEmkKENMcpRzJlx1MDAxMdOEXHUwMDEwn2haaOWX3WhstfqsVWbDV/5wpU8uru2dzOMysCrdTUxl0YEp6FRKwSvmp+U1SvnCw9SZaZhcbkSRjuaUm3HqS1x1MDAxMFxyr/FcdTAwMDJcdTAwMWaOSSRyr1x1MDAxZbDc5Y9cdTAwMTJRvnD5gPiIgz+qXHUwMDE0XTHnTaeClFx1MDAxNjzTbOrHXHUwMDBi2bdcdTAwMDekcI43t65cboTf7Y37XHUwMDBmN6i8XCLj81x1MDAwYnbUwlxmc02EKUWhpfbsTGxcdTAwMWPGn4lcdTAwMWXG6cfS6tlxOmE0XHUwMDE0S2iRca8w4kJjX0fEg+3nLFCcz2fPXHUwMDE4XHUwMDBimNOrX1Z4g029XFzMfH1wYT7+kXZy2t18I8j9Z4iINJ2t+YXIXHLEbDegXHUwMDE250S60lx1MDAxZoRcdTAwMTlH2C5gPtNcdTAwMWJPiyd5rPHkMyrxXGaTSCbZQ0Cv1/aNVLWtTTQuXmzgXHUwMDAzefK6f5UlmKtCkygyLk8+XHUwMDE2/L+JSpjV81xcXFwmvMwgXHUwMDEzsNJcdTAwMThjieNkXHUwMDAyT1RcdOB1XG6t/PVXclx1MDAxMLNIXCJOjP6Yx9DmatZwyFT8qXHbp+s3du6cXHRcXFx1MDAxYjWvd57K3fPNjZhM+kS9r8FcdTAwMDRrxVx1MDAxNJNcdTAwMTR+QuVcdTAwMWX4l30m3P2JoJpQboFDhFx1MDAxMKVMXG7hX9bm+Vx1MDAwMr+Lx1qr9Fx1MDAxYT83vvSA8FxmOFdcdTAwMTIrTkFcdTAwMWTF4JzJxFCv0dbCVIuYa7p0XHUwMDAym7hqYnvnXHUwMDE3xbudi/phqYlq+FbdXHUwMDExgXLzXHUwMDA3mMRcIqdyYmdbQ7YtKq/V4uDQObZPXHUwMDBiY/5As4NcdTAwMDS886+qfCbFXHUwMDA0qSBKJsWJhJaE4ZhcdTAwMTjsuixffogpZp3nTK7LxyV4XHUwMDE4VMfOc0I3JiNJYMFcdTAwMTlVOvd6Rc394rBVPb6UzVOBXHUwMDBm1fi5/rTdikfSL63Ll97O1FxcVT6py4dcdTAwMTSmNFxioUldviRcYq3r8rlH51x1MDAwNc3WXGY0wygwukA0vjBcdTAwMWZJnsggoPtAJMhcdTAwMTWsLVx1MDAxND9l8XNcdTAwMGLzTSGhrMk6c1dcdTAwMTbiLWFfXW/vjm5Ko9fW+cXbXWE8XHUwMDAzXHSChvgkQU5cdTAwMDTn0ntLXHUwMDFmUvGLwLhcdTAwMTKY6GhlsDVcdOaI592FSZAhIVx1MDAxNdFcIs5pxDh62Fu2qSRTmoi5JkHSOFx1MDAxMJ1cdTAwMWTp6rA52uhcdTAwMWTt3Vx1MDAxZtqVncFcdTAwMWJOUJO/lFx1MDAwM9PbmcaBTH9xIMh1qXVIR0ppsVRcYq1J0D06L2hcdTAwMTIqOsdXp2VYXHUwMDExxuPTaphKTE1cdTAwMTVcdTAwMDJpXHRcdTAwMTesXnHaleDAKVx1MDAxY7R0XHUwMDBl7KnXzuC62Ti4XHUwMDFk35SeRzdcdTAwMDfV3V4xit344rRcdTAwMDJhi1wiI1+RMkvvwsGSL/aCKygn093AdY3aWdCbUFt6hvxWsJlSXHUwMDBiXHUwMDE5W6RWoOT8VmrksEAsd6/v2imS886wXFyq7500ittDsndWP87EeD/SbutcdTAwMTRcdTAwMWaf9+jV6d2YVJtDdF7HJ0+H8bedIywjqFx1MDAxNsRcdTAwMGa++Zk0/fnTmFRcdTAwMTBmSUIl40Ck0bilXCKWXHUwMDA0L58oLpDSKFx1MDAxYbdcXJepdY/Oi8WEgtFxTKqV5Fx1MDAxNOnYZZJMJc5OcKJcdTAwMTjViC7Dl/TNTv22dWqnXHUwMDEw2fLr1Jaf9+9oj1x1MDAwZlx1MDAxZKeG7lx1MDAwZuTN5cF1V2T3JidcdTAwMWKdaGzKx+hgPEhcdTAwMTE6kcJUsLi01rU3mVx1MDAxYpxx5pKYid6kVIBz013x3mRyRNXAXHUwMDEyXFzR+YpzpZHg9qjR237rPfDycXG8tTPYXHUwMDFjbN5cdTAwMTUzcet0XHUwMDBlxFx1MDAxY2M8y8KUXHUwMDE0XHUwMDFjpbcz004nIE5A2OBgRUnF0CQgXHUwMDEzXHUwMDBmobU36Vx1MDAxZZ1cdTAwMTc0Z9k5kFx1MDAxMK5cdKHx6GAqMaKqXHUwMDEwN1x1MDAwYpp1ziQ4+/iNkOBK7HQyhYKWvtNJ765bvC3dPTvN42dnfOFcXPU5j1n8OG2nXHUwMDEzQphcdTAwMTAoiVx1MDAwMKkkWMUo2LUz+W1cdTAwMDHwJlx1MDAxNIedwZnUJtkh0G/+tZLJhGfWZGimcO6T8TvvR8flkuxVh+rl+eSl3H/qdlx1MDAxZVeQ8NLbmW1rXHUwMDEy49MpXHUwMDE2glxmXHUwMDEzXHUwMDE2cZ0+TIDuYlx1MDAxNiGunT736LyYmaFUq5CU4kCRa5/Tp5NcdTAwMTdYUK0l0jjfnLRcXFxib0W8vnTGWb7XN641nq6pdkZcdTAwMTeK8v1L1jo77mdf9WRWl3wllLmkXHUwMDE3XGbbXHUwMDAwzVn0M8uGMeRbvLpeebJ4SulcdTAwMDMuzTD/IbTUMlx1MDAxZcBcdTAwMTQlXHUwMDAymFx1MDAxMlxu+OVcIufZj5z4Z+raXHUwMDEwklJFWmjOXHUwMDE4/JmFtz9XXGKeXFw0bprjd4E6XHUwMDAzVFx1MDAxMmdcdTAwMTfPXHUwMDAyv6/IXHUwMDEwyr44JFxyryxcdK+/auFcYjNcdTAwMDRcdTAwMDLyIMeFI1x1MDAwYnDHeuHIeuFI0lxyftLCkVx1MDAwN+zfXHUwMDAxYmkrR9rHfYlIjZ7dlZu1XHUwMDExK79cXF9VY/b8nJ6EboLzlIVcIln4y8yAxNfEt75+Ul1ESkt6XHUwMDFmn/RfJ6FnXHUwMDExXHUwMDA2V9mFXHUwMDAxXHUwMDAx95VzXHUwMDE5P52jk+dQwe/ViOrclYEw2Yrer13IMy10XGLfOcfFXCJmJ+P68/tx6/ouJp4zNUtcXCNcdTAwMDVOKibBUTyJ6Fx1MDAxMG029Flv3r7UIZ15V4OULHHoLibAJ42Tijg5XHUwMDA1XGIrXHUwMDA2Toj0q5CcQjyXR1x1MDAxYse98sbhqTitnpeO2rvomXmKMjBIf2mIJ72dmbLEXHUwMDA1oqDGaVhwfs1pJGFoPafhXHUwMDFlnVx1MDAxNzUz7CCgXHUwMDE1YpjFz+ubsZSIXHUwMDBlXHUwMDAy+NBa5T2zn0eMZ1x1MDAxNSY1ppDQ0jPkSJlutjtcdTAwMTdbt/vnw1a90Ljv7e5Us5OglyVcdTAwMGW0XGZdXHUwMDFkJsEvKYdcdTAwMTXilERjtGtcdTAwMTLMXHUwMDExzpk3JkjOXHUwMDEyV+DWK1x1MDAxNoWzi2SWXHUwMDEyyTXmXHUwMDE5+adHcuLAY4E3RtX9TVTrlVvDS9682dktrCBcdTAwMDemtzNblrgwXHUwMDAyJFx1MDAxY3Sh+lx1MDAxM2BJXHUwMDEwWnOge3Re0MywXHUwMDA1gKSMUuTPy/ZzoEzeRlx1MDAwN/SLcVRzrb74XHUwMDA3ceBcdTAwMTRcdTAwMGVaOlx1MDAwNzaqZ1x1MDAwN8V3QlvXZV5771x1MDAxNVx1MDAwZfXN02D2LHFcdTAwMThcdTAwMWFcdTAwMTLcgFx1MDAwNFx1MDAwNmSISM6mu4Hrif1Z0JvDRlx1MDAwMtQk93NcdTAwMTJd6GhcdTAwMDa8SpzZJ8xksnGaeybb6dXtho1cdTAwMWRReri8PSpdXHUwMDFl3JYvXHUwMDFmz1aQ8dLbmS2bW8P//Dtkf3h93MJcYjxxiTknmkTjf+uJfffovJiZqWI/XHUwMDE2nMfnefLo9sle+Fx1MDAwZkabUiTvZVF/zMT+XHUwMDE0xln+xH7tiDVcdTAwMWZPySl+f9V3g3Fzg7++laPg/VgvXHUwMDEyRC+lwmImNq9AXHJJLkOEN4neXHUwMDBiLmSMXFxcdTAwMDW1Slx1MDAxNJJCYEXB2SC+VZFe1X9mgZqF4eNOMFPiS/1ZXHUwMDA1MK9UMD+HfVx1MDAwMFx1MDAxONhRTGNcdTAwMTJ1jOZNKY9BiUlXRbnuy2FQbopKsbmKiudbUipxpJpPITpIvVx1MDAxYkaY+KeVnIKx7YRcdTAwMDI1aTPEdvOhPcpE9/pp/6lxvrn5XFx6r1TH242ni7qqZ6J7wphxcIHSgfg5psHpPsmRW1x1MDAwYlx1MDAwNGkppKIsxmIoZimEXHUwMDE0I0QlObgrRvcrZSFm2F5AmPlcdTAwMGatY5dM+i19tFBcdTAwMGX0XHIh+VtcdTAwMDKMMJ5l9WGE7y9vz/aLXHUwMDFiR/eljaPtmZhfXHUwMDA1brX8QubxXHLNKY3vrrFbeqvcXHUwMDBly/qdjp62amwsY7acjGN7wYmlsVx1MDAxNkhJ6lZ7XHSyPaFcdTAwMTZ4vaZAXHUwMDA0sL0/U8yrXHUwMDAzaTFOQchjTajR9DGT9SAolDBcdTAwMGJ1mZlNXHUwMDE1Pn9gpbH8XHUwMDBipHstXHUwMDA3sjdResRYXFxcIrtMW7mFtZnTn69cdTAwMTZWXG7EOZWCziXpc65cdTAwMWaZNE7Np1x1MDAxMFx1MDAxOaJ/NNdLec56VU53m1x1MDAxN+T+tLuDyl37Jlx1MDAxM9dzXHTOu9aYm93GNVx1MDAwZvpcdTAwMDZfVI9BLVGqfMp/TfX52Fx1MDAwNzs71WOJMEGYx/r2lCVyPZFSY4Vl3lSPXHUwMDA1YmihiidXJ1elq9+B6uNcdTAwMWKaXHUwMDBm1fePyEGXXl899ne7t+X69n1xXFw/mmE+l2NLXHUwMDEz+EGMXHUwMDBigHBwPpfzz9KX2ITnNItm7GNMLXCV3HW+Zj83XHUwMDE1492vc/NSXHUwMDEwPMu2Ju72XHUwMDA1JiUuVq2nROdAzoHTj3JPWOqQ/ttDY696+DQ6vSld1fXhnuNcdMrAXHUwMDAwnKNcdTAwMTJcdFx1MDAwN+2SUyWSMq6xXHUwMDA3zp8lc0b2xePpceP+NNsqbMKQXHUwMDA1I59qwIcmXGZcdTAwMDdna1x1MDAxOZNcdTAwMTYxXHUwMDFmLITGQvnmXddcdTAwMTSXXHUwMDBmQJ5mXHUwMDAwXGJ4JdBcdTAwMGI6uuJcdTAwMWFOalx1MDAxY4HNZL9ZcIHB8cxcdTAwMWZcdTAwMWZcdTAwMGJcdTAwMTRQ/pF236XijmPj4S1CzGtcdTAwMWb8b1O4fX/39nSwSXZYr95cdTAwMWZsNp4q11uZTFx1MDAwZdPAqZhyZSbEmFxmTZd5JoeDfqI0xlx1MDAwNV9bXHUwMDFj9+i8XHUwMDE2p5nd4jCEuGaxXHUwMDA1x0RyelxiuEJunaJV2sAp1d4steLY4vZm7VxiTOWa1JhcdTAwMWahxEJEa4lcdTAwMDVHXCJobjhcdTAwMTNcdTAwMTYnUihJwZRIXHUwMDFjNTcmXHUwMDAygClCYJg45f5cdTAwMDLSXHUwMDEza8OFXHUwMDA1wlUprVx1MDAxNGFcdTAwMDKG/2+S0/krrE/CzjCzhPyIyc2E/ord/zClXHUwMDEwXCI4gTRQPzG7WVxuPE2OXHUwMDEwzzXmlzhSzadcdTAwMTBcdTAwMWSk3v1cIlZs5pifr//sj9e2/ylcdTAwMTPS9ndxXHJWdWBeQFx1MDAwMVlEXCJcbrSOtaJcZtxx7rvqsdIxrG4xSSgmmlxuxSk8YDTxN1x1MDAxMG5MalS6u1x1MDAxNGxcdTAwMTRcdTAwMDJ9IVx1MDAxOLwtplx1MDAxNdK+zeUnrZJcdTAwMTacM7tYXHUwMDBiZnZlIDw6cH9apDNdOadaSaa1xTW8V0UkyDNcdTAwMWRcbpUwalx0U41Lm9JcdTAwMThcIprEhCWxXGJ8ncFcdTAwMGIzwdBYOyktpFx1MDAxMCMwRDVDXHUwMDA0rUVaopls5WAmXHUwMDE1dKJcdTAwMTSxXHUwMDFiIZDkREBcdTAwMTPrgt5cdTAwMTFzrXD5LexkIXmsuqcjw/RnXHUwMDE4yvRAp88mIXB+JOdcdTAwMWFLxd2Yt/Zd9GWRXHUwMDE4dnPWKDyFJGzJVlx1MDAxMllg/1x1MDAwNFx1MDAxNlxmuleBdfAqKUxaxCyqXHJjXHUwMDBiU6RcdTAwMDLM6a+0keleRLqNxMiiREkzy4fNQuKgjVx1MDAwNIrFTJlcYo2igvqUyMRIcqBoMKCcaLM5LouxkcJMOIFnXHUwMDBiZGLWXHUwMDBmreeOkyxke3FcdTAwMGJcdL1cYnoovkxcZklZ/0BcYnS92eUgf1x1MDAwYkmxlnMliOQ7e5w0Ts2nXHUwMDEwXHUwMDFloj/DPKbvXHUwMDBiXHUwMDFl0pGIXHUwMDAyXHUwMDA2XHRcIsJs9MJwVLLhIFCRjlxmkUxcdTAwMTYyPVx1MDAwNlx1MDAxNlx1MDAxNrfUlFx1MDAxMOBm2t2toVx1MDAxN2mUsiSlQmDNiFAmc+xcdTAwMTfayPSISHqGXHJTlqliQyVcdTAwMTPIZFmEbKS2XGbpStBcdTAwMDKIsJhcdTAwMDUkpjZcblx1MDAxMyZmrTGIUlx1MDAxOTPjJrRcdTAwMDXf51LCu0QgRj1cdTAwMWOujWTQSHZyMJJm7kBHi2a5fZ3obFx1MDAxM2wqw8i5XG5mTDORjKtcdTAwMTVcdTAwMTCRieP042x4iP5cZiOZXlU9oNg4WCBcclwiXHUwMDFmK+1f5OVZSFPRXHUwMDA2g7Wi8GigkUVUseVrXCKRhaXZNp1QTpGkXGYpXHUwMDFhadSXp22KzkqGZMygXYaF/Ouzs75XOp1SXHUwMDFmRuPkeb9cdTAwMGZcdTAwMWR7tFx1MDAxObUg/6q7XHUwMDFmXHUwMDEzJ3bbZqyH7Vx1MDAwMu+fv/75fzRC8b8ifQ==120RELUCTANTGREEDY0123purchase_time (seconds)22201202220TYPICAL_SALEUNUSUAL_SALETYPICAL_SALEUNUSUAL_SALE \ No newline at end of file diff --git a/docs/anomaly-detection/assets/skip_past_last_row.excalidraw.svg b/docs/anomaly-detection/assets/skip_past_last_row.excalidraw.svg new file mode 100644 index 0000000..9b96ff5 --- /dev/null +++ b/docs/anomaly-detection/assets/skip_past_last_row.excalidraw.svg @@ -0,0 +1,5 @@ + + +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daVMqyVx1MDAxMv0+v8JwvrxcdTAwMTcx9NS+zDf3XHJXXFzvi1x0opFcdTAwMTZQNqFcdTAwMDF1Yv77y1x1MDAwMqX3toH2ijPijVx1MDAxYtpcdTAwMWLVXXnqnMzKrP7rl5WVVfe566z+sbLqPN3azUa1Z49WfzPbh06v3+i0YVx1MDAxN1x1MDAxOf/d71xmerfjI+uu2+3/8fvv3lx1MDAxOdZtpzU5y2k6Laft9uG4/8HfKyt/jf+HPY3q+NzHQVx1MDAwZr3s4Kqq7d1UflRcdTAwMWXu+/Xm+NTxQW+N6Tm3rt2uNVx1MDAxZG/XXHUwMDEzbMeE0OmGZ9igXHUwMDEwmv49alTd+nibhfxcdTAwMWY5PaLuNGp1XHUwMDE3XHUwMDBlkdrS/o+YXHUwMDFlMvnSP1a8y/bdXufB2eg0Oz3Tsl+xY368dlXs24darzNoV6fHuD273e/aPXhcdTAwMTDecXeNZrPkPo+vXHUwMDBlXHUwMDBmXHUwMDEzXHUwMDFl3GroO65eb1x1MDAwMIe2J51cdTAwMDVfWqu3nX4/0N5O175tuObhcG+jaWB3rzrulj+9JvXslrNn+qU9aDanm1x1MDAxYu2qY572qk1cdTAwMDJf1q6+ftlbn3pcdTAwMWRGX7f87TXdccyFMSOaXHUwMDEyJKSe7vFcZlx1MDAwYmMkwpuPOu2xlWEshdKESq/DXHUwMDFi/U0wL3d83Tu72Xe8PjCN21xum57f/Hwm2Dq42nnZudted9eP28/DjVx1MDAxZof7V8XpjVx1MDAwNszQdZ7c1emOv19/81x1MDAxZeCgW7Un7cGSXHUwMDEzzDSnlFxir/+ajfZD+Ok2O7dcdTAwMGbeLfzie2ohqKS3M9jGXHUwMDEwSjix8NiuNVOEXHUwMDExXHUwMDEyxFxmllx1MDAxNo+ghmBLXHUwMDE4tFxijCjHXFxcdTAwMTBcdTAwMWWFXHLjS4VcdTAwMTMyXHUwMDA3TvCH4ITG4yRw9CsgoEOo4kLiODwwXCKT8ECoppgojeeBQ55cdTAwMTbrXHUwMDE5oDE8uHvfIHHXabulxst4RFx1MDAxMIGt23ar0TRcdTAwMGZbXHUwMDA0zl9rNmrmzldvoalOb9V/925cdTAwMDPoZXpAq1Gt+ungXHUwMDE2Lmo32k5vL1x1MDAwYq90eo1ao203z6Nttlx1MDAwN27nzOlPWu32XHUwMDA2jv9ZOLtvto8twlPAOrzcPb25cfsvhYH7fGq/jLZGz3dRsNq9XmdcdTAwMTRCK2Ke/ZknpEWU07wtUzBcdTAwMTLlbfyqaIxjLYxcdTAwMTaG41FW2lwiibSlXHUwMDAxpoIyQf1cdTAwMTb5XG5T34NcdTAwMGaTXHUwMDE2Z0JKzOcjrWSUcqWI5tpvmVlR2u00wpzo/bbi9cD4j+nvf/72/tFcdTAwMDWwQO+EXGI3Nu2+u9FptVx1MDAxYS7cyYlpRWREdO2eu1x1MDAwZd3WaNfC+5x2NWHP+Kw1g6O6Y0eMXHUwMDAwzvPvXHUwMDAzo26EdKTTrHRGmVxiuKTv9O51eUTY8dbZ6cN5q0VeWvlhWpFcdTAwMThUf2M6XHUwMDAx08dcdTAwMGJjWmCpsNBcIkKxpi94eONcdTAwMTTSkkhOlfZcdNjFIY21ZFxuK+xTXHUwMDFln1x1MDAwNWmfXHUwMDE1ho7/XHUwMDA3XCJa71x1MDAwZtujh7XRYfWxsN/UxL4+vD/OXCKpXHUwMDE1ppbyfTxjeFx1MDAwZSH5XHLdoOUs4v94lODp6eVCe6LfWe2YXHUwMDFl/6lwP8uuqDGjXGb8RFx1MDAxZUfVTCdyNedcbmGlRJ64zkdQX5S2zlxunMbLalx1MDAxMti6sKx2O90kTVx1MDAxZGhyWEBH2lx1MDAxOJDRwfuaRUdcdTAwMWbUu05jg/ygj5t6Z3BQrHU6pWpcdTAwMTSh5pI5yejlwuBcdTAwMTIx7uXiKppyhjDhcYxLk1x1MDAxOVx1MDAxN2suMXjIeYpoYFxcwlx1MDAxMTSI+83y80U0WXrCXHLs63aaz7Vx/71HteLuaK+3Uy5Wj7pb7v2WWL/ZXHUwMDFmnGaKXiFcdTAwMTJcdTAwMDKy8lxm4VxyyCxcdTAwMDbJ9OtHpz5cYslX2cmUYFxmbp3wK1x1MDAxY1x1MDAwZrOahDdOMcskoFYz7UE9J8xi6Vx1MDAwZlx1MDAxMs/BpiiWR4laXHUwMDBlXHUwMDFlRbkz6IVzXFytyLq9d2xvXHUwMDE3XHUwMDFhe0e9a3zYzcSg/ojSN4PmgLvrxVx1MDAxOVRhTbGUKFbcJlx1MDAwN4sxpuBxfFx1MDAwMFx1MDAxY78p1HfWXHUwMDA3U+h5pdlcdTAwMTjW9yuyuFs5L15XR1x1MDAwN8Oim4lCifim0HyhfDNcdTAwMDOFalx1MDAwNi4pQXGqXHUwMDE3Y5aIWTiLKOHrgmXxR/FSMyjOnUHL5aNcdTAwMGKX4CPHhr7ZXHUwMDFk3Fx1MDAxNWpu6UcmXHUwMDA25WHp+s2gr1vnhN2PxVx1MDAxOVRzwbXyj4BcdTAwMWVcdTAwMWN5slx1MDAxM4qQklx1MDAxYSH+zaBfmEFPXHUwMDA2/NF2hie2utlrdzvqsdhcdTAwMTKZ4r2Yo29cdTAwMDbNXHUwMDE3yvZcZlx1MDAxMV1Qr5pcIkTiMIujW725XHUwMDFhzqlcdTAwMTCI5lx02o9MklhcdTAwMTZcdTAwMGVNSoeYn0NcdTAwMDV+7Fx1MDAxNXfKtdNcdTAwMGWnSJxcXO2vY1x1MDAxNDN3XHUwMDFhw6EyrF2/OfR165zAq+TghXIhXGJjsYFcXFx1MDAxMclk8rIhXGKWREn5zaFfmEPL7fJzV7f3yoNcdTAwMGKn/3y5dlTU5DlcdTAwMTOHyvCMzDeHLlxi5dtcdTAwMTk4VEhcdTAwMGU/SMe6oTQl71bIpUwzjJ9cdTAwMGZdXHUwMDE2XHUwMDA2zX8m9Fx1MDAxOVx1MDAxZJGHJno56F4+XHUwMDBl91x1MDAwZp9InVxmrjLhLjxcdTAwMTOKkS/bdJp9hEM58FFcdTAwMWN+pyckXHUwMDAy8W5cdTAwMDYgUiGVSVCIXHUwMDAzXCLBKrz1XHKIXG5xXHUwMDBlSjbXXHUwMDEw7mKphG9I7Fx1MDAwZXq3dbvvlN1Gy1n5T99cdTAwMDGcVPv/Xep8hXfbnJfu7bVetnr39YvdXXuTbZfcs+tcdTAwMDKLojahvkVcbmIxRVx1MDAwNSZEK1BdnnGM89SpsjRTklKkXHUwMDA1o75psWnSXHUwMDExjuNTQbCFOCZCaES50F5cYnKpYT0jvS6M6fos5Ko4YvGphCR5llQqgkC65lxyaYlcdTAwMDT8MsskaZrcXHUwMDFiPXdxs7DT4vXuXnH/QtvdwUFmXHUwMDAzXHUwMDA2qiGW0FJSyZjAyPeETFx1MDAxZslp4ZVCXGJcdTAwMGXQXHUwMDExXHUwMDBiXHUwMDE2XFxakjJcdTAwMDT+XHUwMDA2uP+Y4Kg5Y/JW4IW1XHUwMDA2h1x1MDAxZv8z7XnxqpSHXHUwMDE5XGZaUVx1MDAwZYbpr7byk1SyWoSuYlSQXFxdPDBpXHUwMDAxzib2ccecJlx1MDAxZFx1MDAxZmbwXHUwMDE33ETlsTeHSkA8a79cdCff2lx1MDAwNDtP5U2+21fNzaIz2tpcdTAwMWW2ZLfWulhcdTAwMTJcdTAwMWJ6g1x1MDAwNrUww1xcXHUwMDEzwSk3Wc1eh098Nsm86knm043PXHUwMDAxzTiGr0xE2eT02VE25TFcdTAwMTRDY77shDerRVx1MDAwNIZUwnzWXHUwMDE3XHUwMDBmlVksXHUwMDBmc0VcdTAwMTidwfJ8T2FcdTAwMWVcdTAwMWY+Y2xgTi//o8JcdTAwMWTs3cPFzMdcdTAwMDfLdvFvaTvfu5rPgsZ/hpxcdTAwMWRNZ2t+IXJcdTAwMDEx21x1MDAwNajFOZFjX1x1MDAwMLDDXHUwMDExdlxumM/0xNPiS96Y76t5S+CHRCrIXHUwMDFlXHUwMDEyqlx1MDAwZWxqO4XOS1x0XHUwMDFmXHUwMDE2XHUwMDFk+qCHO5tcdTAwMTfZNVx1MDAwMlPI4pNuxMY18sI9ryXdb32tQFx1MDAwN0uP3b3CmVBfRMeKcIG3d8g/SVwiLO7IVlDmWrnkXHUwMDEyb4mkqdXGcWpYR9Mgplx1MDAwZS6MtohcdTAwMGI1V8LDtHkxXHUwMDA13rX148bm1tpG5Vx1MDAwNK/vXHUwMDBmz6uHZ3ToPdWAiYZcbrx/S7vs5dBcdTAwMTl1WY0+VstXZ92N+16zOFx1MDAxOMVfdlJcdTAwMTLmXTdcdTAwMDLYXHUwMDBmLVx1MDAxY0+//9RZT4wnhePgUlxipFhQvY9cdTAwMGLHU5H5Ly5cIs9cdTAwMDOLXHQ1brGCnTOpqNYkksGwOl5XISWxQVHoO4GXL8K7XHUwMDE0heTvcNuHXHUwMDE3kjfk0eDYLnW3r/Vavdg/vb3YXHUwMDE52VHwxkycMqYtolx1MDAwMMJcdTAwMDJcdTAwMTFcdTAwMGUkXHUwMDFiwK7m2lIgPiTGXG4xoqLYxf6TZWBRXHUwMDE038xM6CpfxPWu2v36LFx1MDAxMeKFfe9cbpqlgE1IIbiQNG5NXGKZXHUwMDFjIYYxQFx1MDAwMfBQ3s73XHUwMDAyUM7V4Sgkm+R4d9RcdTAwMWFTJPJXnoJFVFTrw3NOXHUwMDBljm+OXHUwMDBlborlXHUwMDFm1fJZdExwms1Gt1x1MDAxZlx1MDAxZVx1MDAxNiiVluBaUkSExIiFXFx6UNtcdTAwMTOFLIG5KYlOXHUwMDE0SWFcdIVMIVx1MDAxN8WEI+pB3lPbnokuwzDAQts/LVx1MDAwMldBsyQ9maI3JHnsSklcXCTnXHJcdTAwMDOjg9JiPMeFkmIlos8g9Vx1MDAxZCN17JbuTy43t0svrfqIunpcdTAwMTl0tFx1MDAxMuCNzFx1MDAxNL1JgV38XWbR0ZQzXHUwMDE4ujQlmkhKlFxiebigo1x1MDAxOSGKcqKp5L5cdTAwMDDjNCuCWsS4x4QgiVxm7JZeRi9cdTAwMTP1JiQ8xYNcdTAwMGXoU7OYdZdMU2jiTFx1MDAwZeOcXHS/b5tcdTAwMGLxKmhccvwspKHjy91+soh+h7BcIlwiOqlcbm5uXHUwMDE1fdm2R2rwdENcdTAwMDdcdTAwMGbD46Y+2di8IDvZSmeAXHUwMDEwpVx1MDAxOYU5MsvWkaCMXHUwMDE2mFpcdTAwMTiZxYHAQabaR3xv0NXKYkwxcFx1MDAwNynDUouY4NSS5TctXHUwMDEzdGdJcGJKUaExi1PNMdlcdTAwMTa+8nJcdTAwMDSuXHUwMDEyyzSvM1PcaVut3Zyc8k2y2zlD+PDooHtw1M5GbKmEiZRgT6j0dLe7vrn36IjD+lx1MDAwZd/MK/BEzFNcXMhtP1xcO9/YXerkrFBcdTAwMGLzXHUwMDE5YlxuL0hfXFyc1deO11x1MDAxYYVcdTAwMWRnQ1x1MDAxZlx1MDAwZk9l9iA4RtSiXGZcdTAwMTGJsFx1MDAwNudPRdY1fZtpg1x1MDAxMYnw6HqN36uc5jfqVONHnVx1MDAxOWLgXHUwMDA2StCjsVXzXHUwMDE48+RcdTAwMTRpRlx0XHUwMDAz50rPtVxuTdqg8Vgg++ii1KGFfXJevj+9vd3C+5mC4O+LbFx1MDAwZS6HXnhyPks705hcdTAwMWFTYoFbarxaYFxcf8hzXCKygYhTXHUwMDEx9C9cdTAwMGVW54BcdTAwMTknO1NcdTAwMTNBsFx1MDAwMK1cdTAwMTS/5KlMXlRcdTAwMDKDrVx1MDAxMZn7aoqz229UZi+Dyn6HgT48VJ3uyKeuj1xibq+lx7xFKZHEl5I4Tlx1MDAxM9P8naBcdTAwMTRm0yNMhJDGra5GlUU1k5IyiYWAf8uF5WWaXHUwMDA0niGdmTFBlNbRtSfMPlx1MDAxMuE/b9pJXCKCac5I1lxcUDpcdTAwMWaSP3Bd1KjhvXd+sjl/zFx1MDAwNbXFwangnCNcdTAwMDE8KYj0rlx1MDAxN2H/mVx1MDAwM+c+2fT6NoAsQYHxmHk7MD1SMNKWXGKkXGKh4IFcdTAwMGJcdTAwMDKetpdcbm3M3O5cdTAwMWFcdTAwMTKzJlx1MDAxOSRcdTAwMTjuwKyTwv2Xajx5jVxy9Fx1MDAxNIhcdTAwMDIl4HJKYUH961x1MDAxY41cdTAwMWZcZnyzML67MnaFtUZUR1x1MDAxZY0n/lx1MDAwMrNcdTAwMDJJt50+oVx1MDAxOLlthrSginNmiErjyF1j+tp3iiiTS+zV5L172zCsYoUkXHUwMDAx/Fx1MDAwMqtG7pvwhFxmq6jozW3Bzlx1MDAwMPeMW19yau/p29dxi/jvYNKm0F2HgJmOpFx1MDAwMJYmt1x1MDAxMblcXDqs3r1+isZcdTAwMWU3f69f6jq3XHK7XHUwMDE5XHUwMDE5raAxkZ1potq9Q1x1MDAwN09bP1x1MDAxZUll96DcXHUwMDFk/lDP+09cdTAwMTlFNbVcdTAwMTAhSFx1MDAxOPvSQobyt5GwXHUwMDEwl1JcdTAwMTOsXHUwMDE4XHUwMDFjXHUwMDE0XHJ/XHUwMDAxO1x1MDAxOCvGVFx1MDAxMIqYUDJGVH+V+NcnXHUwMDEwcW2G+Fx1MDAxN1KUXHUwMDAz/GPXmaE4UVWDM6RcdTAwMTljc5b4JUBy0fhXXHUwMDA0XHUwMDBmXHUwMDExzVx1MDAwZcNcdTAwMTdeLDS+tn2+dbYyjlx1MDAwNa2UXHUwMDBl9k5WTtZK5ytF89/Z8VWsoE9cbmCpwMU/PoCVten56Pr0fkzV9Vx1MDAwNGtLMeBrypCEXHUwMDExXCLok0tELUUpXHUwMDEyWjBcdTAwMDKWXHUwMDFiLWBcIspcIszM+FOzyDLmvlxuqOnwwZElXHQlTHApxvXl36NJwmjSyFx1MDAxYddKrvznXFxcdTAwMTJcdTAwMTRYlco3yLDElTiEmbhcdTAwMDTXPu/FrIgpUvn8RdOTzdR8XG5cdTAwMTFcdTAwMGL9XHUwMDE58jqd84M6U5hkXHUwMDE5UJpcdTAwMWNcdTAwMTHOKPIr54nKNGuXQ/fBXqOSsZZcdTAwMTFcdTAwMDPJpH3Tp+GCbVx1MDAwMmlcbmpeXGLzuijwS2ikTTxqpPkuXHUwMDFjnyhEo93hXHUwMDE3Y+EvXHLvS1x1MDAxOWmHNzvyQSj3qdO7PD1cdTAwMTghfT94jFmfKGlcdTAwMGVBYKPrJUdggojh0LvRpJ5W63AhkI5cdTAwMTbbfSfS5zjY3i+eSI85RqY3Y2dcdTAwMTFcdTAwMTBJXHUwMDBllI5dZT3fWmVpik7UitubjFx1MDAxZt1cZnHLbjxW21x1MDAwNzV1XHUwMDE1r+hmflVcdTAwMWGhVOhZXHUwMDA0XVxujtLbmebvgLdvYVx1MDAwMyFcdTAwMDBcYnjVLCZTJ1x1MDAxNUL/4kmEPEAzQ4kqh1x1MDAwN4xcdTAwMDJcdTAwMGWlf00wkjzFRkFtytzXUZjdfiNcdTAwMGVJwlx1MDAxYlx1MDAxZX7uJMI7XHUwMDE0lHWdk7mdjfTkhvRJXHUwMDA0pCypXHUwMDE01kJxLrhcbmJXUGRcdTAwMTHNhcZYUsB2XHUwMDA0ulx1MDAxOFRcdTAwMWNXXHUwMDEyzlx1MDAwNVdEXHUwMDAxumMyW7/KXG4oPz9Tp7mwb8HBXHUwMDA3XHUwMDE00DlxbKeS03dMzEMwyvKuX1x1MDAwMThcdTAwMGJfTtBnuVx1MDAxNslWXHUwMDE5Oj3CsLl5XHUwMDEys6h2hE2mXHUwMDFjOGVcblx1MDAxZVwiSFx1MDAxNOGLyr7qdm1xQbU2+l4hrlXEXHUwMDFl8n4zVLkzcIP1MdnfXHUwMDEwdXfc4y9cdTAwMWR7t3K/1r86um6VesdcdTAwMTcx5Tdxw1x1MDAxMVbQc1x1MDAwNFx1MDAxYz5qXHUwMDE2nFU6PFx1MDAxY8FAXGJesTJcdTAwMGJUgsyLKcBh1KJcbuyQU6ZMbD86XHUwMDFjgVahQlx0RaUkoP7B/3Rcbr7jvseo4Fx1MDAxONVaeIzC2swxRqvpjFknKlx1MDAwZSPGOUjCvN8sRUHj489cdTAwMGZ+XHUwMDE0XHUwMDEyXHJ1sjfWRvNcdTAwMWO3PvS1cotcZlx1MDAxZbLi9jbKmLdOd+wutlx1MDAxZiussr+R3Z1nQlpcdTAwMTSEJTeRboHC3jxcdTAwMDZXXHUwMDA0U1x1MDAwNqxJiX9cdTAwMDG/qStCY/z3r+Kwzzo65OF8tLM7XHUwMDFmTFx1MDAxMVx1MDAxZJBcdTAwMDf+9L5Ev1xcm7VK1ZxlXHUwMDAyU8TE+OXssVxcLq5vbp9tte0zfFHdub68TEjum6NcdTAwMTRdXG7hc1lcdTAwMTdyzNNcdTAwMWKaSqdCXHUwMDEyi4BcdTAwMDBiWILNXHUwMDBiXHUwMDExSlx1MDAxMVx1MDAwMrKViMPTXHUwMDA1+S+VjsIhZlx1MDAwNVx1MDAxOegqXHUwMDBigUzR3KSA+WKM39iY9JmHjc7i1ImFZlx1MDAxMnRhXFzaX/JcdTAwMWFpXFwy83Z0Pc/MQeBmXCJ2LbRcdTAwMTTq86taQ+vKhE0yT6JMXHUwMDEy+Ok85Vx1MDAxM/jIkmZAMMs4XHUwMDBiyX1J5iveRFx1MDAwMbgqknPEXHUwMDA1I1xmXHUwMDFjgIXkfVxcjD6lgjY7M3dcXHXlXGZ2u7w7uLnu1ffPT29K19FxKG5JXGapLWVcbixcdTAwMTFcdTAwMTM8MlxmmfChUMxoM1x1MDAxMPWcxWQqKmZcdTAwMTnPx1x1MDAxNPwlXHUwMDA1XGK/SkLEZ1xmRN3sJE2Qplx1MDAxYytcdTAwMTH/qo+UiFx1MDAwMiWEKUHm0evvXGY5KFx1MDAxMJaaI0Z4fnOyt7FWLJfWiltLnaJcdTAwMTDf0HxihP3N2tVcdTAwMDNcdTAwMTD9buHw4OZu5JZcdTAwMWa4v57n3TUpjWNuPkxgs7ZbsFx1MDAwMl7qt9WoTM6CjqlcdTAwMTOgIESoXCKCUlxm1/CvYv6Pl9k5OOGPM6Q0aYUojc8txip5XHUwMDExSjNnRs1yhvPo7Fx1MDAxNCUshFx1MDAxNr71XHUwMDBlsirhX15Je9XudksuXFxySsWrw4YzWo92969344+R6WNcdTAwMDSYrnbGeuvvX/7+Pz9cdTAwMDUhXHUwMDE2In0=2USER-530123purchase_time (seconds)220MATCH2AFTER MATCH SKIP PAST LAST ROW3TYPICAL_SALE \ No newline at end of file diff --git a/docs/anomaly-detection/index.md b/docs/anomaly-detection/index.md index 43344c9..3e4629e 100644 --- a/docs/anomaly-detection/index.md +++ b/docs/anomaly-detection/index.md @@ -236,7 +236,7 @@ MATCH_RECOGNIZE ( ); ``` -> Note: [The `ORDER BY` clause is required](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#order-of-events). With streaming, this ensures the output of the query will be correct even if some events arrive late. +[The `ORDER BY` clause is required](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#order-of-events), it allows us to search for patterns based on [different notions of time](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/concepts/time_attributes/). With streaming, this ensures the output of the query will be correct, even if some sales arrive late. We `DEFINE` a single `SALE` ["pattern variable"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#defining-a-pattern) for our condition, then include it in our pattern. We then output both the quantity and timestamp of the unusual sale. @@ -294,11 +294,27 @@ MATCH_RECOGNIZE ( This query might look intimidating at first, but becomes easier to understand once we break it down. -`PARTITION BY user_id` is similar to `GROUP BY user_id` in the original query, it lets us calculate `AVG()` values and find matches for each user separately. Unlike with a global user average, we won't receive false positives because of users who order in large quantities often. +--- + +#### `ORDER BY purchase_time` + +Like previously mentioned, this clause allows us to look for pattens based on time. In our case, the purchase time of the sale. + +--- + +#### `PARTITION BY user_id` + +This is similar to `GROUP BY user_id` in the original query, it lets us calculate `AVG()` values and find matches for each user separately. + +Unlike with a global user average, we won't receive false positives because of a minority of users who order in large quantities often. ![](assets/useful_match.excalidraw.svg) -We use two "pattern variables" in `PATTERN (TYPICAL_SALE+? UNUSUAL_SALE)`: +--- + +#### `PATTERN (TYPICAL_SALE+? UNUSUAL_SALE)` + +We use two "pattern variables" in our pattern: - `TYPICAL_SALE`: We use this to match all the sales made before an "unusual" sale. - By not specifying a condition for this variable in `DEFINE`, the [default condition](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#define--measures) is used, which evaluates to `true` for every row/sale. @@ -317,6 +333,50 @@ We use two "pattern variables" in `PATTERN (TYPICAL_SALE+? UNUSUAL_SALE)`: - This prevents "unusual" sales from skewing our `AVG()` and creating false positives. -Unlike in our original query, we can easily use `FIRST()` and `LAST()` to output the timestamps for the first and last sales that we use to calculate our `AVG()` respectively. This information lets us know exactly which previous sales were used to determine an "unusual" sale. +![](assets/reluctant_quantifier.excalidraw.svg) + +We append `WITHIN INTERVAL '10' SECOND` after the pattern to set a ["time constraint"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#time-constraint). + +> Note: We use an `INTERVAL` of `10 SECOND`s for quick user feedback in this tutorial. In a real situation, you would probably use something like `WITHIN INTERVAL '1' HOUR`. + +This provides several benefits: + +- Only the sales made within the last `10 SECOND`s are used. + - Memory use becomes more efficient, since we can prune sales older than this. + +- Our `AVG()` calculation changes from a typical arithmetic mean to a [simple moving average](https://en.wikipedia.org/wiki/Moving_average). + - There are benefits and downsides to both approaches. + + - In our case, sales are frequent, so only using recent sales can be beneficial. + +![](assets/moving_average.excalidraw.svg) + +--- + +#### `FIRST(TYPICAL_SALE.purchase_time) AS avg_first_sale_tstamp`, `LAST(...)` + +We use `FIRST()` and `LAST()` to output timestamps for the first and last sales that were used to calculate our `AVG()`. This information lets us know exactly which previous sales were used to determine an "unusual" sale. ![](assets/avg_range.excalidraw.svg) + +--- + +#### `ONE ROW PER MATCH` + +Currently, this is the only supported ["output mode"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#output-mode). + +As the name suggests, it indicates to only output one row when a match is found. + +Once released, `ALL ROWS PER MATCH` will be able to output multiple rows instead. + +--- + +#### `AFTER MATCH SKIP PAST LAST ROW` + +This is pretty self-explanatory, we skip past the last row/sale of a match before looking for the next match. + +Other ["After Match Strategies"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#after-match-strategy) are available for skipping to different rows and pattern variable values inside the current match. However, they aren't particularly useful in our scenario. + +Our strategy skips past the "unusual" sale of the current match. This prevents the "unusual" sale from being wrongly used as the first "typical" sale of the next match and skewing the `AVG()`. + +![](assets/skip_past_last_row.excalidraw.svg) From ee48a9876b4d6aa42a06e2fbf44eebdb4354af6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Thu, 10 Jul 2025 16:18:12 +0100 Subject: [PATCH 09/15] Add to "Persisting back to Kafka" section --- docs/anomaly-detection/index.md | 85 +++++++++++++++++++ .../standalone-etl-anomaly-deployment.yaml | 80 +++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 tutorials/anomaly-detection/standalone-etl-anomaly-deployment.yaml diff --git a/docs/anomaly-detection/index.md b/docs/anomaly-detection/index.md index 3e4629e..579502e 100644 --- a/docs/anomaly-detection/index.md +++ b/docs/anomaly-detection/index.md @@ -380,3 +380,88 @@ Other ["After Match Strategies"](https://nightlies.apache.org/flink/flink-docs-r Our strategy skips past the "unusual" sale of the current match. This prevents the "unusual" sale from being wrongly used as the first "typical" sale of the next match and skewing the `AVG()`. ![](assets/skip_past_last_row.excalidraw.svg) + +## Persisting back to Kafka + +Just like in the [Interactive ETL tutorial](../interactive-etl/index.md), we can create a new table to persist the output of our query back to Kafka (look at that tutorial for an explanation of the steps below). This way, we don't have to run the query every time we want to find "unusual" sales. + +First, let's define the table, and specify `csv` as the format so we don't have to provide a schema: + +```sql +CREATE TABLE UnusualSalesRecordTable ( + user_id STRING, + unusual_invoice_id STRING, + unusual_quantity INT, + unusual_tstamp TIMESTAMP(3), + avg_quantity INT, + avg_first_sale_tstamp TIMESTAMP(3), + avg_last_sale_tstamp TIMESTAMP(3), + PRIMARY KEY (`user_id`) NOT ENFORCED +) WITH ( + 'connector' = 'upsert-kafka', + 'topic' = 'flink.unusual.sales.records.interactive', + 'properties.bootstrap.servers' = 'my-cluster-kafka-bootstrap.flink.svc:9092', + 'properties.client.id' = 'sql-cleaning-client', + 'properties.transaction.timeout.ms' = '800000', + 'key.format' = 'csv', + 'value.format' = 'csv', + 'value.fields-include' = 'ALL' +); +``` + +Next, let's insert the results of our "unusual" sales pattern matching query into it: + +```sql +INSERT INTO UnusualSalesRecordTable +SELECT * +FROM SalesRecordTable +MATCH_RECOGNIZE ( + PARTITION BY user_id + ORDER BY purchase_time + MEASURES + UNUSUAL_SALE.invoice_id AS unusual_invoice_id, + CAST(UNUSUAL_SALE.quantity AS INT) AS unusual_quantity, + UNUSUAL_SALE.purchase_time AS unusual_tstamp, + AVG(CAST(TYPICAL_SALE.quantity AS INT)) AS avg_quantity, + FIRST(TYPICAL_SALE.purchase_time) AS avg_first_sale_tstamp, + LAST(TYPICAL_SALE.purchase_time) AS avg_last_sale_tstamp + ONE ROW PER MATCH + AFTER MATCH SKIP PAST LAST ROW + PATTERN (TYPICAL_SALE+? UNUSUAL_SALE) WITHIN INTERVAL '10' SECOND + DEFINE + UNUSUAL_SALE AS + UNUSUAL_SALE.quantity > AVG(CAST(TYPICAL_SALE.quantity AS INT)) * 3 +); +``` + +Finally, we can verify the data is being written to the new topic by running the following command in a new terminal: + +```shell +$ kubectl exec -it my-cluster-dual-role-0 -n flink -- /bin/bash \ + ./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \ + --topic flink.unusual.sales.records.interactive + +user-67,7850595442358871117,30,"2025-07-10 14:07:02.697",1,"2025-07-10 14:06:54.566","2025-07-10 14:07:01.679" +user-77,787429984061010435,10,"2025-07-10 14:07:04.729",1,"2025-07-10 14:06:58.641","2025-07-10 14:07:01.672" +user-98,3476938040725302112,20,"2025-07-10 14:07:05.751",1,"2025-07-10 14:06:56.594","2025-07-10 14:07:05.749" +``` + +## Converting to a stand alone Flink job + +The ETL query (deployed above) will have to compete for resources with other queries running in the same Flink session cluster. + +Instead, like in the [Interactive ETL example](../interactive-etl/index.md), we can use a `FlinkDeployment` CR for deploying our queries as a stand-alone Flink Job. + +There is an example `FlinkDeployment` CR (`standalone-etl-anomaly-deployment.yaml`) that we can use: + +```shell +kubectl apply -n flink -f anomaly-detection/standalone-etl-anomaly-deployment.yaml +``` + +Finally, we can verify that data is being written to the new topic: + +```shell +kubectl exec -it my-cluster-dual-role-0 -n flink -- /bin/bash \ + ./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \ + --topic flink.unusual.sales.records +``` diff --git a/tutorials/anomaly-detection/standalone-etl-anomaly-deployment.yaml b/tutorials/anomaly-detection/standalone-etl-anomaly-deployment.yaml new file mode 100644 index 0000000..afbe16d --- /dev/null +++ b/tutorials/anomaly-detection/standalone-etl-anomaly-deployment.yaml @@ -0,0 +1,80 @@ +apiVersion: flink.apache.org/v1beta1 +kind: FlinkDeployment +metadata: + name: standalone-etl-anomaly +spec: + image: quay.io/streamshub/flink-sql-runner:0.2.0 + flinkVersion: v2_0 + flinkConfiguration: + taskmanager.numberOfTaskSlots: "1" + serviceAccount: flink + jobManager: + resource: + memory: "2048m" + cpu: 1 + taskManager: + resource: + memory: "2048m" + cpu: 1 + job: + jarURI: local:///opt/streamshub/flink-sql-runner.jar + args: [" + CREATE TABLE SalesRecordTable ( + invoice_id STRING, + user_id STRING, + product_id STRING, + quantity STRING, + unit_cost STRING, + `purchase_time` TIMESTAMP(3) METADATA FROM 'timestamp', + WATERMARK FOR purchase_time AS purchase_time - INTERVAL '1' SECOND + ) WITH ( + 'connector' = 'kafka', + 'topic' = 'flink.sales.records', + 'properties.bootstrap.servers' = 'my-cluster-kafka-bootstrap.flink.svc:9092', + 'properties.group.id' = 'sales-record-group', + 'value.format' = 'avro-confluent', + 'value.avro-confluent.url' = 'http://apicurio-registry-service.flink.svc:8080/apis/ccompat/v6', + 'scan.startup.mode' = 'latest-offset' + ); + CREATE TABLE UnusualSalesRecordTable ( + user_id STRING, + unusual_invoice_id STRING, + unusual_quantity INT, + unusual_tstamp TIMESTAMP(3), + avg_quantity INT, + avg_first_sale_tstamp TIMESTAMP(3), + avg_last_sale_tstamp TIMESTAMP(3), + PRIMARY KEY (`user_id`) NOT ENFORCED + ) WITH ( + 'connector' = 'upsert-kafka', + 'topic' = 'flink.unusual.sales.records', + 'properties.bootstrap.servers' = 'my-cluster-kafka-bootstrap.flink.svc:9092', + 'properties.client.id' = 'sql-cleaning-client', + 'properties.transaction.timeout.ms' = '800000', + 'key.format' = 'csv', + 'value.format' = 'csv', + 'value.fields-include' = 'ALL' + ); + INSERT INTO UnusualSalesRecordTable + SELECT * + FROM SalesRecordTable + MATCH_RECOGNIZE ( + PARTITION BY user_id + ORDER BY purchase_time + MEASURES + UNUSUAL_SALE.invoice_id AS unusual_invoice_id, + CAST(UNUSUAL_SALE.quantity AS INT) AS unusual_quantity, + UNUSUAL_SALE.purchase_time AS unusual_tstamp, + AVG(CAST(TYPICAL_SALE.quantity AS INT)) AS avg_quantity, + FIRST(TYPICAL_SALE.purchase_time) AS avg_first_sale_tstamp, + LAST(TYPICAL_SALE.purchase_time) AS avg_last_sale_tstamp + ONE ROW PER MATCH + AFTER MATCH SKIP PAST LAST ROW + PATTERN (TYPICAL_SALE+? UNUSUAL_SALE) WITHIN INTERVAL '10' SECOND + DEFINE + UNUSUAL_SALE AS + UNUSUAL_SALE.quantity > AVG(CAST(TYPICAL_SALE.quantity AS INT)) * 3 + ); + "] + parallelism: 1 + upgradeMode: stateless From fc8176b107561b0d7a1412e290dd7aef3f0c60af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Thu, 10 Jul 2025 16:20:19 +0100 Subject: [PATCH 10/15] Minor changes for clarity --- docs/anomaly-detection/index.md | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/anomaly-detection/index.md b/docs/anomaly-detection/index.md index 579502e..93e8263 100644 --- a/docs/anomaly-detection/index.md +++ b/docs/anomaly-detection/index.md @@ -75,11 +75,6 @@ user-9467&63188787247555258843 ... ``` -| user_id | product_id | invoice_id | quantity | unit_cost | -|---------|------------|---------------------|----------|-----------| -| user-11 | 188 | 3029976743068934888 | 2 | £838 | -| user-94 | 67 | 6318878724755525884 | 3 | £971 | - ### High Sale Quantities We want to detect if a user ordered a higher quantity than they usually do. This could be a sign that they made a mistake or their account has been hijacked, which could cause troubles for all parties involved if not dealt with promptly. @@ -209,7 +204,7 @@ While useful, the query above has several flaws: - Limiting the `AVG()` to sales made, for example, in the past week, might be more useful. - The query would become much more complex if, for example, we only wanted to return a match if two "unusual" sales occurred one after another. - - We would likely have to use some elaborate combination of [`WITH_TIES`](https://learn.microsoft.com/en-us/sql/t-sql/queries/top-transact-sql?view=sql-server-ver17#with-ties), [`OVER`](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql?view=sql-server-ver17), and [`PARTITION BY`](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql?view=sql-server-ver17). Assuming they're even supported. + - In a typical database, we would likely have to use some combination of [`WITH_TIES`](https://learn.microsoft.com/en-us/sql/t-sql/queries/top-transact-sql?view=sql-server-ver17#with-ties), [`OVER`](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql?view=sql-server-ver17), and [`PARTITION BY`](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql?view=sql-server-ver17). Assuming those are even supported. `MATCH_RECOGNIZE` lets us easily and concisely solve these problems. @@ -236,9 +231,18 @@ MATCH_RECOGNIZE ( ); ``` -[The `ORDER BY` clause is required](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#order-of-events), it allows us to search for patterns based on [different notions of time](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/concepts/time_attributes/). With streaming, this ensures the output of the query will be correct, even if some sales arrive late. +[The `ORDER BY` clause is required](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#order-of-events), it allows us to search for patterns based on [different notions of time](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/concepts/time_attributes/). + +- We pass it the `purchase_time` field from our `SalesRecordTable`, which contains [copies of the timestamp embedded in our source Kafka `ConsumerRecord`s, as the event time](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/connectors/datastream/kafka/#event-time-and-watermarks). + +- With streaming, this ensures the output of the query will be correct, even if some sales arrive late. + +The `DEFINE` and `MEASURES` clauses are similar to the `WHERE` and `SELECT` SQL clauses respectively. + +- We `DEFINE` a single `SALE` ["pattern variable"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#defining-a-pattern) with our condition, then include it in our pattern. + - > Note: The value of `SALE` is the row/sale which matches our `DEFINE`d condition. -We `DEFINE` a single `SALE` ["pattern variable"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#defining-a-pattern) for our condition, then include it in our pattern. We then output both the quantity and timestamp of the unusual sale. +- In `MEASURES`, we use the value of `SALE` to output both the quantity and timestamp of the "unusual" sale. ### Pattern navigation @@ -265,7 +269,9 @@ MATCH_RECOGNIZE ( ![](assets/first_last.excalidraw.svg) -Notice how the `SALE` pattern variable doesn't simply hold one value, it maps to multiple rows/sales/events. We're able to pass it to the `FIRST()` and `LAST()` functions to output the quantities from both the first and second matching sale respectively. These functions are specifically referred to as ["offset functions"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#logical-offsets), since we use "logical offsets" to navigate the events mapped to a particular pattern variable. +Notice how the `SALE` pattern variable doesn't simply hold one value, it maps to multiple rows/sales/events. We're able to pass it to the `FIRST()` and `LAST()` functions to output the quantities from both the first and second matching sale respectively. + +These functions are specifically referred to as ["offset functions"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#logical-offsets), since we use "logical offsets" to navigate the events mapped to a particular pattern variable. ### Useful pattern @@ -278,7 +284,8 @@ MATCH_RECOGNIZE ( PARTITION BY user_id ORDER BY purchase_time MEASURES - UNUSUAL_SALE.quantity AS unusual_quantity, + UNUSUAL_SALE.invoice_id AS unusual_invoice_id, + CAST(UNUSUAL_SALE.quantity AS INT) AS unusual_quantity, UNUSUAL_SALE.purchase_time AS unusual_tstamp, AVG(CAST(TYPICAL_SALE.quantity AS INT)) AS avg_quantity, FIRST(TYPICAL_SALE.purchase_time) AS avg_first_sale_tstamp, @@ -353,7 +360,9 @@ This provides several benefits: --- -#### `FIRST(TYPICAL_SALE.purchase_time) AS avg_first_sale_tstamp`, `LAST(...)` +#### `MEASURES` + +Like in a typical SQL `SELECT`, we use this clause to specify what to output, and use values from the "pattern variables" to output information on the "unusual" sale and the `AVG()` of the typical sales. We use `FIRST()` and `LAST()` to output timestamps for the first and last sales that were used to calculate our `AVG()`. This information lets us know exactly which previous sales were used to determine an "unusual" sale. @@ -367,7 +376,7 @@ Currently, this is the only supported ["output mode"](https://nightlies.apache.o As the name suggests, it indicates to only output one row when a match is found. -Once released, `ALL ROWS PER MATCH` will be able to output multiple rows instead. +Once released, `ALL ROWS PER MATCH` will allow you to output multiple rows instead. --- From f937301daec17601e18517e4f70fb914f7bcd5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Fri, 11 Jul 2025 15:58:18 +0100 Subject: [PATCH 11/15] Apply suggestions from code review Co-authored-by: Thomas Cooper --- docs/anomaly-detection/index.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/anomaly-detection/index.md b/docs/anomaly-detection/index.md index 93e8263..f8b559c 100644 --- a/docs/anomaly-detection/index.md +++ b/docs/anomaly-detection/index.md @@ -4,9 +4,11 @@ title = 'Anomaly Detection' > Note: This tutorial is mainly focused on anomaly detection. For detailed information on working with [Flink ETL Jobs](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/learn-flink/etl/) and [Session Clusters](https://nightlies.apache.org/flink/flink-kubernetes-operator-docs-main/docs/custom-resource/overview/#session-cluster-deployments), look at the [Interactive ETL example](../interactive-etl/index.md). -[FlinkCEP](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/libs/cep/) (Complex Event Processing) is a Flink library made for finding patterns in data streams e.g. for detecting suspicious bank transactions. It can be accessed in [Flink SQL](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/overview/) using the [`MATCH_RECOGNIZE` clause](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/). In this tutorial, we will use Flink SQL and its `MATCH_RECOGNIZE` clause to detect and report suspicious sales across many users in real-time. +[Flink CEP](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/libs/cep/) (Complex Event Processing) is a Flink library made for finding patterns in data streams e.g. for detecting suspicious bank transactions. +It can be accessed in [Flink SQL](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/overview/) using the [`MATCH_RECOGNIZE` clause](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/). +In this tutorial, we will use Flink SQL and its `MATCH_RECOGNIZE` clause to detect and report suspicious sales across many users in real-time. -The tutorial is based on the StreamsHub [Flink SQL Examples](https://github.com/streamshub/flink-sql-examples) repository and the code can be found under the `tutorials/anomaly-detection` directory. +The tutorial is based on the StreamsHub [Flink SQL Examples](https://github.com/streamshub/flink-sql-examples) repository and the code can be found under the [`tutorials/anomaly-detection`](https://github.com/streamshub/flink-sql-examples/tree/main/tutorials/anomaly-detection) directory. ## Setup @@ -77,7 +79,10 @@ user-9467&63188787247555258843 ### High Sale Quantities -We want to detect if a user ordered a higher quantity than they usually do. This could be a sign that they made a mistake or their account has been hijacked, which could cause troubles for all parties involved if not dealt with promptly. +We want to detect if a user ordered a higher quantity than they usually do. +This could be a sign that they made a mistake or their account has been hijacked, which could cause troubles for all parties involved if not dealt with promptly. +It could also be a positive sign of increased interest in a particular product and our sales team might want to be notified. +Regardless, it is a situation we would like to be able to spot and take action against. ![](assets/scenario.excalidraw.svg) @@ -171,7 +176,8 @@ WHERE quantity > 3; > Note: This query might take a couple of seconds to return results, since the sales with high quantities are unusual. -Of course, this wouldn't be a particularly good measure. Specific users might always order higher quantities than the average user. +Of course, this wouldn't be a particularly good measure. +Specific users might always order higher quantities than the average user. A more useful measure would involve calculating the average sale quantity of each user, and considering a quantity "unusual" if it is, for example, 3 times higher than that user's average. @@ -305,7 +311,7 @@ This query might look intimidating at first, but becomes easier to understand on #### `ORDER BY purchase_time` -Like previously mentioned, this clause allows us to look for pattens based on time. In our case, the purchase time of the sale. +As mentioned previously, this clause allows us to look for pattens based on time. In our case, the purchase time of the sale. --- @@ -344,11 +350,11 @@ We use two "pattern variables" in our pattern: We append `WITHIN INTERVAL '10' SECOND` after the pattern to set a ["time constraint"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#time-constraint). -> Note: We use an `INTERVAL` of `10 SECOND`s for quick user feedback in this tutorial. In a real situation, you would probably use something like `WITHIN INTERVAL '1' HOUR`. +> Note: We use an `INTERVAL` of `10 SECOND`s for quick user feedback in this tutorial. In a real situation, you would probably use a much longer interval, for example: `WITHIN INTERVAL '1' HOUR`. -This provides several benefits: +Setting a time interval provides several benefits: -- Only the sales made within the last `10 SECOND`s are used. +- Only the sales made within the specified period are used. - Memory use becomes more efficient, since we can prune sales older than this. - Our `AVG()` calculation changes from a typical arithmetic mean to a [simple moving average](https://en.wikipedia.org/wiki/Moving_average). From 4e39966a54be663b7eea35db8766e13cc49171a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Fri, 11 Jul 2025 16:05:49 +0100 Subject: [PATCH 12/15] Don't use wildcard imports https://github.com/streamshub/flink-sql-examples/pull/61/files#r2200868697 --- .../kafka/data/generator/DataGenerator.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java index 7d85fbc..008eb9f 100644 --- a/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java +++ b/tutorials/data-generator/src/main/java/com/github/streamshub/kafka/data/generator/DataGenerator.java @@ -1,15 +1,24 @@ package com.github.streamshub.kafka.data.generator; -import org.apache.kafka.clients.admin.*; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; -import static org.apache.kafka.clients.CommonClientConfigs.*; +import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.CLIENT_ID_CONFIG; +import static org.apache.kafka.clients.CommonClientConfigs.REQUEST_TIMEOUT_MS_CONFIG; import static org.apache.kafka.common.config.TopicConfig.RETENTION_MS_CONFIG; public class DataGenerator implements Runnable { From c6a16feabc3f49ca45af93ec82c6788852507bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Fri, 11 Jul 2025 16:29:14 +0100 Subject: [PATCH 13/15] Add more information on output in classifying section https://github.com/streamshub/flink-sql-examples/pull/61#discussion_r2200754025 --- docs/anomaly-detection/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/anomaly-detection/index.md b/docs/anomaly-detection/index.md index f8b559c..d7db7d8 100644 --- a/docs/anomaly-detection/index.md +++ b/docs/anomaly-detection/index.md @@ -143,6 +143,8 @@ There are many arbitrary ways we could use to define an "unusual" or "suspicious By looking at the data in the `SalesRecordTable`, we can observe that users typically order quantities between `1` and `3` (inclusive): +> Note: The output from the following queries will automatically update as new sales are generated. This might feel a bit unusual if you're used to typical static SQL, but it lets us do cool stuff like calculating `COUNT()` and `AVG()` values that update in real time! + 1. Fetch all the sales and group them by user: ```sql From 92caf953bc38653d4fe714352bde2fc28fe452be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Fri, 11 Jul 2025 17:01:37 +0100 Subject: [PATCH 14/15] Change `SALE` to `UNUSUAL_SALE` https://github.com/streamshub/flink-sql-examples/pull/61#discussion_r2200786460 --- .../assets/first_last.excalidraw.svg | 4 +-- docs/anomaly-detection/index.md | 32 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/anomaly-detection/assets/first_last.excalidraw.svg b/docs/anomaly-detection/assets/first_last.excalidraw.svg index 37107be..dd9b9c3 100644 --- a/docs/anomaly-detection/assets/first_last.excalidraw.svg +++ b/docs/anomaly-detection/assets/first_last.excalidraw.svg @@ -1,5 +1,5 @@ -eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWVPqSFx1MDAxYr4/v8JybuarXHUwMDFhmN6XuXNBRVx1MDAxMVx1MDAxNVxc+WqKXG5cdTAwMTAghy1AXHUwMDAwcer89+mgkFx1MDAxMDpcdTAwMTggKk5cdTAwMDGWJZ1OeNP9Pnnerdt/fuzt7Ttj29z/a2/ffC5cdTAwMWJNq9IzRvt/uO1Ds9e3Om11XGJNPvc7g1550rPuOHb/rz//9M5Iljut17PMptky205f9fu/+ry398/ktzpiVSbndlx1MDAwNz3wclxuK6KWfipcdTAwMTVKjZ/9enNy6qTTVJieWXaMdq1peoeeVTuEXHUwMDAwzFx1MDAxYcaqQVx1MDAwMJAkWEBcdTAwMDJcdJaAMTE7OrIqTn3SI1x0/C8+61E3rVrdUV24TEr/i826vIrw1573pX2n12mYR51mp+fK+Vx1MDAxYjTdtydlySg3ar3OoF2Z9XF6RrtvXHUwMDFiPTUsXr+q1WzmnfHk6mpo1TDuXHUwMDA3vuPh7Vx1MDAwNmCgPews9aW1etvs9+fk7dhG2XLcofJcdTAwMGadK6Gdrkxm6W9Ppp7RMtPuNLVcdTAwMDfN5qzZaldMd/D3XHI0923tytu3TafYmz/81vLLk9003Vx1MDAwYkOCJEaAcTk74ulcdTAwMTlcdTAwMDRcdTAwMDRcdTAwMDebs532ROmgkJghgXw9rP6x0jZnct2q0eyb3iS4wqWCmujXRp9Gti5cdTAwMWVOX06rJ4fO4VV7PDwqXFyeP2RmNzqnlY757OzPXHUwMDBl/Hr7y1x1MDAxYsCBXTFe5YGcXCLAIWdcZlHvVptWu1x1MDAxMVx1MDAxY91mp9zwbuGHb9RcdTAwMDLIWS7nvIxcdTAwMDHQIJmEXHUwMDEzxZZEjSBC81x1MDAxMII8KZdCXGLBJHOhwyDAXHUwMDE0UnVLi1x1MDAxOFwidKtAg9ZcdTAwMDBccvxcdTAwMTjQYD1o5nq/oUPNXHUwMDBlXHUwMDE2lHGoXHUwMDAzXHUwMDA3ZiRcdTAwMWNcdTAwMWOYSOQ+XHUwMDA311x1MDAwMUec+uupo6uG6vahb/Y6bSdvvUyeXHUwMDBmbK71xGhZTXe42dz5XHUwMDA3Tavm3vp+WYlq9vb9t+9YintmXHUwMDFkWlal4ueKsrqoYbXNXjpcbul0elbNalx1MDAxYs3bRZmNgdPJmf1XqZ3ewPSPhXk21X6YRHRcdHSH92c3T09O/yUxcMY3xssoNVx1MDAxYVdcdTAwMTeha/R6nVFcdTAwMDC7gMxcdTAwMTOeZN7nKT69llx1MDAxOVx1MDAxY5HwXHUwMDFhvytcdTAwMWU/iMSyUUlcZoWSmFQ4ZZiwXHUwMDA1tnJVmFx1MDAwN1x1MDAxYqcopYwySFx0XovBwkFcbpjkSqB1QGp3rCBBen/teVx1MDAxMzD5MPv77z/e751QXG7onbBAlE2j71x1MDAxY3VaLctRd3LtSrHwRHSMnnOoZs1q14LHzHYl5MjkrFx1MDAwM1x1MDAxN0Z101jQXHUwMDAxdZ7/mNJpK2Bjms1SZ1x1MDAxNImN87Iqz1x1MDAxZYsjRK5SuZvGbauFXlrxQVogXHKod5BcdTAwMGWB9NXGkGaQXHUwMDBiyORcInjduaDBxlx1MDAxOe9yxClcdTAwMTbSZ81uXHUwMDBlaSg5XHUwMDExUEDqXHLLV0Hap4WB/v9BRMvzYXvUOFx1MDAxOF1WuonzpkTG4+XPqyj2tYA4KXwvT1x1MDAxOcZcdTAwMDEkT9GtvKAk8r88SvBcZurtQnuoXHUwMDE3Wum4M/6pcM9Ft6iVT0NcdTAwMTBWINVZ1D6EXHUwMDA1gK1McI6VTb199vRdPpVLQKa3qtFcXOvGVrXTscNM6jmRg/bzgoxzVvT8fa1iRjeer8f3w9t6ySaVq0xcblVPjMFT9NhcdTAwMTGD3nRGi1x1MDAxZFx1MDAwNVx1MDAwM0PezIXGjrwuW43az+fo281jR1xicuBcdTAwMTKjzj3mWFx1MDAwNFtndjdcdTAwMTFcdTAwMDJyQtZcIumZeJrIXHUwMDExsVx1MDAxMsfX8tKw8vzgyjxcdTAwMWGwg1T2Pp7I0epG/Vx1MDAxMtwsl3NZ5IhcdTAwMTGwi1x1MDAxY31Z5OguOs9cdDU/TI2xLqwqcWhUXHUwMDE1S1x1MDAwNVxmJEGc9ut/KG70XHUwMDBl4Xx43OiibpvWXHUwMDExKuDusTxcdTAwMWRcXGRqnU6+slxiXFz3kjGFjbbL6NxcIlx1MDAxN/M+Kn2FR40wJVx1MDAwMFwiqnMxcbiLXHQl5VDZo3FGjZSLiShQXHUwMDAyUb9afn3UXGJtvYc5d8zuNMe1yfy9x8Csmk33TouZStZOOT9T7PDpfHBcdTAwMTOJgVx1MDAwMVxuXHUwMDAwWXiKMFx1MDAwNTLRIFx1MDAxOX9/Vv0gJD9EZ1VcdTAwMDQhXHUwMDEygPldelx1MDAxZq2iYONcZrOEK9RKXCI9qMeEWcgh8LhuXHJaXHUwMDA1WlpFYq71y1x1MDAxY0dcdTAwMTBCoOu7jHfmVaXE60b6yjhJWOls71x1MDAxMV7akVx1MDAxONSfQdkxaFxmuHvcnEFcdTAwMDWUXHUwMDE4clx1MDAwZXTRXHUwMDFjXHUwMDEymndRKMZcYuFcdTAwMGaA445CfWd9MIXelprWsH5e4pmz0m3msTK6XHUwMDE4ZpxIXHUwMDE0itiOQuOF8tNcblx1MDAxNCrd6Fx1MDAwMFx1MDAwMjqrXHUwMDE3wtBcdTAwMDAsxFC5pS6Lblx1MDAxZIXqPdNtodAwXHUwMDFmdH1cbi1cdTAwMTazd1x1MDAwZYJZ01CTczaoJmpOvlx1MDAxMIlCadB23VHoW+uauCtsTqGSMiqF/1x1MDAxMejhkYZ7oVx1MDAwMFxiLlx1MDAwMaA7XG79xlx1MDAxNHo9oF3DXHUwMDFjXlx1MDAxYuIp3bY7optpsUhcdTAwMTlOSMGOQuOFsrFCXHUwMDBlU5mvXHUwMDEyXHUwMDAzgLRJTFx1MDAxOJr34Fxur4hxXHUwMDE2Z1x1MDAxMjNcdTAwMWVcbkVbTaEodlxuZbDby5xcdTAwMTZrN1x1MDAxZIpcdTAwMDG7fjg/hEBTLKShUFx1MDAxZbRdd1x1MDAxNPrWuibuSjF4oZQxRIg2kMtcdTAwMTZykzNcbqVcYnIkON9R6Dem0GK7OLZlO11cdTAwMWPcmf3x/UE2I9E4XHUwMDEyhfJgRmZHoVx1MDAxYkK5vFx1MDAwMoUyTtVcdTAwMWJIrVx1MDAxYopCXHUwMDBi61x1MDAwNZGcXG6BYk++bEyheKspXHUwMDE0x06hY5BFjSZ4ubDvu8Pzy2dUR4OHSMBcdTAwMGKmQiHwra+Y1dvCQFx1MDAxZM9cIlx1MDAxMHdcdTAwMDV5oUisroBEzLhwS/J0SEQgfImLgrBUrCtiLlXYoHp+XG5Fe9Ar142+WXSslrn3e99UQKn0/7fVJXrvylx1MDAxY5fl22u9pHo/63dnZ8YxOck7ucdcdTAwMDSJXFyyXHUwMDE3tH5cdTAwMTlYtH4h1LEmZd9cdTAwMDStK9LmxlCtr0Kaglx1MDAwMqKviVembChSXHUwMDExXHUwMDEyXHUwMDA0Sya/vKhomVx1MDAxZDdcdTAwMWHbsJk4bdG6nc6c30nDXHUwMDFlXFxELyVcdTAwMDWB5CCflYFcblx1MDAwMFxiYXJBTZlOSyGaLk6GUlKKIflPKi3dWGtcdTAwMWIraK3AXHUwMDE0Yom5NlqyJGnPJEFIOXexLzBcdTAwMWXeXHUwMDE3ioeVWreTPc7d0Juj49tL0teXib4uXHUwMDE3mlx1MDAxZPm0XHUwMDE1xvpcYoRv3IhP4Vx1MDAwM1x1MDAwMychXHUwMDA2SEpcdTAwMTTFr31cdTAwMWSQ5+IxPeuL5nHGXHUwMDFjpU6GLW7XWndboqBT3OEkJJBKxCim7lx1MDAwMlx1MDAxZk8rpnFcdTAwMTlvW1x1MDAwMcJcdTAwMDOF5XDuacBcdTAwMDNSelx1MDAxMH49fXVcYs+4XHUwMDBmaFx1MDAxZSq+mZpCXHUwMDAyIC5cdTAwMTBcIj6bS4/Dz7Gb1vPuI0Zcctb0/z8qXHUwMDEwQt7tzlbuP7+fXHUwMDA1/GPZwfeu5tOgycfgalx1MDAwNrya+ImFXHUwMDBisNUugJOUXCI+cVx1MDAxMlx1MDAxNHYogGZcdTAwMDLSlUZ8WeTJI1x1MDAxNN/q71x1MDAxMPJcdOWZ6MFcInCZRz3nslR4qCVOXHUwMDFi7Xaims1fRjcyXHUwMDEwXGaU2+/Wq0R9oG/u05ZcdTAwMDDU21x1MDAxY6ssWFGTpZ6Wi5uauPMgWLB1Vq9cdTAwMDQ4Jlx1MDAxMHHuRf5iskQu+oWWZWVHNXztnD31ReGwUijrLZFcdTAwMTVcdTAwMTesKDYgkJGYXHUwMDE2rCyXc2mtXHUwMDBmgbtcdTAwMDUrX7VgpVx1MDAwNEI2XGLS2+lcdTAwMTJhjLhcdTAwMTZcdTAwMWRShOdRMFx1MDAwNFxcKJMzzjxKPGtW9DHZT16z8lx1MDAwZelEjdSuvWZcdTAwMDU9XHUwMDFk3bZcdTAwMWGZRDd7cCjYRTbRKLCT6KSHfeiaeNaahCfS+tLfJeJTMfr1z43Pllx1MDAwMF2hYFx1MDAwZlCBXHUwMDA0W1x1MDAwNKCryqFcdTAwMGLJiMCQYyDWoqxPivn0riBcdTAwMTdcdTAwMDOc+4mG193roZGp1kxNIZtuy1x1MDAwZeIrOZ5opW+PuGXJO//c7ZQyoJR881Q8RIBIyoQ2XHUwMDE1XHUwMDFmXHUwMDFh6kFAOVx1MDAxOIJgXHUwMDFje4RS/cC1KCRWXHUwMDBmNEFcdTAwMDJOXHUwMDE52Ip0vG53jiVOefSdO5ZcdTAwMDfYllx1MDAwMptcIlx1MDAwZsivwPZUaVx1MDAwN+w1gX24MbCZRJIpO1670j+8aFx1MDAxY1x1MDAxMiqVY/X1LPQxuP5YWPum521cdTAwMTPXdIRUycSqLVx1MDAwZibqlMSQIa5cdTAwMWMrKVxi44xcdTAwMTPm61Uz7MmAXHUwMDA2XHUwMDEyXHUwMDFliyqwXHJPXHUwMDE02S3gRFx1MDAxMeds26Hj0e3904to1VwiOaA4UCnLNParcmCSbmRYQoxVf+6ztba17Gerni9HK1izakKIxFx1MDAxOOpWcKLQXHUwMDEwXGZVRrCb84h5q75cdTAwMTh8zJN0Ln/7u764IKz6R8xd5eOLXHUwMDBiXHUwMDE2ZIzHs0zlnluUPybyxZvDo2zq5mxET0+jla/7RiZcZpRcdTAwMTIlicIkUiSCqIKkZz3uMFx1MDAxOVx1MDAwMZPH0TEp1ENPKDzoILmkfpZcdTAwMTDCXHUwMDE5pGD79uTKXHUwMDFjbD0kg1wixlx1MDAxNOtcdTAwMTlcdTAwMTHy5LSrve7h0DZcdTAwMGVLqYfuw3kkRFx1MDAxMlx1MDAxY7C7NbvksUksXHUwMDE2XHSgpp1DwTWh2Fx1MDAxZFwiw1x1MDAxMZlaIVx1MDAxNsuxuzFcdNauXG5joavCKFx1MDAwNdK1uLdcdTAwMGaR+YNMaqvxOC9gPGjMZrKJoVlr1Fx1MDAxMoXK3dnltXPXXHUwMDFlXHUwMDFhi2g0m03L7i/EXZlMulx0V8wwZdy/S81r6lx1MDAxMb2mXHUwMDFlXHUwMDAxo+pHXHUwMDEz/eI8SSVQbyhcdTAwMDRHXHUwMDEyakxa7lxy8jaAlVx1MDAwNNq/Mtd4slxuVlx1MDAxMVx1MDAxMJhqNvVyjZjFLalnYHVcclx1MDAxYugvTds4qah9vvtcdTAwMTSyL1x1MDAxZnPZetXA/W72ppzI5ZDTuV6pukkwiFxi8aNh/fCvXpooZIWl8lpcdTAwMTmXVKk2I8S3XFzgNakokoJcdTAwMDGpWEoojJDFZVx1MDAxZN8wqbhN4DiNXHUwMDBlXHUwMDBlXHUwMDA0XFzPXHIhXTyYslx1MDAwNYPT28dcdTAwMTlwTihcdTAwMDHrXHUwMDA0jubk2Fxcg1x1MDAxN7hMv1jyk7OK73BL1CWUa3NbrZu7KqWh9VxcXHUwMDFj3nXNWq1gnV9p/vmJntuIUFdcdTAwMDdqfqmycqTkXHUwMDAx/M64TUJcdJlYrN3dcdveXHUwMDA28D1bgdtcYiaAz1x1MDAxNSr6/ztcdTAwMTBcYlx1MDAwNbBiNkDcXHUwMDA37OeRW/m49LNvwfuXPOpcXMGH9HnCejxYjdw4WW3h5lx1MDAxMnzopYlCbsqGT1wiN1arqI1yXHUwMDE4xm2EXCIhfe7WjttiXHUwMDAxRzo6OCjCUkJK9GGT0FAm54yqh1asO3espb3bSWzvXHUwMDEwS5zE9uPtabBv2HbeUaO5P0267Fx1MDAwZi1zdLio6L9VJy/3qTKBvavk5iSV9uvHr39cdTAwMDFoZqBcdTAwMGYifQ==1USER-1610123purchase_time (seconds)3FIRST()LAST()UNUSUAL_SALE22 \ No newline at end of file diff --git a/docs/anomaly-detection/index.md b/docs/anomaly-detection/index.md index d7db7d8..3dac204 100644 --- a/docs/anomaly-detection/index.md +++ b/docs/anomaly-detection/index.md @@ -230,12 +230,12 @@ FROM SalesRecordTable MATCH_RECOGNIZE ( ORDER BY purchase_time MEASURES - SALE.quantity AS unusual_quantity, - SALE.purchase_time AS unusual_tstamp - PATTERN (SALE) + UNUSUAL_SALE.quantity AS unusual_quantity, + UNUSUAL_SALE.purchase_time AS unusual_tstamp + PATTERN (UNUSUAL_SALE) DEFINE - SALE AS - CAST(SALE.quantity AS INT) > 3 + UNUSUAL_SALE AS + CAST(UNUSUAL_SALE.quantity AS INT) > 3 ); ``` @@ -247,10 +247,10 @@ MATCH_RECOGNIZE ( The `DEFINE` and `MEASURES` clauses are similar to the `WHERE` and `SELECT` SQL clauses respectively. -- We `DEFINE` a single `SALE` ["pattern variable"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#defining-a-pattern) with our condition, then include it in our pattern. - - > Note: The value of `SALE` is the row/sale which matches our `DEFINE`d condition. +- We `DEFINE` a single `UNUSUAL_SALE` ["pattern variable"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#defining-a-pattern) with our condition, then include it in our pattern. + - > Note: The value of `UNUSUAL_SALE` is the row/sale which matches our `DEFINE`d condition. -- In `MEASURES`, we use the value of `SALE` to output both the quantity and timestamp of the "unusual" sale. +- In `MEASURES`, we use the value of `UNUSUAL_SALE` to output both the quantity and timestamp of the "unusual" sale. ### Pattern navigation @@ -264,20 +264,20 @@ FROM SalesRecordTable MATCH_RECOGNIZE ( ORDER BY purchase_time MEASURES - FIRST(SALE.quantity) AS first_unusual_quantity, - FIRST(SALE.purchase_time) AS first_unusual_tstamp, - LAST(SALE.quantity) AS last_unusual_quantity, - LAST(SALE.purchase_time) AS last_unusual_tstamp - PATTERN (SALE{2}) + FIRST(UNUSUAL_SALE.quantity) AS first_unusual_quantity, + FIRST(UNUSUAL_SALE.purchase_time) AS first_unusual_tstamp, + LAST(UNUSUAL_SALE.quantity) AS last_unusual_quantity, + LAST(UNUSUAL_SALE.purchase_time) AS last_unusual_tstamp + PATTERN (UNUSUAL_SALE{2}) DEFINE - SALE AS - CAST(SALE.quantity AS INT) = 2 + UNUSUAL_SALE AS + CAST(UNUSUAL_SALE.quantity AS INT) = 2 ); ``` ![](assets/first_last.excalidraw.svg) -Notice how the `SALE` pattern variable doesn't simply hold one value, it maps to multiple rows/sales/events. We're able to pass it to the `FIRST()` and `LAST()` functions to output the quantities from both the first and second matching sale respectively. +Notice how the `UNUSUAL_SALE` pattern variable doesn't simply hold one value, it maps to multiple rows/sales/events. We're able to pass it to the `FIRST()` and `LAST()` functions to output the quantities from both the first and second matching sale respectively. These functions are specifically referred to as ["offset functions"](https://nightlies.apache.org/flink/flink-docs-release-2.0/docs/dev/table/sql/queries/match_recognize/#logical-offsets), since we use "logical offsets" to navigate the events mapped to a particular pattern variable. From dcb0827945b68220e14d2691000630c9293e1eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20P=C5=82aczek?= Date: Fri, 11 Jul 2025 17:23:19 +0100 Subject: [PATCH 15/15] Even more detail in classifying section --- docs/anomaly-detection/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/anomaly-detection/index.md b/docs/anomaly-detection/index.md index 3dac204..e0d9b8f 100644 --- a/docs/anomaly-detection/index.md +++ b/docs/anomaly-detection/index.md @@ -143,7 +143,7 @@ There are many arbitrary ways we could use to define an "unusual" or "suspicious By looking at the data in the `SalesRecordTable`, we can observe that users typically order quantities between `1` and `3` (inclusive): -> Note: The output from the following queries will automatically update as new sales are generated. This might feel a bit unusual if you're used to typical static SQL, but it lets us do cool stuff like calculating `COUNT()` and `AVG()` values that update in real time! +> Note: Don't worry if the constantly changing output in the following queries is confusing. The main thing to take away is that the values in the `avg_quantity` column stay as `2` most of the time, which means that the average quantity for each user stays as `2` even as new sales are generated and cause the calculation to update. 1. Fetch all the sales and group them by user: