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..dd9b9c3 --- /dev/null +++ b/docs/anomaly-detection/assets/first_last.excalidraw.svg @@ -0,0 +1,5 @@ + + +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWXPquFx1MDAxMn6fX5HKvMytXHUwMDFhXHUwMDE47cu8ZSErIVx1MDAwYiHbramUXHUwMDAxXHUwMDA3fMKOgZCp89+vTFx1MDAwMpaN7Fx1MDAxOHBcdTAwMTLOLZypM0GWTdvqr7/uVkv597etrW133LG3/97atl8qVsOp9qzR9p9e+9Du9Z12S51Ck8/99qBXmfSsu26n//dff/lXZCvt5ttVdsNu2i23r/r9V33e2vp38q8641Qn13ZcdTAwMDc98HpcYquidnxffig//+jXXHUwMDFik0snnabC9OyKa7VqXHLbP/Wi2iFcdTAwMDRg1jBWXHJcdTAwMDKALMFcdTAwMDJcdTAwMTJIsFx1MDAwNIyJ2dmRU3Xrk1x1MDAxZVmgXHUwMDFmfNajbju1uqu6cJmV+sFmXd5E+HvL/9K+22s/23vtRrvnyfk7tL1cdTAwMWZfyrJVea712oNWddbH7Vmtfsfqqdfi93tyXHUwMDFhjaI7ntxdvVr1XHUwMDFht0Pfcfv+XHUwMDAwMNRcdTAwMWV1lfrSWr1l9/tcdTAwMDF521x1MDAxZKviuN6r0l+dJ2HnuDpcdTAwMTmlf3yZelbTPvaGqTVoNGbNTqtqey9/20KBb2tV379tOsT++OH3lp++7Lbt3Vx1MDAxOFx1MDAxMiQxXHUwMDAyjMvZXHUwMDE5X8/U6Ipwc6HdmihcdTAwMWSaXGYxkIL6cvX3lba5k/s+WY2+7Vx1MDAwZoInXFwurIm6NmpcdTAwMWHZPL09fD18Oth1d89b4+Hew9nJbX72oFx1MDAwMa107Vx1MDAxN3d7duLn+2/+XHUwMDBiXHUwMDFjdKrWmzyQU4SIJJxL5qtTw2k9h99uo1159lx1MDAxZuE37a2FkFx1MDAxMy9nUMZcdTAwMTBokMzCiWJLXCJcdTAwMTBBKFxiIcizMlx1MDAxNkJcYmaZXHUwMDA3XHUwMDFkXHUwMDA2XHUwMDAxppAyROcxROhagVx1MDAwNi1cdTAwMDFcdTAwMWH4OaDBZtBcdTAwMDR6v6NDjVx1MDAwZVx1MDAxNpRxaFx1MDAwModCTiQ4XHUwMDAw5pBcdTAwMTE1PMuAI0399dXRU0P1+FBcdTAwMWK9dsstOq9cdTAwMTP7wFx1MDAwMq1cdTAwMDdW02l4r5tcdTAwMDWu32k4Ne/RtytKVLu3rT++6yjumXVoOtWqzlx1MDAxNVx1MDAxNXVTy2nZveMkpNPuOTWnZTWu52W2XHUwMDA2bvvK7r9J7fZcdTAwMDa2/i7so6n2wyyiMdBcdTAwMWTeXHUwMDFjXd7fu/3XzMBcdTAwMWRfWq+j3Gj8NFx1MDAwZl2r12uPQthcdTAwMDUkSHiS+Z+n+PRbZnBEwm/8VfH4SSRWSEpiKJLEpMIpw4RhXSPfcSpQuHGKUopcYpNcdTAwMDCTpVx1MDAxOOxTQNppO2GC9H/b8lx1MDAwN2DyYfb7P39+3DujXHUwMDE00L9gjihcdTAwMWJW391rN5uOq57kwpNizlwiulbP3VWj5rRq4XN2q1x1MDAxYXFmctWOXHUwMDA3o7ptzemAuk4/p3TaXHT5mHaj3Fx1MDAxZSVi46J8kkd3jyNEznNXl8/XzSZ6baZcdTAwMDdpgVxmoN5AOlx1MDAwMtLnK0OaQS4gk8yPXHUwMDBlNETTcONcdTAwMTTRkCNOsZCaN7s6pKHkREBcdTAwMDGp/1q+XHUwMDBi0ppcdTAwMTaG+v9cdTAwMWZcIlqeXGZbo+ed0Vm1mzlpSGTdnf04T+JfXHUwMDBiiLNCO3xlXHUwMDE4h5A8Rbfy5bJIP3xcIvFcdTAwMWTq9UJ7ZFx1MDAxNFpteyP+pXC/Su5Rq5iGIKxAasA1kZFUjVx1MDAwMEWK5Fx1MDAxOfZcdTAwMWb7u7l66lCXirmrXGZkZrdcdTAwMWFcdTAwMDVaV3ar3XYnyqdcdTAwMGWIXHUwMDFjdqDnZFxmuNHB51rEj35+uVx1MDAxON9cZq/r5Vx1MDAwZame53Po6cBcdTAwMWHcJ09cdTAwMWUx6Fx1MDAwZmey5FE4M+RDOzJ55HdZa9h+PUlfr548QpBcdTAwMDOPXHUwMDE5TfExJ3OtPk1cdTAwMTNcdTAwMDJcdTAwMTnAcqnweCafIXdEnMz+hTyznFwi3zm391x1MDAwNmwnV7hZw9xRvJxxuSNGwCZ39G25o1Jyplx1MDAxM2p8lI5TU2JV4khcdTAwMTdcdTAwMTYxKaFcdTAwMWG5TeborUMoc/RcdTAwMDHjfHrm6LTesZ099IC7+/JwcJqvtdvF6jxwvVumlDhaL7dzjYLMm6T8XHUwMDE1nTfClFx1MDAwMIioKciMRiiEknLIOExcdTAwMTOiKshEXHUwMDE0KIGorpbfnzdCa1x1MDAxZmNcdTAwMDbOddqNcW0yflx1MDAxZjEweypcdTAwMWP3XHUwMDBlXHUwMDFm89VCJ+f+yLHd+5PBZVwiXHUwMDA2XHUwMDA2KFx1MDAwNGThK8JcdTAwMTTIxIBk/Ouz6ich+TY5q1wiXGKRXHUwMDAwTFx1MDAwZuo1Wo2MXHUwMDFmIeFcbrWSSFx1MDAxZuopYVx1MDAxNnLF1qvQKjDSKlx1MDAxMoHWb4tcdTAwMWNBXHUwMDA0gS5cdTAwMWYzluzzapnXreNz6yDjXHUwMDFjXHUwMDE3enfwrJOIQfU5lFxyg6aAu7vVXHUwMDE5VECJIefAmM+Z41W/eFx1MDAwMGKE8CfAcUOh2lWfTKHX5YYzrJ+Uef6ofJ2/q45Oh3k3XHUwMDExhVwitqHQdKF8v1x1MDAwMIVKLzuAgMnrhZCFW6eY5eqagK+83nHpulx1MDAxMGhUXHUwMDA0ujyBPj5cdTAwMTZKLoJcdTAwMDXbUkNzNHjK1NziQ1wiXHUwMDAypWHPdUOg761Lou5hdVx1MDAwMpWUUSl0XHUwMDAz6KORRsegXHUwMDAwXGIuXHUwMDAxSFx1MDAxNY5cdTAwMWJcdTAwMDL9Ylx1MDAwMr1cdTAwMTjQrmVcdTAwMGYvLHF/3Oq0RTffZIlmOCFcdTAwMDVcdTAwMWJcdTAwMDJNXHUwMDE3ytZcdTAwMDJzmMp5lVx1MDAxOFx1MDAwMGTCLETRoEVcZnNAiFg/XHUwMDBlRWvNoSh1XHUwMDBlZbDby1x1MDAxZj7WLttcdTAwMTRcdTAwMDN2cXuyXHUwMDBigaFayMChPOy6bjj0vXVJ4JVTXGJCKWOIXHUwMDEwY1x1MDAxZZdFz0JSXHUwMDA0OVx1MDAxMpxvOPRcdTAwMTfm0MfW47gjW8ePg5LdXHUwMDFm3+xcdTAwMTTyXHUwMDEyjVx1MDAxM3EoXHUwMDBmT8hsOHRFKFdcdTAwMTbgUMap+lx1MDAwMdJcdTAwMTiF4shlJ0pKhjigeP04XHUwMDE0rzWH4tQ5dFxmXG7ouVx1MDAwMV5POzfd4cnZXHUwMDBiqqPBbVwi5IWnQiHQVljMKm5hqJBnXHUwMDFliZuSvEgoPi1cdTAwMDBFzLjwivJMUESa41x1MDAxMoaiUINCIFjDUoXOoFepW3370XWa9tZcdTAwMWZ9W1x1MDAwMaXa/89a1+h9KHNarm+v+Zrr/aiXjo6sfXJQdK/uMiRxzVx1MDAxZWfBkj2q4XJWTVx1MDAwYk20SbUr11x1MDAxYa1cdTAwMGLy5spQrS/CmoJcdTAwMDJiropHMjJ1XHUwMDBikVxuWKEgOM2yeFx1MDAwZqlcdTAwMTRcdTAwMTG+0OxnnCM3XHUwMDFhd2Ajc9ik9c5x/qQkrc7gNHktKVxiTVx1MDAwZfJZXHUwMDFkqFx1MDAwMCrmZvNqykxaXG7RdHkylJJSXGbJ/6XS0pW19nlcdTAwMDGtXHUwMDE1mEIsMTflS1x1MDAxMIz29Vx1MDAwNGaUUrSc2r6fMJaJXHUwMDBlb1x1MDAxZVx1MDAxZXertW67sH91SS/39q/PSN9cXCb6tmBodubL6kTNKVxiffmpjFl+Kihhgokk1fJvb+TlcZ9cdTAwMWX1RWM/b49yXHUwMDA3wybv1JqlNdHQKfBwXHUwMDE2XHUwMDEySCViXHUwMDE0U2+Nj69O08yMv7NcdTAwMDDhodJyXHUwMDE4MFx1MDAwNzwkpY/ht8tcdTAwMTfH8Iz8gMGqaLZpilx0gLhQ2qI5XWYgfo3jtFxcfJ8wb7BkXHUwMDA24LNSIeTD7mzh/sEtLeCfcSc/upumQZOP4fVcZngx8TNzN2CL3Vx1MDAwMGeVXHLmkyhBYYdcdTAwMDJoZyBd6I3H5Z58RtFcdTAwMTaAR7BPJNEkT1x1MDAxN4GzXCLquWflh9ta5vC51co8XHUwMDE1imfJvVxmXHUwMDA0Q/X2m1x1MDAxNStJXHL66kFtXHUwMDE5QLPTsciSXHUwMDE1NVjKWlx1MDAxMpMrwkWkXHUwMDA3jVx1MDAwMMdcdTAwMDRcIs5T3+3ktP/QdJzCqIYv3KP7vnjYrT5UzK7IYitWXHUwMDAwk5xARnyFXck9j5czttiHwM2Kle9asVJcdTAwMDZcdTAwMTF7XHUwMDA0mVx1MDAxZHWJMEbciFx1MDAwZSmiZ1IwXHUwMDA0XFxg5dKkXHUwMDE5X1x1MDAwMq5cXDo12nJcdTAwMDFcdTAwMDX29TE2KfvFi1Y+IJ2kqdqlXHUwMDE3raD7vevmcz7TLezsXG52Wsg8P7CD5KSn75AxXHStXHJTnshcdTAwMThMs18lQWv161+boC1cdTAwMDO6QMVcdTAwMWWgXHUwMDAyXHUwMDA1wjltnVxuXHQ3+utUMKRiyZVkcZRlde9cdTAwMWKlwsvpyeC1RYdHXHUwMDFklLNfXHUwMDBm04qeKWBcdTAwMWMsMnlcdTAwMWGj971zyMVcdTAwMDBf/UDDi+7F0Mo/1WxDpZxpT1x1MDAxMKJVNE90XsuuTXWeXHUwMDFhdlx00N72RuO3glx1MDAxYc9Xn+mHXGIoXHUwMDE1YcI4azhf0aot/+BcdTAwMTAoXHUwMDBmJO3tXHUwMDAzqJeMXYqgUo1vM74qTj4yLUacg1xcirP92tC97zP5Rniy+4Azj/iq03HpeHR9c/8qmjXNQijWrVxmJqqWRZJcbklcYkXqf5hcbkq0XjWrY9aGcFbBtM9ITG4h+Vx1MDAxZSTxicJYXHUwMDBiQlGINbWp7ll5gW9UfFx1MDAwYvKLhH/fYUF2V7YgTFwiZVx1MDAwNaQ07uhcdTAwMTezoVx1MDAxZsaSYv2ylOxcdTAwMDdFgvohz3fZXHUwMDBmX1x1MDAwZr0jw2Sa5iPKSNS6V+flY+i8PFx1MDAwZUtdu1Z7cE7O8yYjkVx1MDAwMVkv9kZcdTAwMDJhQJSZXHUwMDAwjMxbXHSa5VxiQihcdTAwMDWHRFxuQeWcXHUwMDA2JLJcXLmrlybld5ni4+XuXiF3eTSih4dcdTAwMTFCqa/jxFv+R7GKnICYXHUwMDE3ypCG+DLTXHUwMDE1b4PjXHUwMDAydlx1MDAxYyouZlx1MDAwNt9HkW3Wy6RLiLHqzzXfdF1cdTAwMGKl1sqS7S3g/atcdTAwMDEhXHUwMDEyY2ha8lx1MDAxYbNMnSj3nym7lfrsWbxbXHUwMDFk0K4l/H9CpbafzFx1MDAxMiH/wfFV8fpcdTAwMGZzsUdUNZZcYtzl84s95mRMJ9CPt16xXHUwMDBiXG60N1x1MDAxM4V5ibJEQV6ZWYKoQryf5dpAPlx1MDAwMeT3k0NeKJsqXHUwMDE0XHUwMDFlTIjnkVx1MDAwNVlMIMhcdTAwMTlPPd7/1NlySqBcdTAwMDBE1/ZF8Z7fWXu4h0VMKa03XCLk3m099bq7w461W87ddm9PkjE8x1kpXHRT8TRcdTAwMTZIwGCuQy+MmfG9IFx1MDAxZd9cdTAwMGKClMNcdTAwMWGRhN+AP1x1MDAxYfy5XHUwMDA1svBcdTAwMWN762yxcUkgjC6XYepCzCFNne8/N9/HXHUwMDAwlyvhv1QoXHUwMDE1Szv5x+JOPrfWVsAsaDq2oJAvZIZ27bmWeaiWjs4u3FJraM3bXHUwMDAyu9FwOv25XHUwMDA0P5NZb2ZcdTAwMWYzTFx1MDAxOdfXeL/NcaO3OW5cdTAwMTXoqP9cZo5cdTAwMDHnWSqB+oFCcCShIVx1MDAxNuD+y15cdTAwMDfbQELt3zmpfbCIaUBAYKqCXHUwMDAxg2mQ89ufz3Y691xcNqhcdTAwMTdBrmxcdTAwMWGM7KIpZF/eXVx1MDAxNepPXHUwMDE27ndcdTAwMGKXlczVXHUwMDE1cttcdTAwMTdcdTAwMGLMXnMoXHUwMDE4RGRcdTAwMTHLXHUwMDEwXHUwMDAzXHUwMDBmszSJqFLyrPfnUahSbUZcYlwiQWxAkVx1MDAxNVxmSM688lpB5ldcdTAwMTD9grPX61x1MDAwNI7D5OBAyoJRgZBpboCyyNRcdTAwMWXkgHNCXHRI9c9cdTAwMDAso79zzGZelfvFk9dcdTAwMWYwS9K1ukszW3ya8Fx1MDAwM2YjQt1cdTAwMWSo0aXKpZKSh9A7YzZcdCVkYr5GfMNsWyuA92hcdTAwMDFmI5hcdTAwMDBcdTAwMWWoh9Vn9mBk0ItcdTAwMTH3qoLTLMz6iNoq++VcdTAwMWZ9XHUwMDA33rxcdTAwMTZR+1x1MDAxY95cdTAwMWWfZJy7XHUwMDFkjdr+NN/2K4JpRfTaxPVKlGl+yiSUSYHMXCLGXHRThEk5jGJMQpHQN8LdMGYqoDtOXHUwMDBlOm9phYSUmNJMLGYnKM6oMobpblx1MDAwNeVcdTAwMTEmJ0uum18rwvyAsD6dMOPD9fiqXHUwMDE3xlx1MDAwMqXIwZwwp1wiS4x1yrP57CwlnHBMgVx1MDAxMIww4pWKXHUwMDFiimRCXHUwMDA1z2Iz41x1MDAxZInmXHUwMDEzM5pcdTAwMTeqmaGSXHUwMDBiRs1/SieSV71hXHUwMDAxyn9dZsY78DDmjM/3b49cdTAwMTGrq96RmVNT/5ZzVJzarHh8reaWXjpcdTAwMDMg01x1MDAwZa9wTuv2Nlx1MDAwMVxys3ROL1x1MDAxMs2Dx+eWt1x1MDAwMvPgiqrVq0TYXHUwMDBikJGg2lx1MDAxZmmZyYFccnJ8ylT4b++Dsm11OkVXKd7sXHUwMDE5t4eOPdqdt1x1MDAxNr8/TVx1MDAwZc/PmsjmWVxue4Kxn7/9/Fx1MDAxZlSccqEifQ==1USER-1610123purchase_time (seconds)3FIRST()LAST()UNUSUAL_SALE22 \ No newline at end of file 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/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/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/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..e0d9b8f --- /dev/null +++ b/docs/anomaly-detection/index.md @@ -0,0 +1,484 @@ ++++ +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). + +[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`](https://github.com/streamshub/flink-sql-examples/tree/main/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 +... +``` + +### 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. +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) + +## 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): + +> 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: + + ```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. + - 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. + +## 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 + UNUSUAL_SALE.quantity AS unusual_quantity, + UNUSUAL_SALE.purchase_time AS unusual_tstamp + PATTERN (UNUSUAL_SALE) + DEFINE + UNUSUAL_SALE AS + CAST(UNUSUAL_SALE.quantity AS INT) > 3 +); +``` + +[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 `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 `UNUSUAL_SALE` to 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(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 + UNUSUAL_SALE AS + CAST(UNUSUAL_SALE.quantity AS INT) = 2 +); +``` + +![](assets/first_last.excalidraw.svg) + +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. + +### 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.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 +); +``` + +This query might look intimidating at first, but becomes easier to understand once we break it down. + +--- + +#### `ORDER BY purchase_time` + +As mentioned previously, 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) + +--- + +#### `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. + + - 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. + +![](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 a much longer interval, for example: `WITHIN INTERVAL '1' HOUR`. + +Setting a time interval provides several benefits: + +- 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). + - 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) + +--- + +#### `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. + +![](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 allow you 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) + +## 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/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 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 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(); 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..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,30 +1,89 @@ package com.github.streamshub.kafka.data.generator; +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.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.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 { + 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)); - 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(); + 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); } } @@ -61,7 +120,7 @@ private void send(Producer producer, Supplier