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,301 @@
|
|
|
1
|
+
import sys, os, inspect
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import glob
|
|
5
|
+
from clv.executor import CLV
|
|
6
|
+
from sqlalchemy import create_engine, MetaData
|
|
7
|
+
|
|
8
|
+
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
|
9
|
+
parentdir = os.path.dirname(currentdir)
|
|
10
|
+
sys.path.insert(0, parentdir)
|
|
11
|
+
|
|
12
|
+
from customeranalytics.configs import default_es_port, default_es_host, max_elasticsearch_bulk_insert_bytes, default_es_bulk_insert_chunk_size
|
|
13
|
+
from customeranalytics.utils import *
|
|
14
|
+
from customeranalytics.data_storage_configurations.query_es import QueryES
|
|
15
|
+
from customeranalytics.data_storage_configurations.reports import Reports
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
engine = create_engine('sqlite://///' + join(abspath_for_sample_data(), "web", 'db.sqlite3'), connect_args={'check_same_thread': False})
|
|
19
|
+
metadata = MetaData()
|
|
20
|
+
con = engine.connect()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CLVPrediction:
|
|
24
|
+
"""
|
|
25
|
+
Customer Lifetime Value Prediction;
|
|
26
|
+
For more information pls check; https://github.com/caglanakpinar/clv_prediction
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
def __init__(self,
|
|
31
|
+
temporary_export_path,
|
|
32
|
+
host=None,
|
|
33
|
+
port=None,
|
|
34
|
+
time_period='weekly',
|
|
35
|
+
download_index='downloads',
|
|
36
|
+
order_index='orders'):
|
|
37
|
+
"""
|
|
38
|
+
******* ******** *****
|
|
39
|
+
Dimensional CLV Prediction:
|
|
40
|
+
Descriptive Statistics must be created individually for dimensions.
|
|
41
|
+
For instance, the Data set contains locations dimension.
|
|
42
|
+
In this case, each location of 'orders' and 'downloads' indexes must be created individually.
|
|
43
|
+
by using 'download_index' and 'order_index' dimension can be assigned in order to create the CLV Prediction.
|
|
44
|
+
|
|
45
|
+
download_index; downloads_location1 this will be the location dimension of
|
|
46
|
+
parameters in order to query downloads indexes; 'location1'.
|
|
47
|
+
download_index; orders_location1 this will be the location dimension of
|
|
48
|
+
parameters in order to query orders indexes; 'location1'.
|
|
49
|
+
******* ******** *****
|
|
50
|
+
!!!
|
|
51
|
+
|
|
52
|
+
:param host: elasticsearch host
|
|
53
|
+
:param port: elasticsearch port
|
|
54
|
+
:param download_index: elasticsearch port
|
|
55
|
+
:param order_index: elasticsearch port
|
|
56
|
+
"""
|
|
57
|
+
self.port = default_es_port if port is None else port
|
|
58
|
+
self.host = default_es_host if host is None else host
|
|
59
|
+
self.download_index = download_index
|
|
60
|
+
self.order_index = order_index
|
|
61
|
+
self.time_period = time_period
|
|
62
|
+
self.clv = None
|
|
63
|
+
self.query_es = QueryES(port=port, host=host)
|
|
64
|
+
self.path = temporary_export_path
|
|
65
|
+
self.temp_csv_file = join(temporary_export_path, "temp_data.csv")
|
|
66
|
+
self.temp_folder = join(temporary_export_path, "temp_purchase_amount_results", "*.csv")
|
|
67
|
+
self.results = join(temporary_export_path, "results_*.csv")
|
|
68
|
+
self.clv_fields = ["client", "session_start_date", "payment_amount", "dimension"]
|
|
69
|
+
self.result_query = "session_start_date == session_start_date and data_type != 'actual'"
|
|
70
|
+
self.result_columns = ['session_start_date', 'client', 'payment_amount']
|
|
71
|
+
self.clv_predictions = pd.DataFrame()
|
|
72
|
+
self.client_dimensions = pd.DataFrame()
|
|
73
|
+
self.collect_reports = Reports()
|
|
74
|
+
self.has_prev_clv = False
|
|
75
|
+
|
|
76
|
+
def get_customers_dimesions(self, _data):
|
|
77
|
+
_data["dimension"] = _data["dimension"].fillna("main")
|
|
78
|
+
if list(_data['dimension'].unique()) != 1:
|
|
79
|
+
_data_pv = _data.groupby(["client", "dimension"]).agg({"session_start_date": "count"}).reset_index().rename(
|
|
80
|
+
columns={"session_start_date": "order_count"})
|
|
81
|
+
_data_pv['order_seq_num'] = _data_pv.sort_values(
|
|
82
|
+
by=["client", "dimension", "order_count"], ascending=False).groupby(["client"]).cumcount() + 1
|
|
83
|
+
_data_pv = _data_pv.query("order_seq_num == 1")
|
|
84
|
+
_data_pv = _data_pv[["client", "dimension"]].groupby("client").agg({"dimension": "first"}).reset_index()
|
|
85
|
+
self.client_dimensions = pd.merge(_data.drop('dimension', axis=1), _data_pv, on='client', how='left')
|
|
86
|
+
|
|
87
|
+
def get_orders_data(self, end_date):
|
|
88
|
+
"""
|
|
89
|
+
Purchased orders are collected from Orders Index.
|
|
90
|
+
Session_start_date, client, payment_amount are needed for initializing CLv Prediciton.
|
|
91
|
+
:param end_date: final date to start clv prediction
|
|
92
|
+
:return:
|
|
93
|
+
"""
|
|
94
|
+
self.query_es = QueryES(port=self.port, host=self.host)
|
|
95
|
+
self.query_es.date_queries_builder({"session_start_date": {"lt": end_date}})
|
|
96
|
+
self.query_es.query_builder(fields=self.clv_fields,
|
|
97
|
+
boolean_queries=[{"term": {"actions.purchased": True}}])
|
|
98
|
+
_data = pd.DataFrame(self.query_es.get_data_from_es())
|
|
99
|
+
_data['session_start_date'] = _data['session_start_date'].apply(lambda x: str(convert_to_date(x)))
|
|
100
|
+
self.get_customers_dimesions(_data)
|
|
101
|
+
_data.query("session_start_date == session_start_date").to_csv(self.temp_csv_file, index=False)
|
|
102
|
+
|
|
103
|
+
def check_for_previous_clv_prediction(self):
|
|
104
|
+
"""
|
|
105
|
+
check if the last clv prediction is applied in 7 days.
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
|
|
109
|
+
_reports = self.collect_reports.collect_reports(self.port, self.host, 'main',
|
|
110
|
+
query={"report_name": "clv_prediction"})
|
|
111
|
+
if len(_reports) != 0:
|
|
112
|
+
try:
|
|
113
|
+
_directory = pd.read_sql("SELECT * FROM es_connection", con).to_dict('records')[-1]['directory']
|
|
114
|
+
print(_directory)
|
|
115
|
+
clv_report = pd.read_csv(join(_directory, "build_in_reports", "main", "daily_clv.csv"))
|
|
116
|
+
if len(clv_report) != 0:
|
|
117
|
+
clv_report = clv_report.query("data_type == 'prediction'")
|
|
118
|
+
clv_report['date'] = clv_report['date'].apply(lambda x: convert_to_day(x))
|
|
119
|
+
last_transaction_date = min(list(clv_report['date']))
|
|
120
|
+
print(last_transaction_date)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(e)
|
|
123
|
+
_reports['report_date'] = _reports['report_date'].apply(lambda x: convert_to_date(x))
|
|
124
|
+
last_transaction_date = max(list(_reports['report_date']))
|
|
125
|
+
|
|
126
|
+
if abs(last_transaction_date - current_date_to_day()).total_seconds() / 60 / 60 / 24 < 7:
|
|
127
|
+
print("clv predictions will be updated!!!!!")
|
|
128
|
+
self.has_prev_clv = True
|
|
129
|
+
except: pass
|
|
130
|
+
|
|
131
|
+
def insert_into_reports_index(self, clv_predictions, time_period, date=None, index='orders'):
|
|
132
|
+
"""
|
|
133
|
+
via query_es.py, each report can be inserted into the reports index with the given format.
|
|
134
|
+
{"id": unique report id,
|
|
135
|
+
"report_date": date or current date,
|
|
136
|
+
"report_name": "clv_prediction",
|
|
137
|
+
"index": "main",
|
|
138
|
+
"report_types": {"time_period": weekly, monthly, daily },
|
|
139
|
+
"data": segments.fillna(0.0).to_dict("records") - dataframe to list of dictionary
|
|
140
|
+
}
|
|
141
|
+
!!! null values are assigned to 0.
|
|
142
|
+
|
|
143
|
+
:param segments: data set, data frame
|
|
144
|
+
:param index: dimensionality of data index orders_location1 ; dimension = location1
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
iteration = int(len(clv_predictions) / default_es_bulk_insert_chunk_size) + 1
|
|
148
|
+
print("number of bulk iteration :", iteration)
|
|
149
|
+
print("bulk size :", default_es_bulk_insert_chunk_size)
|
|
150
|
+
for i in range(iteration):
|
|
151
|
+
_start = i * default_es_bulk_insert_chunk_size
|
|
152
|
+
_end = ((i+1) * default_es_bulk_insert_chunk_size) if i != (iteration - 1) else len(clv_predictions) + 1
|
|
153
|
+
_clv_predictions = clv_predictions.iloc[_start:_end]
|
|
154
|
+
_list_of_obj = [{"id": np.random.randint(200000000),
|
|
155
|
+
"report_date": current_date_to_day().isoformat() if date is None else convert_to_day(
|
|
156
|
+
date).isoformat(),
|
|
157
|
+
"report_name": "clv_prediction",
|
|
158
|
+
"index": get_index_group(index),
|
|
159
|
+
"report_types": {"time_period": time_period, "partition": i},
|
|
160
|
+
"data": _clv_predictions.fillna(0.0).to_dict("records")}]
|
|
161
|
+
self.query_es.insert_data_to_index(_list_of_obj, index='reports')
|
|
162
|
+
|
|
163
|
+
def convert_time_period(self, period):
|
|
164
|
+
"""
|
|
165
|
+
clv_prediction library takes time-period as week, day, month.
|
|
166
|
+
This converts the general time-period into the clv_prediction form.
|
|
167
|
+
:param period: weekly or daily or month.
|
|
168
|
+
:return: week or day or month
|
|
169
|
+
"""
|
|
170
|
+
if period == 'weekly':
|
|
171
|
+
return 'week'
|
|
172
|
+
if period == 'daily':
|
|
173
|
+
return 'day'
|
|
174
|
+
if period == 'monthly':
|
|
175
|
+
return 'month'
|
|
176
|
+
if period == '6 months':
|
|
177
|
+
return '6*month'
|
|
178
|
+
|
|
179
|
+
def remove_temp_files(self):
|
|
180
|
+
"""
|
|
181
|
+
Removing temp_data.csv, .csv files in temp_purchase_amount_results and results*.csv files
|
|
182
|
+
"""
|
|
183
|
+
# remove temp_data.csv
|
|
184
|
+
try:
|
|
185
|
+
os.unlink(self.temp_csv_file)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
print("no file is observed!!!")
|
|
188
|
+
|
|
189
|
+
# remove results*.csv files
|
|
190
|
+
for f in glob.glob(self.results):
|
|
191
|
+
print(f)
|
|
192
|
+
try:
|
|
193
|
+
os.unlink(f)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
print(e)
|
|
196
|
+
print("no file is observed!!!")
|
|
197
|
+
|
|
198
|
+
# remove .csv files in temp_purchase_amount_results
|
|
199
|
+
for f in glob.glob(self.temp_folder):
|
|
200
|
+
try:
|
|
201
|
+
os.unlink(f)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
print(e)
|
|
204
|
+
print("no file is observed!!!")
|
|
205
|
+
|
|
206
|
+
def execute_clv(self, start_date, job='train_prediction', time_period='6 months'):
|
|
207
|
+
"""
|
|
208
|
+
1. train clv prediction models
|
|
209
|
+
For more details about models pls check; https://github.com/caglanakpinar/clv_prediction
|
|
210
|
+
2. predict users of feature payment amount via using built models.
|
|
211
|
+
3. use 'train_prediction' for the job argument which allows us to train and predict the results.
|
|
212
|
+
If time period is weekly and if there is a stored .json and .h5 trained models in last 7 days,
|
|
213
|
+
skips the train process and uses the latest trained model.
|
|
214
|
+
4. Each model and stored in the directory at elasticsearch folder. After it is used, it is removed.
|
|
215
|
+
5. If last stored clv prediction is applied before 7 days, it is not triggered for prediction again.
|
|
216
|
+
|
|
217
|
+
:param start_date: date of start for clv prediction
|
|
218
|
+
:param job: train or prediction
|
|
219
|
+
:param time_period: weekly, daily, monthly
|
|
220
|
+
"""
|
|
221
|
+
start_date = str(current_date_to_day())[0:10] if start_date is None else start_date
|
|
222
|
+
self.check_for_previous_clv_prediction()
|
|
223
|
+
if not self.has_prev_clv:
|
|
224
|
+
self.get_orders_data(end_date=start_date)
|
|
225
|
+
self.clv = CLV(customer_indicator="client",
|
|
226
|
+
amount_indicator="payment_amount",
|
|
227
|
+
job=job,
|
|
228
|
+
date=start_date,
|
|
229
|
+
data_source='csv',
|
|
230
|
+
data_query_path=self.temp_csv_file,
|
|
231
|
+
time_period=self.convert_time_period(time_period),
|
|
232
|
+
time_indicator="session_start_date",
|
|
233
|
+
export_path=self.path)
|
|
234
|
+
self.clv.clv_prediction()
|
|
235
|
+
self.clv_predictions = self.clv.get_result_data().query(self.result_query)[self.result_columns]
|
|
236
|
+
self.clv_predictions = self.clv_predictions.rename(columns={"session_start_date": "date"})
|
|
237
|
+
self.clv_predictions['date'] = self.clv_predictions['date'].apply(lambda x: convert_to_iso_format(x))
|
|
238
|
+
self.add_dimensions()
|
|
239
|
+
self.clv_predictions = self.clv_predictions[['dimension', 'date', 'payment_amount', 'client']]
|
|
240
|
+
self.insert_into_reports_index(self.clv_predictions,
|
|
241
|
+
time_period=time_period,
|
|
242
|
+
date=start_date,
|
|
243
|
+
index=self.order_index)
|
|
244
|
+
del self.clv
|
|
245
|
+
del self.clv_predictions
|
|
246
|
+
self.clv = None
|
|
247
|
+
self.clv_predictions = pd.DataFrame()
|
|
248
|
+
self.remove_temp_files()
|
|
249
|
+
|
|
250
|
+
def add_dimensions(self):
|
|
251
|
+
_prediction_start_date = min(self.clv_predictions['date'])
|
|
252
|
+
if len(self.client_dimensions) != 0:
|
|
253
|
+
_clv_predictions_newcomers = self.clv_predictions.query("client == 'newcomers'")
|
|
254
|
+
print(_clv_predictions_newcomers.head())
|
|
255
|
+
self.clv_predictions = pd.merge(self.clv_predictions.query("client != 'newcomers'"),
|
|
256
|
+
self.client_dimensions[['client', 'dimension']], on="client", how="left")
|
|
257
|
+
# add newcomers of dimensions
|
|
258
|
+
_dimension_user_ratios = self.clv_predictions.groupby('dimension').agg(
|
|
259
|
+
{'client': lambda x: len(np.unique(x))}).reset_index().rename(columns={'client': 'ratio'})
|
|
260
|
+
_dimension_user_ratios['ratio'] = _dimension_user_ratios['ratio'] / len(
|
|
261
|
+
self.clv_predictions['client'].unique())
|
|
262
|
+
_clv_predictions_newcomers = _clv_predictions_newcomers.groupby("date").agg(
|
|
263
|
+
{"payment_amount": "sum"}).reset_index().rename(columns={'payment_amount': 'pa'})
|
|
264
|
+
_clv_predictions_newcomers['client'] = 'newcomers'
|
|
265
|
+
dfs = []
|
|
266
|
+
for _ratio in list(_dimension_user_ratios['ratio']):
|
|
267
|
+
_clv_predictions_newcomers['payment_amount'] = _clv_predictions_newcomers['pa'] * _ratio
|
|
268
|
+
dfs.append(_clv_predictions_newcomers)
|
|
269
|
+
self.clv_predictions = pd.concat([self.clv_predictions.query("client != 'newcomers'")] + dfs)
|
|
270
|
+
else: self.clv_predictions['dimension'] = 'main'
|
|
271
|
+
self.clv_predictions['dimension'] = self.clv_predictions['dimension'].apply(lambda x: str(x))
|
|
272
|
+
self.clv_predictions = self.clv_predictions.query("date > @_prediction_start_date")
|
|
273
|
+
|
|
274
|
+
def fetch(self, end_date=None, time_period='weekly'):
|
|
275
|
+
"""
|
|
276
|
+
Collect CLV Prediction results with the given date.
|
|
277
|
+
:param end_date: final date to start clv prediction
|
|
278
|
+
:param time_period: weekly or daily or month.
|
|
279
|
+
:return:
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
boolean_queries, date_queries = [], []
|
|
283
|
+
boolean_queries = [{"term": {"report_name": "clv_prediction"}},
|
|
284
|
+
{"term": {"report_types.time_period": time_period}},
|
|
285
|
+
{"term": {"index": get_index_group(self.order_index)}}]
|
|
286
|
+
|
|
287
|
+
if end_date is not None:
|
|
288
|
+
date_queries = [{"range": {"report_date": {"lt": convert_to_iso_format(end_date)}}}]
|
|
289
|
+
|
|
290
|
+
self.query_es = QueryES(port=self.port,
|
|
291
|
+
host=self.host)
|
|
292
|
+
self.query_es.query_builder(fields=None, _source=True,
|
|
293
|
+
date_queries=date_queries,
|
|
294
|
+
boolean_queries=boolean_queries)
|
|
295
|
+
_res = self.query_es.get_data_from_es(index="reports")
|
|
296
|
+
_data = pd.DataFrame()
|
|
297
|
+
if len(_res) != 0:
|
|
298
|
+
_data = pd.DataFrame(_res[0]['_source']['data'])
|
|
299
|
+
if end_date is not None:
|
|
300
|
+
_data[time_period] = _data["session_start_date"].apply(lambda x: convert_to_date(x))
|
|
301
|
+
return _data
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import sys, os, inspect
|
|
4
|
+
import h2o
|
|
5
|
+
from h2o.estimators.kmeans import H2OKMeansEstimator
|
|
6
|
+
from h2o.grid.grid_search import H2OGridSearch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
|
10
|
+
parentdir = os.path.dirname(currentdir)
|
|
11
|
+
sys.path.insert(0, parentdir)
|
|
12
|
+
|
|
13
|
+
from customeranalytics.configs import default_es_port, default_es_host
|
|
14
|
+
from customeranalytics.utils import *
|
|
15
|
+
from customeranalytics.data_storage_configurations.query_es import QueryES
|
|
16
|
+
from customeranalytics.exploratory_analysis import query_exploratory_analysis, ea_configs
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CustomerSegmentation:
|
|
20
|
+
"""
|
|
21
|
+
Customer Segmentation is one of the crucial problems for Businesses that are mostly engaging with their buyers.
|
|
22
|
+
This relationship for some reason might differ from one customer to another one.
|
|
23
|
+
By using RFM (Recency - Frequency Monetary) each customer can be segmented by using Clustering Algorithms.
|
|
24
|
+
|
|
25
|
+
- Data Manipulation:
|
|
26
|
+
- It has been already applied on rfm.py. Collect data from reports index.
|
|
27
|
+
- Segment Each Metric individually by using KMeans with k = 5.
|
|
28
|
+
Why is k decided as 5?
|
|
29
|
+
- It is related to the Segmentation rule after assigning the segmentation process.
|
|
30
|
+
- There, Each cluster number named to a human-readable name rather than 1, 2, ..5.
|
|
31
|
+
|
|
32
|
+
- In order to optimize parameters of KMeans (except k). GRidSearch is applied.
|
|
33
|
+
- After Clustering Process has been done for R, F, and M, each cluster of average value sorted and
|
|
34
|
+
cluster labels (1, 2, 3, 4, 5) are reassigned in order. label 5 will be the highest, 1 will be the lowest.
|
|
35
|
+
- At the end, the user is the label in human-readable format;
|
|
36
|
+
"champions": {'r': [5], 'f': [5], 'm': [5]},
|
|
37
|
+
"loyal_customers": {'r': [3, 4, 5], 'f': [3, 4, 5], 'm': [5, 4, 3, 3]},
|
|
38
|
+
"potential loyalist": {'r': [5, 4, 3], 'f': [5, 4, 3], 'm': [4, 3, 2]},
|
|
39
|
+
"new customers": {'r': [5], 'f': [2, 1], 'm': [1, 2, 3, 4, 5]},
|
|
40
|
+
"promising": {'r': [3, 4], 'f': [1, 2], 'm': [1, 2]},
|
|
41
|
+
"need attention": {'r': [3, 2], 'f': [3, 2], 'm': [3, 2]},
|
|
42
|
+
"about to sleep": {'r': [3, 4], 'f': [1, 2], 'm': [1, 2]},
|
|
43
|
+
"at risk": {'r': [2], 'f': [5, 4, 3], 'm': [5, 4, 3]},
|
|
44
|
+
"can`t lose them": {'r': [1, 2], 'f': [5], 'm': [5]},
|
|
45
|
+
"hibernating": {'r': [2, 3], 'f': [2], 'm': [2]},
|
|
46
|
+
"lost": {'r': [1], 'f': [1, 2, 3, 4, 5], 'm': [1, 2, 3, 4, 5]}}
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
def __init__(self,
|
|
50
|
+
host=None,
|
|
51
|
+
port=None,
|
|
52
|
+
download_index='downloads',
|
|
53
|
+
order_index='orders'):
|
|
54
|
+
"""
|
|
55
|
+
******* ******** *****
|
|
56
|
+
Dimensional Customer Segmentation:
|
|
57
|
+
Descriptive Statistics must be created individually for dimensions.
|
|
58
|
+
For instance, the Data set contains locations dimension.
|
|
59
|
+
In this case, each location of 'orders' and 'downloads' indexes must be created individually.
|
|
60
|
+
by using 'download_index' and 'order_index' dimension can be assigned in order to create the Segmentations
|
|
61
|
+
|
|
62
|
+
download_index; downloads_location1 this will be the location dimension of
|
|
63
|
+
parameters in order to query downloads indexes; 'location1'.
|
|
64
|
+
download_index; orders_location1 this will be the location dimension of
|
|
65
|
+
parameters in order to query orders indexes; 'location1'.
|
|
66
|
+
******* ******** *****
|
|
67
|
+
!!!
|
|
68
|
+
|
|
69
|
+
:param host: elasticsearch host
|
|
70
|
+
:param port: elasticsearch port
|
|
71
|
+
:param download_index: elasticsearch port
|
|
72
|
+
:param order_index: elasticsearch port
|
|
73
|
+
"""
|
|
74
|
+
self.port = default_es_port if port is None else port
|
|
75
|
+
self.host = default_es_host if host is None else host
|
|
76
|
+
self.download_index = download_index
|
|
77
|
+
self.order_index = order_index
|
|
78
|
+
self.query_es = QueryES(port=port, host=host)
|
|
79
|
+
self.rfm = pd.DataFrame()
|
|
80
|
+
self.METRICS = ['recency', 'frequency', 'monetary']
|
|
81
|
+
self.METRIC_VALUES = {'recency': ['recency', 'rec', 'r'],
|
|
82
|
+
'frequency': ['frequency', 'freq', 'f'],
|
|
83
|
+
'monetary': ['monetary', 'mon', 'm']
|
|
84
|
+
}
|
|
85
|
+
self.Z_VALUES = [-1.96, 1.96] # 95% OF CONFIDENCE
|
|
86
|
+
self.clustering_parameters = {'split_ratio': 0.8, 'seed': 1234, 'k': 5,
|
|
87
|
+
'hyper_params': {'standardize': [True, False],
|
|
88
|
+
'init': ['Random', 'Furthest', 'PlusPlus']},
|
|
89
|
+
'search_criteria': {'strategy': "Cartesian"}
|
|
90
|
+
}
|
|
91
|
+
self.accepted_minimum_prob = 0.05
|
|
92
|
+
self.frequency_segments = {}
|
|
93
|
+
self.recency_segments = {}
|
|
94
|
+
self.monetary_segments = {}
|
|
95
|
+
self.customer_segments = {"champions": {'r': [5], 'f': [5], 'm': [5]},
|
|
96
|
+
"loyal customers": {'r': [4, 5], 'f': [4, 5], 'm': [5, 4, 3, 2, 1]},
|
|
97
|
+
"potential loyalist": {'r': [5, 4, 3], 'f': [5, 4, 3], 'm': [4, 3, 2]},
|
|
98
|
+
"new customers": {'r': [5], 'f': [2, 1], 'm': [1, 2, 3, 4, 5]},
|
|
99
|
+
"promising": {'r': [3, 4], 'f': [1, 2], 'm': [1, 2]},
|
|
100
|
+
"need attention": {'r': [3, 2], 'f': [3, 2], 'm': [3, 2]},
|
|
101
|
+
"about to sleep": {'r': [3, 4], 'f': [1, 2], 'm': [1, 2]},
|
|
102
|
+
"at risk": {'r': [2], 'f': [5, 4, 3], 'm': [5, 4, 3]},
|
|
103
|
+
"can`t lose them": {'r': [1, 2], 'f': [5], 'm': [5]},
|
|
104
|
+
"hibernating": {'r': [2, 3], 'f': [2], 'm': [2]},
|
|
105
|
+
"lost": {'r': [1], 'f': [1, 2, 3, 4, 5], 'm': [1, 2, 3, 4, 5]}}
|
|
106
|
+
self.segments_numerics = {}
|
|
107
|
+
self.detected_segments = []
|
|
108
|
+
self.insert_columns = ["client", "recency_segment", "frequency_segment",
|
|
109
|
+
"monetary_segment", "segments", "segments_numeric"]
|
|
110
|
+
|
|
111
|
+
def get_rfm(self, date):
|
|
112
|
+
"""
|
|
113
|
+
RFM values for segmentation can be fetched from the reports index with related dimensions.
|
|
114
|
+
"""
|
|
115
|
+
ea_configs['rfm']['order_index'] = self.order_index
|
|
116
|
+
_total_cols, _counter = 0, 0
|
|
117
|
+
while len(set(self.rfm.columns) & set(self.METRICS)) != 3:
|
|
118
|
+
self.rfm = query_exploratory_analysis(ea_configs, {"start_date": date}, "rfm")
|
|
119
|
+
if _counter >= 10:
|
|
120
|
+
_total_cols = 3
|
|
121
|
+
else:
|
|
122
|
+
_total_cols = len(set(self.rfm.columns) & set(self.METRICS))
|
|
123
|
+
_counter += 1
|
|
124
|
+
|
|
125
|
+
def segmentation(self, data, metric):
|
|
126
|
+
"""
|
|
127
|
+
Each RFM segmentation will be k = 1,.. , 5
|
|
128
|
+
Step of Clustering Process;
|
|
129
|
+
1. Convert rfm data-frame into the H2O data-frame
|
|
130
|
+
2. Split data for train and validation.
|
|
131
|
+
3. Create KMeans process.
|
|
132
|
+
4. Create GridSearch for KMeans parameters. These are;
|
|
133
|
+
a. standardize; standardize the numeric columns to have a mean of zero and unit variance. Standardization is highly recommended;
|
|
134
|
+
b. split ratio; train validation split ratio, the lower ratio can decrease Underfitting Problem
|
|
135
|
+
c. init; 'Random', 'Furthest', 'PlusPlus'
|
|
136
|
+
d. search criteria - strategy;
|
|
137
|
+
These segments are later on CustomerSegmentation will be sorted according to theirs.
|
|
138
|
+
:param data: rfm data-frame with columns recency or monetary or frequency
|
|
139
|
+
:param metric: recency or monetary or frequency
|
|
140
|
+
:return:
|
|
141
|
+
"""
|
|
142
|
+
h2o.init() # initialize the H2O environment
|
|
143
|
+
rfm_data = h2o.H2OFrame(data) # convert pandas data-frame to H2O data-frame
|
|
144
|
+
# split train and test data
|
|
145
|
+
train, valid = rfm_data.split_frame(ratios=[self.clustering_parameters['split_ratio']],
|
|
146
|
+
seed=self.clustering_parameters['seed'])
|
|
147
|
+
# build KMeans algorithm
|
|
148
|
+
rfm_kmeans = H2OKMeansEstimator(k=self.clustering_parameters['k'],
|
|
149
|
+
seed=self.clustering_parameters['seed'],
|
|
150
|
+
max_iterations=int(len(data) / 2))
|
|
151
|
+
rfm_kmeans.train(x=metric, training_frame=train, validation_frame=valid)
|
|
152
|
+
# grid search for KMeans parameters
|
|
153
|
+
grid = H2OGridSearch(model=rfm_kmeans, hyper_params=self.clustering_parameters['hyper_params'],
|
|
154
|
+
search_criteria=self.clustering_parameters['search_criteria'])
|
|
155
|
+
# train using the grid
|
|
156
|
+
grid.train(x=metric, training_frame=train, validation_frame=valid)
|
|
157
|
+
# sort the grid models by total within cluster sum-of-square error.
|
|
158
|
+
sorted_grid = grid.get_grid(sort_by='tot_withinss', decreasing=False)
|
|
159
|
+
prediction = sorted_grid[0].predict(rfm_data)
|
|
160
|
+
data = rfm_data.concat(prediction, axis=1)[[metric, 'predict']].as_data_frame(use_pandas=True)
|
|
161
|
+
data = data.rename(columns={'predict': metric + '_segment'})
|
|
162
|
+
# assign segments for each metric (Recency - Monetary - Frequency)
|
|
163
|
+
data[metric + '_segment'] = data[metric + '_segment'].apply(lambda x: x + 1)
|
|
164
|
+
return data
|
|
165
|
+
|
|
166
|
+
def current_day_r_f_m_clustering(self):
|
|
167
|
+
"""
|
|
168
|
+
combine segmented values to the recent rfm data frame. After concatenation;
|
|
169
|
+
|
|
170
|
+
client frequency recency monetary recency_segment frequency_segment monetary_segment
|
|
171
|
+
6115 u_174194 0.0 1663 32.8500 2 2 3
|
|
172
|
+
10060 u_220840 1.0 342 22.4700 4 2 4
|
|
173
|
+
"""
|
|
174
|
+
for metric in self.METRICS:
|
|
175
|
+
self.rfm = pd.concat([self.rfm, self.segmentation(self.rfm[[metric]], metric).drop(metric, axis=1)], axis=1)
|
|
176
|
+
|
|
177
|
+
def get_frequency_segments(self):
|
|
178
|
+
"""
|
|
179
|
+
Each segmented label is assigned as unsorted format.
|
|
180
|
+
Here finding the cluster centroid for frequency clusters (it is calculated via average at this process).
|
|
181
|
+
e.g. centroids - labels pairs 7.2 - 5, 4.2 - 2, 30.2 - 4, 1.3 - 3, 44 - 1,
|
|
182
|
+
sorted_labels;
|
|
183
|
+
previous_labels sorted_labels centroids
|
|
184
|
+
3 1 1.3
|
|
185
|
+
2 2 4.2
|
|
186
|
+
5 3 7.2
|
|
187
|
+
4 4 30.2
|
|
188
|
+
1 5 44
|
|
189
|
+
"""
|
|
190
|
+
for i in self.rfm.groupby("frequency_segment").agg(
|
|
191
|
+
{"frequency": "mean"}).reset_index().sort_values(
|
|
192
|
+
by='frequency', ascending=True).reset_index(drop=True).reset_index().to_dict('records'):
|
|
193
|
+
self.frequency_segments[i['frequency_segment']] = 5 - i['index']
|
|
194
|
+
|
|
195
|
+
def get_recency_segments(self):
|
|
196
|
+
"""
|
|
197
|
+
Each segmented label is assigned as unsorted format.
|
|
198
|
+
Here finding the cluster centroid for recency clusters (it is calculated via average at this process).
|
|
199
|
+
e.g. centroids - labels pairs 7.2 - 5, 4.2 - 2, 30.2 - 4, 1.3 - 3, 44 - 1,
|
|
200
|
+
sorted_labels;
|
|
201
|
+
previous_labels sorted_labels centroids
|
|
202
|
+
3 1 1.3
|
|
203
|
+
2 2 4.2
|
|
204
|
+
5 3 7.2
|
|
205
|
+
4 4 30.2
|
|
206
|
+
1 5 44
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
for i in self.rfm.groupby("recency_segment").agg(
|
|
210
|
+
{"recency": "mean"}).reset_index().sort_values(
|
|
211
|
+
by='recency', ascending=True).reset_index(drop=True).reset_index().to_dict('records'):
|
|
212
|
+
self.recency_segments[i['recency_segment']] = 5 - i['index']
|
|
213
|
+
|
|
214
|
+
def get_segments_numeric(self):
|
|
215
|
+
"""
|
|
216
|
+
format; {"champions": 0, ..... , "can`t lose them": 8}
|
|
217
|
+
"""
|
|
218
|
+
self.segments_numerics = {s[0]: s[1] for s in zip(list(self.customer_segments.keys()) + ['others'],
|
|
219
|
+
list(range(len(list(self.customer_segments.keys()))+1)))}
|
|
220
|
+
|
|
221
|
+
def get_monetary_segments(self):
|
|
222
|
+
"""
|
|
223
|
+
Each segmented label is assigned as unsorted format.
|
|
224
|
+
Here finding the cluster centroid for monetary clusters (it is calculated via average at this process).
|
|
225
|
+
e.g. centroids - labels pairs 7.2 - 5, 4.2 - 2, 30.2 - 4, 1.3 - 3, 44 - 1,
|
|
226
|
+
sorted_labels;
|
|
227
|
+
previous_labels sorted_labels centroids
|
|
228
|
+
3 1 1.3
|
|
229
|
+
2 2 4.2
|
|
230
|
+
5 3 7.2
|
|
231
|
+
4 4 30.2
|
|
232
|
+
1 5 44
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
for i in self.rfm.groupby("monetary_segment").agg(
|
|
236
|
+
{"monetary": "mean"}).reset_index().sort_values(
|
|
237
|
+
by='monetary', ascending=True).reset_index(drop=True).reset_index().to_dict('records'):
|
|
238
|
+
self.monetary_segments[i['monetary_segment']] = 5 - i['index']
|
|
239
|
+
|
|
240
|
+
def combine_segments(self):
|
|
241
|
+
"""
|
|
242
|
+
apply sorted segments
|
|
243
|
+
"""
|
|
244
|
+
self.rfm['frequency_segment'] = self.rfm['frequency_segment'].apply(lambda x: self.frequency_segments[x])
|
|
245
|
+
self.rfm['recency_segment'] = self.rfm['recency_segment'].apply(lambda x: self.recency_segments[x])
|
|
246
|
+
self.rfm['monetary_segment'] = self.rfm['monetary_segment'].apply(lambda x: self.monetary_segments[x])
|
|
247
|
+
|
|
248
|
+
def detect_customer_segments(self):
|
|
249
|
+
"""
|
|
250
|
+
Human Readable Form Of Segments;
|
|
251
|
+
After the segmentation process, There will be 5 * 5 * 5 = 125 individual segments will be created.
|
|
252
|
+
e.g. 1 - 3 - 5 (r - f - m)
|
|
253
|
+
Now, another topic is how segmentation outputs can be converted to clearly understandable segments.
|
|
254
|
+
By searching on articles about RFM segmentation, one the usual segments are;
|
|
255
|
+
- champions
|
|
256
|
+
- loyal_customers
|
|
257
|
+
- potential loyalist
|
|
258
|
+
- new customers
|
|
259
|
+
- promising
|
|
260
|
+
- need attention
|
|
261
|
+
- about to sleep
|
|
262
|
+
- at risk
|
|
263
|
+
- can`t lose them
|
|
264
|
+
- hibernating
|
|
265
|
+
- lost
|
|
266
|
+
Each segment of label ranges are assigned to the variable 'self.customer_segments'.
|
|
267
|
+
"""
|
|
268
|
+
total_clients = list(self.rfm['client'].unique())
|
|
269
|
+
for s in self.customer_segments:
|
|
270
|
+
_segment_clients = list(self.rfm[(self.rfm['recency_segment'].isin(self.customer_segments[s]['r'])) &
|
|
271
|
+
(self.rfm['frequency_segment'].isin(self.customer_segments[s]['f'])) &
|
|
272
|
+
(self.rfm['monetary_segment'].isin(self.customer_segments[s]['m'])) & (
|
|
273
|
+
self.rfm['client'].isin(total_clients))]['client'])
|
|
274
|
+
self.detected_segments += list(zip(_segment_clients, (len(_segment_clients) * [s])))
|
|
275
|
+
total_clients = list(set(total_clients) - set(_segment_clients))
|
|
276
|
+
|
|
277
|
+
def insert_into_reports_index(self, segments, date=None, index='orders'):
|
|
278
|
+
"""
|
|
279
|
+
via query_es.py, each report can be inserted into the reports index with the given format.
|
|
280
|
+
{"id": unique report id,
|
|
281
|
+
"report_date": date or current date,
|
|
282
|
+
"report_name": "segmentation",
|
|
283
|
+
"index": "main",
|
|
284
|
+
"report_types": {},
|
|
285
|
+
"data": segments.fillna(0.0).to_dict("records") - dataframe to list of dictionary
|
|
286
|
+
}
|
|
287
|
+
!!! null values are assigned to 0.
|
|
288
|
+
|
|
289
|
+
:param segments: data set, data frame
|
|
290
|
+
:param index: dimensionality of data index orders_location1 ; dimension = location1
|
|
291
|
+
"""
|
|
292
|
+
list_of_obj = [{"id": np.random.randint(200000000),
|
|
293
|
+
"report_date": current_date_to_day().isoformat() if date is None else date,
|
|
294
|
+
"report_name": "segmentation",
|
|
295
|
+
"index": get_index_group(index),
|
|
296
|
+
"report_types": {},
|
|
297
|
+
"data": segments.fillna(0.0).to_dict("records")}]
|
|
298
|
+
self.query_es.insert_data_to_index(list_of_obj, index='reports')
|
|
299
|
+
|
|
300
|
+
def execute_customer_segment(self, start_date=None):
|
|
301
|
+
"""
|
|
302
|
+
Process of the Customer Segmentation
|
|
303
|
+
1. fetch rfm calculated values from reports index.
|
|
304
|
+
2. create KMeans Segmentation.
|
|
305
|
+
3. sort labels related to centroids for R, F, and M individually.
|
|
306
|
+
4. combine sorted labels.
|
|
307
|
+
6. assign human-readable segments instead of 1, 2, ..5 labels.
|
|
308
|
+
7. insert human-readable segments and number labels of each segment into the reports index.
|
|
309
|
+
reports index must be from right dimensional, related orders_index.
|
|
310
|
+
|
|
311
|
+
:param start_date: date of reporting date
|
|
312
|
+
:return:
|
|
313
|
+
"""
|
|
314
|
+
self.get_rfm(date=start_date)
|
|
315
|
+
self.current_day_r_f_m_clustering()
|
|
316
|
+
self.get_frequency_segments()
|
|
317
|
+
self.get_recency_segments()
|
|
318
|
+
self.get_monetary_segments()
|
|
319
|
+
self.combine_segments()
|
|
320
|
+
self.detect_customer_segments()
|
|
321
|
+
self.get_segments_numeric()
|
|
322
|
+
|
|
323
|
+
self.rfm = pd.merge(self.rfm,
|
|
324
|
+
pd.DataFrame(self.detected_segments).rename(columns={0: "client", 1: "segments"}),
|
|
325
|
+
on='client', how='left')
|
|
326
|
+
self.rfm['segments'] = self.rfm['segments'].fillna('others')
|
|
327
|
+
self.rfm['segments_numeric'] = self.rfm['segments'].apply(lambda x: self.segments_numerics[x])
|
|
328
|
+
|
|
329
|
+
self.insert_into_reports_index(self.rfm[self.insert_columns], date=start_date, index=self.order_index)
|
|
330
|
+
h2o.shutdown(prompt=False)
|
|
331
|
+
|
|
332
|
+
def fetch(self, start_date=None):
|
|
333
|
+
"""
|
|
334
|
+
Collect Customer segmentation results with the given date.
|
|
335
|
+
:param start_date: customer_segmentation first date
|
|
336
|
+
:return:
|
|
337
|
+
"""
|
|
338
|
+
boolean_queries, date_queries = [], []
|
|
339
|
+
boolean_queries = [{"term": {"report_name": "segmentation"}},
|
|
340
|
+
{"term": {"index": get_index_group(self.order_index)}}]
|
|
341
|
+
|
|
342
|
+
if start_date is not None:
|
|
343
|
+
date_queries = [{"range": {"report_date": {"gte": convert_to_iso_format(start_date)}}}]
|
|
344
|
+
|
|
345
|
+
self.query_es = QueryES(port=self.port,
|
|
346
|
+
host=self.host)
|
|
347
|
+
self.query_es.query_builder(fields=None, _source=True,
|
|
348
|
+
date_queries=date_queries,
|
|
349
|
+
boolean_queries=boolean_queries)
|
|
350
|
+
_res = self.query_es.get_data_from_es(index="reports")
|
|
351
|
+
_data = pd.DataFrame()
|
|
352
|
+
if len(_res) != 0:
|
|
353
|
+
_data = pd.DataFrame(_res[-1]['_source']['data'])
|
|
354
|
+
return _data
|