customeranalytics 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- customeranalytics/__init__.py +319 -0
- customeranalytics/configs.py +480 -0
- customeranalytics/data_storage_configurations/__init__.py +245 -0
- customeranalytics/data_storage_configurations/data_access.py +165 -0
- customeranalytics/data_storage_configurations/data_works_pipeline.py +207 -0
- customeranalytics/data_storage_configurations/es_create_index.py +544 -0
- customeranalytics/data_storage_configurations/logger.py +36 -0
- customeranalytics/data_storage_configurations/query_es.py +157 -0
- customeranalytics/data_storage_configurations/reports.py +684 -0
- customeranalytics/data_storage_configurations/schedule_data_integration.py +355 -0
- customeranalytics/docs/queries.yaml +245 -0
- customeranalytics/docs/web_configs.yaml +2 -0
- customeranalytics/exploratory_analysis/__init__.py +109 -0
- customeranalytics/exploratory_analysis/churn.py +232 -0
- customeranalytics/exploratory_analysis/cohorts.py +535 -0
- customeranalytics/exploratory_analysis/descriptive_statistics.py +470 -0
- customeranalytics/exploratory_analysis/funnels.py +407 -0
- customeranalytics/exploratory_analysis/product_analytics.py +340 -0
- customeranalytics/exploratory_analysis/promotion_analytics.py +299 -0
- customeranalytics/exploratory_analysis/rfm.py +202 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_avg_order_count_per_promo_per_cust.csv +99 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_chart_1_search.csv +2 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_chart_2_search.csv +46 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_chart_3_search.csv +2 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_chart_4_search.csv +2 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_churn.csv +2 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_churn_weekly.csv +10 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_client_feature_predicted.csv +1590 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_client_kpis.csv +7060 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_clvrfm_anomaly.csv +561 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_clvsegments_amount.csv +8 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_customer_journey.csv +18 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_clv.csv +103 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_cohort_downloads.csv +68 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_cohort_from_1_to_2.csv +66 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_cohort_from_2_to_3.csv +64 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_cohort_from_3_to_4.csv +55 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_dimension_values.csv +199 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_funnel.csv +36 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_funnel_downloads.csv +68 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_inorganic_ratio.csv +6070 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_orders.csv +57 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_organic_orders.csv +64 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_products_of_sales.csv +12574 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_promotion_discount.csv +64 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_daily_promotion_revenue.csv +6070 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_dcohort_anomaly.csv +6 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_dcohort_anomaly_2.csv +118 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_deliver.csv +47 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_deliver_kpis.csv +2 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_deliver_weekday_hour.csv +25 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_dfunnel_anomaly.csv +118 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_dimension_kpis.csv +199 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_dorders_anomaly.csv +85 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_frequency_clusters.csv +6 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_frequency_recency.csv +3740 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_hourly_funnel.csv +25 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_hourly_funnel_downloads.csv +25 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_hourly_inorganic_ratio.csv +2353 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_hourly_orders.csv +25 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_hourly_organic_orders.csv +2353 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_inorganic_orders_per_promotion_per_day.csv +6070 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_kpis.csv +2 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_monetary_clusters.csv +6 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_monetary_frequency.csv +3740 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_monthly_funnel.csv +5 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_monthly_funnel_downloads.csv +5 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_monthly_orders.csv +4 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_most_combined_products.csv +11 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_most_ordered_categories.csv +11 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_most_ordered_products.csv +11 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_order_and_payment_amount_differences.csv +91 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_prepare.csv +101 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_prepare_weekday_hour.csv +25 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_product_kpis.csv +201 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_product_usage_before_after_amount_accept.csv +11 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_product_usage_before_after_amount_reject.csv +7 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_product_usage_before_after_orders_accept.csv +11 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_product_usage_before_after_orders_reject.csv +11 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_promotion_comparison.csv +91 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_promotion_kpis.csv +99 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_promotion_number_of_customer.csv +99 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_promotion_usage_before_after_amount_accept.csv +81 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_promotion_usage_before_after_amount_reject.csv +11 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_promotion_usage_before_after_orders_accept.csv +55 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_promotion_usage_before_after_orders_reject.csv +37 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_purchase_amount_distribution.csv +22 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_recency_clusters.csv +6 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_recency_monetary.csv +3740 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_rfm.csv +2001 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_ride.csv +101 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_ride_weekday_hour.csv +25 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_segmentation.csv +11 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_segments_change_daily_before_after_amount.csv +10 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_segments_change_daily_before_after_orders.csv +9 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_segments_change_monthly_before_after_amount.csv +10 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_segments_change_monthly_before_after_orders.csv +11 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_segments_change_weekly_before_after_amount.csv +10 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_segments_change_weekly_before_after_orders.csv +10 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_user_counts_per_order_seq.csv +47 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_weekly_average_order_per_user.csv +13 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_weekly_average_payment_amount.csv +13 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_weekly_average_session_per_user.csv +13 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_weekly_cohort_downloads.csv +13 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_weekly_cohort_from_1_to_2.csv +13 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_weekly_cohort_from_2_to_3.csv +13 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_weekly_cohort_from_3_to_4.csv +12 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_weekly_funnel.csv +25 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_weekly_funnel_downloads.csv +25 -0
- customeranalytics/exploratory_analysis/sample_data/sample_data_weekly_orders.csv +10 -0
- customeranalytics/ml_process/__init__.py +64 -0
- customeranalytics/ml_process/ab_test.py +615 -0
- customeranalytics/ml_process/anomaly_detection.py +456 -0
- customeranalytics/ml_process/clv_prediction.py +301 -0
- customeranalytics/ml_process/customer_segmentation.py +354 -0
- customeranalytics/ml_process/delivery_analytics.py +754 -0
- customeranalytics/utils.py +186 -0
- customeranalytics/web/app/__init__.py +37 -0
- customeranalytics/web/app/base/__init__.py +9 -0
- customeranalytics/web/app/base/forms.py +16 -0
- customeranalytics/web/app/base/models.py +43 -0
- customeranalytics/web/app/base/routes.py +126 -0
- customeranalytics/web/app/base/static/assets/css/app.css +20 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Black.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Black.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-BlackItalic.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-BlackItalic.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Bold.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Bold.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-BoldItalic.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-BoldItalic.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-ExtraBold.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-ExtraBold.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-ExtraBoldItalic.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-ExtraBoldItalic.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-ExtraLight-BETA.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-ExtraLight-BETA.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-ExtraLightItalic-BETA.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-ExtraLightItalic-BETA.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Italic.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Italic.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Light-BETA.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Light-BETA.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-LightItalic-BETA.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-LightItalic-BETA.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Medium.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Medium.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-MediumItalic.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-MediumItalic.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Regular.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Regular.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-SemiBold.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-SemiBold.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-SemiBoldItalic.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-SemiBoldItalic.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Thin-BETA.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-Thin-BETA.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-ThinItalic-BETA.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-ThinItalic-BETA.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-italic.var.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter-upright.var.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/Inter.var.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-brands-400.eot +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-brands-400.svg +3570 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-brands-400.ttf +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-brands-400.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-brands-400.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-regular-400.eot +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-regular-400.svg +801 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-regular-400.ttf +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-regular-400.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-regular-400.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-solid-900.eot +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-solid-900.svg +5028 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-solid-900.ttf +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-solid-900.woff +0 -0
- customeranalytics/web/app/base/static/assets/fonts/fa-solid-900.woff2 +0 -0
- customeranalytics/web/app/base/static/assets/img/avatars/avatar-1.png +0 -0
- customeranalytics/web/app/base/static/assets/img/avatars/avatar-2.png +0 -0
- customeranalytics/web/app/base/static/assets/img/avatars/avatar-3.png +0 -0
- customeranalytics/web/app/base/static/assets/img/avatars/avatar-4.png +0 -0
- customeranalytics/web/app/base/static/assets/img/avatars/avatar-5.png +0 -0
- customeranalytics/web/app/base/static/assets/img/avatars/info.jpeg +0 -0
- customeranalytics/web/app/base/static/assets/img/icons/customanality.png +0 -0
- customeranalytics/web/app/base/static/assets/img/icons/icon-48x48.png +0 -0
- customeranalytics/web/app/base/static/assets/js/app.js +2 -0
- customeranalytics/web/app/base/static/assets/js/app.js.LICENSE +566 -0
- customeranalytics/web/app/base/static/assets/js/app.js.LICENSE.txt +556 -0
- customeranalytics/web/app/base/static/assets/js/modules/bootstrap.js +15 -0
- customeranalytics/web/app/base/static/assets/js/modules/chartjs.js +4 -0
- customeranalytics/web/app/base/static/assets/js/modules/feather.js +8 -0
- customeranalytics/web/app/base/static/assets/js/modules/flatpickr.js +4 -0
- customeranalytics/web/app/base/static/assets/js/modules/moment.js +4 -0
- customeranalytics/web/app/base/static/assets/js/modules/sidebar.js +23 -0
- customeranalytics/web/app/base/static/assets/js/modules/theme.js +16 -0
- customeranalytics/web/app/base/templates/accounts/login.html +81 -0
- customeranalytics/web/app/base/templates/accounts/register.html +80 -0
- customeranalytics/web/app/base/templates/includes/footer.html +28 -0
- customeranalytics/web/app/base/templates/includes/navigation.html +51 -0
- customeranalytics/web/app/base/templates/includes/scripts.html +3 -0
- customeranalytics/web/app/base/templates/includes/sidebar.html +146 -0
- customeranalytics/web/app/base/templates/layouts/base-fullscreen.html +36 -0
- customeranalytics/web/app/base/templates/layouts/base.html +84 -0
- customeranalytics/web/app/base/util.py +30 -0
- customeranalytics/web/app/home/__init__.py +9 -0
- customeranalytics/web/app/home/forms.py +1039 -0
- customeranalytics/web/app/home/models.py +438 -0
- customeranalytics/web/app/home/profiles.py +310 -0
- customeranalytics/web/app/home/routes.py +386 -0
- customeranalytics/web/app/home/search.py +288 -0
- customeranalytics/web/app/home/static/assets/img/avatars/avatar-1.png +0 -0
- customeranalytics/web/app/home/static/assets/img/avatars/avatar-2.png +0 -0
- customeranalytics/web/app/home/static/assets/img/avatars/avatar-3.png +0 -0
- customeranalytics/web/app/home/static/assets/img/avatars/avatar-4.png +0 -0
- customeranalytics/web/app/home/static/assets/img/avatars/avatar-5.png +0 -0
- customeranalytics/web/app/home/static/assets/img/icons/customanality.png +0 -0
- customeranalytics/web/app/home/static/assets/img/icons/icon-48x48.png +0 -0
- customeranalytics/web/app/home/templates/abtest-product.html +280 -0
- customeranalytics/web/app/home/templates/abtest-promotion.html +362 -0
- customeranalytics/web/app/home/templates/abtest-segments.html +298 -0
- customeranalytics/web/app/home/templates/add-data-delivery.html +184 -0
- customeranalytics/web/app/home/templates/add-data-product.html +177 -0
- customeranalytics/web/app/home/templates/add-data-purchase.html +392 -0
- customeranalytics/web/app/home/templates/anomaly.html +261 -0
- customeranalytics/web/app/home/templates/clv.html +211 -0
- customeranalytics/web/app/home/templates/cohorts.html +308 -0
- customeranalytics/web/app/home/templates/customer-segmentation.html +308 -0
- customeranalytics/web/app/home/templates/data-es.html +152 -0
- customeranalytics/web/app/home/templates/data-execute.html +397 -0
- customeranalytics/web/app/home/templates/delivery.html +404 -0
- customeranalytics/web/app/home/templates/funnel-customer.html +240 -0
- customeranalytics/web/app/home/templates/funnel-session.html +236 -0
- customeranalytics/web/app/home/templates/index.html +524 -0
- customeranalytics/web/app/home/templates/index2.html +515 -0
- customeranalytics/web/app/home/templates/page-403.html +46 -0
- customeranalytics/web/app/home/templates/page-404.html +45 -0
- customeranalytics/web/app/home/templates/page-500.html +45 -0
- customeranalytics/web/app/home/templates/pages-blank.html +33 -0
- customeranalytics/web/app/home/templates/pages-sign-in.html +67 -0
- customeranalytics/web/app/home/templates/pages-sign-up.html +61 -0
- customeranalytics/web/app/home/templates/product.html +203 -0
- customeranalytics/web/app/home/templates/profile.html +1009 -0
- customeranalytics/web/app/home/templates/rfm.html +254 -0
- customeranalytics/web/app/home/templates/search.html +167 -0
- customeranalytics/web/app/home/templates/settings.html +205 -0
- customeranalytics/web/app/home/templates/stats-desc.html +230 -0
- customeranalytics/web/app/home/templates/stats-purchase.html +221 -0
- customeranalytics/web/config.py +30 -0
- customeranalytics/web/db.sqlite3 +0 -0
- customeranalytics/web/run.py +30 -0
- customeranalytics-0.1.dist-info/METADATA +1102 -0
- customeranalytics-0.1.dist-info/RECORD +254 -0
- customeranalytics-0.1.dist-info/WHEEL +4 -0
- customeranalytics-0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
from os.path import join, abspath
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
from math import sqrt
|
|
5
|
+
import datetime
|
|
6
|
+
from scipy import stats
|
|
7
|
+
import pygeohash as gh
|
|
8
|
+
import random
|
|
9
|
+
import shutil
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import sys, os, inspect
|
|
13
|
+
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
|
14
|
+
parentdir = os.path.dirname(currentdir)
|
|
15
|
+
sys.path.insert(0, parentdir)
|
|
16
|
+
|
|
17
|
+
from tensorflow.keras.models import Model
|
|
18
|
+
from tensorflow.keras.layers import Input
|
|
19
|
+
from tensorflow.keras.layers import Dense
|
|
20
|
+
|
|
21
|
+
from tensorflow.keras.optimizers import Adam
|
|
22
|
+
|
|
23
|
+
from kerastuner.tuners import RandomSearch
|
|
24
|
+
from kerastuner.engine.hyperparameters import HyperParameters
|
|
25
|
+
|
|
26
|
+
from customeranalytics.data_storage_configurations.query_es import QueryES
|
|
27
|
+
from customeranalytics.configs import not_required_default_values, default_es_port, default_es_host, \
|
|
28
|
+
delivery_anomaly_model_parameters, delivery_anomaly_model_hyper_parameters, delivery_threshold_z_score
|
|
29
|
+
from customeranalytics.utils import convert_to_date, get_index_group, dimension_decision, \
|
|
30
|
+
current_date_to_day, find_week_of_monday
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DeliveryAnalytics:
|
|
34
|
+
"""
|
|
35
|
+
Delivery Analytics;
|
|
36
|
+
|
|
37
|
+
These analysis are only able to be run when delivery data source is created.
|
|
38
|
+
It detects abo-normal durations of rides according to customer location, location of order, hour of the order,
|
|
39
|
+
weekday of the order.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
def __init__(self,
|
|
43
|
+
temporary_export_path,
|
|
44
|
+
has_delivery_connection=True,
|
|
45
|
+
host=None,
|
|
46
|
+
port=None,
|
|
47
|
+
download_index='downloads',
|
|
48
|
+
order_index='orders'):
|
|
49
|
+
"""
|
|
50
|
+
******* ******** *****
|
|
51
|
+
Dimensional :
|
|
52
|
+
Delivery Analytics must be created individually for dimensions.
|
|
53
|
+
For instance, the Data set contains locations dimension.
|
|
54
|
+
In this case, each location of 'orders' and 'downloads' indexes must be created individually.
|
|
55
|
+
by using 'download_index' and 'order_index' dimension can be assigned in order to create the Delivery Analytics.
|
|
56
|
+
|
|
57
|
+
download_index; downloads_location1 this will be the location dimension of
|
|
58
|
+
parameters in order to query downloads indexes; 'location1'.
|
|
59
|
+
download_index; orders_location1 this will be the location dimension of
|
|
60
|
+
parameters in order to query orders indexes; 'location1'.
|
|
61
|
+
******* ******** *****
|
|
62
|
+
!!!
|
|
63
|
+
|
|
64
|
+
:param host: elasticsearch host
|
|
65
|
+
:param port: elasticsearch port
|
|
66
|
+
:param download_index: elasticsearch port
|
|
67
|
+
:param order_index: elasticsearch port
|
|
68
|
+
:param has_delivery_connection: is any additional delivery data source
|
|
69
|
+
"""
|
|
70
|
+
self.port = default_es_port if port is None else port
|
|
71
|
+
self.host = default_es_host if host is None else host
|
|
72
|
+
self.download_index = download_index
|
|
73
|
+
self.order_index = order_index
|
|
74
|
+
self.has_delivery_connection = has_delivery_connection
|
|
75
|
+
self.temporary_export_path = temporary_export_path
|
|
76
|
+
self.query_es = QueryES(port=self.port, host=self.host)
|
|
77
|
+
self.data = pd.DataFrame()
|
|
78
|
+
self.has_data_end_date = False
|
|
79
|
+
self.has_location_data = False
|
|
80
|
+
self.features = lambda x: list(range(1, 8)) if x == 'weekday_hour' else list(range(1, 6))
|
|
81
|
+
self.features_norm = lambda x: [str(i) + '_norm' for i in self.features(x)]
|
|
82
|
+
self.model_data = {}
|
|
83
|
+
self.centroid = []
|
|
84
|
+
self.duration_metrics = ['deliver', 'prepare', 'ride', 'returns']
|
|
85
|
+
self.anomaly_metrics = ["customer", 'location', 'weekday_hour']
|
|
86
|
+
self._model = {}
|
|
87
|
+
self.params = delivery_anomaly_model_parameters
|
|
88
|
+
self.hyper_params = delivery_anomaly_model_hyper_parameters
|
|
89
|
+
self.hp = HyperParameters()
|
|
90
|
+
self.parameter_tuning_trials = 5 # number of parameter tuning trials
|
|
91
|
+
self.delivery_fields = ['id', 'client', 'session_start_date', 'date', 'payment_amount', 'discount_amount',
|
|
92
|
+
'delivery.delivery_date', 'delivery.prepare_date',
|
|
93
|
+
'delivery.return_date', 'delivery.latitude', 'delivery.longitude']
|
|
94
|
+
self.model = {m1: {m2: {"data": pd.DataFrame(),
|
|
95
|
+
"feature_data": pd.DataFrame(),
|
|
96
|
+
"train": [],
|
|
97
|
+
"test": [],
|
|
98
|
+
'features': self.features(m1),
|
|
99
|
+
'features_norm': self.features_norm(m1),
|
|
100
|
+
'model': None,
|
|
101
|
+
'params': self.params,
|
|
102
|
+
'hyper_params': self.hyper_params}
|
|
103
|
+
for m2 in self.anomaly_metrics}
|
|
104
|
+
for m1 in self.duration_metrics}
|
|
105
|
+
self.functions = {m[0]: m[1] for m in
|
|
106
|
+
zip(self.anomaly_metrics, [self.customer_delivery_anomaly,
|
|
107
|
+
self.location_delivery_anomaly, self.weekday_hour_delivery_anomaly])}
|
|
108
|
+
|
|
109
|
+
def dimensional_query(self, boolean_query=None):
|
|
110
|
+
if dimension_decision(self.order_index):
|
|
111
|
+
if boolean_query is None:
|
|
112
|
+
boolean_query = [{"term": {"dimension": self.order_index}}]
|
|
113
|
+
else:
|
|
114
|
+
boolean_query += [{"term": {"dimension": self.order_index}}]
|
|
115
|
+
return boolean_query
|
|
116
|
+
|
|
117
|
+
def get_delivery_data(self):
|
|
118
|
+
"""
|
|
119
|
+
collect data for delivery anomaly from orders index.
|
|
120
|
+
It needs only purchased orders.
|
|
121
|
+
Delivery object needs to be split.
|
|
122
|
+
"""
|
|
123
|
+
self.query_es.query_builder(fields=self.delivery_fields,
|
|
124
|
+
boolean_queries=self.dimensional_query([{"term": {"actions.purchased": True}}]))
|
|
125
|
+
|
|
126
|
+
self.data = pd.DataFrame(self.query_es.get_data_from_es()).rename(
|
|
127
|
+
columns={'id': 'order_id', 'delivery.delivery_date': 'delivery_date',
|
|
128
|
+
'delivery.prepare_date': 'prepare_date', 'delivery.return_date': 'return_date',
|
|
129
|
+
'delivery.latitude': 'latitude', 'delivery.longitude': 'longitude'})
|
|
130
|
+
|
|
131
|
+
def data_source_date_decision(self, row, dt):
|
|
132
|
+
"""
|
|
133
|
+
Check each columns 'date', 'prepare_date', 'return_date', 'latitude', 'longitude'
|
|
134
|
+
that it is assigned as default value of them
|
|
135
|
+
|
|
136
|
+
:param row: each row at data-frame
|
|
137
|
+
:param dt: columns names 'date', 'prepare_date', 'return_date', 'latitude', 'longitude'
|
|
138
|
+
"""
|
|
139
|
+
if row[dt] == not_required_default_values.get(dt, None):
|
|
140
|
+
# get(dt, None )this is only for end_date (date) columns
|
|
141
|
+
return True
|
|
142
|
+
else:
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
def get_has_data_end_date(self):
|
|
146
|
+
"""
|
|
147
|
+
check data that has session end date.
|
|
148
|
+
Count unique end_date values and count total number of purchased sessions and calcualte the ratio
|
|
149
|
+
if the ratio is more than .1, end_date is assigned with sessions data source
|
|
150
|
+
"""
|
|
151
|
+
end_date = list(self.data['date'])
|
|
152
|
+
unique_end_date = set(end_date)
|
|
153
|
+
if len(unique_end_date) / len(end_date) > 0.1:
|
|
154
|
+
self.has_data_end_date = True
|
|
155
|
+
|
|
156
|
+
def delivery_durations(self, row):
|
|
157
|
+
"""
|
|
158
|
+
*** Duration Caşculations ***
|
|
159
|
+
|
|
160
|
+
prepare duration;
|
|
161
|
+
If prepare date is assigned it is possible to be calculated.
|
|
162
|
+
prepare = end_date - prepare_date
|
|
163
|
+
delivery duration;
|
|
164
|
+
If delivery data source is assigned delivery_date is required so, delivery durations can be calculated.
|
|
165
|
+
delivery = prepare_date - delivery_date
|
|
166
|
+
return duration;
|
|
167
|
+
If return date is assigned return duration can be calculated.
|
|
168
|
+
return = delivery_date - return_date
|
|
169
|
+
|
|
170
|
+
:param row: each row at data-frame
|
|
171
|
+
"""
|
|
172
|
+
deliver, prepare, ride, returns = ['-'] * 4
|
|
173
|
+
if self.has_data_end_date: # if there is no significant end_date, assigned as session_start_date
|
|
174
|
+
session_end = row['date']
|
|
175
|
+
else:
|
|
176
|
+
session_end = row['session_start_date']
|
|
177
|
+
|
|
178
|
+
# if there is no prepare_date assigned it as end_date.
|
|
179
|
+
# It end_date is also not existed assigned as session_start_date
|
|
180
|
+
if row['prepare_date'] is not None:
|
|
181
|
+
prepare_date = row['prepare_date']
|
|
182
|
+
ride_start_date = row['prepare_date']
|
|
183
|
+
else:
|
|
184
|
+
if self.has_data_end_date:
|
|
185
|
+
prepare_date = row['date']
|
|
186
|
+
ride_start_date = row['prepare_date']
|
|
187
|
+
else:
|
|
188
|
+
prepare_date = None
|
|
189
|
+
ride_start_date = row['session_start_date']
|
|
190
|
+
|
|
191
|
+
# If there is no return_date, assign it as None
|
|
192
|
+
if row['return_date'] is not None:
|
|
193
|
+
return_date = row['return_date']
|
|
194
|
+
else:
|
|
195
|
+
return_date = None
|
|
196
|
+
|
|
197
|
+
# delivery_date is required column for delivery dta source
|
|
198
|
+
deliver = abs(row['delivery_date'] - session_end).total_seconds() / 60
|
|
199
|
+
# if both prepare_date and end_date is not assigned, both will be assigned as session_start_date.
|
|
200
|
+
# So, If they are not assigned prepare duration will be 0
|
|
201
|
+
if prepare_date == prepare_date:
|
|
202
|
+
prepare = abs(prepare_date - session_end).total_seconds() / 60
|
|
203
|
+
ride = abs(row['delivery_date'] - ride_start_date).total_seconds() / 60
|
|
204
|
+
# return duration will be assigned as '-' if duration_date isn`t existed
|
|
205
|
+
if return_date == return_date:
|
|
206
|
+
returns = abs(row['return_date'] - row['delivery_date']).total_seconds() / 60
|
|
207
|
+
return pd.Series([deliver, prepare, ride, returns])
|
|
208
|
+
|
|
209
|
+
def encode_geo_hash(self, row):
|
|
210
|
+
"""
|
|
211
|
+
encoding locations (lat - lon) with geohash library
|
|
212
|
+
:params row: each row represents purchased transactions from orders index
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
return gh.encode(row['latitude'], row['longitude'], precision=8)
|
|
216
|
+
except Exception as e:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def parse_lat_lon(self, geohash):
|
|
220
|
+
"""
|
|
221
|
+
parsing lat - lon from tuple format
|
|
222
|
+
"""
|
|
223
|
+
return pd.Series([geohash[0], geohash[1]])
|
|
224
|
+
|
|
225
|
+
def geo_hash_perc7(self, _location, perc):
|
|
226
|
+
"""
|
|
227
|
+
GeoHashing;
|
|
228
|
+
- Optimum precision for encoding lat - lon is decided as 7.
|
|
229
|
+
- decoding each transaction with geohash precision 7.
|
|
230
|
+
- apply parse_lat_lon (encoding hashed lat - lon).
|
|
231
|
+
- In that case, locations are centralized with hashed encoded - decoded lat - lon.
|
|
232
|
+
"""
|
|
233
|
+
_location['location_perc%s' % str(perc)] = _location.apply(
|
|
234
|
+
lambda row: gh.decode(row['geohash_perc%s' % str(perc)]), axis=1)
|
|
235
|
+
_location[['lat%s' % str(perc), 'lon%s' % str(perc)]] = _location.apply(
|
|
236
|
+
lambda row: self.parse_lat_lon(row['location_perc%s' % str(perc)]), axis=1)
|
|
237
|
+
return _location
|
|
238
|
+
|
|
239
|
+
def convert_data_format(self, value, data_type):
|
|
240
|
+
"""
|
|
241
|
+
if data_type is latitude or longitude, convert the value to float, otherwise, convert the value to timestamp.
|
|
242
|
+
"""
|
|
243
|
+
if data_type in ['latitude', 'longitude']:
|
|
244
|
+
return float(value)
|
|
245
|
+
else:
|
|
246
|
+
return convert_to_date(value)
|
|
247
|
+
|
|
248
|
+
def data_manipulations(self):
|
|
249
|
+
"""
|
|
250
|
+
1. Converting date columns to timestamp.
|
|
251
|
+
2. Converting the payment_amount and the discount_amount columns to float.
|
|
252
|
+
3. apply lat - lon to geo-hash (perc=7)
|
|
253
|
+
4. create hour - isoweekday - week columns. Week column is a timestamp that only covers Mondays of the week.
|
|
254
|
+
5. Calculate Duration metrics;
|
|
255
|
+
a. Delivery Duration (required):
|
|
256
|
+
It is total duration between order purchased date and order of delivered date to the customer.
|
|
257
|
+
b. Prepare Duration (optional):
|
|
258
|
+
It is total duration between order purchased date and ordered prepared date.
|
|
259
|
+
c. Ride Duration (optional):
|
|
260
|
+
It is total duration between order purchased date and the date
|
|
261
|
+
which the delivery person returns to the first location after the order is delivered.
|
|
262
|
+
|
|
263
|
+
"""
|
|
264
|
+
for dt in ['session_start_date', 'delivery_date']:
|
|
265
|
+
self.data[dt] = self.data.apply(lambda row: convert_to_date(row[dt]), axis=1)
|
|
266
|
+
|
|
267
|
+
for dt in ['date', 'prepare_date', 'return_date', 'latitude', 'longitude']:
|
|
268
|
+
self.data[dt] = self.data.apply(
|
|
269
|
+
lambda row: self.convert_data_format(row[dt], dt)
|
|
270
|
+
if not self.data_source_date_decision(row, dt) else None, axis=1)
|
|
271
|
+
|
|
272
|
+
for a in ['payment_amount', 'discount_amount']:
|
|
273
|
+
self.data[a] = self.data[a].apply(lambda x: float(x))
|
|
274
|
+
|
|
275
|
+
# geo hash encoding-decoding precision=7
|
|
276
|
+
self.data['geohash_perc7'] = self.data.apply(lambda row: self.encode_geo_hash(row), axis=1)
|
|
277
|
+
|
|
278
|
+
# weekday and hour
|
|
279
|
+
self.data['isoweekday'] = self.data['session_start_date'].apply(lambda x: x.isoweekday())
|
|
280
|
+
self.data['hour'] = self.data['session_start_date'].apply(lambda x: x.hour)
|
|
281
|
+
self.data['week'] = self.data['session_start_date'].apply(lambda x: find_week_of_monday(x))
|
|
282
|
+
|
|
283
|
+
# duration calculations; ride, delivery, prepare
|
|
284
|
+
self.data[self.duration_metrics] = self.data.apply(lambda row: self.delivery_durations(row), axis=1)
|
|
285
|
+
|
|
286
|
+
def min_max_norm(self, value, _min, _max):
|
|
287
|
+
"""
|
|
288
|
+
Min - Max Normalization for the feature set of AutoEncoder.
|
|
289
|
+
"""
|
|
290
|
+
if abs(_max - _min) != 0:
|
|
291
|
+
return (value - _min) / abs(_max - _min)
|
|
292
|
+
else:
|
|
293
|
+
return 0
|
|
294
|
+
|
|
295
|
+
def min_max_norm_reverse(self, norm_value, _min, _max):
|
|
296
|
+
"""
|
|
297
|
+
Reverse back to actual value from the normalized values.
|
|
298
|
+
"""
|
|
299
|
+
return (norm_value * abs(_max - _min)) + _min
|
|
300
|
+
|
|
301
|
+
def create_feature_set_and_train_test_split(self):
|
|
302
|
+
"""
|
|
303
|
+
Creating feature set of a given model. Split data into the train and test sets.
|
|
304
|
+
Assigned split data to the self._main_data dictionary. After the model trained process is done,
|
|
305
|
+
it is assigned to the self.model dictionary.
|
|
306
|
+
"""
|
|
307
|
+
self._model['features_norm'] = [str(i) + '_norm' for i in range(1, 6)]
|
|
308
|
+
self._model['feature_data'] = self._model['feature_data'].reset_index(drop=True).reset_index()
|
|
309
|
+
index = list(self._model['feature_data']['index'])
|
|
310
|
+
_train_size = int(len(index) * 0.8)
|
|
311
|
+
_test_size = len(index) - _train_size
|
|
312
|
+
_train_index = random.sample(index, _train_size)
|
|
313
|
+
_test_index = list(set(index) - set(_train_index))
|
|
314
|
+
|
|
315
|
+
self._model['train'] = self._model['feature_data'].query("index in @_train_index")[self._model['features_norm']].values
|
|
316
|
+
self._model['test'] = self._model['feature_data'].query("index in @_test_index")[self._model['features_norm']].values
|
|
317
|
+
|
|
318
|
+
def build_parameter_tuning_model(self, hp):
|
|
319
|
+
"""
|
|
320
|
+
Parameter tuning model implementation. THis process can only be triggered with the keras-tuner object.
|
|
321
|
+
- Hidden Layer & Hidden Unit Decisions with an Example;
|
|
322
|
+
Hidden layer Count = 4
|
|
323
|
+
Hidden Layer Unit = 64
|
|
324
|
+
1st Hidden Layer; hid. unit = 64
|
|
325
|
+
2nd Hidden Layer; hid. unit = 64 / 2 = 32
|
|
326
|
+
3rd Hidden Layer; hid. unit = 64 / (2*2) = 16
|
|
327
|
+
"""
|
|
328
|
+
_input = Input(shape=(self._model['train'].shape[1],))
|
|
329
|
+
_unit = hp.Choice('h_l_unit', self._model['hyper_params']['h_l_unit'])
|
|
330
|
+
_layer = Dense(hp.Choice('h_l_unit', self._model['hyper_params']['h_l_unit']),
|
|
331
|
+
activation=hp.Choice('activation', self._model['hyper_params']['activation'])
|
|
332
|
+
)(_input)
|
|
333
|
+
|
|
334
|
+
# This process is decision of the hidden layer count
|
|
335
|
+
for i in range(1, hp.Choice('hidden_layer_count', self._model['hyper_params']['hidden_layer_count'])):
|
|
336
|
+
_unit = _unit / 2
|
|
337
|
+
_layer = Dense(_unit,
|
|
338
|
+
activation=hp.Choice('activation', self._model['hyper_params']['activation'])
|
|
339
|
+
)(_layer)
|
|
340
|
+
|
|
341
|
+
output = Dense(self._model['train'].shape[1], activation='sigmoid')(_layer)
|
|
342
|
+
model = Model(inputs=_input, outputs=output)
|
|
343
|
+
model.compile(loss=self._model['hyper_params']['loss'],
|
|
344
|
+
optimizer=Adam(lr=hp.Choice('lr', self._model['hyper_params']['lr'])))
|
|
345
|
+
return model
|
|
346
|
+
|
|
347
|
+
def remove_keras_tuner_folder(self):
|
|
348
|
+
"""
|
|
349
|
+
removing keras tuner file. while you need to update the parameters it will affect rerun the parameter tuning.
|
|
350
|
+
It won`t start unless the folder has been removed.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
shutil.rmtree(join(self.temporary_export_path, "delivery_anomaly"))
|
|
355
|
+
except Exception as e:
|
|
356
|
+
print(" Parameter Tuning Keras Turner dummy files have already removed!!")
|
|
357
|
+
|
|
358
|
+
def parameter_tuning(self):
|
|
359
|
+
"""
|
|
360
|
+
THis is the execution of the keras-tuner with RandomSearch
|
|
361
|
+
execution model function : self.build_parameter_tuning_model
|
|
362
|
+
hyper-parameters: check self.hyper_params
|
|
363
|
+
max_trials: check self.parameter_tuning_trials
|
|
364
|
+
|
|
365
|
+
After parameter tuning is finalized, the folder of the keras-tuner at the self.temporary_export_path is removed.
|
|
366
|
+
Parameter tuning is applied every time delivery analytics is triggered.
|
|
367
|
+
"""
|
|
368
|
+
kwargs = {'directory': join(self.temporary_export_path, "delivery_anomaly")}
|
|
369
|
+
tuner = RandomSearch(self.build_parameter_tuning_model,
|
|
370
|
+
max_trials=self.parameter_tuning_trials,
|
|
371
|
+
hyperparameters=self.hp,
|
|
372
|
+
allow_new_entries=True,
|
|
373
|
+
objective='loss', **kwargs)
|
|
374
|
+
tuner.search(x=self._model['train'],
|
|
375
|
+
y=self._model['train'],
|
|
376
|
+
epochs=5,
|
|
377
|
+
batch_size=self._model['hyper_params']['batch_size'],
|
|
378
|
+
verbose=1,
|
|
379
|
+
validation_data=(self._model['test'], self._model['test']))
|
|
380
|
+
|
|
381
|
+
for p in tuner.get_best_hyperparameters()[0].values:
|
|
382
|
+
if p in list(self._model['params'].keys()):
|
|
383
|
+
self._model['params'][p] = tuner.get_best_hyperparameters()[0].values[p]
|
|
384
|
+
self.remove_keras_tuner_folder()
|
|
385
|
+
|
|
386
|
+
def calculating_loss_function(self, X, model, features):
|
|
387
|
+
"""
|
|
388
|
+
Lost values from AutoEncoder
|
|
389
|
+
:param X: values list shape(feature count, number of sample size)
|
|
390
|
+
:param model: trained model
|
|
391
|
+
:param features: feature list
|
|
392
|
+
:return: anomaly scores
|
|
393
|
+
"""
|
|
394
|
+
y_pred, Y = model.predict(X), X
|
|
395
|
+
anomaly_calculations = list(map(lambda x: np.mean([abs(x[0][f] - x[1][f]) for f in range(len(features))]),
|
|
396
|
+
zip(y_pred, Y)))
|
|
397
|
+
return anomaly_calculations
|
|
398
|
+
|
|
399
|
+
def norm_values_outlier(self, scores):
|
|
400
|
+
"""
|
|
401
|
+
Scoring each AutoEncoder outputs with p-value of the Standart Normal Distribution
|
|
402
|
+
"""
|
|
403
|
+
mean_scores = np.mean(scores)
|
|
404
|
+
std_scores = np.std(scores)
|
|
405
|
+
standart_error = delivery_threshold_z_score * sqrt(std_scores / len(scores))
|
|
406
|
+
return mean_scores + standart_error
|
|
407
|
+
|
|
408
|
+
def detect_outliers(self, type):
|
|
409
|
+
"""
|
|
410
|
+
1. Assign each AtutoEncoder score to Anomaly Scores.
|
|
411
|
+
2. Calculate standart_error_right_tail from self.norm_values_outlier
|
|
412
|
+
3. assign label as
|
|
413
|
+
a. if standart_error_right_tail > value, anomaly = 0
|
|
414
|
+
b. if standart_error_right_tail < value, anomaly = 1
|
|
415
|
+
"""
|
|
416
|
+
self._model['feature_data']['anomaly_scores'] = self.calculating_loss_function(
|
|
417
|
+
self._model['feature_data'][self._model['features_norm']].values,
|
|
418
|
+
self._model['model'], self._model['features_norm'])
|
|
419
|
+
standart_error_right_tail = self.norm_values_outlier(list(self._model['feature_data']['anomaly_scores']))
|
|
420
|
+
self._model['feature_data'][type + '_anomaly'] = self._model['feature_data']['anomaly_scores'].apply(
|
|
421
|
+
lambda x: 1 if x > standart_error_right_tail else 0)
|
|
422
|
+
|
|
423
|
+
def build_model(self, type):
|
|
424
|
+
"""
|
|
425
|
+
Creating Auto Encoder Network with tensorfow, keras
|
|
426
|
+
"""
|
|
427
|
+
_input = Input(shape=(self._model['train'].shape[1],))
|
|
428
|
+
_unit = self._model['params']['h_l_unit']
|
|
429
|
+
_layer = Dense(self._model['params']['h_l_unit'],
|
|
430
|
+
activation=self._model['params']['activation']
|
|
431
|
+
)(_input)
|
|
432
|
+
|
|
433
|
+
for i in range(1, self._model['params']['hidden_layer_count']):
|
|
434
|
+
_unit = _unit / 2
|
|
435
|
+
_layer = Dense(_unit, activation=self._model['params']['activation'])(_layer)
|
|
436
|
+
|
|
437
|
+
output = Dense(self._model['train'].shape[1], activation='sigmoid')(_layer)
|
|
438
|
+
self._model['model'] = Model(inputs=_input, outputs=output)
|
|
439
|
+
self._model['model'].compile(loss='mse', optimizer=Adam(lr=self._model['params']['lr']), metrics=['mse'])
|
|
440
|
+
self._model['model'].fit(self._model['train'], self._model['train'],
|
|
441
|
+
epochs=int(self._model['params']['epochs']),
|
|
442
|
+
batch_size=int(self._model['params']['batch_size']),
|
|
443
|
+
verbose=True,
|
|
444
|
+
validation_split=0.2, shuffle=True)
|
|
445
|
+
|
|
446
|
+
self.detect_outliers(type)
|
|
447
|
+
|
|
448
|
+
def calculate_features_min_max_normalization(self):
|
|
449
|
+
"""
|
|
450
|
+
Applying Min - Max Normalization to the feature set
|
|
451
|
+
"""
|
|
452
|
+
max_value = self._model['feature_data'][self._model['features']].values.max()
|
|
453
|
+
min_value = self._model['feature_data'][self._model['features']].values.min()
|
|
454
|
+
for i in self._model['features']:
|
|
455
|
+
self._model['feature_data'][str(i) + '_norm'] = self._model['feature_data'][i].apply(
|
|
456
|
+
lambda x: self.min_max_norm(float(x), min_value, max_value))
|
|
457
|
+
|
|
458
|
+
def customer_delivery_anomaly(self, metric):
|
|
459
|
+
"""
|
|
460
|
+
Customer Anomaly is related to durations per location of each customer per sequential order from 1 to 5.
|
|
461
|
+
Each customer's average duration is calculated. Each sequential value from 1 to 5 is used as feature data.
|
|
462
|
+
|
|
463
|
+
Example of feature set;
|
|
464
|
+
|
|
465
|
+
customer | 1st Order | 2nd Order | 3rd Order | 4th Order | 5th Order Anomaly
|
|
466
|
+
___________________________________________________________________ _______
|
|
467
|
+
client_1 | 13.34 | 15.33 | 17.22 | 12.56 | 11.45 0
|
|
468
|
+
client_2 | 20.34 | 19.39 | 18.22 | 22.56 | 25.45 1
|
|
469
|
+
.... ..... ..... .... .... .... .... .... .... .... .... .... ....
|
|
470
|
+
.... ..... ..... .... .... .... .... .... .... .... .... .... ....
|
|
471
|
+
.... ..... ..... .... .... .... .... .... .... .... .... .... ....
|
|
472
|
+
client_22 | 13.34 | 10.39 | 12.23 | 12.55 | 11.93 0
|
|
473
|
+
client_24 | 13.59 | 11.94 | 16.40 | 12.60 | 60.12 **** 1
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
1. calculate average durations (delivery - ride - prepare) per customer per order seq.
|
|
477
|
+
2. remove client who has less than 5 orders. It might not represent the customer of correct durations and
|
|
478
|
+
the outlier ratio must be higher than expected.
|
|
479
|
+
3. apply min-max normalization, parameter tuning, train model, and anomaly score calculation.
|
|
480
|
+
|
|
481
|
+
"""
|
|
482
|
+
# create feature set with sequential values.
|
|
483
|
+
client_deliveries = self.data.groupby("client").agg(
|
|
484
|
+
{metric: "mean", "order_id": "count"}).reset_index().sort_values(by='order_id', ascending=False)
|
|
485
|
+
client_deliveries = client_deliveries.query("order_id > 5")
|
|
486
|
+
clients = list(client_deliveries['client'].unique())
|
|
487
|
+
self._model['data'] = self.data.query("client in @clients").sort_values(
|
|
488
|
+
['client', 'session_start_date'], ascending=True)
|
|
489
|
+
self._model['data']['order_seq'] = self._model['data'].sort_values(by=['client', 'session_start_date']).groupby(
|
|
490
|
+
['client']).cumcount() + 1
|
|
491
|
+
|
|
492
|
+
self._model['data'] = self._model['data'].merge(
|
|
493
|
+
self._model['data'].groupby('client').agg({"order_seq": "max"}).reset_index().rename(
|
|
494
|
+
columns={"order_seq": "order_seq_max"}),
|
|
495
|
+
on='client', how='left')
|
|
496
|
+
self._model['data']['order_seq'] = self._model['data']['order_seq_max'] - self._model['data']['order_seq']
|
|
497
|
+
self._model['data'] = self._model['data'].query("order_seq_max > 5 and order_seq < 5")
|
|
498
|
+
self._model['feature_data'] = pd.DataFrame(np.array(self._model['data'].pivot_table(columns='order_seq',
|
|
499
|
+
index='client',
|
|
500
|
+
aggfunc={metric: "mean"}
|
|
501
|
+
).reset_index())).rename(
|
|
502
|
+
columns={0: "client"}).reset_index(drop=True).reset_index().fillna("-")
|
|
503
|
+
|
|
504
|
+
# remove non-numeric values from data set
|
|
505
|
+
for i in self._model['features']:
|
|
506
|
+
self._model['feature_data'] = self._model['feature_data'][self._model['feature_data'][i] != '-']
|
|
507
|
+
|
|
508
|
+
self.calculate_features_min_max_normalization() # normalization of the feature set
|
|
509
|
+
self.create_feature_set_and_train_test_split() # train - test split for train model
|
|
510
|
+
self.parameter_tuning() # parameter tuning
|
|
511
|
+
self.build_model('customer') # train model
|
|
512
|
+
self.model[metric]['customer'] = self._model # assigned model to self.model
|
|
513
|
+
|
|
514
|
+
def location_delivery_anomaly(self, metric):
|
|
515
|
+
"""
|
|
516
|
+
Location-based anomaly is calculated with geo-hashed decoded locations with perc=7 and week of the transactions.
|
|
517
|
+
Basically, the feature set represents each location (geo-hash prec=7) of the last 4 weeks of average durations.
|
|
518
|
+
|
|
519
|
+
Example of feature set;
|
|
520
|
+
|
|
521
|
+
gehashed_prec_7 | last week | 2 week before | 3 week before | 4 week before Anomaly
|
|
522
|
+
____________________________________________________________________________
|
|
523
|
+
34.092349 - 43.092349 | 13.34 | 15.33 | 17.22 | 12.56 0
|
|
524
|
+
34.023434 - 43.234342 | 20.34 | 19.39 | 18.22 | 22.56 1
|
|
525
|
+
.... ... .. ..... .... .... .... .... .... .. .. .... ...
|
|
526
|
+
.... ... .. ..... .... .... .... .... .... .. .. .... ...
|
|
527
|
+
.... ... .. ..... .... .... .... .... .... .. .. .... ...
|
|
528
|
+
34.222332 - 43.023424 | 13.34 | 10.39 | 12.23 | 12.55 0
|
|
529
|
+
34.932423 - 43.234444 | 13.59 | 11.94 | 16.40 | 60.60 **** 1
|
|
530
|
+
|
|
531
|
+
"""
|
|
532
|
+
self._model['data'] = self.data.groupby(["week", 'geohash_perc7']).agg({metric: "mean"}).reset_index()
|
|
533
|
+
self._model['data']['week_seq'] = self._model['data'].sort_values(by=['geohash_perc7', 'week']).groupby(
|
|
534
|
+
['geohash_perc7']).cumcount() + 1
|
|
535
|
+
|
|
536
|
+
locations_max_weeks = self._model['data'].groupby("geohash_perc7").agg(
|
|
537
|
+
{"week_seq": "max"}).reset_index().rename(columns={"week_seq": "week_seq_max"})
|
|
538
|
+
self._model['data'] = self._model['data'].merge(locations_max_weeks, on='geohash_perc7', how='left')
|
|
539
|
+
self._model['data']['week_seq'] = self._model['data']['week_seq_max'] - self._model['data']['week_seq']
|
|
540
|
+
|
|
541
|
+
self._model['feature_data'] = pd.DataFrame(np.array(self._model['data'].query("week_seq < 5").pivot_table(
|
|
542
|
+
columns="week_seq",
|
|
543
|
+
index='geohash_perc7',
|
|
544
|
+
aggfunc={metric: "max"}).reset_index())).rename(columns={0: "location"})
|
|
545
|
+
|
|
546
|
+
for f in self._model['features']:
|
|
547
|
+
mean_duration = self._model['feature_data'][self._model['feature_data'][f] == self._model['feature_data'][f]]
|
|
548
|
+
mean_duration = mean_duration[self._model['features']].values.mean()
|
|
549
|
+
self._model['feature_data'] = self._model['feature_data'].fillna(mean_duration)
|
|
550
|
+
|
|
551
|
+
self.calculate_features_min_max_normalization() # normalization of the feature set
|
|
552
|
+
self.create_feature_set_and_train_test_split() # train - test split for train model
|
|
553
|
+
self.parameter_tuning() # parameter tuning
|
|
554
|
+
self.build_model('location') # train model
|
|
555
|
+
self.model[metric]['location'] = self._model # assigned model to self.model
|
|
556
|
+
|
|
557
|
+
def weekday_hour_delivery_anomaly(self, metric):
|
|
558
|
+
"""
|
|
559
|
+
Weekday - hour Anomaly Detection process;
|
|
560
|
+
1. calculate average durations per hour per weekday
|
|
561
|
+
2. assign Min-Max Normalization for the Average of Durations breakdown with weekday - hour
|
|
562
|
+
3. assign abnormal if the normalized value more than 0.9.
|
|
563
|
+
"""
|
|
564
|
+
self._model['feature_data'] = self.data.groupby(["isoweekday", "hour"]).agg(
|
|
565
|
+
{metric: "mean"}).reset_index().rename(columns={metric: "weekday_hour_norm"})
|
|
566
|
+
max_value = self._model['feature_data'][["weekday_hour_norm"]].values.max()
|
|
567
|
+
min_value = self._model['feature_data'][["weekday_hour_norm"]].values.min()
|
|
568
|
+
self._model['feature_data']['weekday_hour_norm'] = self._model['feature_data']['weekday_hour_norm'].apply(
|
|
569
|
+
lambda x: self.min_max_norm(float(x), min_value, max_value))
|
|
570
|
+
|
|
571
|
+
self._model['feature_data']['weekday_hour_anomaly'] = self._model['feature_data']['weekday_hour_norm'].apply(
|
|
572
|
+
lambda x: 1 if x > 0.9 else 0)
|
|
573
|
+
|
|
574
|
+
def merge_type_anomaly_data(self, data, metric):
|
|
575
|
+
"""
|
|
576
|
+
Customer Anomaly Detection is applied for customer.
|
|
577
|
+
Location Anomaly Detection is applied for Location (geo-hashed prec=7).
|
|
578
|
+
Weekday Anomaly Detection is applied for weekday per hour.
|
|
579
|
+
This process is merged these detection into the each transaction. Then, decides for the abnormal transactions.
|
|
580
|
+
Rules of the Anomaly Decision;
|
|
581
|
+
- Rule 1; If 3 anomaly detection cases are positive, assign transaction as Abnormal Transaction.
|
|
582
|
+
- Rule 2; If location base or customer base are positive and weekday - hour case is positive,
|
|
583
|
+
assign as Abnormal Transaction.
|
|
584
|
+
"""
|
|
585
|
+
# customers of detected abnormal transactions
|
|
586
|
+
data = data.merge(self.model[metric]['customer']['feature_data'][['client', 'customer_anomaly']],
|
|
587
|
+
on='client', how='left')
|
|
588
|
+
|
|
589
|
+
# locations (geo-hashed (perc=7)) of detected abnormal transactions
|
|
590
|
+
if self.has_location_data:
|
|
591
|
+
data = data.merge(
|
|
592
|
+
self.model[metric]['location']['feature_data'].rename(
|
|
593
|
+
columns={"location": "geohash_perc7"})[['geohash_perc7', 'location_anomaly']],
|
|
594
|
+
on='geohash_perc7', how='left')
|
|
595
|
+
else:
|
|
596
|
+
data['location_anomaly'] = 0
|
|
597
|
+
|
|
598
|
+
# weekday - hour of detected abnormal transactions
|
|
599
|
+
data = data.merge(self.model[metric]['weekday_hour']['feature_data']
|
|
600
|
+
[['hour', 'isoweekday', 'weekday_hour_anomaly']], on=['hour', 'isoweekday'], how='left')
|
|
601
|
+
|
|
602
|
+
data['anomaly_totals'] = data['customer_anomaly'] + data['location_anomaly'] + data['weekday_hour_anomaly']
|
|
603
|
+
data = data.query("anomaly_totals == 3 or ((customer_anomaly == 1 and weekday_hour_anomaly == 1) or (location_anomaly == 1 and weekday_hour_anomaly == 1))")
|
|
604
|
+
return data[['session_start_date', 'hour', 'isoweekday', 'latitude', 'longitude',
|
|
605
|
+
metric, 'client', 'customer_anomaly', 'location_anomaly', 'weekday_hour_anomaly']]
|
|
606
|
+
|
|
607
|
+
def decision_for_location_anomaly(self):
|
|
608
|
+
"""
|
|
609
|
+
Has Delivery Data Source the latitude and longitude values?
|
|
610
|
+
"""
|
|
611
|
+
if len(list(self.data['latitude'].unique())) > 1 or len(list(self.data['latitude'].unique())) > 1:
|
|
612
|
+
self.has_location_data = True
|
|
613
|
+
else:
|
|
614
|
+
self.anomaly_metrics = list(set(self.anomaly_metrics) - {'location'})
|
|
615
|
+
|
|
616
|
+
def delivery_kpis(self):
|
|
617
|
+
"""
|
|
618
|
+
Delivery KPIs;
|
|
619
|
+
|
|
620
|
+
General metrics in order to follow up on how the delivery system is running at the business.
|
|
621
|
+
- Average Delivery Duration (min); Average delivery duration of all purchased transactions.
|
|
622
|
+
- Average Prepare Duration (min); Average prepare duration of all purchased transactions.
|
|
623
|
+
- Average Ride Duration (min); Average ride duration of all purchased transactions.
|
|
624
|
+
- Average Return Duration (min); Average return duration of all purchased transactions.
|
|
625
|
+
- Total Number Location (min); Total delivered locations of all purchased transactions.
|
|
626
|
+
"""
|
|
627
|
+
kpis = {}
|
|
628
|
+
for m in self.duration_metrics:
|
|
629
|
+
kpis[m] = np.mean(self.data[m])
|
|
630
|
+
|
|
631
|
+
self.data['locations'] = self.data.apply(lambda row: "_".join([str(row['latitude']), str(row['longitude'])]), axis=1)
|
|
632
|
+
kpis['total_locations'] = len(np.unique(self.data['locations']))
|
|
633
|
+
return pd.DataFrame([kpis])
|
|
634
|
+
|
|
635
|
+
def execute_delivery_analysis(self):
|
|
636
|
+
"""
|
|
637
|
+
1. Check if there is delivery data source.
|
|
638
|
+
2. collect the delivery data with deliver - return - prepare dates and lat - lon values.
|
|
639
|
+
3. check for the end date which indicates session purchase date.
|
|
640
|
+
4. assign data_manipulations. (check self.data_manipulations for details)
|
|
641
|
+
5. Check if there is prepare - return date is assigned. Otherwise, Location Anomaly Detection is not triggered.
|
|
642
|
+
6. Anomaly Detection for Duration Metrics (Deliver - Prepare - Ride):
|
|
643
|
+
Each metric (Deliver - Prepare - Ride) of Anomaly Detection is applied seperatelly.
|
|
644
|
+
For each metric, customer - location - weekday - hour base anomaly detection is calculated
|
|
645
|
+
and decide for transactions of abnormal values individually.
|
|
646
|
+
7. Insert each metric of abnormal transactions to to reports index with anomaly_type.
|
|
647
|
+
8. If delivery data source has latitude-longitude values, insert abnormal transactions with the lats - lons.
|
|
648
|
+
9. Insert Normalized values of average durations per weekday per hour to the reports index.
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
"""
|
|
652
|
+
if self.has_delivery_connection:
|
|
653
|
+
self.get_delivery_data()
|
|
654
|
+
self.get_has_data_end_date()
|
|
655
|
+
self.data_manipulations()
|
|
656
|
+
self.decision_for_location_anomaly()
|
|
657
|
+
|
|
658
|
+
# anomaly calculations
|
|
659
|
+
for metric in self.duration_metrics:
|
|
660
|
+
for type in self.anomaly_metrics:
|
|
661
|
+
print("type :", type, " || metric :", metric)
|
|
662
|
+
self._model = self.model[metric][type]
|
|
663
|
+
self.functions[type](metric)
|
|
664
|
+
_result = self.merge_type_anomaly_data(self.data, metric)
|
|
665
|
+
self.insert_into_reports_index(delivery_anomaly=_result, anomaly_type=metric, index=self.order_index)
|
|
666
|
+
|
|
667
|
+
# visualization and analytics calculation
|
|
668
|
+
for metric in self.duration_metrics:
|
|
669
|
+
self.insert_into_reports_index(delivery_anomaly=self.model[metric]['weekday_hour']['feature_data'],
|
|
670
|
+
anomaly_type=metric + '_weekday_hour', index=self.order_index)
|
|
671
|
+
|
|
672
|
+
if self.has_location_data:
|
|
673
|
+
for metric in self.duration_metrics:
|
|
674
|
+
_location = self.model[metric]['location']['data']
|
|
675
|
+
_location = self.geo_hash_perc7(_location, perc=7)
|
|
676
|
+
if sum(_location[metric]) != 0:
|
|
677
|
+
self.insert_into_reports_index(delivery_anomaly=_location,
|
|
678
|
+
anomaly_type=metric + '_location', index=self.order_index)
|
|
679
|
+
# delivery KPIs
|
|
680
|
+
self.insert_into_reports_index(delivery_anomaly=self.delivery_kpis(),
|
|
681
|
+
anomaly_type='deliver_kpis', index=self.order_index)
|
|
682
|
+
|
|
683
|
+
def insert_into_reports_index(self, delivery_anomaly, start_date=None, anomaly_type='ride', index='orders'):
|
|
684
|
+
"""
|
|
685
|
+
via query_es.py, each report can be inserted into the reports index with the given format.
|
|
686
|
+
{"id": unique report id,
|
|
687
|
+
"report_date": start_date or current date,
|
|
688
|
+
"report_name": "delivery_anomaly",
|
|
689
|
+
"index": "main",
|
|
690
|
+
"report_types": {
|
|
691
|
+
"type": anomaly_type; 'deliver', 'prepare', 'ride', 'returns'
|
|
692
|
+
},
|
|
693
|
+
"data": delivery_anomaly
|
|
694
|
+
}
|
|
695
|
+
:param delivery_anomaly: data set, data frame
|
|
696
|
+
:param start_date: data start date
|
|
697
|
+
:param anomaly_type: data types; 'deliver', 'prepare', 'ride', 'returns'
|
|
698
|
+
:param index: dimentionality of data index orders_location1 ; dimension = location1
|
|
699
|
+
"""
|
|
700
|
+
list_of_obj = [{"id": np.random.randint(200000000),
|
|
701
|
+
"report_date": current_date_to_day().isoformat() if start_date is None else start_date,
|
|
702
|
+
"report_name": "delivery_anomaly",
|
|
703
|
+
"index": get_index_group(index),
|
|
704
|
+
"report_types": {"type": anomaly_type},
|
|
705
|
+
"data": delivery_anomaly.fillna(0).to_dict("records")}]
|
|
706
|
+
self.query_es.insert_data_to_index(list_of_obj, index='reports')
|
|
707
|
+
|
|
708
|
+
def fetch(self, anomaly_type, start_date=None):
|
|
709
|
+
"""
|
|
710
|
+
This allows us to query the created delivery_anomaly reports.
|
|
711
|
+
anomaly_type is crucial for us to collect the correct filters.
|
|
712
|
+
Example of queries;
|
|
713
|
+
- anomaly_type: delivery_anomaly,
|
|
714
|
+
- start_date: 2021-01-01T00:00:00
|
|
715
|
+
|
|
716
|
+
{'size': 10000000,
|
|
717
|
+
'from': 0,
|
|
718
|
+
'_source': True,
|
|
719
|
+
'query': {'bool': {'must': [
|
|
720
|
+
{'term': {'report_name': 'delivery_anomaly'}},
|
|
721
|
+
{"term": {"index": "orders_location1"}}
|
|
722
|
+
{'term': {'report_types.type': 'ride'}},
|
|
723
|
+
{'range': {'report_date': {'lt': '2021-04-01T00:00:00'}}}]}}}
|
|
724
|
+
|
|
725
|
+
- start date will be filtered from data frame. In this example; .query("daily > @start_date")
|
|
726
|
+
|
|
727
|
+
:param anomaly_type: 'deliver', 'prepare', 'ride', 'returns'
|
|
728
|
+
:param start_date: delivery anomaly executed date
|
|
729
|
+
:param index: index_name in order to get dimension_of data. If there is no dimension, no need to be assigned
|
|
730
|
+
:return: data frame
|
|
731
|
+
"""
|
|
732
|
+
boolean_queries, date_queries = [], []
|
|
733
|
+
boolean_queries = [{"term": {"report_name": 'delivery_anomaly'}},
|
|
734
|
+
{"term": {"index": get_index_group(self.order_index)}},
|
|
735
|
+
{"term": {"report_types.type": anomaly_type}},
|
|
736
|
+
{'range': {'report_date': {
|
|
737
|
+
'lt': current_date_to_day().isoformat() if start_date is None else start_date}}}]
|
|
738
|
+
|
|
739
|
+
self.query_es = QueryES(port=self.port,
|
|
740
|
+
host=self.host)
|
|
741
|
+
self.query_es.query_builder(fields=None, _source=True,
|
|
742
|
+
date_queries=date_queries,
|
|
743
|
+
boolean_queries=boolean_queries)
|
|
744
|
+
_res = self.query_es.get_data_from_es(index="reports")
|
|
745
|
+
return pd.DataFrame(_res[0]['_source']['data'])
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
|